PythonEstadísticaMachine LearningBacktestingSeries temporales

Dos formas en que un backtest te miente (y cómo evitarlas)

Publicado el 2026-07-17 · Xiliux

Pruebas una estrategia, o un modelo, sobre datos históricos. El backtest da un número bonito. Y luego, en real, no aparece. Casi siempre es una de estas dos ilusiones — y las dos se descartan con muy poco código.

Empaqueté las dos correcciones como librería: honest-eval, Python puro, sin dependencias. Salieron de un bot de trading, pero el rigor no tiene nada de específico al trading.

Ilusión 1: el modelo vio el futuro

Partir los datos con el clásico train_test_split aleatorio es correcto para datos independientes. En una serie temporal es un desastre silencioso: mete muestras de mañana en el conjunto de entrenamiento, y el modelo "predice" en el test cosas que en producción todavía no habrían pasado. La métrica sale inflada, y confías en un edge que no existe.

El test honesto es siempre el futuro: el tramo más reciente en el tiempo.

from honest_eval import temporal_split

train_idx, test_idx = temporal_split(timestamps, test_frac=0.20, embargo=24)
X_tr, X_te = X[train_idx], X[test_idx]

Devuelve índices, así lo aplicas a numpy, pandas o listas por igual.

El embargo cierra una fuga más sutil: si tu etiqueta mira h pasos adelante, una muestra de entrenamiento a menos de h del corte ya conoce parte del resultado del test. embargo=h descarta ese borde. La métrica baja — pero por fin es la real out-of-sample.

Ilusión 2: la variante ganó por suerte

Tienes varias variantes y quieres la mejor. Eliges la de mayor media. Error: con pocas muestras, eso premia la varianza, no la ventaja. La variante más ruidosa suele quedar arriba por azar.

Dos correcciones, ambas dentro de select_best_variant:

Aparear. Mide variante y baseline sobre el mismo ensayo y trabaja con δ = variante − baseline. La varianza común del ensayo se cancela en la resta, y te quedas con la señal.

Exigir cota inferior de confianza > 0. Gradúa una variante solo si media − z·SE > 0: "incluso siendo pesimista dentro del margen de confianza, sigue por encima del baseline".

from honest_eval import select_best_variant

variants = {
    "chandelier": [0.8, 1.1, -0.2, 0.9, 1.0, 0.7],
    "momentum":   [2.0, -1.5, 3.0, -0.5, 1.2, -1.0],   # media alta, muy dispersa
}

elegido = select_best_variant(variants, z=1.6449, min_effective_n=5)
politica = elegido.name if elegido else "baseline"

momentum puede tener la media más alta y aun así no graduar: su dispersión hunde la cota inferior. Eso es exactamente lo que quieres que pase — es el mecanismo que rechaza al ganador de suerte.

Cuando lo reciente pesa más

Si el proceso cambia con el tiempo, pondera por recencia. El tamaño de muestra efectivo de Kish (Σw)² / Σw² evita que unas pocas muestras muy pesadas se hagan pasar por muchas:

from honest_eval import halflife_weight

weights = [halflife_weight(now - t, halflife=7*86400) for t in exit_ts]
variants = {"chandelier": (deltas, weights)}

No crean señal — dejan de fabricarla

Ninguna de las dos inventa edge: eso son datos y buenas features. Lo que hacen es dejar de fabricarlo donde no lo hay. Son el mínimo para no mentirte antes de arriesgar algo real.

Es software libre. Desde agosto de 2026 el código no se publica en registros ni en GitHub —trabajamos en seguridad y que nos extraigan el fuente sería un argumento en contra del producto—: se entrega a pedido, con firma y SHA-256, en contacto@xiliux.com. En honest-eval están la descripción y cómo solicitarlo.

Preguntas frecuentes

¿Por qué el train_test_split aleatorio está mal en una serie temporal?

Es correcto para datos independientes, pero en una serie temporal mezcla en silencio muestras de mañana en el entrenamiento, así que el modelo 'predice' en el test cosas que en producción aún no habrían pasado. La métrica se infla y confías en un edge que no existe. El test honesto es siempre el futuro: el tramo más reciente en el tiempo.

¿Para qué sirve el embargo?

Cierra una fuga más sutil: si tu etiqueta mira h pasos adelante, una muestra de entrenamiento a menos de h del corte ya conoce parte del resultado del test. embargo=h descarta ese borde. La métrica baja, pero por fin es real.

Mi variante ganó al baseline en el backtest, ¿el edge es real?

Puede que solo ganara por ruido. Un gate de significancia apareado te dice si la diferencia sobrevive, usando el tamaño de muestra efectivo (n_eff de Kish, porque tus muestras están correlacionadas) y una cota inferior de confianza — no solo una estimación puntual. Si no pasa el gate, no arriesgues dinero.

¿Esto es solo para trading?

No. Las dos ilusiones —fuga del futuro y ganadores por ruido— aparecen en cualquier evaluación sobre datos correlacionados o con orden temporal. honest-eval salió de un bot de trading, pero el rigor no tiene nada de específico al trading.

← Ver más artículosCotizar un proyecto