4、协整检验实战:Engle-Granger两步法、Johansen检验、滚动窗口协整、p值阈值设定

协整检验,说白了就是判断两个或多个价格序列之间,是否存在长期稳定的线性关系。我刚开始做配对交易时,总觉得只要相关系数高就能配对,结果吃了不少亏。后来才明白,相关系数高不代表它们会一起回归——协整才是那个真正管用的东西。

4.1 为什么需要协整检验?

你想想看,两个股票价格走势看起来很像,但可能只是巧合。比如2017年茅台和五粮液,相关系数高达0.95,但如果你用它们做配对交易,会发现价差经常跑偏不回来。为什么?因为它们的价格关系不是「协整」的。

协整的核心思想是:两个非平稳序列的线性组合,可能是平稳的。换句话说,A和B各自乱跑,但A - β×B 却乖乖地围绕均值波动。这才是配对交易能赚钱的基础。

关键点:协整 ≠ 相关。相关看的是同步性,协整看的是长期均衡关系。我见过太多人把这两个概念搞混,结果回测漂亮,实盘亏钱。

4.2 Engle-Granger两步法

这个方法最直观,也最常用。我个人习惯用它来做快速筛选,因为计算量小,适合几百只股票的大规模扫描。

第一步:估计长期关系

用OLS回归估计协整系数β:

import statsmodels.api as sm
import numpy as np

# 假设price_A和price_B是两个价格序列
# 第一步:OLS回归
X = sm.add_constant(price_B)
model = sm.OLS(price_A, X).fit()
beta = model.params[1]  # 协整系数
residuals = model.resid  # 残差序列

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

对残差做ADF检验。如果残差是平稳的,就说明两个序列协整。

from statsmodels.tsa.stattools import adfuller

# 第二步:ADF检验残差
adf_result = adfuller(residuals, maxlag=1)
p_value = adf_result[1]

print(f"ADF统计量: {adf_result[0]:.4f}")
print(f"p值: {p_value:.4f}")

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

我的经验:EG两步法有个坑——第一步的OLS估计会引入「估计误差」,导致第二步的ADF检验偏向于拒绝原假设。所以我一般会把p值阈值设得更严,比如0.01而不是0.05。

4.3 Johansen检验

EG两步法只能处理一对一的协整关系。但如果你有三只、四只股票呢?比如银行板块的招行、工行、建行,它们之间可能存在多个协整关系。这时候就要用Johansen检验了。

Johansen检验基于VAR模型,可以同时检验多个协整向量。我个人觉得它比EG法更稳健,但计算量也大得多。

from statsmodels.tsa.vector_ar.vecm import coint_johansen

# 假设data是一个DataFrame,包含多只股票的价格
# 比如 data = ['招商银行', '工商银行', '建设银行']
johansen_result = coint_johansen(data, det_order=0, k_ar_diff=1)

# 迹统计量检验
trace_stat = johansen_result.lr1
trace_crit = johansen_result.cvt

# 最大特征值检验
max_eigen_stat = johansen_result.lr2
max_eigen_crit = johansen_result.cvm

print("迹统计量:", trace_stat)
print("95%临界值:", trace_crit[:, 1])

怎么看结果?

  • 迹统计量 > 临界值 → 拒绝原假设,存在协整关系
  • 从r=0开始检验,直到无法拒绝原假设为止
  • r的值就是协整关系的个数

注意:Johansen检验对滞后阶数很敏感。我建议用AIC或BIC来选择最优滞后阶数,别用默认值。曾经有一次我偷懒没调滞后阶数,结果检验出3个协整关系,实际只有1个——白白浪费了两天时间调试策略。

4.4 滚动窗口协整

协整关系不是一成不变的。2015年股灾期间,很多原本协整的股票对都「脱钩」了。如果你用固定窗口做协整检验,等发现关系变了,可能已经亏了不少。

滚动窗口协整,就是每隔一段时间重新做一次检验。我一般用60个交易日作为窗口长度,每20个交易日滚动一次。

def rolling_coint(price_A, price_B, window=60, step=20):
    results = []
    for i in range(0, len(price_A) - window, step):
        sub_A = price_A[i:i+window]
        sub_B = price_B[i:i+window]
        
        # EG两步法
        X = sm.add_constant(sub_B)
        model = sm.OLS(sub_A, X).fit()
        residuals = model.resid
        
        adf_result = adfuller(residuals, maxlag=1)
        p_value = adf_result[1]
        
        results.append({
            'window_end': price_A.index[i+window-1],
            'p_value': p_value,
            'cointegrated': p_value < 0.05
        })
    
    return results

# 使用示例
coint_history = rolling_coint(price_A, price_B)
coint_df = pd.DataFrame(coint_history)
print(f"协整比例: {coint_df['cointegrated'].mean():.2%}")

实战建议:滚动窗口的协整比例如果低于60%,我建议放弃这个配对。说明它们的关系太不稳定,交易风险大。另外,窗口长度别太短,少于30个交易日的话,ADF检验的效力会大幅下降。

4.5 p值阈值设定

p值阈值怎么设?这问题看似简单,其实门道不少。

场景 推荐p值阈值 说明
快速筛选(几百个配对) 0.05 先粗筛,后续再精挑
实盘交易 0.01 宁可漏掉,不要假阳性
高频配对(分钟级数据) 0.001 高频噪声大,阈值要更严
多品种组合(3只以上) 0.05(Johansen) Johansen本身偏保守

我的经验:别死磕0.05这个数字。我曾经用0.05筛出50个配对,实盘跑了3个月,只有12个赚钱。后来改成0.01,虽然只筛出18个配对,但赚钱的有14个。阈值设严一点,虽然配对少了,但质量高了。

一个小技巧:如果你用EG两步法,可以试试「动态阈值」。比如先跑一遍历史数据,看看p值的分布,然后取10%分位数作为阈值。这样比固定0.05更适应市场环境。

4.6 本章知识体系

下面这张图总结了协整检验的核心逻辑,我画了很久才理清楚:

协整检验知识体系 协整检验 Engle-Granger两步法 Johansen检验 滚动窗口协整 p值阈值设定 OLS估计长期关系 ADF检验残差平稳性 迹统计量检验 最大特征值检验 固定窗口长度 滚动步长设定 场景化阈值选择 动态阈值调整 核心目标:找到长期稳定的线性关系

嗯,这张图把四个方法的关系理得很清楚。EG两步法适合快速筛选,Johansen适合多品种组合,滚动窗口解决关系漂移问题,p值阈值则是最后的把关。四者结合使用,才能构建出稳健的配对交易策略。

最后提醒:协整检验只是第一步。即使通过了检验,也不代表这个配对一定能赚钱。你还需要考虑交易成本、滑点、流动性等因素。我见过太多人把协整检验当成万能药,结果实盘一跑就露馅了。


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