第四节:协整检验实战——ADF检验原理、Engle-Granger两步法、Johansen检验、Python代码实现
协整检验,说白了就是帮我们判断两个或多个价格序列之间,是否存在长期稳定的线性关系。做套利的朋友都知道,没有协整关系,就没有统计套利的基础。今天我就把这套东西掰开揉碎了讲清楚。
4.1 协整的核心思想
先问一个问题:为什么两个不平稳的序列,它们的线性组合可能是平稳的?
举个例子。你想想看,一个醉汉牵着一条狗走路。醉汉的路径是随机游走的,狗的路径也是随机游走的。但狗绳的长度是固定的——醉汉和狗之间的距离,始终围绕某个均值波动。这个距离,就是平稳的。
协整关系就是这个道理。两个非平稳的价格序列,如果存在一个线性组合使得残差平稳,那它们就是协整的。说白了,它们之间有一根看不见的「绳子」牵着。
重要概念:协整 ≠ 相关性。相关性高不代表协整,协整也不要求高相关性。我见过太多新手把相关系数高的品种直接拿去做套利,结果亏得怀疑人生。记住,协整检验的是长期均衡关系,不是短期同步性。
4.2 ADF检验原理
ADF检验,全称Augmented Dickey-Fuller检验。它是判断时间序列是否平稳的「金标准」之一。
原理其实不复杂。我们检验一个序列是否存在单位根。如果存在单位根,序列就是不平稳的;反之则是平稳的。
ADF检验的回归方程长这样:
Δy_t = α + βt + γy_{t-1} + δ₁Δy_{t-1} + δ₂Δy_{t-2} + ... + δ_pΔy_{t-p} + ε_t
核心假设是:H₀: γ = 0(存在单位根,不平稳)。如果检验统计量小于临界值,就拒绝原假设,认为序列平稳。
我个人习惯,在做ADF检验时,会先看一眼序列的走势图。如果明显有趋势,就选带趋势项的检验模式;如果看起来围绕均值波动,就选只带截距项的模式。这个选择直接影响检验结果,千万别马虎。
实战小技巧:ADF检验的滞后阶数p,我一般用AIC或BIC自动选择。别手动设一个固定值,不同品种的最优滞后阶数差异很大。我在做螺纹钢和热卷的协整检验时,就吃过这个亏——手动设了5阶,结果死活检验不出协整关系,换成AIC自动选择后,一下子就通过了。
4.3 Engle-Granger两步法
这是最经典的协整检验方法。名字听着高大上,其实就两步:
- 第一步:用OLS回归估计协整关系。比如我们检验X和Y是否协整,就做回归 Y = α + βX + ε,得到残差序列e_t。
- 第二步:对残差e_t做ADF检验。如果残差是平稳的,就说明X和Y存在协整关系。
听起来很简单对吧?但坑也不少。
我曾经在检验豆粕和菜粕的协整关系时,发现第一步回归的R²很高,但残差ADF检验就是不通过。后来仔细排查,发现是数据频率不一致——豆粕用的是主力连续合约,菜粕用的是指数合约,两个数据的换月规则不同,导致残差里藏了结构性断点。嗯,这里要注意:数据预处理比检验本身更重要。
注意事项:Engle-Granger两步法有个明显的缺点——它只能检验一对变量之间的协整关系。如果你有三四个品种想一起看,就得用Johansen检验了。另外,第一步回归中,选择哪个变量作为因变量,会影响检验结果。我建议两个方向都试一下,取检验结果更显著的那个。
4.4 Johansen检验
Johansen检验是基于VAR模型的协整检验方法。它的优势在于:
- 可以同时检验多个变量之间的协整关系
- 能给出协整向量的个数(即存在几组协整关系)
- 不需要事先指定哪个是因变量哪个是自变量
Johansen检验有两种统计量:迹统计量(Trace Statistic)和最大特征值统计量(Max Eigenvalue Statistic)。我个人更常用迹统计量,因为它对协整关系的检测更敏感一些。
在实际项目中,我一般先用Johansen检验看看整体情况,再用Engle-Granger两步法去具体构建套利组合。两者配合使用,效果最好。
核心要点:Johansen检验的结果解读,关键是看「协整秩」(cointegrating rank)。秩为0表示没有协整关系,秩为1表示存在一组协整关系,以此类推。秩的个数,就是你套利组合中独立线性关系的数量。
4.5 Python代码实现协整检验
好了,理论讲完了,咱们直接上代码。我用的库是statsmodels,它把ADF检验和Johansen检验都封装好了,非常方便。
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.vector_ar.vecm import coint_johansen
# 模拟两个协整序列
np.random.seed(42)
T = 500
# 生成随机游走作为共同趋势
common_trend = np.cumsum(np.random.randn(T))
# 生成两个序列,它们与共同趋势的差是平稳的
y1 = common_trend + np.random.randn(T) * 0.5
y2 = common_trend + np.random.randn(T) * 0.5 + 2 # 有截距偏移
# 1. ADF检验(单个序列的平稳性检验)
def adf_test(series, name=''):
result = adfuller(series, autolag='AIC')
print(f'序列 {name} 的ADF检验结果:')
print(f' ADF统计量: {result[0]:.4f}')
print(f' p值: {result[1]:.4f}')
print(f' 临界值 (1%): {result[4]["1%"]:.4f}')
print(f' 临界值 (5%): {result[4]["5%"]:.4f}')
print(f' 临界值 (10%): {result[4]["10%"]:.4f}')
if result[1] < 0.05:
print(' → 结论:序列平稳(拒绝单位根假设)')
else:
print(' → 结论:序列不平稳(存在单位根)')
print()
return result
# 先检验原始序列(应该不平稳)
adf_test(y1, 'y1')
adf_test(y2, 'y2')
# 2. Engle-Granger两步法
# 第一步:OLS回归
X = sm.add_constant(y1)
model = sm.OLS(y2, X).fit()
residuals = model.resid
print('Engle-Granger两步法:')
print(f' 回归系数: 截距={model.params[0]:.4f}, 斜率={model.params[1]:.4f}')
# 第二步:对残差做ADF检验
eg_result = adfuller(residuals, autolag='AIC')
print(f' 残差ADF统计量: {eg_result[0]:.4f}')
print(f' 残差ADF p值: {eg_result[1]:.4f}')
if eg_result[1] < 0.05:
print(' → 结论:残差平稳,y1和y2存在协整关系')
else:
print(' → 结论:残差不平稳,y1和y2不存在协整关系')
# 3. Johansen检验
def johansen_test(data, p=1):
"""
data: 二维数组,每列是一个序列
p: VAR模型的滞后阶数
"""
result = coint_johansen(data, det_order=0, k_ar_diff=p)
print('\nJohansen检验结果:')
print(f' 迹统计量: {result.lr1}')
print(f' 迹统计量临界值 (90%, 95%, 99%):')
print(f' {result.cvt}')
print(f' 最大特征值统计量: {result.lr2}')
print(f' 最大特征值临界值 (90%, 95%, 99%):')
print(f' {result.cvm}')
# 判断协整秩
for i in range(len(result.lr1)):
if result.lr1[i] > result.cvt[i, 1]: # 使用95%临界值
print(f' 协整秩 ≥ {i+1}(在95%置信水平下显著)')
else:
print(f' 协整秩 = {i}(在95%置信水平下不显著)')
break
return result
# 将两个序列合并成矩阵
data_matrix = np.column_stack([y1, y2])
johansen_test(data_matrix, p=1)
# 4. 可视化残差
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(y1, label='y1')
plt.plot(y2, label='y2')
plt.title('原始序列')
plt.legend()
plt.subplot(2, 1, 2)
plt.plot(residuals, label='残差(价差)', color='red')
plt.axhline(y=0, color='black', linestyle='--', alpha=0.3)
plt.axhline(y=residuals.mean(), color='green', linestyle='--', alpha=0.5, label='均值')
plt.title('Engle-Granger残差(价差序列)')
plt.legend()
plt.tight_layout()
plt.show()
代码解读:上面这段代码,我故意模拟了两个协整序列。你运行后会看到:原始序列的ADF检验p值很高(不平稳),但残差的ADF检验p值很低(平稳)。这就是协整的典型特征。Johansen检验的结果会告诉你协整秩为1,说明存在一组协整关系。
4.6 实战中的避坑指南
做协整检验这么多年,我踩过的坑比走过的路还多。这里分享几个最关键的:
- 数据频率要一致:别把日线数据和小时线数据混在一起检验。我见过有人用5分钟K线做协整,结果发现残差有很强的自相关——那是因为高频数据存在微观结构噪声。
- 小心结构性断点:2015年股灾、2020年疫情,这些事件会导致协整关系暂时破裂。我建议在检验前,先对数据做滚动窗口的协整检验,看看关系是否稳定。
- 不要过度优化:如果你用不同的滞后阶数、不同的检验模式反复试,总能找到「显著」的结果。但这叫数据挖掘,不叫统计套利。我一般会固定一套参数,然后做样本外验证。
- 协整关系会变:两个品种的协整关系不是永恒的。我建议每个月重新检验一次,如果发现协整关系消失了,就果断放弃这个组合。
最后说一句:协整检验只是第一步。真正赚钱的套利策略,还需要考虑交易成本、滑点、资金管理、止损止盈。别以为检验出协整关系就万事大吉了——那只是万里长征的第一步。