Files
NewQuant/strategy_manager/core/manager.py

287 lines
10 KiB
Python
Raw Normal View History

import os
import sys
import json
import time
import psutil
import subprocess
from pathlib import Path
from typing import Dict, Any, List
from datetime import datetime
# ==================== 动态路径配置 ====================
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.strategies: Dict[str, Dict[str, Any]] = {}
self.load_strategies()
def _load_main_config(self, config_path: str) -> Dict[str, Any]:
path = Path(config_path)
if not path.exists():
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/ 目录,查找 .config 文件"""
self.strategies = {}
if not self.strategies_dir.exists():
print("[ERROR] 策略配置目录不存在: {}".format(self.strategies_dir))
return
for config_file in self.strategies_dir.rglob("*.config"):
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
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
}
except Exception as e:
print("[ERROR] 加载配置失败 {}: {}".format(config_file, e))
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):
"""
刷新进程状态 - 双重验证
1. 检查PID文件是否存在
2. 检查进程是否存在
3. 验证进程名是否为python防止PID复用
"""
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)
# 验证进程名是否包含python
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
# PID不存在或验证失败清理
self._cleanup_stopped_strategy(name, pid_file)
except Exception as e:
print("[WARNING] 刷新状态失败 {}: {}".format(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:
"""
检查策略是否运行中 - 实时刷新状态
确保与status命令结果一致
"""
# 先刷新状态确保最新
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 stop_strategy(self, name: str, timeout: int = 30) -> bool:
"""停止单个策略"""
if name not in self.strategies:
print("[ERROR] 策略不存在: {}".format(name))
return False
# 再次检查状态(确保最新)
if not self._is_running(name):
print("[WARNING] 策略未运行: {}".format(name))
return False
info = self.strategies[name]
try:
pid = info["pid"]
process = psutil.Process(pid)
print("\n[INFO] 正在停止: {} (PID: {})...".format(name, pid))
# 优雅终止
process.terminate()
try:
process.wait(timeout=timeout)
print("[SUCCESS] 已停止: {}".format(name))
except psutil.TimeoutExpired:
print("[WARNING] 超时,强制终止: {}".format(name))
process.kill()
process.wait()
# 清理状态
self._cleanup_stopped_strategy(name, self.pid_dir / "{}.pid".format(name))
self._save_status()
return True
except Exception as e:
print("[ERROR] 停止失败 {}: {}".format(name, e))
return False
def restart_strategy(self, name: str) -> bool:
"""重启策略"""
print("\n[INFO] 正在重启: {}".format(name))
self.stop_strategy(name)
time.sleep(2)
return self.start_strategy(name)
def start_all(self):
"""启动所有启用的策略"""
print("\n" + "=" * 100)
print("正在启动所有启用的策略...")
print("=" * 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)
print("\n[SUCCESS] 成功启动 {} 个策略".format(len(started)))
if started:
print("策略: {}".format(", ".join(started)))
def stop_all(self):
"""停止所有运行的策略"""
print("\n" + "=" * 100)
print("正在停止所有运行的策略...")
print("=" * 100)
stopped = []
for name in self.strategies.keys():
if self._is_running(name):
if self.stop_strategy(name):
stopped.append(name)
print("\n[SUCCESS] 成功停止 {} 个策略".format(len(stopped)))
if stopped:
print("策略: {}".format(", ".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):
"""状态持久化"""
status = self.get_status()
with open(self.status_file, 'w') as f:
json.dump(status, f, indent=2, ensure_ascii=False)
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)