SPX ---
NDX ---
BTC ---
ORO ---
VIX ---
US3M ---
US5Y ---
US10Y ---
US30Y ---

System 7 (parte 2): nos fuimos al heads-up, y el explotador por fin cobró

En la primera parte terminamos con una lección incómoda: construimos un motor explotador elaboradísimo —clasificación de manos, filtros matemáticos, node-locking, un LLM para los nudos difíciles— y perdió. Contra el panel de referencia, el node-locking heurístico puro hacía −9.75 bb/100 y la versión híbrida con LLM −1.66 bb/100. Lo que ganaba, y por goleada, era la táctica más tonta imaginable: abrir muchísimo y robar ciegas sin complejos. +119 bb/100 con una estrategia que cabe en una servilleta.

La moraleja de aquella entrega fue que el motor le gana a la idea brillante: la ventaja no está en la jugada genial, sino en el bucle de autor → test → comparar → desplegar → coach → repetir. Y que la explotación solo paga cuando hay una fuga real que explotar.

Lo que no contamos —porque todavía no había pasado— es qué ocurre cuando le das a ese motor un rival de verdad al que apuntar. Esta es esa historia. Desde entonces cambiamos de mesa, reescribimos medio motor, lo metimos en Docker y aprendimos, a base de golpes, que en producción la fiabilidad es una habilidad de póker más.

Recapitulando: dónde lo dejamos

Para quien llegue nuevo: System 7 es un agente de No-Limit Hold'em que juega en el Arena de dev.fun, un torneo donde agentes de software se enfrentan entre sí (y a humanos). El núcleo es Python determinista —rápido, reproducible, testeable— con un "cerebro" LLM (MiniMax M3) al que solo llamamos para los nudos difíciles. Todo está abierto en GitHub.

La parte 1 se jugó en mesas de 6-max. El diagnóstico honesto fue que el panel de referencia, aunque teóricamente sólido, tenía una fuga estructural: foldeaba demasiado preflop. Nuestra maquinaria de microscopio no hacía falta; bastaba un martillo. Pero un 6-max reparte la culpa entre cinco rivales y trocea la muestra: nunca ves suficientes manos de un mismo villano como para explotarlo con confianza. El microscopio no tenía a quién enfocar.

Así que hicimos lo obvio que no habíamos hecho: quitamos a cuatro jugadores de la mesa.

El giro al heads-up (temporada S4)

El heads-up —uno contra uno— lo cambia todo, y no por romanticismo. Lo cambia por estadística. En 6-max, 500 manos son ~80 manos de cada rival: ruido. En heads-up, 500 manos son 500 manos del mismo rival. Por fin la muestra crece lo bastante rápido como para que "este tío foldea demasiado a la 3-bet" pase de corazonada a número con intervalo de confianza.

Toda la temporada S4 la dedicamos a rehacer el motor pensando en un solo oponente. En términos de repo, esto fue la v0.6.0: activación de node-locking, EV en all-in, un coach específico de heads-up y un panel unificado. Pero la frase de una línea esconde el cambio de mentalidad: dejamos de preguntar "¿cuál es la jugada correcta contra el campo?" y empezamos a preguntar "¿cuál es la jugada correcta contra este que llevo 1.500 manos mirando?".

Es exactamente la tesis de la parte 1 —explotar en lugar de jugar equilibrio— pero por fin con el terreno a favor.

Node-locking que por fin paga

El node-locking, recordemos, es fijar (clavar, lock) una parte de la estrategia del rival según lo que has observado, y recalcular tu respuesta óptima contra esa fuga concreta. En la parte 1 era, en la práctica, un adorno caro: la muestra no daba para fiarse del lock.

En heads-up sí da. Calibramos el umbral de adaptación a ~1.900 manos por oponente, que es el orden de magnitud que el Arena reparte en una sesión seria. Por debajo de eso, jugamos base sólida y no nos inventamos reads. Por encima, el motor empieza a explotar de verdad los arquetipos clásicos:

  • NIT — el que solo juega manos premium: le robamos las ciegas sin descanso y le pagamos poquísimo cuando por fin despierta.
  • STATION — el que paga todo: dejamos de farolear y engordamos el valor. Aquí el farol es literalmente prender dinero.
  • LAG — el agresivo suelto: le devolvemos la agresión donde la matemática aguanta y le tendemos trampas en los nudos.
  • MANIAC — la versión extrema: paciencia, trampas y dejar que se estrelle solo.

Con esto vinieron dos ajustes finos que suenan a poco y valen mucho. Primero, ampliamos el rango de c-bet en botón a boards coordinados: en heads-up, la iniciativa vale, y ceder demasiado te convierte en el NIT que quieres explotar. Segundo, arreglamos una fuga propia que llevábamos arrastrando: las parejas bajas en boards emparejados (una pareja de menos frente a, digamos, KK en el board) dejaron de tratarse como manos de compromiso y pasaron a clasificarse como lo que son, bluff-catchers. Es una corrección pequeña de una tabla de clasificación de manos, y de esas viven o mueren los bb/100.

Todo el afinado vive en system7-hu.json: tamaños de apertura, frecuencias de barril, 3-bets de farol y rangos de ciega pequeña. Estrategia como dato, versionada, intercambiable con una variable de entorno. Ese diseño —lo prometimos en la parte 1— es lo que hace que iterar sea barato.

Motor v1.5: la lógica de supervivencia

Y aquí llegamos a lo de hoy mismo. La v0.7.0, publicada esta misma tarde, trae lo que internamente llamamos motor v1.5, "supervivencia". Es el cambio conceptual más grande desde la parte 1, y nace de una observación fea: un motor optimizado para maximizar EV en cada mano toma decisiones estupendas para el largo plazo y ruinosas para seguir en la mesa.

El ejemplo canónico es AKo. Contra un solo rival, meter la pila con AKo suele estar bien. Pero en secuencias multiway de 4-bet/5-bet, meterla plano con AKo es tirar fichas: cuando entra tanto dinero de tanta gente, tu mano preciosa es, con demasiada frecuencia, la segunda mejor. El motor v1.5 deja de hacerlo.

La pieza nueva son los niveles de compromisoshallow / deep / all-in— indexados por la profundidad de la re-subida. En vez de una regla uniforme de "con esta fuerza, voy con todo", el compromiso se gradúa por tramo: cuanto más se recrudece la guerra de re-raises, más selectiva se vuelve la mano con la que aceptas jugarte la pila. En multiway, además, las parejas dejan de comprometerse automáticamente en SPR bajo. El comportamiento en heads-up puro se conserva intacto: allí seguimos jugando a maximizar.

Dos detalles de ingeniería que importan más de lo que parece. Todo esto está detrás de un flag, survival:true: por defecto el comportamiento no cambia, así que nadie se lleva sorpresas y podemos hacer A/B contra el motor anterior. Y llegó acompañado de 18 tests nuevos. Porque una regla de compromiso mal escrita no falla ruidosamente: falla silenciosamente, una pila cada varias horas, justo del tipo de fuga que la parte 1 nos enseñó a temer.

Supervivencia no es lo contrario de maximización. Es reconocer que en un formato con escalera y temporadas, seguir jugando tiene un valor que la EV mano a mano no captura.

Equity por temporada, escalera heads-up y auto-reanudación

La v0.7.0 trae además tres piezas menos glamurosas y absolutamente necesarias:

Equity por temporada. Antes, las curvas de equity mezclaban manos de competiciones distintas y te pintaban un dibujo mentiroso. Ahora la curva se acota por competition_id: cada temporada, su gráfica. Suena a contabilidad, y lo es —pero medir bien fue el tema central de la parte 1, y mezclar temporadas es exactamente la clase de autoengaño estadístico contra el que predicábamos.

Escalera heads-up (el ladder bot). Empaquetamos un agente estático en su propio directorio ladder/, con un adaptador defensivo y la librería treys vendorizada dentro. ¿Por qué? Porque el sandbox donde corren algunos agentes no tiene sistema de archivos ni forma de instalar dependencias. Un agente con cero dependencias de disco es un agente que arranca donde sea. Es la diferencia entre "funciona en mi máquina" y "funciona en la suya".

Auto-reanudación de PvP. El bucle de juego persiste su estado en data/prod_resume.json y sobrevive a reinicios del host, recreaciones del contenedor y caídas de subprocesos. Antes, un reinicio a deshora te dejaba una partida a medias y un hueco en los datos. Ahora se levanta solo y sigue donde estaba. Esto conecta directamente con la siguiente sección, porque aprendimos a valorarlo por las malas.

Del código a la trinchera: historias de producción

La parte 1 terminaba en el "tooling": el panel de tres zonas (LAB / COACH / PRODUCCIÓN). Lo que no contaba es lo que cuesta tener eso vivo semana tras semana. Spoiler: en un torneo online, la fiabilidad operativa es una habilidad de póker más. Estas son las cicatrices.

Docker, y el pecado de recrear el contenedor. El despliegue vivo es Docker en /opt/arena-system7-llm (imagen system7-poker-arena:latest, dashboard en el puerto 8787, restart: unless-stopped). Aprendimos la regla más cara del proyecto: nunca hagas up -d --force-recreate con jobs en vuelo. Los Eval y los PvP son subprocesos hijos del dashboard; recrear el contenedor los mata en seco, y te quedas con evaluaciones a medias y una carpeta .clasif vacía. Horas de partidas convertidas en nada por un despliegue impaciente.

El WAL que se comió el disco. SQLite en modo WAL es maravilloso hasta que un job se atasca y el write-ahead log crece a gigabytes. Nos pasó. El hardening fue a base de tres candados: journal_size_limit para que el WAL no crezca sin freno, un timeout por match para que ningún job se quede colgado eternamente, y un lock de instancia única para que no haya dos procesos peleándose por la misma base de datos. Nada de esto es póker. Todo esto es lo que te permite jugar al póker.

El self-play local miente. Uno pensaría que la forma sensata de medir un motor es enfrentarlo contra sí mismo en local. No: el self-play del kit no mide bien la fuerza por posición, porque clava al héroe en la ciega pequeña y sesga el HUD. La fuerza real de un motor solo se mide en vivo, en el Eval y el PvP del Arena. Otra lección de humildad estadística, de las que ya coleccionamos.

El cuello de botella no era el que creíamos. Subimos los recursos en caliente a 6 cores / 4 GB. Y la intuición de ingeniero —"si va lento, es la CPU"— resultó falsa: cada partida con M3 come ~170 MB de RAM, y el verdadero cuello es la RAM y el tempo en tiempo real del Arena, no el cómputo. El motor determinista responde en milisegundos; lo que marca el ritmo es la mesa. Por cierto, las llamadas al LLM van sobre planes de tokens de MiniMax/Xiaomi, con las claves en .env —nada de proveedores hardcodeados.

El coach que se mejora solo

La pieza que más ha madurado es el coach. En la parte 1 era, sobre todo, un diagnosticador con reviews de manos vía LLM. Ahora es un bucle que se mejora a sí mismo: M3 lee los informes de estadísticas, detecta fugas y propone nuevas versiones de estrategia —esos JSON versionados— para que las probemos en A/B. El humano sigue en el asiento del conductor (aprobando o descartando versiones), pero el trabajo de "aquí hay un patrón, prueba a subir esta frecuencia" lo hace la máquina.

En heads-up el coach se volvió consciente de la posición: objetivos distintos para el botón (SB) y para la defensa de ciega grande, activándose a partir de las 500 manos y avisando cuando el diagnóstico se queda obsoleto porque llevas demasiadas manos sin actualizar la estrategia desplegada. Es el mismo principio de siempre: no confíes en un read viejo.

Resultados honestos, otra vez

Aquí es donde nos toca ser fieles a nosotros mismos. En la parte 1 pusimos números concretos porque los teníamos, con sus intervalos de confianza de ±20 bb/100 y todo. En esta entrega no vamos a inventarnos un winrate de heads-up para cerrar bonito, porque la medición seria de esta temporada sigue viva y no queremos venderte ruido disfrazado de señal.

Lo que sí podemos decir con honestidad es direccional: el node-locking, que en 6-max era decoración, en heads-up tiene a quién apuntar y se comporta como debe contra los arquetipos. El motor v1.5 sangra menos en los multiway marginales. Y las curvas de equity —ahora separadas por temporada, con su línea de habilidad (sólida) y su línea de varianza (discontinua)— por fin cuentan una historia que no está contaminada por manos de otra competición.

El resto lo dirá la muestra, cuando la muestra sea muestra. Preferimos "seguimos vivos e iterando" a un titular con un número grande y un intervalo de confianza vergonzoso. Esa disciplina fue el corazón de la parte 1, y no pensamos abandonarla ahora que las cosas empiezan a salir.

Lo que nos llevamos (parte 2)

  1. La explotación necesita un blanco. Toda la maquinaria de la parte 1 era correcta; le faltaba un rival al que enfocar el microscopio. El heads-up se lo dio. La idea no estaba mal; estaba mal la mesa.
  2. Supervivencia no es lo contrario de maximizar. Optimizar cada mano en el vacío te hace perder pilas que importan. El motor v1.5 sacrifica algo de EV puntual a cambio de seguir en pie, y en un formato con escalera eso es EV de verdad.
  3. La fiabilidad es una habilidad de póker. Un WAL desbocado, un --force-recreate a destiempo o un reinicio sin auto-reanudación te cuestan tantas fichas como una mala línea. En un torneo online, el uptime juega manos.
  4. El bucle sigue ganándole a la idea. Como en la parte 1: autor → test → desplegar → coach → repetir. El coach que ahora propone estrategias no es magia; es el mismo bucle, con un peldaño más automatizado.

Seguimos vivos en el Arena. Y por primera vez, el explotador que escribimos con tanto mimo tiene delante a alguien a quien explotar.

Recursos y enlaces

El proyecto sigue vivo en el Arena. Si te interesa el cruce entre IA, teoría de juegos y decisión bajo incertidumbre, el repo está abierto y las cicatrices, documentadas.