「指数涨了 1%,我的对冲头寸怎么反而亏了?」——这是用固定对冲比做配对/对冲交易的人,最常在 regime 切换时遇到的问题。根子在哪儿?你用来对冲的那个 β,它自己就在变。
本文不堆公式,直接给你一套能跑的代码:把对冲比放进状态空间模型,用卡尔曼滤波(Kalman Filter)从价格序列里实时「学」出动态 β,再和静态 OLS、滚动 OLS 做硬碰硬的对比。你会发现,光是「让对冲比动起来」这一步,就能把对冲残差的方差砍掉一个数量级。

一、为什么固定对冲比会失效#
最朴素的对冲思路是最小二乘:用一段历史窗口回归 y = β·x + ε,拿到一个 β 后,长期持有「多 y、空 β 份 x」的中性组合。问题在于——这个 β 是历史的平均值,而市场不是静止的。
真实世界里,对冲比会变的原因至少有三:
- Regime 切换:比如股指期货和现货的基差结构在升水/贴水之间切换,对冲比会跳变;
- 结构性漂移:标的的基本面关系缓慢变化(如两只股票从强相关走向分化);
- 流动性与微观结构:成交量、买卖价差变化会改变两者的最优对冲比例。
下图第一根线就是「真实 β_t」:它在中段有一个正弦慢漂移,在 t=950 附近还有一个阶跃(模拟一次 regime 切换)。后面三条线分别是三种估计方法——你能直观看到,红色虚线(静态 OLS)在最右侧已经系统性偏离,橙色(滚动 OLS)能跟上但噪声大、滞后明显,而蓝色(卡尔曼)既跟得住又平滑。
二、卡尔曼滤波:一句话直觉#
卡尔曼滤波干的事,就是在「信任模型预测」和「信任新观测」之间,按不确定性动态加权。
想象你蒙眼往前走(状态预测),每隔几秒有人告诉你真实位置(观测)。如果你走得很稳(预测不确定小),你就更信自己的估计;如果观测噪声很小(测量很准),你就更信那个读数。卡尔曼滤波用严格的数学,把这两种「信任度」量化成每一步的卡尔曼增益(Kalman Gain),自动求出最优折中。
放到对冲场景,我们把「对冲比 β」当作隐藏状态——它看不见,但能通过 y 和 x 的关系推断出来。
三、动态对冲比的状态空间模型#
把对冲关系写成:
观测方程: y_t = β_t · x_t + μ_t + ε_t, ε_t ~ N(0, R) ← R 是观测噪声方差
状态方程: [β_t, μ_t]^T = [β_{t-1}, μ_{t-1}]^T + w_t, w_t ~ N(0, Q) ← Q 是状态演化方差plaintext这里 β_t 是我们要的动态对冲比,μ_t 是缓慢漂移的截距(吸收价差、基差等水平项),Q 控制 β 变化的「快慢」,R 控制我们对价格噪声的容忍。整套逻辑就是含时变系数的线性回归,用卡尔曼滤波在线估计。
下面是从零实现的卡尔曼滤波(不依赖 pykalman,纯 numpy,方便你嵌进回测):
import numpy as np
def kalman_hedge_ratio(y, x, R=1.4, Qb=5e-6, Qm=5e-5):
"""
在线估计动态对冲比 β_t 与截距 μ_t。
状态 θ_t = [β_t, μ_t],随机游走演化;观测 H_t = [x_t, 1]。
"""
n = len(y)
theta = np.zeros((n, 2)) # 状态估计 [beta, mu]
P = np.zeros((n, 2, 2)) # 状态协方差
theta[0] = [0.5, 0.0]
P[0] = np.eye(2) * 10.0 # 初始不确定度大一点
Q = np.diag([Qb, Qm]) # 状态演化噪声
for k in range(1, n):
# —— 预测步:状态按随机游走外推,不确定度累积 ——
theta_pred = theta[k - 1].copy()
P_pred = P[k - 1] + Q
# —— 更新步:用新观测 y[k] 修正 ——
H = np.array([x[k], 1.0]) # 观测矩阵
S = H @ P_pred @ H + R # 新息(innovation)方差
K = P_pred @ H / S # 卡尔曼增益
innov = y[k] - H @ theta_pred # 预测误差
theta[k] = theta_pred + K * innov # 修正状态
P[k] = (np.eye(2) - np.outer(K, H)) @ P_pred # 更新不确定度
return theta[:, 0], theta[:, 1] # 返回 beta_t, mu_t 序列python注意 Q 和 R 这两个旋钮:Q 调大,滤波更「跟手」但更抖;R 调大,滤波更「平滑」但更滞后。实战里通常用一段样本做极大似然或网格搜索来定,下文会讲。
四、实证对比:静态 OLS vs 滚动 OLS vs 卡尔曼#
我们用一段合成数据(x 为标的物、y 为其可套利对手,真实 β_t 含慢漂移 + 一次阶跃)跑三种方法,看对冲后的残差有多干净。对照组代码:
def rolling_ols(y, x, W=120):
"""滚动窗口 OLS,模拟实务中常用的固定窗口再估计。"""
n = len(y)
beta = np.full(n, np.nan)
for k in range(W, n):
b = np.polyfit(x[k - W:k], y[k - W:k], 1)
beta[k] = b[0]
beta[:W] = np.polyfit(x, y, 1)[0]
return beta
# 三种对冲比的残差(持有「多 y + 空 β·x」中性组合)
beta_kf, mu_kf = kalman_hedge_ratio(y, x)
beta_roll = rolling_ols(y, x, 120)
b_static, a_static = np.polyfit(x, y, 1) # 全样本静态
res_static = y - b_static * x - a_static
res_kf = y - beta_kf * x - mu_kfpython把对冲残差的累计轨迹画出来(下图),结论很直观:卡尔曼动态对冲的净值线明显更扁、更贴着 0 轴,说明它把绝大部分共同波动都对冲掉了,剩下的是真正的 idiosyncratic 噪声。

更硬的指标看分布。把日度残差(即对冲组合的每日 PnL)画成直方图:

在我跑的这组样本里,静态 OLS 对冲残差的标准差约 18.8,而卡尔曼动态对冲只有 1.9,方差下降约 99%。这不是「魔法」——静态方法在 regime 切换后,用的是过时的 β,等于对冲了一半「错误的方向」,残差里混入了大量本可对冲掉的系统性波动。
提醒:99% 是合成数据刻意放大了 regime 切换的演示效果。真实市场里削减幅度没这么夸张,但「动态对冲显著优于静态」的结论是稳健的,尤其在基差/价差结构易变的产品上。
五、另一战场:动态 Delta 对冲#
卡尔曼滤波不止能对冲「两只标的的关系」,也能对冲「期权与标的的关系」。期权 Delta 本质就是对标的的局部对冲比,而它会随价格、时间、波动率一起变。
思路完全通用:把期权价格当 y、标的当 x,用卡尔曼滤波在线估计「隐含 Delta」——它甚至不依赖 BS 模型的波动率假设,是从 tick 数据里直接「反」出来的经验 Delta。对波动率曲面扭曲、临近到期的合约,这种数据驱动的 Delta 往往比解析公式更贴实际。本质上,这就是把上一节的 β_t 换成 Δ_t,其余代码一行都不用改。
六、滤波器直观:它到底在权衡什么#
下图把卡尔曼滤波的「灵魂」单独拎出来演示:一条被重噪声淹没的绿色真实信号,灰色是含噪观测,蓝色是滤波估计,蓝色阴影是 95% 置信带。

你能看到:在信号平稳段,置信带很窄、估计很稳;在信号快速变化时,滤波会「试探性」跟随但保留安全裕度。这正是 Q(状态敢不敢变)和 R(敢不敢信观测)两个旋钮在起作用——不是猜测,而是每一步都用不确定度算出来的最优权衡。
七、实战三大陷阱#
- Q/R 调参即模型风险:滤波质量高度依赖
Q、R。调得太「跟手」会过拟合噪声、制造假信号;太「平滑」又滞后。务必在样本外或用滚动窗口重估,别把全样本最优参数当圣旨。 - 假设线性高斯:标准卡尔曼滤波假设状态方程和观测方程都是线性、噪声是高斯的。跳空缺口、涨跌停、流动性黑洞会让这些假设瞬间崩塌,此时需要扩展卡尔曼(EKF)或无迹卡尔曼(UKF),甚至粒子滤波。
- 别忽略交易成本:动态对冲意味着更频繁调仓。β_t 每变一点就 rebalance,手续费和冲击成本可能吃掉全部方差削减收益。实务上要加「死区」(β 变化超过阈值才调仓)或在目标函数里直接计入成本。
八、小结#
卡尔曼滤波给动态对冲提供了一套在线、自适应、带不确定度量的框架:它把「对冲比」从「历史平均值」升级成「随市场实时演化的状态」,在 regime 切换和高噪声环境里,比静态/滚动 OLS 更跟手、更干净。它的真正价值不在「预测」,而在「在信模型还是信数据之间,每一步都做出数学最优的取舍」。
下一篇,我们把镜头从「估计一个参数」转向「在几千种组合里搜索最优解」——遗传算法如何在大约束下做组合优化。