5.4 KiB
趋势策略状态机审计
审计日期: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;能避免半写入文件,但不构成“券商下单 + 本地持久化”的跨系统事务。
运行流程
启动
├─ 刷新券商订单 → OrderBook.data
├─ 查询持仓
└─ State.reconcile(持仓, 订单快照)
每 30 秒的 RunOnce
├─ 刷新订单;失败则本轮退出
├─ 查询资产、市场、持仓
├─ State.reconcile(持仓, OrderBook.data)
├─ 开仓:OrderBook 方向锁防重;不写 State
└─ 补仓:下单成功 → 立即写 StateItem 并持久化
OrderBook 与 State 的边界如下:
OrderBook.lock:进程内的同方向未决委托锁,防止同轮或相邻轮重复提交。OrderBook.data:本轮的进行中或完成订单快照,提供给State.reconcile()。State:跨进程保存持仓补仓层级及最近补仓记录;它不是开仓防重的唯一来源。
对账和清理规则
State.reconcile() 在每轮执行以下操作:
- 对尚未接管的真实持仓创建状态,记录为已完成底仓。
- 以
local_order_id匹配本地订单号;若拆单全部为状态56,将底仓或补仓状态更新为OK,否则保持ING。 - 删除不再存在真实持仓的证券状态。
- 保存状态文件。
这意味着:持仓是状态项是否保留的最终依据;补仓计数是业务状态,不会由当前持仓反推或重置。
发现的问题
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 调用契约问题,无法作为状态机全绿的证明。