Quant500 Academy

1. Alinear datos y señales sin mirar al futuro

Nivel 6: De los datos a la decisión

Los cinco niveles anteriores te han dejado piezas sueltas: precios de un proveedor con identificadores estables, un histórico ajustado por splits y dividendos, una rutina de limpieza que detecta lo imposible y un almacén que se actualiza sin duplicar filas. Este módulo es donde esas piezas se juntan en una sola tabla, y es también donde se comete el error más caro del oficio, porque es el único que no lanza ninguna excepción: la unión sale perfecta, el backtest se ejecuta, y las rentabilidades salen preciosas porque la tabla contiene cosas que en aquella fecha no sabía nadie.

Tres relojes que no marcan la misma hora

Una estrategia mínimamente seria mezcla series con frecuencias distintas. Los precios son diarios y llegan al cierre de la sesión. Los fundamentales son trimestrales, pero tienen dos fechas que no coinciden: el fin del periodo que describen y el día en que la empresa los presentó. El macro es mensual o trimestral, llega con semanas de retraso y además se revisa después de publicado, a veces mucho.

Ese desfase no es un detalle administrativo. En Estados Unidos, una empresa grande dispone de unos cuarenta días naturales tras el cierre del trimestre para presentar su informe trimestral y de unos sesenta para el anual; las empresas más pequeñas tienen plazos aún más largos. Si tu tabla pega el beneficio del primer trimestre a la fila del 31 de marzo, le estás regalando a la estrategia entre cinco y ocho semanas de ventaja sobre el resto del mercado, justo en el tramo donde se concentra la reacción al anuncio.

La unión hacia atrás es la única honesta

La operación correcta no es una unión por clave exacta ni un relleno hacia adelante sobre un calendario diario. Es una unión por aproximación hacia atrás: para cada fila de precios, coge la última observación cuya fecha de publicación sea anterior o igual a la de esa fila.

import pandas as pd

precios = pd.read_parquet("precios_diarios.parquet")
# columnas: fecha, simbolo, apertura, cierre_ajustado
fundamentales = pd.read_parquet("fundamentales_trim.parquet")
# columnas: simbolo, fin_periodo, fecha_publicacion, bpa, patrimonio_neto

# La clave temporal de la union NUNCA es fin_periodo: es el dia en que el dato fue publico.
precios = precios.sort_values("fecha")
fundamentales = fundamentales.sort_values("fecha_publicacion")

panel = pd.merge_asof(
    precios,
    fundamentales,
    left_on="fecha",
    right_on="fecha_publicacion",
    by="simbolo",                       # empareja dentro de cada empresa
    direction="backward",               # solo mira hacia atras
    allow_exact_matches=True,           # el dia de la presentacion ya cuenta
    tolerance=pd.Timedelta("400D"),     # un trimestre viejo no se arrastra eternamente
)

Tres detalles que la gente pasa por alto. Uno: merge_asof exige que ambas tablas estén ordenadas por su clave temporal global, no dentro de cada grupo. Dos: si una empresa presenta tarde, el panel arrastrará un trimestre antiguo durante semanas, y eso es exactamente lo correcto, porque era lo único conocido. Y tres: la tolerancia evita que una empresa que dejó de reportar mantenga vivo un dato de hace tres años.

Remuestrear mal no da error, da rentabilidad

El atajo que arruina más backtests es este, y a primera vista parece razonable:

# MAL: reindexar por fin de periodo y propagar hacia adelante.
malo = (fundamentales
        .set_index("fin_periodo")
        .groupby("simbolo")["bpa"]
        .resample("D")
        .ffill())
# La fila del 31 de marzo ya trae el beneficio del primer trimestre,
# que no se conocio hasta finales de abril. Cuarenta dias de futuro, cero avisos.

El mismo problema aparece al alinear macro: si tomas la serie de PIB con su fecha de referencia y la extiendes hacia atrás con un remuestreo, tu estrategia sabrá en enero cómo terminó el trimestre. Y hay una variante más sutil: cualquier transformación que mire la serie entera de golpe, como normalizar por la media y la desviación de todo el histórico o rellenar huecos con interpolación, mete futuro en cada fila sin que ninguna fecha esté mal.

Señal, decisión y ejecución son tres fechas

Aunque la unión sea impecable, queda el segundo desfase: entre saber algo y cobrarlo. Son tres momentos distintos y confundirlos cuesta dinero:

panel = panel.sort_values(["simbolo", "fecha"])

# 1) Señal: se calcula con lo disponible hasta el cierre de t.
panel["senal"] = panel.groupby("simbolo")["cierre_ajustado"].pct_change(60)

# 2) Decision: la señal del cierre de t se convierte en orden esa noche,
#    asi que la posicion existe a partir de la apertura de t+1.
panel["posicion"] = panel.groupby("simbolo")["senal"].shift(1)

# 3) Ejecucion: cobras el tramo que va de la apertura de t a la de t+1.
panel["ret_apertura"] = panel.groupby("simbolo")["apertura"].pct_change()
panel["pnl"] = panel["posicion"] * panel.groupby("simbolo")["ret_apertura"].shift(-1)

Si tu operativa no puede tocar la apertura, hace falta un desplazamiento más, y si además pasas órdenes a mano, cuenta el retraso real y no el ideal. La comprobación práctica es brutalmente simple: cuando dudes del sentido de un shift, prueba el otro. Si el resultado mejora, el que mejora es el equivocado. Los errores de alineamiento tienen una firma inconfundible, y es que siempre caen a tu favor.

Caso trabajado: cuántos días de futuro te estás colando

Haz la cuenta antes de discutir. Fundamentales trimestrales, cuatro presentaciones al año, mediana de unos cuarenta días naturales entre el fin del periodo y la presentación. Si unes por fin de periodo, tu tabla contiene un dato que aún no era público durante 4 × 40 = 160 días de cada 365: cerca del 44% del histórico. Y no son 160 días cualesquiera, son precisamente los que rodean al anuncio de resultados, donde una acción grande se mueve de forma habitual varios puntos porcentuales en una sola sesión.

Cuando corriges esto, lo normal no es una mejora ni un ajuste fino: es que la rentabilidad anualizada se desplome y la curva pierda su suavidad sospechosa. Marcos López de Prado lleva años señalando el sesgo de anticipación como una de las causas principales de que los backtests no se parezcan a la realidad; conviene tratar esa caída como la primera buena noticia del proyecto, porque es la primera cifra que describe el mercado y no tu hoja de cálculo.

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.