1. 回测基础:vn.py回测引擎架构、回测与实盘差异、为什么需要模拟滑点与手续费
1.1 vn.py回测引擎到底长什么样?
说实话,我第一次接触vn.py的回测引擎时,第一反应是——这玩意儿比我想象的复杂多了。但用顺手之后,你会发现它的设计其实很优雅。
vn.py的回测引擎,核心就干三件事:喂数据、跑策略、算结果。但中间的过程,远比这三个词丰富。
我习惯把回测引擎拆成四个模块来看:
- 数据模块:负责加载历史K线或Tick数据,按时间顺序喂给策略
- 策略模块:你的交易逻辑就写在这里,比如金叉买入、死叉卖出
- 撮合模块:这是最关键的——模拟订单怎么成交、以什么价格成交
- 记录模块:记录每一笔交易、持仓变化、资金曲线
嗯,这里要注意,vn.py的默认撮合方式是市价撮合。什么意思?就是你发出买入指令,系统直接按下一根Bar的开盘价给你成交。这在理想世界里没问题,但实盘哪有这么便宜的事?
核心要点:回测引擎的撮合逻辑,直接决定了你的回测结果靠不靠谱。默认的市价撮合,说白了就是「理想状态」,和实盘差距很大。
1.2 回测和实盘,差在哪?
我经常跟团队里的人说一句话:「回测是天堂,实盘是地狱」。你想想看,回测时你面对的是已经走完的K线,所有数据都是确定的。但实盘呢?
我总结了一下,回测和实盘至少有这几点差异:
| 维度 | 回测 | 实盘 |
|---|---|---|
| 成交价格 | 按收盘价或开盘价成交 | 按买卖盘口撮合,有滑点 |
| 成交速度 | 瞬间成交 | 有网络延迟、排队延迟 |
| 手续费 | 可忽略或固定值 | 按交易所规则收取,复杂 |
| 市场冲击 | 不存在 | 大单会推动价格 |
| 数据质量 | 历史数据,无噪声 | 实时数据,有跳空、断线 |
说白了,回测是一个「无摩擦」的理想环境。而实盘处处是摩擦。如果你回测时不把这些摩擦考虑进去,那结果就是——回测赚得盆满钵满,实盘亏得底裤都不剩。
我曾经见过一个团队,回测年化收益50%,实盘跑了三个月直接腰斩。为什么?就是因为回测时没考虑滑点和手续费。你想想看,高频策略一天交易几十次,每次滑点一个tick,手续费再扣一点,利润早就被吃干净了。
1.3 为什么非要模拟滑点和手续费?
这个问题,我直接给你答案:不模拟滑点和手续费的回测,就是耍流氓。
咱们来拆解一下:
- 滑点:你看到的价格和实际成交的价格之间的差值。比如你看到卖一价是10.00,你下单买入,结果成交在10.02。这0.02就是滑点。在流动性差的品种上,滑点可能大到让你怀疑人生。
- 手续费:交易所和券商收的钱。别小看这千分之一、万分之几,高频策略跑下来,手续费可能比你的策略收益还高。
我个人习惯在回测时,至少设置双边滑点各1个tick,手续费按实盘标准上浮10%。为什么上浮?因为实盘有时候会有意外费用,比如撤单费、过夜费,留点余量总没错。
我的小技巧:如果你不确定滑点设多少,可以先跑一次无滑点回测,再跑一次带滑点的。对比一下收益差距。如果差距超过20%,说明你的策略对滑点非常敏感,实盘要格外小心。
1.4 vn.py回测引擎的核心流程
来,我画个图帮你理解vn.py回测引擎是怎么工作的。这个图我每次培训都会用,因为它把整个流程讲得很清楚。
你看这个流程,其实就是一个事件循环。每来一根Bar或一个Tick,策略判断要不要交易,如果要交易,就送到撮合模块去模拟成交。成交时,滑点和手续费就在这里被加进去。
注意:很多新手会忽略一个细节——vn.py的默认撮合是不带滑点和手续费的。你需要手动在策略初始化时设置。如果不设置,你的回测结果会偏乐观,实盘时就会被打脸。
1.5 一个简单的滑点手续费设置示例
来,我直接给你看代码。这是我在vn.py里设置滑点和手续费的惯用写法:
from vnpy.app.cta_strategy import CtaTemplate
from vnpy.trader.constant import Direction, Offset
class MyStrategy(CtaTemplate):
"""我的策略 - 带滑点和手续费"""
def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
super().__init__(cta_engine, strategy_name, vt_symbol, setting)
# 设置滑点(单位:tick)
self.slip = 1 # 双边各滑1个tick
# 设置手续费(万分之几)
self.rate = 0.0003 # 万分之三
# 设置最小交易量
self.fixed_size = 1
def on_bar(self, bar):
"""处理K线"""
# 策略逻辑...
pass
def buy(self, price, volume):
"""买入 - 带滑点"""
# 实际成交价 = 买入价 + 滑点
slip_price = price + self.slip * self.pricetick
return super().buy(slip_price, volume)
def sell(self, price, volume):
"""卖出 - 带滑点"""
# 实际成交价 = 卖出价 - 滑点
slip_price = price - self.slip * self.pricetick
return super().sell(slip_price, volume)
这段代码里,我手动在买入和卖出时加了滑点。手续费是在vn.py的底层自动计算的,你只需要设置好self.rate就行。
我的经验:滑点设置不要死板。对于流动性好的品种(比如股指期货),滑点设1个tick就够了。但对于流动性差的品种(比如某些商品期货),我建议设3-5个tick。我曾经在螺纹钢上吃过亏,回测设了1个tick,实盘滑了4个tick,那叫一个酸爽。
1.6 总结一下
这一章咱们聊了三个核心问题:
- vn.py回测引擎的架构:数据加载 → 策略运行 → 订单撮合 → 结果记录,循环往复
- 回测和实盘的差异:回测是理想环境,实盘有滑点、手续费、延迟、市场冲击
- 为什么要模拟滑点和手续费:不模拟就是自欺欺人,实盘会让你怀疑人生
说白了,回测的目的不是为了证明你的策略有多牛,而是为了提前发现坑。滑点和手续费就是最大的两个坑。下一章咱们会深入讲vn.py里具体的滑点模拟方法,包括固定滑点、百分比滑点、还有更高级的限价单模拟。
嗯,今天就到这儿。记住一句话:回测不模拟滑点,实盘两行泪。