Files
NewQuant/strategy_manager/core/manager.py
liaozhaorun c0d996f39b feat(strategy_manager): 添加策略自动启动的白名单管理
实现全面的白名单管理系统,支持策略自动启动:

- 添加 WhitelistManager 类用于持久化白名单配置存储
- 将白名单功能集成到 StrategyManager 核心模块
- 添加用于白名单 CRUD 操作的 REST API 端点
- 通过 start.py 创建用于白名单管理的 CLI 命令
- 更新前端 UI,添加白名单状态指示器和批量操作功能
- 实现每日 08:58 的自动启动调度
- 为白名单操作添加配置验证和日志记录

同时添加项目文档:
- 代码格式规则(缩进、行长度、导入、命名)
- 文件、变量、常量、函数、类的命名规范
- 受限制文件和目录的保护规则
2026-01-26 01:21:46 +08:00

541 lines
20 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
from core.whitelist_manager import WhitelistManager
# 添加项目根路径到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.whitelist_manager = WhitelistManager()
self.logger.info("📋 白名单管理器已初始化")
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()
# 构建状态数据
status = {
"timestamp": datetime.now().isoformat(),
"total": len(self.strategies),
"running": sum(1 for s in self.strategies.values() if s["status"] == "running"),
"strategies": self.strategies
}
# 添加白名单信息到每个策略
for name, info in status["strategies"].items():
info["in_whitelist"] = self.whitelist_manager.is_in_whitelist(name)
info["whitelist_enabled"] = self.whitelist_manager.is_enabled_in_whitelist(name)
# 添加自动启动状态
auto_start_status = self.whitelist_manager.get_auto_start_status()
status["whitelist_auto_start_today"] = auto_start_status["should_auto_start"]
status["whitelist_last_date"] = auto_start_status["last_auto_start_date"]
status["whitelist_total"] = auto_start_status["whitelist_count"]
status["whitelist_enabled"] = auto_start_status["enabled_count"]
return status
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 add_to_whitelist(self, name: str) -> bool:
"""
添加策略到白名单
Args:
name: 策略标识符
Returns:
是否添加成功
"""
if name not in self.strategies:
self.logger.error("❌ 策略不存在: %s", name)
return False
if self.whitelist_manager.add(name, enabled=True):
self.logger.info("✅ 添加到白名单: %s", name)
self._save_status()
return True
return False
def remove_from_whitelist(self, name: str) -> bool:
"""
从白名单移除策略
Args:
name: 策略标识符
Returns:
是否移除成功
"""
if self.whitelist_manager.remove(name):
self.logger.info("✅ 从白名单移除: %s", name)
self._save_status()
return True
return False
def set_whitelist_enabled(self, name: str, enabled: bool) -> bool:
"""
设置策略在白名单中的启用状态
Args:
name: 策略标识符
enabled: 是否启用
Returns:
是否设置成功
"""
if self.whitelist_manager.set_enabled(name, enabled):
self.logger.info("✅ 设置白名单状态: %s -> %s", name, enabled)
self._save_status()
return True
return False
def auto_start_whitelist_strategies(self) -> Dict[str, bool]:
"""
自动启动白名单中所有未运行的策略
一天只执行一次
Returns:
Dict[str, bool]: 每个策略的启动结果
"""
if not self.whitelist_manager.should_auto_start_today():
self.logger.info("⏰ 今天已经执行过自动启动,跳过")
return {}
self.logger.info("🚀 开始执行白名单自动启动...")
results = {}
whitelist = self.whitelist_manager.get_all()
for name, config in whitelist.items():
if not config.get("enabled", True):
self.logger.info("⏭️ 跳过禁用策略: %s", name)
continue
if name not in self.strategies:
self.logger.warning("⚠️ 策略不在系统中: %s", name)
continue
# 检查是否已在运行
if self._is_running(name):
self.logger.info("✅ 策略已在运行: %s", name)
results[name] = True
continue
# 尝试启动
self.logger.info("🚀 启动白名单策略: %s", name)
success = self.start_strategy(name)
# 记录启动结果
results[name] = success
if success:
self.logger.info("✅ 白名单策略启动成功: %s", name)
else:
self.logger.error("❌ 白名单策略启动失败: %s", name)
# 更新日期
self.whitelist_manager.update_last_auto_start_date(
datetime.now().date().isoformat()
)
# 统计结果
success_count = sum(1 for v in results.values() if v)
fail_count = len(results) - success_count
self.logger.info("📊 白名单自动启动完成: 成功 %d, 失败 %d", success_count, fail_count)
return results
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]")