
简介面向股票量化分析与时间序列研究人群这份资源围绕协整性检验与多股票同步分析展开重点解决非平稳序列间长期均衡关系的识别与预测问题。压缩包共10个文件、约351KB内含Python协整分析脚本、3个CSV行情数据、4张ADF检验与差分回归结果图以及2份docx说明文档可对照完整流程一步步复现从原始数据到检验结论的分析过程。内容覆盖直接回归ADF、差分后回归ADF、周期共振与差值走势可视化等关键环节并以600776、002409、300473三只股票的真实数据作为演示样本脚本可直接运行图表与文档便于理解协整概念在股票配对交易、风险对冲中的实际价值。已有296人学习下载适合金融数据分析初学者、具备一定统计学基础的投资者巩固协整与时间序列分析技能。1. 相关系数 0.9 的两只股票照样能让你亏光做股票分析时很多人习惯先算相关性矩阵把相关系数最高的两只股票凑成一对做配对交易。相关系数 0.9 以上、回测曲线漂亮实盘却连续止损——问题往往不在参数而在把「相关性」当成了「协整性」。相关性描述两条序列同步波动的程度协整性描述它们之间是否存在长期稳定的线性均衡关系。两只股票相关性高价差照样越漂越远相关性一般价差却可能反复回归。做配对交易这类时间序列分析真正要检验的是协整性。下面按「协整 × 股票分析」这条主线讲清协整的定义与检验方法、用 Python 做协整性检验的可复现流程以及把检验结果变成交易信号时的参数与验证手段。适合量化股票分析师、写时间序列模型的工程师以及想搞懂配对交易底层逻辑的投资者。读完你能独立完成从数据获取、协整检验到价差交易的整套逻辑也能说清为什么它比只看相关性可靠。2. 协整的数学定义与检验原理从平稳性到两种主流检验2.1 平稳性与单整阶数I(0) 和 I(1) 是绕不过去的前置概念协整不是凭空定义出来的它建立在「非平稳」这个现象之上。一个时间序列弱平稳要求均值、方差恒定协方差只与时间间隔有关。股票价格明显不满足它长期漂移方差随价格水平放大。时间序列分析里最常用的单位根检验是 ADFAugmented Dickey-Fuller零假设是序列存在单位根、非平稳。p 值大于 0.05 时不能拒绝单位根序列就带随机游走成分。import numpy as np import pandas as pd from statsmodels.tsa.stattools import adfuller np.random.seed(42) steps np.random.normal(0, 1, 500) random_walk pd.Series(np.cumsum(steps)) stat_walk, p_walk, *_ adfuller(random_walk, autolagAIC) stat_diff, p_diff, *_ adfuller(random_walk.diff().dropna(), autolagAIC) print(f随机游走 p 值: {p_walk:.4f}) print(f一阶差分后 p 值: {p_diff:.4f})逻辑说明第一行用正态随机步长累计出一个随机游走这是股票价格最粗糙的模型对它做 ADFp 值通常大于 0.05说明存在单位根对一阶差分后的序列再检验p 值通常小于 0.01说明差分后平稳。一个序列经过 d 次差分才平稳就叫 d 阶单整记作 I(d)。股票价格基本是 I(1) 序列。autolagAIC让函数在自动选择的滞后阶范围内做检验比手写maxlag更稳尤其在样本量不同的股票之间对比时能保证口径一致。2.2 协整的定义线性组合平稳才是真正的协整性两个 I(1) 序列 x_t 和 y_t如果存在一个非零常数 β使得 y_t − β·x_t 是平稳序列I(0)就说这两个序列之间存在协整关系。直观理解x 和 y 各自随机游走但它们某种线性组合不会漂移太远始终被一个看不见的均衡力拉回。这个 β 在配对交易里就是 hedge ratio即对冲比例。协整与相关的核心区别可以看下面这个对比维度相关系数协整性度量对象两条序列同步波动的方向与强度两条序列线性组合的平稳性对非平稳的态度不区分非平稳序列也能算出高相关要求序列单整阶数一致且组合平稳经济含义同期联动长期均衡、误差修正伪回归风险高独立随机游走也能算出显著相关低检验设计上排除了伪回归常见误用是把两个独立的随机游走序列直接算相关性照样能得到显著的相关系数这就是经典的伪回归。协整检验从设计上先承认序列是非平稳的再检验它们的组合能否把非平稳成分消掉所以它回答的是「长期均衡是否存在」而不是「短期走势像不像」。2.3 Engle-Granger 两步法先回归、再检验残差实操中最常用的是 Engle-Granger 两步法。第一步用最小二乘回归 y_t α β·x_t ε_t估计出 hedge ratio第二步对残差序列 ε_t 做 ADF 检验残差平稳就说明 y_t − β·x_t 是 I(0)两者协整。这里有一个关键细节残差是从估计出的回归里得到的ADF 的临界值不能直接套用标准 Dickey-Fuller 表而要用 MacKinnon1991针对 Engle-Granger 两阶段估计调整过的临界值。statsmodels 的coint函数内部处理了这一修正所以不推荐只用adfuller对残差检验就下结论——那会把临界值放得太松多出一批假的协整对。另外 Engle-Granger 对回归方向敏感y 对 x 回归和 x 对 y 回归会得到不同的 beta 和不同的检验结论实际项目里应两个方向都算取更显著的结果。2.4 Johansen 检验超过两只股票时用向量方法Engle-Granger 只检验一个线性关系。当股票池超过两只、怀疑存在多个协整向量时要用 Johansen 检验。它基于向量误差修正模型VECM做极大似然估计输出 trace 统计量和最大特征值统计量用于判断协整关系的个数。from statsmodels.tsa.vector_ar.vecm import coint_johansen # prices 是 DataFrame列包含多只股票价格 jres coint_johansen(prices[[KO, PEP, MDLZ]], det_order0, k_ar_diff1) print(trace 统计量:, jres.trace_stat) print(90% / 95% / 99% 临界值:\n, jres.trace_stat_crit_vals)参数与判断逻辑det_order0表示模型含常数项k_ar_diff1是差分项的滞后阶即实际用到的总滞后是 2 阶。看 trace 统计量时从第一个值开始它检验「不存在协整关系」的零假设大于 95% 临界值就拒绝说明至少有一个协整向量再看第二个值检验「至多一个」以此类推直到不能拒绝为止。注意coint_johansen在 statsmodels 里属于低层接口输出字段随版本有变化生产环境要固定版本并对结果做单元测试。3. 用 Python 做股票协整性检验从数据准备到批量扫描3.1 数据准备价格对齐比想象中重要协整检验对数据质量极其敏感。两条序列的日期索引必须严格对齐停牌、退市、节假日不同步都会造成长度不一致直接影响残差和检验统计量。常见做法是取交集再用前向填充处理极个别缺失绝不能把缺失日期的价格凭空插出来。import yfinance as yf tickers [KO, PEP] raw yf.download(tickers, start2020-01-01, end2024-12-31, auto_adjustTrue)[Close] prices raw[tickers].dropna(howall).ffill().dropna() prices.columns tickers print(prices.shape, prices.index[0], prices.index[-1])参数说明auto_adjustTrue取复权后收盘价避免分红除权造成的价格跳空干扰协整检验dropna(howall)去掉所有股票都没有交易的日子ffill()只适合个别缺失缺失太多应该直接放弃这对股票。如果分析 A 股把 yfinance 换成 AKShare 或 Tushare拿到 DataFrame 后保持同样的结构即可。注意两只股票的数据长度差异大时协整检验的结论会失真尤其是其中一只上市时间短的情形。最少要求 250 个交易日以上的共同区间低于这个量级的检验结果只能当参考。3.2 手动实现 Engle-Granger每一步都知道在算什么先手写一遍能确认残差形态是否符合预期而不是把结论全部交给黑盒函数。from statsmodels.regression.linear_model import OLS from statsmodels.tools import add_constant x prices[PEP] y prices[KO] # 第一步回归 y a b*x估计对冲比例 beta X add_constant(x) model OLS(y, X).fit() beta model.params[PEP] alpha model.params[const] resid y - alpha - beta * x # 第二步对残差序列做 ADF 检验 adf_stat, p_value, usedlag, nobs, crit_vals, icbest adfuller(resid, autolagAIC) print(fhedge ratio: {beta:.4f}) print(f残差 ADF 统计量: {adf_stat:.4f}, p 值: {p_value:.4f})逻辑说明OLS 估计出的 beta 直接作为配对交易的 hedge ratio含义是每持有 1 股 KO需要 beta 股 PEP 来对冲残差就是对冲后的价差序列。第二步的 ADF 结果用来判断残差是否平稳但这个 p 值只能作参考正式结论以 3.3 节的coint为准因为它的临界值针对两阶段估计做了修正。建议顺手画残差图平稳残差围绕 0 波动、没有明显趋势若残差带线性趋势考虑在回归中加入时间趋势项。3.3 用 coint 函数一步到位参数与默认值statsmodels.tsa.stattools.coint封装了 Engle-Granger 检验并输出修正临界值是实际项目最常用的入口。from statsmodels.tsa.stattools import coint score, pvalue, crit coint(prices[KO], prices[PEP], trendc, methodaeg, maxlagNone, autolagaic) print(f协整 t 统计量: {score:.4f}) print(fp 值: {pvalue:.4f}) print(f1% / 5% / 10% 临界值: {crit})coint的关键参数参数默认值作用使用建议trendc回归中包含的确定性项通常 c常数价格有趋势时试 ctmethodaeg检验方法aeg 是 Engle-Grangerpo 是 Phillips-OuliarismaxlagNone残差 ADF 的最大滞后阶None 表示按样本量自动确定autolagaic滞后阶选择准则aic、bic或 None 固定用 maxlag判断规则p 值小于 0.05 认为 5% 显著性水平下存在协整关系。批量扫描时我一般要求 p 值小于 0.02 才纳入候选——同一批数据上做几十上百次假设检验5% 的显著性会放行一批假阳性。多因子模型里检验残差平稳性同理先收紧再放宽。3.4 批量扫描股票池找出值得深挖的候选对单个股票对的检验回答不了「哪些股票值得做」需要对股票池做全组合扫描。扫描的现实约束是多重检验问题所以在计算前先做基本面分组或行业过滤只在同组内跑组合能显著减少假阳性。from itertools import combinations def coint_pvalue(df, a, b): _, p, _ coint(df[a], df[b], trendc, autolagaic) return p universe [KO, PEP, MDLZ, GIS, K, CPB, SJM, HSY] results [] for a, b in combinations(universe, 2): try: p coint_pvalue(prices, a, b) results.append((a, b, round(p, 4))) except Exception as exc: print(f跳过 {a}-{b}: {exc}) pairs_df pd.DataFrame(results, columns[股票A, 股票B, p值]).sort_values(p值) print(pairs_df[pairs_df[p值] 0.05])逻辑说明combinations生成全部无重复的两两组合每对调用coint_pvalue拿 p 值异常处理把数据不足或序列退化导致的失败跳过而不是中断整轮扫描。扫描结果只是候选池不能直接进实盘——接下来还要看价差的均值回归特性即第 4 章的 z-score 信号和第 5 章的半衰期、滚动协整验证。4. 从协整检验到交易信号价差、z-score 与开平仓参数4.1 用 hedge ratio 构造价差滚动估计避免未来函数协整检验通过只是第一步接着要把 y_t − β·x_t 当作可交易的价差。这里的 beta 是第 3 章 OLS 回归估计出的 hedge ratio。需要强调一点协整关系存在不代表 beta 固定不变公司市值变化、股本结构变动都会让它漂移。常见做法是在固定窗口内滚动重估 beta机构通常按周或按月更新。def rolling_hedge_spread(df, a, b, window60): 滚动估计 hedge ratio返回价差 y - beta * x spread pd.Series(indexdf.index, dtypefloat) for i in range(window, len(df)): win df.iloc[i-window:i] # 只用截至第 i-1 天的数据 X add_constant(win[b]) beta OLS(win[a], X).fit().params[b] spread.iloc[i] df[a].iloc[i] - beta * df[b].iloc[i] return spread.dropna() spread rolling_hedge_spread(prices, KO, PEP, window60)逻辑说明第 i 天的价差使用前 60 个交易日估计出的 beta 计算窗口截断在 i 之前从设计上避免未来函数。逐行循环在 1000 个交易日、几十对股票的场景下完全够用如果股票池很大可以改用rolling.apply向量化但要注意窗口边界与列对齐。window 越大 beta 越稳对短期结构变化反应越慢window 越小 beta 更新越快但估计噪声越大。我一般从 60 天起步再结合第 5 章半衰期调整。4.2 z-score 标准化与开平仓阈值用价差绝对值设置交易阈值是新手常见错误。价差的均值和标准差会随估计窗口漂移必须标准化成 z-scorez 价差 − 均值/ 标准差均值和标准差在同一个滚动窗口内计算。lookback 60 spread_mean spread.rolling(lookback).mean() spread_std spread.rolling(lookback).std() zscore (spread - spread_mean) / spread_std entry_z 2.0 exit_z 0.5 position pd.Series(0, indexzscore.index) for i in range(1, len(zscore)): if zscore.iloc[i] entry_z and position.iloc[i-1] 0: position.iloc[i] -1 # 价差过高做空价差 elif zscore.iloc[i] -entry_z and position.iloc[i-1] 0: position.iloc[i] 1 # 价差过低做多价差 elif abs(zscore.iloc[i]) exit_z and position.iloc[i-1] ! 0: position.iloc[i] 0 # 回归均衡平仓 else: position.iloc[i] position.iloc[i-1]逻辑说明做空价差意味着卖出 KO、买入 beta 倍 PEP做多价差则相反。入场阈值 2.0 对应约 95% 分位的偏离出场阈值 0.5 表示回归到一半就离场而不是等 z 回到 0——后者会把已到手的利润回吐。position 逐日迭代任何一天的位置要么触发新信号、要么继承前一天天然避免了在信号阈值附近反复开平仓的抖动。提示如果同一交易日 z 值同时越过入场和出场阈值极端行情下可能发生处理规则应明确「优先执行已有持仓的出场」因为出场逻辑保护的是已有利润。4.3 参数表与止损三个最容易被忽略的细节参数常见取值影响调参方向lookback估计窗口30120 日窗口越短 beta 和均值更新越快噪声也越大从 60 起步用半衰期校准入场阈值 entry_z1.52.5越大交易越少、单笔越稳结合交易成本回测出场阈值 exit_z01.0越大平仓越早0.30.5 是常见甜点区止损35 倍入场 z防止协整失效时单边亏损必须设价差不回归就是失效第一个容易忽略的细节是交易成本。价差策略换手率高每次开平仓涉及两只股票的双边手续费、印花税和冲击成本回测里每股至少要扣 35 个基点否则实盘会和回测差一大截。第二个细节是价差不回归怎么办。协整是统计意义上的长期关系不保证 10 天之内回归必须设止损常见做法是 z 值达到入场阈值的 1.52 倍时强制平仓。第三个细节是信号频率。日线策略一天只在收盘后计算一次 z 值盘中实时计算会在阈值附近反复触发放大交易成本。如果必须盘中交易可以给信号加一个「确认期」例如连续 2 根 K 线收盘都越阈才算有效。5. 半衰期与滚动协整确认这套关系没有失效5.1 用半衰期判断均值回归速度协整检验通过后还差一个问题价差均值回归大概需要多少天如果回归周期是 200 天那 60 天的估计窗口和 10 天的持仓预期都不成立。半衰期的标准估计来自 Chan2013的 Ornstein-Uhlenbeck 近似把价差差分对滞后一期的价差做回归斜率 λ 衡量回归速度半衰期 −ln(2) / λ。def half_life(spread): spread spread.dropna() y spread.diff().dropna() x spread.shift(1).dropna() df_hl pd.concat([y, x], axis1).dropna() df_hl.columns [dy, y_lag] lam np.polyfit(df_hl[y_lag], df_hl[dy], 1)[0] return -np.log(2) / lam if lam 0 else np.inf print(f半衰期: {half_life(spread):.1f} 个交易日)逻辑说明lam是 y_lag 的系数必须为负才说明价差确实均值回归lam 为正时半衰期是无穷大意味着这个「协整」在可交易的时间尺度上不成立。经验规则是半衰期落在 360 个交易日之间才有配对交易价值太短说明价差几乎不偏离没有利润空间太长说明回归速度覆盖不了资金成本。lam 接近于 0 时计算不稳定此时应结合下面的滚动检验做判断。5.2 滚动协整检验与双层确认协整关系最大的杀手是结构性变化并购、行业周期反转、市场风格切换都会让原本的均衡关系消失。验证方法是在滚动窗口内反复做 coint 检验把 p 值序列画出来看趋势。def rolling_coint(df, a, b, window250): pvalues, idx [], [] for i in range(window, len(df) 1): win df.iloc[i-window:i] try: _, p, _ coint(win[a], win[b], trendc, autolagaic) pvalues.append(p) idx.append(df.index[i-1]) except Exception: continue return pd.Series(pvalues, indexidx) roll_p rolling_coint(prices, KO, PEP, window250) print(roll_p.tail(5))监控方法策略运行期内每天或每周追加一个窗口的数据重新检验一旦滚动 p 值持续高于 0.05就暂停该股票对的交易重新估计 beta 并复查基本面。更保守的做法是同时盯住半衰期半衰期骤升通常比 p 值突变更早出现是更敏感的预警。最后一个具体技巧把半衰期和滚动 p 值当作两个独立开关只有当前半衰期落在 360 个交易日区间内且最近 60 个交易日的滚动协整 p 值低于 0.05 时才允许对该股票对开新仓。单独看任何一个指标都有被有限样本欺骗的可能两个条件同时满足时这套协整关系才真正值得下单。本文还有配套的精品资源点击获取