Tutorial de Bot de Polymarket · Capítulo 32 de 32
Errores reales de bots de Polymarket y postmortems: phantom fills, sticky-fail dedup, whipsaw lol-ctg-ccg, bug de la bandera NegRisk, go-live prematuro - con los commits y fechas que corrigieron cada uno.
Qué cubre este capítulo
Nuestro propio diario de producción de bugs que costaron dinero real. El patrón importa más que los detalles - las mismas clases de bug reaparecen en distintos bots, y la cura suele ser un watchdog que falta, no una mejor estrategia. Este capítulo está pensado para ahorrarte la matrícula.
- Phantom fills (commits e68a087, 8bb7761)
- Bug de la bandera NegRisk (commit 06deaef)
- Sticky-fail dedup (commit 4c0bef1)
- Incidente de whipsaw: lol-ctg-ccg
- Go-live prematuro: wipe de 2025
- Sleep-through-bug: el kill switch funcionó
- Lecciones que se generalizan
Phantom fills (commits e68a087, 8bb7761)
El primer gran incidente de phantom fill en nuestro trader, mayo de 2025. El bot colocó 22 compras FOK, todas matcheadas en el CLOB. El bot intentó inmediatamente publicar 22 ventas GTC. 8 fueron rechazadas con "balance: 0 / sum of active orders: 0 / order amount: 10000000."
Causa raíz: retraso de settlement (capítulo 12). El CLOB matcheó en 100ms, el bot publicó la venta en 200ms, pero la transferencia ERC-1155 en Polygon tardó ~2 segundos. El CLOB rechazó la venta porque la cadena seguía mostrando balance cero.
Corrección: insertar una espera bloqueante de 5 segundos entre cualquier compra exitosa y cualquier seguimiento GTC sobre el mismo token. Commits e68a087 y 8bb7761. Cero incidentes de phantom fill desde entonces.
Lección: el tiempo de la API y el tiempo de la cadena son líneas de tiempo distintas. El código que asume que son síncronos se va a encontrar con este modo exacto de falla.
Bug de la bandera NegRisk (commit 06deaef)
Un evento multi-outcome NegRisk con 8 candidatos tuvo un arbitraje momentáneo de 1.8c (suma de asks YES = 0.982). Nuestro arber ejecutó las 8 compras FOK. 6 se llenaron; 2 se liquidaron en el contrato de exchange incorrecto.
Causa raíz: el bot llamaba createAndPostOrder sin establecer negRisk: true en el objeto de flags. Dos de los mercados tenían una fecha histórica de creación diferente y requerían la bandera; seis no la necesitaban porque su contrato subyacente ya estaba ruteando por NegRisk por defecto.
Corrección: leer market.negRisk desde Gamma para cada mercado, y pasarlo en cada llamada de orden. Commit 06deaef. Volvimos a correr el arbitraje con la bandera seteada; los 2 restantes se liquidaron correctamente.
Lección: nunca pongas por default una propiedad de mercado. Léeela explícitamente de la fuente de verdad cada vez.
Sticky-fail dedup (commit 4c0bef1)
El bot reintentó una compra fallida 5 veces en 12 segundos. El primer intento en realidad tuvo éxito (un timeout de red hizo que el bot no viera la respuesta); los siguientes 4 reintentos crearon 4 posiciones adicionales. Total: 5 posiciones en el mismo mercado cuando queríamos 1.
Causa raíz: no había un client-order-id idempotente. La lógica de retry del bot era "si falló, intenta de nuevo con una sal nueva". El CLOB no tenía forma de reconocer los reintentos como duplicados.
Corrección: generar un UUID determinista por cada orden prevista antes del primer intento. Todos los reintentos usan el mismo client-order-id, lo que permite al CLOB hacer dedup. Commit 4c0bef1.
Lección: los retries sin idempotencia son órdenes duplicadas. Cada orden necesita un identificador estable del lado del cliente.
Incidente de whipsaw: lol-ctg-ccg
Un partido de esports (CTG vs CCG) hizo que el bot entrara en compra a 0.45 cuando el imbalance se volvió positivo. Dentro de 30 segundos, el imbalance se volvió negativo y nuestra venta GTC a 0.50 fue ejecutada por la orden de otra persona. PnL: +5c × 10 shares = +$0.50.
10 minutos después, el imbalance del mismo mercado se volvió positivo otra vez. El bot entró de nuevo a 0.42. Esta vez el imbalance nunca se recuperó; el mid derivó a 0.18 y la posición se fue hasta la resolución en 0.
Causa raíz: la estrategia trataba el imbalance como una señal direccional, pero no seguía que el imbalance estuviera rebotando - ambas señales eran ruido, no información. El bot fue sacudido entre dos señales fallidas en el mismo mercado dentro de 20 minutos.
Corrección: cooldown por mercado - después de un fill, no permitir nuevas entradas en el mismo mercado durante 30 minutos. Se permitían múltiples entradas en distintos mercados, pero no de forma consecutiva en el mismo.
Lección: una señal que rebota no es una señal. Filtra por persistencia antes de actuar.
Go-live prematuro: wipe de 2025
Una nueva estrategia de market making pasó 12 paper trades. El builder no esperó 30, decidió "se ve bien", la desplegó en vivo con $500 de capital. Dentro de 18 horas el wallet estaba en $200.
Causa raíz: 12 trades no son una muestra suficiente para distinguir un WR de 60% de un WR de 35%. La estrategia en realidad tenía 35% de WR; la ventana de 12 paper trades coincidió con una racha no representativa.
El gate de 30 trades existe por una razón. La varianza en una muestra de 12 trades hace que no puedas distinguirla de "la estrategia no funciona".
Lección: la disciplina vence a la convicción. El gate de 30 trades no es negociable.
Sleep-through-bug: el kill switch funcionó
El bot tenía un off-by-one en su filtro de hora del día - se suponía que pausara a las 02:00 UTC, pero en realidad estaba pausando a las 03:00 UTC. Durante la hora sin pausa, de 02:00 a 03:00, el RPC de Polygon estuvo aplicando rate limit agresivo a nuestras requests; la ruta de lectura del bot devolvía datos stale.
El bot siguió operando con precios stale. PnL de la hora: -$3.20 en 22 trades. El kill switch de pérdida diaria se activó en -5%, detuvo el bot y envió una alerta de Telegram a las 03:08 UTC. El builder se despertó con un bot detenido a las 09:00; el daño total quedó limitado al umbral del kill switch.
Lección: el bug era real pero el kill switch funcionó. -$3.20 en vez de -$50.00. Los controles de riesgo no evitan bugs; limitan el costo de los bugs que no viste venir.
Lecciones que se generalizan
En todos los postmortems, se repiten cuatro patrones.
- API time ≠ chain time. Settlement lag, RPC lag, WebSocket lag - todos introducen gaps que el código del bot debe manejar explícitamente.
- Los retries necesitan idempotencia. Un retry sin client-order-id es un riesgo de orden duplicada. Siempre.
- Lee explícitamente cada propiedad del mercado. Bandera NegRisk, tick size, expiración. Nunca defaults; siempre lee desde la fuente de verdad.
- El kill switch es el piso, no una feature. Los controles de riesgo limitan pérdidas por bugs. Las estrategias no previenen bugs; asumen que el bot funciona correctamente. El bot no siempre funcionará correctamente.
Cada capítulo de esta serie tiene uno de estos patrones incrustado en algún lado. Son los principios estructurales de un bot de producción. Sáltatelos y los volverás a encontrar en tus propios postmortems.





