Files
NewStock/qmt/TODO_FIX.md

582 lines
16 KiB
Markdown
Raw Normal View History

# QMT 交易模块缺陷修复任务清单
> 生成时间2026-02-17
> 基于代码审查报告生成
> 状态:🔴 **阻止上线** - 必须先修复 CRITICAL 和 HIGH 级别问题
---
## 📋 执行指南
### 优先级说明
- **P0 (Critical)**:必须立即修复,可能导致资金损失
- **P1 (High)**:必须修复,可能导致交易异常
- **P2 (Medium)**:建议修复,影响系统稳定性
- **P3 (Low)**:可选修复,长期优化项
### 执行顺序
1. 先完成所有 P0 任务
2. 再进行 P1 任务
3. 最后完成 P2/P3 任务
4. 每个任务完成后需在 [x] 中标记完成者姓名和日期
---
## 🔴 P0 - 严重缺陷(阻止上线)
### [x] 1. 修复重复的重连逻辑代码块
**文件**: `qmt_trader.py`
**行号**: 642-672
#### 问题描述
```python
# 第642-672行存在完全重复的代码块
if datetime.date.today().weekday() >= 5:
time.sleep(3600)
continue
# ... 重连逻辑 ...
# 下面又重复了一次完全相同的逻辑!
if datetime.date.today().weekday() >= 5:
time.sleep(3600)
continue
# ... 重复的重连逻辑 ...
```
#### 风险分析
- 重连成功后可能立即再次执行重连
- 导致连接状态混乱
- 可能产生重复的 trader 实例
#### 修复方案
```python
# 删除第642-672行中的重复代码块
# 只保留一组重连逻辑
```
#### 验收标准
- [x] 第642-672行范围内没有重复代码
- [x] 重连逻辑只执行一次
- [x] 日志输出正常,无重复重连记录
**负责人**: Sisyphus **完成日期**: 2026-02-17
---
### [x] 2. 修复卖出逻辑的持仓双重验证
**文件**: `qmt_engine.py`
**行号**: 733-753
#### 问题描述
```python
def _execute_sell(self, unit, strategy_name, data):
v_vol = self.pos_manager.get_position(strategy_name, data['stock_code'])
if v_vol <= 0:
return # 仅检查虚拟持仓,未验证实际持仓
# ... 直接执行卖出
```
#### 风险分析
- 幽灵卖出Redis 有记录但实盘已清仓
- 超卖风险:卖出量超过实际可用持仓
- 可能导致负持仓或违规交易
#### 修复方案
```python
def _execute_sell(self, unit, strategy_name, data):
# 1. 查询实盘持仓(一切以实盘为准)
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
rp = next((p for p in real_pos if p.stock_code == data['stock_code']), None) if real_pos else None
can_use = rp.can_use_volume if rp else 0
# 2. 检查虚拟持仓
v_vol = self.pos_manager.get_position(strategy_name, data['stock_code'])
# 3. 实盘无持仓 -> 拒绝卖出(清理幽灵持仓)
if can_use <= 0:
self.logger.warning(f"[{strategy_name}] 卖出拦截: {data['stock_code']} 实盘无可用持仓")
# 如果虚拟持仓存在但实盘已清仓,清理幽灵持仓
if v_vol > 0:
self.pos_manager.force_delete(strategy_name, data['stock_code'])
self.logger.info(f"[{strategy_name}] 已清理幽灵持仓: {data['stock_code']} 虚拟{v_vol}股")
return
# 4. 实盘有持仓 -> 必须卖出(取虚拟和实盘的最小值,虚拟无持仓则取实盘)
if v_vol <= 0:
self.logger.warning(f"[{strategy_name}] 卖出提醒: {data['stock_code']} 虚拟无持仓但实盘有{can_use}股,以实盘为准执行卖出")
final_vol = min(v_vol, can_use) if v_vol > 0 else can_use
if final_vol <= 0:
self.logger.warning(f"[{strategy_name}] 卖出拦截: {data['stock_code']} 计算后卖出量为0")
return
# ... 执行卖出
```
#### 验收标准
- [x] 卖出前同时验证虚拟持仓和实盘持仓
- [x] 当实盘持仓为0时拒绝卖出并记录日志
- [x] 添加幽灵持仓自动清理机制
- [x] 模拟盘测试超卖场景被正确拦截
- [x] **核心逻辑:一切以实盘持仓为准** - 信号卖出+实盘有持仓=必须执行
**负责人**: Sisyphus **完成日期**: 2026-02-17
---
### [ ] 3. 统一价格偏移配置项名称
**文件**: `qmt_engine.py`, `config.json`
#### 问题描述
- 代码中使用:`buy_price_offset` / `sell_price_offset`
- 配置中使用:`buy_drift_pct` / `sell_drift_fixed`
- 配置项名称不匹配导致策略失效
#### 风险分析
- 价格偏移策略失效
- 可能以不利价格成交
- 实际交易行为与策略设计不符
#### 修复方案(二选一)
**方案A修改代码推荐**
```python
# qmt_engine.py 第707-708行
offset = strat_cfg.get("execution", {}).get("buy_drift_pct", 0.0) # 改为配置中的名称
# 第555-558行
offset = (
self.config["strategies"][strategy_name]
.get("execution", {})
.get("sell_drift_fixed", 0.0) # 改为配置中的名称
)
```
**方案B修改配置文件**
```json
{
"strategies": {
"ST_Strategy": {
"execution": {
"buy_price_offset": 0.005, // 改为代码中使用的名称
"sell_price_offset": -0.01
}
}
}
}
```
#### 验收标准
- [ ] 代码和配置中的价格偏移配置项名称一致
- [ ] 策略能正确读取并使用价格偏移
- [ ] 日志中显示的价格偏移值正确
**负责人**: ___________ **截止日期**: ___________
---
## 🟠 P1 - 高风险(强烈建议修复)
### [ ] 4. 修复买入资金计算逻辑
**文件**: `qmt_engine.py`
**行号**: 696-698
#### 问题描述
```python
total_equity = asset.cash + asset.market_value # 使用总资产
target_amt = total_equity * weight / total_weighted_slots
actual_amt = min(target_amt, asset.cash * 0.98) # 仅预留 2% 手续费
```
#### 风险分析
- 使用总资产(含已持仓市值)而非可用资金计算
- 2% 手续费预留可能不足
- 已持仓较大时可能下单金额超过实际可用资金
#### 修复方案
```python
def _execute_buy(self, unit, strategy_name, data):
# ...
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if not asset:
return
# 使用可用资金而非总资产
available_cash = asset.cash
# 获取该终端下所有策略的持仓情况
terminal_strategies = self.get_strategies_by_terminal(unit.qmt_id)
total_weighted_slots = sum(
self.config["strategies"][s].get("total_slots", 1) *
self.config["strategies"][s].get("weight", 1)
for s in terminal_strategies
)
if total_weighted_slots <= 0:
return
weight = strat_cfg.get("weight", 1)
# 计算目标金额(基于可用资金)
target_amt = available_cash * weight / total_weighted_slots
# 预留更多手续费缓冲5%
actual_amt = min(target_amt, available_cash * 0.95)
# 增加最小金额检查
min_buy_amount = strat_cfg.get("execution", {}).get("min_buy_amount", 2000)
if actual_amt < min_buy_amount:
self.logger.warning(f"[{strategy_name}] 单笔金额 {actual_amt:.2f} 不足 {min_buy_amount},取消买入")
return
# ... 继续执行
```
#### 验收标准
- [ ] 使用 `asset.cash` 而非 `asset.cash + asset.market_value`
- [ ] 手续费预留改为 5%(可配置)
- [ ] 增加最小买入金额配置项检查
- [ ] 资金不足时正确拦截并记录日志
**负责人**: ___________ **截止日期**: ___________
---
### [x] 5. 修复消息处理的静默失败
**文件**: `qmt_engine.py`
**行号**: 556-557
#### 问题描述
```python
try:
# 消息处理逻辑
except:
pass # 异常被完全吞掉,无日志记录
```
#### 风险分析
- 交易信号丢失无法追溯
- 无法排查问题原因
- 系统表现与预期不符时无法定位
#### 修复方案
```python
def process_route(self, strategy_name):
# ...
try:
data = json.loads(msg_json)
# ... 处理逻辑
except json.JSONDecodeError as e:
self.logger.error(f"[{strategy_name}] JSON解析失败: {e}, 消息: {msg_json[:200]}")
except KeyError as e:
self.logger.error(f"[{strategy_name}] 消息缺少必要字段: {e}")
except Exception as e:
self.logger.error(f"[{strategy_name}] 消息处理异常: {str(e)}", exc_info=True)
# 可选:将失败消息存入死信队列以便后续处理
# self.r.rpush(f"{strategy_name}_dead_letter", msg_json)
```
#### 其他同步修复的静默处理问题
本次修复同时检查了qmt模块中所有裸`except: pass`语句,并修复了以下静默处理问题:
| 文件 | 行号 | 问题 | 修复方式 |
|------|------|------|----------|
| `qmt_engine.py` | 171 | 配置文件读取失败静默处理 | 添加日志警告 |
| `qmt_trader.py` | 551 | 健康检查资产查询异常静默处理 | 添加日志警告 |
| `qmt_trader.py` | 651 | 心跳文件写入异常静默处理 | 添加日志警告 |
| `qmt_trader.py` | 736 | API查询持仓异常静默处理 | 添加日志警告 |
**qmt模块现在禁止出现任何静默处理** - 所有异常都必须被捕获并记录到日志。
#### 验收标准
- [x] 所有异常都被捕获并记录到日志
- [x] 包含异常类型、消息内容和堆栈信息
- [ ] 失败消息可追溯(可选:死信队列)
**负责人**: Sisyphus **完成日期**: 2026-02-17
---
### [ ] 6. 添加槽位检查的原子性保护
**文件**: `qmt_engine.py`
**行号**: 669-673
#### 问题描述
```python
# 非原子性操作
if (self.pos_manager.get_holding_count(strategy_name) >= strat_cfg["total_slots"]):
return
# 此时槽位可能被其他线程占用,导致超仓
```
#### 风险分析
- 竞态条件导致超仓
- 多线程环境下槽位计数不准确
- 可能超过策略设定的最大持仓数
#### 修复方案
```python
# 使用 Redis 原子操作实现槽位占用
def _try_acquire_slot(self, strategy_name, stock_code):
"""尝试原子性占用槽位,返回是否成功"""
key = f"POS:{strategy_name}"
# Lua脚本实现原子性检查和占用
lua_script = """
local key = KEYS[1]
local code = ARGV[1]
local max_slots = tonumber(ARGV[2])
local current_count = redis.call('HLEN', key)
local exists = redis.call('HEXISTS', key, code)
-- 如果已存在该股票,允许(可能是加仓)
if exists == 1 then
return 1
end
-- 检查是否还有空槽位
if current_count >= max_slots then
return 0
end
-- 占用槽位
redis.call('HSETNX', key, code, 0)
return 1
"""
max_slots = strat_cfg["total_slots"]
result = self.r.eval(lua_script, 1, key, stock_code, max_slots)
return result == 1
# 在 _execute_buy 中使用
if not self._try_acquire_slot(strategy_name, data['stock_code']):
self.logger.warning(f"[{strategy_name}] 槽位已满,拦截买入")
return
```
#### 验收标准
- [ ] 槽位检查和占用是原子性操作
- [ ] 并发测试不会出现超仓
- [ ] 性能影响可接受
**负责人**: ___________ **截止日期**: ___________
---
### [ ] 7. 修复 API 服务器 CORS 配置
**文件**: `api_server.py`
**行号**: 90-95
#### 问题描述
```python
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源
allow_methods=["*"], # 允许所有方法
allow_headers=["*"], # 允许所有头
)
```
#### 风险分析
- 生产环境允许任意跨域访问
- 存在 CSRF 风险
- API 可被任意网站调用
#### 修复方案
```python
import os
# 从环境变量读取允许的域名
ALLOWED_ORIGINS = os.getenv(
"QMT_ALLOWED_ORIGINS",
"http://localhost:8001,http://127.0.0.1:8001"
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_methods=["GET", "POST"], # 只允许必要的方法
allow_headers=["Content-Type", "Authorization"], # 限制请求头
allow_credentials=False, # 不携带凭证
)
```
#### 验收标准
- [ ] CORS 只允许配置的白名单域名
- [ ] 生产环境不允许 `*`
- [ ] 方法和头信息限制在最小必要范围
**负责人**: ___________ **截止日期**: ___________
---
## 🟡 P2 - 中等问题
### [ ] 8. 移除测试用的价格兜底逻辑
**文件**: `qmt_trader.py`
**行号**: 373-374
#### 问题描述
```python
if price <= 0:
logger.warning(f"价格异常: {price}强制设为1.0以计算股数(仅测试用)")
price = 1.0 # 测试用代码留在生产环境!
```
#### 修复方案
```python
if price <= 0:
logger.error(f"[{strategy_name}] 买入拦截: 价格异常 {price}")
return # 直接拒绝,不使用兜底值
```
**负责人**: ___________ **截止日期**: ___________
---
### [ ] 9. 为 qmt_engine.py 添加日终撤单逻辑
**文件**: `qmt_engine.py`
#### 问题描述
`qmt_engine.py``DailySettlement` 类缺少撤单逻辑(与 `qmt_trader.py` 不同)
#### 修复方案
```python
class DailySettlement:
# ... 现有代码 ...
def run_settlement(self):
trader = self.unit.xt_trader
acc = self.unit.acc_obj
if not trader:
return
logger = logging.getLogger("QMT_Engine")
logger.info("=" * 40)
logger.info("执行收盘清算流程...")
# 1. 撤销所有可撤订单
try:
orders = trader.query_stock_orders(acc, cancelable_only=True)
if orders:
for o in orders:
logger.info(f"收盘清算 - 撤单: OrderID={o.order_id}, Stock={o.stock_code}")
trader.cancel_order_stock(acc, o.order_id)
time.sleep(2)
logger.info(f"收盘清算 - 完成撤单 {len(orders)} 个订单")
except Exception as e:
logger.error(f"收盘清算 - 撤单失败: {str(e)}", exc_info=True)
# 2. 持仓对账(现有逻辑)
# ...
```
**负责人**: ___________ **截止日期**: ___________
---
### [ ] 10. 敏感信息加密存储
**文件**: `config.json`
#### 问题描述
```json
{
"redis": {
"password": "Redis520102" // 明文存储
}
}
```
#### 修复方案
```python
# 使用环境变量覆盖配置文件
import os
# 配置加载时优先使用环境变量
redis_cfg = CONFIG.get("redis", {})
redis_cfg["password"] = os.getenv("REDIS_PASSWORD", redis_cfg.get("password"))
```
**部署说明**:
生产环境应设置环境变量:
```bash
set REDIS_PASSWORD=Redis520102
set QMT_ACCOUNT_PASSWORD=your_password
```
**负责人**: ___________ **截止日期**: ___________
---
## 🟢 P3 - 长期优化
### [ ] 11. 添加交易前价格范围检查
**建议**: 在下单前检查价格是否在合理范围如前收盘价±10%),防止异常价格导致大额损失
### [ ] 12. 添加订单确认机制
**建议**: 大额订单添加二次确认机制,可通过 WebSocket 推送到前端确认
### [ ] 13. 完善监控告警
**建议**:
- 连接断开告警
- 成交异常告警
- 持仓偏差告警
- 资金异常告警
### [ ] 14. 增加单元测试覆盖
**建议**: 为核心交易逻辑添加单元测试,特别是:
- 买入/卖出逻辑
- 持仓计算
- 价格偏移计算
- 重连逻辑
### [ ] 15. 添加交易审计日志
**建议**: 将所有交易操作记录到独立的审计日志,包含:
- 下单时间、价格、数量
- 成交回报
- 错误信息
- 操作来源(信号来源)
---
## 📊 修复进度追踪
| 任务ID | 优先级 | 状态 | 负责人 | 开始日期 | 完成日期 |
|--------|--------|------|--------|----------|----------|
| 1 | P0 | ✅ | Sisyphus | 2026-02-17 | 2026-02-17 |
| 2 | P0 | ✅ | Sisyphus | 2026-02-17 | 2026-02-17 |
| 3 | P0 | ⬜ | | | |
| 4 | P1 | ⬜ | | | |
| 5 | P1 | ✅ | Sisyphus | 2026-02-17 | 2026-02-17 |
| 6 | P1 | ⬜ | | | |
| 7 | P1 | ⬜ | | | |
| 8 | P2 | ⬜ | | | |
| 9 | P2 | ⬜ | | | |
| 10 | P2 | ⬜ | | | |
---
## ✅ 上线前最终检查清单
- [ ] 所有 P0 任务已完成并测试通过
- [ ] 所有 P1 任务已完成并测试通过
- [ ] 代码审查通过
- [ ] 模拟盘测试运行 3 天以上无异常
- [ ] 日终清算功能验证通过
- [ ] 重连机制测试通过
- [ ] API 安全配置验证
- [ ] 日志系统正常工作
- [ ] 监控告警配置完成
- [ ] 回滚方案准备就绪
---
## 📝 版本历史
| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|--------|----------|
| v1.0 | 2026-02-17 | Assistant | 初始版本,基于代码审查报告生成 |
| v1.1 | 2026-02-17 | Sisyphus | 修复缺陷#1(重复重连逻辑)和缺陷#2(卖出双重验证) |
| v1.2 | 2026-02-17 | Sisyphus | 修复缺陷#5(消息处理静默失败)及所有其他静默处理问题 |