# 趋势策略状态机审计 审计日期:2026-08-31 ## 结论 当前状态机的职责明确为:按账户和策略持久化每只证券的底仓信息与**补仓业务数据**。补仓委托提交成功后立即写入状态机是必要且正确的;30 秒轮询中的对账用于用券商快照校正订单状态和接管持仓,不能替代补仓次数等业务数据的记录。 状态机主链路可运行,但补仓单被撤销、拒绝或部分成交时,没有完整的状态回写规则;这会使 `added_status` 和 `added_num` 与实际成交结果不一致。该问题应在实盘前明确处理。 ## 数据模型与持久化 状态文件路径为:`{qmt_data_dir}/{strategy}_{account_id}_state.json`。 每个证券对应一个 `StateItem`: | 数据 | 含义 | 写入来源 | | --- | --- | --- | | `base_qty`、`base_cost`、`base_status` | 首次接管时的底仓数量、成本及状态 | `sync_positions()` | | `added_num` | 已提交的补仓次数 | 补仓委托提交成功后立即递增 | | `added_order_id`、`added_qty`、`added_cost`、`added_status` | 最近一笔补仓委托及状态 | 补仓提交后写入;30 秒对账尝试校正状态 | `State.save()` 先写临时文件,再以 `replace()` 原子替换正式 JSON;能避免半写入文件,但不构成“券商下单 + 本地持久化”的跨系统事务。 ## 运行流程 ```text 启动 ├─ 刷新券商订单 → OrderBook.data ├─ 查询持仓 └─ State.reconcile(持仓, 订单快照) 每 30 秒的 RunOnce ├─ 刷新订单;失败则本轮退出 ├─ 查询资产、市场、持仓 ├─ State.reconcile(持仓, OrderBook.data) ├─ 开仓:OrderBook 方向锁防重;不写 State └─ 补仓:下单成功 → 立即写 StateItem 并持久化 ``` `OrderBook` 与 `State` 的边界如下: - `OrderBook.lock`:进程内的同方向未决委托锁,防止同轮或相邻轮重复提交。 - `OrderBook.data`:本轮的进行中或完成订单快照,提供给 `State.reconcile()`。 - `State`:跨进程保存持仓补仓层级及最近补仓记录;它不是开仓防重的唯一来源。 ## 对账和清理规则 `State.reconcile()` 在每轮执行以下操作: 1. 对尚未接管的真实持仓创建状态,记录为已完成底仓。 2. 以 `local_order_id` 匹配本地订单号;若拆单全部为状态 `56`,将底仓或补仓状态更新为 `OK`,否则保持 `ING`。 3. 删除不再存在真实持仓的证券状态。 4. 保存状态文件。 这意味着:持仓是状态项是否保留的最终依据;补仓计数是业务状态,不会由当前持仓反推或重置。 ## 发现的问题 ### P1:撤销、拒绝和部分成交没有完整回写规则 `OrderBook.refresh()` 仅将进行中状态和完成状态 `56` 放入 `OrderBook.data`。撤销、拒绝等订单会被过滤;超时订单发出 `cancel_by_id()` 后也会直接跳过。`State.reconcile()` 因此找不到对应 `added_order_id`,只能保持原有 `added_status=ING`。 影响:状态文件可能长期显示“处理中”,而 `added_num` 已经递增。系统目前未定义以下情况是否消耗补仓档位: - 委托完全拒绝; - 撤单且零成交; - 部分成交后撤单。 建议:使对账可获得终态订单,或在订单簿中显式传递终态映射;为上述三种情况定义 `added_status`、`added_qty` 和 `added_num` 的最终规则,并增加回归测试。 ### P1:下单成功但状态文件保存失败时,重启恢复会丢失补仓层级 补仓路径顺序是 `orders.place()` 成功后,再执行 `state.set()` 与 `state.save()`。若保存失败,当前进程仍有订单簿方向锁,但程序重启后状态文件不含本次补仓。`sync_positions()` 只能接管当前持仓,不能从持仓推导历史补仓次数,因此存在重复使用补仓档位的风险。 建议:捕获并明确处理 `state.save()` 失败;至少将其作为不可忽略的交易一致性故障告警。若要求重启后绝不重复补仓,需要可恢复的补仓事件记录或可查询的成交历史作为补偿来源。 ### P2:每轮对账会执行两次状态文件写入 `reconcile()` 调用 `sync_positions()`,而 `sync_positions()` 无条件 `save()`;`reconcile()` 结束时又再次 `save()`。这不改变正确性,但每 30 秒至少两次磁盘原子替换。 建议:让 `sync_positions()` 返回是否发生变化,由 `reconcile()` 统一进行一次保存。 ### P2:底仓订单字段与当前开仓流程不一致 开仓路径不再创建 `StateItem`,而首次出现真实持仓时由 `sync_positions()` 建立底仓状态。因此新开仓的 `base_order_id` 通常为空,底仓订单状态对账主要只适用于遗留/外部写入的数据。 建议:保留该字段前,应明确它是否仍承担审计用途;否则可在后续数据模型整理时移除无效的底仓订单状态分支。 ## 测试覆盖 现有测试覆盖了拆单全成后状态改为 `OK`、补仓档位边界和订单簿方向锁,但没有覆盖: - 补仓单撤销、拒绝、部分成交后的状态和次数; - `state.save()` 在下单成功后失败的恢复处理; - 启动恢复后的补仓层级保持; - 状态项随平仓删除的边界。 在 `py-client` 目录运行 `python -m compileall -q .` 通过。现有全量单测仍有趋势替身接口不匹配及 IPO 调用契约问题,无法作为状态机全绿的证明。