feat(strategy_manager): 添加策略自动启动的白名单管理
实现全面的白名单管理系统,支持策略自动启动: - 添加 WhitelistManager 类用于持久化白名单配置存储 - 将白名单功能集成到 StrategyManager 核心模块 - 添加用于白名单 CRUD 操作的 REST API 端点 - 通过 start.py 创建用于白名单管理的 CLI 命令 - 更新前端 UI,添加白名单状态指示器和批量操作功能 - 实现每日 08:58 的自动启动调度 - 为白名单操作添加配置验证和日志记录 同时添加项目文档: - 代码格式规则(缩进、行长度、导入、命名) - 文件、变量、常量、函数、类的命名规范 - 受限制文件和目录的保护规则
This commit is contained in:
96
.kilocode/rules/formatting.md
Normal file
96
.kilocode/rules/formatting.md
Normal 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
|
||||
129
.kilocode/rules/naming_conventions.md
Normal file
129
.kilocode/rules/naming_conventions.md
Normal 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`
|
||||
126
.kilocode/rules/restricted_files.md
Normal file
126
.kilocode/rules/restricted_files.md
Normal 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 |
493
plans/strategy_manager_optimization_plan.md
Normal file
493
plans/strategy_manager_optimization_plan.md
Normal 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/` | 修改 | 前端界面添加白名单管理功能 |
|
||||
5
strategy_manager/config/whitelist.json
Normal file
5
strategy_manager/config/whitelist.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"last_auto_start_date": null,
|
||||
"strategies": {}
|
||||
}
|
||||
@@ -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]):
|
||||
"""格式化打印状态表格"""
|
||||
|
||||
345
strategy_manager/core/whitelist_manager.py
Normal file
345
strategy_manager/core/whitelist_manager.py
Normal 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)
|
||||
338
strategy_manager/frontend/dist/index.html
vendored
338
strategy_manager/frontend/dist/index.html
vendored
@@ -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,16 +46,17 @@
|
||||
</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>
|
||||
<div class="header">
|
||||
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
|
||||
<n-space align="center">
|
||||
<!-- [核心修改] 1. 显示 Git 版本信息 -->
|
||||
<!-- Git 版本信息 -->
|
||||
<n-tag :bordered="false" type="default" size="small">
|
||||
📦 Version: {{ gitInfo }}
|
||||
</n-tag>
|
||||
@@ -64,27 +78,109 @@
|
||||
</n-space>
|
||||
</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>
|
||||
<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">
|
||||
<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.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>
|
||||
<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>
|
||||
@@ -97,17 +193,17 @@
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</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 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>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
@@ -116,6 +212,7 @@
|
||||
</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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("/")
|
||||
|
||||
Reference in New Issue
Block a user