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