docs(design): 影子柜台设计v2(用户反馈):miniQMT实时订阅驱动(替代schtask轮询,与实盘同款)+实时价撮合+步长=策略自身周期(tick~月线)+每实例独立虚拟账户;对账兜底保留 [nas]
CI/CD / test (push) Successful in 19s
CI/CD / nas-deploy (push) Successful in 30s
CI/CD / nas-verify (push) Successful in 13s

This commit is contained in:
2026-08-14 09:05:03 +08:00
parent 869a758b89
commit 02ece66e86
+93 -85
View File
@@ -1,113 +1,121 @@
# 模拟盘影子交易柜台设计(验收第四轮 #4)
# 模拟盘影子交易柜台设计 v2(验收第四轮 #4
> 状态:设计稿(2026-08-14),未动工。用户拍板方向后按分期实施
> 背景:现有模拟盘「实走」= NAS 每晚 20:30 全量重放(`sanguo_trader/portfolio_paper.py`),
> 每日一个 step。对 15m 策略逻辑上不成立(日内 16 根 bar 只走 1 根),用户要求重设计为
> **影子交易柜台**:独立于 miniQMT 的自有撮合台 + 实际行情 + 多策略多账户互不干扰,
> ≈ 多个独立账号的实盘模拟。
> 状态:设计稿 v22026-08-14,按用户反馈重写驱动方式),未动工。
> v1 的「schtask 每 15 分钟定时拉数据」已废弃——用户明确:数据要与实盘完全一样
> 由 miniQMT 实时提供,步长由策略自己的周期决定(tick 到月线),15 分钟只是举例。
## 0. 一句话版(业务视角)
在 VPS 上放一个「模拟券商」:
- **行情**:跟实盘完全一样,从 miniQMT 实时订阅(tick / 各周期 bar);
- **账户**:每个策略实例一个独立虚拟账户(资金、持仓、净值曲线各自独立,互不干扰);
- **交易**:策略下单不发给券商,由模拟券商在**下单那一刻的实时价 + 滑点**本地成交;
- **周期**:每个策略声明自己的周期(tick/1m/5m/15m/30m/d/月线合成),柜台按各自的周期喂数据。
跟实盘唯一的区别就是订单不出门。
## 1. 目标 / 非目标
**目标**
- 盘中按 bar 到达驱动推进(15m 策略盘中每 15 分钟一个 step),非每日一次
- 自有撮合(复用 `sanguo_trader/matcher.py` 的 match_session 语义:NEXT_OPEN / CURRENT_CLOSE),
独立于 miniQMT——miniQMT 只在真实盘才用
- 多策略多账户完全隔离:每个 paper 账户独立 checkpoint / 独立推进 / 互不影响
`paper_accounts` 行级隔离已具备)
- 状态可恢复:进程重启 / 断电后从 checkpoint 续跑,不丢已结算 bar。
- 行情驱动 = 实盘同款:miniQMT(xtdata)实时推送,事件驱动,非定时轮询
- 步长 = 策略自身周期(tick 到日线/月线合成),柜台不预设统一步长。
- 本地模拟撮合:实时价 + 滑点 + 真实费率(佣金/印花税/最低佣金)
- 多策略多账户完全隔离:每实例独立虚拟账户独立盈亏曲线。
- 状态可恢复:进程重启后从断点续跑(checkpoint),不丢已结算数据
**非目标(MVP 不做)**
- tick 级实时撮合(bar 级撮合足够验证策略行为;tick 是后续量级)。
- 影子单同步真实 QMT 账户(真实现盘方向,见 `project-live-arch-shadow-mode`,本设计是其前置)。
- 订单真发券商(这是后续真实现盘;本设计正是它的前置演练场)。
- 撮合排队/部分成交等微观结构(实时价全额成交,先验证策略行为)。
- CTA 个股策略接入(先组合策略;CTA 的 vnpy 线保持现状)。
## 2. 关键可行性结论(已验证,2026-08-14)
bullet_trade 0.9.2 `BacktestEngine` 原生支持增量恢复所需的全部要素:
| 要素 | 引擎能力 | 用途 |
|------|---------|------|
| 持仓恢复 | 构造参数 `initial_positions=[{security, amount, avg_cost}]` | checkpoint 持仓续跑 |
| 资金恢复 | 构造参数 `initial_cash` | checkpoint 现金续跑 |
| 因子历史 | `attribute_history` **直查数据源**count 回看至 current_dt,不受引擎 start_date 窗口限制,见 `bullet_trade/data/api.py:2569` | 从 checkpoint+1bar 起跑无需暖机窗口,动量等因子历史完整 |
| 撮合 | match_session 语义(回放逐 bar 撮合已是引擎行为) | 影子柜台撮合即「引擎撮合 + 我们的费率」 |
→ **增量 step = 每次新建引擎,start=checkpoint 下一根 barend=当前时刻,
initial_cash/initial_positions=checkpoint 快照**。不需要给引擎打补丁,
不需要暖机窗口(不会重放旧调仓 → 不会双重成交)。
## 3. 架构
## 2. 架构
```
VPS唯一有盘中实时数据的机器,xtdata T+0
┌────────────────────────────────────────────────────┐
schtask sanguo-shadow-step(交易日 09:4515:15 每 15m
↓ 触发
sanguo_trader/shadow_desk.py step 全部 running 账户
│ ├─ xtdata 拉自 checkpoint 起的新 15m/日线 bar
│ ├─ 每账户: BacktestEngine(checkpoint 续跑, ≤新bar)
│ │ initial_positions/cash ← paper_checkpoint
│ ├─ 成交/持仓/净值 → paper 库(VPS 本地) │
│ └─ update checkpoint_date
└────────────────────────────────────────────────────┘
↓ 公网 webvnpy.mysanguo.online → VPS API
前端模拟盘页(现有,读库即得盘中刷新)
VPSminiQMT 客户端保持登录,唯一有实时行情的机器
┌───────────────────────────────────────────────────────
│ sanguo-shadow-desk 常驻进程(交易时段运行,开机自启)
├─ 行情层:xtdata 订阅(tick 推送 + 各周期 bar 推送)
│ 按每个策略实例声明的周期分发(月线=日线本地合成)
│ ├─ 账户层:每实例一个虚拟账户(现金/持仓/净值独立)
│ ├─ 撮合层:本地成交 = 下单时刻实时价 + 滑点 + 费率
│ │ (复用 sanguo_trader 费率/风控件,涨跌停/停牌拦截)
│ ├─ 落库:成交/持仓/净值 → paper 库(VPS 本地) │
│ └─ checkpoint:每成交/每 bar 结算后记断点(重启恢复用)
└───────────────────────────────────────────────────────
↓ 公网 webvnpy.mysanguo.online → VPS API
前端模拟盘页(现有,读库即得盘中实时刷新)
NAS:保留现有 20:30 日终全量重放(互为对照;日线账户仍可走 NAS)
兜底:收盘后日终对账(VPS 晚间跑一次,发现盘中断档则用日终全量重放补齐
——沿用现有 portfolio_paper 能力;另保留 NAS 20:30 日终重放作交叉对照)
```
**双轨说明**:同一账户只归一个引擎推进(按 `paper_accounts.engine` 列区分
`eod_replay`NAS 现状)/ `shadow`VPS 新)),避免两边都写净值。
## 3. 关键设计决策
## 4. 数据细节
### 3.1 行情:订阅推送,不轮询
- xtdata `subscribe_quote`(单标的周期 bar/ `subscribe_whole_quote`(全市场 tick 快照)
都是**推送**语义,与实盘网关行为一致;断线自动重连。
- 策略周期映射:tick → tick 推送;1m/5m/15m/30m/d → 对应周期 bar 推送;
月线/周线 → 日线本地合成(与 vnpy BarGenerator 同思路)。
- 需要历史因子(动量等):直查 VPS 本地库(unified provider),与实时推送互补。
- **行情源**xtdataVPS 本地,T+0 当天 bar;日线 + 15m 全周期,见
`xtdata-as-live-source`)。价格与 dbbardata 零漂移已验证;坑:volume ÷100。
- **因子历史**unified provider 读 VPS 本地库(`vps-local-data-layout`)。
- **step 频率**:账户 interval=15m → 每 15 分钟;interval=d → 收盘后一次
(15:15 那次统一结算当日)。非交易日 / xtdata 无新 bar → 幂等跳过(沿用
现有 `already stepped` 语义,粒度从「日」变「bar 时间戳」)。
### 3.2 撮合:实时价成交(与实盘市价单最接近)
- 订单到达即以下单时刻实时价 + 滑点成交(替代回放模式的 NEXT_OPEN/CURRENT_CLOSE
——那是没有实时价时的近似;现在有实时价,直接用)。
- 涨跌停/停牌/未成交约束复用 `sanguo_trader/limit.py` 既有逻辑。
- 费率按账户行配置(佣金/印花税/最低佣金/滑点),与回测同口径。
## 5. 存储变更(paper 库,向后兼容
### 3.3 恢复:checkpoint 增量(保留 v1 验证结论
进程重启/断电后不从头跑:引擎原生支持初始持仓
`initial_positions=[{security, amount, avg_cost}]`+ 初始资金恢复,
策略因子历史直查本地库不受引擎窗口限制 → 从断点下一根 bar 续跑,
无双重成交。对账用例(P0)显式覆盖跨除权日的复权正确性。
- `paper_accounts` + `engine` TEXT DEFAULT 'eod_replay'(新账户默认 'shadow')。
- `paper_checkpoint`(或复用现有 checkpoint 字段)扩展为:
`checkpoint_dt`(最后一根已结算 bar 的完整时间戳,15m 精度)+
`cash` + `positions_json` + `avg_cost`initial_positions 重建用)。
现有行级 `checkpoint_date`(日粒度)迁移:首步把日粒度视作当日 15:00。
- 成交/净值/持仓表不变(datetime 已含时间,15m 粒度天然兼容)。
### 3.4 策略状态约定
- 需要跨 bar 记忆的状态一律放持仓/账户(天然随 checkpoint 恢复);
- 策略内临时变量只放可重建的缓存(重启丢失不影响正确性)。
## 6. 调度
## 4. 存储变更(paper 库,向后兼容)
- VPS schtask `sanguo-shadow-step`:交易日 09:45/10:00/…/15:15 每 15 分钟
.bat 自愈模板沿用 `schtasks-system-bat-gotchas` 经验:timeout 失效用 ping、
断点续传、故障不阻塞下一轮)
- 幂等 + 单实例锁(文件锁,防上一轮 2-3 分钟未跑完时下一轮叠加)。
- step 入口同时暴露 CLI`python -m sanguo_trader.shadow_desk --account N`)便于手工补跑。
- `paper_accounts` + `engine` 列('eod_replay'NAS 现状)/ 'shadow'VPS 新),新账户默认 shadow)。
- checkpoint 扩展为 `checkpoint_dt`(最后已结算 bar 时间戳,tick/分钟精度)
+ `cash` + `positions_json` + `avg_cost`。旧日粒度行迁移:视作当日收盘
- 成交/净值/持仓表结构不变(datetime 字段天然容纳 tick/分钟精度)。
## 7. 分期
## 5. 运行方式
- VPS schtask `sanguo-shadow-desk`:交易时段前启动常驻进程(BootTrigger +
交易日 09:15 拉起,15:30 后自动退出/空闲驻留皆可,首版选驻留+收盘结算)。
- 单实例文件锁(防双开重复撮合)。
- CLI 手工入口:`python -m sanguo_trader.shadow_desk [--account N]`(补跑/调试)。
- miniQMT 登录依赖:现有数据采集 schtask 同样依赖它,无新增要求,
但影子柜台进程在交易时段**不能容忍长时间掉线**——掉线期间不撮合,
重连后由日终对账补(见 §2 兜底)。
## 6. 分期
| 期 | 内容 | 验收 |
|----|------|------|
| P0 | spikecheckpoint 续跑与全量重放对账——同账户同区间,「全量一次跑」vs「分 3 段 checkpoint 续跑」末持仓/净值一致(容差 1e-6 | 对账脚本绿 |
| P1 | shadow_desk.py step + engine 列 + checkpoint 扩展 + VPS schtask;日线账户先上(step=收盘一次,行为与现状等价但引擎换增量) | VPS 盘后 step 落库,前端可见 |
| P2 | 15m 账户盘中每 15m step;前端模拟盘页盘中自动刷新 | 盘中实时性验收 |
| P3(可选) | NAS EOD 重放降级为对账兜底;影子单同步 QMT(真实现盘前置) | — |
| P0 | 对账 spikecheckpoint 续跑 vs 全量重放,末持仓/净值一致(含跨除权用例 | 对账脚本绿 |
| P1 | 影子柜台常驻进程:bar 级订阅(分钟/日线)+ 实时价撮合 + 虚拟账户 + 落库 | 盘中下单成交可见,前端实时刷新 |
| P2 | tick 推送接入(tick 级策略)+ 月线/周线合成 | tick 策略可跑 |
| P3 | 日终对账自动化 + NAS 交叉对照;影子单同步 QMT(真实现盘前置) | — |
## 8. 风险
工程分期不影响使用语义:策略侧从第一天就按「声明周期」配置,
tick 级在 P2 放开。
- **xtdata 未起 / 盘中缺 bar**:幂等跳过 + 下轮补(bar 缺口 ≤ schtask 间隔时
一次补齐;断档大时告警人工触发全量对账)。
- **分红/拆分跨越 checkpoint**:引擎 `_processed_dividend_keys` 按次重放,
增量段跨除权日的复权处理需 P0 对账用例显式覆盖(构造跨除权区间)。
- **策略内全局状态**(g. 变量跨 bar 累积):increment 段内有效,跨 checkpoint 丢失
→ 约定策略把需持久化的状态写 context.portfolio(持仓即状态),g. 只放可重建缓存。
- **费用口径**:沿用账户行费率(commission/stamp_duty/min_commission/slippage),
与回测一致。
## 7. 风险
## 9. 关联
- **盘中掉线/miniQMT 掉登录**:断档期间不撮合,日终对账补齐;连续多日异常告警。
- **跨除权 checkpoint**P0 对账显式覆盖。
- **tick 级数据量**:tick 只在内存消费,落库仍为成交/净值/bar 级快照,库不膨胀;
但全市场 tick 订阅的内存/CPU 峰值需 P2 实测(先单标的/小池子放开)。
- **多账户规模**:虚拟账户互相隔离,几十个实例同进程无压力;
策略数上百时再谈多进程分片。
- memory: `paper-matching-mechanism`(撮合语义)、`project-live-arch-shadow-mode`
(实盘=影子模式既定方向)、`xtdata-as-live-source`VPS 数据源)
- 现状代码:`sanguo_trader/portfolio_paper.py`EOD 全量重放)、
`sanguo_trader/scheduler.py`NAS 20:30 job)、`sanguo_trader/matcher.py`
## 8. 关联
- memory: `project-live-arch-shadow-mode`(实盘=影子模式既定方向,本设计是其前置)、
`xtdata-as-live-source`xtdata 行情能力与坑 volume÷100)、
`paper-matching-mechanism`(旧回放撮合语义,本设计取代其实走部分)
- 现状代码:`sanguo_trader/portfolio_paper.py`(EOD 全量重放→降级为对账兜底)、
`sanguo_trader/scheduler.py`NAS 20:30 job)、`sanguo_trader/matcher.py``sanguo_trader/limit.py`