卡尔曼滤波在配对交易中的进阶应用 (Kalman Filter Applications)¶
在前面的回测示例中,我们使用 OLS 回归在整个历史数据上计算了一个固定的对冲比例 (Hedge Ratio, $\beta$)。
滚动窗口 OLS(Rolling OLS)可以按固定窗口重复估计 $\beta$,但窗口选择会影响估计的滞后与方差。
但这两种方法都有严重的缺陷:
- 全样本估计的固定 $\beta$ 会引入未来函数(Lookahead Bias):若计算 2021 年仓位时使用了包含 2023 年数据的 $\beta$,则使用了交易时点尚不可得的信息。
- 滚动窗口 OLS 噪音大且滞后:窗口设为几天?当早期发生突变并从窗口滑出时,OLS 的 $\beta$ 会发生剧烈的毛刺跳跃 (Ghost Effects)。
一种递推估计方法是卡尔曼滤波(Kalman Filter)。
什么是卡尔曼滤波器?¶
卡尔曼滤波是一种动态线性模型 (DLM) 或状态空间模型。 在嘈杂的数据流中,它能不断读取新的测量数据流连续更新自身以估计信号系统的当前状态。 在金融配对交易中,系统的“隐藏状态”就是真实地随时间漂移的对冲比例 $\beta_t$ 及其误差方差。
- 预测步 (Predict): 基于昨日已知的 $\beta_{t-1}$,预测今天的 $\beta_t$。
- 更新步 (Update): 今天收盘看到了真实的 $Y_t$ 和 $X_t$。通过比较实际的 $Y$ 和用预测 $\beta$ 算出的预期 $Y$,滤波器会对 $\beta_t$ 进行修正。
卡尔曼滤波在量化中的优势¶
- 递推更新:过滤步骤只使用当前及以前的观测;参数估计、初始化和数据预处理仍需单独检查未来信息。
- 无需指定滞后的固定窗口:自带指数衰减记忆机制,杜绝“Ghost Effects”。
- 内置不确定性估计:除了点估计 $\beta$,还有协方差矩阵代表“自信程度”,可用于头寸调节。
1. 这节究竟解决什么实际问题?¶
卡尔曼滤波在配对交易中的进阶应用 (Kalman Filter Applications) 的具体任务是:让对冲比率随时间更新,并检查滤波器参数对交易频率的影响。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解收益率、仓位、交易成本与基本技术指标。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
默认固定历史样本。历史数据只能说明过去在既定假设下发生了什么,不能承诺未来收益。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 卡尔曼滤波在配对交易中的进阶应用 (Kalman Filter Applications) 的输入、计算步骤、输出和局限;尤其能说明:让对冲比率随时间更新,并检查滤波器参数对交易频率的影响。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
回测按照明确的成交和成本假设重放历史数据。它不能完整复现未来流动性、订单竞争和系统故障。
策略收益 = 滞后仓位 × 资产收益 − 换手 × 成本;每一项都要有明确单位。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
回测框架替你管理订单和记录,不会自动判断数据、成本或策略假设是否合理。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“让对冲比率随时间更新,并检查滤波器参数对交易频率的影响。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
信号未滞后、把手续费只扣一次、忽略空仓收益、年化频率错配、将净值曲线当显著性证明。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
检查未来函数、幸存者偏差、重复测试集调参、成交可得性和失败区间;必须报告基线。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/04_backtesting/07_kalman_filter_applications_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/04_backtesting/07_kalman_filter_applications_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Advances in Financial Machine Learning(Lopez de Prado)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 08_market_impact_slippage。本节的概念会成为理解它的输入,而不是孤立的名词。
!pip install pykalman
Requirement already satisfied: pykalman in C:\Users\DELL\.conda\envs\quant\Lib\site-packages (0.11.2) Requirement already satisfied: numpy<3 in C:\Users\DELL\.conda\envs\quant\Lib\site-packages (from pykalman) (2.4.2) Requirement already satisfied: packaging in C:\Users\DELL\.conda\envs\quant\Lib\site-packages (from pykalman) (26.0) Requirement already satisfied: scikit-base<0.14.0 in C:\Users\DELL\.conda\envs\quant\Lib\site-packages (from pykalman) (0.13.0) Requirement already satisfied: scipy<2.0.0 in C:\Users\DELL\.conda\envs\quant\Lib\site-packages (from pykalman) (1.17.1)
import numpy as np
import pandas as pd
from pykalman import KalmanFilter
import statsmodels.api as sm
import warnings
warnings.filterwarnings('ignore')
def kalman_filter_beta(y, x):
"""
使用卡尔曼滤波器动态估计 Y 和 X 之间的对冲比例 Beta。
观测方程: y_t = alpha_t + beta_t * x_t + v_t
状态方程:[alpha_t, beta_t] = [alpha_{t-1}, beta_{t-1}] + w_t
"""
# 构造观测矩阵 (Observation Matrix),加上一列全为1代表常数项 Alpha
obs_mat = sm.add_constant(x.values)[:, np.newaxis]
# 初始化卡尔曼滤波器
kf = KalmanFilter(
n_dim_obs=1, # 观测维度,只有 y
n_dim_state=2, # 状态维度,[alpha, beta]
initial_state_mean=np.zeros(2),
initial_state_covariance=np.ones((2, 2)),
transition_matrices=np.eye(2), # [alpha_t, beta_t] 等待昨日的值
observation_matrices=obs_mat, # H 矩阵: [1, x_t]
observation_covariance=1.0, # R 观测噪声方差 (需调参)
transition_covariance=np.eye(2) * 0.01 # Q 状态漂移方差 (需调参)
)
# 运行滤波器 (使用 filter 进行在线递推估计,不用 smooth,以避免未来函数)
state_means, _ = kf.filter(y.values)
# 返回分离出的动态 Alpha 序列和 Beta 序列
dynamic_alpha = pd.Series(state_means[:, 0], index=y.index)
dynamic_beta = pd.Series(state_means[:, 1], index=y.index)
return dynamic_alpha, dynamic_beta
print("卡尔曼滤波引擎构建完毕。可以将产生的 dynamic_beta 替换此前固定的 Beta 进行 OOS 回测。")
卡尔曼滤波引擎构建完毕。可以将产生的 dynamic_beta 替换此前固定的 Beta 进行 OOS 回测。
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
3. 使用 Kalman Filter 动态估计协整价差¶
我们将模拟一个环境,其中两只股票的对冲比例 $\beta$ 会随着时间发生一次“结构性漂变(Regime Shift)”。
- 固定 OLS 会取一根居中的平均线,导致两头都在亏钱。
- 滚动 OLS 反应迟缓且可能引发假突变。
- Kalman Filter 能灵敏且平滑地追踪到 $\beta$ 的真实漂变。
import matplotlib.pyplot as plt
np.random.seed(42)
# 模拟股票 X 的价格走势
x_returns = np.random.normal(0, 1, 500)
x_prices = np.cumsum(x_returns) + 100
stock_x = pd.Series(x_prices)
# 模拟股票 Y,它的 Beta 发生了一次结构性漂移
# 前 250 天 Beta = 1.5, 后 250 天 Beta = 0.5
true_betas = np.concatenate([np.ones(250) * 1.5, np.ones(250) * 0.5])
true_alphas = np.ones(500) * 20.0
y_prices = true_alphas + true_betas * x_prices + np.random.normal(0, 3, 500) # 加上噪音 (平稳的 Spread)
stock_y = pd.Series(y_prices)
# 1. 传统方法:使用全局 OLS 拟合
fixed_ols_model = sm.OLS(stock_y, sm.add_constant(stock_x)).fit()
fixed_beta = fixed_ols_model.params.iloc[1]
print(f"无论市场如何变化,OLS 想用一个固定的 Beta = {fixed_beta:.4f} 来解释整个历史。")
# 2. 现代方法:卡尔曼滤波动态追踪
dynamic_alpha, dynamic_beta = kalman_filter_beta(stock_y, stock_x)
# --- 绘图展示动态 Beta 的追踪效果 ---
plt.figure(figsize=(12, 6))
plt.plot(true_betas, label='True Hidden Beta', color='black', linestyle='--')
plt.plot([fixed_beta]*500, label='Global OLS Fixed Beta', color='red', alpha=0.5)
plt.plot(dynamic_beta.values, label='Kalman Filter Dynamic Beta', color='blue')
plt.title('Dynamic Beta Tracking: Kalman Filter vs OLS')
plt.xlabel('Days')
plt.ylabel('Hedge Ratio (Beta)')
plt.legend()
plt.show()
无论市场如何变化,OLS 想用一个固定的 Beta = -4.2584 来解释整个历史。
如上图所示,当市场发生制度转换 (Regime Shift) 时,全局 OLS 是完全失效的(用红线去套前后的黑线)。而卡尔曼滤波器在短暂的适应期后,平滑地追踪到了新的真实 Beta。
# 基于卡尔曼滤波器的动态 Spread 计算
# Spread_t = Y_t - (alpha_t + beta_t * X_t)
kalman_spread = stock_y - (dynamic_alpha + dynamic_beta * stock_x)
# 如果我们画出这个动态 Spread
plt.figure(figsize=(12, 4))
plt.plot(kalman_spread.index, kalman_spread.values, color='purple', label='Kalman Filter Spread (Stationary)')
plt.axhline(0, color='black', alpha=0.5)
plt.title('Dynamic Spread properly bounded by Kalman Filter')
plt.legend()
plt.show()
这极大降低了历史过拟合参数(Overfitting)的可能性,并增强了策略在实盘未知数据上的鲁棒性。
本章结语¶
配对交易绝不是拿历史均线套未来的简单游戏。
- 在
06_pairs_trading_strategy.ipynb中,为了演示逻辑框架,我们假设了 $\beta$ 是固定不变且前后眼通天的。这种 Lookahead Bias 是毁掉量化新手的罪魁祸首。 - 在本章中,引入了动态线性模型(卡尔曼滤波器),真正实现了“利用 $t-1$ 的信息来调整 $t$ 取值的滤波器逻辑”,剥离了未来函数的影响。
本节完成了卡尔曼滤波在动态对冲比率估计中的示例。后续章节将转向多因子选股,并继续使用时序估计、样本外验证和风险控制。
练习¶
- 修改卡尔曼滤波器的
transition_covariance参数(Q 矩阵),观察它对 Beta 追踪灵敏度的影响。 - 将上一章的配对交易策略整合到卡尔曼滤波动态 Spread 上,对比新旧回测的夏普比率。
- 比较
pykalman的smooth()与filter():为何双向平滑结果不能直接用于历史时点的可交易信号?
下一节 → ../05_portfolio/04_factor_analysis.ipynb