2026-09-05 11:46:31 +08:00
# IPO / Trend 当前代码审计
2026-09-05 13:53:26 +08:00
> 后续实现更新:已按要求精简为 JSON v3, 订单终态后一次记账, 见 [trend-state.md](trend-state.md)。T12 的持仓缺席自动删除已移除; T13 的提交前写盘失败已增加占位回滚并验证。下方对应复现保留为 v2 历史证据。其余未修复项仍需单独处理; v3 不再保存子单历史及成交增量,旧版本迁移与清仓后重新开仓的操作边界见新说明。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
审计日期: 2026-09-05; 本轮重新读取当前工作树, 重点复核 JSON v2 状态机。范围为 `py-client/strategy/{ipo,trend}` 及直接依赖,不包含 ZT 策略。以下路径相对于 `py-client/` ,服务端路径从仓库根目录起算。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
本轮只更新审计文档,未修改策略源码。已清理的 tests/*.py 不重建;采用临时目录和 mock 进行隔离复现,没有调用真实柜台。本文取代上一轮联合审计的当前结论;状态格式说明见 [trend-state.md ](trend-state.md )。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 结论
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
新的 pending 与业务账本分离、补仓首次成交计数、预开仓数量落盘、完整订单快照对账均已接入。但尚不能据此认定状态恢复与异常生命周期完整:空持仓快照仍可清空历史补仓记录;确定未提交/未受理的订单可能永久占用 pending; 撤单异常仍可阻断整轮状态对账。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
优先处理 T12、T13、T14、T15。下文沿用原编号, 新发现从 T12 起编号,不把已解决的问题重复列为当前缺陷。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 新发现: Trend
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T12【高】一次空持仓快照会清除已完成账本, 恢复后补仓次数归零
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/state.py:212` 、`state.py:124` ;入口 `boot.py:182` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
证据:对所有“曾出现于持仓、当前不在持仓、无 pending”的证券立即删除 items 与 seen_positions。持仓下次恢复时按首次接管重新建立底仓, added_num 从零开始。SDK 的组合解析还会将缺失 positions 字段视为空字典。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
复现 R1: 已有底仓并完成一次补仓 → reconcile([], []) → 原记录被删除 → 持仓恢复 → added_num 从 1 变成 0。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
影响:历史补仓量价丢失,亏损补仓档位可能重新使用。这与已修复的“空订单快照丢 pending”是不同路径。当前文档已声明依赖持仓快照完整, 但代码没有验证此条件。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
建议:持仓缺席进入核查流程,不立即删除账本;结合卖出成交、明确清仓或可靠的持仓二次查询确认后结束证券生命周期。缺失字段不能自动视为有效空账户。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T13【高】提交前写盘失败, 会留下实际上未发送的 pending
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/state.py:79` 、`order.py:114` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
证据: begin 先修改 items、pending、索引和 dirty, 再调用 save。save 抛错后没有回滚; place 因异常没有调用柜台。后续 reconcile 的 save 可以把这个未发送记录写入磁盘。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
复现 R2: 模拟 Path.write_text 抛出磁盘错误;柜台调用次数为 0, 但 busy(A) 为真;磁盘恢复后执行 reconcile 并重载文件, busy(A) 仍为真。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
影响: 该证券持续被阻止买入; 后续没有真实订单可供终态对账, 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, 并记录业务失败结果, 不阻断其他订单和证券。
## 新状态机的条件性风险与恢复边界
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T16【中, 条件性】结束 pending 后不能处理迟到的成交金额修正
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/state.py:107` 、`state.py:197` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
pending 结束即删除子单数据和已记账基准,后续只遍历 pending, 不再处理该订单。复现 R4: 100 股终态成交金额 1000 元完成后,再收到金额 1050 元的同订单快照, base_cost 仍为 10 元。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
是否实际触发取决于柜台终态金额是否可能修正,本轮没有实盘证据。若上游保证终态价格最终不变,可将其作为明确契约;否则应短期保留已完成订单的记账基准并支持差额修正。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### 迁移与核查:已知限制,不认定为新回归
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/state.py:233` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
- 旧 ING 的 expected_qty 迁移为 0, 完成条件要求大于 0, 因此必须人工核实后补齐; 当前没有专门的核查/解除接口。
- 旧文件可能只保存最后一次补仓量价,新累计 added_amount 不能凭空恢复全部历史;迁移有备份和告警,但数值不能直接解释为完整历史成本。
- pending 超时仅告警,未实现按订单号主动远程查询。提交意图落盘后、请求真正发出前崩溃,也需要人工核实;这与 T13 已知写盘失败未回滚不同。
- 终态识别要求系统订单号、正确的本地标识、可汇总申报量和成交价格。如果拒单没有系统订单号,或 QMT 取消态数量语义不同于模型推定, pending 可能无法自动结束。需要以真实回报核对,不能仅凭 mock 宣布兼容。
- 仅提供单进程互斥,不支持同账户同状态文件的多进程并发写入。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 仍存在的既有问题
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T4【高】开仓和补仓缺少统一预算
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/boot.py:191` 、`open.py:51` 、`positions.py:41` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
两条线程各自使用资金,开仓每个信号仍按完整 buy_value 计算。补仓的不确定委托只预留本轮估算金额,下轮又按新的 available 开始,未统一扣除未确认订单的潜在占款。市场价格变化也可能超出快照估算。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
建议:账户级共享预算,明确券商已冻结资金和本地尚未反映的预留,避免漏计或重复扣减。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T5【高】自动撤单不区分策略归属
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/order.py:74` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
超时撤单遍历全账户订单,没有本地订单前缀/策略归属限制。R5 同时确认手工来源订单会被传给撤单接口。建议防重参考全账户,自动撤单仅限明确归属本策略的订单。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T10【中, 策略取舍】止盈门槛会阻断已激活网格回撤
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`strategy/trend/positions.py:108` 、`libs/grid_take_profit.py` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
低于 minimum_profit 即返回,不再观察峰值回撤;进程重启峰值也丢失。若要求激活后持续追踪,应保存激活/峰值;若最低利润是硬性卖出门槛,应明确此行为。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
### T11【中】信号仅启动加载
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
位置:`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` 。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
str(None) 不是空代码;非有限价格可绕过 <=0; int 浮点数量会截断;证券代码直接进入路径。建议有限正价格、严格整数数量、代码格式与路径字符限制。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
IPO 已正确逐条处理 list[dict],使用 with Client, 候选异常不终止后续候选; 本轮未发现这些路径退化。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 已确认的有效修复
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
- T1 原空订单快照丢 pending: 已修复; 本地待确认记录独立持久化, 缺席不删除。T12 是持仓账本清理的另一条路径。
- T2 原过滤取消子单导致误判:已修复;启动和每轮对账均使用 portfolio.orders 完整数据。
- T3 原受理后才写订单关联:已修复;当前提交前保存计划与本地 ID。提交前失败的恢复问题另列 T13。
- T6 下单网络/解码异常与逐证券异常边界已接入。
- T7 统一 finally: 先关闭线程池, 再关闭 Client, 初始化异常可释放资源。
- T8 excluded_codes 在开仓与持仓管理均检查。
- T9 缺失金额导致均价低估:当前保留未定价子单,延后数量/金额记账,不按零金额算均价。
- busy_keys 与 SimpleCache 都在 busy/place 验证;补仓首次成交计一次,已完成首次接管规则与 expected_qty 持久化。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 保留配置与性能观察
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
此前要求跳过的行为仍保留:大盘恒允许、不足一手强制 100 股、现金安全线仅限开仓、固定亏损档位与未生效的两个风控配置。时间退出条件仍为 >=15:00, 交易时间只判断工作日/时段。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
状态查找使用字典/集合,无变化不写盘;但每个新买单都在 OrderBook.mutex 内序列化整个状态文件并同步写盘,同时持有 State 锁。批量下单的耗时随账本大小和磁盘延迟增长;“最高性能”没有基准测试支持。可先测每轮耗时、文件大小、持锁时间,再考虑批量意图预留与一次落盘;不可为减少写入而取消提交前持久化保障。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
## 本轮验证
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
6 组隔离复现已执行: R1 空持仓重置次数、R2 未发送订单残留、R3 HTTP 400 锁定、R4 终态后金额修正忽略、R5 撤单异常中断对账/误触手工委托、R6 IPO failed 响应写锁。
2026-09-05 11:46:31 +08:00
2026-09-05 13:53:26 +08:00
R4 属于有条件的上游契约风险;其余复现验证代码在所述输入/故障下的行为,不等价于断言柜台必然出现该故障。没有新增 tests 文件或真实网络交易请求。