Etapa 3 de 4 · Nivel hedge fund
El filtro de Kalman en trading: un hedge ratio que se adapta (con Python)
El filtro de Kalman explicado desde la intuición y aplicado a finanzas: estimar en tiempo real una beta o un hedge ratio que cambia, en Python desde cero, y compararlo honestamente con una regresión móvil.
En este artículo
El filtro de Kalman nació en los años 60 para un problema muy concreto: saber dónde está una nave espacial a partir de mediciones ruidosas. Se usó en el programa Apolo y hoy está en cada GPS, cada dron y cada coche autónomo. En finanzas resuelve un problema parecido: estimar algo que no se ve directamente y que cambia con el tiempo, a partir de datos con mucho ruido.
En el artículo de pairs trading estimamos el hedge ratio \( \beta \) una sola vez y lo dejamos fijo. Pero la relación entre dos activos cambia: una empresa se endeuda, cambia de negocio, el sector evoluciona. Aquí vamos a construir un filtro de Kalman que actualiza la beta cada día, desde cero en Python.
La intuición: predecir, medir y corregir
El filtro de Kalman repite un ciclo muy natural, el mismo que haces al conducir con niebla:
- Predecir. «Si nada ha cambiado, la beta de hoy es la de ayer, aunque ahora estoy algo menos seguro.»
- Medir. Llega un dato nuevo y comparas lo que esperabas ver con lo que ves.
- Corregir. Ajustas tu estimación hacia el dato. Cuánto la ajustas depende de lo que te fíes de él frente a lo que te fíes de tu estimación anterior.
Ese «cuánto» es la ganancia de Kalman, y lo calcula el propio filtro: si tu estimación es muy incierta y el dato es fiable, te mueves mucho; si tu estimación es sólida y el dato ruidoso, casi no te mueves.
El modelo en dos ecuaciones
Queremos la beta entre las rentabilidades de dos activos, \( r^A_t = \beta_t\, r^B_t + \varepsilon_t \). El filtro de Kalman necesita dos ecuaciones: cómo evoluciona lo que no vemos (el estado) y cómo se relaciona con lo que sí vemos (la observación):
\[ \beta_t = \beta_{t-1} + w_t, \quad w_t \sim \mathcal{N}(0, q) \qquad\qquad r^A_t = \beta_t\, r^B_t + \varepsilon_t, \quad \varepsilon_t \sim \mathcal{N}(0, R) \]
La primera dice que la beta hace un paseo aleatorio lento: cada día puede cambiar un poco, con varianza \( q \). La segunda es la regresión de siempre, con ruido de varianza \( R \). Con esto, el ciclo predecir–medir–corregir queda así (con \( P \) la incertidumbre sobre la beta):
\[ P^- = P + q, \qquad K = \frac{P^- r^B_t}{(r^B_t)^2 P^- + R}, \qquad \beta \leftarrow \beta + K\,(r^A_t - \beta\, r^B_t), \qquad P \leftarrow (1 - K r^B_t)\,P^- \]
Unos datos con la relación cambiando
Para poder comprobar si el filtro acierta necesitamos saber la beta real, así que simulamos dos activos cuya relación pasa de forma gradual de 0,8 a 1,2 a mitad de periodo:
import numpy as np
import pandas as pd
rng = np.random.default_rng(7)
n = 1000
t = np.arange(n)
beta_real = 0.8 + 0.4 / (1 + np.exp(-(t - 500) / 40)) # la relación pasa de 0,8 a 1,2
r_B = rng.normal(0, 0.015, n) # rentabilidades de B
r_A = beta_real * r_B + rng.normal(0, 0.008, n) # A depende de B con beta variableEl filtro de Kalman en Python, desde cero
Con un solo parámetro que estimar, el filtro cabe en diez líneas:
def kalman_beta(r_A, r_B, q=1e-4, R=0.008 ** 2, beta0=1.0, P0=1.0):
beta, P = beta0, P0
betas, desv = np.empty(len(r_A)), np.empty(len(r_A))
for t in range(len(r_A)):
P = P + q # 1) predicción: beta puede haber cambiado
S = r_B[t] ** 2 * P + R # varianza de lo que esperamos observar
K = P * r_B[t] / S # 2) ganancia de Kalman: cuánto creer al dato
beta = beta + K * (r_A[t] - beta * r_B[t]) # 3) corrección con el error de predicción
P = (1 - K * r_B[t]) * P # y la incertidumbre se reduce
betas[t], desv[t] = beta, np.sqrt(P)
return betas, desv
betas, desv = kalman_beta(r_A, r_B)
print(f"Beta estimada al final: {betas[-1]:.2f} ± {2 * desv[-1]:.2f} (real: {beta_real[-1]:.2f})")Beta estimada al final: 1.15 ± 0.16 (real: 1.20)
Además de la estimación, el filtro da su incertidumbre en cada momento, algo que una regresión móvil no ofrece de forma natural. Es muy útil para decidir cuánto fiarse de la señal.
¿Es mejor que una regresión móvil? Comparación honesta
Aquí es donde muchos tutoriales venden humo. Comparemos el error medio del filtro con el de regresiones móviles de distintas ventanas:
def ols_movil(r_A, r_B, ventana):
a, b = pd.Series(r_A), pd.Series(r_B)
return (a.rolling(ventana).cov(b) / b.rolling(ventana).var()).values
m = t >= 250 # comparamos a partir del día 250
print(f"Kalman (q = 1e-4) error medio: {np.mean(np.abs(betas[m] - beta_real[m])):.3f}")
for v in [30, 60, 120, 250]:
est = ols_movil(r_A, r_B, v)
print(f"OLS móvil {v:>3} días error medio: {np.mean(np.abs(est[m] - beta_real[m])):.3f}")Kalman (q = 1e-4) error medio: 0.064 OLS móvil 30 días error medio: 0.101 OLS móvil 60 días error medio: 0.077 OLS móvil 120 días error medio: 0.061 OLS móvil 250 días error medio: 0.080
El filtro de Kalman queda a la altura de la mejor regresión móvil, no muy por encima. Y no es casualidad: el parámetro \( q \) hace el mismo papel que la longitud de la ventana. Un \( q \) grande equivale a una ventana corta (reacciona rápido, pero con ruido); uno pequeño, a una ventana larga (estable, pero lenta):
for q in [1e-6, 1e-5, 1e-4, 1e-3]:
b, _ = kalman_beta(r_A, r_B, q=q)
print(f"q = {q:.0e} error medio: {np.mean(np.abs(b[m] - beta_real[m])):.3f}")q = 1e-06 error medio: 0.155 q = 1e-05 error medio: 0.077 q = 1e-04 error medio: 0.064 q = 1e-03 error medio: 0.086
Entonces, ¿para qué sirve? Sus ventajas reales no son la precisión mágica, sino otras: se actualiza con cada dato sin guardar ventanas, trata igual todos los datos sin el efecto de un día extremo que entra y sale de golpe de la ventana, da una medida de incertidumbre y se generaliza a muchas variables a la vez (varias betas, una constante, tendencias). Y cuidado: igual que la ventana, \( q \) se puede sobreajustar. Elígelo con datos de formación, nunca mirando el resultado final.
Cómo se usa en pairs trading
En la estrategia del artículo de pairs trading, el filtro sustituye al hedge ratio fijo:
- Cada día, el filtro actualiza \( \beta_t \) con el dato nuevo.
- El error de predicción \( e_t = r^A_t - \beta_{t-1} r^B_t \), dividido por su desviación típica \( \sqrt{S_t} \), es directamente una señal tipo z-score: ya viene normalizada por el propio filtro.
- La posición en B se ajusta a la beta del día, así la cartera sigue siendo neutral aunque la relación cambie.
En la versión con precios en lugar de rentabilidades, el estado incluye también una constante, \( (\beta_t, \alpha_t) \), y las ecuaciones son las mismas con matrices en lugar de números. La lógica no cambia.
Más allá del hedge ratio
- Betas de mercado dinámicas para cubrir una cartera frente a su índice.
- Tendencias y niveles ocultos: estimar el «precio justo» de un activo filtrando el ruido.
- Modelos de tipos de interés de varios factores, que se calibran con filtros de Kalman.
- Volatilidad estocástica, con variantes no lineales del filtro.
Resumen
- El filtro de Kalman estima un estado oculto que cambia repitiendo el ciclo predecir–medir–corregir.
- La ganancia de Kalman decide cuánto creer al dato nuevo frente a la estimación previa.
- Para una beta dinámica cabe en diez líneas de Python y da, además, su incertidumbre.
- No es magia: \( q \) hace de ventana y rinde como la mejor regresión móvil. Su ventaja está en ser online, suave, generalizable y en medir su propia incertidumbre.
Con una beta que se adapta, tu estrategia es más robusta, pero también tienes un parámetro más que ajustar. Por eso el siguiente paso es fundamental: por qué tu backtest miente.
Fuentes
- R. E. Kalman (1960), «A New Approach to Linear Filtering and Prediction Problems», Journal of Basic Engineering.
- E. P. Chan (2013), Algorithmic Trading: Winning Strategies and Their Rationale, Wiley (capítulo sobre el filtro de Kalman en pairs trading).