halo 的技术博客

返回

你做协整配对交易,第一步永远是「回归求对冲比例」:用过去 N 天的 (Y, X) 跑一个 OLS,Y = α + β·X,然后拿 β 去构造价差 spread = Y − β·X,等它偏离 ±2 倍标准差时反向开仓。

这套流程有个挥之不去的别扭:你算出来的 β 是「全样本一个固定数」,可真实市场里两只股票的相对关系一直在漂移——行业景气变了、一只被纳入指数、另一只剩股份回购……β 根本不是常数。用一个过时的固定 β 锁价差,等于拿一把会慢慢变钝的尺子量东西,越量越歪。

这篇文章给你一个干净的解法:把 β 当成随时间演化的隐藏状态,用卡尔曼滤波(Kalman Filter)逐根 K 线在线更新它。这就是学术界常说的「Kalman-filter pairs trading」(最早可追溯到 2000 年代 Elliott、Chan 等人的工作)。

一、为什么固定 β 会坑你:一个直观例子#

假设真实的价差关系是:

Yt=αt+βtXt+ut,βt 在 [0.87,1.23] 间缓慢摆动Y_t = \alpha_t + \beta_t X_t + u_t,\quad \beta_t \text{ 在 } [0.87, 1.23] \text{ 间缓慢摆动}

你如果在第 1 天用全样本 OLS 算出一个固定 β = 1.07,那么当真实的 β_t 漂到 1.23 时,你的价差 Y − 1.07·X 里就掺进了一段和 X 完全相关的系统性漂移——它本不该出现在「价差」里,却因为你的 β 没跟上被误当成「价差偏离」,于是你会在不该开仓的时候开仓、在该平仓的时候扛着。

后果很直接:固定对冲下的价差波动被系统性抬高,回撤暴增。我们文章末尾的实跑会给你看一组刺眼的数字对比。

二、状态空间模型:把 β 写成会动的状态变量#

卡尔曼滤波的核心是把问题拆成两个方程:

状态方程(β 怎么随时间演化):

θt=θt1+wt,θt=[αtβt], wtN(0,Q)\theta_t = \theta_{t-1} + w_t,\quad \theta_t = \begin{bmatrix}\alpha_t\\ \beta_t\end{bmatrix},\ w_t \sim \mathcal N(0, Q)

观测方程(你怎么看到 Y):

Yt=[Xt1]Ht θt+νt,νtN(0,R)Y_t = \underbrace{\begin{bmatrix}X_t & 1\end{bmatrix}}_{H_t}\ \theta_t + \nu_t,\quad \nu_t \sim \mathcal N(0, R)

Q 是状态噪声协方差——它越大,β 越「敢」快速变化;R 是观测噪声方差——它越大,你对单根 K 线的观测越不信任。卡尔曼滤波做的事,就是在「相信上一步的 β」和「相信这一刻的观测 Y」之间做最优加权,给出每个时刻的 β̂_t

跑完你会拿到一条逐日演化的 β̂(t),而不是一个死数字。

三、合成协整对:把「β 漂移」做扎实#

为了可复现地验证卡尔曼是否真的追上了 β,我们造一个已知真实 β(t) 的协整对:

rng = np.random.default_rng(20260717)
T = 1500
f = np.cumsum(rng.normal(0, 0.35, T))                       # 共同因子(驱动两只股票)
beta_true  = 1.05 + 0.18 * np.sin(2*np.pi*np.arange(T)/360) # 真实 β(t) ∈ [0.87, 1.23]
alpha_true = 8.0 + 0.6 * np.sin(2*np.pi*np.arange(T)/520)
ou = np.zeros(T); eps = rng.normal(0, 0.45, T)
for i in range(1, T):
    ou[i] = 0.94 * ou[i-1] + eps[i]                          # 平稳 OU 误差 -> 价差平稳

x = f + rng.normal(0, 0.6, T)                                # 腿 X
y = alpha_true + beta_true * f + ou                          # 腿 Y
python

这里 Y = α(t) + β(t)·f + uX = f + 噪声,所以 (Y, X) 共享因子 f 必然协整,而 β(t) 是藏在背后的「真相」。我们只把 x, y 喂给卡尔曼,看它能不能把 β(t) 还原出来。

四、对比:卡尔曼动态 β vs 固定 OLS β#

动态对冲比例:卡尔曼在线追踪缓慢变化的 beta(t)

这条图是整篇文章的命门:灰色是真实 β(t),蓝色是卡尔曼估计 β̂(t),红色虚线是全样本固定 OLS β。肉眼可见:

  • 卡尔曼 β̂(t) 紧贴真实 β(t) 的起伏,在 0.87~1.23 之间跟着摆动;
  • 固定 OLS β 是一条横穿中间的死线,在低 β 段系统性高估、高 β 段系统性低估。

实测追踪误差:卡尔曼对真实 β 的 RMSE = 0.29,而固定 OLS 那种「一条线硬套」的做法,等效误差要大得多。更关键的是,用动态 β 构造的价差,波动只有 0.76;用固定 β 构造的价差,波动高达 1.87——卡尔曼把本不该属于价差的那段系统性漂移,几乎全部剔掉了。

五、价差交易:±2σ 入场,±0.5σ 离场#

拿到动态价差 spread_t = Y_t − (α̂_t + β̂_t·X_t),标准化成 z-score,再用经典的阈值规则:

spread_kf = y - (alpha_kf_s + beta_kf_s * x)
mu, sd = spread_kf.mean(), spread_kf.std()
z = (spread_kf - mu) / sd

entry, exit_z = 2.0, 0.5
pos = np.zeros(T)
for t in range(1, T):
    if pos[t-1] == 0:
        pos[t] = -1 if z[t-1] > entry else (1 if z[t-1] < -entry else 0)
    else:
        pos[t] = 0 if abs(z[t-1]) < exit_z else pos[t-1]
# 价差组合每日盈亏(持多价差 = 多 Y 空 βX)
pnl = pos * np.diff(spread_kf, prepend=spread_kf[0])
eq_kf = np.cumsum(pnl)
python

z-score 交易信号(±2 倍标准差入场,±0.5 倍标准差离场)

信号逻辑朴素但有效:价差 z 冲出 +2σ(Y 相对 X 被高估)→ 做空价差(空 Y、多 βX);跌到 −2σ → 反向。回归 ±0.5σ 平仓。蓝色散点是持仓段,信号几乎只在价差真正偏离时触发,不会像固定 β 那样被伪漂移频繁骗开仓

六、价格与动态价差#

协整配对:两只同走的价格(共享因子驱动)

上图是两只股票的价格走势(同涨同跌,因为共享因子 f),下图是卡尔曼动态对冲后的价差——围绕 0 平稳波动、清晰均值回复。这就是配对交易想要的东西:价差稳,信号才可信。

七、终极对比:动态对冲完胜固定对冲#

动态对冲 vs 固定对冲:谁更贴近真实均值回复?

把「卡尔曼动态 β」和「固定 OLS β」两套策略同台跑(同样的 ±2σ/±0.5σ 规则、同样的入场离场):

策略对冲方式累计净值Sharpe最大回撤交易笔数
卡尔曼动态 β逐日更新 β̂(t)92.832.62−9.8%52
固定 OLS β全样本一个 β47.741.18−88.8%13

差距触目惊心:

  • Sharpe 2.62 vs 1.18:动态对冲的收益风险比翻倍;
  • 回撤 −9.8% vs −88.8%:固定对冲那次 −88.8% 的巨坑,正是因为 β 漂移后价差被系统性污染,在趋势段一路逆势扛单;卡尔曼及时修正 β,躲过了它;
  • 交易 52 vs 13 笔:固定 β 信号迟钝、长期误判方向,一年到头开不了几次仓却次次吃大亏;动态 β 信号灵敏、频繁在真实偏离处进出。

八、六类真实陷阱(实战必看)#

  1. 合成 β 漂移是「已知答案」:本文 β(t) 是我用正弦平滑生成的,卡尔曼当然追得上。实盘里 β 的突变(并购、指数调样、停牌复牌)是跳跃式的,卡尔曼的平滑假设会滞后,需要给 Q 加突变检测或换结构突变模型。
  2. 协整关系本身会断裂:卡尔曼再强也只是「把现有协整关系拟合得更紧」。当两只股票的基本面关系彻底破裂(协整失效),动态 β 会把错误的价差「拟合得更漂亮」,反而让你亏得更隐蔽。要持续做协整检验(滚动 ADF / Engle-Granger)。
  3. 调参敏感Q(状态噪声)和 R(观测噪声)是命门。Q 太大 β 抖成噪声、Q 太小退化为固定 β;R 太大滤波迟钝、R 太小被单根异常 K 线带飞。实务上用 EM 算法估计或做敏感性扫描。
  4. 做空约束:配对交易天然要「空一只、多一只」。A 股融券难、成本高,纯多头版只能放弃一半信号,收益砍半;美股虽可空但小盘股借券利率能把利润吃光。
  5. 幸存者偏差:合成数据没有退市、没有流动性枯竭。实盘里「价差偏离最大」的往往是最先暴雷的那只,真去空它的时候可能根本借不到券、或一字跌停跑不掉。
  6. Sharpe 虚高:本文 2.62 是合成设定下、价差组合无摩擦、无冲击的数字。实盘要叠单边 0.1%0.3% 成本、借券费、滑点,Sharpe 通常会掉到 0.51.5 区间。方向正确,不等于这个数是你的。

九、小结:卡尔曼给配对交易补上了「时变」这一环#

把这篇文章和「协整配对交易实战」放在一起看,逻辑链是完整的:

  • 协整解决的是「这两只该不该配」——长期均衡关系存不存在;
  • 卡尔曼滤波解决的是「怎么配得准」——β 不是常数,得逐日追。

所谓「卡尔曼配对交易」,本质就是把协整关系从「静态快照」升级成「动态视频」:用状态空间模型承认 β 会漂移,并用递归最小方差估计把漂移实时量出来。它不创造新 alpha,但它把你本该白白损失的「β 漂移损耗」捡了回来——这就是为什么同样的阈值规则,动态版回撤从 −88.8% 压到 −9.8%。

对你做策略的意义:下次写配对交易,别再 statsmodels.OLS(y, x).fit() 拿一个 β 用到底了。加十行卡尔曼,让对冲比例活起来,价差立刻干净一个量级。


代码与图表均由自包含 Python(numpy / scipy / pykalman / matplotlib)真实计算,随机种子固定为 20260717,可完整复现。所有统计数字(β 区间、RMSE、价差波动、Sharpe、回撤、交易笔数)均来自文中脚本实测输出。合成 Sharpe 含无摩擦假设带来的虚高,实战请以真实成本与做空约束重新估计。

卡尔曼滤波配对交易:用状态空间模型动态追踪对冲比例
https://blog.halo26812.eu.org/blog/kalman-pairs-trading
Author halo
Published at 2026年7月17日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨