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.

Top comments (0)