8. 回测框架搭建:向量化回测与事件驱动回测、绩效指标计算
回测,说白了就是拿历史数据检验你的策略。我见过太多人,策略在纸上完美,一跑回测就崩。为什么?因为回测框架没搭好。今天咱们就聊聊两种主流回测方式,以及怎么算绩效指标。
8.1 向量化回测 vs 事件驱动回测
这两种方式,我个人的经验是:向量化适合快速验证想法,事件驱动适合实盘模拟。你想想看,向量化就像用Excel拉公式,一行代码算完所有信号;事件驱动则像模拟交易员,一笔一笔处理订单。
8.1.1 向量化回测
向量化回测的核心是一次性计算所有信号和持仓。它不模拟逐笔成交,而是假设你能在收盘价成交。适合做初步筛选。
缺点:忽略滑点、流动性、交易延迟
我曾经用向量化回测跑一个高频策略,夏普高达3.5,兴奋得不行。结果事件驱动一测,直接腰斩到1.2。嗯,这里要注意:向量化回测的结果,永远要打折扣。
import pandas as pd
import numpy as np
# 向量化回测示例:基差均值回归策略
def vectorized_backtest(df, lookback=20, entry_z=2.0):
"""
df: 包含 'basis' 列的DataFrame
"""
# 计算滚动均值和标准差
df['basis_ma'] = df['basis'].rolling(lookback).mean()
df['basis_std'] = df['basis'].rolling(lookback).std()
# 生成信号:z-score超过阈值时开仓
df['z_score'] = (df['basis'] - df['basis_ma']) / df['basis_std']
df['signal'] = 0
df.loc[df['z_score'] > entry_z, 'signal'] = -1 # 做空基差
df.loc[df['z_score'] < -entry_z, 'signal'] = 1 # 做多基差
# 假设次日开盘成交,持仓一天
df['position'] = df['signal'].shift(1)
df['return'] = df['position'] * df['basis'].pct_change()
# 累计收益
df['cum_return'] = (1 + df['return']).cumprod()
return df
8.1.2 事件驱动回测
事件驱动回测,说白了就是模拟一个交易循环:收到数据 → 判断信号 → 执行订单 → 更新持仓。它更接近真实交易环境。
我建议你至少准备三个核心组件:
- 数据源:按时间顺序推送行情
- 策略引擎:接收数据,生成信号
- 风控模块:检查资金、持仓限制
class EventDrivenBacktest:
def __init__(self, data, initial_capital=1000000):
self.data = data
self.capital = initial_capital
self.positions = {} # 持仓字典
self.trades = [] # 成交记录
def on_data(self, timestamp, market_data):
"""每次收到新数据时调用"""
# 1. 更新当前持仓市值
self._update_portfolio(market_data)
# 2. 生成信号
signal = self._generate_signal(market_data)
# 3. 执行交易
if signal != 0:
self._execute_order(timestamp, signal, market_data)
def _generate_signal(self, market_data):
"""策略逻辑:基差突破"""
basis = market_data['spot'] - market_data['futures']
if basis > 50:
return -1 # 做空基差
elif basis < -50:
return 1 # 做多基差
return 0
8.2 绩效指标计算
回测跑完了,怎么评价?光看收益率曲线可不行。我一般看三个核心指标:夏普比率、最大回撤、卡玛比率。
8.2.1 夏普比率
夏普比率衡量的是「每承担一单位风险,能获得多少超额收益」。公式很简单:
Sharpe = (策略年化收益 - 无风险利率) / 年化波动率
我个人习惯用日频数据计算,再年化。为什么?因为日频数据更稳定,不容易被单日极端值带偏。
def calculate_sharpe(returns, risk_free_rate=0.03, periods_per_year=252):
"""
计算夏普比率
returns: 日收益率序列
"""
excess_returns = returns - risk_free_rate / periods_per_year
sharpe = np.sqrt(periods_per_year) * excess_returns.mean() / returns.std()
return sharpe
# 示例
daily_returns = df['return'].dropna()
sharpe = calculate_sharpe(daily_returns)
print(f"夏普比率: {sharpe:.2f}")
8.2.2 最大回撤
最大回撤,说白了就是「从山顶跌到谷底,最多亏了多少」。它比波动率更能反映策略的尾部风险。
我记得有一次,一个策略夏普2.8,但最大回撤高达40%。客户一看直接摇头。后来我加了止损模块,回撤降到15%,夏普反而升到3.1。你想想看,控制回撤本身就是一种收益。
def calculate_max_drawdown(cum_returns):
"""
计算最大回撤
cum_returns: 累计收益率序列
"""
# 计算历史最高点
rolling_max = cum_returns.expanding().max()
# 计算回撤
drawdown = (cum_returns - rolling_max) / rolling_max
max_dd = drawdown.min()
# 找到回撤开始和结束位置
end_idx = drawdown.idxmin()
start_idx = cum_returns[:end_idx].idxmax()
return max_dd, start_idx, end_idx
max_dd, start, end = calculate_max_drawdown(df['cum_return'])
print(f"最大回撤: {max_dd:.2%}")
print(f"回撤区间: {start} 到 {end}")
8.2.3 其他关键指标
| 指标 | 公式 | 我的经验阈值 |
|---|---|---|
| 卡玛比率 | 年化收益 / 最大回撤 | > 2.0 算合格 |
| 胜率 | 盈利交易数 / 总交易数 | 基差交易一般40%-60% |
| 盈亏比 | 平均盈利 / 平均亏损 | > 1.5 才值得做 |
| 索提诺比率 | 类似夏普,但只考虑下行波动 | 比夏普高0.5以上说明策略控制下行风险好 |
8.3 回测框架的核心逻辑
下面这张图,是我自己总结的回测框架流程。你照着搭,基本不会漏掉关键环节。
这个流程里,我最想强调的就是风控模块。很多人回测时忽略风控,结果实盘一上来就爆仓。我建议你在回测框架里就内置止损逻辑,哪怕只是简单的固定比例止损,也能避免很多悲剧。
公众号:蓝海资料掘金营,微信deep3321