Files
sanguo_vnpy_v2/docs/design/backtest-ashare-adapter-plan.md
T
claude_dev 8d55e414fa fix(backtest): A股适配层—定寸/做空拦截/真实费用/口径统一(Phase1+2)
审计发现包装层系统性失真(2 CRITICAL+7 HIGH),vnpy底座可信但A股场景未适配:
- C1 定寸: engine.size=N(满仓手数),策略volume=1手=N股,开平对称(pos归零)
- C2 做空拦截: SHORT+OPEN拒单,long-only,SHORT+CLOSE平多允许
- H3 A股费用: AShareDailyResult重算(佣金保底5元/印花税卖方/过户费沪市)
- H4 收益口径: simple return从balance算(不再用vnpy log return喂empyrical)
- H5+口径: benchmark ffill对齐不缩样本; sizing_shares_per_lot暴露
- H7 退化检测: 零成交/空数据标degenerate不静默done
- H8 task_id: optimize/factor用uuid4(原id()内存地址)
- 静默except改warning

验证: 容器内真实vnpy DoubleMa 600000 2022-2024, total_return 1e-6→42.3%,
end_balance 100万→142万, SHORT+OPEN成交0笔, N=7800股/手.
22 backtest测试全绿(含集成测试), API健康200.
2026-07-12 23:39:45 +08:00

91 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 回测引擎 A 股适配层 — Phase 1+2 实施计划
> 起因:审计发现包装层在"A 股股票回测"场景系统性失真(2 个 CRITICAL + 7 个 HIGH)。
> 见 `memory/backtest-engine-soundness.md`(待写)+ 审计 agent 报告。
> vnpy 源码(项目实际 import 份):容器内 site-packages;参考副本 `~/.openclaw/knowledge_base/vnpy_ctastrategy/`。
> 约束:**vnpy_v4.4.0 源码零修改**,全部用子类化/包装在外层实现。Mac 无 vnpy_ctastrategy,测试在 NAS 容器内跑。
## 目标(Phase 1+2
回测结果**诚实**(不虚构做空盈亏、不 1 股空转)且**准确**(A 股真实费用、收益/年化口径一致)。
---
## Phase 1 — 诚实(3 项)
### C1 定寸:按资金 + 价格算手数
- 机制:策略传 volume 是"满仓单位数"vnpy 模板默认 1 = 1 个满仓),包装层换算成实际股数。
- 公式(每次下单按当时 price 重算):`shares_per_unit = floor(capital * position_pct / price / 100) * 100`(按手取整 100 股);`actual_volume = volume * shares_per_unit`
- 落点:`AShareBacktestingEngine.send_order` 覆写,重算 volume 再 super。
### C2 做空拦截:long-only
- 机制:SSE/SZSE 标的,`direction==SHORT and offset==OPEN` 直接拒单(返回 [],warning 日志)。允许 SHORT+CLOSE(平多)。
- 落点:`AShareBacktestingEngine.send_order` 覆写开头判断。
- T+1:日 bar 策略层面影响小(信号收盘、次日执行),Phase 3 再处理。
### H9 真实集成测试
- 新增 `tests/backtest/test_integration_ashare.py``@pytest.mark.integration`,容器内跑真 vnpyDoubleMa 600000 2024-01~2024-06,断言:
- `end_balance != capital`(非空转)
- 全部 trade `direction != SHORT or offset == CLOSE`(做空拦截)
- 存在 `trade.volume > 100`(定寸生效,满仓手数)
- `total_return` 绝对值 > 1e-3(非噪声)
---
## Phase 2 — 准确(4 项)
### H3 A 股费用模型
- `AShareDailyResult(DailyResult)` 覆写 `calculate_pnl`,每笔 trade
- `turnover = volume * size * price`size=1
- `commission += max(turnover * rate, min_commission)`(双边,rate 默认 0.00025 万 2.5min_commission 默认 5 元)
- `stamp_duty += turnover * stamp_duty_rate if direction==SHORT else 0`(卖方 0.0005
- `transfer_fee += turnover * transfer_fee_rate if 沪市 else 0`0.00001
- `net_pnl = total_pnl - commission - stamp_duty - transfer_fee - slippage`
- 把 stamp_duty/transfer_fee 存为实例属性(持久化可见)。
- 落点:`AShareBacktestingEngine` 覆写 DailyResult 创建处用 `AShareDailyResult`agent 读源码定位 `self.daily_results[date]` 创建点,可能需覆写 run_backtesting 里的工厂或设类属性)。
### H4 log→simple return
- vnpy `df["return"]``np.log(...)`backtesting.py:353)。empyrical 期望 simple return。
- 修:`compute_metrics` 入参改为从 `daily_df["balance"]` 自算 `s = balance.pct_change().fillna(0)`,不再依赖 vnpy 的 log 列。删 cta_engine:198-204 的三路 fallback。
### H5 年化统一 252
- empyrical `period='daily'` 内部 252,已对。确认 statistics 最终用的是 empyrical 那套 scalarscta_engine:210 `statistics.update(metrics_result.scalars)` 覆盖 vnpy 键)。前端展示字段映射到 empyrical scalars。
### 口径统一(MEDIUM 顺带)
- benchmark 对齐:`metrics.py:32` `dropna()``benchmark.reindex(daily_df.index).ffill().fillna(0)`,不丢策略日期。
- equity 单一源:`/equity-curve`(绝对 balance)与 `/benchmark-curve`(相对)尺度对齐——统一改相对净值 `balance/capital`benchmark 用 `cum_returns`,两图同尺度。
---
## 顺带修(成本几乎为零,同文件)
- H8 `runner.py:66,93``id(grid)`/`id(factor_names)``uuid4().hex[:8]`
- H7 `cta_engine`:零成交/空数据 → `status="degenerate"` + `statistics["degenerate_reason"]`,不静默 done。
- 静默吞错改 warning`strategy_registry.py` import except、`cta_engine:127-134` config except、`:229-232` metrics except —— 加 `logging.warning` + 失败时 statistics 塞错误字段。
---
## schemas/routes 改动(最小)
- `CtaBacktestRequest``capital: float = 1_000_000``position_pct: float = 0.95`+ 可选 commission/stamp_duty/transfer_fee/min_commission,给默认值,前端先不暴露)。
- `routes.py` run 端点透传 capital/position_pct → `run_cta_backtest`
- `run_cta_backtest` 签名加这些参数,传给 `AShareBacktestingEngine`
---
## 文件清单
| 文件 | 动作 |
|------|------|
| `sanguo_backtest/ashare_engine.py` | **新**AShareDailyResult + AShareBacktestingEngine |
| `sanguo_backtest/metrics.py` | 改:simple return、ffill 对齐、单一 equity |
| `sanguo_backtest/cta_engine.py` | 改:换 AShare 引擎、传参、删 fallback、degenerate 检测、静默吞改 warning |
| `sanguo_api/schemas.py` | 改:加 capital/position_pct |
| `sanguo_api/routes.py` | 改:透传参数 |
| `sanguo_orchestrator/runner.py` | 改:task_id uuid |
| `sanguo_backtest/strategy_registry.py` | 改:except 加 warning |
| `tests/backtest/test_integration_ashare.py` | **新**:真实集成测试 |
## 不做(Phase 3 候选)
T+1、组合回测(需 vnpy_portfoliostrategy)、optimize parent 分组、SQLite WAL、MockExchange 重构、滑点/年化可配置化、rolling alpha/beta 口径。