Files
big-qmt/docs/RE_AUDIT_2026-08-29.md
2026-08-29 02:19:40 +08:00

11 KiB
Raw Blame History

big-qmt 重新审计报告

  • 审计日期2026-08-29
  • 审计范围:服务端 api/、客户端 py-client/
  • 审计方式静态代码检查、关键交易调用链核对、Python 编译检查、现有单元测试
  • 代码修改:无,本文件仅供人工确认
  • 人工排除依据:docs/todo.md

一、审计边界与结论

本次没有重复列出 docs/todo.md 中人工排除的事项,包括同步 QMT 调用阻塞、重复账户查询、大行情编码、信号运行期刷新、Handler/SDK 结构重构、全局配置重构和自动化测试体系等。

当前源码可以通过 Python 编译检查,py-client/tests 的 7 项测试全部通过。但在人工排除项之外,仍发现 5 项 P0 和 8 项 P1/P2 问题。最危险的路径是:程序每次启动都无条件申购新股;重启对账会删除尚未形成持仓的待成交订单状态;任意成交记录会把部分成交误判为全部完成;开仓和补仓没有共享同一个资金预算。

当前版本在完成 P0 整改和模拟账户验证前,不建议直接长期实盘运行。

二、P0可能造成重复下单或资金失控

2.2 重启对账会删除尚未形成持仓的待成交开仓状态

位置:

  • py-client/strategy/trend/state.py:116-141
  • py-client/strategy/trend/boot.py:153-160

现状:State.reconcile() 先以真实持仓构造 active_codes,随后删除所有不在持仓中的状态。新开仓委托从提交到成交之间通常还没有真实持仓,因此重启时其 ING 状态会先被删除,根本没有机会再与真实委托进行对账。运行期间也会按同样逻辑删除“不在持仓”的状态。

影响:程序在下单后、成交前重启,可能丢失本地订单状态;后续信号可能再次提交同一证券的买单。

解决方案:

  1. 删除状态前先按本地订单号匹配真实活动委托和成交。
  2. ING 且存在活动委托的状态必须保留,即使当前没有持仓。
  3. 只有确认订单终结且不存在持仓时,才能删除状态。
  4. 运行期清理必须同时参考 OrderBook,不能只参考 position_codes
  5. 对无法确认的订单转为 UNKNOWN 并禁止自动重复下单,等待下一轮对账或人工处理。

验收标准:下单未成交时强制退出并重启,状态和订单锁仍然存在,不会再次开仓。

2.3 任意成交记录都会把部分成交误判为全部完成

位置:py-client/strategy/trend/state.py:179-206

现状:_reconcile_leg() 只要在成交记录备注中找到本地订单号,就立即返回 STATUS_OK,没有比较委托数量、累计成交数量和剩余数量。

影响:部分成交后本地状态被视为完成,但真实剩余委托仍可能继续成交;补仓次数、成本和后续交易判断可能与真实账户不一致。

解决方案:

  1. 以真实委托状态作为主要状态来源。
  2. 汇总同一系统委托号的累计成交数量。
  3. 累计成交量 >= 委托量 才标记 OK
  4. 部分成交且委托仍活动时保持 ING
  5. 部分成交后撤单应记录为部分完成状态;若不增加持久化字段,至少保持 UNKNOWN 并阻止自动重复下单。

验收标准0%、部分成交、全部成交、部分成交后撤单四种情况均能得到不同且安全的状态结果。

2.5 下单成功但状态文件保存失败后仍继续运行

位置:py-client/strategy/trend/open.py:52-70

现状:真实订单提交成功后才写入状态文件;save() 抛出 OSError 时只记录日志,随后仍清除观察器并当作开仓成功继续运行。

影响:真实订单存在,但本地状态文件没有订单号。程序再次崩溃或重启后更容易重复下单。

解决方案:

  1. 保存失败时保留内存中的 pending 订单锁,不能清除观察状态。
  2. 将证券标记为不可继续自动交易,直到持久化成功或真实订单对账完成。
  3. 重试状态落盘,并产生高优先级告警。
  4. 补仓路径的 state.save() 也应采用同一故障策略。

验收标准:模拟状态目录只读时,真实订单提交后该证券不会再次下单,并能明确告警和恢复。

三、P1订单状态和配置语义错误

3.1 委托缓存会覆盖同证券同方向的多笔订单

位置:py-client/strategy/trend/order.py:70-79

现状:refresh() 使用 BUY-证券代码SELL-证券代码 作为 data 字典键。同一证券同方向存在多笔委托时,后面的记录会覆盖前面的记录。

影响:过期撤单、状态展示和订单对账只能看到其中一笔;被覆盖的活动订单可能继续成交。

解决方案:

  1. 以真实 order_sys_id 作为订单字典主键。
  2. 单独维护 (side, code) -> set[order_sys_id] 的活动索引。
  3. busy() 只需判断活动索引是否非空。
  4. 刷新时对缺失系统委托号的记录单独告警,不能互相覆盖。

验收标准:同证券同方向同时存在两笔委托时,两笔都能被查询、对账和撤销。

3.2 过期撤单会遍历已完成订单并重复发请求

位置:py-client/strategy/trend/order.py:81-93

现状:cancel_expired() 只检查创建时间和订单 ID没有限定订单状态必须属于 BUSY_STATUSES。如果 QMT 返回历史已完成订单,每 30 秒仍会对旧订单调用一次 cancel_by_id()

影响:订单历史较多时会产生大量无效 HTTP/QMT 往返;真正需要撤销的订单还可能因 3.1 的覆盖问题被遗漏。

解决方案:只遍历活动状态订单;成功发出撤单请求后在本地标记“撤单处理中”,设置重试间隔和最大次数;不要每轮重复撤同一订单。

验收标准:一千条历史订单中仅两条活动超时订单时,只产生两次撤单请求。

3.3 多个交易配置字段定义后未参与业务判断

位置:

  • py-client/config/__init__.py:20-21
  • py-client/config/__init__.py:48-50
  • py-client/strategy/trend/open.py:18-40
  • py-client/strategy/trend/positions.py:18,65,147-150

现状:

  • gt_last_price_is_open 没有用于比较信号昨收价和当前价。
  • loss_trigger_pct 没有参与补仓阈值,代码固定使用 (-30, -50)
  • min_profit_pct 没有参与最低止盈判断。

影响:人工修改 YAML 后策略行为可能完全不变,实际交易规则与配置含义不一致。

解决方案:逐项明确配置是启用参数还是废弃参数;启用则进入唯一的决策公式并补充边界校验,废弃则从 dataclass 和配置样例中删除,禁止保留“看似可配置但实际无效”的字段。

验收标准:分别改变三个配置值时,确定性的策略输入会产生对应且可测试的决策变化。

3.4 规则撤单的参数错误会被改写成 HTTP 500

位置:api/QMT_API.py:1255-1285

现状:处理器在参数错误时主动抛出 HTTP 400但外层 except Exception 又将其捕获并转换为 HTTP 500。

影响:客户端无法区分调用参数错误和服务端故障,可能执行错误的重试或告警策略。

解决方案:与 PassorderHandler 保持一致,先解析和校验参数并返回 400except HTTPError: raiseQMT 撤单失败映射为 502未知内部错误保留 500。

验收标准:缺少 stock 或 volume 非法稳定返回 400QMT 撤单异常返回 502。

四、P1启动和安全边界

4.1 服务端初始化失败后只记录日志,不向 QMT 抛出

位置:api/QMT_API.py:1526-1556

现状:init() 捕获所有异常并仅记录日志。账户绑定、股票池读取、端口监听或服务启动失败后,宿主可能继续认为策略已经完成初始化。

影响形成“策略看似启动、HTTP 服务实际不可用”的假启动状态。

解决方案:只对明确可恢复的步骤做局部处理;不可恢复异常记录后重新抛出;启动完成标志必须在 listen() 成功后设置。

验收标准端口被占用、账户无效或配置文件损坏时QMT 明确显示策略启动失败。

4.2 pass_codes.json 被无条件要求存在

位置:api/QMT_API.py:1538-1542

现状:注释称“按需加载股票池”,但代码无条件打开文件。文件不存在、内容为空或根节点不是列表都会中止整个 HTTP 服务。

影响:不使用 ContextInfo 股票池的部署也必须准备额外文件;普通配置疏漏会导致服务不可用。

解决方案:文件存在时才读取;验证根节点为证券代码数组;不存在时使用空股票池并记录提示。如果业务确实强制要求股票池,应在启动前给出明确配置错误,而不是普通文件异常。

验收标准:不使用股票池时文件不存在仍能启动;文件格式错误时错误信息明确指出路径和格式要求。

4.3 认证令牌硬编码并输出到日志,且服务允许远程关停

位置:

  • api/QMT_API.py:14
  • api/QMT_API.py:50
  • api/QMT_API.py:1305-1310
  • api/QMT_API.py:1551

现状:服务端令牌写死在源码并明文打印;服务监听全部网卡,持有同一令牌的调用方可访问交易接口和关停接口。

影响源码、Git 历史或日志泄露即可获得交易与关停权限,且无法区分不同客户端。

解决方案:

  1. 令牌只从环境变量或受控配置读取,缺失时拒绝启动。
  2. 立即更换当前已进入源码和日志的令牌。
  3. 日志只记录“令牌已配置”,不得输出原文。
  4. 关停接口使用独立管理令牌,或仅允许环回地址调用。
  5. 在主机防火墙限制允许访问的客户端地址。

验收标准:仓库和日志中不存在真实令牌;普通交易客户端不能调用关停接口。

五、P2资源生命周期

5.1 长生命周期 Client 缺少统一关闭路径

位置:

  • py-client/main.py:61-79
  • py-client/strategy/ipo/boot.py:4-22
  • py-client/strategy/trend/boot.py:67-119

现状:等待 API、IPO 和趋势策略分别创建 Client。正常无限循环时问题不明显但启动异常、策略异常退出或人工终止时没有统一 finally 关闭IPO Client 从不关闭。

影响:测试、重启或异常恢复过程中可能遗留连接和文件描述符,增加故障定位难度。

解决方案:主流程统一持有一个 Client或各子流程使用上下文管理器无限循环使用 try/finally,退出时关闭连接并保存必要状态。

六、建议整改顺序

  1. 为自动 IPO 增加开关、幂等记录和异常隔离。
  2. 修复待成交订单状态清理和重启对账顺序。
  3. 修复部分成交状态判断。
  4. 建立开仓与补仓共享资金预算。
  5. 处理下单成功但状态落盘失败的安全状态。
  6. 重构订单缓存主键和过期撤单过滤。
  7. 统一配置字段的真实业务语义。
  8. 修复服务端假启动、股票池加载与令牌安全。

七、验证记录与限制

  • python -B -m unittest discover -s tests -v7 项通过。
  • python -B -m compileall -q api py-client:通过。
  • 未连接真实 QMT未执行真实下单、撤单、行情压力测试或故障注入。
  • api/QMT_API.py 依赖 QMT 宿主注入的内置函数,服务端交易行为仍需在模拟账户中验证。
  • docs/todo.md 中的人工排除项没有计入本报告问题数量和整改顺序。