Files
big-qmt/docs/state.md
2026-08-31 13:00:22 +08:00

5.4 KiB
Raw Blame History

趋势策略状态机审计

审计日期2026-08-31

结论

当前状态机的职责明确为:按账户和策略持久化每只证券的底仓信息与补仓业务数据。补仓委托提交成功后立即写入状态机是必要且正确的30 秒轮询中的对账用于用券商快照校正订单状态和接管持仓,不能替代补仓次数等业务数据的记录。

状态机主链路可运行,但补仓单被撤销、拒绝或部分成交时,没有完整的状态回写规则;这会使 added_statusadded_num 与实际成交结果不一致。该问题应在实盘前明确处理。

数据模型与持久化

状态文件路径为:{qmt_data_dir}/{strategy}_{account_id}_state.json

每个证券对应一个 StateItem

数据 含义 写入来源
base_qtybase_costbase_status 首次接管时的底仓数量、成本及状态 sync_positions()
added_num 已提交的补仓次数 补仓委托提交成功后立即递增
added_order_idadded_qtyadded_costadded_status 最近一笔补仓委托及状态 补仓提交后写入30 秒对账尝试校正状态

State.save() 先写临时文件,再以 replace() 原子替换正式 JSON能避免半写入文件但不构成“券商下单 + 本地持久化”的跨系统事务。

运行流程

启动
  ├─ 刷新券商订单 → OrderBook.data
  ├─ 查询持仓
  └─ State.reconcile(持仓, 订单快照)

每 30 秒的 RunOnce
  ├─ 刷新订单;失败则本轮退出
  ├─ 查询资产、市场、持仓
  ├─ State.reconcile(持仓, OrderBook.data)
  ├─ 开仓OrderBook 方向锁防重;不写 State
  └─ 补仓:下单成功 → 立即写 StateItem 并持久化

OrderBookState 的边界如下:

  • 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_statusadded_qtyadded_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 调用契约问题,无法作为状态机全绿的证明。