2. 平稳性检验:ADF检验原理与Python实现

做多资产协整组合,第一步就是看数据稳不稳。

说白了,如果价格序列像喝醉了酒一样乱晃,那协整关系根本无从谈起。我刚开始做量化那会儿,就吃过这个亏——拿了两只股票直接跑回归,结果R²高得吓人,一验证全是伪回归。嗯,后来才明白,平稳性检验是绕不开的坎。

2.1 什么是平稳性?

平稳性,简单讲就是统计性质不随时间变化。

具体来说,一个平稳序列的均值、方差是常数,自协方差只跟时间间隔有关,跟具体时间点无关。你想想看,如果一只股票今天的波动规律跟十年前完全不一样,那你怎么用历史数据去预测未来?

我习惯把平稳性分成两种:

  • 严平稳:所有统计性质都不变。太严格了,现实中几乎不存在。
  • 弱平稳:均值、方差恒定,自协方差只跟滞后阶数有关。我们平时说的平稳,基本都指这个。

核心要点:协整组合要求的是价格序列本身非平稳,但它们的线性组合是平稳的。所以我们要先确认单变量是否非平稳,再检验组合是否平稳。

2.2 ADF检验的原理

ADF检验,全称Augmented Dickey-Fuller检验。它是DF检验的升级版。

DF检验假设序列是AR(1)过程:

y_t = ρ * y_{t-1} + ε_t

如果ρ=1,就存在单位根,序列非平稳。但现实中的金融数据往往有高阶自相关,DF检验就不够用了。ADF检验在模型里加入了差分滞后项,解决了这个问题。

ADF检验的回归方程长这样:

Δy_t = α + β*t + γ*y_{t-1} + δ₁*Δy_{t-1} + ... + δₚ*Δy_{t-p} + ε_t

其中γ就是我们要检验的核心。原假设H₀:γ=0(存在单位根,非平稳)。备择假设H₁:γ<0(平稳)。

检验统计量跟标准t分布不一样,得查MacKinnon临界值表。如果统计量小于临界值,就拒绝原假设,认为序列平稳。

个人经验:我在项目中遇到过好几次,ADF检验结果说序列平稳,但看走势图明显有趋势。后来发现是滞后阶数选少了。我建议至少用AIC或BIC自动选阶,别用手动设。

2.3 Python实现ADF检验

Python里做ADF检验,最方便的是statsmodels库。一行代码就能搞定。

import numpy as np
import pandas as pd
import yfinance as yf
from statsmodels.tsa.stattools import adfuller

# 下载数据
tickers = ['AAPL', 'MSFT', 'GOOGL']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')['Adj Close']

# 对每只股票做ADF检验
for ticker in tickers:
    result = adfuller(data[ticker].dropna())
    print(f'{ticker}:')
    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()

输出结果解读:

  • ADF统计量:越负越好,说明越倾向于拒绝原假设
  • p值:小于0.05通常认为显著,拒绝原假设
  • 临界值:统计量小于临界值才拒绝原假设

避坑指南:我曾经遇到一个情况,p值0.049,刚好小于0.05,就判定平稳了。但回测结果一塌糊涂。后来发现是样本量太小,检验功效不足。我现在的习惯是:p值小于0.01才放心,0.05边缘的我会多跑几个检验交叉验证。

2.4 实战:完整检验流程

实际项目中,我不会只看ADF检验一个指标。我习惯搭配以下方法:

  1. 可视化检查:先画时序图,看有没有明显趋势或季节性
  2. ACF/PACF图:看自相关是否快速衰减
  3. ADF检验:定量判断
  4. KPSS检验:跟ADF互补,原假设是平稳

下面是一个完整的检验函数:

from statsmodels.tsa.stattools import kpss

def stationarity_test(series, name=''):
    """综合平稳性检验"""
    print(f'===== {name} =====')
    
    # ADF检验
    adf_result = adfuller(series.dropna(), autolag='AIC')
    print(f'ADF p值: {adf_result[1]:.4f}')
    adf_stationary = adf_result[1] < 0.05
    
    # KPSS检验
    kpss_result = kpss(series.dropna(), regression='c')
    print(f'KPSS统计量: {kpss_result[0]:.4f}')
    print(f'KPSS p值: {kpss_result[1]:.4f}')
    kpss_stationary = kpss_result[1] > 0.05
    
    # 综合判断
    if adf_stationary and kpss_stationary:
        print('→ 结论: 平稳')
    elif not adf_stationary and not kpss_stationary:
        print('→ 结论: 非平稳')
    elif adf_stationary and not kpss_stationary:
        print('→ 结论: 趋势平稳(需要去趋势)')
    else:
        print('→ 结论: 差分平稳(需要差分)')
    print()
    return adf_stationary, kpss_stationary

# 测试
for ticker in tickers:
    stationarity_test(data[ticker], ticker)

2.5 核心知识框架

我把平稳性检验的整个逻辑画成了图,方便你理解:

平稳性检验核心流程 原始价格序列 可视化检查 ADF检验(主检验) KPSS检验(互补检验) 综合判断结论 平稳 / 非平稳 / 趋势平稳 / 差分平稳

2.6 常见问题与避坑

问题 原因 解决方案
ADF说平稳,但走势明显有趋势 滞后阶数选择不当 用AIC/BIC自动选阶,别用手动
p值刚好在0.05附近 样本量不足或检验功效低 增加数据量,或结合KPSS检验
不同股票检验结果不一致 数据频率或时间窗口不同 统一数据频率,固定时间窗口
检验结果跟回测对不上 忽略了结构突变 用Zivot-Andrews检验检测突变点

我的习惯:做协整组合前,我会先对所有候选标的做ADF检验,只保留那些非平稳的。然后对它们的线性组合再做ADF检验,确认组合是平稳的。这一步做扎实了,后面回测才能放心。

平稳性检验看起来简单,但坑不少。我建议你每次跑完检验,都画个图看一眼,别光看p值。数据不会骗人,但统计量可能会。

蓝海数据掘金营,专注资料整理