3、价差序列构建:如何计算配对价差、标准化处理(Z-score)、价差的统计特征
好,咱们接着聊。上一章我们把配对选好了,股票对也敲定了。那接下来干嘛?
说白了,就是要把这对股票的“价差”给算出来。
你想想看,配对交易的核心逻辑是什么?就是赌它们的价差会回归均值。所以,价差序列就是我们的交易信号源。如果价差算错了,后面所有策略都是白搭。
我个人习惯把这一步叫做“数据精加工”。原始价格数据就像刚挖出来的矿石,价差序列才是提炼后的黄金。
3.1 配对价差的计算方法
价差怎么算?最直接的想法就是:A股票价格减去B股票价格。但这里有个坑——两只股票的价格量级可能差很多。
举个例子,茅台一股2000块,五粮液一股200块。直接相减,价差波动几乎全被茅台带着走。这就不对了。
所以,我们通常用两种方式来计算价差:
- 价格差(Price Spread):
spread = P_A - β * P_B - 对数价差(Log Spread):
spread = ln(P_A) - β * ln(P_B)
这里的β,就是我们在上一章用协整回归算出来的那个系数。它解决了量级不匹配的问题。
核心要点:
β 的作用是“对齐”两只股票的价格尺度。没有β,价差序列就是扭曲的。
我在项目中遇到过一种情况:有人直接用价格差,没加β。结果回测曲线漂亮得不行,实盘一跑就崩。为什么?因为β会随时间漂移,你不加它,相当于无视了股票之间的真实比例关系。
下面这段代码,是我常用的价差计算方式:
import numpy as np
import pandas as pd
import statsmodels.api as sm
def calculate_spread(price_A, price_B, method='log'):
"""
计算配对价差
:param price_A: 股票A的价格序列
:param price_B: 股票B的价格序列
:param method: 'log' 或 'price'
:return: spread 序列, beta 系数
"""
if method == 'log':
# 对数价差,更稳定
log_A = np.log(price_A)
log_B = np.log(price_B)
# OLS回归求beta
X = sm.add_constant(log_B)
model = sm.OLS(log_A, X).fit()
beta = model.params.iloc[1] # 注意pandas版本差异
spread = log_A - beta * log_B
else:
# 价格差,适合量级相近的股票
X = sm.add_constant(price_B)
model = sm.OLS(price_A, X).fit()
beta = model.params.iloc[1]
spread = price_A - beta * price_B
return spread, beta
嗯,这里要注意:我习惯用对数价差。为什么?因为对数化之后,价差序列更接近正态分布,统计性质更好。你想想看,金融数据大多是指数增长的,取个对数就把趋势变成了线性,处理起来顺手很多。
3.2 标准化处理:Z-score 的妙用
价差算出来了,但问题又来了——这个价差的数值本身没有意义。它可能是0.5,也可能是50,取决于股票的价格水平。
我们需要一个统一的标尺,来判断当前价差是“偏高”还是“偏低”。
这时候,Z-score 就登场了。
Z-score 公式:
Z = (当前价差 - 价差均值) / 价差标准差
Z-score 告诉我们:当前价差偏离均值多少个标准差。通常来说:
- |Z| > 2:价差严重偏离,可以考虑开仓
- |Z| < 0.5:价差在均值附近,考虑平仓
- |Z| > 3:极端情况,小心反转或趋势改变
我的经验:
Z-score 的窗口期选择很关键。我一般用60个交易日作为滚动窗口。太短了,均值不稳定;太长了,反应迟钝。你可以根据股票的交易频率调整,但60天是个不错的起点。
我曾经犯过一个错误:用全量历史数据算Z-score。结果呢?五年前的数据把均值拉偏了,导致开仓信号迟迟不出来。后来改成滚动窗口,信号就灵敏多了。
代码实现也很简单:
def calculate_zscore(spread, window=60):
"""
计算滚动Z-score
:param spread: 价差序列
:param window: 滚动窗口大小
:return: Z-score 序列
"""
rolling_mean = spread.rolling(window=window).mean()
rolling_std = spread.rolling(window=window).std()
zscore = (spread - rolling_mean) / rolling_std
return zscore
你看,就这几行代码。但背后的逻辑,值得你反复琢磨。
3.3 价差的统计特征分析
价差序列建好了,Z-score也算出来了。但别急着跑策略。我们得先看看这个价差到底“长什么样”。
我一般会从四个维度去分析:
| 统计指标 | 含义 | 理想值 |
|---|---|---|
| 均值 | 价差的长期中心位置 | 接近0(标准化后) |
| 标准差 | 价差的波动幅度 | 越小越好,但需适中 |
| 偏度 | 分布是否对称 | 接近0 |
| 峰度 | 尾部厚度 | 接近3(正态分布) |
| 自相关性 | 价差是否依赖过去值 | 低自相关,最好白噪声 |
说白了,我们想要的是一个“稳定、对称、低自相关”的价差序列。如果偏度很大,说明价差经常往一个方向偏,那配对可能有问题。如果自相关性高,说明价差有趋势,不适合做均值回归策略。
避坑指南:
我曾经遇到一个价差序列,看起来完美——均值0,标准差稳定。但自相关系数高达0.8。这意味着什么?价差一旦偏离,就会持续偏离,根本不会回归。这种序列做配对交易,必亏。
所以,一定要检查自相关函数(ACF)。如果前几阶自相关显著不为0,这个配对就要重新考虑了。
下面是我常用的统计特征分析代码:
def spread_statistics(spread):
"""
计算价差的统计特征
"""
from scipy import stats
stats_dict = {
'均值': np.mean(spread),
'标准差': np.std(spread),
'偏度': stats.skew(spread.dropna()),
'峰度': stats.kurtosis(spread.dropna()),
'JB检验p值': stats.jarque_bera(spread.dropna())[1],
'ADF检验p值': sm.tsa.stattools.adfuller(spread.dropna())[1]
}
# 自相关检验(前5阶)
acf_values = sm.tsa.stattools.acf(spread.dropna(), nlags=5)
for i, val in enumerate(acf_values[1:], 1):
stats_dict[f'自相关_{i}阶'] = val
return stats_dict
嗯,这里我加了一个JB检验和ADF检验。JB检验看正态性,ADF检验看平稳性。这两个通过了,价差序列才算合格。
3.4 知识体系总览
说了这么多,我画了一张图,帮你把整个流程串起来:
这张图把整个流程分成了三步:计算价差、标准化、分析统计特征。每一步都有对应的检查点。你照着这个流程走,基本不会出大问题。
最后说一句:价差序列是配对交易的灵魂。花再多时间在这上面都不为过。我见过太多人,选了对就急着下单,结果价差本身就有问题,亏了钱还找不到原因。
先把价差搞明白,再谈策略。这是铁律。
公众号:蓝海资料掘金营,微信deep3321