Files
big-qmt/docs/ipo-trend-audit.md
2026-09-05 13:53:26 +08:00

12 KiB
Raw Blame History

IPO / Trend 当前代码审计

后续实现更新:已按要求精简为 JSON v3订单终态后一次记账trend-state.md。T12 的持仓缺席自动删除已移除T13 的提交前写盘失败已增加占位回滚并验证。下方对应复现保留为 v2 历史证据。其余未修复项仍需单独处理v3 不再保存子单历史及成交增量,旧版本迁移与清仓后重新开仓的操作边界见新说明。

审计日期2026-09-05本轮重新读取当前工作树重点复核 JSON v2 状态机。范围为 py-client/strategy/{ipo,trend} 及直接依赖,不包含 ZT 策略。以下路径相对于 py-client/,服务端路径从仓库根目录起算。

本轮只更新审计文档,未修改策略源码。已清理的 tests/*.py 不重建;采用临时目录和 mock 进行隔离复现,没有调用真实柜台。本文取代上一轮联合审计的当前结论;状态格式说明见 trend-state.md

结论

新的 pending 与业务账本分离、补仓首次成交计数、预开仓数量落盘、完整订单快照对账均已接入。但尚不能据此认定状态恢复与异常生命周期完整:空持仓快照仍可清空历史补仓记录;确定未提交/未受理的订单可能永久占用 pending撤单异常仍可阻断整轮状态对账。

优先处理 T12、T13、T14、T15。下文沿用原编号新发现从 T12 起编号,不把已解决的问题重复列为当前缺陷。

新发现Trend

T12【高】一次空持仓快照会清除已完成账本恢复后补仓次数归零

位置:strategy/trend/state.py:212state.py:124;入口 boot.py:182

证据:对所有“曾出现于持仓、当前不在持仓、无 pending”的证券立即删除 items 与 seen_positions。持仓下次恢复时按首次接管重新建立底仓added_num 从零开始。SDK 的组合解析还会将缺失 positions 字段视为空字典。

复现 R1已有底仓并完成一次补仓 → reconcile([], []) → 原记录被删除 → 持仓恢复 → added_num 从 1 变成 0。

影响:历史补仓量价丢失,亏损补仓档位可能重新使用。这与已修复的“空订单快照丢 pending”是不同路径。当前文档已声明依赖持仓快照完整但代码没有验证此条件。

建议:持仓缺席进入核查流程,不立即删除账本;结合卖出成交、明确清仓或可靠的持仓二次查询确认后结束证券生命周期。缺失字段不能自动视为有效空账户。

T13【高】提交前写盘失败会留下实际上未发送的 pending

位置:strategy/trend/state.py:79order.py:114

证据begin 先修改 items、pending、索引和 dirty再调用 save。save 抛错后没有回滚place 因异常没有调用柜台。后续 reconcile 的 save 可以把这个未发送记录写入磁盘。

复现 R2模拟 Path.write_text 抛出磁盘错误;柜台调用次数为 0但 busy(A) 为真;磁盘恢复后执行 reconcile 并重载文件busy(A) 仍为真。

影响该证券持续被阻止买入后续没有真实订单可供终态对账180 秒日志也不会解除锁定。

建议:将提交前登记视为内存/磁盘事务,明确写盘失败时回滚本次占位,或记录“确定未发送”并走安全撤销流程;与请求发出后结果不确定的情形区分。

T14【高】服务端明确 HTTP 400 拒绝也被永久保留为待确认

位置:strategy/trend/order.py:127sdk/client.py:67、服务端 api/qmt_rest_new.py:235

证据:服务端参数解析失败时在实际 passorder 调用前抛 HTTP 400SDK 转为 APIErrorplace 仅记录并返回 False未调用 state.reject。当前 reject 分支只识别响应字典中的 failed/rejected而本服务端下单处理器正常只返回 success失败主要通过 HTTP 错误表达。

复现 R3mock APIError(400),清除 SimpleCache 并重新加载 State 后,证券仍 busy。

影响:确定未受理的请求无法自动恢复;纠正参数后仍不能下单。

建议:针对已确认的服务端契约区分“明确未受理”和“不确定”。明确的参数拒绝可结束 pending网络超时、502 等不能一概按失败释放。

T15【高】撤单异常仍会阻断完整状态对账及整轮持仓管理

位置:strategy/trend/order.py:86boot.py:132boot.py:182

证据refresh 在遍历订单中直接调用 cancel_by_id一次异常会使 refresh 退出RunOnce 在组合刷新异常分支直接 return已经拿到的其他订单成交快照也不会进入 reconcile。启动阶段同类异常会导致启动失败。

复现 R5快照包含超时委托撤单接口抛错state.reconcile 调用次数为 0。

影响单个撤单故障可以持续阻断全部证券的成交更新、止盈和补仓。T6 的逐持仓隔离在这个阶段尚未执行。

建议:先完成快照采集及状态对账;撤单采用逐订单异常边界,失败保持 busy并记录业务失败结果不阻断其他订单和证券。

新状态机的条件性风险与恢复边界

T16【中条件性】结束 pending 后不能处理迟到的成交金额修正

位置:strategy/trend/state.py:107state.py:197

pending 结束即删除子单数据和已记账基准,后续只遍历 pending不再处理该订单。复现 R4100 股终态成交金额 1000 元完成后,再收到金额 1050 元的同订单快照base_cost 仍为 10 元。

是否实际触发取决于柜台终态金额是否可能修正,本轮没有实盘证据。若上游保证终态价格最终不变,可将其作为明确契约;否则应短期保留已完成订单的记账基准并支持差额修正。

迁移与核查:已知限制,不认定为新回归

位置:strategy/trend/state.py:233

  • 旧 ING 的 expected_qty 迁移为 0完成条件要求大于 0因此必须人工核实后补齐当前没有专门的核查/解除接口。
  • 旧文件可能只保存最后一次补仓量价,新累计 added_amount 不能凭空恢复全部历史;迁移有备份和告警,但数值不能直接解释为完整历史成本。
  • pending 超时仅告警,未实现按订单号主动远程查询。提交意图落盘后、请求真正发出前崩溃,也需要人工核实;这与 T13 已知写盘失败未回滚不同。
  • 终态识别要求系统订单号、正确的本地标识、可汇总申报量和成交价格。如果拒单没有系统订单号,或 QMT 取消态数量语义不同于模型推定pending 可能无法自动结束。需要以真实回报核对,不能仅凭 mock 宣布兼容。
  • 仅提供单进程互斥,不支持同账户同状态文件的多进程并发写入。

仍存在的既有问题

T4【高】开仓和补仓缺少统一预算

位置:strategy/trend/boot.py:191open.py:51positions.py:41

两条线程各自使用资金,开仓每个信号仍按完整 buy_value 计算。补仓的不确定委托只预留本轮估算金额,下轮又按新的 available 开始,未统一扣除未确认订单的潜在占款。市场价格变化也可能超出快照估算。

建议:账户级共享预算,明确券商已冻结资金和本地尚未反映的预留,避免漏计或重复扣减。

T5【高】自动撤单不区分策略归属

位置:strategy/trend/order.py:74

超时撤单遍历全账户订单,没有本地订单前缀/策略归属限制。R5 同时确认手工来源订单会被传给撤单接口。建议防重参考全账户,自动撤单仅限明确归属本策略的订单。

T10【中策略取舍】止盈门槛会阻断已激活网格回撤

位置:strategy/trend/positions.py:108libs/grid_take_profit.py

低于 minimum_profit 即返回,不再观察峰值回撤;进程重启峰值也丢失。若要求激活后持续追踪,应保存激活/峰值;若最低利润是硬性卖出门槛,应明确此行为。

T11【中】信号仅启动加载

位置:strategy/trend/boot.py:69libs/signal.py:17

首次请求失败会得到空结果,此后不刷新;运行中的新增/撤销不生效。建议按业务时效定期刷新,并区分失败与正常空信号。

IPO 当前问题

I1【高】下单结果未检查就写完成锁

位置:strategy/ipo/boot.py:53

passorder 返回值被忽略随后无条件写锁。R6 使用 failed 业务响应,仍生成 A.SH.lock。当前服务端通常将失败表达为 HTTP 异常,该路径会被捕获;但 success 仅说明底层调用未抛异常,服务端未验证字符串化的订单引用,客户端也没有确认受理。

建议依据真实受理契约核查结果;不确定时保存待核查记录,不直接标记完成。

I2【高】检查锁—下单—写锁不原子无券商对账

位置:strategy/ipo/boot.py:49libs/lockfile.py:9

并发调用可同时下单;受理后超时/写锁失败会使后续重新提交。建议账户/发行事件级原子占位与幂等标识,不确定结果先核查。

I3【高共享目录场景】标记不区分账户或发行事件

位置:strategy/ipo/boot.py:49

只有证券代码作为文件名;多账户共享目录时相互阻止申购,旧标记也无法区分新的发行事件。建议加入账户与发行标识。

I4【中】数据校验不完整

位置:strategy/ipo/boot.py:40

str(None) 不是空代码;非有限价格可绕过 <=0int 浮点数量会截断;证券代码直接进入路径。建议有限正价格、严格整数数量、代码格式与路径字符限制。

IPO 已正确逐条处理 list[dict],使用 with Client候选异常不终止后续候选本轮未发现这些路径退化。

已确认的有效修复

  • T1 原空订单快照丢 pending已修复本地待确认记录独立持久化缺席不删除。T12 是持仓账本清理的另一条路径。
  • T2 原过滤取消子单导致误判:已修复;启动和每轮对账均使用 portfolio.orders 完整数据。
  • T3 原受理后才写订单关联:已修复;当前提交前保存计划与本地 ID。提交前失败的恢复问题另列 T13。
  • T6 下单网络/解码异常与逐证券异常边界已接入。
  • T7 统一 finally先关闭线程池再关闭 Client初始化异常可释放资源。
  • T8 excluded_codes 在开仓与持仓管理均检查。
  • T9 缺失金额导致均价低估:当前保留未定价子单,延后数量/金额记账,不按零金额算均价。
  • busy_keys 与 SimpleCache 都在 busy/place 验证;补仓首次成交计一次,已完成首次接管规则与 expected_qty 持久化。

保留配置与性能观察

此前要求跳过的行为仍保留:大盘恒允许、不足一手强制 100 股、现金安全线仅限开仓、固定亏损档位与未生效的两个风控配置。时间退出条件仍为 >=15:00交易时间只判断工作日/时段。

状态查找使用字典/集合,无变化不写盘;但每个新买单都在 OrderBook.mutex 内序列化整个状态文件并同步写盘,同时持有 State 锁。批量下单的耗时随账本大小和磁盘延迟增长;“最高性能”没有基准测试支持。可先测每轮耗时、文件大小、持锁时间,再考虑批量意图预留与一次落盘;不可为减少写入而取消提交前持久化保障。

本轮验证

6 组隔离复现已执行R1 空持仓重置次数、R2 未发送订单残留、R3 HTTP 400 锁定、R4 终态后金额修正忽略、R5 撤单异常中断对账/误触手工委托、R6 IPO failed 响应写锁。

R4 属于有条件的上游契约风险;其余复现验证代码在所述输入/故障下的行为,不等价于断言柜台必然出现该故障。没有新增 tests 文件或真实网络交易请求。