# 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 400;SDK 转为 APIError;place 仅记录并返回 False,未调用 state.reject。当前 reject 分支只识别响应字典中的 failed/rejected,而本服务端下单处理器正常只返回 success,失败主要通过 HTTP 错误表达。 复现 R3:mock 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,不再处理该订单。复现 R4:100 股终态成交金额 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) 不是空代码;非有限价格可绕过 <=0;int 浮点数量会截断;证券代码直接进入路径。建议有限正价格、严格整数数量、代码格式与路径字符限制。 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 文件或真实网络交易请求。