feat(strategy_manager): 添加策略自动启动的白名单管理

实现全面的白名单管理系统,支持策略自动启动:

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

同时添加项目文档:
- 代码格式规则(缩进、行长度、导入、命名)
- 文件、变量、常量、函数、类的命名规范
- 受限制文件和目录的保护规则
This commit is contained in:
2026-01-26 01:21:46 +08:00
parent 1b5021d640
commit c0d996f39b
11 changed files with 1952 additions and 99 deletions

View File

@@ -0,0 +1,5 @@
{
"version": "1.0",
"last_auto_start_date": null,
"strategies": {}
}

View File

@@ -12,6 +12,7 @@ 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()
@@ -34,6 +35,10 @@ class StrategyManager:
# 配置管理器日志
self._setup_logger()
# 初始化白名单管理器
self.whitelist_manager = WhitelistManager()
self.logger.info("📋 白名单管理器已初始化")
self.strategies: Dict[str, Dict[str, Any]] = {}
self.logger.info("🔄 正在加载策略配置...")
self.load_strategies()
@@ -109,15 +114,31 @@ class StrategyManager:
self.logger.error("❌ 加载配置失败 %s: %s", config_file, e, exc_info=True)
def get_status(self) -> Dict[str, Any]:
"""获取完整状态"""
"""获取完整状态(包含白名单信息)"""
self._refresh_status()
return {
# 构建状态数据
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():
@@ -347,6 +368,121 @@ class StrategyManager:
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]):
"""格式化打印状态表格"""

View File

@@ -0,0 +1,345 @@
"""
白名单管理器 - 管理期望自动启动的策略列表
功能:
1. 持久化白名单配置到 JSON 文件
2. 支持添加/移除策略
3. 支持启用/禁用白名单中的策略
4. 跟踪自动启动日期,实现一天只尝试一次
"""
import json
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from datetime import datetime
class WhitelistManager:
"""白名单管理器"""
def __init__(self, config_path: str = "config/whitelist.json"):
"""
初始化白名单管理器
Args:
config_path: 白名单配置文件路径
"""
self.config_path = Path(config_path)
self.data = self._load()
self.logger = self._setup_logger()
def _setup_logger(self):
"""配置日志"""
import logging
logger = logging.getLogger("WhitelistManager")
return logger
def _load(self) -> Dict[str, Any]:
"""
加载白名单配置
Returns:
白名单配置数据
"""
if not self.config_path.exists():
# 返回默认配置
return {
"version": "1.0",
"last_auto_start_date": None,
"strategies": {}
}
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 确保数据结构完整
if "version" not in data:
data["version"] = "1.0"
if "strategies" not in data:
data["strategies"] = {}
return data
except Exception as e:
print(f"[WARNING] 加载白名单配置失败: {e}")
return {
"version": "1.0",
"last_auto_start_date": None,
"strategies": {}
}
def _save(self) -> bool:
"""
保存白名单配置
Returns:
是否保存成功
"""
try:
# 确保目录存在
self.config_path.parent.mkdir(exist_ok=True, parents=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(self.data, f, indent=2, ensure_ascii=False)
self.logger.debug("白名单配置已保存: %s", self.config_path)
return True
except Exception as e:
self.logger.error("保存白名单配置失败: %s", e)
return False
def add(self, strategy_key: str, enabled: bool = True) -> bool:
"""
添加策略到白名单
Args:
strategy_key: 策略标识符 (如 "DualModeTrendlineHawkesStrategy2_FG")
enabled: 是否启用自动启动
Returns:
是否添加成功False 表示已存在)
"""
if strategy_key in self.data["strategies"]:
self.logger.warning("策略已在白名单中: %s", strategy_key)
return False
self.data["strategies"][strategy_key] = {
"enabled": enabled,
"added_at": datetime.now().isoformat(),
"added_by": "web"
}
if self._save():
self.logger.info("添加策略到白名单: %s (enabled=%s)", strategy_key, enabled)
return True
return False
def remove(self, strategy_key: str) -> bool:
"""
从白名单移除策略
Args:
strategy_key: 策略标识符
Returns:
是否移除成功False 表示不存在)
"""
if strategy_key not in self.data["strategies"]:
self.logger.warning("策略不在白名单中: %s", strategy_key)
return False
del self.data["strategies"][strategy_key]
if self._save():
self.logger.info("从白名单移除策略: %s", strategy_key)
return True
return False
def set_enabled(self, strategy_key: str, enabled: bool) -> bool:
"""
设置策略在白名单中的启用状态
Args:
strategy_key: 策略标识符
enabled: 是否启用
Returns:
是否设置成功
"""
if strategy_key not in self.data["strategies"]:
self.logger.warning("策略不在白名单中: %s", strategy_key)
return False
self.data["strategies"][strategy_key]["enabled"] = enabled
self.data["strategies"][strategy_key]["updated_at"] = datetime.now().isoformat()
if self._save():
self.logger.info("设置白名单策略状态: %s -> %s", strategy_key, enabled)
return True
return False
def get_all(self) -> Dict[str, Dict[str, Any]]:
"""
获取所有白名单策略
Returns:
白名单策略字典 {strategy_key: {enabled, added_at, ...}}
"""
return self.data.get("strategies", {})
def get(self, strategy_key: str) -> Optional[Dict[str, Any]]:
"""
获取单个策略的白名单配置
Args:
strategy_key: 策略标识符
Returns:
策略配置或 None
"""
return self.data["strategies"].get(strategy_key)
def is_in_whitelist(self, strategy_key: str) -> bool:
"""
检查策略是否在白名单中
Args:
strategy_key: 策略标识符
Returns:
是否在白名单中
"""
return strategy_key in self.data.get("strategies", {})
def is_enabled_in_whitelist(self, strategy_key: str) -> bool:
"""
检查策略是否在白名单中且已启用
Args:
strategy_key: 策略标识符
Returns:
是否在白名单中且启用
"""
if strategy_key not in self.data.get("strategies", {}):
return False
return self.data["strategies"][strategy_key].get("enabled", True)
def update_last_auto_start_date(self, date_str: str) -> bool:
"""
更新最后自动启动日期
Args:
date_str: 日期字符串 (YYYY-MM-DD)
Returns:
是否更新成功
"""
self.data["last_auto_start_date"] = date_str
return self._save()
def get_last_auto_start_date(self) -> Optional[str]:
"""
获取最后自动启动日期
Returns:
日期字符串或 None
"""
return self.data.get("last_auto_start_date")
def should_auto_start_today(self) -> bool:
"""
检查今天是否应该自动启动
Returns:
今天是否应该自动启动
"""
today = datetime.now().date().isoformat()
last_date = self.data.get("last_auto_start_date")
if last_date is None:
return True
return last_date != today
def get_auto_start_status(self) -> Dict[str, Any]:
"""
获取自动启动状态
Returns:
自动启动状态信息
"""
today = datetime.now().date().isoformat()
last_date = self.data.get("last_auto_start_date")
return {
"should_auto_start": self.should_auto_start_today(),
"last_auto_start_date": last_date,
"is_first_run_today": last_date != today,
"whitelist_count": len(self.data.get("strategies", {})),
"enabled_count": sum(
1 for s in self.data.get("strategies", {}).values()
if s.get("enabled", True)
)
}
def clear(self) -> bool:
"""
清空白名单(谨慎使用)
Returns:
是否清空成功
"""
self.data["strategies"] = {}
self.data["last_auto_start_date"] = None
return self._save()
# ==================== 单元测试 ====================
if __name__ == "__main__":
import os
# 切换到 strategy_manager 目录
os.chdir(Path(__file__).parent.parent)
# 测试白名单管理器
print("=" * 60)
print("WhitelistManager 单元测试")
print("=" * 60)
# 创建测试用的白名单管理器
test_config_path = "config/whitelist_test.json"
wm = WhitelistManager(test_config_path)
# 测试添加
print("\n[测试] 添加策略到白名单...")
assert wm.add("TestStrategy1_FG") == True
assert wm.add("TestStrategy2_FG", enabled=False) == True
assert wm.add("TestStrategy1_FG") == False # 已存在
# 测试获取
print("\n[测试] 获取白名单...")
all_strategies = wm.get_all()
assert "TestStrategy1_FG" in all_strategies
assert "TestStrategy2_FG" in all_strategies
print(f"白名单策略: {list(all_strategies.keys())}")
# 测试状态检查
print("\n[测试] 状态检查...")
assert wm.is_in_whitelist("TestStrategy1_FG") == True
assert wm.is_in_whitelist("TestStrategy1_FG") == True
assert wm.is_in_whitelist("TestStrategy3_FG") == False
assert wm.is_enabled_in_whitelist("TestStrategy1_FG") == True
assert wm.is_enabled_in_whitelist("TestStrategy2_FG") == False
# 测试设置启用状态
print("\n[测试] 设置启用状态...")
assert wm.set_enabled("TestStrategy2_FG", True) == True
assert wm.is_enabled_in_whitelist("TestStrategy2_FG") == True
assert wm.set_enabled("TestStrategy3_FG", True) == False # 不存在
# 测试移除
print("\n[测试] 移除策略...")
assert wm.remove("TestStrategy2_FG") == True
assert wm.remove("TestStrategy2_FG") == False # 已移除
assert wm.is_in_whitelist("TestStrategy2_FG") == False
# 测试自动启动日期
print("\n[测试] 自动启动日期...")
today = datetime.now().date().isoformat()
assert wm.should_auto_start_today() == True
wm.update_last_auto_start_date(today)
assert wm.should_auto_start_today() == False
wm.update_last_auto_start_date("2024-01-01")
assert wm.should_auto_start_today() == True
# 清理测试文件
print("\n[测试] 清理...")
Path(test_config_path).unlink(missing_ok=True)
print("\n" + "=" * 60)
print("✅ 所有测试通过!")
print("=" * 60)

View File

@@ -14,10 +14,23 @@
<style>
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; }
#app { padding: 20px; max-width: 1200px; margin: 0 auto; }
#app { padding: 20px; max-width: 1400px; 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; }
/* 白名单状态标签 */
.whitelist-tag { cursor: pointer; }
.whitelist-tag:hover { opacity: 0.8; }
/* 统计卡片 */
.stats-row { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; }
.stat-card { flex: 1; min-width: 150px; background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.stat-card h4 { margin: 0 0 8px 0; font-size: 13px; color: #666; font-weight: normal; }
.stat-card .value { font-size: 28px; font-weight: bold; color: #333; }
.stat-card.running .value { color: #27ae60; }
.stat-card.stopped .value { color: #e74c3c; }
.stat-card.whitelist .value { color: #9b59b6; }
</style>
</head>
<body>
@@ -33,89 +46,173 @@
</div>
<script>
const { createApp, ref, onMounted, onUnmounted, watch } = Vue;
const { createApp, ref, onMounted, onUnmounted, watch, computed } = Vue;
const naive = window.naive;
// --- 主组件逻辑 ---
const MainLayout = {
template: `
<div class="header">
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
<n-space align="center">
<!-- [核心修改] 1. 显示 Git 版本信息 -->
<n-tag :bordered="false" type="default" size="small">
📦 Version: {{ gitInfo }}
</n-tag>
<div>
<div class="header">
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
<n-space align="center">
<!-- Git 版本信息 -->
<n-tag :bordered="false" type="default" size="small">
📦 Version: {{ gitInfo }}
</n-tag>
<!-- 刷新频率选择器 -->
<n-select
v-model:value="refreshInterval"
:options="intervalOptions"
size="small"
style="width: 130px"
></n-select>
<!-- 刷新频率选择器 -->
<n-select
v-model:value="refreshInterval"
:options="intervalOptions"
size="small"
style="width: 130px"
></n-select>
<!-- 手动刷新按钮 -->
<n-button type="primary" size="small" @click="fetchStatus" :loading="loading">
刷新状态
</n-button>
<!-- 手动刷新按钮 -->
<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-tag type="info" size="small">更新于: {{ lastUpdated }}</n-tag>
</n-space>
</template>
</n-modal>
</div>
<!-- 统计卡片 -->
<div class="stats-row">
<div class="stat-card">
<h4>策略总数</h4>
<div class="value">{{ Object.keys(strategies).length }}</div>
</div>
<div class="stat-card running">
<h4>运行中</h4>
<div class="value">{{ runningCount }}</div>
</div>
<div class="stat-card stopped">
<h4>已停止</h4>
<div class="value">{{ stoppedCount }}</div>
</div>
<div class="stat-card whitelist">
<h4>白名单策略</h4>
<div class="value">{{ whitelistCount }}</div>
</div>
</div>
<!-- 白名单管理工具栏 -->
<n-card title="🛠️ 批量操作" hoverable style="margin-bottom: 20px;">
<n-space wrap>
<n-button type="success" size="small" @click="batchStart" :disabled="selectedKeys.length === 0">
启动选中
</n-button>
<n-button type="error" size="small" @click="batchStop" :disabled="selectedKeys.length === 0">
停止选中
</n-button>
<n-button type="warning" size="small" @click="batchRestart" :disabled="selectedKeys.length === 0">
重启选中
</n-button>
<n-divider vertical />
<n-button type="primary" size="small" @click="batchAddToWhitelist" :disabled="selectedKeys.length === 0">
添加到白名单
</n-button>
<n-button type="info" size="small" @click="batchRemoveFromWhitelist" :disabled="selectedKeys.length === 0">
从白名单移除
</n-button>
<n-button type="success" size="small" @click="batchEnableInWhitelist" :disabled="selectedKeys.length === 0">
启用白名单
</n-button>
<n-button type="warning" size="small" @click="batchDisableInWhitelist" :disabled="selectedKeys.length === 0">
禁用白名单
</n-button>
<n-divider vertical />
<n-button type="warning" size="small" @click="triggerAutoStart">
🚀 手动触发自动启动
</n-button>
<n-tag type="info" size="small">
今日已自动启动: {{ whitelistAutoStarted ? '是' : '否' }}
</n-tag>
</n-space>
</n-card>
<!-- 策略列表 -->
<n-card title="📋 策略列表" hoverable>
<template #header-extra>
<n-space>
<n-button text @click="selectAll" size="small">全选</n-button>
<n-button text @click="clearSelection" size="small">清空</n-button>
</n-space>
</template>
<n-table :single-line="false" striped>
<thead>
<tr>
<th style="width: 40px;">
<n-checkbox :checked="allSelected" :indeterminate="partialSelected" @update:checked="toggleSelectAll" />
</th>
<th>策略标识</th>
<th>策略名称</th>
<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" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
<td>
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
</td>
<td><strong>{{ key }}</strong></td>
<td>{{ info.config.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>
<n-tag :type="info.in_whitelist ? 'success' : 'default'" size="small" class="whitelist-tag"
@click="toggleWhitelist(key)">
{{ info.in_whitelist ? '✓ 在白名单' : '✗ 不在' }}
</n-tag>
</td>
<td>
<n-tag v-if="info.in_whitelist" :type="info.whitelist_enabled ? 'success' : 'warning'" size="small">
{{ info.whitelist_enabled ? '启用' : '禁用' }}
</n-tag>
<span v-else style="color: #999;">-</span>
</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="9" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
</tr>
</tbody>
</n-table>
</n-card>
<!-- 日志弹窗 -->
<n-modal v-model:show="showLogModal" style="width: 900px;" 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" :class="getLogClass(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>
</div>
`,
setup() {
const message = naive.useMessage();
@@ -124,10 +221,12 @@
const strategies = ref({});
const loading = ref(false);
const lastUpdated = ref('-');
// [核心修改] 2. 为 Git 信息创建一个 ref
const gitInfo = ref('Loading...');
// 白名单相关
const whitelistAutoStarted = ref(false);
const selectedKeys = ref([]);
const refreshInterval = ref(0);
const intervalOptions = [
{ label: '✋ 仅手动', value: 0 },
@@ -137,6 +236,14 @@
];
let timer = null;
// 计算属性
const runningCount = computed(() => Object.values(strategies.value).filter(s => s.status === 'running').length);
const stoppedCount = computed(() => Object.values(strategies.value).filter(s => s.status === 'stopped').length);
const whitelistCount = computed(() => Object.values(strategies.value).filter(s => s.in_whitelist).length);
const allSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length === Object.keys(strategies.value).length);
const partialSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length < Object.keys(strategies.value).length);
const fetchStatus = async () => {
if (loading.value) return;
loading.value = true;
@@ -145,11 +252,12 @@
if (!res.ok) throw new Error("Error");
const data = await res.json();
strategies.value = data.strategies;
// [核心修改] 3. 更新 Git 信息
gitInfo.value = data.git_info || 'N/A';
whitelistAutoStarted.value = !data.whitelist_auto_start_today;
lastUpdated.value = new Date().toLocaleTimeString();
// 清理已删除策略的选中状态
selectedKeys.value = selectedKeys.value.filter(k => k in strategies.value);
} catch (e) {
message.error("连接服务器失败");
} finally {
@@ -174,6 +282,33 @@
}
});
// 选择操作
const toggleSelect = (key) => {
const idx = selectedKeys.value.indexOf(key);
if (idx >= 0) {
selectedKeys.value.splice(idx, 1);
} else {
selectedKeys.value.push(key);
}
};
const toggleSelectAll = (checked) => {
if (checked) {
selectedKeys.value = Object.keys(strategies.value);
} else {
selectedKeys.value = [];
}
};
const selectAll = () => {
selectedKeys.value = Object.keys(strategies.value);
};
const clearSelection = () => {
selectedKeys.value = [];
};
// 策略操作
const handleAction = (name, action) => {
const map = { start: '启动', stop: '停止', restart: '重启' };
dialog.warning({
@@ -195,24 +330,179 @@
});
};
// 批量操作
const batchAction = async (action, apiEndpoint) => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/${apiEndpoint}/${name}/${action}`, { method: 'POST' });
if (res.ok) {
results.push(name);
}
} catch (e) {
console.error(`操作失败: ${name}`, e);
}
}
if (results.length > 0) {
message.success(`成功操作 ${results.length} 个策略`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchStart = () => batchAction('start', 'api/strategy');
const batchStop = () => batchAction('stop', 'api/strategy');
const batchRestart = () => batchAction('restart', 'api/strategy');
// 白名单操作
const batchAddToWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已添加到白名单: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchRemoveFromWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已从白名单移除: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchEnableInWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/enable`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已启用: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchDisableInWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/disable`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已禁用: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const toggleWhitelist = async (name) => {
const info = strategies.value[name];
if (!info) return;
try {
if (info.in_whitelist) {
// 在白名单中,询问是否移除
dialog.warning({
title: '白名单操作',
content: `${name} 已在白名单中,是否移除?`,
positiveText: '移除',
negativeText: '取消',
onPositiveClick: async () => {
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
if (res.ok) {
message.success("已从白名单移除");
fetchStatus();
}
}
});
} else {
// 不在白名单中,询问是否添加
dialog.info({
title: '白名单操作',
content: `${name} 添加到白名单?`,
positiveText: '添加',
negativeText: '取消',
onPositiveClick: async () => {
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
if (res.ok) {
message.success("已添加到白名单");
fetchStatus();
}
}
});
}
} catch (e) {
message.error("操作失败");
}
};
const triggerAutoStart = async () => {
try {
const res = await fetch('/api/whitelist/auto-start', { method: 'POST' });
const data = await res.json();
if (data.success) {
message.success(`自动启动完成: 成功 ${data.success_count}, 失败 ${data.fail_count}`);
fetchStatus();
} else {
message.warning("今天已执行过或无需启动");
}
} catch (e) {
message.error("自动启动失败");
}
};
// 日志相关
const showLogModal = ref(false);
const currentLogKey = ref('');
const logLines = ref([]);
const logLoading = ref(false);
const getLogClass = (line) => {
if (line.includes('[ERROR]') || line.includes('❌')) return 'error';
if (line.includes('[WARNING]') || line.includes('⚠️')) return 'warning';
if (line.includes('[INFO]') || line.includes('✅')) return 'info';
if (line.includes('成功') || line.includes('success')) return 'success';
return '';
};
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;
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;
@@ -230,11 +520,21 @@
});
return {
strategies, loading, lastUpdated,
gitInfo, // [核心修改] 4. 将 gitInfo 暴露给模板
strategies, loading, lastUpdated, gitInfo,
refreshInterval, intervalOptions,
showLogModal, currentLogKey, logLines, logLoading,
fetchStatus, handleAction, viewLogs, fetchLogs
fetchStatus, handleAction, viewLogs, fetchLogs, getLogClass,
// 白名单
whitelistAutoStarted,
selectedKeys,
runningCount, stoppedCount, whitelistCount,
allSelected, partialSelected,
toggleSelect, toggleSelectAll, selectAll, clearSelection,
batchStart, batchStop, batchRestart,
batchAddToWhitelist, batchRemoveFromWhitelist,
batchEnableInWhitelist, batchDisableInWhitelist,
toggleWhitelist, triggerAutoStart
};
}
};
@@ -256,4 +556,4 @@
</script>
</body>
</html>
</html>

View File

@@ -27,18 +27,138 @@ def main():
# 查看日志最近50行
python start.py logs -n DualModeTrendlineHawkesStrategy2_FG -t 50
# ========== 白名单管理 ==========
# 查看白名单
python start.py whitelist
# 添加策略到白名单
python start.py whitelist add -n DualModeTrendlineHawkesStrategy2_FG
# 从白名单移除策略
python start.py whitelist remove -n DualModeTrendlineHawkesStrategy2_FG
# 启用白名单中的策略
python start.py whitelist enable -n DualModeTrendlineHawkesStrategy2_FG
# 禁用白名单中的策略
python start.py whitelist disable -n DualModeTrendlineHawkesStrategy2_FG
# 手动触发白名单自动启动
python start.py whitelist auto-start
"""
)
parser.add_argument("action", choices=["start", "stop", "restart", "status", "logs"])
parser.add_argument("action", choices=["start", "stop", "restart", "status", "logs", "whitelist"])
parser.add_argument("-n", "--name", help="策略标识符策略名_品种")
parser.add_argument("-a", "--all", action="store_true", help="对所有策略执行操作")
parser.add_argument("-c", "--config", default="config/main.json", help="主配置文件路径")
parser.add_argument("-t", "--tail", type=int, default=30, help="查看日志末尾行数")
# 白名单子命令
whitelist_group = parser.add_argument_group("白名单操作")
whitelist_group.add_argument("whitelist_action", choices=["list", "add", "remove", "enable", "disable", "auto-start"],
help="白名单操作动作", nargs="?")
args = parser.parse_args()
manager = StrategyManager(args.config)
# 白名单管理
if args.action == "whitelist":
if args.whitelist_action == "list" or args.whitelist_action is None:
# 列出白名单
print("\n" + "=" * 80)
print("📋 白名单列表")
print("=" * 80)
whitelist = manager.whitelist_manager.get_all()
auto_status = manager.whitelist_manager.get_auto_start_status()
print(f"白名单策略总数: {auto_status['whitelist_count']}")
print(f"已启用策略数: {auto_status['enabled_count']}")
print(f"今天已自动启动: {'' if not auto_status['should_auto_start'] else ''}")
print(f"上次自动启动日期: {auto_status['last_auto_start_date'] or '从未'}")
print("-" * 80)
if not whitelist:
print("白名单为空")
else:
print(f"{'策略标识':<45} {'状态':<10} {'添加时间'}")
print("-" * 80)
for name, config in whitelist.items():
status = "启用" if config.get("enabled", True) else "禁用"
added_at = config.get("added_at", "")[:19]
print(f"{name:<45} {status:<10} {added_at}")
print("=" * 80)
elif args.whitelist_action == "add":
# 添加到白名单
if not args.name:
print("❌ 错误: 添加操作必须指定策略名称 (-n)")
sys.exit(1)
if manager.add_to_whitelist(args.name):
print(f"✅ 成功添加到白名单: {args.name}")
else:
print(f"❌ 添加失败,策略可能已存在: {args.name}")
sys.exit(1)
elif args.whitelist_action == "remove":
# 从白名单移除
if not args.name:
print("❌ 错误: 移除操作必须指定策略名称 (-n)")
sys.exit(1)
if manager.remove_from_whitelist(args.name):
print(f"✅ 成功从白名单移除: {args.name}")
else:
print(f"❌ 移除失败,策略可能不在白名单中: {args.name}")
sys.exit(1)
elif args.whitelist_action == "enable":
# 启用白名单中的策略
if not args.name:
print("❌ 错误: 启用操作必须指定策略名称 (-n)")
sys.exit(1)
if manager.set_whitelist_enabled(args.name, True):
print(f"✅ 已启用: {args.name}")
else:
print(f"❌ 启用失败,策略可能不在白名单中: {args.name}")
sys.exit(1)
elif args.whitelist_action == "disable":
# 禁用白名单中的策略
if not args.name:
print("❌ 错误: 禁用操作必须指定策略名称 (-n)")
sys.exit(1)
if manager.set_whitelist_enabled(args.name, False):
print(f"✅ 已禁用: {args.name}")
else:
print(f"❌ 禁用失败,策略可能不在白名单中: {args.name}")
sys.exit(1)
elif args.whitelist_action == "auto-start":
# 手动触发自动启动
print("\n" + "=" * 80)
print("🚀 手动触发白名单自动启动")
print("=" * 80)
results = manager.auto_start_whitelist_strategies()
if not results:
print("⚠️ 今天已执行过自动启动或白名单为空")
else:
success_count = sum(1 for v in results.values() if v)
fail_count = len(results) - success_count
print(f"📊 结果: 成功 {success_count}, 失败 {fail_count}")
for name, success in results.items():
status = "✅ 成功" if success else "❌ 失败"
print(f" {status}: {name}")
print("=" * 80)
return
# 原有逻辑
if args.action == "status":
status = manager.get_status()
print_status_table(status)

View File

@@ -6,6 +6,7 @@ import logging
from collections import deque
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -63,7 +64,7 @@ def get_git_commit_info():
logger.warning(f"无法获取 Git 提交信息: {e}")
return "获取 Git 信息失败"
# ================== 定时任务逻辑 (保持不变) ==================
# ================== 定时任务逻辑 ==================
def scheduled_restart_task():
"""
@@ -94,10 +95,39 @@ def scheduled_restart_task():
logger.info("⏰ [定时任务] 自动重启流程结束")
# ================== FastAPI 事件钩子 (保持不变) ==================
def scheduled_whitelist_auto_start():
"""
定时任务:白名单自动启动
仅在 08:58 执行
"""
logger.info("⏰ [白名单定时任务] 触发白名单自动启动...")
@app.on_event("startup")
async def start_scheduler():
results = manager.auto_start_whitelist_strategies()
if not results:
logger.info("⏰ [白名单定时任务] 今天已执行过或无需启动")
return
success_count = sum(1 for v in results.values() if v)
fail_count = len(results) - success_count
logger.info(f"⏰ [白名单定时任务] 完成: 成功 {success_count}, 失败 {fail_count}")
for name, success in results.items():
if success:
logger.info(f"✅ [白名单定时任务] {name} 启动成功")
else:
logger.error(f"❌ [白名单定时任务] {name} 启动失败")
# ================== FastAPI 生命周期事件 (使用 lifespan 替代废弃的 on_event) ==================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
FastAPI 生命周期管理器,替代废弃的 @app.on_event 装饰器
"""
# 原有重启任务 (08:58, 20:58)
scheduler.add_job(
scheduled_restart_task,
CronTrigger(hour=8, minute=58),
@@ -110,13 +140,29 @@ async def start_scheduler():
id="restart_evening",
replace_existing=True
)
# 新增:白名单自动启动任务(仅 08:58
scheduler.add_job(
scheduled_whitelist_auto_start,
CronTrigger(hour=8, minute=58),
id="whitelist_auto_start",
replace_existing=True
)
scheduler.start()
logger.info("📅 定时任务调度器已启动 (计划时间: 08:58, 20:58)")
logger.info("📅 定时任务调度器已启动")
logger.info(" - 重启任务: 08:58, 20:58")
logger.info(" - 白名单自动启动: 08:58")
yield
@app.on_event("shutdown")
async def stop_scheduler():
# 应用关闭时执行
scheduler.shutdown()
logger.info("📅 定时任务调度器已关闭")
# ================== 初始化 ==================
app = FastAPI(title="策略控制台", lifespan=lifespan)
# ================== API 路由 ==================
@@ -168,9 +214,66 @@ def get_logs(name: str, lines: int = Query(50, le=500)):
raise HTTPException(500, f"读取日志失败: {e}")
# ================== 静态文件挂载 (保持不变) ==================
# 注意: 这里的路径是示例,请确保它与你的项目结构匹配
# 假设你的HTML文件在 "frontend/" 目录下
# ================== 白名单管理 API ==================
@app.get("/api/whitelist")
def get_whitelist():
"""获取白名单列表"""
whitelist = manager.whitelist_manager.get_all()
auto_start_status = manager.whitelist_manager.get_auto_start_status()
return {
"whitelist": whitelist,
"auto_start_status": auto_start_status
}
@app.post("/api/whitelist/{name}/add")
def add_to_whitelist(name: str):
"""添加策略到白名单"""
if manager.add_to_whitelist(name):
return {"success": True, "message": f"已添加到白名单: {name}"}
raise HTTPException(400, f"添加失败,策略可能已存在: {name}")
@app.post("/api/whitelist/{name}/remove")
def remove_from_whitelist(name: str):
"""从白名单移除策略"""
if manager.remove_from_whitelist(name):
return {"success": True, "message": f"已从白名单移除: {name}"}
raise HTTPException(400, f"移除失败,策略可能不在白名单中: {name}")
@app.post("/api/whitelist/{name}/enable")
def enable_in_whitelist(name: str):
"""启用白名单中的策略"""
if manager.set_whitelist_enabled(name, True):
return {"success": True, "message": f"已启用: {name}"}
raise HTTPException(400, f"操作失败,策略可能不在白名单中: {name}")
@app.post("/api/whitelist/{name}/disable")
def disable_in_whitelist(name: str):
"""禁用白名单中的策略"""
if manager.set_whitelist_enabled(name, False):
return {"success": True, "message": f"已禁用: {name}"}
raise HTTPException(400, f"操作失败,策略可能不在白名单中: {name}")
@app.post("/api/whitelist/auto-start")
def trigger_auto_start():
"""手动触发白名单自动启动(用于测试)"""
results = manager.auto_start_whitelist_strategies()
return {
"success": True,
"results": results,
"count": len(results),
"success_count": sum(1 for v in results.values() if v),
"fail_count": sum(1 for v in results.values() if not v)
}
# ================== 静态文件挂载 ==================
# 服务前端构建文件
app.mount("/static", StaticFiles(directory="frontend/dist"), name="static")
@app.get("/")