feat client.

This commit is contained in:
2026-08-31 13:00:22 +08:00
parent a946c5b53d
commit 1b6f5a9f03
33 changed files with 544 additions and 1284 deletions

99
docs/state.md Normal file
View File

@@ -0,0 +1,99 @@
# 趋势策略状态机审计
审计日期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 调用契约问题,无法作为状态机全绿的证明。