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