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

16 KiB
Raw Blame 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

问题描述

# 第642-672行存在完全重复的代码块
if datetime.date.today().weekday() >= 5:
    time.sleep(3600)
    continue
# ... 重连逻辑 ...

# 下面又重复了一次完全相同的逻辑!
if datetime.date.today().weekday() >= 5:
    time.sleep(3600)
    continue
# ... 重复的重连逻辑 ...

风险分析

  • 重连成功后可能立即再次执行重连
  • 导致连接状态混乱
  • 可能产生重复的 trader 实例

修复方案

# 删除第642-672行中的重复代码块
# 只保留一组重连逻辑

验收标准

  • 第642-672行范围内没有重复代码
  • 重连逻辑只执行一次
  • 日志输出正常,无重复重连记录

负责人: Sisyphus 完成日期: 2026-02-17


[x] 2. 修复卖出逻辑的持仓双重验证

文件: qmt_engine.py
行号: 733-753

问题描述

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 有记录但实盘已清仓
  • 超卖风险:卖出量超过实际可用持仓
  • 可能导致负持仓或违规交易

修复方案

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
    
    # ... 执行卖出

验收标准

  • 卖出前同时验证虚拟持仓和实盘持仓
  • 当实盘持仓为0时拒绝卖出并记录日志
  • 添加幽灵持仓自动清理机制
  • 模拟盘测试超卖场景被正确拦截
  • 核心逻辑:一切以实盘持仓为准 - 信号卖出+实盘有持仓=必须执行

负责人: Sisyphus 完成日期: 2026-02-17


[ ] 3. 统一价格偏移配置项名称

文件: qmt_engine.py, config.json

问题描述

  • 代码中使用:buy_price_offset / sell_price_offset
  • 配置中使用:buy_drift_pct / sell_drift_fixed
  • 配置项名称不匹配导致策略失效

风险分析

  • 价格偏移策略失效
  • 可能以不利价格成交
  • 实际交易行为与策略设计不符

修复方案(二选一)

方案A修改代码推荐

# 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修改配置文件

{
  "strategies": {
    "ST_Strategy": {
      "execution": {
        "buy_price_offset": 0.005,  // 改为代码中使用的名称
        "sell_price_offset": -0.01
      }
    }
  }
}

验收标准

  • 代码和配置中的价格偏移配置项名称一致
  • 策略能正确读取并使用价格偏移
  • 日志中显示的价格偏移值正确

负责人: ___________ 截止日期: ___________


🟠 P1 - 高风险(强烈建议修复)

[ ] 4. 修复买入资金计算逻辑

文件: qmt_engine.py
行号: 696-698

问题描述

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% 手续费预留可能不足
  • 已持仓较大时可能下单金额超过实际可用资金

修复方案

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

问题描述

try:
    # 消息处理逻辑
except:
    pass  # 异常被完全吞掉,无日志记录

风险分析

  • 交易信号丢失无法追溯
  • 无法排查问题原因
  • 系统表现与预期不符时无法定位

修复方案

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模块现在禁止出现任何静默处理 - 所有异常都必须被捕获并记录到日志。

验收标准

  • 所有异常都被捕获并记录到日志
  • 包含异常类型、消息内容和堆栈信息
  • 失败消息可追溯(可选:死信队列)

负责人: Sisyphus 完成日期: 2026-02-17


[ ] 6. 添加槽位检查的原子性保护

文件: qmt_engine.py
行号: 669-673

问题描述

# 非原子性操作
if (self.pos_manager.get_holding_count(strategy_name) >= strat_cfg["total_slots"]):
    return
# 此时槽位可能被其他线程占用,导致超仓

风险分析

  • 竞态条件导致超仓
  • 多线程环境下槽位计数不准确
  • 可能超过策略设定的最大持仓数

修复方案

# 使用 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

问题描述

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许所有来源
    allow_methods=["*"],  # 允许所有方法
    allow_headers=["*"],  # 允许所有头
)

风险分析

  • 生产环境允许任意跨域访问
  • 存在 CSRF 风险
  • API 可被任意网站调用

修复方案

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

问题描述

if price <= 0:
    logger.warning(f"价格异常: {price}强制设为1.0以计算股数(仅测试用)")
    price = 1.0  # 测试用代码留在生产环境!

修复方案

if price <= 0:
    logger.error(f"[{strategy_name}] 买入拦截: 价格异常 {price}")
    return  # 直接拒绝,不使用兜底值

负责人: ___________ 截止日期: ___________


[ ] 9. 为 qmt_engine.py 添加日终撤单逻辑

文件: qmt_engine.py

问题描述

qmt_engine.pyDailySettlement 类缺少撤单逻辑(与 qmt_trader.py 不同)

修复方案

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

问题描述

{
  "redis": {
    "password": "Redis520102"  // 明文存储
  }
}

修复方案

# 使用环境变量覆盖配置文件
import os

# 配置加载时优先使用环境变量
redis_cfg = CONFIG.get("redis", {})
redis_cfg["password"] = os.getenv("REDIS_PASSWORD", redis_cfg.get("password"))

部署说明:
生产环境应设置环境变量:

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消息处理静默失败及所有其他静默处理问题