# 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` 无持仓且无活动委托时仍不重复开仓;最后在模拟账户完成下单、部分成交、撤单、重启对账闭环。