问题确认
VeighNa 的 CTA 回测引擎(vnpy_ctastrategy 的 BacktestingEngine)撮合逻辑是:买入按 K 线的收盘价(或你设定滑点后)成交,价格来源就是你加载进 BarData 的 close_price。所以:
- 如果你加载的是前复权的主连数据,那么回测中的成交价 = 复权价
- 最终收益率 = (复权价卖出 - 复权价买入) / 复权价买入
在主力合约换月时,前复权会把旧合约价格整体平移对齐新合约,导致:
- 换月跳空消失 → 回测拿不到真实的换月损益(现实中换月是有真实成本/收益的)
- 历史价格失真 → 固定金额的保证金、手续费按失真价格计算,仓位规模也失真
- 长期回测绩效与现实不可复现
解决方案(你说的"信号与执行分离"在 VeighNa 中的做法)
方案一:双数据集回测(推荐,标准做法)
在回测中同时加载两套数据:
- 复权主连数据:只用于计算技术指标、生成信号(在
ArrayManager 中喂入复权价)
- 真实主力合约数据:用于撮合成交
具体实现需要修改回测引擎的撮合部分,或在策略层做价格映射:
- 回测时按时间对齐,每个交易日同时读复权价
adj_price 和真实价 real_price
- 策略信号用
adj_price 序列计算
- 触发信号后,用同一时刻的
real_price 作为成交价
- 止损止盈触发判断基于真实价格(或基于真实持仓盈亏)
实现途径:
- 数据入库时,给
BarData 保留真实价格,复权序列作为额外字段(可以把真实收盘价放进 open_interest 不现实,建议自定义数据结构或用数据库多存一张表)
- 继承
BacktestingEngine 重写 cross_limit_order,在撮合时查真实价格映射表
- 或者更简单的做法:在策略
on_bar 里存一个 signal_price → real_price 的比例(ratio = real_close / adj_close),下单价格用 信号价 × ratio 转换,止损逻辑同理
方案二:只用简单拼接的真实主力数据 + 换月处理
- 回测数据直接用真实价格简单拼接的主力连续数据(不做复权)
- 在策略里对指标计算做换月平滑处理(比如用收益率序列计算 MA,而不是价格序列)
- 这样成交价天然是真实价格,但指标处理稍复杂
方案三:用组合策略/价差模块
如果涉及跨合约(换月前后的两个合约)的精细模拟,vnpy_portfoliostrategy 模块支持多合约组合回测,可以分别订阅换月前后的真实合约,自行管理换月移仓逻辑——这是最贴近现实的做法,但开发成本最高。
简单验证
你可以用我查一下文档确认回测撮合细节:
文档也印证了这一点(来源:cta_backtester.md):
回测过程中策略发出委托的成交价不一定是原始下单的价格,而要由回测引擎基于当时的行情数据和下单价格进行撮合后算出。
这里的"当时的行情数据"就是你加载的 K 线数据——你喂进去的是复权价,撮合就用复权价,逐日盯市的持仓盈亏也全部基于复权价计算。所以收益失真是必然发生的。
总结建议
| 方案 |
工作量 |
真实性 |
适用场景 |
| 方案一:双数据集(信号用复权、成交用真实) |
中等,需改引擎或策略做价格映射 |
高 |
主力推荐,单合约策略 |
| 方案二:简单拼接真实数据 + 收益率序列算指标 |
小 |
中高 |
快速验证 |
| 方案三:portfoliostrategy 多合约分别回测 |
大 |
最高 |
需要精细模拟换月移仓 |
实盘时的对应做法:实盘本身不存在这个问题——实盘你订阅的是真实主力合约的 Tick,成交价天然是真实的;你只需在策略内部维护一个复权价格序列(换月时按比例调整历史缓存)用于指标计算,下单时直接用真实盘口价即可。这正是你说的"信号在虚拟层、执行在现实层"。
基于RAG技术开发的VeighNa AI助手,你的个人专属 Desk Quant