4. 协整检验实战:Engle-Granger两步法与Johansen检验

说到统计套利,协整检验是绕不开的核心工具。我刚开始做跨市场套利时,踩过最大的坑就是——看着两个品种走势很像,直接上回归,结果亏得怀疑人生。后来才明白,走势像不代表它们真的「有关系」,你得用协整检验来验证。

说白了,协整就是检验两个或多个时间序列之间是否存在长期稳定的线性关系。如果存在,那它们短期可能会偏离,但长期一定会回归。这正是我们做配对交易的理论基础。

4.1 什么是协整?先讲个故事

想象一下,你牵着一条狗散步。你往前走,狗可能跑到前面,也可能落在后面,但绳子总会把它拉回来。协整关系就像这根绳子——两个序列各自随机游走,但它们之间的差值(价差)是平稳的。

我习惯用这个比喻给学生讲:协整 ≠ 相关性。两个完全不相关的序列也可能协整,两个高度相关的序列也可能不协整。关键看价差是否平稳。

核心定义:如果两个非平稳序列 X_t 和 Y_t,存在一个线性组合 Z_t = Y_t - βX_t 是平稳的,那么它们就是协整的。

4.2 Engle-Granger两步法:简单粗暴,但够用

这个方法是我个人最常用的,尤其在做单对品种的快速筛查时。它只有两步,逻辑非常清晰。

第一步:估计长期关系

用OLS回归估计协整系数:

import statsmodels.api as sm
import numpy as np

# 假设我们有两只股票的价格序列
# stock_a 和 stock_b 都是长度相同的数组

# 第一步:OLS回归
X = sm.add_constant(stock_b)  # 添加截距项
model = sm.OLS(stock_a, X)
results = model.fit()

# 得到协整系数
beta = results.params[1]
alpha = results.params[0]

# 计算价差(残差)
spread = stock_a - (alpha + beta * stock_b)

print(f"协整系数 beta: {beta:.4f}")
print(f"截距 alpha: {alpha:.4f}")

嗯,这里要注意:回归方向会影响结果。我建议两个方向都试一下,选残差更平稳的那个。我在项目中遇到过,同一个品种对,A对B回归和B对A回归,协整检验结果可能完全不同。

第二步:检验残差的平稳性

用ADF检验看残差是否平稳:

from statsmodels.tsa.stattools import adfuller

# 对价差做ADF检验
adf_result = adfuller(spread, maxlag=1)

adf_stat = adf_result[0]
p_value = adf_result[1]
critical_values = adf_result[4]

print(f"ADF统计量: {adf_stat:.4f}")
print(f"p值: {p_value:.4f}")
print("临界值:")
for key, value in critical_values.items():
    print(f"  {key}: {value:.4f}")

if p_value < 0.05:
    print("✅ 残差平稳,存在协整关系")
else:
    print("❌ 残差不平稳,不存在协整关系")

我曾经踩过的坑:ADF检验的滞后阶数选择很关键。默认的AIC/BIC自动选择有时会选太多滞后,导致检验失效。我一般会手动限制 maxlag=1 或 maxlag=2,尤其是日频数据。

4.3 Johansen检验:多品种协整的利器

当你需要同时检验三个以上品种时,Engle-Granger就不够用了。Johansen检验可以一次性检验多个序列之间的协整关系,还能告诉你存在几个协整向量。

你想想看,做跨市场套利时,我们经常要同时看股票、期货、ETF三个品种。这时候Johansen就是首选。

from statsmodels.tsa.vector_ar.vecm import coint_johansen

# 假设我们有三个品种的价格矩阵 data_matrix
# 每列是一个品种,每行是一个时间点

def johansen_test(data, p=1):
    """
    执行Johansen协整检验
    p: 滞后阶数
    """
    result = coint_johansen(data, det_order=0, k_ar_diff=p)
    
    # 迹统计量
    trace_stat = result.lr1
    trace_crit = result.cvt
    
    # 最大特征值统计量
    max_eigen_stat = result.lr2
    max_eigen_crit = result.cvm
    
    print("=== 迹统计量检验 ===")
    for i in range(len(trace_stat)):
        print(f"r <= {i}: 统计量={trace_stat[i]:.2f}, 95%临界值={trace_crit[i, 1]:.2f}")
    
    print("\n=== 最大特征值检验 ===")
    for i in range(len(max_eigen_stat)):
        print(f"r <= {i}: 统计量={max_eigen_stat[i]:.2f}, 95%临界值={max_eigen_crit[i, 1]:.2f}")
    
    # 判断协整秩
    r = 0
    for i in range(len(trace_stat)):
        if trace_stat[i] > trace_crit[i, 1]:
            r += 1
        else:
            break
    
    print(f"\n✅ 存在 {r} 个协整关系")
    return result

# 使用示例
# data = np.column_stack([price_a, price_b, price_c])
# result = johansen_test(data, p=1)

4.4 两种方法的对比与选择

对比维度 Engle-Granger Johansen
适用品种数 2个(最多3个,但麻烦) 2个以上,无上限
计算复杂度 低,两步搞定 高,需要解特征值
结果输出 一个协整向量 多个可能的协整向量
小样本表现 较好 需要大样本
我的使用场景 快速筛选、单对交易 多品种组合、指数套利

我的个人习惯:先用Engle-Granger快速扫一遍所有可能的品种对,选出p值小于0.05的。然后用Johansen做最终确认,顺便看看有没有隐藏的第三个品种在起作用。

4.5 实战中的避坑指南

做协整检验这么多年,我总结了几条血泪教训:

  • 数据频率要一致:别把日线和小时线混在一起检验,结果会骗人。
  • 小心结构突变:2015年股灾前后的数据,协整关系可能完全变了。我建议用滚动窗口检验。
  • 不要过度优化:滞后阶数、检验显著性水平,这些参数别为了好看而乱调。
  • 样本外验证:在样本内找到协整关系不算本事,样本外还能保持才算。

我曾经在原油和燃料油的套利上,样本内协整检验p值0.001,觉得稳了。结果实盘三个月,价差越走越远,亏了十几个点。后来复盘发现,是因为那段时间炼厂检修导致供需结构变了。所以,协整关系是会变的,定期重新检验很重要。

4.6 知识体系总览

下面这张图是我自己总结的协整检验全流程,每次做新策略时都会对照着走一遍:

协整检验实战流程 第一步:数据准备 价格序列 → 对数价格 第二步:平稳性检验 ADF检验(确认非平稳) 第三步:选择方法 EG 或 Johansen Engle-Granger两步法 ① OLS回归估计协整系数 ② 残差ADF检验 ③ 判断是否协整 Johansen检验 ① 设定滞后阶数 ② 迹统计量 / 最大特征值 ③ 确定协整秩 r 第四步:构建价差 → 交易信号 Z-score 阈值开仓 / 回归平仓

这张图把整个流程串起来了。从数据准备到最终生成交易信号,每一步都有对应的检验方法。我个人建议,新手先从Engle-Granger入手,跑通一个完整的配对交易流程,再慢慢过渡到Johansen做多品种组合。

最后说一句:协整检验不是万能的。它告诉你两个品种长期会回归,但没说什么时候回归。有时候回归需要几天,有时候需要几个月。所以仓位管理和止损策略,比协整检验本身更重要。


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