第二十六讲:回测框架搭建——基于历史曲面数据的策略回测

做期权量化,最怕什么?

怕策略在实盘里翻车。

我见过太多人,拿着回测曲线漂亮得不像话的策略,一上实盘就亏得亲妈都不认识。为什么?因为回测框架本身就有问题。今天我们就来聊聊,怎么搭建一个靠谱的、基于历史波动率曲面数据的回测框架。

回测框架的核心逻辑

说白了,回测就是模拟历史。你把策略放到过去的数据里跑一遍,看看它能不能赚钱。但期权回测比股票回测复杂得多——你不仅要看价格,还要看波动率曲面的变化。

我个人习惯把回测框架拆成三层:

  • 数据层:历史曲面数据、标的价格、无风险利率
  • 策略层:开仓信号、组合构建、调仓逻辑
  • 评估层:盈亏计算、风险指标、绩效归因

这三层缺一不可。数据层是地基,策略层是房子,评估层是验收报告。地基没打好,后面全是白搭。

核心原则:回测框架必须能处理曲面数据的动态变化,而不是简单用历史波动率均值。

数据层:历史曲面数据的处理

我在项目中遇到过最坑的事,就是历史曲面数据不完整。有些交易日,某些行权价的期权根本没有成交,曲面数据是插值出来的。如果你直接拿这些数据回测,结果会严重失真。

怎么处理?我建议这样做:

  1. 数据清洗:剔除成交量过小的期权合约(比如日成交量低于100张)
  2. 曲面重构:用SVI或SSVI模型对缺失点进行插值
  3. 时间对齐:确保曲面数据与标的价格的时间戳一致

小技巧:回测时尽量用收盘价数据。如果用盘中数据,要考虑滑点和流动性问题。我曾经因为用了盘中tick数据,回测年化收益高了20%,实盘直接打回原形。

策略层:如何模拟真实交易

回测不是纸上谈兵。你得尽可能模拟真实交易环境。

举个例子,假设你的策略是「当隐含波动率高于历史波动率80分位数时,卖出跨式期权」。在回测中,你需要:

  • 检查当天收盘时的曲面数据
  • 计算各期限的隐含波动率
  • 与历史波动率分位数对比
  • 如果触发条件,构建卖出跨式组合
  • 记录开仓时的曲面状态

这里有个坑:你开仓时的曲面数据,必须是当时能拿到的数据。不能拿未来数据去回测。我见过有人用当天收盘后的曲面数据去判断开盘时的信号,这属于典型的未来函数。

警告:回测中一定要加入滑点和交易成本。期权交易成本包括:手续费、买卖价差、冲击成本。我一般按每张合约2-5元计算手续费,买卖价差按0.5-1个tick估算。

评估层:不只是看收益率

很多人回测只看年化收益率和最大回撤。但期权策略的评估维度更多。

我个人习惯看这几个指标:

指标 说明 我的经验阈值
夏普比率 风险调整后收益 大于1.5算合格
Calmar比率 收益与最大回撤之比 大于2.0算优秀
胜率 盈利交易占比 期权卖方策略通常高于70%
盈亏比 平均盈利/平均亏损 大于1.5比较健康
最大连续亏损 连续亏损次数 不超过5次

嗯,这里要注意:期权策略的胜率往往很高,但盈亏比可能很低。比如卖出跨式,胜率可能80%以上,但一次亏损就能吃掉之前所有的利润。所以回测时一定要看最大回撤和尾部风险。

代码实现:一个简单的回测框架

下面是一个简化版的回测框架代码。它基于历史曲面数据,对卖出跨式策略进行回测。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class OptionBacktest:
    def __init__(self, surface_data, spot_data, rf_rate=0.03):
        """
        surface_data: DataFrame, 包含日期、期限、行权价、隐含波动率
        spot_data: DataFrame, 包含日期、标的价格
        rf_rate: 无风险利率
        """
        self.surface = surface_data
        self.spot = spot_data
        self.rf = rf_rate
        self.trades = []
        self.pnl = []
        
    def generate_signals(self, lookback=60, threshold=0.8):
        """
        生成交易信号
        lookback: 历史波动率计算窗口
        threshold: 分位数阈值
        """
        signals = []
        
        # 计算历史波动率
        self.spot['returns'] = self.spot['close'].pct_change()
        self.spot['hist_vol'] = self.spot['returns'].rolling(lookback).std() * np.sqrt(252)
        
        for date in self.surface['date'].unique():
            # 获取当天的曲面数据
            day_surface = self.surface[self.surface['date'] == date]
            
            # 获取当天的历史波动率
            hist_vol = self.spot[self.spot['date'] == date]['hist_vol'].values[0]
            
            # 计算隐含波动率的分位数
            iv_values = day_surface['implied_vol'].values
            iv_percentile = np.percentile(iv_values, threshold * 100)
            
            # 如果隐含波动率高于阈值,生成卖出信号
            if np.mean(iv_values) > iv_percentile:
                signals.append({
                    'date': date,
                    'signal': 'sell_straddle',
                    'iv_mean': np.mean(iv_values),
                    'hist_vol': hist_vol
                })
                
        return signals
    
    def run_backtest(self, signals):
        """
        执行回测
        """
        for signal in signals:
            # 模拟开仓
            entry_date = signal['date']
            entry_spot = self.spot[self.spot['date'] == entry_date]['close'].values[0]
            
            # 假设卖出平值跨式,每张合约赚取2%的权利金
            premium = entry_spot * 0.02
            
            # 持有到下一个交易日平仓
            next_date = entry_date + timedelta(days=1)
            if next_date in self.spot['date'].values:
                exit_spot = self.spot[self.spot['date'] == next_date]['close'].values[0]
                
                # 计算盈亏(简化模型)
                pnl = premium - abs(exit_spot - entry_spot) / entry_spot * 100
                
                self.trades.append({
                    'entry_date': entry_date,
                    'exit_date': next_date,
                    'entry_spot': entry_spot,
                    'exit_spot': exit_spot,
                    'pnl': pnl
                })
                
        # 计算累计收益
        self.pnl = pd.DataFrame(self.trades)
        return self.pnl

# 使用示例
bt = OptionBacktest(surface_data, spot_data)
signals = bt.generate_signals(lookback=60, threshold=0.8)
results = bt.run_backtest(signals)
print(f"总交易次数: {len(results)}")
print(f"胜率: {len(results[results['pnl'] > 0]) / len(results):.2%}")
print(f"平均盈亏: {results['pnl'].mean():.2f}%")

避坑指南:上面的代码是简化版,实际回测要考虑更多细节。比如:

  • 期权到期日处理(临近到期时波动率会异常)
  • 保证金计算(卖出期权需要保证金)
  • 展期操作(当月合约到期后如何切换到下月)

我曾经因为没处理好展期,回测收益虚高了30%。

回测框架的架构图

下面这张图展示了回测框架的整体流程。你可以看到数据从输入到输出的完整路径。

回测框架架构图 数据层 历史曲面数据 标的价格数据 无风险利率 策略层 信号生成 组合构建 调仓逻辑 评估层 盈亏计算 风险指标 绩效归因 回测引擎 逐日模拟、滑点处理、交易成本、保证金计算 输出结果 收益曲线、风险指标、交易记录 策略优化反馈

回测中常见的坑

做期权回测这几年,我踩过的坑能写一本书。这里挑几个最要命的:

  • 幸存者偏差:只回测了存续期较长的合约,忽略了那些提前退市的。结果就是回测收益偏高。
  • 前视偏差:用到了未来数据。比如用当天的收盘价去判断开盘时的信号。
  • 流动性陷阱:回测时假设所有合约都能以中间价成交。实盘里,深度虚值合约可能根本卖不出去。
  • 波动率跳跃:黑天鹅事件发生时,波动率曲面会瞬间变形。回测数据里可能没有包含这种极端情况。

我曾经犯过一个低级错误:回测时用了复权后的标的价格,但期权数据没有复权。结果就是标的价格和期权价格对不上,回测结果完全失真。所以,数据对齐是回测的第一要务。

总结

回测框架搭建,说白了就是三个字:真、准、稳

  • 数据要真——别用未来数据
  • 计算要准——滑点、成本、保证金一个不能少
  • 结果要稳——多跑几遍,看看是不是每次都一样

记住,回测只是工具,不是圣杯。它能帮你排除明显不靠谱的策略,但不能保证实盘一定赚钱。真正的高手,都是在回测和实盘之间反复迭代,不断优化。

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