Hace unos meses eliminé un par de trading de mi bot de futuros. O eso creÃa.
La semana pasada, durante una auditorÃa de rutina, encontré ese mismo par abriendo posiciones, reservando capital, y bloqueando a los demás. No lo habÃa resucitado. Nunca se habÃa ido.
Esta es la primera de una serie de historias reales sobre un sistema que opera con dinero de verdad — donde los bugs no son un test que falla, sino capital moviéndose. Empiezo por esta: un bug silencioso que vivió meses en producción, y por qué "eliminar" algo de una forma frágil es peor que no eliminarlo.
El sÃntoma
Estaba revisando los logs del bot cuando una lÃnea me detuvo:
[PAR-X] SNAP | ... | cap=135.34 | lev=3× | pos=FLAT
Ese par no debÃa estar ahÃ. Lo habÃa sacado de la operación hacÃa meses. Y sin embargo, ahà estaba, con un capital asignado de ~$135 que yo no recordaba haber configurado. Peor: un par de ciclos después, otro par intentó abrir una posición y falló:
[PAR-X] ENTRY SKIP | USDT insuficiente: free=130.98 < needed=138.04
El par fantasma estaba acaparando capital, y por su culpa los pares que sà debÃan operar se quedaban sin fondos. Un componente que creÃa muerto competÃa por recursos con los vivos.
La investigación
Lo primero fue ir a la fuente de configuración. El bot lee el capital de cada par desde un archivo de entorno (.env). Busqué la variable de ese par:
VacÃo. La variable no existÃa en el .env. Entonces, ¿de dónde salÃan los $135?
La respuesta estaba en el código. El bot arma su lista de pares desde un diccionario de configuración, y cada par lee su capital asÃ:
Ahà estaba el problema. os.getenv("PAR_X_USDT") devuelve None cuando la variable no existe. Y None or "133" evalúa a "133". El bot, al no encontrar la variable en el .env, caÃa a un valor por defecto hardcodeado de $133 que alguien (yo, meses atrás) habÃa dejado en el código como fallback.
La causa raÃz — y aquà está la lección real
El bug no era solo el fallback. Era cómo habÃa "eliminado" el par meses atrás.
En su momento, para desactivarlo, habÃa puesto su capital en cero en el .env (PAR_X_USDT=0). Funcionó… hasta que un dÃa restauré un backup del código por otra razón. Ese backup era anterior a mi cambio, o el .env se sobrescribió en el proceso. La variable desapareció. Y sin la variable, el código volvió a caer al fallback de $133.
El par nunca estuvo eliminado de verdad. SeguÃa en el diccionario de configuración que el bot recorre en cada ciclo. Yo solo le habÃa bajado el capital por una vÃa frágil — una variable de entorno — que un restore podÃa borrar. La eliminación real habrÃa sido sacarlo del diccionario de configuración, en el código. No lo hice. Y el sistema, silenciosamente, lo revivió.
El fix
La corrección definitiva fue eliminar el bloque del par directamente del diccionario de configuración en el código, no del .env. Con el protocolo de siempre:
Verificar que el par no tuviera posición abierta (revisar su archivo de estado).
Backup del archivo con timestamp.
Eliminar el bloque de configuración.
Validar la sintaxis (python3 -c "import ast; ast.parse(...)").
Confirmar con grep que el par ya no aparecÃa en ningún lado.
Reinicio manual.
Tras el reinicio, el banner de arranque lo confirmó: el bot ahora recorrÃa solo los pares correctos. El fantasma se habÃa ido de verdad.
El cuerpo del delito
Quedó un detalle que cierra la historia. Tras eliminar el par, su archivo de estado local quedó huérfano en el directorio. Lo abrÃ. Ahà seguÃa, congelado, con su última sesión registrada:
initial_capital: 133. La evidencia fÃsica del fallback, escrita en disco. El par habÃa estado operando todo ese tiempo con un capital que ningún archivo de configuración le asignó — solo un valor por defecto olvidado en una lÃnea de código.
La lección para llevarte
Antes de las lecciones técnicas, la pregunta que me perseguÃa mientras lo corregÃa: ¿cómo sobrevivió esto a las auditorÃas anteriores? HabÃa revisado este bot varias veces. ¿Cómo no lo vi?
La respuesta me pareció más valiosa que el bug en sÃ. No lo detecté antes porque estaba auditando el código, y el código estaba bien. El fallback or "133" no es un error de sintaxis, ni una excepción, ni nada que un grep de "bug" encuentre. Es una lÃnea perfectamente válida que hace exactamente lo que dice — el problema no vivÃa en el código, vivÃa en la brecha entre el código y la configuración. El .env decÃa una cosa (nada), el código asumÃa otra (un default), y ninguna revisión de un solo lado podÃa ver la contradicción, porque cada lado, por separado, era correcto.
El bug solo era visible cruzando ambos contra lo que el sistema hacÃa en vivo — leyendo los logs de ejecución real y preguntándose "¿por qué este par tiene capital si yo no se lo di?". Lo encontré no porque revisara mejor el código, sino porque miré lo que el bot realmente estaba haciendo y no cuadraba con lo que yo creÃa haber configurado.
Esa es la lección de fondo: una auditorÃa que solo lee el código estático nunca atrapa un bug de configuración fantasma. Los sistemas mienten por omisión. La verdad no está en el código ni en la config por separado, sino en el comportamiento observado. Si algo opera distinto de como crees haberlo configurado, esa discrepancia es el bug, aunque cada archivo por separado se vea impecable.
De ahÃ, tres trampas concretas que me llevo:
Los defaults hardcodeados son deuda oculta. Un os.getenv("X") or "133" parece defensivo, pero convierte la ausencia de configuración en un valor silencioso. Si la variable desaparece, el sistema no se detiene ni avisa: sigue con un número que nadie decidió.
Desactivar por la vÃa frágil no es desactivar. Bajar un valor en un .env es reversible por accidente — un restore, una sobrescritura, un despliegue. Si algo debe estar fuera, sácalo de la estructura que el sistema recorre, no de un parámetro que la alimenta.
Un backup puede resucitar tus bugs. Restaurar código viejo no solo revierte features; revierte fixes. Cada restore deberÃa venir con la pregunta: ¿qué correcciones posteriores estoy deshaciendo con esto?
El par fantasma no me costó dinero — estaba en FLAT cuando lo encontré. Pero pudo haberlo hecho. Y lo más inquietante no es el bug en sÃ, sino cuánto tiempo vivió sin que ninguna auditorÃa lo viera. A veces lo más peligroso no es lo que falla ruidosamente, sino lo que funciona en silencio haciendo algo que no autorizaste.
En la próxima entrada desarmo la lÃnea exacta que causó todo esto ese inocente os.getenv("X") or default — y te muestro por qué es una trampa que probablemente tienes en tu propio código ahora mismo. Incluido el caso que casi me muerde a mÃ: cómo intentar apagar algo poniéndolo en cero puede encenderlo.
¿Te ha pasado encontrar un componente haciendo algo que juraste haber desactivado? Cuéntamelo en los comentarios — colecciono estas historias.














