Un backtest produce un número, y los números son persuasivos. Ese es todo el problema. Cuando una simulación está sutilmente equivocada, no se rompe ni te avisa. Te entrega un resultado un poco mejor que la realidad y te deja actuar en consecuencia.
Simular una toma de ganancias fija es fácil: el precio alcanzó tu nivel o no lo alcanzó. Simular una entrada o salida basada en indicadores es donde se vuelve difícil, porque ahora la simulación tiene que responder una pregunta mucho más delicada: ¿qué sabía realmente el bot en ese momento?
Aquí van tres formas en que respondimos mal esa pregunta, y lo que nos enseñó cada una.
Puntos clave
- El bug clásico es leer un indicador de marco temporal superior antes de que su vela haya cerrado. Un RSI de 4 horas consultado en la apertura de la vela arrastra cuatro horas de acción del precio que todavía no habían ocurrido.
- El arreglo es aritmética, no heurística: un valor de un marco temporal T solo es utilizable legítimamente una vez que T ha transcurrido, así que desplaza la consulta hacia atrás por (marco temporal del disparador menos marco temporal de la simulación).
- Los motores en vivo tienen el mismo problema con otro disfraz. Un indicador calculado sobre una vela que aún se está formando repinta, lo que significa que una condición puede ser verdadera y luego falsa en la misma vela.
- Un simulador que omite en silencio lo que no puede reproducir es más permisivo que el bot en vivo, y la diferencia siempre favorece al backtest.
- Lo honesto es mostrar la diferencia en el resultado en lugar de esconderla. Un backtest que te dice dónde está mintiendo vale más que uno que se ve impecable.
1. El bug de anticipación que se esconde en las estrategias de múltiples marcos temporales
Este es el más fácil de escribir y el más difícil de notar.
Digamos que la simulación avanza una vela de 5 minutos a la vez, y la estrategia tiene una condición sobre un RSI de 4 horas. En cada paso de 5 minutos, el motor necesita un valor para ese RSI. La implementación ingenua busca la vela de 4 horas que contiene el momento actual y lee su valor.
Eso es una máquina del tiempo.
Una vela de 4 horas que abrió a las 12:00 no tiene un valor de RSI final hasta que cierra a las 16:00. Si la simulación está parada en las 12:05 y lee esa vela, está leyendo un número calculado a partir de cuatro horas de acción del precio que, desde el punto de vista de las 12:05, todavía no han ocurrido. La estrategia parece entrar justo antes de movimientos que no podría haber visto venir, y la curva de capital se ve excelente.
El arreglo no es una heurística. Cae por su propio peso cuando escribes cuándo es realmente conocible un valor. Una vela que abre en t en un marco temporal de triggerMs solo es conocible en t + triggerMs. La simulación toma su decisión al cierre de la vela actual, candleOpenTime + simulationMs. Así que el valor es utilizable legítimamente exactamente cuando:
t + triggerMs <= candleOpenTime + simulationMs
t <= candleOpenTime - (triggerMs - simulationMs)
Lo que significa que la consulta correcta es la que ya tenías, ejecutada contra una marca de tiempo desplazada hacia atrás por triggerMs - simulationMs.
Vale la pena señalar dos propiedades de esa expresión, porque son las que la hacen segura de aplicar en todas partes:
- Las condiciones del mismo marco temporal reciben un desplazamiento de cero. Un indicador de 5 minutos leído al cierre de una vela de 5 minutos es su propio valor basado en el cierre, que siempre fue correcto. Solo se mueven los marcos temporales genuinamente superiores.
- El desplazamiento se deriva, no se configura. Viene de la propia vela, no de un ajuste que alguien tenga que acordarse de establecer.
La lección general es una que aplicaría a cualquier simulación, no solo al trading: cuando no tengas claro si tienes sesgo de anticipación, escribe la marca de tiempo en la que cada dato se vuelve conocible y compárala con la marca de tiempo en la que lo usas. El sesgo es lo que queda cuando te saltas ese paso y confías en la intuición, y la intuición es mala en esto porque leer «la vela actual de 4 horas» suena completamente razonable.
2. El mismo bug en código en vivo lleva otro disfraz
Pensarías que este problema pertenece al backtesting. No es así. El motor en vivo también lo enfrenta, y ahí se llama repintado.
En cualquier momento, la vela más reciente todavía se está formando. Un indicador calculado sobre una ventana que termina en esa vela cambiará a medida que la vela cambie. Una condición puede ser verdadera a las 12:01, falsa a las 12:03, y verdadera de nuevo a las 12:04, todo dentro de la misma vela, sin que el mercado haga nada inusual. Si el bot actúa según la primera de ellas, actuó según un valor que ya no existe.
Nuestra opción por defecto es descartar la vela que aún se está formando y calcular solo sobre velas confirmadas, para que una señal nunca repinte. Eso también es lo que hace que el modo en vivo y el backtest coincidan, ya que la simulación por construcción trabaja con velas cerradas.
Pero «nunca repintar» no es gratis. Esperar la confirmación significa actuar más tarde, y para algunas estrategias esa tardanza cuesta más que el ruido que evita. Así que esto se expone como una elección por condición: evaluar en la vela confirmada, o evaluar en vivo y aceptar que el valor puede moverse bajo tus pies.
El punto de diseño es la parte que conservaría: cuando un compromiso es genuinamente un compromiso, no lo entierres en un valor por defecto y finjas que es un hecho. Haz que la opción segura sea la predeterminada, y luego deja que el usuario opte por la otra sabiendo lo que hace. Lo que no debes hacer es dejar que los dos modos difieran entre el modo en vivo y el backtest, porque entonces el usuario está comparando dos bots distintos.
3. Lo que el simulador no puede reproducir es más peligroso que lo que se equivoca
Los dos primeros son bugs de corrección. Este es un problema de diseño, y es el que creo que está más subestimado.
Algunas condiciones no se pueden reproducir. Una señal de webhook externa es el caso más claro: no hay registro histórico de lo que un sistema externo habría enviado a las 14:20 de algún día del pasado marzo. La simulación sencillamente no puede saberlo.
Entonces, ¿qué debería hacer? La respuesta tentadora es omitir esa condición y evaluar el resto. Eso es lo que hace que el resultado se vea completo. También es donde ocurre el daño, y vale la pena explicar el mecanismo:
Si una estrategia requiere A Y B, y B no se puede simular, entonces omitir B deja a A decidiendo sola. El bot simulado entra en condiciones en las que el bot en vivo se habría quedado fuera. La simulación es sistemáticamente más permisiva que la realidad.
Fíjate en la dirección. El error no es aleatorio. Descartar una condición bajo lógica Y solo puede producir más operaciones que la realidad, nunca menos, así que el sesgo apunta a un resultado de mejor apariencia todas y cada una de las veces. Esa asimetría es lo que lo hace peligroso: el ruido aleatorio se promedia a lo largo de muchas ejecuciones, y un sesgo unidireccional no.
Elegimos seguir ejecutando la simulación en lugar de negarnos, porque un resultado parcial con una diferencia conocida todavía es útil. Pero entonces la diferencia tiene que ser visible en la salida, no enterrada en un log. El resultado lleva una advertencia explícita, en el propio idioma del usuario, con un nombre que dice qué pasó: «Algunas Condiciones de Entrada No Simuladas». Hay una equivalente para las salidas.
Esa decisión es la parte de este sistema que defendería con más ganas. Un simulador que no puede fallar en voz alta fallará en silencio, y el fallo silencioso en algo que produce números es el peor modo de fallo que existe, porque la gente actúa según los números y no actúa según los stack traces.
Qué llevarte de esto
Si estás construyendo cualquier cosa que reproduzca el histórico para producir un número:
- Cada dato necesita una marca de tiempo de «conocible en». Si no puedes enunciarla, no puedes descartar el sesgo de anticipación.
- Revisa la dirección de tus errores, no solo su tamaño. Un error que siempre apunta hacia el mismo lado se acumula; uno que apunta al azar se promedia. El optimismo unidireccional es la firma del sesgo de anticipación.
- Haz que el simulador y el sistema en vivo compartan su lógica de decisión siempre que puedas. Dos implementaciones de «¿es verdadera esta condición?» divergirán, y te enterarás por un usuario en lugar de por una prueba.
- Reporta lo que no pudiste hacer. La credibilidad de un resultado viene de sus límites declarados, no de su pulcritud.
El resumen incómodo es que un backtest es una afirmación sobre un contrafactual, y los contrafactuales son fáciles de equivocar en silencio. La meta de ingeniería no es un simulador que nunca mienta. Es uno que te diga dónde está mintiendo.
