Filtro de Kalman en trading con Python: hedge ratio dinámico | Quantum Club

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
  1. La intuición: predecir, medir y corregir
  2. El modelo en dos ecuaciones
  3. Unos datos con la relación cambiando
  4. El filtro de Kalman en Python, desde cero
  5. ¿Es mejor que una regresión móvil? Comparación honesta
  6. Cómo se usa en pairs trading
  7. Más allá del hedge ratio
  8. Resumen

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:

  1. Predecir. «Si nada ha cambiado, la beta de hoy es la de ayer, aunque ahora estoy algo menos seguro.»
  2. Medir. Llega un dato nuevo y comparas lo que esperabas ver con lo que ves.
  3. 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:

Python
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 variable

El filtro de Kalman en Python, desde cero

Con un solo parámetro que estimar, el filtro cabe en diez líneas:

Python
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.

0 200 400 600 800 1000 Días (datos simulados) 0,4 0,6 0,8 1,0 1,2 1,4 1,6 Hedge ratio β Regresión móvil de 60 días Filtro de Kalman (±2 desv.) Beta real
La beta real cambia de 0,8 a 1,2. El filtro de Kalman (con su banda de ±2 desviaciones) la sigue dato a dato; la regresión móvil de 60 días también, con algo más de ruido.

¿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:

Python
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):

Python
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:

  1. Cada día, el filtro actualiza \( \beta_t \) con el dato nuevo.
  2. 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.
  3. 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).
Quantum Pablo

Sobre Quantum Pablo

Físico de partículas y quant. Enseño finanzas cuantitativas en español desde la intuición y con código: más de 400 alumnos en más de 25 países se han formado en Quantum Club. También en YouTube e Instagram.

Etapa 3 de 4 · Nivel hedge fund

  1. 10Optimización de carteras de Markowitz y frontera eficiente en Python
  2. 11El modelo GARCH: cómo predecir la volatilidad (con Python)
  3. 12Pairs trading y cointegración en Python: de la idea al backtest
  4. 13El filtro de Kalman en trading: un hedge ratio que se adapta (con Python)
  5. 14Por qué tu backtest miente: sobreajuste y cómo evitarlo
  6. 15La cópula gaussiana y la crisis de 2008: la fórmula que hundió Wall Street
Ver el recorrido completo →