股票协整检验与配对交易:从ADF到策略实现的完整指南
2026/9/15 3:23:30 网站建设 项目流程

简介:面向股票量化分析与时间序列研究者,这份压缩包聚焦协整性检验,结合周期共振与多标的同步分析,可揭示多只股票价格序列间的长期均衡关系及短期偏离规律,适用于配对交易或均值回归策略的前期研究。压缩包共十个文件,包括执行协整分析流程的脚本、三只股票的原始数据表、记录两种单位根检验步骤与解读的文档,以及展示差分前后序列变化的可视化图,整体大小约三百五十一千字节,轻量便携。文档对比了直接回归与差分后回归两种处理路径,并以图形直观呈现协整关系和差分效果,适合具备一定统计学基础、希望掌握时间序列方法的中级数据分析师。已有两百九十六人学习下载,通过该资源可获得可复跑的完整代码、整理好的实验数据、图文并茂的分析报告,并能将分析框架迁移到其他股票组合,为量化建模提供实证参考。

1. 为什么同一套协整代码,换两只股票就失灵

做配对交易的人大概都遇到过这种尴尬:用某只股票组合跑协整检验,p-value漂亮得能当壁纸,可一旦把股票代码换成另一组,同样的流程立刻返回一堆非平稳的残差。问题通常不出在代码,而出在很多人把协整分析做成了"最小二乘回归 + ADF 检验"的流水线,忽略了一个关键前提——两组价格序列的阶数是否一致,以及回归之后残差是不是真的被检验到了点上。

这个资源包里给出的股票协整性分析.py和两组 ADF 结果,正好把这个问题拆开了:一组是"直接回归"再对残差做 ADF,另一组是"各自差分后再回归"的对照。600776、002409、300473 这三只票,从日线价格上看都不是平稳序列,但一阶差分后大概率平稳,这种情况在金融时间序列里叫 I(1) 过程。协整的意义就在于:虽然单看每只股票价格乱走,但它们之间存在一个线性组合,这个组合本身是平稳的,那这个组合就可以拿来构造均值回归策略。

明白这个前提之后再看包里的文件就有意思了。直接回归adf.docx是拿原始价格直接跑 OLS 然后对残差做adfuller()差分后回归adf.docx是先对每只股票做一阶差分、再回归、再跑 ADF。前者若能通过,说明价格之间存在稳定的线性关系;后者能通过,说明关系体现在"变动量"层面,这对构建交易信号的指导意义完全不同。下面从操作层面把两条路线的原理、代码和判读标准说透,顺便把常见报错和误判点也指出来。

2. 协整检验的完整流程与coint函数用法

协整检验不是"跑一下statsmodels.tsa.stattools.coint就完事"这么简单。它背后是 Engle-Granger 两步法:先用 OLS 估计长期均衡关系,再对残差做单位根检验。这个包里的股票协整性分析.py实际上就是把这两步手工展开,好处是对残差序列有完全的控制权,坏处是如果中间某一步序列阶数不一致,结果就会出现"看起来显著、实际上没用"的假象。

2.1 Engle-Granger 两步法的代码实现

常见做法是拿原始价格做 OLS,然后对残差做 ADF。下面的代码是资源里股票协整性分析.py这类脚本的核心片段,用 pandas 和 statsmodels 就能跑通:

import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller, coint import statsmodels.api as sm # 读取三只股票的收盘价序列(假设df.csv包含日期和价格列) df = pd.read_csv('df.csv', index_col='date', parse_dates=True) df = df[['600776', '002409', '300473']].dropna() # 直接回归:用600776作为因变量,另两只作为自变量 X = df[['002409', '300473']] X = sm.add_constant(X) ols_model = sm.OLS(df['600776'], X).fit() residual = ols_model.resid # 对残差做ADF检验,判断残差是否平稳 adf_result = adfuller(residual, autolag='AIC') print(f'ADF统计量: {adf_result[0]:.4f}') print(f'p值: {adf_result[1]:.4f}') for key, value in adf_result[4].items(): print(f'临界值({key}): {value:.4f}') # 对比:直接使用statsmodels的coint函数(内置EG两步法) coint_t, coint_p, coint_crit = coint(df['600776'], df[['002409', '300473']]) print(f'coint-t: {coint_t:.4f}, coint-p: {coint_p:.4f}')

这段代码的逻辑是:先读入三只股票价格,dropna()让序列对齐;然后以 600776 为因变量做 OLS,得到残差序列;对残差做adfuller时用的是 AIC 自动选择滞后阶数,这一步是为了确认残差不含单位根。sm.add_constant是给回归加上截距项,这在股票价格回归里是必须的,因为两只股票的绝对价格水平通常不在一个数量级。

ADF 检验的判定标准是看 p 值:小于 0.05 说明残差平稳,即存在协整关系;大于 0.05 则无法拒绝"残差含单位根"的原假设,说明两条价格序列的线性组合不稳定,不能直接用来构造配对交易信号。这里还有一个容易忽略的细节:adfuller默认带常数项不带趋势项,股票价格回归的残差确实不应该有趋势,如果发现残差有明显的线性趋势,就要回头检查是不是漏了什么结构性断点。资源里的直接回归adf.png应该是把这段代码跑出来的残差走势和 ADF 结果贴在了一张图上,看到残差曲线在零轴上下随机摆动、没有明显的单边漂移,才能初步判断方向是对的。

2.2 回归之后再看差分:两种回归的根本区别

协整性600776-002409-300473差分后回归adf.png对应的做法是把每只股票的价格序列先diff()一次,再用差分后的序列做同样的 OLS 和 ADF。这段处理在 python 里几乎不增加代码量:

df_diff = df.diff().dropna() # 同样以600776为因变量做回归 X_diff = sm.add_constant(df_diff[['002409', '300473']]) ols_diff = sm.OLS(df_diff['600776'], X_diff).fit() resid_diff = ols_diff.resid adf_diff = adfuller(resid_diff, autolag='AIC') print(f'差分后残差ADF p值: {adf_diff[1]:.4f}')

差分后的回归和直接回归在数学上描述的是完全不同的关系。直接回归说"600776 的价格约等于另外两只股票价格的线性组合加上一个稳定误差";差分后回归说"600776 的涨跌幅约等于另外两只涨跌幅的线性组合加上一个稳定误差"。后者对做日频交易的参考意义更大,因为它拟合的是收益率的联动,而不是价格水平的联动。

从结果上看,直接回归那一组如果残差 ADF 通过,说明三只股票存在长期均衡的价格关系,适合做均值回归——偏离了这个均衡关系就开仓,回归了就平仓。差分后回归如果通过,说明收益率的同步性高,那不是均值回归逻辑,而更像是动量共振,参考价值更多体现在不同股票之间能否相互预测短期涨跌。操作时最容易犯的错误就是把这两类结果混着解读:拿差分后回归的显著性去支撑"价格会回归"的判断,这等于把两个不同假设的检验统计量放在同一个决策框架里,钱亏了都不知道是策略错还是检验结果用错。

3. ADF 检验的参数选择与三张图的判读方法

ADF 检验是协整分析的核心裁判,但裁判的尺子不是固定的。adfuller函数的参数直接决定检验结论是否可信。资源里的三张 png 输出,分别用"价格水平直接回归残差"和"差分后回归残差"做了对比演示,看懂这三张图,其实就理解了协整分析在股票数据上最常见的呈现方式。

3.1 ADF 检验的三个关键参数

# 标准写法:指定回归项和滞后阶数选择方式 adf_result = adfuller( residual, maxlag=20, # 最大滞后阶数 regression='c', # 含常数项(c),可选'ct'含趋势项,'n'无常数项 autolag='AIC' # 自动选择滞后阶数 )

参数说明:maxlag控制允许的最大滞后阶数,日频收益率数据一般取 10 到 30 都能覆盖较强的自相关结构;regression='c'表示检验回归方程中带常数项,这是金融价格残差序列的标准配置;autolag='AIC'让函数在 0 到maxlag之间自动挑最优滞后阶数,避免手动设置滞后阶数带来的主观偏差。

regression='ct'(含趋势项)这个选项在股票协整中要特别小心。如果序列本身没有确定性趋势却硬加一个趋势项,检验功效会下降,本来应该拒绝单位根原假设的残差,反而可能被判为不平稳。只有当直接回归的残差图明显出现一条斜线——也就是价差长期在零轴上方向单一漂移——才考虑用趋势项重新检验。

包里的协整性600776-002409-300473差分.png展示的应该是一阶差分后的价格走势。用差分后的序列做检验时,数据里二阶或更高阶的单位根已经被消掉,剩下的问题只是残差平不平稳。两块图连起来看的意义在这里:价格水平图能看出三只票大方向一致但节奏不完全同步,差分图能看出波动聚集现象——涨跌幅大的时段三只票同步放大,平稳时段同步收窄,这正是金融时间序列里常见的波动率聚集特征。

3.2 看图定结论:序列图和 ADF 输出图的配合

做协整检验不能只看 p 值,必须把残差序列图和 ADF 输出合在一起看才有说服力。

观察对象平稳残差特征非平稳残差特征
残差时序图围绕 0 或均值附近随机波动,无趋势、无周期有长周期漂移,偏离后不回归
自相关图各阶自相关迅速衰减到置信区间内低阶自相关显著不为 0,衰减缓慢
ADF 统计量绝对值明显大于 1% / 5% 临界值绝对值小于 5% 临界值

ADF 输出的结果是一个伴随统计量,一般不用单独看t-stat的绝对大小,直接看 p 值即可:p 小于 0.05 拒绝单位根原假设。但要留意的是,ADF 检验在样本量只有 100 到 200 个交易日时会变得非常不敏感,小样本下即使残差真平稳,p 值也可能大于 0.1。所以对日频股票数据,至少要用一年以上的历史数据(250 个交易日以上)才有统计意义。资源里600776-002409-300473直接回归adf.png的样本量从图上应该能看出来,如果不到 200 个点,哪怕 p 值勉强小于 0.05 也要留个心眼。

样本量范围ADF 可靠性建议操作
<100 个交易日很低补充数据或弃用结果
100~250一般结合残差图二次确认
>250较好正常放行

当三张图形成证据链——价格序列同涨同跌、价差序列在固定区间内波动、差分后回归的 ADF p 值显著——这个组合才真正有实际交易含义。缺任何一环,协整结论都可能是统计幻觉。

4. 从协整组合到配对交易策略:回测与调参

协整检验通过只是第一步,把它变成可执行的交易策略才见功力。资源包的df.csv和分文件df1.csvdf2.csv应该是不同时间窗口或不同复权方式的数据集,正好拿来做参数稳健性验证。我一般会先在同一份数据上跑出协整组合,再换到另一份数据上检验同样的参数是否仍然有效。

4.1 z-score 价差信号与参数表

标准配对交易框架长这样:用滚动窗口估算协整系数,实时计算价差,当价差偏离均值超过阈值开仓,回归均值时平仓。

# 核心策略实现在coint结果基础上做z-score def generate_signals(price_a, price_b, coint_coef, window=20, entry=2.0, exit=0.5): # 构造价差spread:price_a - coef * price_b spread = price_a - coint_coef * price_b # 滚动均值与标准差 mean = spread.rolling(window).mean() std = spread.rolling(window).std() zscore = (spread - mean) / std position = np.zeros(len(spread)) # zscore上穿entry做空spread,下穿-exit平仓 position[zscore > entry] = -1 position[zscore < -entry] = 1 position[zscore.abs() < exit] = 0 return pd.Series(position, index=price_a.index).fillna(0)

关键参数集中在entryexitwindow三个值上。entry是开仓阈值,设为 2.0 意味着只有当价差偏离均值超过两个标准差才进场,阈值太小会让策略在价差正常波动区间内反复开平仓,交易成本吃掉收益;阈值太大则交易机会变少,一年下来可能只有几次入场。exit是平仓阈值,常见做法是设到 0.5 或直接设成 0,意思是价差回归均值附近就退出。window控制滚动统计的窗口长度,既要足够长让均值稳定,又不能太长导致对近期市场结构的变化反应迟钝。

4.2 参数敏感性与换期检验

任何协整策略都逃不过一个灵魂拷问:协整关系是数据挖掘出来的,还是真实存在的?解决这个问题有一个很笨但有效的办法——换样本期验证。

验证方式做法通过标准
前段/后段分割前半段找协整组合,后半段看策略表现后段收益为正且回撤可控
换股票验证用同一算法在代码 K 里重跑出现类似比例的显著组合即可
滚动协整检验每次用 250 日窗口滚动做coint协整关系持续存在,不频繁消失

用这个标准去回看资源里的df.csvdf2.csv:如果两个文件是不同行情区间,那么一只股票组合只有在两份数据里都能通过协整检验,同时策略参数在两个区间绩效都稳定,才能说明这个组合值得投入真金白银。如果只在某一小段样本内显著,换一段数据就失效,那就只是过拟合。反过来,如果组合能稳定通过,用它跑特色策略就有底气得多。

这里给出的策略代码是保证金交易时代最常见的基础设置,真实运行时还会加上仓位管理和手续费模拟。做回测时注意别把手续费设成 0,否则 ADF 检验的显著性本身无法覆盖掉交易摩擦,前者解决的是统计问题,后者解决的是成本问题。

5. 用单整阶数对齐绕开协整检验的常见误伤

协整分析里最隐蔽的问题不是参数没调对,而是把不同单整阶数的序列强行塞进回归。GDP 和股价、成交量和价格,这些序列单看都不是平稳的,但它们之间可能根本不存在协整关系。要搞清楚一组序列能不能做协整,先得回答"各自是不是 I(1)",这就需要对每一只股票单独做 ADF 检验。

5.1 单整阶数检验的快速套用

资源包里没有单独给出这一步骤的脚本,但这是几乎每个工程师接手协整分析后最常用的补充逻辑,直接用adfuller就能完成:

def check_stationarity(series): """检查序列平稳性,返回单整阶数""" # 判断原序列是否平稳 adf_t, adf_p, *_ = adfuller(series, autolag='AIC') if adf_p < 0.05: return 0 # 本身平稳,I(0) # 一阶差分后再检验 diff_t, diff_p, *_ = adfuller(series.diff().dropna(), autolag='AIC') if diff_p < 0.05: return 1 # 差分后平稳,I(1) return 2 # 需要二阶差分 # 对三只股票分别检查 for col in ['600776', '002409', '300473']: order = check_stationarity(df[col]) print(f'{col}: I({order})')

这段代码逻辑并不复杂:先对原序列做 ADF,p 小于 0.05 直接判定 I(0);不行就对一阶差分再做一次 ADF,通过的就是 I(1)。之所以要有这一步,是因为 Engle-Granger 两步法对阶数非常敏感:两只股价分别是 I(1) 和 I(2),回归残差必然不平稳,别说协整了,连"伪回归"这个帽子都摘不掉。

值得注意的是,df1.csvdf.csv如果取自不同时间段,同一只股票的单整阶数应该一致——价格序列的一阶差分反映的是日度收益,理论上不随时间窗口改变而改变。如果同一只股票在前半段是 I(1),后半段检验出来是 I(0),说明股价在某一时期本身有均值回归特征(震荡市常见),这种状态下协整的意义就会减弱。

5.2 ADF 与 Johansen 的适用场景分界

Engle-Granger 两步法的弱点在于它只能估计一组协整关系,当处理三只以上股票时,可能存在的协整向量不止一个,OLS 只能捕捉其中一个。更规范的做法是升级到 Johansen 检验,statsmodels 提供对应接口:

from statsmodels.tsa.vector_ar.vecm import coint_johansen # 检查三只股票价格之间是否存在多个协整向量 johansen_res = coint_johansen(df[['600776', '002409', '300473']], det_order=0, k_ar_diff=1) print(f'trace统计量: {johansen_res.lr1}') print(f'最大特征值统计量: {johansen_res.lr2}')

协整向量数量超过 1 时,EG 两步法给出的回归系数实际上只是无数个协整向量中的一个,不一定对应真实的均衡关系。此时建议以 Johansen 检验的 trace 统计量为主判据;股票数量越多,越应该依赖 Johansen 而不是简单回归加 ADF。反过来,如果只处理一对一的配对交易,EG 两步法更直观,回归系数本身就是对冲比例。

实际上,这个资源包里的三只股票组合用 Johansen 检验的统计意义更大——三只股票之间到底存在几个协整关系,决定了策略到底是"两两配对"还是"一组组合共同回归"。分清这个区别才能决定后续策略构造方式,这也是从"用协整检验"走向"用协整做策略"的重要分水岭。

6. 解读"前 100 字"里的信息差:从统计结果到交易决策的最后一公里

协整分析最有价值的地方其实不是它的结论,而是它的边界——数据说明的是统计意义上的长期均衡,而不是交易意义上的稳定盈利。资源包里给出的两组 ADF 对比图,恰好用来做决策流程上最后的把关。

直接回归adf.png里统计量的显著性之前,先注意样本周期内有没有除权除息跳空缺口。a 股经常出现 10 送 10、大比例分红导致的股价跳空,这种跳空会让价格序列产生一个永久性断点,ADF 检验即便通过了,残差图上看得见的结构突变也会在实盘交易中制造假信号,价差一旦跳到断点另一侧就再也回不来。这种问题看起来是统计显著性问题,实际上却是数据清洗的锅,会直接造成协整策略回测中的过度乐观和实盘净值难以复制。

比如 600776 的季K线图上如果存在明显的除权跳空,在回测时会表现为价差"必然"回归——因为除权日后价格一次性调整,价差自然修复——而这种"修复"没有任何套利价值。处理方案是在做协整检验之前,对前复权价格做断点检测,具体做法如下:

# 检测价格序列中的跳空断点(简单阈值法) def detect_gap(price, pct_threshold=0.15): """检查是否存在超过15%的隔日跳空""" ret = price.pct_change().dropna() gap_days = ret[abs(ret) > pct_threshold].index return gap_days gaps = detect_gap(df['600776']) if len(gaps) > 0: # 发现除权跳空,剔除跳空点后再做协整分析 print(f'检测到{gaps.tolist()}') df_clean = df.loc[~df.index.isin(gaps)]

处理除权跳空这个动作看起来微不足道,但它决定了回测结果能否外推。数据清洗干净后,资源里直接回归adf.docx差分后回归adf.docx中给出的统计结论才真正具备参考价值。统计工具本身没有错,错的是在不该用它的地方用了它——协整分析天生适合那些"长期均衡关系真实存在"的资产组合,比如同一板块内的同产业链股票,而不适合拿来进行随机组合的正态性挖掘。三只票如果分别处于完全无关的行业,即便 ADF 通过,市场逻辑也撑不起这个统计结果。

与其把精力放在调参,不如回顾一下两个图库文件的结论来对照自己的数据状况。df.csv那份跑出的协整结果如果在后来的数据里失效,不要先怀疑 python 代码写错了,优先检查是否经历了除权或退市风险警示,这两类事件都会导致长期关系结构性破裂。真正的协整不是永恒定律,而是需要定期重新估计的关系回归过程。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询