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

177 lines
9.3 KiB
Markdown
Raw Permalink 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 V2
## 1. 范围与验证
- 基于当前未提交工作区重新审计,不直接继承上一版报告结论。
- 审计范围:`api/``py-client/`、启动脚本、配置和测试。
- 明确排除:`docs/todo.md`;未读取、未引用其内容。
- `python -B -m unittest discover -s tests -v`11 项中 7 项通过、4 项错误。
- 全部 Python 文件 AST 解析通过。
- 未连接真实 QMT未执行真实申购、买卖、撤单和成交回报测试。
## 2. 总体结论
当前版本仍不建议直接实盘运行。
手动修改已经修复了 IPO 路径中的三个表面问题:`trading_time()` 现在传入时间、数据目录能够正确转成 `Path`、首次申购的本地锁判断方向已经改正APScheduler 也能在趋势策略永久循环之外每日 10:00 触发任务。
但核心安全闭环仍未完成IPO 会在没有确认券商受理的情况下写入“已申购”锁,也没有使用券商委托/成交记录防重;趋势策略的 `UNKNOWN` 状态仍不能阻止再次开仓;开仓资金仍可能超预算。测试失败数量还从上一轮的 3 项变成了 4 项。
## 3. P0致命或可能导致错误交易
### 3.1 IPO 不校验下单结果就写入成功锁,可能永久漏申购
位置:`py-client/strategy/ipo/boot.py:37-51`
`client.passorder()` 的返回值被完全忽略,随后无条件执行 `write_lockfile(lp)`。如果服务端以正常 HTTP 响应返回 `{"status": "failed"}`、空订单号或其他业务拒绝结果,本地仍会记录为已申购,后续每天都会跳过该证券。
影响:券商实际没有接受订单,但本地永久认为已经提交,造成漏申购。
建议:统一校验 `status == "success"``order_ref` 有效;只有明确受理后才能写锁。未知响应应告警并保持可对账状态,不能直接标记成功。
### 3.3 单只 IPO 异常会中止当天全部后续候选
位置:`py-client/strategy/ipo/boot.py:37-51`
候选循环内部没有单只证券级异常隔离。第一只证券的字段缺失、下单拒绝抛异常或锁文件写入失败,都会直接退出 `AutoBuyIpo()`后面的候选不会再尝试。APScheduler 会记录任务异常,但当天 10:00 不会自动重新执行整个任务。
影响:一只异常证券导致当天其他所有新股漏申购。
建议:每只候选独立 `try/except` 并记录证券代码;失败继续处理下一只。任务结束后汇总成功、跳过、失败数量。
### 3.4 趋势策略 `UNKNOWN` 状态仍不能阻止自动重复开仓
位置:
- `py-client/strategy/trend/state.py:83-89,184-237`
- `py-client/strategy/trend/boot.py:145-164`
- `py-client/strategy/trend/open.py:17-70`
`State.delete()` 会保留 `ING/UNKNOWN`,但 `RunOnce()` 生成开仓候选时只排除真实持仓;`open_signal()` 只检查 `OrderBook.busy()`。如果启动对账将订单标记成 `UNKNOWN`,同时券商活动委托列表暂时没有该订单,状态文件虽然存在,下一轮信号仍可再次下单。
此外,运行期调用 `delete()` 后不检查是否真的删除,就清除两个观察器,造成未决状态与观察状态不一致。
影响订单结果无法确认时可能重复买入未达到“UNKNOWN 禁止自动重复下单”的目标。
建议:开仓候选同时排除 `ING/UNKNOWN` 状态代码和活动买单代码;`delete()` 返回实际删除结果;每轮用最新订单簿重新对账状态。
## 4. P1重要业务逻辑问题
### 4.1 IPO 函数契约退化,现有 4 项测试全部报错
位置:
- `py-client/strategy/ipo/boot.py:22`
- `py-client/tests/test_ipo.py:52-136`
`AutoBuyIpo(now: datetime | None = None)` 被改成无参数函数测试无法注入确定时间4 项测试全部以 `TypeError: AutoBuyIpo() takes 0 positional arguments but 1 was given` 结束。APScheduler 并不要求删除可选参数;保留可选 `now` 同样可以无参调度。
测试当前也明确要求但实现未满足:返回成功数量、关闭客户端、本地重启防重、券商记录防重、单只拒绝不影响其他候选。
建议:恢复可选 `now` 参数,内部使用 `current = now or datetime.now()`;不要修改测试来掩盖业务契约缺失。
### 4.3 IPO 客户端从不关闭
位置:`py-client/strategy/ipo/boot.py:31-51`
每日任务创建新的 `httpx.Client`,成功、跳过和异常路径都没有调用 `close()`。长期运行会累积未及时释放的连接池资源。
建议:使用 `with Client(...) as client:`,现有 `Client` 已实现上下文管理器。
### 4.4 IPO 返回类型与真实返回值不一致
位置:`py-client/strategy/ipo/boot.py:22-51`
函数标注返回 `int`,只有禁用和非交易时间返回 0正常处理完候选后隐式返回 `None`,也没有统计提交成功数量。
影响:日志、监控和测试无法知道任务到底提交了多少只证券。
建议:维护 `success_count` 并在所有出口返回整数。
### 4.5 同证券同方向的多笔趋势委托仍会互相覆盖
位置:`py-client/strategy/trend/order.py:70-79,121-125`
订单缓存仍以 `BUY-code` / `SELL-code` 为唯一键。同证券同方向多笔委托只保留最后一笔,其他活动订单无法撤销或对账。
建议:系统委托号作为主键,方向与证券组合作为一对多活动索引。
### 4.6 过期撤单仍会处理已终结历史订单
位置:`py-client/strategy/trend/order.py:81-93`
撤单条件没有检查 `order.status in BUSY_STATUSES`。接口若返回历史订单,程序会对已成交、已撤、已失败订单反复发送撤单请求。
建议:仅处理活动状态,并为已发送撤单请求增加冷却或本地状态。
### 4.7 下单成功但状态落盘失败的安全策略不统一
位置:
- `py-client/strategy/trend/open.py:59-70`
- `py-client/strategy/trend/positions.py:175-183`
开仓保存失败只记日志并继续;补仓保存失败则异常退出本轮。两者都可能已经存在真实订单,却缺少可恢复的本地状态。
建议:落盘失败后将证券加入内存熔断集合、保留订单锁并持续重试;在完成真实订单对账前禁止该证券继续自动交易。
### 5.3 主程序仍有不可达代码、拼写错误和无效符号
位置:
- `py-client/main.py:5,15,100-101,127-128`
`StartTrend()` 正常情况下永久循环,其后的日志不可达;即便返回,`config.account_config.strateg` 也不存在。非 Windows 分支使用未定义的 `log.error``TimedRotatingFileHandler``GLOBAL_CONFIG_PATH` 未使用。
建议:启动成功日志放在进入永久循环前;统一使用 `logging`;删除无效导入和常量。
### 5.4 `State.delete()` 的保护过宽且静默
位置:`py-client/strategy/trend/state.py:83-89`
任何 `ING/UNKNOWN` 都会让删除静默失效,没有返回值、日志、订单证据或人工强制清理入口。这是基于状态字符串的全局拦截,不是真实订单对账,并已造成调用方误清观察器。
建议:把清理许可放入显式对账决策;`delete()` 返回布尔值或明确拒绝原因;人工确认终结后应有可审计的清理路径。
### 5.5 状态对账重复写盘并暴露中间状态
位置:`py-client/strategy/trend/state.py:91-117,119-146`
`reconcile()` 调用会自行保存的 `sync_positions()`,完成订单对账后再次保存。一次启动对账至少写盘两次,第一次还是未完成订单状态恢复的中间结果。
建议:内部同步只改内存,完整对账结束后一次性原子落盘。
### 5.6 调度器配置包含当前进程内不必要的重复任务替换
位置:`py-client/main.py:116-123`
调度器每次启动都是新实例,只添加一次固定 ID 任务,因此 `replace_existing=True` 在当前结构下没有实际作用。它无害,但属于多余防御参数,容易让人误以为使用了持久化任务仓库或存在重复注册路径。
建议:若没有持久化 job store 或重复注册,删除该参数;若未来启用持久化,再保留并补充任务版本策略。
## 6. 本轮已确认改善
- APScheduler 后台调度不会被 `StartTrend()` 的永久循环阻塞。
- `timezone="Asia/Shanghai"` 明确了每日 10:00 的业务时区。
- `coalesce=True``max_instances=1` 能防止任务积压补跑和并发重叠。
- `dev.yaml` 已显式配置 `signal_allow`,趋势信号不再因默认空列表而全部禁用。
- IPO 本地路径拼接和首次锁判断方向已经修正。
- 趋势部分成交判定的四类状态逻辑仍保留。
## 7. 建议整改顺序与验收门槛
1. 恢复 IPO 可选时间参数、成功计数和上下文管理器,让现有 4 项 IPO 测试先全部通过。
2. 增加真实交易日、券商委托/成交防重、下单结果校验和单只异常隔离。
3.`ING/UNKNOWN` 真正参与趋势开仓过滤,并补充重启未成交端到端测试。
4. 修复单笔与同轮开仓资金预算。
5. 重构订单簿一对多索引并限制撤单状态。
6. 清理启动脚本、重复 API 文件和死代码。
最低验收要求:现有 11 项测试全部通过;新增 IPO 返回失败但不写锁、锁丢失但券商已有订单、第一只拒绝而第二只成功、节假日不申购;新增趋势 `UNKNOWN` 无持仓且无活动委托时仍不重复开仓;最后在模拟账户完成下单、部分成交、撤单、重启对账闭环。