4. 价差统计特征:分布、自相关与平稳性检验

好,咱们进入第四讲。前面我们聊了价差怎么算,怎么清洗。但拿到一堆价差数据后,你第一反应是什么?

我个人习惯,先摸清它的「脾气」。这数据是胖是瘦?有没有记忆性?会不会突然变了个样?

说白了,就是三个核心问题:分布、自相关、平稳性。这三个搞定了,你对价差的性格就了如指掌。

4.1 价差的分布特征

先看分布。价差长什么样?是正态分布吗?

我告诉你,绝大多数情况下不是。我在做美股做市商项目时,发现价差分布往往是右偏的——大部分时间价差很小,偶尔会突然拉大。你想想看,流动性好的时候,价差就一两个tick;遇到新闻事件,瞬间跳上去。

4.1.1 描述性统计

先来点基础的。均值、中位数、标准差、偏度、峰度。这些是基本功。

import numpy as np
import pandas as pd

def spread_statistics(spread_series):
    stats = {
        '均值': np.mean(spread_series),
        '中位数': np.median(spread_series),
        '标准差': np.std(spread_series),
        '最小值': np.min(spread_series),
        '最大值': np.max(spread_series),
        '偏度': sp_series.skew(),
        '峰度': sp_series.kurtosis()
    }
    return pd.Series(stats)

# 假设spread是清洗后的价差序列
print(spread_statistics(spread))
我的经验:偏度大于1,基本可以断定不是正态分布。峰度大于3,说明有厚尾——极端价差出现的概率比正态分布预测的要高。这对风控很重要。

4.1.2 分布可视化

光看数字不够,画个图更直观。我习惯用直方图叠加核密度估计。

import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))
sns.histplot(spread, bins=50, kde=True, color='steelblue')
plt.axvline(spread.mean(), color='red', linestyle='--', label='均值')
plt.axvline(spread.median(), color='green', linestyle='--', label='中位数')
plt.title('价差分布特征')
plt.legend()
plt.show()

嗯,这里要注意。如果均值明显大于中位数,说明右偏严重。我曾经遇到一个品种,均值比中位数大了将近一倍——后来发现是每天开盘前几分钟的价差异常大,把均值拉上去了。

4.2 自相关分析

分布看完了,接下来看自相关。说白了就是:现在的价差和过去的价差有没有关系?

如果有,那就有套利空间。如果没有,那就是随机游走,做市商就舒服了。

4.2.1 自相关函数(ACF)

我一般先看ACF图。横轴是滞后阶数,纵轴是相关系数。

from statsmodels.graphics.tsaplots import plot_acf

plt.figure(figsize=(12, 4))
plot_acf(spread, lags=40, alpha=0.05)
plt.title('价差自相关函数')
plt.show()
关键判断:如果ACF在滞后1阶后迅速衰减到0附近,说明是短记忆过程。如果衰减很慢,说明有长记忆性——这种情况做均值回归策略要小心。

我记得有一次,分析某个ETF的价差,ACF到第20阶还有显著的相关性。一开始我以为数据有问题,后来发现是因为那个ETF的做市商报价策略有固定的更新周期。嗯,这其实是个信号。

4.2.2 偏自相关函数(PACF)

ACF看的是总相关性,PACF看的是净相关性——排除中间滞后项的影响后,当前值和某个滞后值的直接关系。

from statsmodels.graphics.tsaplots import plot_pacf

plt.figure(figsize=(12, 4))
plot_pacf(spread, lags=40, alpha=0.05, method='ywm')
plt.title('价差偏自相关函数')
plt.show()

ACF和PACF结合起来,可以初步判断用AR模型还是MA模型。这个后面讲建模时会细说。

4.3 平稳性检验

这是最关键的。平稳性不满足,后面所有建模都是扯淡。

什么叫平稳?简单说:统计性质不随时间变化。均值恒定、方差恒定、自相关结构稳定。

4.3.1 为什么平稳性重要?

你想想看,如果价差的均值一直在漂移,你用什么均值回归策略?今天均值是0.5,明天变成1.0,你的交易信号全错。

我刚开始做量化时,就吃过这个亏。一个品种的价差看起来有回归特性,回测漂亮得很。结果实盘一个月,亏得我怀疑人生。后来一查,那段时间价差均值在缓慢上升,我的策略一直在逆势交易。

血的教训:不要只看回测曲线漂亮就上实盘。先做平稳性检验,这是底线。

4.3.2 ADF检验

最常用的就是Augmented Dickey-Fuller检验。原假设是「存在单位根,即非平稳」。

from statsmodels.tsa.stattools import adfuller

def adf_test(series, title=''):
    result = adfuller(series.dropna(), autolag='AIC')
    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('结论: 无法拒绝原假设,序列非平稳')

adf_test(spread, '价差序列')

p值小于0.05,说明平稳。大于0.05,说明有单位根。

4.3.3 KPSS检验

ADF检验有个问题——它的原假设是非平稳。有时候我们想反过来验证,用KPSS检验。原假设是「序列平稳」。

from statsmodels.tsa.stattests import kpss

def kpss_test(series, title=''):
    result = kpss(series.dropna(), regression='c')
    print(f'KPSS统计量: {result[0]:.4f}')
    print(f'p值: {result[1]:.4f}')
    
    if result[1] < 0.05:
        print('结论: 拒绝原假设,序列非平稳')
    else:
        print('结论: 无法拒绝原假设,序列平稳')

kpss_test(spread, '价差序列')
我的建议:两个检验都做。如果ADF说平稳,KPSS也说平稳,那基本稳了。如果ADF说非平稳,KPSS也说非平稳,那肯定有问题。如果结果矛盾——比如ADF说平稳,KPSS说非平稳——那可能是序列有趋势项或结构突变。

4.3.4 非平稳怎么办?

如果检验发现价差不平稳,别慌。常见处理方式:

  • 一阶差分:对价差取差分,通常能消除趋势
  • 去均值:减去滚动均值,处理时变均值
  • 分段处理:把数据分成不同市场状态,分别建模
# 一阶差分
spread_diff = spread.diff().dropna()
adf_test(spread_diff, '一阶差分后的价差')

我个人的经验是,大多数流动性好的品种,价差在日内是平稳的。但跨日数据往往不平稳——因为市场微观结构在变。所以做高频策略,用日内数据;做低频策略,要格外小心。

4.4 知识体系总览

来,我把这一章的核心逻辑画成一张图。你看一眼就明白了。

价差统计特征分析框架 价差序列 分布特征 自相关分析 平稳性检验 描述性统计(均值/偏度/峰度) 分布可视化(直方图+KDE) 厚尾/偏态判断 ACF(总相关性) PACF(净相关性) 记忆性判断(短/长记忆) ADF检验(原假设:非平稳) KPSS检验(原假设:平稳) 差分/去趋势处理 结论:为价差建模提供统计基础 决定使用ARIMA/GARCH/机器学习等模型的可行性

这张图把咱们这一章的内容串起来了。从价差序列出发,三个方向:分布特征告诉你数据长什么样,自相关告诉你数据有没有记忆,平稳性检验告诉你数据能不能用传统时间序列模型。

三者缺一不可。我见过太多人,只看分布不看平稳性,或者只看自相关不看分布。结果模型建出来,一跑就崩。

好了,这一章就到这里。把代码跑一遍,看看你手里的价差数据是什么性格。下一章咱们聊怎么用这些特征来建模预测。


专注资料整理