<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jesus Corredor</title>
    <description>The latest articles on DEV Community by Jesus Corredor (@jesusaco).</description>
    <link>https://dev.to/jesusaco</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089844%2F43b302f3-0c51-4566-95bd-2b72f6657850.png</url>
      <title>DEV Community: Jesus Corredor</title>
      <link>https://dev.to/jesusaco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jesusaco"/>
    <language>en</language>
    <item>
      <title>La fuga silenciosa: el bot que creía haber vendido</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:18:10 +0000</pubDate>
      <link>https://dev.to/jesusaco/la-fuga-silenciosa-el-bot-que-creia-haber-vendido-nfp</link>
      <guid>https://dev.to/jesusaco/la-fuga-silenciosa-el-bot-que-creia-haber-vendido-nfp</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #5&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El antecedente&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En 2012, Knight Capital perdió 440 millones de dólares en 45 minutos. No fue un mal trade: fue código en producción haciendo algo distinto de lo que sus dueños creían.&lt;/p&gt;

&lt;p&gt;A mi bot, un error de esa misma familia me costó 71 centavos. Pero me tomó dieciséis días descubrirlo. Ese es el verdadero peligro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cambio de bot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hasta aquí, la serie fue sobre el bot de futuros. En la entrada anterior lo pausé: no tenía edge, y cuando até los cabos del backtest, apagarlo fue lo único sensato.&lt;/p&gt;

&lt;p&gt;Pero yo tenía un segundo bot corriendo en paralelo. Uno de spot. Y mientras desenredaba el lío del de futuros, este otro me estaba mintiendo de una forma distinta.&lt;/p&gt;

&lt;p&gt;No con números inflados. En silencio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El síntoma&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Una noche de agosto revisé mi cuenta de Binance. No buscaba nada en particular. Y ahí estaban: posiciones de ETH y BNB abiertas. Vivas.&lt;/p&gt;

&lt;p&gt;El bot juraba haberlas cerrado días atrás. Cada una tenía su línea de venta en el log, su mensaje de Telegram, su fila en el Excel con la ganancia calculada.&lt;/p&gt;

&lt;p&gt;Binance opinaba distinto.&lt;/p&gt;

&lt;p&gt;No era una fuga. Eran tres: BNB el 5 de julio, ETH el 19 de julio, BNB el 31 de julio. La de ETH quedó viva dieciséis días.&lt;/p&gt;

&lt;p&gt;Dieciséis días expuesto al mercado, creyendo que estaba cerrado. Si ETH hubiera dado un salto brusco, me habría enterado por el saldo, no por el bot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El disparador&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La causa era mínima: la comisión.&lt;/p&gt;

&lt;p&gt;Binance cobraba el 0.1% de cada compra en la misma moneda que comprabas. Si el bot compraba 0.032 ETH, en la cuenta quedaban 0.031968 —el resto se lo llevó la comisión—.&lt;/p&gt;

&lt;p&gt;Al cerrar, el bot mandaba vender 0.032. Los que había pedido, no los que tenía.&lt;/p&gt;

&lt;p&gt;Binance respondía con un rechazo: -2010, saldo insuficiente.&lt;/p&gt;

&lt;p&gt;Un rechazo es un rechazo. El problema fue lo que el bot hizo con él.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué era silenciosa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ante el rechazo, el bot no se inmutaba. Hacía lo mismo que ante una venta exitosa:&lt;/p&gt;

&lt;p&gt;escribía EXIT STOP_DYNAMIC en el log;&lt;br&gt;
mandaba "VENTA" por Telegram;&lt;br&gt;
sumaba la fila al Excel con su PnL;&lt;br&gt;
y marcaba la posición como cerrada.&lt;/p&gt;

&lt;p&gt;Cuatro confirmaciones de éxito. Para una operación que había fallado.&lt;/p&gt;

&lt;p&gt;Y lo peor: la fuga se tapaba sola. La siguiente vez que vendía ese par, la venta sí funcionaba —el saldo "olvidado" de la vez anterior cubría justo el faltante—. Cada fuga nueva borraba la huella de la anterior.&lt;/p&gt;

&lt;p&gt;La única prueba estaba donde nadie mira: el USDT libre. Después de una venta real, sube. Después de una fantasma, no se mueve.&lt;br&gt;
05-jul  (fantasma):  141.06 → 141.06&lt;br&gt;
19-jul  (fantasma):  166.69 → 166.69&lt;br&gt;
31-jul  (fantasma):  116.86 → 116.86&lt;br&gt;
31-jul  (real):       20.51 →  70.27&lt;/p&gt;

&lt;p&gt;El bug en una línea&lt;/p&gt;

&lt;p&gt;El problema estaba en la función que hablaba con Binance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x8s1xiacegsoslgc9aa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x8s1xiacegsoslgc9aa.png" alt=" " width="628" height="165"&gt;&lt;/a&gt;&lt;br&gt;
Reintentaba ante rate limits y errores de servidor. Para todo lo demás, devolvía el JSON. Y un 400 con insufficient balance es "todo lo demás".&lt;/p&gt;

&lt;p&gt;Los handlers que recibían esa respuesta solo preguntaban si venía vacía:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc55p7hzf5rosgqlqmln5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc55p7hzf5rosgqlqmln5.png" alt=" " width="605" height="60"&gt;&lt;/a&gt;&lt;br&gt;
Un rechazo de Binance no viene vacío. Tiene su code, su msg. Así que if resp: daba verdadero. El bot confundió "Binance me respondió" con "Binance hizo lo que pedí".&lt;/p&gt;

&lt;p&gt;Es la misma trampa del artículo del getenv, pero peor. Allá leí un valor por defecto. Acá el error venía escrito, clarísimo —insufficient balance—, y mi código lo leyó como un éxito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La parte incómoda&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Al reconstruir el costo real, descubrí algo peor que el bug: mis propias notas estaban mal.&lt;/p&gt;

&lt;p&gt;Para la fuga de ETH había anotado una pérdida de −8.29 USDT. La real era −0.27. Me equivoqué por un factor de treinta, usando el costo promedio de la app en vez del precio real de entrada. Y encima había inventado un supuesto "rastro del problema" en mayo que nunca existió.&lt;/p&gt;

&lt;p&gt;El bot me mintió reportando éxitos falsos. Yo, al investigarlo apurado, amplifiqué la mentira. Dos capas de datos falsos sobre el mismo hecho.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que de verdad costó&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En dinero, casi nada: −0.71 USDT en total.&lt;/p&gt;

&lt;p&gt;El costo real fue otro: dieciséis días operando a ciegas, creyendo que la cuenta estaba en un estado que no era el real. Si en esos días hubiera decidido comprar, retirar o rebalancear, lo habría hecho con información falsa.&lt;/p&gt;

&lt;p&gt;El costo no fueron 71 centavos. Fue la confianza en que mi registro era real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El fix (y por qué no está terminado)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esa misma madrugada lo arreglé: _retry() ahora exige un código 200 ≤ status &amp;lt; 300, y una verificación extra exige que la orden quede FILLED. Si la venta falla, salta una alerta y la posición sigue marcada como abierta —que es la verdad—.&lt;/p&gt;

&lt;p&gt;Pero no lo maté del todo, y no lo escondo: el bot todavía intenta vender la cantidad comprada sin descontar la comisión. El rechazo puede volver. Lo único que cambió es que ahora me enteraría, en vez de recibir una ganancia falsa.&lt;/p&gt;

&lt;p&gt;No se repite por una casualidad: desde agosto pago comisiones en BNB, así que la moneda comprada ya no pierde ese 0.1%. La causa raíz sigue ahí, dormida.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un rechazo no es un éxito, aunque venga en una respuesta válida. "La API me contestó" no es "la API hizo lo que pedí".&lt;/p&gt;

&lt;p&gt;El falso positivo es peor que el error ruidoso: te roba lo único sobre lo que operás, saber en qué estado está tu cuenta de verdad. Knight Capital no quebró por perder dinero. Estuvo a punto de quebrar porque durante 45 minutos su sistema decía que todo iba bien.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Una última cosa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El bug fue lo llamativo, pero no fue lo que terminó con este bot. Fue lo mismo que con el de futuros: no tenía edge.&lt;/p&gt;

&lt;p&gt;Usaba el mismo motor de señales ya descartado. En un mes hizo seis operaciones, todas cerradas por stop; el take profit del 4% no se activó ni una vez. Neto: −0.15 USDT.&lt;/p&gt;

&lt;p&gt;Lo reemplaza algo mejor: un sesgo de baja volatilidad que hoy corre en pruebas. Si este bot vuelve, vuelve con esa estrategia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cierre de la serie&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cinco entradas, cinco formas distintas de que un sistema me dijera una cosa mientras hacía otra: capital que creía retirado, protecciones que no estaban, un valor por defecto leído en vez del real, un backtest que mostraba un edge inexistente, y una venta que nunca ocurrió.&lt;/p&gt;

&lt;p&gt;Vistos juntos, cuentan una sola historia: la distancia entre lo que un sistema reporta y lo que un sistema hace. Todos hablaban con seguridad. Todos mentían.&lt;/p&gt;

&lt;p&gt;La única fuente que nunca mintió fue la cuenta real —los fills, el saldo, el P&amp;amp;L—. Y casi siempre fue lo último que miré. Debió ser lo primero.&lt;/p&gt;

&lt;p&gt;El instrumento más caro del trading automático no es el bot. Es la confianza mal puesta. Cada uno de estos errores costó centavos porque los cacé a tiempo. De no haberlos cazado, el error habría salido costoso.&lt;/p&gt;

&lt;p&gt;Los dos bots cumplieron su papel: me enseñaron a programar y a desconfiar de mis propios registros. Hoy opero distinto, con un enfoque que no depende de adivinar la dirección del precio. Pero esa es otra historia. Quizás otra serie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tu turno&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;¿Cuál fue el error más caro que te enseñó tu propio sistema? Cuéntalo en los comentarios.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Tres errores entre mi backtest y la verdad (y la verdad estaba en el P&amp;L real)</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Thu, 15 Oct 2026 21:50:50 +0000</pubDate>
      <link>https://dev.to/jesusaco/tres-errores-entre-mi-backtest-y-la-verdad-y-la-verdad-estaba-en-el-pl-real-5g6b</link>
      <guid>https://dev.to/jesusaco/tres-errores-entre-mi-backtest-y-la-verdad-y-la-verdad-estaba-en-el-pl-real-5g6b</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #4&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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&amp;amp;L real del bot.&lt;/p&gt;

&lt;p&gt;No resistió el primer chequeo. Ni el segundo. Ni el tercero.&lt;/p&gt;

&lt;p&gt;El punto de partida: un edge que parecía real&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffojlpj6spl0n6bp7rjrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffojlpj6spl0n6bp7rjrx.png" alt=" " width="662" height="152"&gt;&lt;/a&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Esa brecha —backtest brillante, realidad plana— fue el hilo que empecé a tirar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 1: el .env que nadie leyó&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5e1mpm5hgub6v34nbjo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5e1mpm5hgub6v34nbjo6.png" alt=" " width="730" height="65"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Estaba equivocado. Lo que acababa de hacer era alejar el backtest de la realidad, no acercarlo.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TRADING_HOURS_ENABLED=false&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: dos ventanas de tiempo distintas, comparadas como si fueran la misma&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Entonces, ¿por qué el bot real no ganaba?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: el costo que no escalaba&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoe1e0nyx99o2u1kega5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoe1e0nyx99o2u1kega5.png" alt=" " width="663" height="194"&gt;&lt;/a&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyh0tc4sdm6qkm99jeii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyh0tc4sdm6qkm99jeii.png" alt=" " width="664" height="197"&gt;&lt;/a&gt;&lt;br&gt;
Break-even. En la práctica, nada.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La parte que de verdad importa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fíjate en el orden de los villanos, porque es contraintuitivo:&lt;/p&gt;

&lt;p&gt;El horario no fabricó el edge. (Mi primera hipótesis: falsa.)&lt;br&gt;
El costo mal modelado, solo, tampoco lo destruía: BCH seguía en +57%.&lt;br&gt;
Quitar los mejores trades, solo, tampoco: sin ellos BCH seguía en +30%.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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&amp;amp;L del bot en vivo— y durante semanas la ignoré porque confiaba más en mi herramienta que en mi cuenta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;¿Alguna vez tu backtest te mostró un edge que la realidad desmintió? Cuéntalo en los comentarios.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>spanish</category>
      <category>trading</category>
    </item>
    <item>
      <title>El día que descubrí que mi bot no tenía las protecciones que creía tener</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Sun, 04 Oct 2026 21:08:26 +0000</pubDate>
      <link>https://dev.to/jesusaco/el-dia-que-descubri-que-mi-bot-no-tenia-las-protecciones-que-creia-tener-2h1g</link>
      <guid>https://dev.to/jesusaco/el-dia-que-descubri-que-mi-bot-no-tenia-las-protecciones-que-creia-tener-2h1g</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #3&lt;/p&gt;

&lt;p&gt;Durante varios días, mi bot de trading creyó tener órdenes de stop-loss que en realidad no existían en el exchange.&lt;/p&gt;

&lt;p&gt;No fallaban de forma visible. En los registros, cada posición tenía su stop "gestionado". El bot estaba convencido de que cada operación tenía su red de seguridad. Pero esa red no estaba donde debía estar: en Binance.&lt;/p&gt;

&lt;p&gt;En ese tiempo, posiciones apalancadas —donde una caída fuerte puede liquidarte— operaron sin la protección que yo creía activa. Si el bot se caía en el momento equivocado, no había nada abajo.&lt;/p&gt;

&lt;p&gt;Esta es la tercera entrada de la serie, y la más inquietante. No fue un bug ruidoso ni un componente fantasma. Fue peor: la plataforma cambió las reglas —lo anunció, pero yo no lo vi— y mi bot siguió reportando que todo estaba bajo control mientras, en el exchange, las posiciones corrían desnudas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El síntoma: un error que no debería existir&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Todo comenzó con un mensaje nuevo en los registros al intentar colocar un stop:&lt;br&gt;
&lt;strong&gt;HTTP 400 | code: -4120 | Order type not supported for this endpoint.&lt;br&gt;
Please use the Algo Order API endpoints instead.&lt;/strong&gt;&lt;br&gt;
Mi código enviaba la orden de la forma que, según tutoriales y ejemplos, era la correcta. Entonces, ¿por qué Binance rechazaba algo que medio internet daba por válido?&lt;/p&gt;

&lt;p&gt;La pista incómoda: mi código estaba bien escrito, pero el contrato había cambiado. Cuando tu petición es correcta y aun así la rechazan, el problema no está solo en tu lado: está en que las reglas del otro extremo ya no son las mismas.&lt;br&gt;
&lt;strong&gt;La investigación: lo que el bot creía vs. lo que pasaba&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0pzkfnp6ym1h6yhxgsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0pzkfnp6ym1h6yhxgsn.png" alt=" " width="586" height="156"&gt;&lt;/a&gt;&lt;br&gt;
Binance había migrado las órdenes condicionales (stop-loss, take-profit, trailing stop) al nuevo "Algo Order API". El endpoint viejo quedó bloqueado para esos casos. El cambio estaba en el changelog oficial semanas antes, pero yo no lo vi.&lt;/p&gt;

&lt;p&gt;(Nota: no fui el único en chocar con esto. Cuando Binance hizo el cambio —a finales de 2025—, frameworks de trading enteros como Freqtrade y nautilus_trader amanecieron con exactamente este error, documentado en sus repos de GitHub. En mi caso llegué tarde a la fiesta: mi bot empezó a operar después del cambio, así que heredé el problema sin saberlo. Mi código enviaba las órdenes al endpoint viejo —que ya no servía para stops— desde el primer día, y yo creía que las protecciones se colocaban. Hasta que un día revisé de verdad y descubrí que nunca habían estado ahí.)&lt;/p&gt;

&lt;p&gt;Lo más grave no era el error -4120 visible. Era lo que pasaba por debajo: el bot sí calculaba y llevaba el stop en su propio estado interno —tenía su nivel de salida bien definido—, pero ese stop vivía solo en la memoria del bot, no en el exchange. En la interfaz de Binance, el campo de protección de esas posiciones decía, literalmente, -- / --. Mientras el bot corriera, la gestión interna disimulaba el problema. Pero un reinicio, una caída, un corte de red en el momento equivocado, y la posición se quedaba sin absolutamente nada que la protegiera. Yo creía tener una red de seguridad en dos capas; en realidad tenía media.&lt;br&gt;
&lt;strong&gt;Por qué esto es peor que un bug normal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un bug propio lo puedes leer, probar y arreglar. Pero cuando dependes de una API externa, una parte de tu sistema vive en un servidor que no controlas. Pueden cambiar un endpoint, un parámetro o una validación, y tu código —sin modificar una sola línea— empieza a comportarse distinto.&lt;/p&gt;

&lt;p&gt;El fallo más peligroso no es el error claro, sino la falsa confianza: cuando crees que tienes una protección y en realidad no está donde importa. Un stop que vive solo en tu código, pero no en el exchange, es exactamente eso — una red pintada en el suelo.&lt;br&gt;
&lt;strong&gt;El fix: migrar y verificar de verdad&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La solución tuvo dos pasos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrar al nuevo endpoint:&lt;/strong&gt; reescribir la lógica de stops y trailing para usar /algoOrder, con los parámetros que ahora espera. Marqué cada cambio con la etiqueta [ALGO-MIGRATION] para que en el futuro yo —o quien lea el código— sepa qué se movió y por qué. Para las posiciones que ya estaban corriendo sin stop nativo, escribí un script que las recorrió una por una y les colocó retroactivamente la protección que les faltaba.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verificar la existencia real de la orden:&lt;/strong&gt; dejar de confiar en que "el bot lo tiene gestionado" o en que "no dio error" signifique que la protección existe. Ahora el bot consulta directamente al exchange si la orden está viva. Solo cuando Binance confirma que el stop está ahí, el bot lo da por colocado. Hay una diferencia enorme entre "mi petición no dio error" y "la orden está viva en el exchange". El bug vivía justo en esa brecha.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección para llevarte&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"No dio error" no es lo mismo que "funcionó". Verifica el efecto, no la ausencia de queja.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Si construyes sobre APIs de terceros —y hoy casi todo software lo hace—, esta historia deja tres recordatorios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No controlas tus dependencias externas&lt;/strong&gt;. El código que llama a una API vive a medias en un servidor ajeno. Pueden cambiar las reglas —a veces lo avisan en un changelog que nadie lee, a veces el aviso pasa desapercibido— y tu código intacto empezará a comportarse distinto. Cuando el comportamiento cambia pero tu código no, mira hacia afuera: al otro extremo de la conexión, y a los canales donde esa plataforma anuncia sus cambios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lee los changelogs de tu exchange&lt;/strong&gt; — no hacerlo es un error muy común. Binance (y cualquier exchange serio) publica sus cambios de API en un changelog oficial, a veces con semanas de aviso. Este cambio estaba anunciado ahí. Pero casi nadie lo lee: montas el bot, funciona, y te olvidas de que del otro lado hay un equipo cambiando cosas. Yo no lo leía. Freqtrade y nautilus_trader tienen equipos enteros y aun así los pilló. La información estaba disponible; solo que nadie la miró a tiempo. Revisar el changelog cada cierto tiempo —o, mejor, automatizar una alerta cuando cambie— convierte una sorpresa silenciosa en un aviso con margen para reaccionar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifica el efecto, no la respuesta&lt;/strong&gt;. No asumas que algo se hizo porque la llamada no lanzó una excepción, ni porque tu propio código "lo tiene controlado". Si la operación importa —una orden de protección, un pago, un cambio de permisos— confirma que el efecto realmente ocurrió en el sistema externo, no en tu versión de la realidad.&lt;/p&gt;

&lt;p&gt;Ese día entendí que lo más frágil de mi sistema no era mi código, sino el supuesto de que la API seguiría igual que la última vez que la revisé —y que mi estado interno reflejaba el del exchange. Ninguna de las dos cosas era cierta. Y mi bot había estado operando con una red de seguridad imaginaria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En la próxima entrada&lt;/strong&gt; empieza la parte más incómoda de todo el viaje. Descubrí que mi herramienta de backtest —la que usaba para decidir si una estrategia servía— me estaba mintiendo. Tenía un "edge" fabuloso… en un horario en el que el bot ni siquiera operaba. Cómo una sola línea mal ajustada en el tiempo puede convertir meses de "resultados prometedores" en un espejismo.&lt;/p&gt;

&lt;p&gt;¿Alguna vez una API de terceros cambió bajo tus pies y te enteraste tarde? ¿Cómo lo detectaste? Cuéntamelo en los comentarios.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>trading</category>
      <category>spanish</category>
    </item>
    <item>
      <title>Por qué getenv("X") or default es una trampa</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Sun, 13 Sep 2026 20:10:05 +0000</pubDate>
      <link>https://dev.to/jesusaco/por-que-getenvx-or-default-es-una-trampa-2ab6</link>
      <guid>https://dev.to/jesusaco/por-que-getenvx-or-default-es-una-trampa-2ab6</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #2:&lt;/p&gt;

&lt;p&gt;En la entrada anterior conté cómo un componente de mi sistema estuvo meses operando con un capital que nadie le asignó — un "par fantasma" que creía haber eliminado y seguía vivo. Toda la causa cabía en una línea de código. Una línea que probablemente tienes en tu proyecto ahora mismo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqhn1pv9gcuotbchilp1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqhn1pv9gcuotbchilp1.png" alt=" " width="414" height="37"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Se ve inofensiva. Se ve hasta responsable — "si la variable no está, uso un valor por defecto y el programa no se rompe". Ese razonamiento es la trampa. Te cuento por qué.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qué hace ese or&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En cristiano: "usa el valor de la variable; si no hay valor, usa &lt;strong&gt;133&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;Suena bien. El problema es qué cuenta como "no hay valor". Si por accidente borras esa variable —un despliegue que pisa el archivo, un backup que restauras, un typo en el nombre— &lt;strong&gt;el programa no se detiene, no da error, no avisa&lt;/strong&gt;. Simplemente empieza a usar 133 como si alguien lo hubiera decidido.&lt;/p&gt;

&lt;p&gt;En mi caso, ese &lt;strong&gt;133&lt;/strong&gt; era el capital con el que un bot operaba con dinero real. La variable que debía definirlo desapareció en un restore, y el default la reemplazó en silencio. El sistema no falló ruidosamente. Falló callado — que es peor, porque no deja rastro y lo descubres meses después.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La parte que engaña de verdad: el valor cero&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Aquí está el detalle que casi me muerde. Supón que quieres desactivar ese componente poniendo su valor en cero. Lo natural: defines la variable como &lt;strong&gt;0&lt;/strong&gt;. Pues bien — dependiendo de dónde pongas el &lt;strong&gt;or&lt;/strong&gt;, obtienes cosas opuestas:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcyhnxzced5xdbmra3nfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcyhnxzced5xdbmra3nfq.png" alt=" " width="665" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En el segundo caso pusiste &lt;strong&gt;0&lt;/strong&gt; y el programa te devolvió &lt;strong&gt;133&lt;/strong&gt;. ¿Por qué? Porque para el &lt;strong&gt;or&lt;/strong&gt;, el número &lt;strong&gt;0&lt;/strong&gt; cuenta como &lt;strong&gt;"vacío"&lt;/strong&gt; (en jerga: es falsy). Entonces el &lt;strong&gt;or&lt;/strong&gt; lo descarta y salta al default. Intentaste apagar algo poniéndolo en cero, y conseguiste justo lo contrario.&lt;/p&gt;

&lt;p&gt;Lo peligroso: &lt;strong&gt;este bug solo aparece con el valor cero&lt;/strong&gt;. Todos tus tests con valores normales pasan. El fallo espera callado hasta que alguien prueba el caso límite.&lt;/p&gt;

&lt;p&gt;La lección de fondo, en una frase: or &lt;strong&gt;no distingue entre "no hay valor" y "el valor es cero"&lt;/strong&gt;. Para configuración, esas dos cosas son totalmente distintas — pero &lt;strong&gt;or&lt;/strong&gt; las trata igual. Ahí vive el bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No es solo Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si usas otro lenguaje, el mismo patrón te espera con otra cara. En JavaScript, &lt;strong&gt;process.env.VAR || "133"&lt;/strong&gt; tiene el problema idéntico con &lt;strong&gt;0&lt;/strong&gt;, "" y &lt;strong&gt;false&lt;/strong&gt;. (Por eso JS moderno introdujo ??, el &lt;strong&gt;nullish coalescing&lt;/strong&gt;, que solo cae al default cuando el valor es realmente nulo — creado justo para arreglar esto.) En &lt;strong&gt;Go&lt;/strong&gt; se disfraza de &lt;strong&gt;if val == "" { val = "133" }&lt;/strong&gt;, mezclando "no existe" con "cadena vacía".&lt;/p&gt;

&lt;p&gt;El patrón es universal porque el impulso que lo crea es universal: quiero un valor sensato si la config falla. Buena intención. Mala ejecución.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qué hacer en su lugar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La regla: &lt;strong&gt;si un valor es importante, su ausencia no debe ser silenciosa&lt;/strong&gt;. Tres opciones, de más a menos estricta:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Si es obligatorio, que el programa grite&lt;/strong&gt;. No inventes default; exige que exista.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lu2p23nzf4ys8y2s1bi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lu2p23nzf4ys8y2s1bi.png" alt=" " width="444" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Un programa que se niega a arrancar sin su config es mucho más seguro que uno que arranca con un valor fantasma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Si quieres un default, usa el argumento correcto&lt;/strong&gt; &lt;strong&gt;(no&lt;/strong&gt; or*&lt;em&gt;)&lt;/em&gt;*.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa1e3l0l5bw0pfizvjvh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa1e3l0l5bw0pfizvjvh1.png" alt=" " width="414" height="69"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El segundo argumento de getenv solo actúa cuando la variable no existe — no cuando vale cero. Que es lo que de verdad querías.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Si el default es a propósito, hazlo visible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9w3gvq4mxtmdkv4pu7m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9w3gvq4mxtmdkv4pu7m.png" alt=" " width="591" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Así, si un restore borra la variable, no tienes un bug silencioso: tienes una línea en el log que te dice exactamente qué pasó.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esto no es solo trading&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mi caso fue un bot con capital real, pero el patrón vive en cualquier sistema donde un valor equivocado importa: un &lt;strong&gt;límite de acceso&lt;/strong&gt; que cae a un default permisivo, una &lt;strong&gt;configuración de seguridad&lt;/strong&gt; que desaparece y se reemplaza sin avisar, un &lt;strong&gt;umbral&lt;/strong&gt; que silenciosamente vuelve a un valor de fábrica. En todos, un default silencioso convierte "se borró la config" en "el sistema hace algo que nadie pidió" — sin dejar rastro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección para llevarte&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Un default silencioso cambia un fallo que ves por uno que no ves&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;El &lt;strong&gt;getenv("X") or default&lt;/strong&gt; parece robustez, pero es fragilidad disfrazada: en vez de detenerse y avisarte cuando falta algo, sigue adelante con un valor que nadie decidió.&lt;/p&gt;

&lt;p&gt;Un programa que se niega a arrancar te cuesta cinco minutos. Uno que arranca con el valor equivocado te cuesta encontrarlo cuando ya hizo daño. En mi caso, ese cambio de una línea fue la diferencia entre semanas de fuga silenciosa y un arranque limpio.&lt;/p&gt;

&lt;p&gt;Los valores por defecto no son gratis. Son una decisión sobre qué pasa cuando el mundo no es como esperabas — y esa decisión merece más que un or al final de una línea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En la próxima entrada cambio de escenario&lt;/strong&gt;: Binance movió silenciosamente uno de sus endpoints de la API, y mi bot llevaba días colocando órdenes de protección que en realidad no existían. Cómo se detecta que una plataforma de la que dependes cambió las reglas sin avisar — y cómo migrar sin quedarte expuesto.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;¿Cuál es el default silencioso más peligroso que has encontrado en código ajeno (o propio)? Los leo en los comentarios.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>showdev</category>
      <category>spanish</category>
    </item>
    <item>
      <title>"El par fantasma: cómo un bot operaba con capital que nadie le asignó"</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:52:20 +0000</pubDate>
      <link>https://dev.to/jesusaco/diario-de-un-bot-que-opera-con-dinero-real-entrada-1-1gip</link>
      <guid>https://dev.to/jesusaco/diario-de-un-bot-que-opera-con-dinero-real-entrada-1-1gip</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #1: &lt;/p&gt;

&lt;p&gt;Hace unos meses eliminé un par de trading de mi bot de futuros. O eso creía.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El síntoma&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Estaba revisando los logs del bot cuando una línea me detuvo:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[PAR-X] SNAP | ... | cap=135.34 | lev=3× | pos=FLAT&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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ó:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[PAR-X] ENTRY SKIP | USDT insuficiente: free=130.98 &amp;lt; needed=138.04&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La investigación&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkj74f7zl2mjkwskmwl47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkj74f7zl2mjkwskmwl47.png" alt=" " width="237" height="29"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vacío. La variable no existía en el .env. Entonces, ¿de dónde salían los $135?&lt;/p&gt;

&lt;p&gt;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í:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoc13jyolob98u0rvo5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoc13jyolob98u0rvo5b.png" alt=" " width="504" height="34"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ahí estaba el problema. &lt;code&gt;os.getenv("PAR_X_USDT")&lt;/code&gt; 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, &lt;strong&gt;caía a un valor por defecto hardcodeado de $133&lt;/strong&gt; que alguien (yo, meses atrás) había dejado en el código como fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La causa raíz — y aquí está la lección real&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El bug no era solo el fallback. Era &lt;strong&gt;cómo había "eliminado" el par meses atrás.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En su momento, para desactivarlo, había puesto su capital en cero en el &lt;code&gt;.env (PAR_X_USDT=0)&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;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ó.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;Verificar que el par no tuviera posición abierta (revisar su archivo de estado).&lt;br&gt;
Backup del archivo con timestamp.&lt;br&gt;
Eliminar el bloque de configuración.&lt;br&gt;
Validar la sintaxis &lt;code&gt;(python3 -c "import ast; ast.parse(...)")&lt;/code&gt;.&lt;br&gt;
Confirmar con grep que el par ya no aparecía en ningún lado.&lt;br&gt;
Reinicio manual.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El cuerpo del delito&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gs7f8bsp9o1qikz9wub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gs7f8bsp9o1qikz9wub.png" alt=" " width="257" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección para llevarte&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Antes de las lecciones técnicas, la pregunta que me perseguía mientras lo corregía: &lt;strong&gt;¿cómo sobrevivió esto a las auditorías anteriores?&lt;/strong&gt; Había revisado este bot varias veces. ¿Cómo no lo vi?&lt;/p&gt;

&lt;p&gt;La respuesta me pareció más valiosa que el bug en sí. No lo detecté antes &lt;strong&gt;porque estaba auditando el código, y el código estaba bien&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Esa es la lección de fondo: &lt;strong&gt;una auditoría que solo lee el código estático nunca atrapa un bug de configuración fantasma&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;De ahí, tres trampas concretas que me llevo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Los defaults hardcodeados son deuda oculta&lt;/strong&gt;. Un &lt;code&gt;os.getenv("X")&lt;/code&gt; 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ó.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desactivar por la vía frágil no es desactivar&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Un backup puede resucitar tus bugs&lt;/strong&gt;. 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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En la próxima entrada desarmo&lt;/strong&gt; la línea exacta que causó todo esto ese inocente &lt;code&gt;os.getenv("X") or default&lt;/code&gt; — 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.&lt;/p&gt;

&lt;p&gt;¿Te ha pasado encontrar un componente haciendo algo que juraste haber desactivado? Cuéntamelo en los comentarios — colecciono estas historias.&lt;/p&gt;

</description>
      <category>trading</category>
      <category>spanish</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Empecé este bot por desconfianza, no por avaricia.</title>
      <dc:creator>Jesus Corredor</dc:creator>
      <pubDate>Sat, 22 Aug 2026 15:13:11 +0000</pubDate>
      <link>https://dev.to/jesusaco/empece-este-bot-por-desconfianza-no-por-avaricia-4f0a</link>
      <guid>https://dev.to/jesusaco/empece-este-bot-por-desconfianza-no-por-avaricia-4f0a</guid>
      <description>&lt;p&gt;Diario de un bot que opera con dinero real — Entrada #0: el origen&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Todo empezó con un tuit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Uno de esos que seguramente también has visto: una captura de una wallet, "$100 convertidos en $10.000 en 24 horas con este bot de trading", flechas verdes, emojis de cohetes, y un "sígueme para más". Debajo, cientos de likes y gente pidiendo el enlace.&lt;/p&gt;

&lt;p&gt;Mi primera reacción no fue "quiero eso". Fue "eso es mentira".&lt;/p&gt;

&lt;p&gt;Y no hace falta ser matemático para verlo. Un retorno del 10.000% en un día no es una estrategia — es un billete de lotería premiado que alguien presenta como si fuera un método repetible. Si de verdad tuvieras un sistema que multiplica tu dinero por cien cada 24 horas, no lo estarías publicando en X pidiendo likes. Lo estarías usando en silencio hasta comprar una isla. El que regala el mapa del tesoro es porque el tesoro no existe — el verdadero producto que se vende en esos tuits no es el bot: eres tú, tu like, tu follow, tu atención.&lt;/p&gt;

&lt;p&gt;Así que no le di like. Pero me quedé pensando.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La pregunta que sí valía la pena&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Descartado el humo, quedaba una pregunta honesta debajo: despojado de la mentira del 10.000%, ¿hay algo real ahí?&lt;/p&gt;

&lt;p&gt;Porque los bots de trading existen. La automatización de estrategias es legítima. Los mercados operan 24/7 y un programa no duerme ni entra en pánico. La idea de fondo —dejar que un sistema ejecute una estrategia con disciplina, sin la emoción que arruina las decisiones humanas— no es una estafa. La estafa es el número. La estafa es prometer un retorno imposible para vender seguidores.&lt;/p&gt;

&lt;p&gt;Entonces me hice la pregunta que inició todo esto: &lt;strong&gt;¿qué pasa si alguien escéptico construye un bot de trading de verdad, con expectativas sobrias, y documenta la verdad completa — incluida la parte donde todavía no sabe si funciona?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esa es la serie que estás empezando a leer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que es, y lo que no es&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Para que no haya malentendidos, porque tú y yo ya sabemos cómo suele terminar este tipo de contenido:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esto no es&lt;/strong&gt; un tutorial de "hazte rico". No voy a mostrarte una wallet inflada. No hay enlace de afiliado, no hay curso al final, no voy a decirte que copies mi estrategia y te retires en un año.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esto sí es&lt;/strong&gt; el registro honesto de construir un sistema real que mueve dinero real: los bugs que encontré (algunos que me pusieron los pelos de punta), las auditorías, las decisiones tomadas con datos en vez de con ilusión, y los errores que documenté justamente para no repetirlos.&lt;/p&gt;

&lt;p&gt;Y aquí viene la parte que ningún tuit de captura de wallet te va a decir jamás: &lt;strong&gt;al momento de escribir esto, mi bot no ha demostrado que gane dinero de forma sostenida&lt;/strong&gt;. Tiene meses de trabajo de ingeniería sólida detrás, sobrevive a fallos que tumbarían a otros, y hay evidencia de que su estrategia podría tener una ventaja real. Pero "podría" no es "tiene". Estoy en pleno proceso de validar, con dinero real y en vivo, si esa ventaja existe de verdad o si es un espejismo de los datos históricos.&lt;/p&gt;

&lt;p&gt;Eso es lo más honesto que puedo ofrecerte, y es exactamente lo contrario del tuit que me trajo aquí. El estafador te vende certeza que no tiene. Yo te ofrezco un proceso honesto cuyo desenlace ni yo conozco todavía.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué escribo esto&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Porque el espacio está tan lleno de humo que la verdad, por aburrida que sea a veces, es refrescante.&lt;/p&gt;

&lt;p&gt;Cada entrada de esta serie es un post-mortem real: algo que salió mal, cómo lo investigué, qué aprendí. No hay finales felices garantizados. Hay un componente que operó meses con dinero que nadie le asignó. Hay protecciones que el bot creía tener y no tenía. Hay una fuga silenciosa que tardé meses en detectar. Y hay decisiones —como cerrar proyectos que no funcionaban— tomadas sin dejar que el orgullo o el dinero ya invertido nublaran el juicio.&lt;/p&gt;

&lt;p&gt;Si buscabas el bot que convierte $100 en $10.000, esta no es tu serie, y con cariño te digo que esa serie no existe en ningún lado — solo existe el tuit que te la promete.&lt;/p&gt;

&lt;p&gt;Pero si te interesa cómo se construye de verdad un sistema que opera con dinero, con toda la honestidad de sus errores y la incertidumbre honesta de su resultado, quédate. Empecé esto por desconfianza. Y resulta que la desconfianza, bien usada, es el mejor punto de partida para construir algo real.&lt;br&gt;
&lt;strong&gt;En la próxima entrada empieza lo bueno:&lt;/strong&gt; cómo encontré un componente de mi bot operando con un capital que nadie le asignó — un "par fantasma" que creía haber eliminado meses atrás, y que seguía vivo, acaparando recursos, escondido a plena vista.&lt;/p&gt;

&lt;p&gt;¿Tú también llegaste a los bots de trading a través de una promesa que sabías que era falsa? ¿Qué te hizo dudar? Cuéntamelo en los comentarios.&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>spanish</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
