3. 价差序列构建:回归残差与标准化价差
好,咱们接着聊。上一章我们把配对选出来了,那接下来最关键的一步来了——怎么把两个价格序列拧成一根绳子,也就是构建价差序列。
我个人习惯把这一步叫做“信号提纯”。你想想看,两个股票的价格走势再相似,也不可能完全同步。我们要做的,就是找到它们之间那个稳定的“差值”,然后盯着这个差值做文章。
3.1 回归残差:最朴素的价差
最直接的方法,就是做线性回归。把股票A的价格当作自变量X,股票B的价格当作因变量Y。拟合出一条直线:
Y = α + β * X + ε
这里的ε,就是残差。说白了,就是实际价格和“预测价格”之间的差距。
我在项目中遇到过一个问题:直接用收盘价做回归,结果残差序列看起来挺平稳,但一跑回测就亏钱。后来发现,是因为没有考虑滞后效应。两个股票的价格反应速度不一样,有时候A涨了,B要过几分钟才跟上来。
代码实现其实很简单,用statsmodels或者sklearn都行。我个人偏爱statsmodels,因为它给出的统计指标更全:
import statsmodels.api as sm
import numpy as np
# 假设price_a和price_b是两个Series
X = sm.add_constant(price_a)
model = sm.OLS(price_b, X).fit()
residuals = model.resid
# 看一眼回归系数
print(model.params)
# 看一眼R方,判断拟合效果
print(f"R-squared: {model.rsquared:.4f}")
嗯,这里要注意:R方不是越高越好。有时候R方太高,反而说明两个股票走势太一致,套利空间反而小。我一般要求R方在0.6到0.9之间,太低了说明相关性不够,太高了说明没机会。
3.2 标准化价差:让信号可比较
回归残差有个问题——它的数值大小没有统一标准。有的配对残差波动在±5之间,有的在±50之间。你没法用一个固定的阈值去判断开仓平仓。
所以我们需要标准化。说白了,就是把残差序列减去均值,再除以标准差:
z_score = (residuals - mean(residuals)) / std(residuals)
这样得到的z-score,就变成了一个均值为0、标准差为1的序列。这时候,你就可以用±1.5、±2.0这样的通用阈值了。
我曾经犯过一个错误:用全量历史数据计算均值和标准差,然后直接用于实盘。结果市场结构一变,均值和标准差都漂移了,信号全乱套。后来我改用滚动窗口计算,比如用过去60天的数据来算当前的z-score。
def calculate_zscore(series, window=60):
rolling_mean = series.rolling(window=window).mean()
rolling_std = series.rolling(window=window).std()
zscore = (series - rolling_mean) / rolling_std
return zscore
# 对残差序列做滚动标准化
zscore_series = calculate_zscore(residuals, window=60)
3.3 两种方法的对比
说了这么多,咱们来总结一下。回归残差和标准化价差,到底选哪个?
| 维度 | 回归残差 | 标准化价差 |
|---|---|---|
| 计算复杂度 | 需要拟合回归模型 | 简单,只需均值和标准差 |
| 参数数量 | 需要估计α和β | 只需均值和标准差 |
| 信号可比性 | 不同配对不可直接比较 | 统一尺度,可直接比较 |
| 对市场变化的适应性 | 需要定期重估回归系数 | 滚动窗口可自适应 |
| 我的推荐场景 | 长期稳定配对,如ETF套利 | 中高频交易,如期货跨期套利 |
我个人在实际项目中,通常两种方法都会用。先用回归残差做初步筛选,找到那些残差序列看起来平稳的配对。然后切换到标准化价差,用滚动窗口的方式生成交易信号。
你可能会问:为什么不直接用标准化价差?因为标准化价差的前提是残差序列的均值和标准差是稳定的。如果残差本身就不平稳,那标准化出来的东西也是垃圾。所以先做回归,检验残差的平稳性,这一步不能省。
3.4 核心逻辑框架
下面这张图,是我自己总结的价差构建流程。每次做新配对,我都会按这个步骤走一遍:
这张图我建议你保存下来。每次做配对交易,就按这个流程走一遍。刚开始可能会觉得步骤多,但做熟了之后,你会发现每一步都有它的道理。
好了,价差序列构建这块就聊到这儿。记住一句话:残差是基础,标准化是工具,平稳性是前提。这三样都搞定了,你的信号才算真正能用。
公众号:蓝海资料掘金营,微信deep3321