DEV Community

Jesus Corredor
Jesus Corredor

Posted on

Tres errores entre mi backtest y la verdad (y la verdad estaba en el P&L real)

Diario de un bot que opera con dinero real — Entrada #4
En la entrada anterior cerré con una promesa: contar cómo mi herramienta de backtest —la que usaba para decidir si una estrategia servía— me había mentido. Según sus números, tenía un "edge" fabuloso… justo en un horario en el que el bot ni siquiera operaba.

Lo primero que tengo que decirte es que esa frase era falsa. El horario nunca fabricó el edge. Pero para descubrirlo tuve que atravesar una historia de tres errores encadenados, donde cada corrección destapaba otro fallo más profundo. Y al final, el único dato que nunca mintió fue el que menos miraba: el P&L real del bot.

No resistió el primer chequeo. Ni el segundo. Ni el tercero.

El punto de partida: un edge que parecía real

Mi bot de futuros operaba varios pares con apalancamiento 3x y un filtro de régimen. El backtest cubría cuatro de ellos, y los números que yo tenía a la vista a mediados de agosto eran prometedores:


Factor de beneficio cerca de 1.3 en los tres mejores. Para cualquiera que haya corrido backtests, eso se ve como una estrategia que funciona.

Pero había un detalle que no cerraba: el bot real, operando en vivo con esta misma estrategia, no ganaba. Entre fines de mayo y agosto cerró con unos +3 USDT antes de comisiones, y unos −8 USDT después de pagarlas. No +30%, no +70%. Plano, tirando a rojo.

Esa brecha —backtest brillante, realidad plana— fue el hilo que empecé a tirar.

Error 1: el .env que nadie leyó

Mi primera hipótesis, en agosto, fue el horario. "A lo mejor el bot solo opera en ciertas horas y el backtest las ignora." Así que miré el código:

Ahí estaba: el bot operaba de 06 a 22 UTC. El backtest no tenía filtro horario. Esa era la causa, pensé. Agregué el filtro al backtest para "corregirlo", y las cifras se desplomaron: BCH pasó de +69% a −31%. "Ajá —me dije—, el edge era un espejismo del horario." Problema resuelto. Tan resuelto que lo anuncié como el tema de esta entrada.

Estaba equivocado. Lo que acababa de hacer era alejar el backtest de la realidad, no acercarlo.

Porque ese or "true" es un valor por defecto del código. Y mi .env de producción —el archivo que de verdad configura lo que corre— decía otra cosa:

TRADING_HOURS_ENABLED=false

El bot no tenía horario. Operaba las 24 horas. Lo confirmé donde no se puede discutir: en Binance. De 260 órdenes de apertura entre el 10 de junio y el 16 de agosto, 88 se abrieron fuera del horario 06-22 UTC. ETH a las 03:55. BCH a las 03:32. Una de cada tres aperturas fue de madrugada.

Había leído lo que el código decía por defecto, no lo que el sistema hacía de verdad. Es exactamente la trampa del artículo #2 —getenv("X") or default— pero esta vez no estaba en el bot: estaba en mi forma de analizarlo. Mi "corrección" era el error.

Error 2: dos ventanas de tiempo distintas, comparadas como si fueran la misma

Corregí el rumbo: si el bot operaba 24h, el backtest fiel era el de 24h, sin filtro. Volví a la tabla que había usado para comparar… y encontré el segundo nudo.

La columna "24h" que yo había tomado como referencia —la de la tabla de arriba— no era de la corrida de agosto. Era de un reporte de tres semanas antes, de otro período. Sin darme cuenta, había enfrentado una ventana de ≈abril-julio contra otra de ≈mayo-agosto, como si midieran lo mismo.

Peras contra manzanas, presentadas en la misma tabla como si fueran peras contra peras. Cualquier conclusión sacada de ahí estaba viciada desde el origen, antes de discutir horarios o cualquier otra cosa.

Rehíce todo sobre el mismo período, con la versión de 24h, la fiel al bot. Y el edge no solo seguía ahí: era más grande. BCH +100.7%, BNB +54.6%, ETH +53.6%, con factores de beneficio entre 1.39 y 1.55. (BTC, para que no falte, perdía: −23.9%.) El horario nunca había fabricado el edge.

Entonces, ¿por qué el bot real no ganaba?

Error 3: el costo que no escalaba

El tercer nudo no era nuevo. Lo había cazado el 20 de agosto, en otro experimento, y lo había corregido para esas pruebas. Lo que nunca hice fue volver a pasar por esa corrección los números de esta historia.

El bot operaba con apalancamiento 3x. Cuando abres una posición 3x, los costos —comisiones, slippage— se cobran sobre el tamaño apalancado, no sobre tu capital. Mi backtest cobraba los costos como si no hubiera apalancamiento. Cada trade, en el modelo, era tres veces más barato de lo que era en la realidad.

Recalculé con el costo bien escalado. Y aquí viene la parte que casi cuento mal: el costo solo no mató el edge. Lo partió a la mitad. Pero lo que quedaba seguía viéndose bien:


Faltaba el último chequeo, el que me obligo a hacer siempre: quitar los 3 mejores trades de cada par y ver si la estrategia sigue en pie.


Break-even. En la práctica, nada.

Y ahí, por fin, todo encajó con lo que decía la cuenta: el bot real ganó un poco operando y lo perdió pagando comisiones. La estrategia no tenía edge. Lo que el backtest mostraba como ventaja eran tres o cuatro trades afortunados por par, inflados por un costo que el modelo cobraba a un tercio de su valor. El bot real llevaba meses diciéndome la verdad; eran mis backtests los que estaban mal contados.

Como confirmación, en agosto había probado variantes simplificadas de la estrategia fuera de la muestra. Las dos que parecían robustas dentro de la muestra colapsaron fuera de ella, con factor de beneficio por debajo de 0.9. No había edge que rescatar.

La parte que de verdad importa

Fíjate en el orden de los villanos, porque es contraintuitivo:

El horario no fabricó el edge. (Mi primera hipótesis: falsa.)
El costo mal modelado, solo, tampoco lo destruía: BCH seguía en +57%.
Quitar los mejores trades, solo, tampoco: sin ellos BCH seguía en +30%.
Lo que mataba el edge eran las dos cosas juntas. Un costo subestimado que inflaba cada trade, y una ganancia que dependía de un puñado de operaciones con suerte. Cada grieta por separado dejaba un resultado presentable. Juntas, dejaban cero.

Ese es el error más traicionero del backtesting, porque el backtest funciona: corre, da números, y los números son bonitos. Nada avisa que están mal. Y si corriges una sola cosa, el número sigue siendo bonito, y te convences de que ya revisaste. La única señal de alarma fue la realidad —el P&L del bot en vivo— y durante semanas la ignoré porque confiaba más en mi herramienta que en mi cuenta.

La lección

Un backtest no vale por lo que mide, sino por qué tan fielmente mide lo que de verdad corre en producción. Mi análisis falló en tres dimensiones: configuración equivocada (leí el default, no el .env), ventana equivocada (mezclé dos períodos), costo equivocado (ignoré el apalancamiento). Y sobre las tres, una cuarta que no es un bug sino una tentación: aceptar un resultado sin preguntar cuántos trades lo sostienen.

Y hay una lección dentro de la lección, la que más me costó tragar. En agosto llegué a la conclusión correcta —no hay edge, el bot se pausa— por la razón equivocada. Y esa razón equivocada la publiqué, con total convicción, como adelanto de esta entrada. Si no hubiera verificado antes de escribir, hoy estarías leyendo una historia falsa sobre un horario fantasma.

Cuando tu backtest y tu cuenta real no coinciden, no es la cuenta la que se equivoca. Es el backtest. La realidad no tiene bugs. Tu modelo de la realidad, sí —y a veces hacen falta varias pasadas para encontrarlos todos.

En la próxima entrada cambio de bot. Mi otro bot, el de spot, creía haber cerrado posiciones que seguían abiertas: vendía al aire, anotaba la venta y me avisaba que todo estaba en orden. La fuga silenciosa.

¿Alguna vez tu backtest te mostró un edge que la realidad desmintió? Cuéntalo en los comentarios.

Top comments (2)

Collapse
 
arhancanli profile image
Arhan Canli •

Buen cierre con "la realidad no tiene bugs". Un par de cosas que comprobé con tus números: 88 de 260 aperturas fuera de 06-22 UTC es 33,8 %, o sea "una de cada tres" cuadra. Y la brecha en vivo es +3 USDT antes de comisiones contra −8 después, es decir unos 11 USDT de comisiones sobre 260 aperturas.

Un punto que no mencionas: BCH +100,7 %, BNB +54,6 % y ETH +53,6 % son los tres mejores de cuatro pares. Elegir los mejores de cuatro ya infla el resultado antes de tocar costos o quitar los 3 mejores trades. Si el bot opera los cuatro, la cifra que importa es la del conjunto, no la de los ganadores.

Sobre el costo con apalancamiento: además de comisiones y slippage sobre el nominal, en perpetuos el funding también se cobra sobre el nominal 3x y depende del tiempo en posición. ¿Lo incluiste en la corrida corregida, o solo comisión y slippage?

Collapse
 
jesusaco profile image
Jesus Corredor •

Hola Arhan, agradezco tu comentario. Confirmo tus cálculos: 33,8 % cuadra con “una de cada tres”, y sí, esos ~11 USDT de comisiones sobre 260 aperturas son justo la brecha entre el +3 bruto y el −8 neto.

Sobre los tres mejores de cuatro: toda la razón. BCH, BNB y ETH eran los positivos; el cuarto, BTC, perdía (−31,3 % con el costo escalado, −51,9 % al quitarle sus tres mejores trades). Mostré el podio y no el conjunto, y el conjunto es peor. Lo cual, si lo pienso, refuerza la conclusión en lugar de salvarla: la estrategia daba cero incluso seleccionando a su favor. Gracias por la nota, me lo llevo como corrección.

Y en el funding diste en el clavo: el backtest no lo modelaba. Solo descontaba comisión y slippage sobre el nominal 3x (2 × (fee + slippage) × leverage), nada de funding. Así que fui a medirlo en vez de suponerlo: bajé las tasas de funding históricas reales de Binance para cada par y las apliqué trade por trade, multiplicadas por el 3x y con el signo según la dirección de cada posición —porque el bot iba long y short, y el funding cobra o paga según el lado—. Resultado: resta entre 0,2 y 1,3 puntos por par. BCH +56,7 → +54,8 %, BNB +38,4 → +36,8 %, ETH +34,5 → +34,2 %, BTC −31,3 → −31,5 %.

Es decir, el funding era un cuarto costo que me faltaba, tenías razón en señalarlo, pero empuja en la misma dirección que todo lo demás. No rescata el edge; lo hunde un poco más. Con el costo escalado y sin los top-3 trades, con o sin funding, queda en break-even. Que es justo lo que decía el P&L real del bot desde el principio.