Quant500 Academy

2. Datos en el momento correcto y el sesgo de anticipación

Nivel 2: Los sesgos que rompen un backtest

El sesgo de anticipación es el más humillante de todos, porque no requiere ninguna sofisticación para cometerlo y produce los resultados más espectaculares que verás nunca. Consiste en una sola cosa: en la fecha T tu modelo usa información que en la fecha T no existía todavía. Una línea de código mal alineada basta, y el síntoma no es un error, es un Sharpe de cuatro.

Cuatro vías por las que se cuela el futuro

Alinear un fundamental con su fecha de publicación

La herramienta correcta es una unión por proximidad temporal, con la clave puesta en la fecha en que el dato se hizo público y nunca en la fecha del periodo al que se refiere.

import pandas as pd

fund = fund.sort_values("fecha_publicacion")
precios = precios.sort_values("fecha")

panel = pd.merge_asof(
    precios,
    fund[["fecha_publicacion", "ticker", "bpa_ttm"]],
    left_on="fecha",
    right_on="fecha_publicacion",   # NUNCA fecha_periodo
    by="ticker",
    direction="backward",           # solo lo ya publicado
    tolerance=pd.Timedelta("400D"), # descarta datos rancios
)

Tres detalles importan. direction igual a backward garantiza que cada fila de precio solo pueda engancharse con publicaciones anteriores o iguales. El argumento by evita que los datos de un emisor se filtren a otro. Y tolerance impide arrastrar indefinidamente un dato de hace tres años como si siguiera vigente. Si cambias right_on a la fecha del periodo, el código sigue funcionando sin quejarse y el backtest mejora: esa mejora es la fuga.

Un solo día de desplazamiento cambia el resultado entero

Este es el fragmento que más veces se rompe, y el que más veces pasa desapercibido porque parece impecable.

media = precio.rolling(20).mean()
senal = (precio < media).astype(int)
retornos = precio.pct_change()

# MAL: la senal de hoy cobra el retorno de hoy
pnl_malo = senal * retornos

# BIEN: decides al cierre de hoy y cobras el movimiento de manana
pnl_bueno = senal.shift(1) * retornos

print(pnl_malo.mean() / pnl_malo.std() * 252 ** 0.5)
print(pnl_bueno.mean() / pnl_bueno.std() * 252 ** 0.5)

La versión sin desplazar no es un poco mejor: suele ser otra estrategia. La razón es que la señal está construida sobre el precio del mismo día, de modo que multiplicarla por el retorno de ese día equivale a comprar solo los días que ya sabes que van a cerrar por debajo de su media. Es reversión a la media con la respuesta delante. En estrategias de horizonte corto es habitual ver cómo un Sharpe holgadamente superior a dos se queda por debajo de la mitad al insertar ese único shift.

Conviene entender por qué el efecto es tan brutal siendo tan pequeño el desfase. La correlación entre el retorno de un día y una señal construida con ese mismo retorno es enorme por pura aritmética, mientras que la correlación entre el retorno de mañana y cualquier señal de hoy es diminuta: en acciones líquidas, unas centésimas. Estás comparando una identidad contable con una predicción real. Por eso el desplazamiento no matiza el resultado, lo sustituye. Y por eso, cuando alguien te enseñe una curva espectacular, la primera pregunta no es qué indicador usa, sino dónde está el shift.

Normalizar es una decisión con fecha

# MAL: media y desviacion de toda la serie, futuro incluido
z_malo = (factor - factor.mean()) / factor.std()

# BIEN: solo con lo conocido hasta cada fecha
media_exp = factor.expanding(min_periods=252).mean()
desv_exp = factor.expanding(min_periods=252).std()
z_bueno = (factor - media_exp) / desv_exp

Que la variable objetivo no aparezca en ninguna de las dos líneas hace que la primera parezca inofensiva. No lo es: el z-score de una fecha cualquiera depende de todos los valores posteriores, y si el factor tuvo un régimen distinto al final de la muestra, la normalización global reparte esa información hacia atrás. Lo mismo vale para winsorizar por percentiles globales, para escalar por la volatilidad de toda la serie y para cualquier ajuste que lea la muestra completa antes de partirla.

Caso real: por qué seis meses de margen

En el trabajo empírico serio esto se resuelve con un margen deliberadamente generoso. Fama y French, al construir la cartera de valor que sirve de referencia a media profesión, emparejan los datos contables del ejercicio cerrado en el año anterior con las rentabilidades que empiezan en julio del año siguiente. Es decir, imponen una separación mínima de seis meses entre el cierre contable y el momento en que ese dato puede influir en una decisión, muy por encima de los plazos legales de presentación de cuentas. No lo hacen por pereza: lo hacen porque cualquier margen menor obliga a demostrar, empresa por empresa, que la información estaba realmente disponible, y porque las bases de datos contables reescriben cifras cuando hay reformulaciones.

Ese es el criterio que deberías copiar. Si no puedes justificar con una fecha concreta que el dato existía, retrásalo hasta que sí puedas. Un backtest con margen de sobra que da un resultado modesto es información útil; un backtest ajustado al límite que da un resultado extraordinario no es información de ninguna clase.

Para ampliar esta lección

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.