配对交易 · 协整
:::info 学习目标 你将能区别相关与协整,使用滚动历史估计对冲比率,构造进出场状态机,并计入双腿成本、借券和结构断裂风险。 :::
直觉
配对交易是统计套利的经典:找两只「长期绑在一起」的股票(比如同一行业的龙头),它们的价差虽然时大时小,但总会回归均值。价差拉得太开就「做空强的、做多弱的」,赌它收敛。关键前提是两只股票协整(cointegrated)——各自乱走,但差值平稳。
协整与价差
两只股票价格 各自是非平稳的,但若存在 使
平稳(均值与方差恒定),则称二者协整, 是对冲比率。
「人话」解释:协整 vs 相关,区别在哪?
相关是「同涨同跌」——两个都涨的序列相关性可能很高。 协整更强:要求「差值」稳定在某个水平附近,不会越走越远。 两条同时上涨的线可能高度相关、但差值发散(不协整);只有差值「像橡皮筋拉住」的,才能做配对交易——因为你可以确信「价差终会回来」。
交易规则(z-score 进出场)
用价差的滚动 z-score:
- → 价差过低,做多价差(多 、空 );
- → 价差过高,做空价差;
- → 回归,平仓。
可运行案例:AAPL / MSFT 配对(合成数据)
读取 aapl_msft_daily.csv,用过去 252 日滚动 OLS 估计 ,构造价差与 z-score,按规则交易。滚动估计确保早期交易不会偷看后面的样本;价差组合收益还要扣除双腿换手成本。
:::caution 合成数据 本数据刻意协整(真值 ,价差 ADF )。你估计出的 会偏离真值——这正是「估计误差」的教学点,并非套利 bug。 :::
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('/data/aapl_msft_daily.csv', parse_dates=['date']).set_index('date')
pa, pm = df['aapl_close'], df['msft_close']
la, lm = np.log(pa), np.log(pm)
# 仅用过去 252 日做滚动 OLS: la ~ a + b*lm
window = 252
b = la.rolling(window).cov(lm) / lm.rolling(window).var()
a = la.rolling(window).mean() - b * lm.rolling(window).mean()
spread = la - (a + b*lm)
print(f"滚动 β 中位数 = {b.median():.3f} (合成数据真值 ≈ 1.30)")
z = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()
raw = pd.Series(np.nan, index=pa.index)
raw[z < -2.0] = 1.0
raw[z > 2.0] = -1.0
raw[z.abs() < 0.5] = 0.0
signal = raw.ffill().fillna(0.0)
# 昨日估计的 β 决定今日对冲;双腿换手都要付成本
ra = np.log(pa / pa.shift(1)); rm = np.log(pm / pm.shift(1))
spread_ret = (ra - b.shift(1)*rm).fillna(0.0)
position = signal.shift(1).fillna(0.0) # 防前视(同引擎约定)
leg_y = position
leg_x = (-position * b.shift(1)).fillna(0.0)
turnover = leg_y.diff().abs().fillna(0) + leg_x.diff().abs().fillna(0)
strat_ret = (position * spread_ret - turnover * 2/1e4).fillna(0.0)
equity = (1 + strat_ret).cumprod()
print(quant.performance_summary(equity, strat_ret, freq=252).round(3))
fig, ax = plt.subplots(2, 1, figsize=(9, 5), sharex=True,
gridspec_kw={'height_ratios':[2,1]})
ax[0].plot(z, color='#2563eb', lw=0.9); ax[0].axhline(2, color='r', ls='--', lw=0.8)
ax[0].axhline(-2, color='g', ls='--', lw=0.8); ax[0].axhline(0, color='gray', lw=0.8)
ax[0].set_title('价差 z-score(红=做空价差, 绿=做多价差)')
ax[1].plot(equity, color='crimson'); ax[1].set_title('配对策略资金曲线')
plt.tight_layout(); plt.show()
动手改一改:拖动阈值即时看回测
拖动进场阈值 与平仓阈值——阈值越小交易越频繁、成本越敏感;越大越挑剔、信号越少。
# ParamSlider(SSR 预览)
参数: ENTRY, EXIT
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('/data/aapl_msft_daily.csv', parse_dates=['date']).set_index('date')
pa, pm = df['aapl_close'], df['msft_close']
la, lm = np.log(pa), np.log(pm)
window = 252
b = la.rolling(window).cov(lm) / lm.rolling(window).var()
a = la.rolling(window).mean() - b * lm.rolling(window).mean()
spread = la - (a + b * lm)
z = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()
raw = pd.Series(np.nan, index=pa.index)
raw[z < -ENTRY] = 1.0
raw[z > ENTRY] = -1.0
raw[z.abs() < EXIT] = 0.0
signal = raw.ffill().fillna(0.0)
ra = np.log(pa / pa.shift(1)); rm = np.log(pm / pm.shift(1))
spread_ret = (ra - b.shift(1) * rm).fillna(0.0)
position = signal.shift(1).fillna(0.0)
leg_y = position
leg_x = (-position * b.shift(1)).fillna(0.0)
turnover = leg_y.diff().abs().fillna(0) + leg_x.diff().abs().fillna(0)
strat = (position * spread_ret - turnover * 2/1e4).fillna(0.0)
eq = (1 + strat).cumprod()
print(quant.performance_summary(eq, strat, freq=252).round(3))
plt.figure(figsize=(9, 3.4))
plt.plot(eq, color='crimson')
plt.title(f'配对策略 进场 |z|>{ENTRY} 平仓 |z|<{EXIT}'); plt.ylabel('净值')
plt.tight_layout(); plt.show()
进一步:把滚动窗口从
252改成126或504。短窗口适应快但估计噪声大,长窗口稳定却可能跟不上结构变化;比较的不只是收益,还应看 的稳定性和换手。
失效机制与研究检查
协整是统计关系,不是经济保证。公司基本面、指数纳入、并购或商业模式变化都可能让关系永久断裂。若先扫描大量配对再挑 p 值最小者,会有严重数据窥探;应保留发现期/验证期,并监控滚动 ADF、 和半衰期。仓位还应按两腿名义价值或风险统一归一化。
实践与延伸
- 加入滚动协整失效阈值,比较“继续持有”和“立即停机”。
- 分别按股数、美元和波动率中性构造两腿权重。
延伸阅读:Engle–Granger;Gatev、Goetzmann 与 Rouwenhorst 的配对交易研究。
小结
- 配对交易赌的是协整价差的均值回归;
- 应使用当时可得历史滚动估计,价差 ,z-score 决定进出场;
- 多空两条腿都产生换手、价差和借券成本,不能只看无成本价差收益;
- 协整 相关——只有差值平稳的「橡皮筋」关系才能套利。
章末自测
1. 关于本章主题,哪项说法更准确?