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

165 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.
# IPO / Trend 当前代码审计
> 后续实现更新:已按要求精简为 JSON v3订单终态后一次记账见 [trend-state.md](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](trend-state.md)。
## 结论
新的 pending 与业务账本分离、补仓首次成交计数、预开仓数量落盘、完整订单快照对账均已接入。但尚不能据此认定状态恢复与异常生命周期完整:空持仓快照仍可清空历史补仓记录;确定未提交/未受理的订单可能永久占用 pending撤单异常仍可阻断整轮状态对账。
优先处理 T12、T13、T14、T15。下文沿用原编号新发现从 T12 起编号,不把已解决的问题重复列为当前缺陷。
## 新发现Trend
### T12【高】一次空持仓快照会清除已完成账本恢复后补仓次数归零
位置:`strategy/trend/state.py:212``state.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:79``order.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:127``sdk/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:86``boot.py:132``boot.py:182`
证据refresh 在遍历订单中直接调用 cancel_by_id一次异常会使 refresh 退出RunOnce 在组合刷新异常分支直接 return已经拿到的其他订单成交快照也不会进入 reconcile。启动阶段同类异常会导致启动失败。
复现 R5快照包含超时委托撤单接口抛错state.reconcile 调用次数为 0。
影响单个撤单故障可以持续阻断全部证券的成交更新、止盈和补仓。T6 的逐持仓隔离在这个阶段尚未执行。
建议:先完成快照采集及状态对账;撤单采用逐订单异常边界,失败保持 busy并记录业务失败结果不阻断其他订单和证券。
## 新状态机的条件性风险与恢复边界
### T16【中条件性】结束 pending 后不能处理迟到的成交金额修正
位置:`strategy/trend/state.py:107``state.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:191``open.py:51``positions.py:41`
两条线程各自使用资金,开仓每个信号仍按完整 buy_value 计算。补仓的不确定委托只预留本轮估算金额,下轮又按新的 available 开始,未统一扣除未确认订单的潜在占款。市场价格变化也可能超出快照估算。
建议:账户级共享预算,明确券商已冻结资金和本地尚未反映的预留,避免漏计或重复扣减。
### T5【高】自动撤单不区分策略归属
位置:`strategy/trend/order.py:74`
超时撤单遍历全账户订单,没有本地订单前缀/策略归属限制。R5 同时确认手工来源订单会被传给撤单接口。建议防重参考全账户,自动撤单仅限明确归属本策略的订单。
### T10【中策略取舍】止盈门槛会阻断已激活网格回撤
位置:`strategy/trend/positions.py:108``libs/grid_take_profit.py`
低于 minimum_profit 即返回,不再观察峰值回撤;进程重启峰值也丢失。若要求激活后持续追踪,应保存激活/峰值;若最低利润是硬性卖出门槛,应明确此行为。
### T11【中】信号仅启动加载
位置:`strategy/trend/boot.py:69``libs/signal.py:17`
首次请求失败会得到空结果,此后不刷新;运行中的新增/撤销不生效。建议按业务时效定期刷新,并区分失败与正常空信号。
## IPO 当前问题
### I1【高】下单结果未检查就写完成锁
位置:`strategy/ipo/boot.py:53`
passorder 返回值被忽略随后无条件写锁。R6 使用 failed 业务响应,仍生成 A.SH.lock。当前服务端通常将失败表达为 HTTP 异常,该路径会被捕获;但 success 仅说明底层调用未抛异常,服务端未验证字符串化的订单引用,客户端也没有确认受理。
建议依据真实受理契约核查结果;不确定时保存待核查记录,不直接标记完成。
### I2【高】检查锁—下单—写锁不原子无券商对账
位置:`strategy/ipo/boot.py:49``libs/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 文件或真实网络交易请求。