第二十九讲:曲面回测框架——历史数据回放、策略绩效评估、稳健性检验
各位同学,今天我们来聊聊回测。说实话,我见过太多人把回测当成「跑个代码看曲线」的简单活儿。但真正做过实盘的人都知道——回测框架设计得好不好,直接决定了你是在「验证策略」还是在「自欺欺人」。
曲面策略的回测,比普通期权策略要复杂一个量级。为什么?因为曲面本身是动态的,你不仅要判断方向,还要判断曲面的形态变化。嗯,这里我们先理清核心逻辑。
一、回测框架的核心架构
我个人习惯把曲面回测框架拆成三层:
- 数据层:历史曲面数据、标的价格、无风险利率
- 策略层:信号生成、交易执行、仓位管理
- 评估层:绩效指标、风险度量、稳健性检验
说白了,就是「喂数据→跑策略→看结果」三步走。但每一步都有坑。
核心原则:回测必须模拟真实交易环境。包括交易成本、滑点、流动性限制。我曾经见过一个策略回测年化50%,实盘直接亏20%——就是因为没考虑曲面报价的买卖价差。
下面这张图是我自己常用的回测流程,你可以参考一下:
二、历史数据回放——别让「未来信息」污染了你的回测
这是回测里最容易犯的错误,没有之一。我刚开始做曲面策略时,就踩过这个坑——用当天的数据去预测当天的信号,结果回测曲线漂亮得像假的一样。嗯,它确实是假的。
正确的做法是:滚动窗口回放。每个交易日,你只能用截止到前一个交易日的数据来生成信号。
我的经验:建议用「事件驱动」的方式来回放。每次只推进一个时间步,更新曲面数据,然后判断是否触发交易信号。别用向量化回测,那玩意儿在曲面策略里容易出bug。
下面是一个简单的回放框架示例:
class SurfaceBacktestEngine:
def __init__(self, surface_data, spot_data, r=0.03):
self.surface_data = surface_data # 历史曲面数据
self.spot_data = spot_data # 标的价格序列
self.r = r # 无风险利率
self.portfolio = Portfolio() # 投资组合
self.trades = [] # 交易记录
def run(self, strategy_func):
"""滚动回放主循环"""
for i in range(1, len(self.surface_data)):
# 只使用到 i-1 的数据
current_surface = self.surface_data[:i]
current_spot = self.spot_data[:i]
# 生成信号
signal = strategy_func(current_surface, current_spot)
# 执行交易(假设在 i 时刻以收盘价成交)
if signal is not None:
self.execute_trade(signal, i)
# 每日盯市
self.portfolio.mark_to_market(
self.surface_data[i],
self.spot_data[i]
)
def execute_trade(self, signal, idx):
"""执行交易,考虑滑点和交易成本"""
# 这里要模拟真实的成交价格
fill_price = self.estimate_fill_price(signal, idx)
self.trades.append({
'time': idx,
'signal': signal,
'fill_price': fill_price,
'cost': self.calc_transaction_cost(signal)
})
self.portfolio.update(signal, fill_price)
三、策略绩效评估——别只看收益率曲线
很多人回测完只看两个指标:总收益率和最大回撤。这远远不够。曲面策略的特殊性在于,它的风险暴露是动态变化的。
我个人习惯用下面这套指标体系:
| 指标类别 | 具体指标 | 说明 |
|---|---|---|
| 收益类 | 年化收益率、累计收益率 | 基础指标,但别单独看 |
| 风险类 | 年化波动率、最大回撤、VaR | 曲面策略的波动率往往不恒定 |
| 风险调整类 | 夏普比率、卡玛比率、索提诺比率 | 夏普比率在期权策略里可能失真 |
| 曲面特定类 | Vega暴露、Gamma暴露、期限结构暴露 | 这是曲面策略独有的风险维度 |
| 交易统计类 | 胜率、盈亏比、平均持仓周期 | 帮助理解策略的交易逻辑 |
注意:曲面策略的夏普比率容易被「虚高」。为什么?因为期权策略的收益分布往往是非正态的,有肥尾特征。我建议同时看索提诺比率,它只考虑下行波动。
举个例子,我曾经回测一个曲面套利策略,夏普比率高达2.8,但索提诺比率只有1.2。后来发现,这个策略在大部分时间赚小钱,但偶尔会大亏——这就是典型的「卖期权」特征。
四、稳健性检验——别让你的策略「过拟合」
这是整个回测框架里最重要的一环,也是最容易被忽视的。说白了,稳健性检验就是回答一个问题:你的策略是发现了真正的市场规律,还是只是拟合了历史噪音?
我常用的稳健性检验方法有四种:
- 参数敏感性分析:改变策略参数(比如阈值、窗口长度),看绩效是否稳定。如果参数稍微一变,绩效就大幅波动,那这个策略基本不可用。
- 不同市场环境测试:把回测期分成牛市、熊市、震荡市、高波动市、低波动市,分别看绩效。曲面策略在不同市场环境下的表现差异可能很大。
- 蒙特卡洛模拟:对历史数据进行重采样,生成多条「合成历史」,然后跑策略。如果大部分合成路径上策略都赚钱,那说明策略有统计显著性。
- 样本外测试:把数据分成训练集和测试集。在训练集上优化参数,在测试集上验证。这是最基础的稳健性检验,但很多人连这一步都不做。
避坑指南:我曾经犯过一个错误——在回测时用了全样本数据来优化参数,然后说「这个策略很稳健」。结果实盘时市场风格一变,策略直接失效。后来我学乖了,每次回测都留出最后20%的数据做样本外测试。
下面是一个参数敏感性分析的代码示例:
def parameter_sensitivity_test(strategy_func, param_grid):
"""
对策略参数进行网格搜索,分析绩效稳定性
"""
results = []
for param_name, param_values in param_grid.items():
for value in param_values:
# 设置参数
setattr(strategy_func, param_name, value)
# 运行回测
engine = SurfaceBacktestEngine(surface_data, spot_data)
engine.run(strategy_func)
# 记录绩效
perf = engine.calc_performance_metrics()
results.append({
'param': param_name,
'value': value,
'sharpe': perf['sharpe_ratio'],
'max_drawdown': perf['max_drawdown'],
'win_rate': perf['win_rate']
})
# 分析参数变化对绩效的影响
for param_name in param_grid.keys():
param_results = [r for r in results if r['param'] == param_name]
sharpe_values = [r['sharpe'] for r in param_results]
# 计算夏普比率的标准差
sharpe_std = np.std(sharpe_values)
if sharpe_std > 0.5:
print(f"警告:参数 {param_name} 对绩效影响较大,策略可能不稳定")
else:
print(f"参数 {param_name} 对绩效影响较小,策略较稳健")
return results
五、实战中的几个关键细节
最后,分享几个我在实战中总结的经验:
- 交易成本别设太低:曲面策略通常涉及多个期权合约,交易成本会显著影响净收益。我一般按每张合约2-5元来估算,再加上滑点。
- 注意数据频率:曲面数据通常是日频的,但日内波动可能很大。如果你的策略是日频调仓,那没问题。如果是高频策略,需要更高频的曲面数据——但说实话,个人投资者很难拿到高质量的日内曲面数据。
- 别忽略流动性:深度虚值或深度实值的期权流动性很差。回测时如果用了这些合约,实盘可能根本成交不了。我建议在回测中过滤掉流动性不足的合约。
- 滚动展期要模拟真实:曲面策略经常需要展期(比如从近月合约滚动到远月)。展期时的价差和成本,一定要在回测中模拟出来。
我的习惯:每次回测完,我都会手动检查几笔交易的成交价格是否合理。比如,某个信号触发了,但当时的市场深度是否足够?如果不够,这笔交易在实盘里可能无法按回测价格成交。这种「人工抽检」虽然费时间,但能发现很多自动化回测发现不了的问题。
好了,关于曲面回测框架的核心内容就这些。记住,回测不是为了证明你的策略有多牛,而是为了发现你的策略在什么情况下会亏钱。把亏钱的情况想清楚了,实盘才能活得久。
公众号:蓝海资料掘金营,微信deep3321