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 完整的统计套利流程

把上面三个步骤串起来,就是一个完整的统计套利模型。我画了个流程图,方便你理解整体逻辑。

统计套利模型流程图 步骤1:品种选择 步骤2:协整检验 p < 0.05? 否(换品种) 步骤3:均值回归分析 步骤4:Z-score计算 步骤5:交易执行

10.5 实战中的注意事项

理论讲完了,说点实战中踩过的坑。

问题 原因 解决方案
协整关系突然失效 市场结构变化、政策干预 设置协整关系「保质期」,定期重新检验
价差长期不回归 半衰期估计不准、趋势性漂移 加入止损机制,动态调整阈值
交易成本吃掉利润 价差波动太小,频繁交易 提高开仓阈值,降低交易频率
回测表现好,实盘差 过拟合、幸存者偏差 使用样本外数据验证,做压力测试

我曾经踩过的一个大坑:

用2018-2020年的数据做协整检验,回测收益曲线漂亮得不行。结果2021年一上线,连续亏损三个月。后来复盘发现,2020年疫情后市场结构发生了根本性变化,协整关系早就变了。从那以后,我养成了一个习惯:每季度重新做一次协整检验,发现失效立刻停用。

10.6 小结

统计套利的核心就三件事:

  • 协整检验:找真正会「回来」的品种对
  • 均值回归分析:衡量回归的速度和可靠性
  • Z-score计算:量化偏离程度,生成交易信号

这三步缺一不可。跳过任何一步,都可能掉进坑里。

嗯,最后提醒一句:统计套利不是印钞机。它赚的是「均值回归」的钱,但市场有时候就是不回归。做好风控,比什么都重要。

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