Una lupa suspendida sobre un bloque denso de texto de terminal en verde brillante sobre una pantalla oscura, con marcas de aviso en rojo resaltando unas pocas líneas concretas del código, y fragmentos de velas japonesas desenfocados de fondo
Ilustración propia generada con IA (Adobe Firefly) — no representa datos reales.

Por qué los backtests mienten: 5 bugs reales, encontrados antes de tocar el test

Antes de que el torneo de 2.255 estrategias tocara un solo dato de la partición de validación, un auditor externo leyó línea a línea el motor de simulación completo. Encontró 5 bugs reales — dos de ellos habrían invalidado el experimento entero sin dar ningún aviso visible.

Ningún resultado de backtest vale más que la auditoría que lo sostiene, y la inmensa mayoría de los backtests que circulan por internet no pasan por ninguna. El torneo de estrategias de trading que ya contamos en la pieza anterior de esta serie sí lo hizo, en el peor momento posible para descubrir un error: justo antes de gastar el único disparo de test que el protocolo permitía. Este artículo es el catálogo completo de lo que encontró esa auditoría, y por qué dos de los cinco hallazgos son exactamente el tipo de fallo silencioso que hace que un backtest "funcione" sin que nadie se entere de que está mintiendo.

Por qué se audita antes de mirar los resultados, no después

Línea de tiempo con cuatro etapas: motor construido y probado en train, auditoría externa línea a línea con 2 hallazgos de severidad alta y 3 de severidad media o baja, selección sobre validación ya con el motor corregido, y test como disparo único al final
Diagrama propio a partir del registro de cambios verificado del experimento — el orden importa tanto como los hallazgos en sí.

Los dos hallazgos que habrían invalidado todo el experimento

El primero es el más peligroso de los cinco porque no habría producido ningún error visible. La función que carga el conjunto de candidatas para seleccionar ganadoras tenía un valor por defecto que, con un simple descuido de una línea (olvidar pasar el argumento de qué partición usar), podía cargar en silencio datos de la partición de entrenamiento en vez de la de validación. La selección de ganadoras se habría hecho igual, con números igual de plausibles, sobre datos que ya se habían usado para calibrar el propio motor. Se corrigió quitando ese valor por defecto (ahora es obligatorio indicarlo) y añadiendo dos comprobaciones independientes que verifican que los datos cargados pertenecen de verdad a la partición pedida: una en la función que carga, otra en la función que selecciona, precisamente porque esta última no puede asumir que el conjunto que recibe ya viene limpio.

El segundo tiene que ver con cómo Python compara valores indefinidos. Cuando una estrategia apenas opera en algún activo, su ratio de Sharpe puede quedar indefinido, y no era un caso raro: 1.600 de las 2.400 filas de una de las cinco familias de estrategias (dos de cada tres) en la partición real de entrenamiento tenían este problema. Sin una regla explícita, un valor indefinido en la comparación de candidatas rompía el orden del ranking de forma silenciosa, y un solo candidato vecino con ese problema podía descartar a un ganador legítimo como si fuera un resultado catastrófico. Se ratificaron dos reglas distintas a propósito: un candidato con el problema en cualquier activo queda inelegible sin más (nunca compite con datos parciales), pero un vecino con el mismo problema se excluye del cálculo sin vetar al candidato que sí tiene datos completos. Dos situaciones distintas que, antes del fix, se trataban exactamente igual.

Bugs más pequeños, mismos principios

No todos los hallazgos tenían la misma gravedad, y no todos se resolvieron de la misma forma. Una de las cinco familias de estrategias reajusta el tamaño de su posición casi a diario sin cambiar de signo, y el motor, antes del fix, trataba cualquier cambio de tamaño como si fuera cerrar la operación y abrir una nueva, reseteando el precio de referencia del stop-loss cada vez. El resultado práctico: el stop nunca llegaba a estar a más de un día del precio de ayer, muy lejos de calcularse desde el precio real de entrada, como exigía el diseño. Corregido, la única familia afectada por este problema pasó de registrar una mediana de 318 operaciones a 58. La señal de que el stop había estado disparándose por su propio reajuste de tamaño, no por movimientos reales del precio.

Los otros dos hallazgos no tocaron ni una línea de código. Uno era un comentario en el motor que prometía que una orden de toma de beneficios se ejecutaba "a su nivel o mejor" (como una orden límite real), cuando el código siempre la ejecutaba exactamente al nivel, incluso con el precio abriendo más favorable. Más conservador que lo que el texto describía, nunca al revés. Se corrigió el comentario, no el comportamiento: es el sesgo correcto, y cambiar el código para aprovechar ese hueco habría introducido una ventaja que el resto del diseño evita deliberadamente en cualquier otro punto. El último hallazgo documentó dos casos límite de cómo se procesa una reversión de posición el mismo día que se dispara un stop: ambigüedades reales, sin ningún efecto sobre el resultado final, que se dejaron por escrito en vez de disimular que nunca habían existido.

La verificación no se quedó en un solo informe

El propio auditor volvió sobre el código ya corregido para comprobar que los arreglos funcionaban de verdad, y no solo que sonaban razonables sobre el papel. Reprodujo el ataque original del hallazgo más grave (construir a propósito un conjunto de datos que mezclara las dos particiones) contra la versión corregida, y confirmó que ahora el proceso se detiene con un error explícito antes de seleccionar nada, en vez de continuar en silencio. Encontró además, de paso, un hueco menor que no había llegado a documentar como hallazgo en la primera pasada, y confirmó que también estaba resuelto. Dos notas quedaron anotadas sin bloquear nada: una sobre versionar los resultados de validación para que futuras regeneraciones se puedan comparar por diferencia exacta, y otra reconociendo un límite real de lo que se pudo verificar de forma directa frente a lo que quedó cubierto solo por los tests automáticos.

Qué tienen en común estos cinco bugs

Ninguno de los cinco fue el tipo de error que un backtest "revienta" con un mensaje de error evidente. Los cinco son fallos silenciosos: producen un número, ese número parece razonable, y solo una lectura línea a línea del código, no un vistazo a los resultados, los saca a la luz. Es exactamente la clase de error que un backtest publicado sin auditoría nunca detecta, porque el resultado final no avisa de que algo fue mal calculado. La mayoría de las estrategias de trading que circulan con un backtest "impresionante" detrás nunca pasan por este tipo de revisión adversarial. No porque nadie quiera hacerlo, sino porque encontrar estos bugs es trabajo caro y aburrido, y no cambia el titular si el resultado ya "se ve bien".

En NodeWitness no auditamos el backtest de nuestro propio Score con este mismo procedimiento adversarial línea a línea. Sería deshonesto dar a entender que sí. Lo que tiene es una disciplina relacionada: los datos que alimentan el backtest quedan congelados la primera vez que se calculan, para que una fuente externa no pueda reescribir el pasado en silencio, y cada intento interno de mejorar la fórmula se verifica contra casos reales conocidos antes de adoptarse. Ninguno lo ha superado todavía. Son salvaguardas distintas contra el mismo problema de fondo: que un número que "se ve bien" no es lo mismo que un número correcto.

El siguiente artículo de esta serie cubre uno de los hallazgos con más peso en el veredicto final: por qué la pata corta pierde incluso cobrando el funding. Puedes leer primero el veredicto completo del torneo o consultar el Score de ciclo en vivo de NodeWitness, con la misma disciplina de publicar también lo que no funciona.

Última actualización: 9 de agosto de 2026