2. 平稳性与单位根检验:为什么均值回归策略只对平稳时间序列有效?ADF检验与KPSS检验的实战应用

做均值回归策略,第一个绕不开的问题就是:这个品种到底能不能回归?

我刚开始做量化那会儿,踩过一个坑。当时看到某只股票价格跌到历史低位,觉得「跌多了肯定涨回来」,直接重仓杀入。结果呢?它一路跌到退市。后来复盘才发现,那根本不是均值回归,而是趋势反转——或者说,它根本就不是一个平稳序列。

所以今天咱们就来聊聊,为什么平稳性是均值回归策略的命根子,以及怎么用ADF检验和KPSS检验来给数据「体检」。

2.1 什么是平稳时间序列?

先给个直观理解。平稳时间序列,说白了就是:统计性质不随时间变化

具体来说,一个序列如果满足以下三个条件,就称为弱平稳(也叫宽平稳):

  • 均值恒定:序列的期望值不随时间变化,比如始终围绕0上下波动
  • 方差恒定:波动幅度不随时间变化,不会出现「前期平稳、后期剧烈震荡」
  • 协方差只与时间间隔有关:两个时间点的相关性只取决于它们相隔多远,与具体时刻无关

举个例子:白噪声序列就是最完美的平稳序列。它均值是0,方差恒定,任意两个时间点都不相关。

那非平稳序列长什么样?

  • 趋势性:比如股价长期上涨,均值一直在变
  • 季节性:比如气温数据,冬天低夏天高,均值周期性变化
  • 随机游走:比如大多数股票价格,今天的价格 = 昨天的价格 + 随机扰动

嗯,这里要注意:随机游走是非平稳的典型代表。而均值回归策略,恰恰要求序列是平稳的——因为只有平稳序列,价格才会围绕某个均值来回摆动。

核心结论:均值回归策略的本质,是赌价格偏离均值后会「拉回来」。如果序列不平稳,均值本身都在漂移,你赌什么回归?

2.2 为什么均值回归策略只对平稳序列有效?

我习惯用一个比喻来解释这个问题:

想象你手里牵着一只狗,狗绳长度固定。狗会跑来跑去,但始终被你牵着——这就是平稳序列,狗(价格)围绕你(均值)来回摆动。你可以在狗跑远时拉回来(做空),在狗跑近时放出去(做多)。

但如果狗绳断了呢?狗越跑越远,你再也拉不回来——这就是非平稳序列。你之前以为的「回归」,其实只是趋势中的一次回调。

从数学角度看,均值回归策略通常基于以下假设:

  • 价格序列 X_t 是平稳的,存在长期均值 μ
  • X_t 偏离 μ 时,有向 μ 回复的倾向
  • 偏离程度越大,回复力度越强

如果序列非平稳,比如 X_t 是随机游走:

X_t = X_{t-1} + ε_t

那么任何一次「偏离」都可能是永久性的。你基于历史均值做的交易决策,很可能就是错的。

我曾经在回测某商品期货的均值回归策略时,发现夏普比率高达3.0,兴奋得不行。结果实盘一个月就亏了15%。后来一查,原来回测期间该品种处于震荡市(平稳),而实盘时进入了趋势行情(非平稳)。教训就是:回测时一定要检验平稳性,而且要用滚动窗口检验

2.3 单位根检验:判断平稳性的核心工具

怎么判断一个序列是否平稳?最常用的方法就是单位根检验

先简单说说单位根是什么。考虑一个一阶自回归模型:

X_t = ρ * X_{t-1} + ε_t

如果 |ρ| < 1,序列是平稳的(冲击会衰减)。如果 ρ = 1,就是随机游走(冲击永久存在)。这个 ρ = 1 的情况,就叫「存在单位根」。

所以单位根检验,本质上就是检验 ρ 是否等于1。

常用的两种检验方法:

检验方法 原假设 H0 备择假设 H1 适用场景
ADF检验 存在单位根(非平稳) 不存在单位根(平稳) 检测趋势平稳或差分平稳
KPSS检验 不存在单位根(平稳) 存在单位根(非平稳) 检测趋势平稳或水平平稳

你发现没有?这两个检验的假设正好相反。所以我的习惯是:两个检验一起做,交叉验证

我个人习惯:先用ADF检验,如果p值小于0.05,拒绝「非平稳」的原假设,初步认为序列平稳。然后再用KPSS检验,如果p值大于0.05,不能拒绝「平稳」的原假设,那就更放心了。如果两个检验结果矛盾,我会进一步检查数据是否有结构性突变。

2.4 ADF检验实战应用

ADF(Augmented Dickey-Fuller)检验是最常用的单位根检验。它扩展了DF检验,加入了滞后项来处理自相关。

直接上代码:

import numpy as np
import pandas as pd
import yfinance as yf
from statsmodels.tsa.stattools import adfuller, kpss
import matplotlib.pyplot as plt

# 下载数据
ticker = 'SPY'
data = yf.download(ticker, start='2020-01-01', end='2023-12-31')
price = data['Close']

# ADF检验
def adf_test(series, title=''):
    result = adfuller(series.dropna(), autolag='AIC')
    print(f'=== ADF检验: {title} ===')
    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}')
    if result[1] < 0.05:
        print('结论: 拒绝原假设,序列平稳')
    else:
        print('结论: 不能拒绝原假设,序列非平稳')
    return result

# 对价格序列做ADF检验
adf_test(price, 'SPY收盘价')

# 对收益率序列做ADF检验
returns = price.pct_change().dropna()
adf_test(returns, 'SPY日收益率')

运行结果通常是这样:

  • 价格序列:p值很大(比如0.5),不能拒绝非平稳——说明价格本身不平稳
  • 收益率序列:p值很小(比如0.0001),拒绝非平稳——说明收益率是平稳的

这解释了为什么很多均值回归策略用收益率或价差,而不是直接用价格。

2.5 KPSS检验实战应用

KPSS(Kwiatkowski-Phillips-Schmidt-Shin)检验是ADF的互补工具。它的原假设是「序列平稳」。

def kpss_test(series, title=''):
    result = kpss(series.dropna(), regression='c', nlags='auto')
    print(f'\n=== KPSS检验: {title} ===')
    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}')
    if result[1] < 0.05:
        print('结论: 拒绝原假设,序列非平稳')
    else:
        print('结论: 不能拒绝原假设,序列平稳')
    return result

# 对价格序列做KPSS检验
kpss_test(price, 'SPY收盘价')

# 对收益率序列做KPSS检验
kpss_test(returns, 'SPY日收益率')

结果通常:

  • 价格序列:p值很小,拒绝平稳——与ADF结论一致
  • 收益率序列:p值很大,不能拒绝平稳——也与ADF结论一致

实战建议:我一般会写一个组合函数,同时输出ADF和KPSS的结果,并给出综合判断。如果两者结论一致,那就放心用。如果不一致,我会检查数据是否有趋势项或季节性,或者考虑用差分后的数据。

2.6 组合检验:让结论更可靠

单独用ADF或KPSS都可能误判。比如:

  • ADF对趋势平稳的序列可能误判为非平稳
  • KPSS对接近单位根的序列可能误判为平稳

所以我的标准流程是:

def stationarity_test(series, title=''):
    """组合检验:ADF + KPSS"""
    print(f'\n{"="*50}')
    print(f'平稳性检验: {title}')
    print(f'{"="*50}')
    
    # ADF检验
    adf_result = adfuller(series.dropna(), autolag='AIC')
    adf_pvalue = adf_result[1]
    
    # KPSS检验
    kpss_result = kpss(series.dropna(), regression='c', nlags='auto')
    kpss_pvalue = kpss_result[1]
    
    # 综合判断
    if adf_pvalue < 0.05 and kpss_pvalue > 0.05:
        conclusion = '✅ 序列平稳(ADF和KPSS一致)'
    elif adf_pvalue < 0.05 and kpss_pvalue < 0.05:
        conclusion = '⚠️ 可能存在趋势平稳(ADF说平稳,KPSS说非平稳)'
    elif adf_pvalue > 0.05 and kpss_pvalue > 0.05:
        conclusion = '⚠️ 数据量不足或存在结构性突变'
    else:
        conclusion = '❌ 序列非平稳(ADF和KPSS一致)'
    
    print(f'ADF p值: {adf_pvalue:.4f}')
    print(f'KPSS p值: {kpss_pvalue:.4f}')
    print(f'综合结论: {conclusion}')
    
    return {
        'adf_pvalue': adf_pvalue,
        'kpss_pvalue': kpss_pvalue,
        'conclusion': conclusion
    }

# 使用示例
result = stationarity_test(price, 'SPY收盘价')
result = stationarity_test(returns, 'SPY日收益率')

2.7 知识体系图:平稳性检验的核心逻辑

下面这张图,是我自己梳理的平稳性检验决策流程,分享给你:

平稳性检验决策流程图 原始时间序列 ADF检验 + KPSS检验 结论一致? 序列平稳 / 非平稳 检查趋势项 或结构性突变 建议:两个检验同时使用,交叉验证结论 ADF原假设:非平稳 | KPSS原假设:平稳

2.8 实战中的避坑指南

最后分享几个我踩过的坑:

  • 坑一:只看ADF不看KPSS。有一次ADF说序列平稳,我直接用了,结果策略回测很好、实盘很差。后来用KPSS一查,发现序列其实是趋势平稳,均值在缓慢漂移。
  • 坑二:不检查滞后阶数。ADF检验的滞后阶数选择很关键。用AIC自动选择通常没问题,但如果你发现结果不稳定,可以手动试试不同阶数。
  • 坑三:忽略结构性突变。如果序列在某个时间点发生了结构性变化(比如政策变化、公司重组),那么整个序列可能分段平稳。这时候要用Zivot-Andrews检验或分段检验。
  • 坑四:样本量太小。ADF和KPSS对样本量敏感,一般建议至少100个数据点。少于50个时,检验结果基本不可信。

我的经验:在实际项目中,我通常会用滚动窗口做平稳性检验。比如每60个交易日检验一次,看看序列的平稳性是否随时间变化。如果发现平稳性不稳定,那这个品种就不适合做均值回归。

好了,关于平稳性和单位根检验,今天就聊到这儿。记住一句话:没有平稳性,就没有均值回归。下次做策略之前,先给你的数据做个「体检」吧。

专注资料整理