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检验一个指标。我习惯搭配以下方法:
- 可视化检查:先画时序图,看有没有明显趋势或季节性
- ACF/PACF图:看自相关是否快速衰减
- ADF检验:定量判断
- 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 核心知识框架
我把平稳性检验的整个逻辑画成了图,方便你理解:
2.6 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ADF说平稳,但走势明显有趋势 | 滞后阶数选择不当 | 用AIC/BIC自动选阶,别用手动 |
| p值刚好在0.05附近 | 样本量不足或检验功效低 | 增加数据量,或结合KPSS检验 |
| 不同股票检验结果不一致 | 数据频率或时间窗口不同 | 统一数据频率,固定时间窗口 |
| 检验结果跟回测对不上 | 忽略了结构突变 | 用Zivot-Andrews检验检测突变点 |
我的习惯:做协整组合前,我会先对所有候选标的做ADF检验,只保留那些非平稳的。然后对它们的线性组合再做ADF检验,确认组合是平稳的。这一步做扎实了,后面回测才能放心。
平稳性检验看起来简单,但坑不少。我建议你每次跑完检验,都画个图看一眼,别光看p值。数据不会骗人,但统计量可能会。