Files
NewQuant/strategy_manager/core/manager.py

405 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import time
import psutil
import subprocess
import logging
from pathlib import Path
from typing import Dict, Any, List
from datetime import datetime
import importlib.util
import json # 确保导入json模块
# ==================== 动态路径配置 ====================
from core.path_utils import add_project_root_to_path
# 添加项目根路径到sys.path
PROJECT_ROOT = add_project_root_to_path()
# ==================================================
class StrategyManager:
def __init__(self, config_path: str = "config/main.json"):
self.config = self._load_main_config(config_path)
self.strategies_dir = Path("strategies")
self.logs_dir = Path(self.config["logs_dir"])
self.status_file = Path(self.config["status_file"])
self.pid_dir = Path(self.config["pid_dir"])
# 创建目录
self.logs_dir.mkdir(exist_ok=True)
self.pid_dir.mkdir(exist_ok=True)
# 配置管理器日志
self._setup_logger()
self.strategies: Dict[str, Dict[str, Any]] = {}
self.logger.info("🔄 正在加载策略配置...")
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]:
path = Path(config_path)
if not path.exists():
self.logger.warning("⚠️ 主配置文件不存在,使用默认配置")
return {
"logs_dir": "logs",
"status_file": "status.json",
"pid_dir": "pids"
}
with open(path, 'r') as f:
return json.load(f)
def load_strategies(self):
"""递归扫描 strategies/ 目录,查找 .py 配置文件"""
self.strategies = {}
if not self.strategies_dir.exists():
self.logger.error("❌ 策略配置目录不存在: %s", self.strategies_dir)
return
for config_file in self.strategies_dir.rglob("*.py"):
try:
spec = importlib.util.spec_from_file_location(
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']
for field in required:
if field not in config:
raise ValueError("配置缺少必要字段: {}".format(field))
relative_path = config_file.relative_to(self.strategies_dir)
strategy_name = relative_path.parent.name
symbol = config_file.stem
strategy_key = "{}_{}".format(strategy_name, symbol)
self.strategies[strategy_key] = {
"strategy_name": strategy_name,
"symbol": symbol,
"config_file": str(config_file),
"config": config,
"status": "stopped",
"pid": None,
"started_at": None,
"uptime": None
}
self.logger.info("📄 加载配置: %s", config_file)
except Exception as e:
self.logger.error("❌ 加载配置失败 %s: %s", config_file, e, exc_info=True)
def get_status(self) -> Dict[str, Any]:
"""获取完整状态"""
self._refresh_status()
return {
"timestamp": datetime.now().isoformat(),
"total": len(self.strategies),
"running": sum(1 for s in self.strategies.values() if s["status"] == "running"),
"strategies": self.strategies
}
def _refresh_status(self):
"""刷新进程状态 - 双重验证"""
for name, info in self.strategies.items():
pid_file = self.pid_dir / "{}.pid".format(name)
if pid_file.exists():
try:
with open(pid_file, 'r') as f:
pid = int(f.read().strip())
if psutil.pid_exists(pid):
try:
proc = psutil.Process(pid)
if "python" in proc.name().lower():
info["status"] = "running"
info["pid"] = pid
if info["started_at"]:
started = datetime.fromisoformat(info["started_at"])
uptime = datetime.now() - started
info["uptime"] = str(uptime).split('.')[0]
continue
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
self._cleanup_stopped_strategy(name, pid_file)
except Exception as e:
self.logger.warning("⚠️ 刷新状态失败 %s: %s", name, e)
self._cleanup_stopped_strategy(name, pid_file)
else:
info["status"] = "stopped"
info["pid"] = None
info["started_at"] = None
info["uptime"] = None
def _is_running(self, name: str) -> bool:
"""检查策略是否运行中"""
self._refresh_status()
info = self.strategies[name]
if not info["pid"]:
return False
pid_file = self.pid_dir / "{}.pid".format(name)
if not pid_file.exists():
return False
try:
with open(pid_file, 'r') as f:
pid = int(f.read().strip())
if psutil.pid_exists(pid):
try:
proc = psutil.Process(pid)
return "python" in proc.name().lower()
except (psutil.NoSuchProcess, psutil.AccessDenied):
return False
return False
except:
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:
"""停止单个策略"""
if name not in self.strategies:
self.logger.error("❌ 策略不存在: %s", name)
return False
if not self._is_running(name):
self.logger.warning("⚠️ 策略未运行: %s", name)
return False
info = self.strategies[name]
try:
pid = info["pid"]
process = psutil.Process(pid)
self.logger.info("\n" + "=" * 80)
self.logger.info("⏹️ 正在停止策略: %s (PID: %d)", name, pid)
# 优雅终止
process.terminate()
try:
process.wait(timeout=timeout)
self.logger.info("✅ 已优雅停止: %s", name)
except psutil.TimeoutExpired:
self.logger.warning("⏱️ 超时,强制终止: %s", name)
process.kill()
process.wait()
# 清理状态
self._cleanup_stopped_strategy(name, self.pid_dir / "{}.pid".format(name))
self._save_status()
self.logger.info("=" * 80)
return True
except Exception as e:
self.logger.error("❌ 停止失败 %s: %s", name, e, exc_info=True)
return False
def restart_strategy(self, name: str) -> bool:
"""重启策略"""
self.logger.info("\n" + "=" * 80)
self.logger.info("🔄 正在重启: %s", name)
self.stop_strategy(name)
time.sleep(2)
return self.start_strategy(name)
def start_all(self):
"""启动所有启用的策略"""
self.logger.info("\n" + "=" * 100)
self.logger.info("🚀 正在启动所有启用的策略...")
self.logger.info("=" * 100)
started = []
for name, info in self.strategies.items():
if info["config"]["enabled"] and not self._is_running(name):
if self.start_strategy(name):
started.append(name)
self.logger.info("\n✅ 成功启动 %d 个策略", len(started))
if started:
self.logger.info("📋 策略: %s", ", ".join(started))
def stop_all(self):
"""停止所有运行的策略"""
self.logger.info("\n" + "=" * 100)
self.logger.info("⏹️ 正在停止所有运行的策略...")
self.logger.info("=" * 100)
stopped = []
for name in self.strategies.keys():
if self._is_running(name):
if self.stop_strategy(name):
stopped.append(name)
self.logger.info("\n✅ 成功停止 %d 个策略", len(stopped))
if stopped:
self.logger.info("📋 策略: %s", ", ".join(stopped))
def _cleanup_stopped_strategy(self, name: str, pid_file: Path):
"""清理已停止的策略状态"""
pid_file.unlink(missing_ok=True)
info = self.strategies[name]
info["status"] = "stopped"
info["pid"] = None
info["started_at"] = None
info["uptime"] = None
def _save_status(self):
"""状态持久化修复排除不可序列化的config字段"""
try:
status = self.get_status()
# 创建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]):
"""格式化打印状态表格"""
print("\n" + "=" * 130)
print("策略状态总览 (更新时间: {})".format(status['timestamp']))
print("总计: {} | 运行中: {} | 已停止: {}".format(
status['total'], status['running'], status['total'] - status['running']
))
print("=" * 130)
if not status["strategies"]:
print("未找到任何策略")
return
print(
"配置标识 策略名称 状态 PID 运行时长 启动时间")
print("-" * 130)
for name, info in status["strategies"].items():
status_text = "RUNNING" if info["status"] == "running" else "STOPPED"
pid_text = str(info["pid"]) if info["pid"] else "-"
uptime_text = info["uptime"] if info["uptime"] else "-"
started_text = info["started_at"][:19] if info["started_at"] else "-"
print("{:<35} {:<40} {:<10} {:<10} {:<15} {:<25}".format(
name, info['config']['name'], status_text, pid_text, uptime_text, started_text
))
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]")