update api,libs
This commit is contained in:
204
docs/CODE_AUDIT_2026-08-29.md
Normal file
204
docs/CODE_AUDIT_2026-08-29.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 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` 不足一手、多信号总金额超过现金、价格滑点场景。
|
||||
- 新增:同证券同方向两笔活动委托都能被发现和撤销。
|
||||
- 在模拟账户完成至少一次完整的下单、部分成交、撤单、重启对账闭环后,再考虑实盘。
|
||||
Reference in New Issue
Block a user