8d55e414fa
审计发现包装层系统性失真(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.
5.4 KiB
5.4 KiB
回测引擎 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:210statistics.update(metrics_result.scalars)覆盖 vnpy 键)。前端展示字段映射到 empyrical scalars。
口径统一(MEDIUM 顺带)
- benchmark 对齐:
metrics.py:32dropna()→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.pyimport except、cta_engine:127-134config except、:229-232metrics 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.pyrun 端点透传 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 口径。