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.

pip install honest-eval

Repo, tests (con la matemática anclada a mano y verificada por mutación) y detalle: github.com/isazajuancarlos/honest-eval.

← Ver más artículosCotizar un proyecto