diff --git a/docs/superpowers/specs/2026-07-07-phase3c-paper-trading-design.md b/docs/superpowers/specs/2026-07-07-phase3c-paper-trading-design.md new file mode 100644 index 0000000..6be94ce --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-phase3c-paper-trading-design.md @@ -0,0 +1,358 @@ +# Phase 3c:模拟盘(Paper Trading)设计 + +> 日期:2026-07-07(v2,吸收架构 + A 股业务双 review) +> 阶段:Phase 3c(C 期) +> 状态:设计待审阅 +> 维护:Main Agent + +--- + +## 1. 背景与目标 + +**已交付**:Phase 1 数据层(A 股日 K / 15min / 5min / 1min parquet + SQLite);Phase 2 因子 + 回测引擎;Phase 3a 研究 API;Phase 3b 投研 + 回测 Web 控制台(`vnpy.mysanguo.top`)。 + +**C 期目标**:建 A 股**模拟盘引擎**——策略在"未见过的数据"上 forward 跑,纸面撮合,跟踪虚拟账户,为 D 期实盘做前置演练。 + +**4 期路线**(用户确认):投研 → 回测 → **模拟(本期)** → 实盘(D 期,路线待重新评估,见 §3.4)。 + +**两种模式**(都做,A 先 C 后): +- **A 回放模式**:选历史区间逐根 bar forward 重放,一次跑完。验证策略是否过拟合。 +- **C 实走模式**:每日收盘后定时增量喂当天 bar,持续跟踪。实盘前演练。 + +--- + +## 2. 范围 + +| 类别 | 内容 | +|---|---| +| ✅ 多频率引擎 | `interval` 可配:日频 + 15min(数据现成),5min/1min 预留 | +| ✅ A 股撮合(板块感知)| 涨跌停按板块(主板±10/ST±5/创业科创±20/北交所±30)、封板判据、T+1、资金 T+0、100 股、佣金+印花税+过户费+最低佣金 | +| ✅ 多撮合时点 | `match_session`:next_open(收盘型)/ current_close(尾盘型)/ call_auction(预留)| +| ✅ 复权双数据源 | 信号/因子用 qfq,撮合/涨跌停/均价用 raw | +| ✅ 一对多账户 | 总账(对齐实盘)+ 分户归因 | +| ✅ A 回放 + C 实走 | 共用引擎 | +| ✅ 前端 | 点亮 B 期预留"模拟"入口 | +| ✅ Issue #3 顺带 | 费率/资金参数化 | +| ⚠️ 分期(首版标注限制)| 分红送股事件、归因软限额、科创板 200 股手数、集合竞价撮合 | +| ❌ 盘中实时(tick 级)/ 实盘交易 | D 期 | + +**策略谱系**(用户确认):日频收盘型 + 日内型(尾盘抓涨停、次日开板卖)共存,故不固定频率、需多撮合时点。 + +--- + +## 3. 调研依据 + +### 3.1 vnpy 能力边界 +- vnpy **核心库**只给积木(`EventEngine` + 数据模型 + `BaseGateway` + `OmsEngine`)。 +- 纸面撮合(`vnpy_paperaccount`)、live CTA 引擎(`vnpy_ctastrategy`)是独立项目。**`vnpy_ctastrategy` 是 pip 依赖**(容器有,本机可能无 → 代码 lazy import + fallback,沿用现有 `cta_engine.py:69` 模式)。 +- 项目现有回测用 `vnpy_ctastrategy` 的 **`CtaTemplate`(`on_bar(bar)` 单根)**,不是 `AlphaStrategy`(`on_bars dict`)。 +- `BacktestingEngine` 本身就是个"假 cta_engine"(回测拦截 `send_order`)——**PaperCtaEngine 可参考它的策略桥接**,区别只是逐根 forward + A 股规则 + 多策略。 + +### 3.2 借鉴对象 +| 对象 | 借鉴 | 不借鉴 | +|------|------|--------| +| **freqtrade dry-run** | live/paper 分支;内存挂单+SQLite 落账;订单 ID 命名;配置驱动 | orderbook 滑点;T+0 假设 | +| **vnpy_paperaccount** | cross_order/update_position/calculate_pnl 拆分;持仓冻结;均价计算 | monkey-patch;tick 级撮合 | +| **现有 BacktestingEngine** | 策略类 + cta_engine 桥接(send_order 拦截);CtaTemplate on_bar 单根 | 一次性 load_data | + +### 3.3 数据源与复权(关键) +- **历史 parquet**:NAS 日线 + 15min(5350 标的,1.5G)+ 5min/1min。`sanguo_data/datafeed.py:117 adjustflag="2"` → 现有数据是**前复权(qfq)**。 +- **复权矛盾**(业务 review CRITICAL):策略信号/因子需价格连续(qfq);**撮合/涨跌停/持仓成本需真实交易价(raw)**。一套数据用到底会让涨跌停判断系统性失真。 +- **双数据源设计**:DataSource 加 `adjust` 参数(`"qfq"` 信号用 / `"raw"` 撮合用)。raw 数据首版通过 akshare `adjustflag="3"`(不复权)下载补齐,或存复权因子表运行时还原。 +- **实走当日**:akshare `stock_zh_a_hist`(主)+ tushare(兜底),T 日 20:00 后稳。 + +### 3.4 ⚠️ D 期风险(不影响 C 期) +miniQMT 据称 2026/7/6 停止新申请,`xtquant` 受影响。C 期数据源(akshare/tushare + 历史 parquet)与此无关。D 期路线启动前需单独讨论。 + +### 3.5 关键洞察 +> 日频/分钟级模拟盘比 tick 级 paper 简单一个量级。核心三件套:逐根 bar 重放 + 纸面撮合 + 账户跟踪。 + +--- + +## 4. 整体架构 + +``` +浏览器 (Vue,"模拟"入口点亮) ↕ HTTPS vnpy.mysanguo.top +FastAPI sanguo_api (:8000) + ├─ /api/v1/paper/* → 模拟盘路由 + ├─ /ws/paper/{id} → 进度推送(轮询共享 DB) + └─ sanguo_orchestrator → submit_paper(回放,ProcessPool)/ APScheduler(实走) + ↕ + sanguo_trader/(新模块) + PaperEngine ─ Matcher ─ Account(总账) ─ StrategyRunner×N(分户) + │ │ │ │ + DataSource limit.py PositionLedger(per-symbol 持仓对象) + │ + Persistence(SQLite 落账 + checkpoint) + ↕ + sanguo_data(qfq/raw 双源) + vnpy 数据模型 + CtaTemplate 策略类 +``` + +--- + +## 5. 组件设计(`sanguo_trader/`,单一职责) + +> review 修正:明确 PositionLedger 是**单标的持仓对象**(非全局计算模块);涨跌停抽独立纯函数 `limit.py`;StrategyRunner 是 **CtaTemplate 适配器**。 + +| 组件 | 职责 | 关键接口 | +|------|------|---------| +| **PaperEngine** | 主循环:逐根 bar → 喂各 StrategyRunner → 收 OrderRequest → 调 Matcher → 双层记账 → 盯市 → 入库 | `run()`;`step(bar)`(实走用)| +| **PaperCtaEngine** | **策略适配器**:实现 cta_engine 接口,注入 CtaTemplate,拦截 `send_order()` 转 OrderRequest(参考 BacktestingEngine 桥接)| `send_order(...)`→收集订单;`on_bar(bar)` 转发策略 | +| **Matcher** | A 股撮合纯函数:`cross_order(order, bars, prev_close_raw, cfg) → Trade \| Reject` | 无副作用,TDD 核心 | +| **limit.py** | 涨跌停纯函数:板块幅度查表 + 封板判断(一字板/T 字板)| `limit_price(symbol, prev_close_raw, board)`;`is_locked(bar)` | +| **Account** | 总账:cash(资金 T+0)、合并持仓 `dict[symbol→PositionLedger]`、净值 | `apply_trade()`;`mark_to_market(bar)` | +| **StrategyRunner** | 分户账:持 PaperCtaEngine + 策略实例 + 分户持仓 `dict[symbol→PositionLedger]` + PnL | `on_bar()`;`apply_trade()` | +| **PositionLedger** | **单标的持仓对象**:volume / frozen(T+1) / avg_price(raw 计);含 update/freeze 方法 | `update(trade)`;`unfreeze()` | +| **Persistence** | SQLite 落账(4 表 + checkpoint);启动恢复实走 job | `save_*()`;`load_checkpoint()`;`restore_live_jobs()` | +| **DataSource** | 统一行情:`iter_bars(symbols, start, end, interval, adjust="qfq"\|"raw")`;`fetch_day(symbol, date, interval, adjust)` | 双复权源 | + +**持仓状态归属**(review H-2):Account 持总账 `dict[symbol, PositionLedger]`,每个 StrategyRunner 持自己的分户 `dict[symbol, PositionLedger]`。PositionLedger 是被持有的对象,非全局单例。均价/T+1 计算是它的方法。涨跌停在 `limit.py` 独立。 + +**文件组织**(200-400 行/文件): +``` +sanguo_trader/ +├── __init__.py +├── engine.py # PaperEngine +├── cta_adapter.py # PaperCtaEngine(策略适配器) +├── matcher.py # Matcher(A股撮合,纯函数) +├── limit.py # 涨跌停(板块表 + 封板判断,纯函数) +├── account.py # Account 总账 +├── strategy_runner.py # StrategyRunner 分户 +├── position_ledger.py # PositionLedger 单标的持仓对象 +├── persistence.py # SQLite + checkpoint + job 恢复 +├── data_source.py # 行情双源(qfq/raw) +├── models.py # PaperAccount/Order/Trade/Reject 数据类 +└── scheduler.py # APScheduler(C 实走) +``` + +--- + +## 6. 撮合规则(A 股核心,业务 review 大改) + +### 6.1 撮合时点(`match_session`,新增) +策略在 `PaperAccount.strategies[].match_session` 声明,PaperEngine 按此路由: + +| match_session | 撮合价 | 适用 | lookahead 约束 | +|---|---|---|---| +| `next_open`(默认)| 下一根 bar 的 open | 收盘型策略 | 安全(信号当根、撮合下根)| +| `current_close` | 当根 bar 的 close | **尾盘抓涨停型**(当日尾盘买、次日卖)| **契约**:策略 `on_bar` 内**不得使用当根 close/high/low**(否则 lookahead)| +| `call_auction` | 预留 | 集合竞价 | 首版不实现 | + +> 业务 review CRITICAL:抓涨停策略实盘是"当日尾盘买",若推到 next_open,涨停股次日一字板直接拒单,策略永远买不进——逻辑失真。故必须支持 current_close。 + +### 6.2 涨跌停(板块感知,review CRITICAL) + +**幅度表**(`limit.py` 查表,按 symbol 前缀判断板块): + +| 板块 | 普通 | ST/*ST | 新股首日/前 5 日 | +|---|---|---|---| +| 主板(沪/深)| ±10% | ±5% | ±44% | +| 创业板(300)| ±20% | ±5% | 前 5 日不设限 | +| 科创板(688)| ±20% | ±5% | 前 5 日不设限 | +| 北交所 | ±30% | ±30% | 前 5 日不设限 | + +**价位计算**:`limit_price = prev_close_raw × (1 ± ratio)`,四舍五入到 pricetick。**必须用 raw 价格**(§3.3),不能用 qfq。 + +**封板判据**(收紧,review CRITICAL)——用 raw OHLC: +- **严格一字板**(`open==high==low==close==limit_price`)→ 买单拒单(无对手盘) +- **T 字板 / 秒板**(`open==limit 且 close==limit 且 low limit_price` 或非封板形态)→ 按规则成交 +- 跌停对称 + +> OHLC 近似的已知乐观偏差:T 字板实际可能瞬间开板成交,首版保守拒单会在归因里标注。 + +### 6.3 T+1 与资金 T+0(review HIGH) +- **股票 T+1**:买入成交量当日进 `PositionLedger.frozen`,次日开盘前 `frozen→available`(解冻后才可卖)。 +- **资金 T+0**:卖出回笼资金**当日即可用于再买入**(`Account.cash` 卖出即时增加)。这是 A 股硬规则,对短周期策略(抓涨停→次日卖→再买)影响大。 + +### 6.4 费用(review HIGH,参数化 + Issue #3) +``` +commission = max(volume × price × rate, min_commission) # 最低佣金 5 元 +stamp_duty = volume × price × stamp_duty_rate # 仅卖出,0.05%(2023.8.28 起) +transfer_fee = volume × price × transfer_fee_rate × 2 # 沪深双向,0.001% +total_cost = commission + stamp_duty + transfer_fee +``` +默认值:`rate=0.0003`、`min_commission=5.0`、`stamp_duty_rate=0.0005`、`transfer_fee_rate=0.00001`、`slippage=0`。全部 `PaperAccount` 字段可配(**Issue #3 落地**)。 + +### 6.5 手数(review MEDIUM) +- 买入:主板/创业板向下取整到 100 股;**科创板首版统一按 100 股处理,标注已知限制**(实盘 200 股起 +1 递增)。 +- 卖出:允许零股(≤ available 即可),不取整——退出持仓的基本操作。 + +--- + +## 7. 账户模型(一对多,对齐实盘) + +**双层记账**——一笔带 `strategy_id` 的成交同时更新两层: + +| 层 | 内容 | 回答 | +|----|------|------| +| **Account(总账)** | 合并 cash(资金 T+0)、合并持仓、总净值 | "账户整体赚不赚"(= 实盘真实状态)| +| **StrategyRunner(分户)** | 该策略标记持仓 + PnL | "哪个策略在赚/亏" | + +**资金占用与归因公平**(review MEDIUM): +- **首版**:多策略共享资金池,**先到先得**,买单检查 Account 总现金,不足则拒单(`reject_reason="insufficient_cash"`)。 +- **拒单归因**:`paper_trades.reject_reason` 记 `blocked_by_strategy=`(谁的持仓占了钱),前端可见。 +- **分期**(C-S2 后):每策略 `max_allocation` 软限额 + 资金占用成本(按无风险利率日扣),消除"先到后到"的不可复现性。首版标注此简化。 + +--- + +## 8. 数据契约 + +### 8.1 PaperAccount(`paper_accounts` 表,review H-1/H-3/H-4 补字段) +``` +id, task_id, owner_id(默认"admin",多用户预留), name, +mode("replay"|"live"), interval("d"|"15m"|"5m"), +symbols(JSON), strategies(JSON: [{name, class_name, params, match_session}]), +initial_capital, rate, slippage, size, pricetick, +stamp_duty_rate, transfer_fee_rate, min_commission, # 费用(6.4) +status("pending"|"running"|"done"|"failed"), +start_date, end_date, # 回放 +last_run_date, next_run_at, scheduler_job_id, # 实走(H-3 恢复用) +checkpoint_date, # 续跑 checkpoint(H-4) +error_msg, created_at, updated_at +``` + +### 8.2 PaperTrade(`paper_trades`,含拒单) +``` +id, account_id, strategy_id, datetime, symbol, +direction, offset, match_session, price, volume, +commission, stamp_duty, transfer_fee, +rejected(bool), reject_reason, # "limit_up_locked"/"insufficient_cash"/"blocked_by_strategy=s1" +bar_date, strategy_id_blocked_by(可空) +``` + +### 8.3 PaperPosition(`paper_positions`,每日快照) +``` +account_id, scope("account"|"strategy:"), symbol, date, +volume, frozen, avg_price(raw), market_value, updated_at +``` +> `scope` 区分总账行(`account`)与分户行(`strategy:id`)。 + +### 8.4 DailyBalance(`paper_daily_balance`) +``` +account_id, date, cash, market_value, total_equity, +per_strategy_pnl(JSON: {strategy_id: {pnl, equity}}), is_checkpoint(bool) +``` + +所有接口返回 JSON 安全值(Timestamp→str)。 + +--- + +## 9. 任务模型 & 调度(review C-1 修正) + +### 9.1 A 回放(orchestrator + 共享 DB 进度) +- `task_type="paper"`,ProcessPoolExecutor spawn 一次性任务。 +- **进度机制**(解 C-1):worker 直接写 **NAS 共享 SQLite 文件**(sqlite WAL 多进程兼容)——每 N 根 bar 落一次 `paper_daily_balance(is_checkpoint=true)` + 更新 `paper_accounts.checkpoint_date`。主进程轮询 DB(或 WS 推 stage 级进度:"回放中,已处理至 2024-06-15")。 +- **续跑**:worker 启动读 `checkpoint_date`,从其后继续;中断不丢(状态在 DB 文件)。 +- 进度粒度:stage 级(每 N 根 bar 更新一次,非逐 bar)。前端体验可接受(回放非实时)。 + +### 9.2 C 实走(APScheduler + 启动恢复) +- 创建实走盘 → 注册 APScheduler job(每日 20:30),job_id 存 `paper_accounts.scheduler_job_id`。 +- 每次触发:`DataSource.fetch_day(adjust="raw")` + `(adjust="qfq")` 拉当日 → `PaperEngine.step(bar)` 增量喂 → 更新 `last_run_date`。 +- **启动恢复**(review H-3):容器启动调 `Persistence.restore_live_jobs()`,遍历 `status="running" AND mode="live"` 的账户重新注册 job。状态全在 SQLite,重启不丢。 +- 走停:`POST /paper/{id}/start|stop` 注册/移除 job。 + +> 容器单 worker(B 期已定)+ APScheduler 在 FastAPI 主进程内,兼容。 + +--- + +## 10. API(`/api/v1/paper/*`,JWT,沿用 B 期模式) + +| 方法 | 路径 | 用途 | +|------|------|------| +| POST | `/paper/create` | 建盘(mode/interval/策略集含 match_session/标的/资金/费率)| +| GET | `/paper/{id}` | 状态+配置 | +| GET | `/paper` | 列表(?mode=&status=,按 owner_id 过滤)| +| GET | `/paper/{id}/equity` | 账户净值曲线 | +| GET | `/paper/{id}/strategies` | 分策略 PnL 归因 | +| GET | `/paper/{id}/positions` | 持仓(总账/分户)| +| GET | `/paper/{id}/trades` | 成交(含拒单+原因+blocked_by)| +| POST | `/paper/{id}/start` `/stop` | 实走走停 | +| WS | `/ws/paper/{id}?token=` | 进度(事件格式:`{type:"progress",bar_date,equity,stage}`)| + +--- + +## 11. 前端(点亮"模拟"入口,沿用 B 期技术栈) + +``` +模拟 ✅ + ├ 新建 /paper/new 模式 + 策略集(多选,每策略 match_session) + 标的集 + 区间/频率 + 资金 + 费率参数 + ├ 进度 /paper/progress/:id WS + ├ 结果 /paper/result/:id 净值曲线(总) + 分策略归因 + 持仓 + 成交(拒单高亮) + └ 实走 /paper/live/:id 今日信号 + 持仓快照 +``` + +--- + +## 12. 切片计划(A 先 C 后,每片闭环) + +| 切片 | 内容 | 验收 | +|------|------|------| +| **C-S0** | 引擎核心 TDD:`limit.py`(板块表+封板)+ `Matcher`(match_session/费率/T+1/资金T+0)+ `PositionLedger` + `models` + **Issue #3 费率参数化** | 单测全覆盖(各板块涨跌停、一字/T字板、current_close/next_open、T+1、资金T+0、最低佣金 5 元)| +| **C-S1** | A 回放端到端:`PaperCtaEngine`(策略适配)+ `PaperEngine` + `DataSource`(qfq/raw 双源 + 补 `read_parquet_15min`)+ orchestrator + 共享 DB 进度 + API + 前端结果页。**引擎从一开始支持多 StrategyRunner**(M-3) | 跑一个收盘型策略一段历史,净值/持仓/成交(含拒单);抓涨停策略用 current_close 可买入 | +| **C-S2** | 多策略分户归因 + 前端归因展示 + 拒单归因(blocked_by) | 一个账户跑 2 策略,看分策略 PnL + 拒单归因 | +| **C-S3** | C 实走:akshare/tushare DataSource + APScheduler + 启动恢复 + 续跑 + 前端实走态 | 创建实走盘,连续几天看每日信号入账;重启容器 job 自动恢复 | +| **分期(C-S3 后或下期)** | 分红送股事件(送股/转增/现金分红/停牌盯市,事件源 dividend_detail)、归因软限额+占用成本、科创板 200 股手数、集合竞价撮合 | — | + +--- + +## 13. 测试 + +- **limit.py TDD**:各板块幅度查表、一字板/T 字板/开板判断(raw OHLC)。 +- **Matcher TDD**(核心):next_open/current_close 两种时点、各板块涨跌停封板拒单、开板成交、T+1(次日才能卖)、资金 T+0(卖后即买)、100 股取整、卖出零股、佣金 max(.,5)、印花税仅卖、过户费双向。 +- **PositionLedger**:加仓/减仓/反手均价(raw)、冻结/解冻。 +- **Account/StrategyRunner**:双层记账一致性、资金 T+0、拒单归因。 +- **PaperEngine 集成**:已知策略 + 已知数据 → 已知净值;可用简单 case 对齐 BacktestingEngine 交叉验证。 +- **回放端到端冒烟**:`scripts/smoke_phase3c.py`。 +- pytest + 覆盖 80%+。 + +--- + +## 14. 错误处理 + +| 场景 | 处理 | +|------|------| +| bar 缺失(停牌)| 跳过信号/拒单;持仓市值按**前一日收盘价**盯市(不算 0)| +| 策略抛异常 | **隔离**:catch + 记该 `strategy_id` error_msg,不影响其他策略/账户 | +| 撮合边界(封板/停牌)| 拒单 + `reject_reason` 落表,前端可见 | +| 资金不足 | 拒单 + `blocked_by_strategy`,不部分成交(首版)| +| 实走数据源失败 | akshare→tushare 兜底;连续失败暂停 job + error_msg | +| **分红除权**(首版限制)| **未处理**:净值在除权日有跳变,已知限制,归因标注。C-S3 后补事件处理 | +| 复权一致性 | Matcher/limit/均价强制用 raw;DataSource 双源,类型不匹配时报错 | + +--- + +## 15. 与 D 期衔接(预留) + +- PaperEngine 留 **live/paper 分支**(freqtrade `if dry_run`):接实盘只换 DataSource 为实时 gateway + 加 live 下单。 +- ⚠️ D 期实盘路线待重新评估(§3.4),C 完成后单独讨论。 + +--- + +## 16. 风险与约束 + +| 风险/约束 | 处理 | +|---|---| +| 复权 qfq 不能直接撮合 | 双数据源(raw);首版 akshare 补 raw | +| 15min 数据量大 | interval 参数化;checkpoint 续跑 | +| vnpy 零修改 | 只复用数据模型 + CtaTemplate;适配在 sanguo_trader | +| 单 worker + 常驻 scheduler | APScheduler 主进程;回放走 ProcessPool + 共享 DB | +| 涨跌停无 tick | OHLC 近似,T 字板保守拒单,标注乐观偏差 | +| akshare 限频 | 间隔 ≥3s;双源切换 | +| NAS CPU 弱 | 标的集默认关注列表(不默认全 5350)| + +--- + +## 17. 开放项(实现阶段确认) + +- raw 数据补齐方式:akshare 重下 vs 复权因子表还原(C-S1 评估)。 +- checkpoint 间隔:15min 每 500 根 bar(可调)。 +- current_close 的 lookahead 契约如何强制(策略白名单 vs 运行时检测)。 +- 标的范围默认空(用户填关注列表)。 + +--- + +## 参考文档 +- Phase 3b 设计:`docs/superpowers/specs/2026-07-07-phase3b-vue-frontend-design.md` +- 部署实况:`docs/deployment/nas-deploy-plan.md` +- 调研依据:freqtrade dry-run / vnpy_paperaccount / akshare+tushare(§3)