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.
This commit is contained in:
2026-07-12 23:39:45 +08:00
parent 292de31eaf
commit 8d55e414fa
11 changed files with 578 additions and 85 deletions
@@ -0,0 +1,90 @@
# 回测引擎 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 口径。