4. 回归法策略:OLS回归估计对冲比率、残差序列的均值回复特性、Z-score信号生成
好,咱们进入配对交易的核心环节——回归法策略。
说实话,很多新手一上来就搞协整检验,觉得高大上。但我个人习惯,先跑一遍OLS回归,看看两个资产到底什么关系。这就像谈恋爱,先看看性格合不合,再谈能不能结婚。
4.1 OLS回归估计对冲比率
配对交易的本质,是找到两个价格走势高度相关的资产。它们之间的价差,就是我们的利润来源。
怎么量化这个关系?用OLS回归。
假设我们有两支股票,A和B。我们想用A去对冲B。那么回归方程就是:
B = α + β × A + ε
这里的β,就是我们要找的对冲比率。说白了,就是每持有1股A,需要持有多少股B,才能把系统性风险对冲掉。
核心要点:β不是随便算的。它代表的是两个资产之间的线性关系。如果β=1.5,意味着A涨1块钱,B平均涨1.5块钱。反过来,你空头1股A,就要多头1.5股B,才能保持中性。
代码实现其实很简单。我拿Python的statsmodels库举个例子:
import statsmodels.api as sm
import numpy as np
# 假设price_A和price_B是两列价格序列
X = sm.add_constant(price_A) # 加截距项
model = sm.OLS(price_B, X)
results = model.fit()
beta = results.params[1] # 这就是对冲比率
alpha = results.params[0] # 截距项
residuals = results.resid # 残差序列
print(f"对冲比率 β = {beta:.4f}")
print(f"截距 α = {alpha:.4f}")
嗯,这里要注意一点。我刚开始做的时候,直接拿原始价格去回归,结果发现β一直在变。后来才意识到,价格序列是非平稳的,回归结果不可靠。
避坑指南:我曾经用日频数据跑OLS,β算出来是1.2,觉得挺稳。结果换到周频数据,β变成了0.8。为什么?因为时间频率不同,两个资产的相关性结构会变。我建议你至少用3种频率验证一下,看看β是否稳定。
4.2 残差序列的均值回复特性
回归做完,我们得到了残差序列。这个残差,就是价差——两个资产之间没有被对冲掉的部分。
配对交易赚钱的逻辑,就是赌这个残差会回归到均值。说白了,残差高了就做空,残差了就做多。
但问题来了:残差真的会均值回复吗?
不一定。有些配对,残差跑着跑着就飞了,再也不回来。这就是所谓的「伪配对」。
怎么判断?我一般看两个指标:
- 自相关系数(ACF):如果残差的一阶自相关系数接近0,说明它像个白噪声,没有记忆性,很难回复。如果一阶自相关系数在0.5以上,说明有较强的回复倾向。
- 半衰期(Half-life):残差偏离均值后,回到均值一半所需的时间。半衰期越短,回复越快,交易机会越多。
我举个例子。假设残差序列的一阶自相关系数是0.7,半衰期是5天。这意味着,如果今天残差偏离均值1个标准差,大约5天后它会回到0.5个标准差的位置。这个速度,做短线交易是够的。
个人经验:我一般要求半衰期在3-10天之间。太短了(比如1天),交易成本太高,赚不到钱。太长了(比如30天),资金占用太久,性价比低。
计算半衰期的代码:
import numpy as np
from statsmodels.tsa.stattools import adfuller
def half_life(residuals):
# 对残差做ADF检验,得到滞后项的系数
lag = 1
X = residuals[:-lag]
y = residuals[lag:]
X = sm.add_constant(X)
model = sm.OLS(y, X)
results = model.fit()
rho = results.params[1] # 滞后项系数
# 半衰期 = ln(0.5) / ln(rho)
hl = np.log(0.5) / np.log(rho)
return abs(hl)
hl = half_life(residuals)
print(f"半衰期 = {hl:.2f} 天")
4.3 Z-score信号生成
好,现在我们知道残差有均值回复特性了。下一步,就是怎么生成交易信号。
最常用的方法,就是Z-score。
Z-score的计算公式很简单:
Z = (当前残差 - 残差均值) / 残差标准差
Z-score告诉我们,当前价差偏离均值多少个标准差。如果Z=2,说明价差比均值高了2个标准差,大概率会跌回来。这时候就该做空价差。
具体怎么用?我一般设两个阈值:
| Z-score范围 | 信号 | 操作 |
|---|---|---|
| Z > 2.0 | 做空信号 | 空头价差(卖B,买A) |
| Z < -2.0 | 做多信号 | 多头价差(买B,卖A) |
| |Z| < 0.5 | 平仓信号 | 平掉所有头寸 |
| |Z| > 3.0 | 止损信号 | 强制平仓,防止价差不回归 |
你想想看,为什么设2.0和-2.0?因为正态分布下,Z-score超过2的概率只有5%左右。也就是说,95%的情况下价差会在±2个标准差以内波动。超过这个范围,就是小概率事件,大概率会回归。
代码实现:
def generate_signals(residuals, entry_threshold=2.0, exit_threshold=0.5):
# 计算Z-score
mean = np.mean(residuals)
std = np.std(residuals)
z_scores = (residuals - mean) / std
# 生成信号
signals = pd.Series(index=residuals.index, data=0)
# 做空信号
signals[z_scores > entry_threshold] = -1
# 做多信号
signals[z_scores < -entry_threshold] = 1
# 平仓信号
signals[abs(z_scores) < exit_threshold] = 0
# 注意:这里需要处理持仓状态,不能频繁开平仓
# 实际交易中,需要加入持仓状态机逻辑
return signals, z_scores
我曾经踩过的坑:一开始我直接用Z-score的实时值开仓,结果一天之内信号来回变,手续费亏死。后来我加了持仓状态机——只有当前没有持仓时,才根据Z-score开仓。持仓期间,Z-score再高也不开新仓。这样交易次数降下来了,胜率反而上去了。
4.4 知识体系总览
说了这么多,我画个图帮你理清思路。整个回归法策略的流程,其实就三步:
这个图把整个流程串起来了。你从左边开始,先做OLS回归,算出β。然后看残差有没有均值回复特性。最后用Z-score生成信号。每一步都有对应的细节,缺一不可。
我的建议:刚开始做的时候,别急着上实盘。先用历史数据回测,看看Z-score的阈值设多少最合适。我一般用2.0作为默认值,但不同配对、不同市场环境,最优阈值可能不一样。多试几次,找到最适合你策略的参数。
好了,回归法策略的核心内容就这些。说白了,就是三步走:回归算β,残差看回复,Z-score定买卖。每一步都有坑,但踩过去之后,你会发现配对交易其实没那么神秘。