上一篇讲了配对交易的核心逻辑——找两只"有关系"的股票,等价差偏离时做多低估的、做空高估的,赌关系回归。
听起来简单。但问题来了:你怎么知道这两只股票真的"有关系"?
很多人觉得,同行业就行了。都是银行股,肯定配。都是白酒股,没问题。然后一看相关性,0.85、0.90,更高了,信心满满地上仓。
结果,价差越拉越大,越等越不回,账户一直在亏。
问题出在哪?出在你用了一个远远不够严格的筛选标准。相关性只是"看起来像",协整才是"确实是"。
今天这期,把协整检验这件事彻底讲透。从数学直觉到实操代码,从Engle-Granger到Johansen,再到怎么批量筛选、怎么监控、怎么判断一对品种还能不能继续用。全部拆开给你看。
Why:为什么相关性不够,必须做协整检验?
一个真实案例
2020年3月,有个量化新手做了个"配对交易":做多东方财富,做空中信证券。逻辑很直觉——都是券商股,相关性高达0.85,走势看起来步调一致,应该很配。
结果呢?疫情之后,东方财富因为互联网券商逻辑暴涨了40%,中信证券只涨了15%。价差越拉越大,账户浮亏25%,最后被迫止损离场。
事后复盘,他才发现一个关键事实:这两只股票确实是"同涨同跌"的,相关性很高,但它们的价差一直在往上漂移——东方财富的估值中枢从2018年开始就不断上移,中信证券没有跟上。
这意味着什么?意味着价差从来没有"回归"的倾向。它不是橡皮筋,拉开会弹回来。它更像是一根绳子,一旦拉开,就永远拉开了。
这就是"高相关但低协整"的典型陷阱。
相关性和协整的本质区别
打个比方。
你和一个朋友每天从同一个小区出发去上班,一个坐地铁,一个开车。你们每天到达公司的时间有相关性——路上堵的时候两个人都晚,路上顺畅时两个人都早。这就像"相关性":两只股票同涨同跌。
但如果你关心的是"你们两人到达时间之差是否稳定"——这就是协整。
如果你们每天出门时间固定、路上耗时模式也稳定,那这个时间差就是稳定的——这就是协整。
但如果你的朋友最近换了工作,新公司更远,他的到达时间每天比你晚半小时,而且这个差距还在慢慢扩大——你们依然高度相关(都受早高峰影响),但时间差不再稳定了。
在金融里,两只股票可以有0.9的相关性,但价差持续漂移。东财和中信就是这样——它们都受市场情绪驱动(所以相关),但估值逻辑不同(所以价差漂移)。
相关性是必要条件,协整才是充分条件。
诺贝尔奖级别的工具
协整理论是由新西兰经济学家Clive Granger在1982年提出的,他和Robert Engle一起因此获得了2003年诺贝尔经济学奖。
这不是学术玩具,是配对交易的根基。没有协整检验,配对交易就是在赌两只股票"应该会回归"——但凭什么?凭什么你说了算?
协整检验给你的,是统计学上严格的证据:这两只股票之间的价差,确实存在均值回归的特性。 有了这个证据,你的交易才有根基。
不做协整检验的后果
跳过协整检验直接做配对交易,等于在沙子上盖楼:
• 你以为在套利,实际在赌方向。 价差如果不回归,你的多头仓位就是在裸多,空头仓位就是在裸空。两个裸仓叠加,遇到单边行情可能两头亏。
• 止损变成了最难的决策。 因为没有统计依据告诉你"价差偏离多少算异常",你分不清当前偏离是正常波动还是关系破裂,只能凭感觉止损,要么止早了要么止晚了。
• 回测结果看起来很美,实盘一塌糊涂。 因为你在历史数据里找到的"稳定价差"可能只是偶然——没有协整关系做支撑,这种稳定随时消失。
What:协整的数学概念与直觉理解
什么是协整?
严格定义:
两个时间序列X(t)和Y(t),如果满足以下条件,则它们是协整的:
1. X(t)和Y(t)各自都是一阶单整的,记为I(1)——也就是说,它们各自都是非平稳的,有自己的趋势和随机波动
2. 存在常数α和β,使得线性组合 Z(t) = Y(t) - α - β×X(t) 是平稳的,记为I(0)
平稳意味着什么?
• Z(t)有一个固定的均值(不会无限漂移)
• Z(t)有一个有限的方差(不会无限发散)
• 偏离均值后,会被"拉回来"(均值回复性)
这三条,恰好是配对交易所需要的一切。价差有均值,你可以知道"正常水平"是多少。价差会回归,你可以预期偏离后的修复。方差有限,你可以量化风险和止损位。
直觉理解:想象两只股票各自像醉汉一样随机游走,路径完全不可预测。但协整的意思是,这两个醉汉之间拴着一根橡皮筋。每个人怎么走 unpredictable,但他们之间的距离会被橡皮筋限制。拉远了,会被拉回来。
这个"橡皮筋",就是协整关系的本质。
平稳性有多重要?
反过来想:如果价差Z(t)不是平稳的,会发生什么?
• 没有固定均值:你根本不知道"正常水平"是什么,Z-score无从计算
• 偏离不会回归:今天价差偏离了10%,可能明天偏离15%,后天20%,永远不回来
• 方差无限大:风险无法量化,止损位设在哪里都没有意义
一句话:没有平稳性,均值回归策略的地基就塌了。
为什么股价本身不平稳,价差却可以是平稳的?
股票价格本质上是一个随机游走过程——今天的价格和昨天的价格高度相关,明天的价格不可预测。这在统计学上叫"一阶单整"(I(1)),也就是差分一次后才变成平稳。
但两只股票如果受共同的基本面驱动(同行业、同市场、同政策环境),它们的随机游走可能有共同的"趋势成分"。当你用Y减去β倍的X时,共同的趋势成分被抵消掉了,剩下的就是围绕均值波动的平稳成分。
打个比方。两个人各自在随机散步,但他们被一根绳子连着。每个人走过的路径都是随机的、不可预测的,但他们之间的距离始终被绳子约束着。你把两个人的位置相减(算出距离),这个距离序列就是平稳的——它不会无限增大,因为绳子在那里。
两种主要的协整检验方法
Engle-Granger两步法
最经典、最直观的方法:
第一步,对Y关于X做OLS回归:Y = α + βX + ε,得到残差序列ε(t)。
第二步,对残差ε(t)做ADF检验,判断残差是否平稳。
如果ADF检验的p值小于0.05,拒绝"残差不平稳"的原假设,确认X和Y存在协整关系。
优点:简单直观,容易理解,两个变量时非常好用。
局限:
• 只能检验两个变量之间的关系
• Y和X的角色互换可能得到不同结果(顺序依赖性)
• 小样本下检验效力偏弱
• 只能检测一个协整关系
Johansen检验
更强大的方法,适合多变量场景:
核心思路是把问题转化为向量自回归(VAR)系统的特征值分解。通过对系数矩阵Π做特征值分解,特征值的个数就是协整关系的个数。
优点:
• 不依赖变量顺序
• 可以同时处理多个变量
• 能检测出多个协整关系
• 统计效力更强
局限:
• 数学理解门槛更高
• 参数设置更复杂(确定性项的选择、滞后阶数)
• 两个变量时和EG方法结论基本一致,没有明显优势
实操选择:两个品种用Engle-Granger就够了,三个及以上用Johansen。
如何解读检验结果
拿到协整检验结果后,不能只看p值。需要综合判断四个维度:
| 指标 | 含义 | 判断标准 | | --- | --- | --- | | p值 | 拒绝"不协整"原假设的概率 | < 0.05 为显著 | | β(协整系数) | 对冲比率 | 0.5-2.0之间,偏离太大说明经济逻辑有问题 | | 半衰期 | 价差偏离后回归到一半所需天数 | 5-30天为佳,太短是噪音,太长不实用 | | 价差图 | 残差的时间序列可视化 | 应围绕0震荡,无明显趋势或结构断点 |
特别重要:一定要画图看价差。
p值只是统计检验的结果,它告诉你"在给定样本下,协整关系在统计上显著"。但统计显著不等于实际好用。
好的价差图:围绕0轴来回震荡,波动幅度大致均匀,没有明显的趋势性偏移,没有突然的结构跳跃。
差的价差图:虽然p值 < 0.05,但图上能看到明显的上升或下降趋势(弱平稳),或者在某个时间点前后出现了永久性的偏移(结构断点)。这种情况说明协整关系可能正在弱化或者已经破裂。
p值是门槛,价差图才是决策依据。
How:协整检验的完整实操步骤
第一步:数据准备和预处理
// python
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint, adfuller
def prepare_data(price_a, price_b):
"""
准备协整检验数据
关键:使用对数价格而非原始价格
原因:对数价格的回归系数β直接解释为弹性
(Y变动1%对应X变动β%),对冲比率更稳定
"""
# 取对数
log_a = np.log(price_a)
log_b = np.log(price_b)
# 去掉缺失值
valid = ~(np.isnan(log_a) | np.isnan(log_b))
return log_a[valid], log_b[valid]
逐行解释:
• np.log(price):取对数。用对数价格而不是原始价格做回归,好处是β直接表示弹性关系,而且对数变换可以减少异方差性
• 为什么要去除缺失值?因为OLS回归不能处理NaN,停牌日的数据需要在前一步已经处理好
数据选择建议:
• 时间窗口:至少2年日频数据,太短样本不够,太长可能包含已经过期的历史关系
• 频率:日频最适合配对交易,分钟数据噪音太大
• 复权方式:统一使用前复权或后复权,不能混用
第二步:Engle-Granger协整检验
// python
def engle_granger_test(y, x, significance=0.05):
"""
Engle-Granger两步法协整检验
参数:
- y: 股票Y的对数价格序列
- x: 股票X的对数价格序列
- significance: 显著性水平,默认0.05
返回:
- 包含beta、p值、半衰期、价差序列等信息的字典
"""
# ===== 第一步:OLS回归 =====
x_with_const = sm.add_constant(x)
model = sm.OLS(y, x_with_const).fit()
alpha = model.params.iloc[0] # 截距
beta = model.params.iloc[1] # 斜率(对冲比率)
# 残差 = 价差
spread = y - alpha - beta * x
# ===== 第二步:ADF检验残差平稳性 =====
adf_result = adfuller(spread, maxlag=1)
adf_stat = adf_result[0]
pvalue = adf_result[1]
critical_values = adf_result[4]
# ===== 计算半衰期 =====
# 方法:对价差做AR(1)回归,从系数推算回归速度
spread_lag = spread.shift(1).dropna()
delta_spread = (spread - spread.shift(1)).dropna()
reg = sm.OLS(delta_spread, sm.add_constant(spread_lag)).fit()
gamma = reg.params.iloc[1] # AR(1)系数
# 半衰期 = -ln(2) / gamma
# gamma为负时才有意义(说明偏离会被拉回)
if gamma < 0:
half_life = -np.log(2) / gamma
else:
half_life = np.inf # 不回归
# ===== 汇总结果 =====
result = {
'beta': round(beta, 4),
'alpha': round(alpha, 4),
'pvalue': round(pvalue, 4),
'adf_stat': round(adf_stat, 4),
'critical_5pct': critical_values['5%'],
'is_cointegrated': pvalue < significance,
'half_life': round(half_life, 1),
'spread': spread
}
return result
逐行解释:
• sm.add_constant(x):给X添加常数项列,否则OLS不会计算截距α
• sm.OLS(y, x_with_const).fit():最小二乘回归,找到最优的α和β
• spread = y - alpha - beta * x:这就是价差序列,也就是我们要交易的"东西"
• adfuller(spread, maxlag=1):ADF检验。原假设是"序列有单位根(不平稳)"。p值越小,越有理由拒绝原假设,说明价差是平稳的
• 半衰期计算:用AR(1)模型拟合价差变化,gamma是回复速度系数。half_life = -ln(2)/gamma,gamma越负,回归越快,半衰期越短
• np.inf:如果gamma不是负的,说明价差没有回归趋势,半衰期无穷大,这个配对不能用
第三步:Johansen检验(多变量场景)
// python
from statsmodels.tsa.vector_ar.vecm import coint_johansen
def johansen_test(data, det_order=0, k_ar_diff=1):
"""
Johansen协整检验
参数:
- data: DataFrame,每列一个变量的对数价格
- det_order: 确定性项类型
-1 = 无确定性项
0 = 只有常数项(常选)
1 = 常数项+线性趋势
- k_ar_diff: VAR差分滞后阶数
返回:
- 协整关系数量、协整向量、检验统计量
"""
result = coint_johansen(data, det_order, k_ar_diff)
# 提取检验统计量
trace_stat = result.lr1 # 迹统计量
max_eig_stat = result.lr2 # 最大特征值统计量
trace_cv = result.cvt # 迹检验临界值(3列:90%/95%/99%)
max_eig_cv = result.cvm # 最大特征值检验临界值
# 判断协整秩(5%显著性水平下)
n_coint = 0
for i in range(len(trace_stat)):
if trace_stat[i] > trace_cv[i, 1]: # 第2列是95%临界值
n_coint = i + 1
else:
break
# 提取协整向量
eigenvectors = result.evec
return {
'n_coint_relations': n_coint,
'eigenvectors': eigenvectors,
'trace_stats': trace_stat,
'trace_cv_95': trace_cv[:, 1],
'max_eig_stats': max_eig_stat,
'max_eig_cv_95': max_eig_cv[:, 1]
}
逐行解释:
• coint_johansen(data, det_order, k_ar_diff):核心函数。data每列是一个时间序列
• det_order=0:表示VAR模型中包含常数项但不含时间趋势。这是最常用的设置
• trace_stat vs max_eig_stat:两种不同的检验方法。迹检验检验"协整秩是否≤r",最大特征值检验检验"第r+1个协整关系是否显著"。一般以迹检验为主
• trace_cv[:, 1]:临界值矩阵的第2列,对应95%置信水平。统计量大于临界值,拒绝原假设,认为存在协整关系
• eigenvectors:协整向量矩阵,每一列是一个协整关系。可以用来构造多变量价差
第四步:批量筛选配对品种
// python
def batch_coint_screen(price_df, stock_list,
p_threshold=0.05,
hl_min=5, hl_max=30,
beta_range=(0.5, 2.0)):
"""
批量协整检验筛选
在一组股票中,两两做协整检验,
筛出满足所有条件的配对
参数:
- price_df: 价格DataFrame,列为股票代码
- stock_list: 要筛选的股票列表
- p_threshold: p值上限
- hl_min/hl_max: 半衰期范围(天)
- beta_range: 对冲比率合理范围
返回:
- 符合条件的配对列表,按p值排序
"""
from itertools import combinations
results = []
for stock_a, stock_b in combinations(stock_list, 2):
# 取对数价格
y = np.log(price_df[stock_a].dropna())
x = np.log(price_df[stock_b].dropna())
# 对齐长度
min_len = min(len(y), len(x))
y = y.iloc[-min_len:]
x = x.iloc[-min_len:]
# 做协整检验
test = engle_granger_test(y, x)
# 筛选条件
if (test['is_cointegrated'] and
hl_min <= test['half_life'] <= hl_max and
beta_range[0] <= abs(test['beta']) <= beta_range[1]):
results.append({
'pair': f"{stock_a}-{stock_b}",
'beta': test['beta'],
'pvalue': test['pvalue'],
'half_life': test['half_life'],
'spread': test['spread']
})
# 按p值从小到大排序(越显著越靠前)
results.sort(key=lambda r: r['pvalue'])
return results
# 示例:在银行股中批量筛选
# bank_stocks = ['601398', '601939', '601288', '601988',
# '600036', '601166', '000001', '600000']
# pairs = batch_coint_screen(price_data, bank_stocks)
# for p in pairs:
# print(f"{p['pair']}: p={p['pvalue']}, "
# f"β={p['beta']}, 半衰期={p['half_life']}天")
逐行解释:
• combinations(stock_list, 2):两两组合,不重复不遗漏
• np.log():取对数价格,让β解释为弹性
• 对齐长度:不同股票可能有不同的停牌日,需要对齐数据长度
• 三重筛选条件:p值 < 0.05(统计显著)+ 半衰期5-30天(回归速度合理)+ β在0.5-2.0之间(经济意义合理)
• 按p值排序:p值越小,协整关系越显著,配对越可靠
实操经验:
• A股银行股中,通常能筛出5-8对有效配对
• 最稳定的往往是大行之间:工行-建行、农行-中行,半衰期10-15天
• 大行和股份行之间也可能协整,但β可能偏离1较多,对冲成本更高
第五步:持续监控与再检验
回测数据仅作教学演示,不代表未来表现
协整关系不是一劳永逸的。两家公司可能因为并购、业务转型、行业政策变化等原因,价差关系发生结构性改变。
监控体系设计:
// python
def monitor_pair(spread, lookback=60, warning_p=0.10,
emergency_z=4.0):
"""
配对交易持续监控
参数:
- spread: 最新价差序列
- lookback: 滚动窗口
- warning_p: p值预警阈值
- emergency_z: Z-score紧急止损阈值
返回:
- status: 'normal'/'warning'/'emergency'
- details: 详细指标
"""
# 1. 滚动ADF检验(最近lookback天)
recent_spread = spread.iloc[-lookback:]
adf_result = adfuller(recent_spread, maxlag=1)
current_p = adf_result[1]
# 2. 当前Z-score
mean = spread.rolling(lookback).mean().iloc[-1]
std = spread.rolling(lookback).std().iloc[-1]
current_z = (spread.iloc[-1] - mean) / std
# 3. 对冲比稳定性(用滚动窗口的β标准差衡量)
# 省略具体实现...
# 判断状态
if abs(current_z) > emergency_z:
status = 'emergency' # 紧急止损
elif current_p > warning_p:
status = 'warning' # 协整关系弱化
else:
status = 'normal' # 正常运行
return {
'status': status,
'current_p': round(current_p, 4),
'current_z': round(current_z, 2),
'current_spread': round(spread.iloc[-1], 4)
}
监控频率:
• 每日:检查价差Z-score,判断是否触发入场/出场/止损
• 每周:滚动ADF检验,检查p值是否还在显著范围
• 每季度:完全重新跑协整检验,更新β和半衰期
• 每半年:在全市场重新扫描,寻找新的配对机会
退出信号:
• p值 > 0.10:协整关系在弱化,暂停新开仓,已有仓位设紧止损
• 半衰期翻倍:回归速度明显变慢,减仓观望
• Z-score超过4:极端偏离,大概率关系破裂,无条件止损
• β变化超过30%:对冲比率大幅漂移,需要重新校准或退出
实操数据参考(A股银行股+券商股2019-2024年):
• 从50只金融股中筛出8对有效配对
• p值范围:0.001-0.04
• 半衰期范围:8-22天
• 最稳定的配对:工行-建行(半衰期12天,β=1.02,接近1:1对冲)
• 最不稳定的配对:某股份行-某城商行(半年后p值升至0.15以上,需退出)
总结
协整检验是配对交易的身份证查验。没有它,你分不清哪些配对是真有关系、哪些只是"凑巧看起来像"。
今天讲了五个关键知识点:
1. 相关性 ≠ 协整:同涨同跌不代表价差会回归,必须用协整检验确认价差的平稳性
2. 协整的直觉:两个醉汉被橡皮筋拴着,各自的路线不可预测,但距离会回归
3. 两种检验方法:两个变量用Engle-Granger(简单直观),三个以上用Johansen(更强大)
4. 不要只看p值:p值是门槛,还必须看半衰期是否合理、β是否有经济意义、价差图是否真的平稳
5. 持续监控是必须的:协整关系会随时间退化,定期再检验是配对交易的生命线
最后一句话:做配对交易之前,先过协整检验——这不是可选项,是必选项。