# 回测引擎 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`,容器内跑真 vnpy:DoubleMa 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.5,min_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 那套 scalars(cta_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 口径。