224 lines
11 KiB
Markdown
224 lines
11 KiB
Markdown
# 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` 保持一致,先解析和校验参数并返回 400;`except HTTPError: raise`;QMT 撤单失败映射为 502;未知内部错误保留 500。
|
||
|
||
验收标准:缺少 stock 或 volume 非法稳定返回 400,QMT 撤单异常返回 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 -v`:7 项通过。
|
||
- `python -B -m compileall -q api py-client`:通过。
|
||
- 未连接真实 QMT,未执行真实下单、撤单、行情压力测试或故障注入。
|
||
- `api/QMT_API.py` 依赖 QMT 宿主注入的内置函数,服务端交易行为仍需在模拟账户中验证。
|
||
- `docs/todo.md` 中的人工排除项没有计入本报告问题数量和整改顺序。
|