17. 回测框架搭建:事件驱动回测引擎设计、交易成本与滑点处理、绩效评估指标
回测这件事,说白了就是让历史数据告诉你,你的策略到底行不行。我见过太多人,策略回测曲线漂亮得像教科书,一上实盘就崩。为什么?因为回测框架没搭好,尤其是事件驱动、交易成本和滑点这几个坑,一个没填好,结果就是纸上富贵。
我个人习惯,做事件驱动策略的回测,必须用事件驱动引擎。为什么?因为事件驱动策略本身就是等事件、做反应,你用传统的向量化回测(一次性算完所有信号),根本模拟不了真实的交易节奏。你想想看,财报出来那一刻,你才决定下单,向量化回测能模拟这种“等待”和“瞬间决策”吗?不能。
事件驱动回测引擎设计
事件驱动引擎的核心,就是一个循环:时间往前走,事件排队处理。我把它拆成三个模块:
- 事件队列:按时间排序,存放市场数据事件、信号事件、订单执行事件。
- 策略模块:监听事件,做出买卖决策,生成订单。
- 执行模块:接收订单,模拟撮合,返回成交结果。
嗯,这里要注意,事件队列不能只放K线数据。对于事件驱动策略,财报发布、宏观数据公布、公司公告都是独立的事件类型。我曾经犯过一个错,把财报事件和K线事件混在一起,结果策略在财报发布前就提前“偷看”了数据,回测结果虚高。后来我强制要求:每个事件必须带时间戳,且只能按时间顺序逐个处理。
下面是一个简化的事件驱动引擎核心逻辑,我用Python伪代码表示:
class EventDrivenBacktest:
def __init__(self):
self.events = [] # 事件队列
self.strategy = None
self.portfolio = Portfolio()
def run(self):
# 按时间排序事件
self.events.sort(key=lambda e: e.timestamp)
for event in self.events:
# 1. 更新市场状态
self.market.update(event)
# 2. 策略判断
orders = self.strategy.on_event(event)
# 3. 执行订单
for order in orders:
fill = self.executor.execute(order)
self.portfolio.update(fill)
# 4. 记录绩效
self.portfolio.record_snapshot(event.timestamp)
这个框架看着简单,但实际开发时,事件的时间粒度是个大坑。如果你用日线数据,但事件发生在盘中,那你的回测精度就不够。我建议至少用分钟级数据,对于财报事件,甚至可以精确到秒。
交易成本与滑点处理
交易成本是回测的照妖镜。很多漂亮的策略,一加上千分之几的交易成本,立马原形毕露。我处理交易成本时,会分三块:
- 固定成本:佣金、印花税、过户费。这些是明确的,按交易所标准算就行。
- 滑点成本:这是大头。你看到的回测价格是历史成交价,但实际下单时,因为流动性不足或冲击成本,你买不到那个价。
- 冲击成本:大单交易时,你的订单本身会推动价格变动。
对于滑点,我个人习惯用百分比滑点模型。比如,买入时在成交价上加0.1%,卖出时减0.1%。这个0.1%不是随便拍的,我一般会拿历史订单数据去拟合。如果你没有历史数据,那就保守一点,用0.2%甚至0.3%。
我曾经在回测一个波动率曲面套利策略时,用了0.05%的滑点,回测年化收益20%。结果实盘一跑,滑点实际到了0.15%,收益直接腰斩。从那以后,我定了个规矩:回测滑点至少是实盘预期的两倍。
下面是我常用的滑点处理代码片段:
def apply_slippage(price, side, slippage_rate=0.001):
"""
side: 'buy' 或 'sell'
slippage_rate: 滑点比例,默认0.1%
"""
if side == 'buy':
return price * (1 + slippage_rate)
elif side == 'sell':
return price * (1 - slippage_rate)
else:
raise ValueError("side must be 'buy' or 'sell'")
绩效评估指标
回测跑完了,一堆数字摆在你面前,怎么看?我只看三个核心指标:夏普比率、最大回撤、收益回撤比。其他什么胜率、盈亏比,都是辅助。
夏普比率
夏普比率衡量的是每承担一单位风险,能获得多少超额收益。公式很简单:
Sharpe Ratio = (策略年化收益率 - 无风险利率) / 策略年化波动率
但这里有个坑:计算频率。如果你用日收益率算夏普,年化时要乘以根号252。如果用周收益率,乘以根号52。我见过有人用月收益率算夏普,然后乘以根号12,结果夏普比率虚高。为什么?因为月收益率平滑了波动,算出来的波动率偏低。
我个人习惯,统一用日收益率计算。这样最标准,也方便和其他策略对比。
最大回撤
最大回撤,就是策略从最高点跌到最低点的最大幅度。这个指标直接反映了策略的心理承受压力。你想想看,如果策略回撤30%,你还能拿住吗?
计算最大回撤的代码很简单:
def max_drawdown(equity_curve):
peak = equity_curve[0]
max_dd = 0
for value in equity_curve:
if value > peak:
peak = value
dd = (peak - value) / peak
if dd > max_dd:
max_dd = dd
return max_dd
但这里我要提醒一句:最大回撤是历史数据,不代表未来。我曾经有一个策略,回测最大回撤只有8%,结果实盘遇到了黑天鹅,回撤直接干到25%。所以我现在看回撤,更关注回撤的持续时间。如果回撤持续了3个月以上,即使幅度不大,也说明策略可能失效了。
收益回撤比
这个指标是我最看重的。它等于年化收益率除以最大回撤。比如年化20%,最大回撤10%,收益回撤比就是2。我个人认为,收益回撤比大于2的策略,才值得上实盘。
下面是一个完整的绩效评估表格示例:
| 指标 | 值 | 评价 |
|---|---|---|
| 年化收益率 | 18.5% | 良好 |
| 年化波动率 | 12.3% | 中等 |
| 夏普比率 | 1.42 | 良好 |
| 最大回撤 | -9.8% | 优秀 |
| 收益回撤比 | 1.89 | 接近优秀 |
本章知识体系
下面我用一张SVG图,把本章的核心逻辑串起来。你可以把它当作回测框架的“施工蓝图”。
这张图把整个回测流程串起来了。你从事件队列开始,一步步走到绩效评估,中间任何一个环节出问题,结果都会失真。我个人建议,先搭一个最小可用版本,跑通一个简单策略,再逐步加入滑点、成本、多事件类型等细节。别一开始就想搞大而全,容易把自己绕进去。