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 本章知识体系
下面这张图总结了协整检验的核心逻辑,我画了很久才理清楚:
嗯,这张图把四个方法的关系理得很清楚。EG两步法适合快速筛选,Johansen适合多品种组合,滚动窗口解决关系漂移问题,p值阈值则是最后的把关。四者结合使用,才能构建出稳健的配对交易策略。
最后提醒:协整检验只是第一步。即使通过了检验,也不代表这个配对一定能赚钱。你还需要考虑交易成本、滑点、流动性等因素。我见过太多人把协整检验当成万能药,结果实盘一跑就露馅了。
公众号:蓝海资料掘金营,微信deep3321