跳到主要内容

配对交易 · 协整

直觉

配对交易是统计套利的经典:找两只「长期绑在一起」的股票(比如同一行业的龙头),它们的价差虽然时大时小,但总会回归均值。价差拉得太开就「做空强的、做多弱的」,赌它收敛。关键前提是两只股票协整(cointegrated)——各自乱走,但差值平稳。

协整与价差

两只股票价格 y,xy, x 各自是非平稳的,但若存在 β\beta 使

spreadt=lnytβlnxt\text{spread}_t = \ln y_t - \beta \ln x_t

平稳(均值与方差恒定),则称二者协整,β\beta 是对冲比率。

「人话」解释:协整 vs 相关,区别在哪?

相关是「同涨同跌」——两个都涨的序列相关性可能很高。 协整更强:要求「差值」稳定在某个水平附近,不会越走越远。 两条同时上涨的线可能高度相关、但差值发散(不协整);只有差值「像橡皮筋拉住」的,才能做配对交易——因为你可以确信「价差终会回来」。

交易规则(z-score 进出场)

用价差的滚动 z-score:

zt=spreadtspread60σ60(spread)z_t = \frac{\text{spread}_t - \overline{\text{spread}}_{60}}{\sigma_{60}(\text{spread})}
  • z<2z < -2 → 价差过低,做多价差(多 yy、空 βx\beta x);
  • z>+2z > +2 → 价差过高,做空价差
  • z<0.5|z| < 0.5 → 回归,平仓

可运行案例:AAPL / MSFT 配对(合成数据)

读取 aapl_msft_daily.csv,OLS 估计 β\beta,构造价差与 z-score,按规则交易。价差组合的收益 = raaplβrmsftr^{aapl} - \beta\, r^{msft}

:::caution 合成数据 本数据刻意协整(真值 β1.30\beta\approx1.30,价差 ADF t142.86t\approx-14\ll-2.86)。你估计出的 β\beta 会偏离真值——这正是「估计误差」的教学点,并非套利 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)

# OLS: la ~ a + b*lm
x, y = lm.values, la.values
b = np.cov(x, y, ddof=0)[0,1] / np.var(x)
a = y.mean() - b * x.mean()
spread = la - (a + b*lm)            # 残差(近似平稳)
print(f"估计 β = {b:.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)

# 价差组合收益 = aapl 对数收益 − β*msft 对数收益
ra = np.log(pa / pa.shift(1)); rm = np.log(pm / pm.shift(1))
spread_ret = (ra - b*rm).fillna(0.0)
position = signal.shift(1).fillna(0.0)        # 防前视(同引擎约定)
strat_ret = (position * spread_ret).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()

动手改一改:拖动阈值即时看回测

拖动进场阈值 z|z| 与平仓阈值——阈值越小交易越频繁、成本越敏感;越大越挑剔、信号越少。

# 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)
x, y = lm.values, la.values
b = np.cov(x, y, ddof=0)[0, 1] / np.var(x)
a = y.mean() - b * x.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 * rm).fillna(0.0)
strat = (signal.shift(1).fillna(0.0) * spread_ret).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()

进一步:把代码里的对冲比率 b 手动改成 1.30(真值)再跑——用真值时价差更平稳、回撤往往更小,凸显精确估计 β 的价值

小结

  • 配对交易赌的是协整价差的均值回归
  • β\beta 由 OLS 估计,价差 =lnyβlnx=\ln y-\beta\ln x,z-score 决定进出场;
  • 协整 \ne 相关——只有差值平稳的「橡皮筋」关系才能套利。