10. 统计套利模型:协整检验、均值回归、Z-score计算
统计套利,说白了就是找两个品种之间的「亲戚关系」。
我刚开始做跨品种套利时,犯过一个低级错误——直接用相关性去配对。结果呢?两个品种相关性高达0.95,但价差越走越远,亏得我头皮发麻。后来才明白,相关性高不代表它们会「回来」。
真正靠谱的,是协整关系。
10.1 协整检验:找「真亲戚」
协整检验的核心思想很简单:两个非平稳的时间序列,它们的线性组合可能是平稳的。
举个例子。你想想看,一个醉汉牵着一条狗走路。醉汉的路径是随机的(非平稳),狗的路径也是随机的。但狗绳的长度是固定的——这就是协整。醉汉和狗虽然各自乱走,但它们的距离(价差)会围绕一个均值波动。
关键区别:
- 相关性:两个序列同涨同跌,但可能越走越远
- 协整性:两个序列的价差会回归均值
我常用的协整检验方法是Engle-Granger两步法。嗯,这里要注意,第一步是回归,第二步是检验残差的平稳性。
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
def cointegration_test(series1, series2):
"""
Engle-Granger 协整检验
返回:协整检验的p值,以及回归系数
"""
# 第一步:OLS回归
X = sm.add_constant(series1)
model = sm.OLS(series2, X).fit()
beta = model.params[1] # 对冲比率
alpha = model.params[0] # 截距
# 计算残差(价差)
spread = series2 - beta * series1 - alpha
# 第二步:ADF检验残差的平稳性
adf_result = adfuller(spread, maxlag=1, autolag='AIC')
p_value = adf_result[1]
return {
'p_value': p_value,
'beta': beta,
'alpha': alpha,
'spread': spread,
'is_cointegrated': p_value < 0.05
}
# 使用示例
result = cointegration_test(price_a, price_b)
print(f"协整检验p值: {result['p_value']:.4f}")
print(f"对冲比率: {result['beta']:.4f}")
我在项目中遇到过一个问题:用日线数据做协整检验,结果很漂亮。但换到小时线,协整关系就消失了。为什么?因为协整关系的时间尺度很重要。不同频率的数据,协整关系可能完全不同。
避坑指南:
我曾经用5分钟K线做协整检验,发现一堆「伪协整」对。后来才意识到,高频数据的微观结构噪声会严重干扰检验结果。建议先用日线筛选,再逐步降频验证。
10.2 均值回归:价差会「回来」吗?
协整检验通过了,不代表均值回归就一定能赚钱。你想想看,协整关系是「长期」的,但交易是「短期」的。价差偏离均值后,多久能回来?回来多少?这才是关键。
我个人习惯用半衰期(Half-life)来衡量均值回归的速度。半衰期越短,回归越快,交易机会越多。
def calculate_half_life(spread):
"""
计算均值回归的半衰期
半衰期 = ln(2) / |lambda|
其中lambda是自回归系数
"""
spread_lag = spread.shift(1).dropna()
spread_diff = spread.diff().dropna()
# 对齐数据
spread_lag = spread_lag.iloc[1:]
spread_diff = spread_diff.iloc[1:]
# 回归:Δspread = λ * spread_lag + ε
X = sm.add_constant(spread_lag)
model = sm.OLS(spread_diff, X).fit()
lambda_coef = model.params[1]
half_life = -np.log(2) / lambda_coef
return half_life
half_life = calculate_half_life(result['spread'])
print(f"半衰期: {half_life:.2f} 个周期")
半衰期小于5个周期的,我一般会重点关注。大于20个周期的,基本就不碰了——等它回归,黄花菜都凉了。
经验之谈:
半衰期不是固定不变的。市场结构变化时,半衰期会漂移。我每周都会重新计算一次,动态调整交易频率。
10.3 Z-score计算:量化「偏离程度」
有了价差序列,怎么判断什么时候开仓?Z-score就是干这个的。
Z-score = (当前价差 - 价差均值) / 价差标准差
说白了,就是看当前价差偏离均值多少个标准差。Z-score的绝对值越大,说明偏离越极端,回归的概率越高。
def calculate_zscore(spread, window=20):
"""
计算滚动Z-score
window: 滚动窗口大小
"""
rolling_mean = spread.rolling(window=window).mean()
rolling_std = spread.rolling(window=window).std()
zscore = (spread - rolling_mean) / rolling_std
return zscore
zscore = calculate_zscore(result['spread'], window=20)
# 交易信号
entry_threshold = 2.0 # 开仓阈值
exit_threshold = 0.5 # 平仓阈值
# 做空价差(卖高买低)
short_signals = zscore > entry_threshold
# 做多价差(买低卖高)
long_signals = zscore < -entry_threshold
# 平仓信号
exit_signals = abs(zscore) < exit_threshold
我一般用2倍标准差作为开仓阈值。但要注意,这个阈值不是死的。波动率大的时候,2倍标准差可能太宽松;波动率小的时候,又可能太严格。
动态阈值调整:
我建议根据近期波动率动态调整阈值。比如用过去60天的Z-score标准差,乘以一个系数(比如1.5~2.5),作为当前的开仓阈值。
10.4 完整的统计套利流程
把上面三个步骤串起来,就是一个完整的统计套利模型。我画了个流程图,方便你理解整体逻辑。
10.5 实战中的注意事项
理论讲完了,说点实战中踩过的坑。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 协整关系突然失效 | 市场结构变化、政策干预 | 设置协整关系「保质期」,定期重新检验 |
| 价差长期不回归 | 半衰期估计不准、趋势性漂移 | 加入止损机制,动态调整阈值 |
| 交易成本吃掉利润 | 价差波动太小,频繁交易 | 提高开仓阈值,降低交易频率 |
| 回测表现好,实盘差 | 过拟合、幸存者偏差 | 使用样本外数据验证,做压力测试 |
我曾经踩过的一个大坑:
用2018-2020年的数据做协整检验,回测收益曲线漂亮得不行。结果2021年一上线,连续亏损三个月。后来复盘发现,2020年疫情后市场结构发生了根本性变化,协整关系早就变了。从那以后,我养成了一个习惯:每季度重新做一次协整检验,发现失效立刻停用。
10.6 小结
统计套利的核心就三件事:
- 协整检验:找真正会「回来」的品种对
- 均值回归分析:衡量回归的速度和可靠性
- Z-score计算:量化偏离程度,生成交易信号
这三步缺一不可。跳过任何一步,都可能掉进坑里。
嗯,最后提醒一句:统计套利不是印钞机。它赚的是「均值回归」的钱,但市场有时候就是不回归。做好风控,比什么都重要。