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,96 @@
# 代码格式规则 (Formatting Rules)
本项目遵循以下代码格式规范:
## 1. Python 代码规范
### 1.1 缩进
- 使用 **4个空格** 进行缩进
- 不使用 Tab 键
### 1.2 行长度
- 每行代码最大长度:**100 字符**
- 文档字符串最大长度:**80 字符**
### 1.3 空行
- 类定义之间:**2个空行**
- 方法定义之间:**1个空行**
- 函数定义之间:**2个空行**
- 逻辑段落之间:**1个空行**
### 1.4 导入顺序
```python
# 标准库导入
import os
import sys
from datetime import datetime
# 第三方库导入
import numpy as np
import pandas as pd
# 本地项目导入
from src.backtest_engine import BacktestEngine
from src.data_manager import DataManager
```
### 1.5 空格使用
- 在逗号后添加空格:`func(a, b, c)`
- 在运算符两侧添加空格:`a + b`
- 不要在括号内添加空格:`func(a)` 而不是 `func( a )`
- 函数参数列表内不要有多余空格
## 2. 命名规范
### 2.1 变量命名
- 使用 **小写字母 + 下划线**`trade_volume`, `stop_loss_points`
- 私有变量使用前缀下划线:`_internal_cache`
### 2.2 常量命名
- 使用 **全大写 + 下划线**`MAX_POSITION`, `DEFAULT_STOP_LOSS`
### 2.3 函数命名
- 使用 **小写字母 + 下划线**`calculate_metrics()`, `get_bar_history()`
### 2.4 类命名
- 使用 **大驼峰命名法 (PascalCase)**`SimpleLimitBuyStrategy`, `BacktestEngine`
## 3. 文档字符串规范
### 3.1 所有公共类和方法必须有文档字符串
```python
class SimpleLimitBuyStrategy(Strategy):
"""
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
具备以下特点:
- 每根K线开始时取消上一根K线未成交的订单
- 最多只能有一个开仓挂单和一个持仓
- 包含简单的止损和止盈逻辑
Args:
context: 模拟器实例
symbol: 交易合约代码
trade_volume: 单笔交易量
"""
```
### 3.2 复杂逻辑必须添加注释说明
## 4. 类型提示规范
- 推荐为函数参数和返回值添加类型提示
- 使用 `from typing` 导入类型
- 循环导入时使用 `TYPE_CHECKING`
```python
from typing import Dict, Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from src.backtest_engine import BacktestContext
```
## 5. Git 提交规范
- 提交信息使用中文或英文
- 格式:`[类型] 描述`
- 类型feat, fix, docs, style, refactor, test, chore

View File

@@ -0,0 +1,129 @@
# 命名约定规则 (Naming Conventions)
本项目定义了一套统一的命名约定,以确保代码库的一致性和可读性。
## 1. 文件命名规范
### 1.1 Python 文件
- 使用 **小写字母 + 下划线**`backtest_engine.py`, `data_manager.py`
- 避免使用连字符或空格
- 测试文件以 `test_` 前缀开头:`test_backtest_engine.py`
### 1.2 配置文件
- 使用小写字母和下划线:`config.json`, `strategy_params.yaml`
- 环境配置文件使用 `.env``.env.{environment}` 格式
### 1.3 数据文件
- 遵循 `{数据类型}_{日期范围}.{扩展名}` 格式
- 示例:`btc_ohlcv_2023_2024.csv`, `strategy_results_202401.json`
## 2. 变量命名规范
### 2.1 普通变量
- 使用 **小写字母 + 下划线**snake_case
- 使用描述性名称,避免缩写
- ✅ 正确示例:`current_price`, `trade_volume`, `stop_loss_price`
- ❌ 错误示例:`cp`, `tv`, `sl`
### 2.2 布尔变量
- 使用 `is_`, `has_`, `are_` 等前缀
- ✅ 正确示例:`is_valid`, `has_position`, `are_orders_filled`
- ❌ 错误示例:`valid`, `position`, `filled`
### 2.3 私有变量
- 使用单下划线前缀(约定俗成的私有)
- ✅ 正确示例:`_internal_cache`, `_last_order_id`
- ❌ 错误示例:`__private_var`(除非确实需要 name mangling
### 2.4 临时变量
- 循环变量可使用单字母或简短名称
- ✅ 正确示例:`for i in range(n):`, `for bar in bars:`
## 3. 常量命名规范
### 3.1 全局常量
- 使用 **全大写字母 + 下划线**SCREAMING_SNAKE_CASE
- 定义在文件顶部或单独的 `constants.py` 模块中
- ✅ 正确示例:`MAX_POSITION = 5`, `DEFAULT_COMMISSION_RATE = 0.0003`
- ❌ 错误示例:`max_position`, `default_commission_rate`
### 3.2 配置常量
- 使用全大写字母和下划线
- ✅ 正确示例:`SYMBOL_FUTURES_SUFFIX = ".FG"`
## 4. 函数命名规范
### 4.1 普通函数
- 使用 **小写字母 + 下划线**snake_case
- 函数名应清晰表达其功能
- ✅ 正确示例:`calculate_metrics()`, `get_bar_history()`, `send_order()`
- ❌ 错误示例:`calc()`, `getData()`, `send()`
### 4.2 返回布尔值的函数
- 使用 `is_`, `has_`, `are_` 等前缀
- ✅ 正确示例:`is_trending_up()`, `has_active_orders()`, `are_positions_valid()`
### 4.3 私有函数
- 使用单下划线前缀
- ✅ 正确示例:`_validate_params()`, `_calculate_pnl()`
## 5. 类命名规范
### 5.1 公共类
- 使用 **大驼峰命名法**PascalCase
- ✅ 正确示例:`BacktestEngine`, `SimpleLimitBuyStrategy`, `DataManager`
- ❌ 错误示例:`backtest_engine`, `simple_limit_buy_strategy`
### 5.2 抽象基类
- 使用 `ABC` 后缀或 `Base` 前缀
- ✅ 正确示例:`Strategy(ABC)`, `BaseIndicator`
### 5.3 异常类
- 使用 `Error``Exception` 后缀
- ✅ 正确示例:`BacktestError`, `OrderExecutionException`
## 6. 特定领域命名规范
### 6.1 交易相关
- 方向:`BUY`, `SELL`, `CLOSE_LONG`, `CLOSE_SHORT`
- 订单类型:`LIMIT`, `MARKET`, `STOP`
- 持仓:`LONG`, `SHORT`, `FLAT`
### 6.2 策略参数
- 使用描述性名称,包含参数含义
- ✅ 正确示例:`stop_loss_points`, `take_profit_points`, `range_factor`
- ❌ 错误示例:`sl`, `tp`, `rf`
### 6.3 K线数据
- 时间周期:`1m`, `5m`, `15m`, `1h`, `4h`, `1d`
- OHLCV`open`, `high`, `low`, `close`, `volume`
## 7. 数据库/缓存命名规范
### 7.1 缓存键
- 使用冒号分隔的层次结构
- ✅ 正确示例:`strategy:rb:positions`, `market:btc:price`
### 7.2 日志文件
- 使用 `{策略名}/{品种}.log` 格式
- ✅ 正确示例:`logs/SpectralTrendStrategy/rb.log`
## 8. 命名一致性检查清单
在提交代码前,请确认以下检查项:
- [ ] 所有变量名使用 snake_case
- [ ] 所有类名使用 PascalCase
- [ ] 所有常量使用 SCREAMING_SNAKE_CASE
- [ ] 函数名清晰表达功能
- [ ] 命名具有描述性,避免模糊缩写
- [ ] 私有成员使用下划线前缀
- [ ] 布尔变量使用适当的前缀
## 9. 命名反模式(应避免)
- ❌ 使用单个字母 `l`, `O`, `I` 作为变量名(容易与数字 1, 0 混淆)
- ❌ 使用魔法数字或字符串(应定义为常量)
- ❌ 使用不一致的命名风格混合
- ❌ 使用过于通用的名称如 `data`, `info`, `temp`
- ❌ 使用项目保留名称如 `strategy`, `engine`, `manager`

View File

@@ -0,0 +1,126 @@
# 受限文件规则 (Restricted Files Rules)
本项目定义了以下受限文件和目录,这些内容 **禁止访问、不允许访问、更不允许修改**
## 1. 受限目录列表
### 1.1 策略目录 - `futures_trading_strategies/`
**状态:** 🚫 **完全禁止访问**
- 目录路径:`futures_trading_strategies/`
- 说明:包含所有期货交易策略的历史版本和实验代码
- 禁止操作:
- ❌ 读取文件内容
- ❌ 修改文件内容
- ❌ 删除文件
- ❌ 创建新文件
- ❌ 查看目录结构
### 1.2 策略源文件目录 - `src/strategies/`
**状态:** 🚫 **完全禁止访问**
- 目录路径:`src/strategies/`
- 说明:包含所有核心策略实现文件
- 包含内容:
- `base_strategy.py` - 策略基类
- `SimpleLimitBuyStrategy.py` - 简单限价买入策略
- `OpenTwoFactorStrategy.py` - 双因子开仓策略
- `OpenTwoFactorStrategyDouble.py` - 双因子开仓策略(双倍版本)
- `smc_pure_h1_long_strategy.py` - SMC H1 长线策略
- `utils.py` - 工具函数
- 以及其他所有策略文件和子目录
- 禁止操作:
- ❌ 读取文件内容
- ❌ 修改文件内容
- ❌ 删除文件
- ❌ 创建新文件
- ❌ 查看目录结构
### 1.3 策略管理目录 - `strategy_manager/`
**状态:** ⚠️ **部分禁止访问**
- 目录路径:`strategy_manager/`
- 说明:包含策略管理和运行的核心组件
- **子目录 `strategy_manager/strategies/` 完全禁止访问** 🚫
- 其他文件和目录可以正常访问
**策略配置子目录(禁止访问):**
- `strategy_manager/strategies/` - 策略配置文件目录
- ❌ 读取文件内容
- ❌ 修改文件内容
- ❌ 删除文件
- ❌ 创建新文件
- ❌ 查看目录结构
**其他可访问的目录和文件:**
- `strategy_manager/launcher.py` - 策略启动器 ✅
- `strategy_manager/start.py` - 启动脚本 ✅
- `strategy_manager/restart_daemon.py` - 重启守护进程 ✅
- `strategy_manager/web_backend.py` - Web后端 ✅
- `strategy_manager/status.json` - 状态文件 ✅
- `strategy_manager/config/` - 配置目录 ✅
- `strategy_manager/core/` - 核心模块 ✅
- `strategy_manager/logs/` - 日志目录 ✅
- `strategy_manager/pids/` - PID目录 ✅
## 2. 特殊文件保护
### 2.1 策略核心文件
以下文件受到特别保护:
- `src/strategies/base_strategy.py` - 策略基类,包含邮件通知等核心功能
- `src/strategies/utils.py` - 策略工具函数
- `strategy_manager/launcher.py` - 策略启动器
- `strategy_manager/web_backend.py` - Web后端服务
### 2.2 配置文件保护
- `strategy_manager/config/` 目录下的所有配置文件
- `strategy_manager/status.json` - 策略状态文件
## 3. 违规处理
### 3.1 自动拒绝
系统将自动拒绝任何对受限目录和文件的访问尝试,包括:
- 读取操作 (read_file)
- 写入操作 (write_to_file, edit_file)
- 搜索操作 (search_files)
- 列表操作 (list_files)
- 删除操作 (delete_file)
**重要说明:** 即使在对话中用户主动提供了受限文件的内容AI 助手也不应该读取、分析或引用这些内容。所有受限文件的内容都应被视为不可访问的机密信息。
### 3.2 错误提示
当尝试访问受限资源时,系统将返回明确的错误信息:
```
错误:尝试访问受限资源 [文件/目录路径]
原因:此文件/目录被标记为受保护
建议:请勿尝试访问或修改这些核心策略文件
```
## 4. 例外情况
以下情况需要特殊授权:
- 紧急修复生产环境问题
- 安全漏洞修补
- 性能优化(需要详细审查)
**注意:** 即使在例外情况下,也必须先获得项目管理员的明确授权。
## 5. 规则说明
这些限制规则的目的是:
1. **保护策略资产**:防止核心交易策略被意外或恶意修改
2. **确保系统稳定性**:避免因修改关键文件导致系统崩溃
3. **维护代码质量**:确保只有经过充分测试的代码才能部署
4. **分离关注点**:开发工作与生产环境严格分离
**警告:** 违反这些规则可能导致:
- 策略失效
- 资金损失
- 系统故障
- 项目责任追究
# Restricted files
Files in the list contain sensitive data, they MUST NOT be read
- strategy_manager/strategies/
- futures_trading_strategies/
- src/strategies/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -0,0 +1,493 @@
# 策略管理优化方案
## 1. 需求概述
### 1.1 问题背景
- 策略启动后如果出现异常会停止进程
- 需要机制确保重要策略能够自动重启
### 1.2 优化目标
1. **白名单机制**: 本地文件保存希望自动启动的策略列表
2. **Web管理**: 界面可以管理白名单策略
3. **定时启动**: 每个工作日 8:58 自动尝试启动白名单中未运行的策略
4. **单日单次**: 每天只尝试一次,避免重复尝试
5. **手动控制**: 手动停止后不自动重启,需要用户重新操作
6. **双状态展示**: 每个策略显示"是否在白名单"和"是否运行中"
---
## 2. 系统架构
### 2.1 新增/修改文件
| 文件 | 操作 | 说明 |
|------|------|------|
| `config/whitelist.json` | 新增 | 白名单配置文件 |
| `core/whitelist_manager.py` | 新增 | 白名单管理器 |
| `core/manager.py` | 修改 | 集成白名单功能 |
| `web_backend.py` | 修改 | 新增白名单管理 API |
| `frontend/` | 修改 | 前端界面添加白名单管理 |
### 2.2 核心数据结构
#### 白名单配置 (`config/whitelist.json`)
```json
{
"version": "1.0",
"last_auto_start_date": "2024-01-25",
"strategies": {
"DualModeTrendlineHawkesStrategy2_FG": {
"enabled": true,
"added_at": "2024-01-25T10:00:00",
"added_by": "web"
},
"SpectralTrendStrategy_rb": {
"enabled": true,
"added_at": "2024-01-25T10:00:00",
"added_by": "web"
}
}
}
```
#### 策略状态结构扩展
```python
# 在 StrategyManager.strategies 字典中新增字段
{
"strategy_key": {
# ... 现有字段 ...
"in_whitelist": True, # 是否在白名单中
"whitelist_enabled": True, # 白名单中是否启用
"last_auto_start_attempt": "2024-01-25T08:58:00", # 上次自动启动尝试
"auto_start_success": False # 上次自动启动是否成功
}
}
```
---
## 3. 详细设计
### 3.1 白名单管理器 (`core/whitelist_manager.py`)
```python
class WhitelistManager:
"""白名单管理器"""
def __init__(self, config_path: str = "config/whitelist.json"):
self.config_path = Path(config_path)
self.data = self._load()
def _load(self) -> Dict:
"""加载白名单配置"""
if not self.config_path.exists():
return {"version": "1.0", "strategies": {}}
with open(self.config_path, 'r') as f:
return json.load(f)
def _save(self):
"""保存白名单配置"""
self.config_path.parent.mkdir(exist_ok=True)
with open(self.config_path, 'w') as f:
json.dump(self.data, f, indent=2, ensure_ascii=False)
def add(self, strategy_key: str, enabled: bool = True) -> bool:
"""添加策略到白名单"""
if strategy_key in self.data["strategies"]:
return False # 已存在
self.data["strategies"][strategy_key] = {
"enabled": enabled,
"added_at": datetime.now().isoformat(),
"added_by": "web"
}
self._save()
return True
def remove(self, strategy_key: str) -> bool:
"""从白名单移除策略"""
if strategy_key not in self.data["strategies"]:
return False
del self.data["strategies"][strategy_key]
self._save()
return True
def set_enabled(self, strategy_key: str, enabled: bool) -> bool:
"""设置策略在白名单中的启用状态"""
if strategy_key not in self.data["strategies"]:
return False
self.data["strategies"][strategy_key]["enabled"] = enabled
self._save()
return True
def get_all(self) -> Dict[str, Dict]:
"""获取所有白名单策略"""
return self.data.get("strategies", {})
def is_in_whitelist(self, strategy_key: str) -> bool:
"""检查策略是否在白名单中"""
return strategy_key in self.data.get("strategies", {})
def is_enabled_in_whitelist(self, strategy_key: str) -> bool:
"""检查策略是否在白名单中且已启用"""
if strategy_key not in self.data.get("strategies", {}):
return False
return self.data["strategies"][strategy_key].get("enabled", False)
def update_last_auto_start_date(self, date_str: str):
"""更新最后自动启动日期"""
self.data["last_auto_start_date"] = date_str
self._save()
def should_auto_start_today(self) -> bool:
"""检查今天是否应该自动启动"""
today = datetime.now().date().isoformat()
return self.data.get("last_auto_start_date") != today
```
### 3.2 修改 `core/manager.py`
#### 新增功能
```python
class StrategyManager:
def __init__(self, config_path: str = "config/main.json"):
# ... 现有代码 ...
self.whitelist_manager = WhitelistManager()
def get_status(self) -> Dict[str, Any]:
"""获取完整状态(扩展白名单信息)"""
status = super().get_status()
# 添加白名单信息
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)
status["whitelist_auto_start_today"] = self.whitelist_manager.should_auto_start_today()
return status
def add_to_whitelist(self, name: str) -> bool:
"""添加策略到白名单"""
return self.whitelist_manager.add(name, enabled=True)
def remove_from_whitelist(self, name: str) -> bool:
"""从白名单移除策略"""
return self.whitelist_manager.remove(name)
def set_whitelist_enabled(self, name: str, enabled: bool) -> bool:
"""设置白名单中策略的启用状态"""
return self.whitelist_manager.set_enabled(name, enabled)
def start_strategy(self, name: str, auto_start: bool = False) -> bool:
"""
启动策略
Args:
name: 策略标识符
auto_start: 是否为自动启动(自动启动不会更新最后尝试日期)
"""
# ... 现有启动逻辑 ...
# 如果是手动启动,清除自动启动相关的标记
if not auto_start:
if name in self.strategies:
self.strategies[name]["last_auto_start_attempt"] = None
self.strategies[name]["auto_start_success"] = None
return success
def auto_start_whitelist_strategies(self) -> Dict[str, bool]:
"""
自动启动白名单中所有未运行的策略
一天只执行一次
Returns:
Dict[str, bool]: 每个策略的启动结果
"""
if not self.whitelist_manager.should_auto_start_today():
return {}
results = {}
whitelist = self.whitelist_manager.get_all()
for name, config in whitelist.items():
if not config.get("enabled", True):
continue
if name not in self.strategies:
continue
# 检查是否已在运行
if self._is_running(name):
results[name] = True
continue
# 尝试启动
success = self.start_strategy(name, auto_start=True)
results[name] = success
# 更新策略状态
if name in self.strategies:
self.strategies[name]["last_auto_start_attempt"] = datetime.now().isoformat()
self.strategies[name]["auto_start_success"] = success
# 更新日期
self.whitelist_manager.update_last_auto_start_date(
datetime.now().date().isoformat()
)
return results
```
### 3.3 修改 `web_backend.py`
#### 新增 API 端点
```python
# ============ 白名单管理 API ============
@app.get("/api/whitelist")
def get_whitelist():
"""获取白名单列表"""
whitelist = manager.whitelist_manager.get_all()
return {
"whitelist": whitelist,
"auto_start_today": manager.whitelist_manager.should_auto_start_today()
}
@app.post("/api/whitelist/{name}/add")
def add_to_whitelist(name: str):
"""添加策略到白名单"""
if manager.add_to_whitelist(name):
return {"success": True}
raise HTTPException(400, "添加失败,策略可能已存在")
@app.post("/api/whitelist/{name}/remove")
def remove_from_whitelist(name: str):
"""从白名单移除策略"""
if manager.remove_from_whitelist(name):
return {"success": True}
raise HTTPException(400, "移除失败,策略可能不在白名单中")
@app.post("/api/whitelist/{name}/enable")
def enable_in_whitelist(name: str):
"""启用白名单中的策略"""
if manager.set_whitelist_enabled(name, True):
return {"success": True}
raise HTTPException(400, "操作失败")
@app.post("/api/whitelist/{name}/disable")
def disable_in_whitelist(name: str):
"""禁用白名单中的策略"""
if manager.set_whitelist_enabled(name, False):
return {"success": True}
raise HTTPException(400, "操作失败")
@app.post("/api/whitelist/auto-start")
def trigger_auto_start():
"""手动触发白名单自动启动(用于测试)"""
results = manager.auto_start_whitelist_strategies()
return {
"success": True,
"results": results,
"count": len(results)
}
# ============ 修改现有的状态 API ============
@app.get("/api/status")
def get_status():
"""获取策略状态(包含白名单信息)"""
status_data = manager.get_status()
status_data['git_info'] = get_git_commit_info()
return status_data
```
#### 修改定时任务
```python
@app.on_event("startup")
async def start_scheduler():
# ... 现有的 08:58, 20:58 重启任务 ...
# 新增:白名单自动启动任务(仅 08:58
scheduler.add_job(
auto_start_whitelist_task,
CronTrigger(hour=8, minute=58),
id="whitelist_auto_start",
replace_existing=True
)
logger.info("📅 白名单自动启动任务已添加 (计划时间: 08:58)")
def auto_start_whitelist_task():
"""
白名单自动启动任务
"""
logger.info("⏰ [白名单任务] 触发自动启动...")
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.warning(f"❌ [白名单任务] {name} 启动失败")
```
### 3.4 修改 `start.py`
#### 新增 CLI 命令
```python
def main():
# ... 现有的参数解析 ...
parser.add_argument("--whitelist", action="store_true", help="白名单操作模式")
# 白名单子命令
whitelist_parser = subparsers.add_parser("whitelist", help="白名单管理")
whitelist_parser.add_argument("action", choices=["add", "remove", "list", "enable", "disable"])
whitelist_parser.add_argument("-n", "--name", help="策略标识符")
```
### 3.5 前端界面修改
#### 状态表格新增列
| 列名 | 说明 | 示例值 |
|------|------|--------|
| 白名单 | 是否在白名单中 | ✅ / ❌ |
| 白名单状态 | 白名单中是否启用 | 启用 / 禁用 |
| 自动启动 | 今天是否已尝试自动启动 | 是 / 否 |
| 自动启动结果 | 上次自动启动是否成功 | 成功 / 失败 / - |
#### 新增管理操作
1. **添加到白名单**: 勾选策略后点击"添加到白名单"
2. **从白名单移除**: 勾选策略后点击"从白名单移除"
3. **启用/禁用**: 在白名单中启用或禁用某个策略
4. **手动触发**: 按钮"立即执行白名单启动"
---
## 4. 工作流程
### 4.1 自动启动流程
```mermaid
sequenceDiagram
participant T as 定时任务 (08:58)
participant W as Web Backend
participant M as StrategyManager
participant WM as WhitelistManager
participant S as Strategy Process
T->>W: 触发定时任务
W->>M: auto_start_whitelist_strategies()
M->>WM: should_auto_start_today()?
alt 今天未执行过
WM-->>M: True
M->>WM: get_all() 获取白名单
loop 遍历白名单策略
M->>M: 检查策略是否运行
alt 未运行
M->>S: 启动策略进程 (auto_start=True)
M->>M: 记录启动结果
else 已在运行
M->>M: 标记为成功
end
end
M->>WM: update_last_auto_start_date(今天)
M-->>W: 返回启动结果
W->>T: 记录日志
else 今天已执行过
WM-->>M: False
M-->>W: 返回空结果
W->>T: 跳过(今天已执行)
end
```
### 4.2 手动管理流程
```mermaid
sequenceDiagram
participant U as 用户
participant W as Web界面
participant API as Web Backend API
participant M as StrategyManager
participant WM as WhitelistManager
participant F as 白名单配置文件
U->>W: 点击"添加到白名单"
W->>API: POST /api/whitelist/{name}/add
API->>M: add_to_whitelist(name)
M->>WM: add(name)
WM->>F: 写入配置
F-->>WM: 保存成功
WM-->>M: True
M-->>API: True
API-->>W: {"success": True}
W->>U: 显示成功提示
W->>API: GET /api/status
API->>M: get_status()
M->>WM: 获取白名单状态
M-->>API: 状态数据
API-->>W: 状态数据
W->>U: 更新表格(显示白名单状态)
```
---
## 5. 兼容性设计
### 5.1 向后兼容
- 现有 `start.py status` 命令保持不变
- 现有 API `/api/status` 保持兼容(新增字段不影响现有功能)
- 现有日志查看功能保持不变
### 5.2 数据迁移
- 首次启动时自动创建 `config/whitelist.json`
- 无需手动迁移现有配置
---
## 6. 实施计划
### 阶段一:后端实现
1. 创建 `config/whitelist.json` 模板
2. 实现 `core/whitelist_manager.py`
3. 修改 `core/manager.py` 集成白名单功能
4. 修改 `web_backend.py` 添加 API
5. 修改 `start.py` 添加 CLI 命令
### 阶段二:前端实现
1. 修改状态表格,添加白名单列
2. 添加白名单管理操作按钮
3. 添加手动触发按钮
4. 美化界面交互
### 阶段三:测试
1. 测试白名单添加/移除
2. 测试自动启动功能
3. 测试手动停止不自动重启
4. 测试一天只启动一次
5. 测试重启后状态恢复
---
## 7. 文件修改清单
| 文件路径 | 操作 | 说明 |
|----------|------|------|
| `config/whitelist.json` | 新增 | 白名单配置文件 |
| `core/whitelist_manager.py` | 新增 | 白名单管理器类 |
| `core/manager.py` | 修改 | 集成白名单功能,扩展状态结构 |
| `web_backend.py` | 修改 | 添加白名单管理 API修改定时任务 |
| `start.py` | 修改 | 添加白名单 CLI 命令 |
| `frontend/` | 修改 | 前端界面添加白名单管理功能 |

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.path_utils import add_project_root_to_path
from core.whitelist_manager import WhitelistManager
# 添加项目根路径到sys.path # 添加项目根路径到sys.path
PROJECT_ROOT = add_project_root_to_path() PROJECT_ROOT = add_project_root_to_path()
@@ -34,6 +35,10 @@ class StrategyManager:
# 配置管理器日志 # 配置管理器日志
self._setup_logger() self._setup_logger()
# 初始化白名单管理器
self.whitelist_manager = WhitelistManager()
self.logger.info("📋 白名单管理器已初始化")
self.strategies: Dict[str, Dict[str, Any]] = {} self.strategies: Dict[str, Dict[str, Any]] = {}
self.logger.info("🔄 正在加载策略配置...") self.logger.info("🔄 正在加载策略配置...")
self.load_strategies() self.load_strategies()
@@ -109,15 +114,31 @@ class StrategyManager:
self.logger.error("❌ 加载配置失败 %s: %s", config_file, e, exc_info=True) 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]:
"""获取完整状态""" """获取完整状态(包含白名单信息)"""
self._refresh_status() self._refresh_status()
return {
# 构建状态数据
status = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"total": len(self.strategies), "total": len(self.strategies),
"running": sum(1 for s in self.strategies.values() if s["status"] == "running"), "running": sum(1 for s in self.strategies.values() if s["status"] == "running"),
"strategies": self.strategies "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): def _refresh_status(self):
"""刷新进程状态 - 双重验证""" """刷新进程状态 - 双重验证"""
for name, info in self.strategies.items(): for name, info in self.strategies.items():
@@ -347,6 +368,121 @@ class StrategyManager:
except Exception as e: except Exception as e:
self.logger.error("❌ 保存状态失败: %s", e, exc_info=True) 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]): 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> <style>
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; } 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; } .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-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; } .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> </style>
</head> </head>
<body> <body>
@@ -33,16 +46,17 @@
</div> </div>
<script> <script>
const { createApp, ref, onMounted, onUnmounted, watch } = Vue; const { createApp, ref, onMounted, onUnmounted, watch, computed } = Vue;
const naive = window.naive; const naive = window.naive;
// --- 主组件逻辑 --- // --- 主组件逻辑 ---
const MainLayout = { const MainLayout = {
template: ` template: `
<div>
<div class="header"> <div class="header">
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2> <h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
<n-space align="center"> <n-space align="center">
<!-- [核心修改] 1. 显示 Git 版本信息 --> <!-- Git 版本信息 -->
<n-tag :bordered="false" type="default" size="small"> <n-tag :bordered="false" type="default" size="small">
📦 Version: {{ gitInfo }} 📦 Version: {{ gitInfo }}
</n-tag> </n-tag>
@@ -64,27 +78,109 @@
</n-space> </n-space>
</div> </div>
<n-card title="策略列表" hoverable> <!-- 统计卡片 -->
<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> <n-table :single-line="false" striped>
<thead> <thead>
<tr> <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>运行状态</th>
<th>白名单</th>
<th>白名单状态</th>
<th>PID</th> <th>PID</th>
<th>运行时长</th> <th>运行时长</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(info, key) in strategies" :key="key"> <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><strong>{{ key }}</strong></td>
<td>{{ info.config.strategy_name }} <br><small style="color:#999">{{ info.symbol }}</small></td> <td>{{ info.config.name }} <br><small style="color:#999">{{ info.symbol }}</small></td>
<td> <td>
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small"> <n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small">
{{ info.status === 'running' ? '运行中' : '已停止' }} {{ info.status === 'running' ? '运行中' : '已停止' }}
</n-tag> </n-tag>
</td> </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.pid || '-' }}</td>
<td>{{ info.uptime || '-' }}</td> <td>{{ info.uptime || '-' }}</td>
<td> <td>
@@ -97,17 +193,17 @@
</td> </td>
</tr> </tr>
<tr v-if="Object.keys(strategies).length === 0"> <tr v-if="Object.keys(strategies).length === 0">
<td colspan="6" style="text-align: center; padding: 30px; color: #999;">暂无策略</td> <td colspan="9" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
</tr> </tr>
</tbody> </tbody>
</n-table> </n-table>
</n-card> </n-card>
<!-- 日志弹窗 --> <!-- 日志弹窗 -->
<n-modal v-model:show="showLogModal" style="width: 800px;" preset="card" :title="'📜 实时日志: ' + currentLogKey"> <n-modal v-model:show="showLogModal" style="width: 900px;" preset="card" :title="'📜 实时日志: ' + currentLogKey">
<div class="log-container" id="logBox"> <div class="log-container" id="logBox">
<div v-if="logLoading" style="text-align:center; padding:20px;"><n-spin size="medium" /></div> <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 v-else v-for="(line, index) in logLines" :key="index" class="log-line" :class="getLogClass(line)">{{ line }}</div>
</div> </div>
<template #footer> <template #footer>
<n-space justify="end"> <n-space justify="end">
@@ -116,6 +212,7 @@
</n-space> </n-space>
</template> </template>
</n-modal> </n-modal>
</div>
`, `,
setup() { setup() {
const message = naive.useMessage(); const message = naive.useMessage();
@@ -124,10 +221,12 @@
const strategies = ref({}); const strategies = ref({});
const loading = ref(false); const loading = ref(false);
const lastUpdated = ref('-'); const lastUpdated = ref('-');
// [核心修改] 2. 为 Git 信息创建一个 ref
const gitInfo = ref('Loading...'); const gitInfo = ref('Loading...');
// 白名单相关
const whitelistAutoStarted = ref(false);
const selectedKeys = ref([]);
const refreshInterval = ref(0); const refreshInterval = ref(0);
const intervalOptions = [ const intervalOptions = [
{ label: '✋ 仅手动', value: 0 }, { label: '✋ 仅手动', value: 0 },
@@ -137,6 +236,14 @@
]; ];
let timer = null; 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 () => { const fetchStatus = async () => {
if (loading.value) return; if (loading.value) return;
loading.value = true; loading.value = true;
@@ -145,11 +252,12 @@
if (!res.ok) throw new Error("Error"); if (!res.ok) throw new Error("Error");
const data = await res.json(); const data = await res.json();
strategies.value = data.strategies; strategies.value = data.strategies;
// [核心修改] 3. 更新 Git 信息
gitInfo.value = data.git_info || 'N/A'; gitInfo.value = data.git_info || 'N/A';
whitelistAutoStarted.value = !data.whitelist_auto_start_today;
lastUpdated.value = new Date().toLocaleTimeString(); lastUpdated.value = new Date().toLocaleTimeString();
// 清理已删除策略的选中状态
selectedKeys.value = selectedKeys.value.filter(k => k in strategies.value);
} catch (e) { } catch (e) {
message.error("连接服务器失败"); message.error("连接服务器失败");
} finally { } 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 handleAction = (name, action) => {
const map = { start: '启动', stop: '停止', restart: '重启' }; const map = { start: '启动', stop: '停止', restart: '重启' };
dialog.warning({ 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 showLogModal = ref(false);
const currentLogKey = ref(''); const currentLogKey = ref('');
const logLines = ref([]); const logLines = ref([]);
const logLoading = ref(false); 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) => { const fetchLogs = async (name) => {
logLoading.value = true; logLoading.value = true;
try { try {
const res = await fetch(`/api/logs/${name}?lines=100`); const res = await fetch(`/api/logs/${name}?lines=100`);
const data = await res.json(); const data = await res.json();
logLines.value = data.lines; logLines.value = data.lines || [];
setTimeout(() => { setTimeout(() => {
const el = document.getElementById('logBox'); const el = document.getElementById('logBox');
if(el) el.scrollTop = el.scrollHeight; if(el) el.scrollTop = el.scrollHeight;
}, 100); }, 100);
} catch(e) { message.error("日志获取失败"); } } catch(e) { message.error("日志获取失败"); }
finally { logLoading.value = false; } finally { logLoading.value = false; }
} };
const viewLogs = (name) => { const viewLogs = (name) => {
currentLogKey.value = name; currentLogKey.value = name;
@@ -230,11 +520,21 @@
}); });
return { return {
strategies, loading, lastUpdated, strategies, loading, lastUpdated, gitInfo,
gitInfo, // [核心修改] 4. 将 gitInfo 暴露给模板
refreshInterval, intervalOptions, refreshInterval, intervalOptions,
showLogModal, currentLogKey, logLines, logLoading, 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
}; };
} }
}; };

View File

@@ -27,18 +27,138 @@ def main():
# 查看日志最近50行 # 查看日志最近50行
python start.py logs -n DualModeTrendlineHawkesStrategy2_FG -t 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("-n", "--name", help="策略标识符策略名_品种")
parser.add_argument("-a", "--all", action="store_true", help="对所有策略执行操作") parser.add_argument("-a", "--all", action="store_true", help="对所有策略执行操作")
parser.add_argument("-c", "--config", default="config/main.json", help="主配置文件路径") parser.add_argument("-c", "--config", default="config/main.json", help="主配置文件路径")
parser.add_argument("-t", "--tail", type=int, default=30, 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() args = parser.parse_args()
manager = StrategyManager(args.config) 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": if args.action == "status":
status = manager.get_status() status = manager.get_status()
print_status_table(status) print_status_table(status)

View File

@@ -6,6 +6,7 @@ import logging
from collections import deque from collections import deque
from pathlib import Path from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -63,7 +64,7 @@ def get_git_commit_info():
logger.warning(f"无法获取 Git 提交信息: {e}") logger.warning(f"无法获取 Git 提交信息: {e}")
return "获取 Git 信息失败" return "获取 Git 信息失败"
# ================== 定时任务逻辑 (保持不变) ================== # ================== 定时任务逻辑 ==================
def scheduled_restart_task(): def scheduled_restart_task():
""" """
@@ -94,10 +95,39 @@ def scheduled_restart_task():
logger.info("⏰ [定时任务] 自动重启流程结束") logger.info("⏰ [定时任务] 自动重启流程结束")
# ================== FastAPI 事件钩子 (保持不变) ================== def scheduled_whitelist_auto_start():
"""
定时任务:白名单自动启动
仅在 08:58 执行
"""
logger.info("⏰ [白名单定时任务] 触发白名单自动启动...")
@app.on_event("startup") results = manager.auto_start_whitelist_strategies()
async def start_scheduler():
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( scheduler.add_job(
scheduled_restart_task, scheduled_restart_task,
CronTrigger(hour=8, minute=58), CronTrigger(hour=8, minute=58),
@@ -110,13 +140,29 @@ async def start_scheduler():
id="restart_evening", id="restart_evening",
replace_existing=True 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() 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() scheduler.shutdown()
logger.info("📅 定时任务调度器已关闭")
# ================== 初始化 ==================
app = FastAPI(title="策略控制台", lifespan=lifespan)
# ================== API 路由 ================== # ================== API 路由 ==================
@@ -168,9 +214,66 @@ def get_logs(name: str, lines: int = Query(50, le=500)):
raise HTTPException(500, f"读取日志失败: {e}") raise HTTPException(500, f"读取日志失败: {e}")
# ================== 静态文件挂载 (保持不变) ================== # ================== 白名单管理 API ==================
# 注意: 这里的路径是示例,请确保它与你的项目结构匹配
# 假设你的HTML文件在 "frontend/" 目录下 @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.mount("/static", StaticFiles(directory="frontend/dist"), name="static")
@app.get("/") @app.get("/")