4、统计套利基础:平稳性检验、协整关系、回归分析、残差分析

统计套利,说白了就是找两个品种之间的「稳定关系」。

你想想看,如果A和B的价格走势长期来看是绑在一起的,那一旦它们暂时分开,就是我们的机会。但这里有个前提——你得先确认它们的关系是「真稳定」,而不是「假暧昧」。

我个人习惯把这一章叫做「地基工程」。地基没打好,后面建的模型再漂亮,也是空中楼阁。

4.1 平稳性检验:别被假象骗了

先问一个问题:为什么非平稳数据不能直接用来做回归?

因为两个完全不相关的随机游走序列,跑个回归出来,R²可能高达0.9。这就是典型的「伪回归」。我在项目中遇到过好几次,刚看到结果时兴奋得不行,结果一检查,全是假的。

平稳性,简单理解就是:

  • 均值是常数,不随时间漂移
  • 方差是常数,不会越波动越大
  • 协方差只跟时间间隔有关,跟具体时间点无关

检验平稳性最常用的工具是ADF检验(Augmented Dickey-Fuller Test)。

核心逻辑:

原假设H₀:序列存在单位根(非平稳)

备择假设H₁:序列平稳

p值 < 0.05 → 拒绝H₀ → 序列平稳

嗯,这里要注意:ADF检验对滞后阶数的选择比较敏感。我一般用AIC或BIC自动选滞后阶数,而不是拍脑袋定一个固定值。

# Python示例:ADF检验
from statsmodels.tsa.stattools import adfuller

def check_stationarity(series, name=''):
    result = adfuller(series.dropna(), autolag='AIC')
    p_value = result[1]
    print(f'{name} ADF p-value: {p_value:.4f}')
    if p_value < 0.05:
        print(f'  → {name} 是平稳的')
    else:
        print(f'  → {name} 非平稳,需要差分')
    return p_value

如果原始序列非平稳,怎么办?

  • 一阶差分后通常就平稳了(I(1)过程)
  • 但差分会丢失长期信息,所以后面我们会用协整来保留它

4.2 协整关系:找到那个「绑在一起的绳子」

两个非平稳序列,如果它们的线性组合是平稳的,那它们就存在协整关系。

你可以把协整想象成「一根绳子牵着两条狗」。每只狗各自乱跑(非平稳),但绳子长度基本固定(线性组合平稳)。

检验协整的经典方法是Engle-Granger两步法:

  1. 第一步:用OLS做回归,得到残差序列
  2. 第二步:对残差做ADF检验,看是否平稳

避坑指南:

我曾经犯过一个低级错误——直接用价格做回归,忘了先确认两个序列都是I(1)。结果残差看起来平稳,其实是因为样本量太小导致的假象。后来我养成了习惯:做协整之前,一定先分别做ADF检验,确认同阶单整。

# Python示例:Engle-Granger协整检验
import statsmodels.api as sm

def engle_granger_test(y, x):
    # 第一步:OLS回归
    x_with_const = sm.add_constant(x)
    model = sm.OLS(y, x_with_const).fit()
    residuals = model.resid
    
    # 第二步:残差ADF检验
    adf_result = adfuller(residuals.dropna(), autolag='AIC')
    p_value = adf_result[1]
    
    print(f'协整检验 p-value: {p_value:.4f}')
    if p_value < 0.05:
        print('→ 存在协整关系')
    else:
        print('→ 不存在协整关系')
    return p_value, model.params

除了Engle-Granger,还有Johansen检验,适合多品种协整。我个人习惯:做两品种套利用EG就够了,三品种以上才上Johansen。

4.3 回归分析:找到那个「最优比例」

一旦确认了协整关系,下一步就是找到具体的对冲比例。

说白了,就是解一个方程:

价差 = Y - β × X

这个β就是我们要找的对冲系数。

最直接的方法是用OLS回归:

# OLS回归求对冲比例
import numpy as np

def calculate_hedge_ratio(y, x):
    # 直接用numpy的lstsq
    X = np.column_stack([np.ones(len(x)), x])
    beta, alpha = np.linalg.lstsq(X, y, rcond=None)[0]
    print(f'对冲比例 β = {beta:.4f}')
    print(f'截距项 α = {alpha:.4f}')
    return beta, alpha

但这里有个坑——OLS假设误差是独立同分布的,而金融数据往往有异方差和自相关。

我建议的做法:

  • 先用OLS跑一遍,得到初步的β
  • 然后用这个β构建价差,再对价差做平稳性检验
  • 如果价差平稳,说明β可用
  • 如果不平稳,试试用稳健回归(如Huber回归)

注意:

回归时要不要加截距项?我个人习惯加。因为两个品种之间可能存在固定的基差,截距项就是用来捕捉这个的。但如果你做的是期货跨期套利,理论上截距应该接近0,这时候可以强制过原点。

4.4 残差分析:价差信号的「体检报告」

残差,就是价差。它是我们交易信号的直接来源。

残差分析要回答三个问题:

  1. 残差平稳吗?——这是协整检验已经做过的
  2. 残差的分布特征?——均值、标准差、偏度、峰度
  3. 残差的自相关性?——影响我们开仓信号的独立性
# 残差分析
def residual_analysis(residuals):
    mean = np.mean(residuals)
    std = np.std(residuals)
    
    # 计算Z-score
    z_scores = (residuals - mean) / std
    
    # 自相关检验(Ljung-Box)
    from statsmodels.stats.diagnostic import acorr_ljungbox
    lb_result = acorr_ljungbox(residuals, lags=[10], return_df=True)
    
    print(f'残差均值: {mean:.4f}')
    print(f'残差标准差: {std:.4f}')
    print(f'Ljung-Box p-value: {lb_result["lb_pvalue"].values[0]:.4f}')
    
    # 判断开仓信号
    entry_threshold = 2.0  # 2倍标准差
    long_signal = z_scores < -entry_threshold
    short_signal = z_scores > entry_threshold
    
    return z_scores, long_signal, short_signal

嗯,这里有个经验值:

  • Z-score超过±2 → 开仓
  • Z-score回归到0附近 → 平仓
  • Z-score超过±3 → 小心,可能是结构变了

实战中的一个小技巧:

我习惯用滚动窗口来更新回归系数。比如用过去60天的数据每天重新算一次β。这样能捕捉到品种关系的缓慢变化。但窗口不能太小,否则β会乱跳。我个人觉得60-120天是个不错的区间。

4.5 完整流程总结

把上面这些串起来,一个完整的统计套利流程就是:

步骤 做什么 关键指标
1. 数据准备 获取两个品种的价格序列 同频、对齐时间戳
2. 平稳性检验 分别对两个序列做ADF检验 p值 < 0.05
3. 协整检验 EG两步法或Johansen检验 残差ADF p值 < 0.05
4. 回归分析 OLS求对冲比例β β稳定、残差平稳
5. 残差分析 计算Z-score,设定阈值 均值≈0,标准差稳定
6. 信号生成 Z-score突破阈值开仓,回归平仓 回测夏普比 > 1.5

最后说一句:统计套利不是万能药。它假设品种间的关系是稳定的,但市场结构会变。我见过太多人拿着历史回测曲线就冲进去,结果实盘一跑就崩了。

所以,每次开仓前问自己一句:这个价差回归,是统计上的必然,还是我的一厢情愿?