Files
big-qmt/docs/CODE_AUDIT_2026-08-29.md
2026-08-30 00:34:27 +08:00

205 lines
12 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.
# big-qmt 代码复审报告2026-08-29
## 1. 审计范围与方法
- 审计范围:当前工作区中的 `api/``py-client/`、启动脚本及测试。
- 明确排除:`docs/todo.md`,本报告没有读取或引用该文件作为判断依据。
- 关注项:致命错误、交易业务逻辑错误、代码冗余、过度验证。
- 动态验证:`python -B -m unittest discover -s tests -v`;共 11 项8 项通过、3 项错误。
- 编译验证:`python -B -m compileall -q .` 通过。
- 限制:未连接真实 QMT未执行真实委托、撤单或成交回报验证。
## 2. 总结
当前版本不适合直接实盘运行。至少存在 4 条会使计划业务完全不执行或造成重复/超额下单的高风险路径:
1. IPO 功能在启用时必然先抛出 `TypeError`,而且其后仍有路径拼接、锁判断和调度生命周期错误。
2. 趋势策略的 `UNKNOWN` 状态虽然不会被删除,但并未参与开仓过滤,无法兑现“禁止自动重复下单”。
3. 多个开仓信号之间没有共享可用资金预算,且首笔开仓数量函数可能主动超过配置金额。
4. `OrderBook` 会覆盖同证券同方向的多笔订单,并可能反复撤销已终结的历史订单。
测试失败并非测试环境问题,而是直接命中了当前生产函数的确定性参数错误。
## 3. P0致命或可能造成错误交易
### 3.1 IPO 入口启用后必然报错,全部核心测试无法执行
位置:
- `py-client/strategy/ipo/boot.py:22-29`
- `py-client/libs/calc.py:5-7`
`AutoBuyIpo(now)` 调用 `trading_time()` 时没有传入必需的 `now` 参数。只要 `enable_auto_ipo=True`,函数便在创建客户端、读取候选和检查重复委托之前抛出 `TypeError`
复现结果:
- `test_local_record_prevents_duplicate_after_restart`ERROR
- `test_broker_order_prevents_duplicate`ERROR
- `test_one_rejection_does_not_stop_other_candidates`ERROR
影响:自动打新完全不可用;如果由主程序同步调用,异常还可能中止启动流程。
建议:传入 `now or datetime.now()`,并将交易日与交易时段判断统一成一个可测试入口。修复后必须重新运行现有 4 项 IPO 测试。
### 3.2 IPO 路径还有两层确定性错误:字符串路径相除、锁语义反向
位置:
- `py-client/strategy/ipo/boot.py:37-49`
- `py-client/config/__init__.py:28,109`
- `py-client/libs/lockfile.py:9-18`
即使修复 3.1,代码仍执行:
```python
Path(config.global_config.qmt_data_dir / f"{stock}.lock")
```
`qmt_data_dir``GlobalConfig` 中是 `str`,因此先计算 `str / str`,会再次抛出 `TypeError`。此外,`is_lock()` 的语义是“锁文件已存在”,当前代码却仅在其返回 `True` 时提交申购;首次运行没有锁文件,所有候选都会被跳过。正确业务语义应是“没有本地成功记录时才继续”,并且还要和券商订单/成交记录交叉核对。
影响修复第一个异常后IPO 仍无法正常首申购;若人工预建锁文件绕过判断,则反而允许对已锁定证券再次申购。
建议:使用 `Path(qmt_data_dir) / f"{stock}.lock"`;反转本地锁判断;提交成功后才写锁;提交失败不得写锁;恢复测试中已经表达但当前实现缺失的券商订单和成交去重。
### 3.3 `UNKNOWN` 只被保存在状态文件中,没有真正阻止再次开仓
位置:
- `py-client/strategy/trend/state.py:76-82,175-228`
- `py-client/strategy/trend/boot.py:136-164`
- `py-client/strategy/trend/open.py:17-70`
`State.delete()` 会静默保留 `ING/UNKNOWN`,但 `RunOnce()` 构造 `allow_open` 时只排除真实持仓代码,不排除 `State` 中的未决代码。`open_signal()` 也只检查 `OrderBook.busy()`。当重启对账无法找到真实活动委托并把订单转成 `UNKNOWN` 后,如果券商订单查询暂时缺失或委托已终结但结果不明,`OrderBook` 没有活动锁,下一次同证券信号仍可提交新买单。
另外,`RunOnce()` 无论 `State.delete()` 是否因保护而拒绝删除,都会执行 `open_watch.forget()``add_watch.forget()`,形成“状态保留但观察锁被清除”的不一致状态。
影响:前一笔订单结果无法确认时仍可能重复开仓;这与状态修复目标和验收标准直接冲突。
建议:开仓候选必须同时排除持仓代码、`ING/UNKNOWN` 状态代码以及活动买单代码。`delete()` 应返回是否实际删除,调用方只在删除成功后清理观察器。运行期每轮应将订单簿刷新结果用于状态对账,而不是仅在启动时对账一次。
## 4. P1重要业务逻辑错误
### 4.1 IPO 定时任务实际上不会在 10:00 被调度
位置:`py-client/main.py:111-115`
代码注册每日 10:00 任务后只调用一次 `schedule.run_pending()`,随后进入 `StartTrend()` 的永久循环,再也没有机会驱动调度器。除非程序恰好在任务已到期的极窄窗口启动,否则自动打新不会执行。
建议:把调度轮询合并进主循环,或使用独立受控线程/独立进程IPO 异常必须隔离,不能终止趋势策略。
### 4.2 样例账户没有启用任何趋势信号
位置:
- `py-client/config/__init__.py:48`
- `py-client/etc/dev.yaml`
- `py-client/libs/signal.py:23-28`
`signal_allow` 默认空列表,而当前 `dev.yaml` 没有该字段。`init_signals()` 只加载显式出现在允许列表中的信号,因此样例配置启动后趋势策略永远没有开仓候选,日志也不会提示“允许列表为空”。
建议:明确选择一种语义:空列表表示全部禁用,并在启动时显著告警;或空列表表示允许全部。样例配置应与预期业务一致。
### 4.3 同证券同方向的多笔委托会互相覆盖
位置:`py-client/strategy/trend/order.py:68-77,118-122`
订单缓存以 `BUY-code` / `SELL-code` 为唯一键。券商返回同一证券同方向的多笔委托时,字典推导只保留最后一笔。过期撤单、状态展示和锁判断无法看到被覆盖的订单。
建议:以系统委托号作为主键,另建 `(side, code) -> set[system_order_id]` 活动索引。
### 4.4 过期撤单没有过滤活动状态
位置:`py-client/strategy/trend/order.py:79-90`
`cancel_expired()` 遍历 `data.values()` 时只检查时间和系统委托号,没有要求 `order.status in BUSY_STATUSES`。如果接口返回历史委托,程序每轮都可能对已成交、已撤销或已失败订单调用撤单。
建议:只对活动状态且超过时限的订单撤单,并维护撤单请求中的冷却状态,避免每 30 秒重复请求。
### 4.5 状态落盘失败后的故障策略不一致
位置:
- `py-client/strategy/trend/open.py:59-70`
- `py-client/strategy/trend/positions.py:175-183`
开仓提交成功但保存失败时只记录日志,仍清除观察器并继续运行;补仓保存失败则异常向外传播,但真实订单已经提交。两条路径都可能出现“真实订单存在、本地恢复信息缺失”,处理策略却不一致。
建议:真实下单成功后若持久化失败,应将证券置于内存级熔断集合,保留订单簿锁并持续重试落盘;禁止该证券继续自动交易,直至对账确认。
### 4.6 启动脚本会杀死机器上的所有 Python 进程
位置:`run.bat:1-7`
`taskkill /IM python.exe /F` 不限定当前项目、PID 或命令行,会强制终止机器上所有 Python 工作负载。脚本也没有先切换到自身目录,`python main.py` 是否能找到文件取决于调用时当前目录;仓库根目录本身并不存在 `main.py`
影响:可能中断无关服务、研究任务或其他交易程序,同时自身仍可能因工作目录错误无法启动。
建议:保存本项目 PID 并只终止该 PID脚本开头使用 `cd /d "%~dp0py-client"` 或调用绝对脚本路径;不要在生产启动流程中无条件 `git pull`
## 5. P2代码冗余、失效代码和过度验证
### 5.1 API 服务文件完全重复且正式入口不明确
位置:
- `api/qmt_api_new.py`
- `api/qmt_api_rele.py`
- 工作区状态中的已删除 `api/QMT_API.py`
两个现存文件 SHA-256 完全一致,属于逐字节重复;原正式命名文件当前又处于删除状态。部署人员无法从启动脚本或文档可靠判断应加载哪个版本,后续修复也容易只改到其中一份。
建议:保留唯一正式入口,开发版本通过 Git 分支管理,不以 `_new``_rele` 复制整文件。
### 5.2 IPO 模块呈现“旧实现覆盖新设计”的死代码特征
位置:`py-client/strategy/ipo/boot.py:5-19`
`json``Any``IPO_STRATEGY_NAME``IPO_REMARKS``IPO_SESSIONS``TRADING_CALENDAR_SYMBOL` 均未使用;测试则期待交易日查询、券商对账、异常隔离、上下文管理器和返回成功数量,但当前函数都未实现。这不是单纯格式问题,而是实现与测试/设计发生大段脱节的信号。
建议:不要逐个删除常量掩盖问题;先恢复完整 IPO 流程,再移除确认无用的符号。
### 5.3 主程序存在不可达代码、拼写错误和未使用符号
位置:
- `py-client/main.py:5,15,100-101,115-116`
`StartTrend()` 正常情况下永久循环,因此其后的“策略启动成功”日志不可达;即便未来返回,使用的 `config.account_config.strateg` 也不存在。非 Windows 分支调用未定义的 `log.error``TimedRotatingFileHandler``GLOBAL_CONFIG_PATH` 没有使用。
建议:启动成功日志应放在进入循环前;统一使用 `logging`;清除未使用导入和常量。
### 5.4 `State.delete()` 的全局保护属于过度且不透明的验证
位置:`py-client/strategy/trend/state.py:76-82`
任何调用者请求删除 `ING/UNKNOWN` 状态都会被静默拒绝,没有返回值、日志、强制删除入口或订单证据参数。它确实避免了一类误删,但也会阻止人工确认后的清理,并使调用者误以为删除成功。当前 `RunOnce()` 正因此错误清除了观察器。
建议:将“是否可清理”的业务判断放在显式对账流程中;`delete()` 返回布尔值或抛出明确异常;如需保护,提供带审计原因的显式强制路径。验证应基于订单证据,不应仅基于状态字符串。
### 5.5 对账存在重复落盘
位置:`py-client/strategy/trend/state.py:84-108,110-137`
`reconcile()` 先调用会自行 `save()``sync_positions()`,完成订单对账和清理后又 `save()`。每次启动对账至少写两次同一状态文件,第一份还是未完成订单对账的中间状态。
建议:为 `sync_positions()` 增加不立即保存的内部版本,整个对账事务只在最终一致状态落盘一次。
## 6. 建议整改顺序
1. 先恢复 IPO 的可执行性,并让现有 4 项 IPO 测试全部通过。
2. 完成 `UNKNOWN/ING` 与开仓候选、订单簿之间的统一锁定,增加重启未成交端到端测试。
3. 修复开仓数量和同轮资金预留,增加高价股、多信号资金边界测试。
4. 重构 `OrderBook` 主键与活动索引,并限制撤单状态。
5. 修复调度生命周期和启动脚本的进程范围。
6. 最后清理重复 API 文件、死代码、重复落盘和无效日志。
## 7. 验收门槛
- 全部 11 项现有测试通过,且不通过跳过/删除失败测试达成。
- 新增IPO 首次申购、重复启动、券商已有委托、单只拒单不影响其他候选。
- 新增:开仓未成交重启、订单查询暂时缺失、部分成交活动、部分成交撤单、完全成交。
- 新增:`buy_value` 不足一手、多信号总金额超过现金、价格滑点场景。
- 新增:同证券同方向两笔活动委托都能被发现和撤销。
- 在模拟账户完成至少一次完整的下单、部分成交、撤单、重启对账闭环后,再考虑实盘。