新增web界面管理策略
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,5 +5,6 @@ data/data/
|
|||||||
|
|
||||||
logs/
|
logs/
|
||||||
pids/
|
pids/
|
||||||
status/
|
states/
|
||||||
status.json
|
status.json
|
||||||
|
/strategy_manager/states/
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2025-11-19T07:13:51.818890Z",
|
"end_time": "2025-11-21T01:54:34.434731Z",
|
||||||
"start_time": "2025-11-19T07:13:50.827090Z"
|
"start_time": "2025-11-21T01:54:31.509589Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"source": [
|
"source": [
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2025-11-19T07:13:51.840183Z",
|
"end_time": "2025-11-21T01:54:34.537950Z",
|
||||||
"start_time": "2025-11-19T07:13:51.822905Z"
|
"start_time": "2025-11-21T01:54:34.445969Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2025-11-19T07:14:58.236155Z",
|
"end_time": "2025-11-21T01:55:38.643534Z",
|
||||||
"start_time": "2025-11-19T07:13:51.846812Z"
|
"start_time": "2025-11-21T01:54:34.551413Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"初始化回测引擎...\n",
|
"初始化回测引擎...\n",
|
||||||
"模拟器初始化:初始资金=100000.00, 滑点率=0.0, 佣金率=0.0001\n",
|
"模拟器初始化:初始资金=100000.00, 滑点率=0.0, 佣金率=0.0001\n",
|
||||||
"内存仓储已初始化,管理ID: 'futures_trading_strategies.SA.Spectral.SpectralTrendStrategy.SpectralTrendStrategy_228362dc13ea83accde90203f5f5434e'\n",
|
"内存仓储已初始化,管理ID: 'futures_trading_strategies.SA.Spectral.SpectralTrendStrategy.SpectralTrendStrategy_f774488b98ac53758169305e51fd8595'\n",
|
||||||
"\n",
|
"\n",
|
||||||
"--- 回测引擎初始化完成 ---\n",
|
"--- 回测引擎初始化完成 ---\n",
|
||||||
" 策略: SpectralTrendStrategy\n",
|
" 策略: SpectralTrendStrategy\n",
|
||||||
@@ -196,8 +196,8 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2025-11-19T07:15:05.968736Z",
|
"end_time": "2025-11-21T01:55:46.179638Z",
|
||||||
"start_time": "2025-11-19T07:14:58.277441Z"
|
"start_time": "2025-11-21T01:55:38.675521Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
|
|||||||
@@ -123,10 +123,11 @@ class SpectralTrendStrategy(Strategy):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 核心逻辑:相变入场/退出
|
# 核心逻辑:相变入场/退出
|
||||||
if position_volume == 0:
|
if self.trading:
|
||||||
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq)
|
if position_volume == 0:
|
||||||
else:
|
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq)
|
||||||
self.manage_open_position(position_volume, trend_strength, dominant_freq)
|
else:
|
||||||
|
self.manage_open_position(position_volume, trend_strength, dominant_freq)
|
||||||
|
|
||||||
def calculate_trend_strength(self, prices: np.array) -> (float, float):
|
def calculate_trend_strength(self, prices: np.array) -> (float, float):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import psutil
|
import psutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import importlib.util
|
||||||
|
import json # 确保导入json模块
|
||||||
|
|
||||||
# ==================== 动态路径配置 ====================
|
# ==================== 动态路径配置 ====================
|
||||||
from core.path_utils import add_project_root_to_path
|
from core.path_utils import add_project_root_to_path
|
||||||
@@ -29,12 +31,32 @@ class StrategyManager:
|
|||||||
self.logs_dir.mkdir(exist_ok=True)
|
self.logs_dir.mkdir(exist_ok=True)
|
||||||
self.pid_dir.mkdir(exist_ok=True)
|
self.pid_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 配置管理器日志
|
||||||
|
self._setup_logger()
|
||||||
|
|
||||||
self.strategies: Dict[str, Dict[str, Any]] = {}
|
self.strategies: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self.logger.info("🔄 正在加载策略配置...")
|
||||||
self.load_strategies()
|
self.load_strategies()
|
||||||
|
self.logger.info("✅ 策略加载完成,共发现 %d 个策略", len(self.strategies))
|
||||||
|
|
||||||
|
def _setup_logger(self):
|
||||||
|
"""配置管理器日志"""
|
||||||
|
log_file = self.logs_dir / "manager.log"
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(log_file, encoding='utf-8'),
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
],
|
||||||
|
force=True
|
||||||
|
)
|
||||||
|
self.logger = logging.getLogger("StrategyManager")
|
||||||
|
|
||||||
def _load_main_config(self, config_path: str) -> Dict[str, Any]:
|
def _load_main_config(self, config_path: str) -> Dict[str, Any]:
|
||||||
path = Path(config_path)
|
path = Path(config_path)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
self.logger.warning("⚠️ 主配置文件不存在,使用默认配置")
|
||||||
return {
|
return {
|
||||||
"logs_dir": "logs",
|
"logs_dir": "logs",
|
||||||
"status_file": "status.json",
|
"status_file": "status.json",
|
||||||
@@ -44,16 +66,23 @@ class StrategyManager:
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
def load_strategies(self):
|
def load_strategies(self):
|
||||||
"""递归扫描 strategies/ 目录,查找 .config 文件"""
|
"""递归扫描 strategies/ 目录,查找 .py 配置文件"""
|
||||||
self.strategies = {}
|
self.strategies = {}
|
||||||
if not self.strategies_dir.exists():
|
if not self.strategies_dir.exists():
|
||||||
print("[ERROR] 策略配置目录不存在: {}".format(self.strategies_dir))
|
self.logger.error("❌ 策略配置目录不存在: %s", self.strategies_dir)
|
||||||
return
|
return
|
||||||
|
|
||||||
for config_file in self.strategies_dir.rglob("*.config"):
|
for config_file in self.strategies_dir.rglob("*.py"):
|
||||||
try:
|
try:
|
||||||
with open(config_file, 'r', encoding='utf-8') as f:
|
spec = importlib.util.spec_from_file_location(
|
||||||
config = json.load(f)
|
f"config_{config_file.stem}", config_file
|
||||||
|
)
|
||||||
|
config_module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(config_module)
|
||||||
|
|
||||||
|
if not hasattr(config_module, 'CONFIG'):
|
||||||
|
raise ValueError("配置文件缺少 CONFIG 变量")
|
||||||
|
config = config_module.CONFIG
|
||||||
|
|
||||||
required = ['name', 'strategy_class', 'enabled', 'engine_params', 'strategy_params']
|
required = ['name', 'strategy_class', 'enabled', 'engine_params', 'strategy_params']
|
||||||
for field in required:
|
for field in required:
|
||||||
@@ -75,8 +104,9 @@ class StrategyManager:
|
|||||||
"started_at": None,
|
"started_at": None,
|
||||||
"uptime": None
|
"uptime": None
|
||||||
}
|
}
|
||||||
|
self.logger.info("📄 加载配置: %s", config_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[ERROR] 加载配置失败 {}: {}".format(config_file, e))
|
self.logger.error("❌ 加载配置失败 %s: %s", config_file, e, exc_info=True)
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, Any]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""获取完整状态"""
|
"""获取完整状态"""
|
||||||
@@ -89,12 +119,7 @@ class StrategyManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _refresh_status(self):
|
def _refresh_status(self):
|
||||||
"""
|
"""刷新进程状态 - 双重验证"""
|
||||||
刷新进程状态 - 双重验证
|
|
||||||
1. 检查PID文件是否存在
|
|
||||||
2. 检查进程是否存在
|
|
||||||
3. 验证进程名是否为python(防止PID复用)
|
|
||||||
"""
|
|
||||||
for name, info in self.strategies.items():
|
for name, info in self.strategies.items():
|
||||||
pid_file = self.pid_dir / "{}.pid".format(name)
|
pid_file = self.pid_dir / "{}.pid".format(name)
|
||||||
|
|
||||||
@@ -103,28 +128,23 @@ class StrategyManager:
|
|||||||
with open(pid_file, 'r') as f:
|
with open(pid_file, 'r') as f:
|
||||||
pid = int(f.read().strip())
|
pid = int(f.read().strip())
|
||||||
|
|
||||||
# 双重验证
|
|
||||||
if psutil.pid_exists(pid):
|
if psutil.pid_exists(pid):
|
||||||
try:
|
try:
|
||||||
proc = psutil.Process(pid)
|
proc = psutil.Process(pid)
|
||||||
# 验证进程名是否包含python
|
|
||||||
if "python" in proc.name().lower():
|
if "python" in proc.name().lower():
|
||||||
# 验证成功,更新为运行中
|
|
||||||
info["status"] = "running"
|
info["status"] = "running"
|
||||||
info["pid"] = pid
|
info["pid"] = pid
|
||||||
if info["started_at"]:
|
if info["started_at"]:
|
||||||
started = datetime.fromisoformat(info["started_at"])
|
started = datetime.fromisoformat(info["started_at"])
|
||||||
uptime = datetime.now() - started
|
uptime = datetime.now() - started
|
||||||
info["uptime"] = str(uptime).split('.')[0]
|
info["uptime"] = str(uptime).split('.')[0]
|
||||||
continue # 跳过清理逻辑
|
continue
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
# 进程已死或无权访问,继续清理
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# PID不存在或验证失败,清理
|
|
||||||
self._cleanup_stopped_strategy(name, pid_file)
|
self._cleanup_stopped_strategy(name, pid_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[WARNING] 刷新状态失败 {}: {}".format(name, e))
|
self.logger.warning("⚠️ 刷新状态失败 %s: %s", name, e)
|
||||||
self._cleanup_stopped_strategy(name, pid_file)
|
self._cleanup_stopped_strategy(name, pid_file)
|
||||||
else:
|
else:
|
||||||
info["status"] = "stopped"
|
info["status"] = "stopped"
|
||||||
@@ -133,13 +153,8 @@ class StrategyManager:
|
|||||||
info["uptime"] = None
|
info["uptime"] = None
|
||||||
|
|
||||||
def _is_running(self, name: str) -> bool:
|
def _is_running(self, name: str) -> bool:
|
||||||
"""
|
"""检查策略是否运行中"""
|
||||||
检查策略是否运行中 - 实时刷新状态
|
|
||||||
确保与status命令结果一致
|
|
||||||
"""
|
|
||||||
# 先刷新状态确保最新
|
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
|
|
||||||
info = self.strategies[name]
|
info = self.strategies[name]
|
||||||
if not info["pid"]:
|
if not info["pid"]:
|
||||||
return False
|
return False
|
||||||
@@ -152,7 +167,6 @@ class StrategyManager:
|
|||||||
with open(pid_file, 'r') as f:
|
with open(pid_file, 'r') as f:
|
||||||
pid = int(f.read().strip())
|
pid = int(f.read().strip())
|
||||||
|
|
||||||
# 双重验证
|
|
||||||
if psutil.pid_exists(pid):
|
if psutil.pid_exists(pid):
|
||||||
try:
|
try:
|
||||||
proc = psutil.Process(pid)
|
proc = psutil.Process(pid)
|
||||||
@@ -163,15 +177,74 @@ class StrategyManager:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def start_strategy(self, name: str) -> bool:
|
||||||
|
"""启动单个策略(守护进程模式)"""
|
||||||
|
if name not in self.strategies:
|
||||||
|
self.logger.error("❌ 策略不存在: %s", name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._is_running(name):
|
||||||
|
self.logger.warning("⚠️ 策略已在运行: %s", name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
info = self.strategies[name]
|
||||||
|
config_file = Path(info["config_file"])
|
||||||
|
|
||||||
|
# 创建策略专属日志目录
|
||||||
|
strategy_log_dir = self.logs_dir / info["strategy_name"]
|
||||||
|
strategy_log_dir.mkdir(exist_ok=True, parents=True)
|
||||||
|
log_file = strategy_log_dir / "{}.log".format(info["symbol"])
|
||||||
|
|
||||||
|
self.logger.info("\n" + "=" * 80)
|
||||||
|
self.logger.info("🚀 启动策略: %s", name)
|
||||||
|
self.logger.info("📄 配置文件: %s", config_file)
|
||||||
|
self.logger.info("📝 日志文件: %s", log_file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动子进程 - 关键修改:脱离终端会话
|
||||||
|
with open(log_file, 'a') as f:
|
||||||
|
# 使用 start_new_session=True 创建新会话,防止终端关闭影响
|
||||||
|
# stdin 重定向到 /dev/null,完全脱离终端
|
||||||
|
with open(os.devnull, 'r') as devnull:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[sys.executable, "launcher.py", "--config", str(config_file)],
|
||||||
|
stdout=f,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
stdin=devnull, # 关键:从/dev/null读取输入
|
||||||
|
# start_new_session=True, # 关键:创建新会话,脱离终端
|
||||||
|
cwd=Path.cwd()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
info["pid"] = process.pid
|
||||||
|
info["status"] = "running"
|
||||||
|
info["started_at"] = datetime.now().isoformat()
|
||||||
|
info["uptime"] = "00:00:00"
|
||||||
|
|
||||||
|
# 保存PID文件
|
||||||
|
pid_file = self.pid_dir / "{}.pid".format(name)
|
||||||
|
with open(pid_file, 'w') as f:
|
||||||
|
f.write(str(process.pid))
|
||||||
|
|
||||||
|
self._save_status()
|
||||||
|
self.logger.info("✅ 启动成功! PID: %d", process.pid)
|
||||||
|
self.logger.info("ℹ️ 该进程已脱离终端会话,关闭窗口不会停止策略")
|
||||||
|
self.logger.info("=" * 80)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("❌ 启动失败: %s", e, exc_info=True)
|
||||||
|
self._cleanup_stopped_strategy(name, self.pid_dir / "{}.pid".format(name))
|
||||||
|
return False
|
||||||
|
|
||||||
def stop_strategy(self, name: str, timeout: int = 30) -> bool:
|
def stop_strategy(self, name: str, timeout: int = 30) -> bool:
|
||||||
"""停止单个策略"""
|
"""停止单个策略"""
|
||||||
if name not in self.strategies:
|
if name not in self.strategies:
|
||||||
print("[ERROR] 策略不存在: {}".format(name))
|
self.logger.error("❌ 策略不存在: %s", name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 再次检查状态(确保最新)
|
|
||||||
if not self._is_running(name):
|
if not self._is_running(name):
|
||||||
print("[WARNING] 策略未运行: {}".format(name))
|
self.logger.warning("⚠️ 策略未运行: %s", name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
info = self.strategies[name]
|
info = self.strategies[name]
|
||||||
@@ -180,40 +253,43 @@ class StrategyManager:
|
|||||||
pid = info["pid"]
|
pid = info["pid"]
|
||||||
process = psutil.Process(pid)
|
process = psutil.Process(pid)
|
||||||
|
|
||||||
print("\n[INFO] 正在停止: {} (PID: {})...".format(name, pid))
|
self.logger.info("\n" + "=" * 80)
|
||||||
|
self.logger.info("⏹️ 正在停止策略: %s (PID: %d)", name, pid)
|
||||||
|
|
||||||
# 优雅终止
|
# 优雅终止
|
||||||
process.terminate()
|
process.terminate()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process.wait(timeout=timeout)
|
process.wait(timeout=timeout)
|
||||||
print("[SUCCESS] 已停止: {}".format(name))
|
self.logger.info("✅ 已优雅停止: %s", name)
|
||||||
except psutil.TimeoutExpired:
|
except psutil.TimeoutExpired:
|
||||||
print("[WARNING] 超时,强制终止: {}".format(name))
|
self.logger.warning("⏱️ 超时,强制终止: %s", name)
|
||||||
process.kill()
|
process.kill()
|
||||||
process.wait()
|
process.wait()
|
||||||
|
|
||||||
# 清理状态
|
# 清理状态
|
||||||
self._cleanup_stopped_strategy(name, self.pid_dir / "{}.pid".format(name))
|
self._cleanup_stopped_strategy(name, self.pid_dir / "{}.pid".format(name))
|
||||||
self._save_status()
|
self._save_status()
|
||||||
|
self.logger.info("=" * 80)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[ERROR] 停止失败 {}: {}".format(name, e))
|
self.logger.error("❌ 停止失败 %s: %s", name, e, exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restart_strategy(self, name: str) -> bool:
|
def restart_strategy(self, name: str) -> bool:
|
||||||
"""重启策略"""
|
"""重启策略"""
|
||||||
print("\n[INFO] 正在重启: {}".format(name))
|
self.logger.info("\n" + "=" * 80)
|
||||||
|
self.logger.info("🔄 正在重启: %s", name)
|
||||||
self.stop_strategy(name)
|
self.stop_strategy(name)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
return self.start_strategy(name)
|
return self.start_strategy(name)
|
||||||
|
|
||||||
def start_all(self):
|
def start_all(self):
|
||||||
"""启动所有启用的策略"""
|
"""启动所有启用的策略"""
|
||||||
print("\n" + "=" * 100)
|
self.logger.info("\n" + "=" * 100)
|
||||||
print("正在启动所有启用的策略...")
|
self.logger.info("🚀 正在启动所有启用的策略...")
|
||||||
print("=" * 100)
|
self.logger.info("=" * 100)
|
||||||
|
|
||||||
started = []
|
started = []
|
||||||
for name, info in self.strategies.items():
|
for name, info in self.strategies.items():
|
||||||
@@ -221,15 +297,15 @@ class StrategyManager:
|
|||||||
if self.start_strategy(name):
|
if self.start_strategy(name):
|
||||||
started.append(name)
|
started.append(name)
|
||||||
|
|
||||||
print("\n[SUCCESS] 成功启动 {} 个策略".format(len(started)))
|
self.logger.info("\n✅ 成功启动 %d 个策略", len(started))
|
||||||
if started:
|
if started:
|
||||||
print("策略: {}".format(", ".join(started)))
|
self.logger.info("📋 策略: %s", ", ".join(started))
|
||||||
|
|
||||||
def stop_all(self):
|
def stop_all(self):
|
||||||
"""停止所有运行的策略"""
|
"""停止所有运行的策略"""
|
||||||
print("\n" + "=" * 100)
|
self.logger.info("\n" + "=" * 100)
|
||||||
print("正在停止所有运行的策略...")
|
self.logger.info("⏹️ 正在停止所有运行的策略...")
|
||||||
print("=" * 100)
|
self.logger.info("=" * 100)
|
||||||
|
|
||||||
stopped = []
|
stopped = []
|
||||||
for name in self.strategies.keys():
|
for name in self.strategies.keys():
|
||||||
@@ -237,9 +313,9 @@ class StrategyManager:
|
|||||||
if self.stop_strategy(name):
|
if self.stop_strategy(name):
|
||||||
stopped.append(name)
|
stopped.append(name)
|
||||||
|
|
||||||
print("\n[SUCCESS] 成功停止 {} 个策略".format(len(stopped)))
|
self.logger.info("\n✅ 成功停止 %d 个策略", len(stopped))
|
||||||
if stopped:
|
if stopped:
|
||||||
print("策略: {}".format(", ".join(stopped)))
|
self.logger.info("📋 策略: %s", ", ".join(stopped))
|
||||||
|
|
||||||
def _cleanup_stopped_strategy(self, name: str, pid_file: Path):
|
def _cleanup_stopped_strategy(self, name: str, pid_file: Path):
|
||||||
"""清理已停止的策略状态"""
|
"""清理已停止的策略状态"""
|
||||||
@@ -251,10 +327,25 @@ class StrategyManager:
|
|||||||
info["uptime"] = None
|
info["uptime"] = None
|
||||||
|
|
||||||
def _save_status(self):
|
def _save_status(self):
|
||||||
"""状态持久化"""
|
"""状态持久化(修复:排除不可序列化的config字段)"""
|
||||||
status = self.get_status()
|
try:
|
||||||
with open(self.status_file, 'w') as f:
|
status = self.get_status()
|
||||||
json.dump(status, f, indent=2, ensure_ascii=False)
|
|
||||||
|
# 创建JSON安全的版本(排除config字段,因为它可能包含timedelta等不可序列化对象)
|
||||||
|
status_for_json = status.copy()
|
||||||
|
status_for_json["strategies"] = {}
|
||||||
|
|
||||||
|
for name, info in status["strategies"].items():
|
||||||
|
# 复制所有字段除了config
|
||||||
|
strategy_info = {k: v for k, v in info.items() if k != "config"}
|
||||||
|
status_for_json["strategies"][name] = strategy_info
|
||||||
|
|
||||||
|
with open(self.status_file, 'w') as f:
|
||||||
|
json.dump(status_for_json, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
self.logger.debug("💾 状态已保存到 %s", self.status_file)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("❌ 保存状态失败: %s", e, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def print_status_table(status: Dict[str, Any]):
|
def print_status_table(status: Dict[str, Any]):
|
||||||
@@ -284,4 +375,31 @@ def print_status_table(status: Dict[str, Any]):
|
|||||||
name, info['config']['name'], status_text, pid_text, uptime_text, started_text
|
name, info['config']['name'], status_text, pid_text, uptime_text, started_text
|
||||||
))
|
))
|
||||||
|
|
||||||
print("=" * 130)
|
print("=" * 130)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
manager = StrategyManager()
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
command = sys.argv[1]
|
||||||
|
if command == "status":
|
||||||
|
print_status_table(manager.get_status())
|
||||||
|
elif command == "start-all":
|
||||||
|
manager.start_all()
|
||||||
|
elif command == "stop-all":
|
||||||
|
manager.stop_all()
|
||||||
|
elif command.startswith("start:"):
|
||||||
|
name = command.split(":", 1)[1]
|
||||||
|
manager.start_strategy(name)
|
||||||
|
elif command.startswith("stop:"):
|
||||||
|
name = command.split(":", 1)[1]
|
||||||
|
manager.stop_strategy(name)
|
||||||
|
elif command.startswith("restart:"):
|
||||||
|
name = command.split(":", 1)[1]
|
||||||
|
manager.restart_strategy(name)
|
||||||
|
else:
|
||||||
|
print("未知命令:", command)
|
||||||
|
print("用法: python manager.py [status|start-all|stop-all|start:NAME|stop:NAME|restart:NAME]")
|
||||||
|
else:
|
||||||
|
print("用法: python manager.py [status|start-all|stop-all|start:NAME|stop:NAME|restart:NAME]")
|
||||||
253
strategy_manager/frontend/dist/index.html
vendored
Normal file
253
strategy_manager/frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>策略控制台</title>
|
||||||
|
<!-- 字体 -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- 1. 引入 Vue 3 -->
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<!-- 2. 引入 Naive UI -->
|
||||||
|
<script src="https://unpkg.com/naive-ui/dist/index.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; }
|
||||||
|
#app { padding: 20px; max-width: 1200px; margin: 0 auto; }
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.log-container { background: #1e1e1e; padding: 15px; border-radius: 4px; height: 400px; overflow: auto; font-family: monospace; font-size: 12px; color: #ddd; }
|
||||||
|
.log-line { margin: 2px 0; border-bottom: 1px solid #333; padding-bottom: 2px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<main-layout></main-layout>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, onMounted, onUnmounted, watch } = Vue;
|
||||||
|
const naive = window.naive;
|
||||||
|
|
||||||
|
// --- 主组件逻辑 ---
|
||||||
|
const MainLayout = {
|
||||||
|
template: `
|
||||||
|
<div class="header">
|
||||||
|
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
|
||||||
|
<n-space align="center">
|
||||||
|
<!-- 1. 刷新频率选择器 -->
|
||||||
|
<n-select
|
||||||
|
v-model:value="refreshInterval"
|
||||||
|
:options="intervalOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 130px"
|
||||||
|
></n-select>
|
||||||
|
|
||||||
|
<!-- 2. 手动刷新按钮 -->
|
||||||
|
<n-button type="primary" size="small" @click="fetchStatus" :loading="loading">
|
||||||
|
刷新状态
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-tag type="info" size="small">更新于: {{ lastUpdated }}</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-card title="策略列表" hoverable>
|
||||||
|
<n-table :single-line="false" striped>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>策略标识</th>
|
||||||
|
<th>策略名称</th>
|
||||||
|
<th>运行状态</th>
|
||||||
|
<th>PID</th>
|
||||||
|
<th>运行时长</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(info, key) in strategies" :key="key">
|
||||||
|
<td><strong>{{ key }}</strong></td>
|
||||||
|
<td>{{ info.config.strategy_name }} <br><small style="color:#999">{{ info.symbol }}</small></td>
|
||||||
|
<td>
|
||||||
|
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small">
|
||||||
|
{{ info.status === 'running' ? '运行中' : '已停止' }}
|
||||||
|
</n-tag>
|
||||||
|
</td>
|
||||||
|
<td>{{ info.pid || '-' }}</td>
|
||||||
|
<td>{{ info.uptime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<n-space>
|
||||||
|
<n-button v-if="info.status === 'stopped'" type="success" size="small" ghost @click="handleAction(key, 'start')">启动</n-button>
|
||||||
|
<n-button v-if="info.status === 'running'" type="error" size="small" ghost @click="handleAction(key, 'stop')">停止</n-button>
|
||||||
|
<n-button v-if="info.status === 'running'" type="warning" size="small" ghost @click="handleAction(key, 'restart')">重启</n-button>
|
||||||
|
<n-button size="small" @click="viewLogs(key)">日志</n-button>
|
||||||
|
</n-space>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="Object.keys(strategies).length === 0">
|
||||||
|
<td colspan="6" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 日志弹窗 -->
|
||||||
|
<n-modal v-model:show="showLogModal" style="width: 800px;" preset="card" :title="'📜 实时日志: ' + currentLogKey">
|
||||||
|
<div class="log-container" id="logBox">
|
||||||
|
<div v-if="logLoading" style="text-align:center; padding:20px;"><n-spin size="medium" /></div>
|
||||||
|
<div v-else v-for="(line, index) in logLines" :key="index" class="log-line">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button size="small" @click="fetchLogs(currentLogKey)">刷新</n-button>
|
||||||
|
<n-button size="small" @click="showLogModal = false">关闭</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
const message = naive.useMessage();
|
||||||
|
const dialog = naive.useDialog();
|
||||||
|
|
||||||
|
const strategies = ref({});
|
||||||
|
const loading = ref(false);
|
||||||
|
const lastUpdated = ref('-');
|
||||||
|
|
||||||
|
// --- 修改点:默认值为 0 (仅手动) ---
|
||||||
|
const refreshInterval = ref(0);
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ label: '✋ 仅手动', value: 0 }, // 建议将手动选项放在第一个
|
||||||
|
{ label: '⚡ 3秒自动', value: 3000 },
|
||||||
|
{ label: '⏱ 5秒自动', value: 5000 },
|
||||||
|
{ label: '🐢 10秒自动', value: 10000 }
|
||||||
|
];
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
// --- 核心数据获取 ---
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/status');
|
||||||
|
if (!res.ok) throw new Error("Error");
|
||||||
|
const data = await res.json();
|
||||||
|
strategies.value = data.strategies;
|
||||||
|
lastUpdated.value = new Date().toLocaleTimeString();
|
||||||
|
} catch (e) {
|
||||||
|
message.error("连接服务器失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 定时器管理 ---
|
||||||
|
const resetTimer = () => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
// 只有大于 0 才启动定时器
|
||||||
|
if (refreshInterval.value > 0) {
|
||||||
|
timer = setInterval(fetchStatus, refreshInterval.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听下拉框变化
|
||||||
|
watch(refreshInterval, () => {
|
||||||
|
resetTimer();
|
||||||
|
if (refreshInterval.value > 0) {
|
||||||
|
message.info(`已切换为 ${refreshInterval.value/1000} 秒自动刷新`);
|
||||||
|
fetchStatus();
|
||||||
|
} else {
|
||||||
|
message.info("已切换为手动刷新模式");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 其他逻辑 ---
|
||||||
|
const handleAction = (name, action) => {
|
||||||
|
const map = { start: '启动', stop: '停止', restart: '重启' };
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认操作',
|
||||||
|
content: `确定要 ${map[action]} ${name} 吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/strategy/${name}/${action}`, { method: 'POST' });
|
||||||
|
if (res.ok) {
|
||||||
|
message.success("操作成功");
|
||||||
|
fetchStatus();
|
||||||
|
} else {
|
||||||
|
message.error("操作失败");
|
||||||
|
}
|
||||||
|
} catch (e) { message.error("请求错误"); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLogModal = ref(false);
|
||||||
|
const currentLogKey = ref('');
|
||||||
|
const logLines = ref([]);
|
||||||
|
const logLoading = ref(false);
|
||||||
|
|
||||||
|
const fetchLogs = async (name) => {
|
||||||
|
logLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/logs/${name}?lines=100`);
|
||||||
|
const data = await res.json();
|
||||||
|
logLines.value = data.lines;
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('logBox');
|
||||||
|
if(el) el.scrollTop = el.scrollHeight;
|
||||||
|
}, 100);
|
||||||
|
} catch(e) { message.error("日志获取失败"); }
|
||||||
|
finally { logLoading.value = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewLogs = (name) => {
|
||||||
|
currentLogKey.value = name;
|
||||||
|
showLogModal.value = true;
|
||||||
|
fetchLogs(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStatus(); // 页面加载时请求一次
|
||||||
|
resetTimer(); // 初始化定时器(当前为0,所以不启动)
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
strategies, loading, lastUpdated,
|
||||||
|
refreshInterval, intervalOptions,
|
||||||
|
showLogModal, currentLogKey, logLines, logLoading,
|
||||||
|
fetchStatus, handleAction, viewLogs, fetchLogs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
components: { MainLayout },
|
||||||
|
setup() {
|
||||||
|
return { zhCN: naive.zhCN, dateZhCN: naive.dateZhCN }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(naive).forEach(key => {
|
||||||
|
if (key.startsWith('N') && !key.startsWith('NTh') && !key.startsWith('use')) {
|
||||||
|
app.component(key, naive[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import signal
|
import signal
|
||||||
from datetime import timedelta
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import importlib
|
import importlib.util
|
||||||
|
import logging # 新增:日志模块
|
||||||
|
|
||||||
# ==================== 动态路径配置 ====================
|
# ==================== 动态路径配置 ====================
|
||||||
from core.path_utils import add_project_root_to_path
|
from core.path_utils import add_project_root_to_path
|
||||||
@@ -19,7 +18,6 @@ print(f"[INFO] Python路径: {sys.path[:3]}")
|
|||||||
def load_strategy_class(class_path: str):
|
def load_strategy_class(class_path: str):
|
||||||
"""动态加载策略类"""
|
"""动态加载策略类"""
|
||||||
try:
|
try:
|
||||||
# class_path: "futures_trading_strategies.FG.TrendlineBreakoutStrategy.DualModeTrendlineHawkesStrategy2.DualModeTrendlineHawkesStrategy"
|
|
||||||
module_path, class_name = class_path.rsplit('.', 1)
|
module_path, class_name = class_path.rsplit('.', 1)
|
||||||
module = importlib.import_module(module_path)
|
module = importlib.import_module(module_path)
|
||||||
return getattr(module, class_name)
|
return getattr(module, class_name)
|
||||||
@@ -30,26 +28,96 @@ def load_strategy_class(class_path: str):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_strategy_logger(config_file: Path, strategy_name: str, symbol: str) -> logging.Logger:
|
||||||
|
"""配置策略专属日志"""
|
||||||
|
# 创建日志目录: logs/{策略名}/
|
||||||
|
log_dir = Path("logs") / strategy_name
|
||||||
|
log_dir.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
# 日志文件: logs/{策略名}/{品种}.log
|
||||||
|
log_file = log_dir / f"{symbol}.log"
|
||||||
|
|
||||||
|
# 配置logger
|
||||||
|
logger = logging.getLogger(f"Strategy.{strategy_name}.{symbol}")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 清除旧handler(防止重复)
|
||||||
|
for h in logger.handlers[:]:
|
||||||
|
logger.removeHandler(h)
|
||||||
|
|
||||||
|
# 文件handler
|
||||||
|
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||||
|
file_handler.setFormatter(
|
||||||
|
logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
|
||||||
|
)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
return logger, log_file
|
||||||
|
|
||||||
|
|
||||||
def run_strategy(config_path: str):
|
def run_strategy(config_path: str):
|
||||||
"""通过配置文件运行策略"""
|
"""通过配置文件运行策略"""
|
||||||
# 1. 加载配置
|
config_file = Path(config_path)
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
if not config_file.exists():
|
||||||
config = json.load(f)
|
print(f"[ERROR] 配置文件不存在: {config_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 动态加载配置模块
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
f"strategy_config_{config_file.stem}", config_file
|
||||||
|
)
|
||||||
|
config_module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(config_module)
|
||||||
|
|
||||||
|
if not hasattr(config_module, 'CONFIG'):
|
||||||
|
print(f"[ERROR] 配置文件缺少 CONFIG 变量: {config_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config = config_module.CONFIG
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] 加载配置文件失败 {config_path}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 配置策略专属日志(关键修改点)
|
||||||
|
strategy_name = config_file.parent.name
|
||||||
|
symbol = config_file.stem
|
||||||
|
logger, log_file = setup_strategy_logger(config_file, strategy_name, symbol)
|
||||||
|
|
||||||
|
# 重定向print到logger(捕获策略内的print调用)
|
||||||
|
class PrintToLogger:
|
||||||
|
def __init__(self, logger, level=logging.INFO):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
self.linebuf = ''
|
||||||
|
|
||||||
|
def write(self, buf):
|
||||||
|
for line in buf.rstrip().splitlines():
|
||||||
|
self.logger.log(self.level, line.rstrip())
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 重定向stdout和stderr
|
||||||
|
sys.stdout = PrintToLogger(logger, logging.INFO)
|
||||||
|
sys.stderr = PrintToLogger(logger, logging.ERROR)
|
||||||
|
|
||||||
|
# 所有后续的print都会写入日志文件
|
||||||
print(f"[INFO] [{config['name']}] 正在启动...")
|
print(f"[INFO] [{config['name']}] 正在启动...")
|
||||||
|
print(f"[INFO] 日志文件: {log_file}")
|
||||||
|
|
||||||
# 2. 动态加载策略类
|
# 动态加载策略类
|
||||||
strategy_class = load_strategy_class(config["strategy_class"])
|
strategy_class = load_strategy_class(config["strategy_class"])
|
||||||
|
|
||||||
# 3. 创建API
|
# 创建API
|
||||||
from tqsdk import TqApi, TqAuth, TqKq
|
from tqsdk import TqApi, TqAuth, TqKq
|
||||||
api = TqApi(TqKq(), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
api = TqApi(TqKq(), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||||
|
|
||||||
# 4. 准备策略参数
|
# 准备策略参数
|
||||||
strategy_params = config["strategy_params"].copy()
|
strategy_params = config["strategy_params"].copy()
|
||||||
strategy_params["main_symbol"] = config["engine_params"]["symbol"].split(".")[-1]
|
strategy_params["main_symbol"] = config["engine_params"]["symbol"].split(".")[-1]
|
||||||
|
|
||||||
# 5. 创建引擎
|
# 创建引擎
|
||||||
from src.tqsdk_real_engine import TqsdkEngine
|
from src.tqsdk_real_engine import TqsdkEngine
|
||||||
|
|
||||||
engine = TqsdkEngine(
|
engine = TqsdkEngine(
|
||||||
@@ -60,24 +128,24 @@ def run_strategy(config_path: str):
|
|||||||
duration_seconds=config["engine_params"]["duration_seconds"],
|
duration_seconds=config["engine_params"]["duration_seconds"],
|
||||||
roll_over_mode=config["engine_params"]["roll_over_mode"],
|
roll_over_mode=config["engine_params"]["roll_over_mode"],
|
||||||
history_length=config["engine_params"]["history_length"],
|
history_length=config["engine_params"]["history_length"],
|
||||||
close_bar_delta=timedelta(**config["engine_params"]["close_bar_delta"])
|
close_bar_delta=config["engine_params"]["close_bar_delta"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# 6. 信号处理
|
# 信号处理
|
||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
print(f"\n[INFO] [{config['name']}] 收到停止信号 {sig},正在关闭...")
|
print(f"\n[INFO] [{config['name']}] 收到停止信号 {sig},正在关闭...")
|
||||||
api.close()
|
# api.close()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
# 7. 运行
|
# 运行
|
||||||
try:
|
try:
|
||||||
print(f"[INFO] [{config['name']}] 开始运行")
|
print(f"[INFO] [{config['name']}] 开始运行")
|
||||||
engine.run()
|
engine.run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] [{config['name']}] 运行出错: {e}")
|
print(f"[ERROR] [{config['name']}] 运行出错: {e}", exc_info=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
api.close()
|
api.close()
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ class RestartDaemon:
|
|||||||
|
|
||||||
# 确保目录存在
|
# 确保目录存在
|
||||||
self.pid_dir.mkdir(exist_ok=True)
|
self.pid_dir.mkdir(exist_ok=True)
|
||||||
|
self.log_dir.mkdir(exist_ok=True) # 确保日志目录存在
|
||||||
|
|
||||||
def _setup_logger(self):
|
def _setup_logger(self):
|
||||||
"""配置日志"""
|
"""配置日志"""
|
||||||
self.log_dir.mkdir(exist_ok=True)
|
|
||||||
log_file = self.log_dir / "restart_daemon.log"
|
log_file = self.log_dir / "restart_daemon.log"
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -41,9 +41,14 @@ class RestartDaemon:
|
|||||||
handlers=[
|
handlers=[
|
||||||
logging.FileHandler(log_file, encoding='utf-8'),
|
logging.FileHandler(log_file, encoding='utf-8'),
|
||||||
logging.StreamHandler(sys.stdout)
|
logging.StreamHandler(sys.stdout)
|
||||||
]
|
],
|
||||||
|
force=True # 防止日志配置冲突
|
||||||
)
|
)
|
||||||
return logging.getLogger("RestartDaemon")
|
logger = logging.getLogger("RestartDaemon")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("📝 日志系统初始化完成")
|
||||||
|
logger.info("📂 日志文件: %s", log_file.absolute())
|
||||||
|
return logger
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""启动守护进程"""
|
"""启动守护进程"""
|
||||||
@@ -57,8 +62,8 @@ class RestartDaemon:
|
|||||||
|
|
||||||
self.logger.info("=" * 80)
|
self.logger.info("=" * 80)
|
||||||
self.logger.info("✅ 重启守护进程已启动")
|
self.logger.info("✅ 重启守护进程已启动")
|
||||||
self.logger.info("⏰ 监控时间点: {}".format(", ".join(self.RESTART_TIMES)))
|
self.logger.info("⏰ 监控时间点: %s", ", ".join(self.RESTART_TIMES))
|
||||||
self.logger.info("📂 PID目录: {}".format(self.pid_dir.absolute()))
|
self.logger.info("📂 PID目录: %s", self.pid_dir.absolute())
|
||||||
self.logger.info("=" * 80)
|
self.logger.info("=" * 80)
|
||||||
|
|
||||||
# 主线程阻塞(保持进程运行)
|
# 主线程阻塞(保持进程运行)
|
||||||
@@ -97,14 +102,14 @@ class RestartDaemon:
|
|||||||
time.sleep(60) # 每分钟检查一次
|
time.sleep(60) # 每分钟检查一次
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error("❌ 检查循环出错: {}".format(e))
|
self.logger.error("❌ 检查循环出错: %s", e, exc_info=True)
|
||||||
self.logger.error("=" * 80)
|
self.logger.error("=" * 80)
|
||||||
time.sleep(60) # 出错后等待1分钟继续
|
time.sleep(60) # 出错后等待1分钟继续
|
||||||
|
|
||||||
def _perform_restart(self, time_point: str):
|
def _perform_restart(self, time_point: str):
|
||||||
"""执行重启"""
|
"""执行重启"""
|
||||||
self.logger.info("\n" + "=" * 80)
|
self.logger.info("\n" + "=" * 80)
|
||||||
self.logger.info("⏰ 到达重启时间: {}".format(time_point))
|
self.logger.info("⏰ 到达重启时间: %s", time_point)
|
||||||
self.logger.info("=" * 80)
|
self.logger.info("=" * 80)
|
||||||
|
|
||||||
# 1. 扫描所有PID文件
|
# 1. 扫描所有PID文件
|
||||||
@@ -113,7 +118,7 @@ class RestartDaemon:
|
|||||||
self.logger.info("⚠️ 未发现运行中的策略")
|
self.logger.info("⚠️ 未发现运行中的策略")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info("📋 发现 {} 个策略需要重启".format(len(pid_files)))
|
self.logger.info("📋 发现 %d 个策略需要重启", len(pid_files))
|
||||||
|
|
||||||
# 2. 停止所有策略
|
# 2. 停止所有策略
|
||||||
stopped_count = 0
|
stopped_count = 0
|
||||||
@@ -124,21 +129,21 @@ class RestartDaemon:
|
|||||||
|
|
||||||
if psutil.pid_exists(pid):
|
if psutil.pid_exists(pid):
|
||||||
proc = psutil.Process(pid)
|
proc = psutil.Process(pid)
|
||||||
self.logger.info("⏹️ 停止策略 PID {}: {}".format(pid, proc.name()))
|
self.logger.info("⏹️ 停止策略 PID %d: %s", pid, proc.name())
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proc.wait(timeout=30)
|
proc.wait(timeout=30)
|
||||||
self.logger.info("✅ 已优雅停止 PID {}".format(pid))
|
self.logger.info("✅ 已优雅停止 PID %d", pid)
|
||||||
stopped_count += 1
|
stopped_count += 1
|
||||||
except psutil.TimeoutExpired:
|
except psutil.TimeoutExpired:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
self.logger.info("🔥 强制终止 PID {}".format(pid))
|
self.logger.info("🔥 强制终止 PID %d", pid)
|
||||||
stopped_count += 1
|
stopped_count += 1
|
||||||
else:
|
else:
|
||||||
self.logger.warning("⚠️ PID文件存在但进程已死: {}".format(pid))
|
self.logger.warning("⚠️ PID文件存在但进程已死: %d", pid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error("❌ 停止失败 {}: {}".format(pid_file, e))
|
self.logger.error("❌ 停止失败 %s: %s", pid_file, e, exc_info=True)
|
||||||
|
|
||||||
if stopped_count == 0:
|
if stopped_count == 0:
|
||||||
self.logger.warning("⚠️ 未成功停止任何策略")
|
self.logger.warning("⚠️ 未成功停止任何策略")
|
||||||
@@ -154,38 +159,38 @@ class RestartDaemon:
|
|||||||
for pid_file in pid_files:
|
for pid_file in pid_files:
|
||||||
try:
|
try:
|
||||||
# 从PID文件名推导配置路径
|
# 从PID文件名推导配置路径
|
||||||
# DualModeTrendlineHawkesStrategy2_FG.pid -> strategies/DualModeTrendlineHawkesStrategy2/FG.config
|
# DualModeTrendlineHawkesStrategy2_FG.pid -> strategies/DualModeTrendlineHawkesStrategy2/FG.py
|
||||||
name = pid_file.stem
|
name = pid_file.stem
|
||||||
if '_' not in name:
|
if '_' not in name:
|
||||||
self.logger.error("❌ PID文件名格式错误: {}".format(name))
|
self.logger.error("❌ PID文件名格式错误: %s", name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
strategy_name, symbol = name.split('_', 1)
|
strategy_name, symbol = name.split('_', 1)
|
||||||
config_file = Path("strategies") / strategy_name / "{}.config".format(symbol)
|
config_file = Path("strategies") / strategy_name / "{}.py".format(symbol)
|
||||||
|
|
||||||
if not config_file.exists():
|
if not config_file.exists():
|
||||||
self.logger.error("❌ 配置文件不存在: {}".format(config_file))
|
self.logger.error("❌ 配置文件不存在: %s", config_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 启动新进程(不阻塞,立即返回)
|
# 启动新进程(不阻塞,立即返回)
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
[sys.executable, "launcher.py", "--config", str(config_file)],
|
[sys.executable, "launcher.py", "--config", str(config_file)],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL, # launcher内会自行处理日志
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
cwd=Path.cwd()
|
cwd=Path.cwd()
|
||||||
)
|
)
|
||||||
|
|
||||||
self.logger.info("✅ 启动新进程 PID {}: {}".format(process.pid, config_file.name))
|
self.logger.info("✅ 启动新进程 PID %d: %s", process.pid, config_file.name)
|
||||||
restarted_count += 1
|
restarted_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error("❌ 启动失败: {}".format(e))
|
self.logger.error("❌ 启动失败: %s", e, exc_info=True)
|
||||||
|
|
||||||
# 5. 统计结果
|
# 5. 统计结果
|
||||||
self.logger.info("\n" + "=" * 80)
|
self.logger.info("\n" + "=" * 80)
|
||||||
self.logger.info("📊 重启统计:")
|
self.logger.info("📊 重启统计:")
|
||||||
self.logger.info(" 停止成功: {}个".format(stopped_count))
|
self.logger.info(" 停止成功: %d个", stopped_count)
|
||||||
self.logger.info(" 启动成功: {}个".format(restarted_count))
|
self.logger.info(" 启动成功: %d个", restarted_count)
|
||||||
|
|
||||||
if stopped_count == restarted_count and stopped_count > 0:
|
if stopped_count == restarted_count and stopped_count > 0:
|
||||||
self.logger.info("✅ 所有策略重启成功")
|
self.logger.info("✅ 所有策略重启成功")
|
||||||
|
|||||||
31
strategy_manager/strategies/SpectralTrendStrategy/SA.py
Normal file
31
strategy_manager/strategies/SpectralTrendStrategy/SA.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 策略配置(Python格式)
|
||||||
|
from src.indicators.indicators import ZScoreATR
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
"name": "傅里叶趋势策略",
|
||||||
|
"version": "1.0",
|
||||||
|
"enabled": True,
|
||||||
|
|
||||||
|
"strategy_class": "futures_trading_strategies.SA.Spectral.SpectralTrendStrategy.SpectralTrendStrategy",
|
||||||
|
|
||||||
|
"engine_params": {
|
||||||
|
"symbol": "KQ.m@CZCE.SA",
|
||||||
|
"duration_seconds": 900,
|
||||||
|
"roll_over_mode": True,
|
||||||
|
"history_length": 1000,
|
||||||
|
# 支持Python对象
|
||||||
|
"close_bar_delta": __import__('datetime').timedelta(minutes=58)
|
||||||
|
},
|
||||||
|
|
||||||
|
"strategy_params": {
|
||||||
|
'main_symbol': 'SA', # <-- 替换为你的交易品种代码,例如 'GC=F' (黄金期货), 'ZC=F' (玉米期货)
|
||||||
|
'trade_volume': 1,
|
||||||
|
'model_indicator': ZScoreATR(14, 100, 0.5, 3),
|
||||||
|
'spectral_window_days': 8, # STFT窗口大小(天)
|
||||||
|
'low_freq_days': 8, # 低频下限(天)
|
||||||
|
'high_freq_days': 4, # 高频上限(天)
|
||||||
|
'trend_strength_threshold': 0.7, # 相变临界值
|
||||||
|
'exit_threshold': 0.3, # 退出阈值
|
||||||
|
'enable_log': True
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "test策略",
|
|
||||||
"version": "1.0",
|
|
||||||
"enabled": true,
|
|
||||||
|
|
||||||
"strategy_class": "futures_trading_strategies.FG.TrendlineBreakoutStrategy.DualModeTrendlineHawkesStrategy2.DualModeTrendlineHawkesStrategy",
|
|
||||||
|
|
||||||
"engine_params": {
|
|
||||||
"symbol": "KQ.m@CZCE.FG",
|
|
||||||
"duration_seconds": 900,
|
|
||||||
"roll_over_mode": true,
|
|
||||||
"history_length": 1000,
|
|
||||||
"close_bar_delta": {"minutes": 58}
|
|
||||||
},
|
|
||||||
|
|
||||||
"strategy_params": {
|
|
||||||
"main_symbol": "FG",
|
|
||||||
"trade_volume": 2,
|
|
||||||
"enable_log": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
strategy_manager/strategies/TestConnectionStrategy/FG.py
Normal file
23
strategy_manager/strategies/TestConnectionStrategy/FG.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 策略配置(Python格式)
|
||||||
|
CONFIG = {
|
||||||
|
"name": "玻璃双模式趋势线策略",
|
||||||
|
"version": "1.0",
|
||||||
|
"enabled": True,
|
||||||
|
|
||||||
|
"strategy_class": "futures_trading_strategies.FG.TrendlineBreakoutStrategy.DualModeTrendlineHawkesStrategy2.DualModeTrendlineHawkesStrategy",
|
||||||
|
|
||||||
|
"engine_params": {
|
||||||
|
"symbol": "KQ.m@CZCE.FG",
|
||||||
|
"duration_seconds": 900,
|
||||||
|
"roll_over_mode": True,
|
||||||
|
"history_length": 1000,
|
||||||
|
# 支持Python对象
|
||||||
|
"close_bar_delta": __import__('datetime').timedelta(minutes=58)
|
||||||
|
},
|
||||||
|
|
||||||
|
"strategy_params": {
|
||||||
|
"main_symbol": "FG",
|
||||||
|
"trade_volume": 2,
|
||||||
|
"enable_log": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
144
strategy_manager/web_backend.py
Normal file
144
strategy_manager/web_backend.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# 引入调度器
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
# 复用现有manager
|
||||||
|
from core.manager import StrategyManager
|
||||||
|
|
||||||
|
# ================== 初始化 ==================
|
||||||
|
app = FastAPI(title="策略控制台")
|
||||||
|
manager = StrategyManager()
|
||||||
|
|
||||||
|
# 初始化调度器
|
||||||
|
scheduler = AsyncIOScheduler()
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ================== 定时任务逻辑 ==================
|
||||||
|
|
||||||
|
def scheduled_restart_task():
|
||||||
|
"""
|
||||||
|
定时任务:重启所有正在运行的策略
|
||||||
|
"""
|
||||||
|
logger.info("⏰ [定时任务] 触发自动重启流程...")
|
||||||
|
|
||||||
|
# 获取当前所有策略状态
|
||||||
|
status = manager.get_status()
|
||||||
|
running_strategies = [
|
||||||
|
name for name, info in status['strategies'].items()
|
||||||
|
if info['status'] == 'running'
|
||||||
|
]
|
||||||
|
|
||||||
|
if not running_strategies:
|
||||||
|
logger.info("⏰ [定时任务] 当前无运行中的策略,无需重启")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"⏰ [定时任务] 即将重启以下策略: {running_strategies}")
|
||||||
|
|
||||||
|
for name in running_strategies:
|
||||||
|
try:
|
||||||
|
# 调用 manager 的重启逻辑 (包含 stop -> sleep -> start)
|
||||||
|
manager.restart_strategy(name)
|
||||||
|
logger.info(f"✅ [定时任务] {name} 重启成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ [定时任务] {name} 重启失败: {e}")
|
||||||
|
|
||||||
|
logger.info("⏰ [定时任务] 自动重启流程结束")
|
||||||
|
|
||||||
|
|
||||||
|
# ================== FastAPI 事件钩子 ==================
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def start_scheduler():
|
||||||
|
"""服务启动时,加载定时任务"""
|
||||||
|
# 任务 1: 每天 08:55
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_restart_task,
|
||||||
|
CronTrigger(hour=8, minute=55),
|
||||||
|
id="restart_morning",
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 任务 2: 每天 20:55
|
||||||
|
scheduler.add_job(
|
||||||
|
scheduled_restart_task,
|
||||||
|
CronTrigger(hour=20, minute=55),
|
||||||
|
id="restart_evening",
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
|
logger.info("📅 定时任务调度器已启动 (计划时间: 08:55, 20:55)")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def stop_scheduler():
|
||||||
|
"""服务关闭时停止调度器"""
|
||||||
|
scheduler.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
# ================== 原有 REST API (保持不变) ==================
|
||||||
|
|
||||||
|
@app.get("/api/status")
|
||||||
|
def get_status():
|
||||||
|
return manager.get_status()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/strategy/{name}/start")
|
||||||
|
def start_strategy(name: str):
|
||||||
|
if manager.start_strategy(name):
|
||||||
|
return {"success": True}
|
||||||
|
raise HTTPException(400, "启动失败")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/strategy/{name}/stop")
|
||||||
|
def stop_strategy(name: str):
|
||||||
|
if manager.stop_strategy(name):
|
||||||
|
return {"success": True}
|
||||||
|
raise HTTPException(400, "停止失败")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/strategy/{name}/restart")
|
||||||
|
def restart_strategy(name: str):
|
||||||
|
# 修复了之前提到的返回值问题
|
||||||
|
if manager.restart_strategy(name):
|
||||||
|
return {"success": True}
|
||||||
|
raise HTTPException(400, "重启失败,请检查日志")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/logs/{name}")
|
||||||
|
def get_logs(name: str, lines: int = Query(50, le=500)):
|
||||||
|
try:
|
||||||
|
if '_' not in name:
|
||||||
|
return {"lines": []}
|
||||||
|
strategy_name, symbol = name.split('_', 1)
|
||||||
|
log_file = Path(f"logs/{strategy_name}/{symbol}.log")
|
||||||
|
|
||||||
|
if not log_file.exists():
|
||||||
|
return {"lines": ["日志文件尚未生成"]}
|
||||||
|
|
||||||
|
# 修复编码和读取
|
||||||
|
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
last_lines = deque(f, maxlen=lines)
|
||||||
|
return {"lines": [line.rstrip() for line in last_lines]}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"读取日志失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ================== 静态文件挂载 ==================
|
||||||
|
app.mount("/static", StaticFiles(directory="frontend/dist"), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def serve_frontend():
|
||||||
|
return FileResponse("frontend/dist/index.html")
|
||||||
Reference in New Issue
Block a user