Files
NewStock/qmt/TODO_FIX.md
liaozhaorun 29706da299 fix(qmt): 修复交易模块核心缺陷
- 修复重复的重连逻辑代码块,避免重复连接
- 修复卖出逻辑:增加实盘持仓校验,一切以实盘为准
- 修复幽灵持仓自动清理机制
- 修复消息处理的静默异常,添加完整日志记录
- 统一 qmt 模块所有静默处理问题
- 添加 qmt_signal_sender.py 信号发送器
- 生成 TODO_FIX.md 缺陷修复任务清单
2026-02-17 23:10:28 +08:00

582 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(消息处理静默失败)及所有其他静默处理问题 |