卡尔曼滤波动态跟踪:用状态空间模型把噪声里的真值估出来
卡尔曼滤波是状态空间模型下的最优(最小均方)递推估计器:把要追踪的量写成隐藏状态、把带噪观测写成观测方程,每步只需「预测+更新」,卡尔曼增益自动在信状态与信观测之间取最优折中。本文用纯 numpy 从零实现通用卡尔曼滤波,在噪声价格还原真值、时变对冲比β_t在线跟踪、过程噪声Q的U形敏感性三个场景真实跑通,并诚实指出「因果 KF 的 RMSE 会输给偷看未来的移动平均」这条最被忽视的边界(中阶)。
你手里有带噪声的观测:成交价里混着买卖价差跳动、协整价差的对冲比 一直在漂、隐含波动率藏在带噪的期权报价背后。你想追踪的「真值」看不见,只能看到被噪声污染的信号。怎么办?
卡尔曼滤波(Kalman Filter, 1960) 就是为这个量身定做的:它是线性高斯状态下最小均方误差的递推估计器,也是「状态空间模型」的工程引擎。它的妙处不在公式复杂,而在于每步只做两件极简的事——预测(用状态演化往前推一步)和更新(用新观测修正),中间一个叫卡尔曼增益 的数自动权衡「该信状态还是该信观测」。
本文用纯 numpy 从零实现通用卡尔曼滤波(支持标量状态与时变参数回归),在三个量化场景真实跑通:① 噪声价格里还原平滑真值 ② 时变对冲比 在线跟踪 ③ 过程噪声 的 U 形敏感性。并诚实指出「因果 KF 的 RMSE 会输给偷看未来的移动平均」这条最被忽视的边界。所有图表均由下文 Python 真实计算,非占位图。
![]()
一、状态空间模型:把问题写成两个方程#
卡尔曼滤波假设你面对的是一个状态空间模型:
- 状态方程(隐藏的真值怎么演化):
- 观测方程(你能看到什么):
是你真正想追踪的(真价格 / 真对冲比 / 真波动率), 是观测(噪声价格 / 资产2 报价 / 期权 IV), 是状态怎么转移(随机游走就取单位阵), 是状态怎么映射到观测, 是状态演化的噪声(过程噪声), 是观测噪声。
滤波每个时刻只做两步:
- 预测:,不确定性
- 更新:新息 ,卡尔曼增益 ,修正
就是核心:它永远在 之间—— 表示「观测太噪,我信状态」, 表示「状态在乱漂,我信观测」。最优折中由 与 的比值自动决定,你不用手调。纯 numpy 实现(Joseph 形式保证协方差半正定):
import numpy as np
def kalman_filter(y, F, H, Q, R, x0, P0):
"""线性高斯卡尔曼滤波。H 可传 (T, m, n) 逐时刻变化(回归场景)。"""
y = np.asarray(y, float)
if y.ndim == 1:
y = y[:, None]
T, n = y.shape[0], F.shape[0]
tv = H.ndim == 3
x_filt = np.zeros((T, n))
x, P, I = np.asarray(x0, float).copy(), np.asarray(P0, float).copy(), np.eye(n)
for t in range(T):
Ht = H[t] if tv else H
x_pred = F @ x
P_pred = F @ P @ F.T + Q
S = Ht @ P_pred @ Ht.T + R
K = P_pred @ Ht.T @ np.linalg.inv(S) # 卡尔曼增益
e = y[t] - Ht @ x_pred # 新息
x = x_pred + K @ e
P = (I - K @ Ht) @ P_pred @ (I - K @ Ht).T + K @ R @ K.T
x_filt[t] = x
return x_filtpython二、场景1:噪声价格里还原真值#
构造一条平滑随机游走「真值」,再叠加 6 倍于信号的噪声观测(SNR=0.167),用随机游走状态( 很小)跑滤波:
F = np.array([[1.0]]); H = np.array([[1.0]])
Q = np.array([[1e-3]]); R = np.array([[0.09]]) # 观测噪声 ≈0.30²
kf = kalman_filter(obs, F, H, Q, R, np.array([obs[0]]), np.array([[1.0]]))python对真值的 RMSE 对比:
| 估计器 | RMSE |
|---|---|
| 原始噪声观测 | 0.299 |
| 移动平均 MA(20) | 0.094 |
| 卡尔曼滤波 | 0.127 |
![]()
注意一个反直觉但必须诚实说的事实:KF 的 RMSE(0.127)比 MA(20)(0.094)略高。原因是 MA 是非因果的——它用了未来 10 个点来平滑当前点,等于偷看了未来,所以任何平滑指标只要能看未来就必然更准。卡尔曼滤波是因果的实时估计器,只能用「到目前为止」的信息,自然在「平滑一条已知曲线」这种事后任务上略逊于 MA。它的真正优势不在场景1,而在下一节的时变状态追踪——状态一旦会变,MA 的平均窗就被糊掉了。
三、场景2:时变对冲比 β_t 在线跟踪#
这是卡尔曼滤波在量化里最经典的用武之地——协整配对里 会漂。经典协整配对用全样本 OLS 锁一个固定 ,可真实 一直在摆。把对冲比写成二维状态 ,观测方程变成回归 :
F2 = np.eye(2) # 状态随机游走演化
H2 = np.stack([np.ones(M), x], axis=1)[:, None, :] # 逐时刻 H_t = [1, x_t]
Q2 = np.array([[1e-4, 0], [0, 1e-4]]) # 过程噪声(β 缓慢漂移)
R2 = np.array([[0.25]]) # 观测噪声 ≈0.5²
kf2 = kalman_filter(y, F2, H2, Q2, R2, np.array([0.0, 1.0]), np.eye(2))
beta_kf = kf2[:, 1]python合成一条真 在 间缓慢摆动,KF 跟踪 vs 静态 OLS:
真 β 范围 : [0.650, 1.350]
静态 OLS β : 0.960 (被平均糊成一条线)
β 追踪 RMSE : KF=0.0815 OLS=0.2541 (KF 精度 3.1×)
价差重建 RMSE: KF=0.249 OLS=2.080 (KF 精度 8.3×)plaintext![]()
KF 把 的追踪误差压到 OLS 的 1/3、把「用估计 β 重建的动态价差」误差压到 OLS 的 1/8——这正是配对交易里动态对冲比能多赚、少踩伪突破的原因。如果 在漂而你用固定 OLS 锁死,价差的「偏离」有一半是 漂移假造的,会频繁误触发交易。
四、场景3:过程噪声 Q 的 U 形敏感性#
是卡尔曼滤波最该调、也最易调错的旋钮。它控制「我觉得状态每步能变多大」:
- Q 太小(太刚):滤波过于信任模型、几乎不跟新观测,状态被「焊死」,滞后严重;
- Q 太大(太软):滤波几乎只信观测、每步都被噪声带着抖,等于没滤波。
在场景1 上扫 Q(对数网格 ),KF 对真值的 RMSE 呈漂亮 U 形:
![]()
网格搜索最优 Q ≈ 0.0030
本文设定 Q = 0.0010 (已接近最优,增益自适应良好)plaintextU 形谷底才是好 Q:它恰好平衡了「状态能漂」和「观测太噪」。调参经验法——先从观测噪声 出发,把 设成比 小 1~2 个数量级,再在验证集上扫出来。增益 随时间自适应变化(右图),说明滤波在「状态平稳时少信观测、噪声跳变时多信观测」,这正是 在干活的证据。
五、实战落地:用 KF 跟踪的 β_t 直接生成配对信号#
追踪到动态 不是终点,把它接进配对交易的进出场才是价值。配对交易的核心假设是「价差偏离后回归」,价差定义在状态空间里就是残差 ——也就是滤波的新息。KF 已经白送你这个信号:
alpha_kf, beta_kf = kf2[:, 0], kf2[:, 1]
spread_dynamic = y - (alpha_kf + beta_kf * x) # = 滤波新息 e_t
# 用动态价差的标准差做 z-score,±2 开仓、±0.5 平仓
zs = (spread_dynamic - spread_dynamic.mean()) / spread_dynamic.std()
long_entry = zs < -2.0 # 价差被压太低 → 做多价差(买 y 卖 βx)
short_entry = zs > 2.0 # 价差被顶太高 → 做空价差
exit_signal = np.abs(zs) < 0.5python为什么用 KF 的 而不是 OLS 的固定 ?因为固定 造出的静态价差里,有一大块偏离是 漂移假造的(本文里静态价差 RMSE 是动态的 8.3 倍)。这部分「伪偏离」会频繁穿过 ±2σ 触发线,制造一堆不回归的假信号;KF 的动态价差把真偏离和 漂移剥开,信号的回归率自然更高。实务上还有三点要注意:
- 价差要标准化再开仓:动态价差的方差随时间变,直接拍硬阈值会在波动大的段过度交易;
- 状态要预热:前 30~50 步 KF 还在收敛,这期间信号别用;
- 成本必须单笔核算:KF 让信号更干净,但配对交易决胜在手续费+冲击,别只看信号频率。
把这节和第三节的数字合起来看:KF 把价差的重建误差压到 OLS 的 1/8,不是论文里的漂亮话,而是直接决定「你触发的偏离是不是真的会回归」。
六、五类真实陷阱(必看)#
- 把因果 KF 和 MA 比 RMSE 是不公平的:MA 偷看未来,KF 是实时因果估计器,平滑已知曲线任务上 KF 必然略输。比 KF 该比的是「实时、只用到当前及之前信息」的估计器(如前向 EMA、单次指数平滑),或在时变状态场景比 OLS。
- Q 定错=滤波失效:Q 太小拖成滞后、Q 太大抖成噪声,U 形谷底才是好 Q,必须调。很多人直接套
Q=1e-5然后抱怨 KF「没用」,其实是 Q 焊死了。 - 线性高斯假设:KF 最优性只在「状态转移与观测都线性、噪声都高斯」时成立。价格里的厚尾、跳变、 regime 切换会让 KF 系统性滞后——这时该上 EKF/UKF 或粒子滤波(本专栏另有粒子滤波专文)。
- 协方差 P 不准会雪崩:KF 的增益来自 (对不确定性的自我认知)。若初始化 太小或 比例失真,前几十步会剧烈震荡。实务上 宁可设大一点,让滤波先「谦虚」地多信观测。
- 观测矩阵 H 时变要显式建模:回归场景的 随 变,必须逐时刻传进滤波(本文
H.ndim==3分支)。若把 当成常数、用全样本平均的 ,时变 的追踪立刻退化。
结语#
卡尔曼滤波的精髓不是公式长,而是一个极简的递推闭环:预测→更新→用卡尔曼增益自动折中信状态还是信观测。它在量化里最该用的地方不是「平滑一条已知曲线」(那会输给偷看未来的 MA),而是实时追踪会变的隐藏状态——时变对冲比 让配对价差重建误差降到 OLS 的 1/8,噪声价格里的真值也能砍掉 58% 的噪声。落地三件事:Q 必须调(U 形谷底)、 宁可设大、H 时变要显式建模。把它和本专栏的粒子滤波、TVP-VAR 放一起看——三者都是状态空间模型的引擎,区别只在噪声是不是高斯、状态是不是线性。
完整可复跑代码与全部图表脚本已随本文生成(gen_kalman_filter_tracking.py),噪声还原、时变 β 跟踪、Q 敏感性三组实验均由该脚本真实计算。