Look-Ahead Bias: cómo un error de una vela fabrica un Sharpe de 15 a partir de puro ruido
Parte de la serie "Backtests sin ilusiones".
📄 Este artículo se convirtió en un paper de investigación. Tres fugas sutiles de look-ahead se someten a una prueba controlada contra una verdad conocida (4.000 historias simuladas). Lea el paper en línea (versión interactiva + PDF) en lookahead.marketmaker.cc, código y datos en github.com/suenot/lookahead-inflation.
Hace unas semanas nuestro benchmark de búsqueda de parámetros nos estaba mintiendo, y casi no lo notamos.
El motor parecía limpio. Lógica de vela cerrada, una división walk-forward rodante honesta, una búsqueda Sobol/QMC sobre el espacio de parámetros, una ventana de test reservada. La búsqueda encontró configuraciones que se veían bien in-sample. El único problema: fuera de muestra, casi todo era negativo. Asumimos que la estrategia simplemente era débil.
Entonces encontramos una línea. La señal se decidía al cierre de la vela i, pero la ejecución se registraba en la misma vela i en lugar de en la apertura de la siguiente vela. Un error de índice de uno en la ejecución. Movimos la ejecución a open[i+1] — el único precio al que realmente se podía operar tras ver el cierre de la vela i — y el resultado fuera de muestra invirtió el signo. La búsqueda Sobol pasó de pérdida a ganancia. Nada de la estrategia había cambiado. Simplemente habíamos dejado de operar en el pasado.
Eso es look-ahead bias, y lo inquietante es lo pequeño que fue el error y lo grande que fue la consecuencia. Este artículo es una autoauditoría controlada: construimos un simulador donde la verdad se conoce por construcción, inyectamos las fugas sutiles una a una y medimos exactamente cuánto infla cada una el backtest. El titular: con ningún edge real en absoluto, una ejecución en la misma vela fabrica un Sharpe anualizado de +14,8 a partir de puro ruido.
Qué es realmente el look-ahead bias

El look-ahead bias es cualquier punto de su pipeline donde una decisión o una medición usa información que no habría estado disponible, en tiempo real, en el momento en que se usa. Los ejemplos de libro de texto son burdos — usar las ganancias anuales completas de una acción en enero, o una reformulación que aún no se había publicado. Esos son fáciles de detectar. Los que sobreviven a una revisión de código son sutiles, y se esconden en tres lugares:
- Ejecución — decide en la vela
iy ejecuta en la velai(o usa el máximo/mínimo de la velaipara stops en la misma vela que generó la señal). Opera a un precio que está correlacionado con lo que le activó. - Normalización — hace z-score, min-max o escala de otro modo un feature usando estadísticas calculadas sobre toda la serie, incluyendo el futuro. El escalador "conoce" el conjunto de test.
- Indicadores / features — suaviza o filtra con una ventana centrada (o que de algún modo mira hacia adelante), de modo que el valor en la vela
iya contiene un fragmento de la velai+1.
Las tres son formas de lo que la literatura de machine learning llama leakage: la contaminación del entrenamiento/evaluación con información del futuro del objetivo (Kaufman et al., 2012; Kapoor & Narayanan, 2023). En finanzas el tratamiento canónico es Advances in Financial Machine Learning de López de Prado (2018) — validación cruzada purgada, embargo, los peligros del backtesting. La disciplina point-in-time se remonta al menos a Fama & French (1992), que deliberadamente retrasan los datos contables seis meses para que la variable se conozca antes del retorno que explica.
La pregunta que responde este artículo es cuantitativa: no "¿es malo el leakage?" (todos están de acuerdo), sino "¿cuántos puntos de Sharpe compra cada forma, y cuáles son peligrosas?" Sin un número no se puede razonar sobre ello. No se puede saber si una inflación de +0,3 es ruido o una inflación de +14 es una prueba irrefutable.
Un simulador con verdad conocida

Para medir la inflación hay que conocer la verdad. Los datos reales nunca revelan la verdad — dan una realización y ningún oráculo. Así que construimos un mercado sintético donde nosotros fijamos el edge.
El proceso generador de datos es estrictamente causal y no explosivo:
Aquí es una deriva latente persistente exógena (un AR(1) con ), y el retorno de la vela tiene una pequeña deriva que se conoce una vela por adelantado. Como no depende de retornos pasados, no hay realimentación y nada explota. El parámetro es el dial de cuánto edge real existe:
- — el nulo: ningún edge en absoluto. Cualquier Sharpe positivo del backtest es 100% artefacto.
- — un edge real y operable: una regla de momentum honesta realmente gana dinero.
La estrategia es deliberadamente simple — una regla de signo de momentum. El feature es la suma de retornos de las últimas velas ( velas), y la posición es su signo:
csum = np.concatenate(([0.0], np.cumsum(r))) # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]
signal = np.sign(mom) # position for the next bar
Este feature de momentum es el vehículo perfecto para estudiar la fuga de misma-vela, porque tiene una propiedad que comparten los indicadores reales: contiene mecánicamente la vela actual. mom[t] incluye r[t]. Así que si registra r[t] como su operación, está apostando en parte a una cantidad que ya está dentro de su propia señal. Esa es la fuga, hecha concreta.
Configuración: (1% de volatilidad por vela), una comisión de una dirección de 0,00045 (round-trip 0,09%, correspondiendo a nuestro motor), Sharpe anualizado por (velas horarias), 4.000 historias independientes de 4.000 velas cada una. Todo está sembrado (seeded) y es determinista.
El pipeline honesto (el único operable)
Decidir al cierre de la vela t, ganar el retorno de la siguiente vela, pagar comisiones en los cambios de posición:
def sharpe(sig, ret_booked):
dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
pnl = sig * ret_booked - FEE_ONEWAY * dpos
return pnl.mean() / pnl.std() * np.sqrt(8760)
honest = sharpe(signal[idx], r[idx + 1]) # earn r[t+1]: tradable
Las tres fugas, cada una un único cambio quirúrgico
same_bar = sharpe(signal[idx], r[idx])
z_full = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])
z_sm = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0 # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])
Cada fuga está a una línea de distancia del pipeline honesto. Ese es el punto clave: no son errores exóticos, son el tipo de cosa que pasa una revisión de código.
Resultados: la magnitud de cada fuga

Ejecutado en 4.000 semillas, aquí está el Sharpe anualizado que reporta cada pipeline, bajo el nulo (sin edge) y bajo un edge real (, ajustado para que el Sharpe honesto sea un creíble +1,57):
| Pipeline | Nulo (sin edge) | Edge real |
|---|---|---|
| Honesto (la verdad) | −0,74 | +1,57 |
| Ejecución misma vela | +14,79 | +15,85 |
| Vistazo de indicador (1 vela) | +4,76 | +6,62 |
| Normalización de toda la serie | −0,84 | +1,46 |
Los intervalos de confianza del 95% a través de las semillas son de ±0,05 o más estrechos en cada celda; las pruebas t pareadas sobre la inflación son astronómicamente significativas donde el efecto es real (t > 400, p ≈ 0).
Lea primero la columna nula, porque es el experimento más limpio posible: no hay edge, así que el pipeline honesto correctamente pierde dinero (−0,74, el lastre de pagar comisiones por operar ruido). Ahora vea qué le hacen las fugas a esa misma nada:
- Ejecución misma vela: −0,74 → +14,79. Una estrategia sin poder predictivo alguno, operando ruido aleatorio, reporta un Sharpe anualizado de casi 15. No es un sesgo sutil; es una fabricación. El mecanismo es exactamente el que construimos: el feature de momentum contiene
r[t], así que registrarr[t]es apostar a la propia señal. - Vistazo de indicador: −0,74 → +4,76. Dejar que el suavizador mire una vela hacia el futuro fabrica un Sharpe cercano a 5 a partir de ruido, porque el valor suavizado en
tahora correlaciona con elr[t+1]que está a punto de ganar. - Normalización de toda la serie: −0,74 → −0,84. Prácticamente ninguna inflación. Este es el hallazgo honesto y no obvio (más sobre esto abajo).
La columna de edge entrega el mensaje más insidioso. Cuando existe un edge real (honesto +1,57), las fugas no solo suman una constante — empujan el Sharpe medido a +15,85 y +6,62, muy por encima del +1,57 que realmente se podría operar. Así que el número medido no puede distinguir habilidad de fuga. Un +6 con fuga y un +6 honesto se ven idénticos en el reporte. Solo se descubre cuál de los dos se tenía después de haber desplegado capital.
La fuga es un gradiente, no un interruptor

Una objeción natural: "registrar la vela de señal entera es un error extremo e irrealista." Así que barrimos la dosis — la fracción de la vela de señal capturada por la fuga, de 0 (honesto) a 1 (misma vela completa):
| Fracción capturada | Sharpe nulo | Sharpe con edge |
|---|---|---|
| 0,00 (honesto) | −0,74 | +1,57 |
| 0,25 | +3,90 | +6,41 |
| 0,50 | +9,86 | +12,20 |
| 1,00 (fuga total) | +14,79 | +15,85 |
Capturar solo un cuarto de la vela de señal lleva a una estrategia sin edge de −0,74 a +3,90. No hace falta el error completo para ser engañado; una ejecución que es ligeramente demasiado favorable — un toque de slippage optimista en la vela de señal, un stop intrabar comprobado contra la misma vela que lo activó — basta para superar la mayoría de los umbrales "desplegables". La inflación es suave y monótona respecto a cuánto del presente se permite operar.
¿Con qué frecuencia esto pone en producción una estrategia perdedora?
El número que debería preocupar a un practicante es la tasa de despliegue falso: con qué frecuencia una fuga hace que una configuración verdaderamente perdedora supere el umbral que se usaría para autorizarla. Usando "Sharpe anualizado ≥ 1,0" como criterio de despliegue, bajo el nulo:
- Ejecución misma vela: el 68% de las estrategias sin edge parecen desplegables y pierden dinero de verdad. Dos de cada tres configuraciones de puro ruido pasarían una compuerta Sharpe-≥-1 y perderían dinero en vivo. (Esta tasa está definida limpiamente aquí porque la fuga está puramente en la ejecución — la contraparte honesta es la misma señal con una ejecución honesta.)
- Vistazo de indicador: empuja prácticamente a toda configuración sin edge por encima del umbral de despliegue también (el 99,9% supera Sharpe ≥ 1) — llevaría ruido directo a producción.
- Normalización de toda la serie: el 12% supera el umbral — esencialmente la tasa base de ruido, sin prima de leakage.
La taxonomía, y cómo detectar cada una
Las tres fugas no son igual de peligrosas, y las diferencias son instructivas.
1. Leakage de ejecución (la costosa)

Síntoma: el precio de ejecución está correlacionado con la señal porque vienen de la misma vela. Magnitud: enorme (+15 desde ruido a dosis completa, +3,9 a un cuarto de dosis). Por qué es la peor: su señal está, casi por definición, construida a partir de la acción de precio reciente, así que el retorno de la vela de señal es exactamente aquello con lo que su feature está más correlacionado. Registrarlo es casi como buscar la respuesta.
Detección — la prueba de desplazamiento de una vela. Este es el diagnóstico más valioso de este artículo. Tome su backtest y desplace cada ejecución una vela más tarde (decida en i, ejecute en open[i+1]). Si el resultado apenas se mueve, su ejecución era honesta. Si el resultado se derrumba o invierte el signo, estaba operando en el pasado. Esto es precisamente lo que le pasó a nuestra búsqueda Sobol: desplace las ejecuciones, y un OOS "rentable" resultó ser una pérdida — o mejor dicho, la relación real salió a la luz una vez eliminada la fuga.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Leakage de indicador / feature (la silenciosa)
Síntoma: un indicador en la vela i depende de datos de i+1 o posteriores — una media móvil centrada, un filtro sin retardo causal, una etiqueta de pico/valle que necesita velas futuras para confirmarse, una transformación tipo Heikin-Ashi alimentada con velas futuras. Magnitud: grande (+4,8 desde ruido). Por qué se esconde: la fuga está enterrada dentro de una llamada a una biblioteca. scipy.signal.filtfilt es de fase cero — y fase cero significa no causal. Un feature de "esta vela es un máximo local" es incognoscible hasta que se imprime la siguiente vela.
Detección: para cada indicador, pregunte ¿cuál es el índice más alto que lee? Si calcular el valor en t toca alguna vez t+1, es no causal. Calcule los indicadores sobre una ventana causal expansiva/rodante y verifique que el valor en la vela t sea idéntico exista o no velas posteriores a t en el array. (Nuestras implementaciones de HMA/ADX pasan esta prueba: cada salida en t lee solo entradas en ≤ t.)
3. Leakage de normalización (la específica del canal)
Síntoma: un escalador (StandardScaler, min-max, un z-score global) se ajusta sobre todo el conjunto de datos, incluido el de test. Las advertencias canónicas de ML son explícitas al respecto — el §7.10.2 de Elements of Statistical Learning de Hastie, Tibshirani & Friedman ("the wrong and right way to do cross-validation"), y la propia guía de errores comunes de scikit-learn: "the average should be the average of the train subset, not the average of all the data."
Magnitud en nuestra prueba: ≈ cero (−0,74 → −0,84). Este es el resultado sorprendente y honesto, y vale la pena entenderlo en lugar de memorizarlo.
¿Por qué no infló? Porque nuestra estrategia usa el feature solo a través de su signo (un umbral cero). El escalado por desviación estándar nunca cambia un signo, y el centrado por media global solo desplaza ligeramente el cruce por cero. Así que la estandarización de toda la serie de una regla de signo puro es casi inocua.
No generalice esto en exceso. El leakage de normalización es específico del canal. En el momento en que su estrategia use la magnitud del feature — tamaño de posición proporcional a un z-score, un umbral de entrada distinto de cero elegido observando la distribución escalada, una red neuronal que consume entradas estandarizadas —, el escalador consciente del futuro empieza a importar, y importa más cuanto más difieran las estadísticas globales de las causales. Nuestro resultado no es "el leakage de normalización es seguro." Es "la magnitud del leakage depende del canal por el que la cantidad filtrada entra en la decisión, y hay que medirla en lugar de asumirla." Una regla de signo es el único caso en que esta fuga concreta sale barata.
Dónde se conecta esto
El look-ahead bias es el primer eslabón de una cadena que esta serie ha estado documentando:
- Corrompe el input de la validación. Un backtest con fuga navegará sin problemas por una división walk-forward y se verá como una meseta amplia en lugar de un pico de overfit — la fuga es consistente a través de los folds, así que la validación cruzada no puede detectarla. El leakage es el modo de fallo anterior al overfitting, y ninguna cantidad de validación honesta posterior lo salvará.
- Interactúa con la búsqueda de parámetros: una búsqueda de miles de trials sobre datos con fuga encontrará la configuración que explota la fuga más agresivamente. El "ganador" es el peor infractor.
- Es la razón por la que la paridad backtest-live diverge. Una fuga es la explicación más limpia para una brecha del 30–50% entre el backtest y el bot, porque el trading en vivo es, mecánicamente, el único lugar donde no se puede espiar.
La disciplina que atrapa todo esto es la misma que la literatura académica ha estado exigiendo durante años: tratar un backtest como un experimento estadístico con una frontera de información estricta. Bailey, Borwein, López de Prado & Zhu mostraron con cuánta facilidad el overfitting fabrica rendimiento falso (2014); el protocolo de backtesting de Arnott, Harvey & Markowitz (2019) codifica esta higiene. El look-ahead bias es la frontera más básica de todas — la frontera en el tiempo — y la más barata de violar por accidente.
Conclusiones

- El look-ahead bias es cuantitativamente enorme y cualitativamente invisible. Un único error de ejecución de una vela convirtió un Sharpe de −0,74 (puro ruido, perdiendo correctamente) en +14,79. El error es una línea; la consecuencia es un historial fabricado.
- Es un gradiente. Capturar incluso el 25% de la vela de señal produce +3,90 de la nada. No hace falta un bug flagrante — un poco de optimismo de más en las ejecuciones basta.
- El número medido no puede distinguir habilidad de fuga. Cuando existe un edge real, las fugas inflan el reporte muy por encima de la verdad operable. La única defensa es el proceso, no la métrica.
- La prueba de desplazamiento de una vela es su diagnóstico más rápido. Mueva cada ejecución una vela más tarde. Si el rendimiento se derrumba, estaba operando en el pasado.
- La magnitud del leakage es específica del canal. Las fugas de ejecución e indicador son devastadoras; la normalización de toda la serie de una regla de signo es casi gratuita. Mida la fuga a través del canal por el que realmente entra — no lo asuma.
El estudio controlado completo — las tres fugas, el barrido de dosis, el análisis de despliegue falso, los métodos formales y cada número reproducible a partir de un único script determinista — está en el paper complementario en lookahead.marketmaker.cc, con código y datos en github.com/suenot/lookahead-inflation.
La estrategia de nuestro experimento nulo no tenía ningún edge en absoluto. Aun así mostró un Sharpe de 15. Si su backtest se ve demasiado bueno, lo primero que hay que sospechar no es su genio — es su reloj.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.