4、平稳性检验:ADF检验原理、KPSS检验、如何解读检验结果、实战:对股票对进行平稳性检验
做统计套利的朋友,咱们今天聊个绕不开的话题——平稳性检验。
说实话,我刚开始做配对交易那会儿,踩过最大的坑就是没认真做平稳性检验。直接拿两个股票的价格序列去算回归、算残差,结果回测曲线漂亮得不行,一上实盘就崩了。后来才明白,很多所谓的"统计套利机会",其实就是两个非平稳序列的"伪回归"。
嗯,咱们今天就把这事彻底讲透。
4.1 为什么非要检验平稳性?
先问个问题:你拿两个随机游走的价格序列去做回归,会得到什么?
答案是——你会得到看起来非常显著的回归结果。t统计量很大,R²很高,但全是假的。这就是著名的"伪回归"问题。
我有个朋友,当年用茅台和五粮液做配对,没做平稳性检验,直接上模型。回测年化收益30%+,他差点就辞职了。后来我让他跑个ADF检验,结果两个序列都是非平稳的。说白了,那30%的收益就是运气,跟策略没关系。
所以,平稳性检验是统计套利的"安检门"。过不了这关,后面的一切都是空中楼阁。
4.2 ADF检验:最常用的平稳性检验
ADF检验,全称是Augmented Dickey-Fuller检验。名字挺长,但原理其实不复杂。
4.2.1 原理一句话说清楚
ADF检验的核心思想:检验时间序列是否存在单位根。
什么叫单位根?简单说,就是序列的当前值完全由上一期的值决定,加上一个随机扰动。比如:
Y_t = Y_{t-1} + ε_t
这种序列就是随机游走,它没有"均值回归"的特性。你今天涨了10%,明天可能继续涨,也可能跌,但不会因为今天涨了就必然回调。
ADF检验的原假设H₀:序列存在单位根(非平稳)。
备择假设H₁:序列不存在单位根(平稳)。
所以,p值越小,越拒绝原假设,序列越可能是平稳的。
4.2.2 代码实现
用Python做ADF检验,其实就几行代码。我习惯用statsmodels库:
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
# 模拟一个随机游走序列(非平稳)
np.random.seed(42)
n = 200
random_walk = np.cumsum(np.random.randn(n))
# 模拟一个白噪声序列(平稳)
white_noise = np.random.randn(n)
# ADF检验函数
def adf_test(series, name=''):
result = adfuller(series, autolag='AIC')
print(f'序列: {name}')
print(f'ADF统计量: {result[0]:.4f}')
print(f'p值: {result[1]:.4f}')
print(f'临界值:')
for key, value in result[4].items():
print(f' {key}: {value:.4f}')
print(f'结论: {"平稳" if result[1] < 0.05 else "非平稳"}')
print('-' * 40)
return result
# 检验
adf_test(random_walk, '随机游走')
adf_test(white_noise, '白噪声')
输出结果大概长这样:
序列: 随机游走
ADF统计量: -1.2345
p值: 0.6543
临界值:
1%: -3.4567
5%: -2.8765
10%: -2.5678
结论: 非平稳
序列: 白噪声
ADF统计量: -5.6789
p值: 0.0000
临界值:
1%: -3.4567
5%: -2.8765
10%: -2.5678
结论: 平稳
你看,随机游走的p值0.65,远大于0.05,不能拒绝原假设,所以是非平稳的。白噪声的p值接近0,拒绝原假设,是平稳的。
4.3 KPSS检验:ADF的"互补"检验
ADF检验有个问题——它的原假设是"非平稳"。这就意味着,如果数据不够多,或者信号不够强,我们可能"无法拒绝"原假设,但并不能证明序列就是平稳的。
说白了,ADF检验的"证据"是反着来的。你只能证明"非平稳",不能直接证明"平稳"。
这时候,KPSS检验就派上用场了。
4.3.1 KPSS检验的原理
KPSS检验的原假设正好相反:序列是平稳的。
所以,KPSS检验的p值如果很小(小于0.05),就说明序列是非平稳的。如果p值很大,说明没有证据拒绝"平稳"这个假设。
4.3.2 代码实现
from statsmodels.tsa.stattools import kpss
def kpss_test(series, name=''):
result = kpss(series, regression='c')
print(f'序列: {name}')
print(f'KPSS统计量: {result[0]:.4f}')
print(f'p值: {result[1]:.4f}')
print(f'临界值:')
for key, value in result[3].items():
print(f' {key}: {value:.4f}')
print(f'结论: {"非平稳" if result[1] < 0.05 else "平稳"}')
print('-' * 40)
return result
kpss_test(random_walk, '随机游走')
kpss_test(white_noise, '白噪声')
4.4 如何解读检验结果?
很多新手拿到检验结果就懵了。我给大家一个简单的判断矩阵:
| ADF结果 | KPSS结果 | 结论 | 怎么办? |
|---|---|---|---|
| 平稳 | 平稳 | 序列平稳 | 直接使用 |
| 非平稳 | 非平稳 | 序列非平稳 | 需要差分或变换 |
| 平稳 | 非平稳 | 可能存在趋势平稳 | 检查确定性趋势 |
| 非平稳 | 平稳 | 可能存在单位根但均值回归 | 进一步分析 |
4.5 实战:对股票对进行平稳性检验
好了,理论说完了,咱们来点实战。假设我们要检验"招商银行"和"兴业银行"的价差是否平稳。
4.5.1 获取数据
import yfinance as yf
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller, kpss
# 下载数据
tickers = ['600036.SS', '601166.SS'] # 招商银行、兴业银行
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')['Adj Close']
data.columns = ['CMB', 'CIB']
# 计算对数价格
log_prices = np.log(data)
# 计算价差(假设协整向量为[1, -1])
spread = log_prices['CMB'] - log_prices['CIB']
4.5.2 检验价差的平稳性
# ADF检验
adf_result = adfuller(spread.dropna(), autolag='AIC')
print('=== ADF检验结果 ===')
print(f'统计量: {adf_result[0]:.4f}')
print(f'p值: {adf_result[1]:.4f}')
print(f'结论: {"平稳" if adf_result[1] < 0.05 else "非平稳"}')
# KPSS检验
kpss_result = kpss(spread.dropna(), regression='c')
print('\n=== KPSS检验结果 ===')
print(f'统计量: {kpss_result[0]:.4f}')
print(f'p值: {kpss_result[1]:.4f}')
print(f'结论: {"非平稳" if kpss_result[1] < 0.05 else "平稳"}')
4.5.3 可视化价差
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.plot(spread.index, spread.values, label='价差', color='#2E86AB')
plt.axhline(y=0, color='red', linestyle='--', alpha=0.5)
plt.axhline(y=spread.mean(), color='green', linestyle='--', alpha=0.5, label='均值')
plt.title('招商银行 vs 兴业银行 价差序列')
plt.xlabel('日期')
plt.ylabel('价差')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
4.6 本章知识体系
下面这张图,是我自己总结的平稳性检验的完整流程,大家可以保存下来:
嗯,这张图把整个流程串起来了。从原始序列开始,到ADF和KPSS两个检验,再到综合判断。你想想看,是不是比单纯看文字清晰多了?