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 = (策略年化收益 - 无风险利率) / 年化波动率

我个人习惯用日频数据计算,再年化。为什么?因为日频数据更稳定,不容易被单日极端值带偏。

注意:夏普比率有个坑——它假设收益是正态分布。但基差交易的收益分布往往有厚尾。我建议同时看偏度峰度,如果偏度小于-1或峰度大于5,夏普比率可能虚高。
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 回测框架的核心逻辑

下面这张图,是我自己总结的回测框架流程。你照着搭,基本不会漏掉关键环节。

回测框架核心流程 数据准备 策略信号生成 交易执行引擎 风控与资金管理 绩效评估与报告 清洗、对齐、去极值 z-score、阈值、信号 滑点、手续费、成交概率 止损、仓位、杠杆限制 夏普、回撤、胜率、盈亏比 参数优化循环

这个流程里,我最想强调的就是风控模块。很多人回测时忽略风控,结果实盘一上来就爆仓。我建议你在回测框架里就内置止损逻辑,哪怕只是简单的固定比例止损,也能避免很多悲剧。

最后提醒:回测是工具,不是答案。我见过太多人把回测曲线优化得完美无瑕,结果实盘一塌糊涂。记住:回测的唯一价值,是帮你排除明显不行的策略

公众号:蓝海资料掘金营,微信deep3321