Skip to content

Latest commit

 

History

History
519 lines (399 loc) · 23 KB

File metadata and controls

519 lines (399 loc) · 23 KB

Polymarket Bot — Implementation Plan

Fecha: 2026-02-26 | Basado en: análisis de sesión + paper arxiv:2508.03474 + Codex xhigh review Objetivo: bot de research edge profesional para mercados de eventos no-crypto


Estrategia Core

NO es arb sistemático. Es research edge:

  1. Encontrar mercados donde el precio de Polymarket diverge de la evidencia pública disponible
  2. Analizar con LLM + fuentes externas solo cuando hay discrepancia real (≥8-10pp)
  3. Ejecutar manualmente en Polymarket (evolucionar a CLI cuando esté validado)

Plataformas:

  • Polymarket: política, cultural (Oscars, Goyas, Grammy), tech, eventos generales
  • Betfair: sports (pendiente — cuando Sergio cree cuenta)

Fases de Implementación


FASE 1: Market Discovery Engine ✅ COMPLETADA

Objetivo: Scanner profesional que encuentra candidatos reales Completada: 2026-02-26 | Commit: e05bad5 | Tests: 200/200 passing

Tareas

1.1 — Arreglar paginación del scanner

  • Problema actual: market_scanner.py solo obtiene top 100 por volumen. Polymarket tiene miles.
  • Fix: implementar paginación completa con offset en Gamma API
  • Añadir parámetro max_pages (default 5 = 500 mercados)
  • Añadir filtro de categoría: politics, sports, culture, technology, economy
  • Archivos: src/data/market_scanner.py

1.2 — Precios reales (best ask, no midpoints)

  • Problema actual: usamos outcomePrices de Gamma API (midpoints/estimados)
  • Fix: para candidatos, obtener best_ask del CLOB orderbook
  • Esto es crítico para el arb real: best_ask_yes + best_ask_no vs $1.00
  • Archivos: src/data/polymarket_client.py, src/data/market_scanner.py

1.3 — Detector de intra-market arb

  • Lógica: si best_ask_YES + best_ask_NO < 1 - epsilon (epsilon = 0.02 + fees)
  • Para mercados neg_risk=true: suma de todos los YES < 1 - epsilon
  • Exponer campo arb_spread en el objeto Market
  • Solo reportar si liquidez del orderbook > $100 en cada lado
  • Archivos: src/signals/arb_detector.py (nuevo)

1.4 — Clasificación y scoring de candidatos

  • Score compuesto: volumen + odds range + días hasta resolución + arb_spread
  • Priorizar: binarios, resolución < 30 días, volumen > $10K, odds 15-85%
  • Exportar shortlist en JSON para siguiente fase
  • Archivos: src/data/market_scanner.py

Criterios de aceptación:

  • Scanner pagina correctamente y devuelve mercados de todas las categorías
  • best_ask se obtiene del CLOB endpoint /price?side=sell (fix crítico: /book daba falsos positivos)
  • arb_spread calculado con precios reales, filtrado por liquidez > $100
  • 200/200 tests passing | Verificado en vivo: 0 arbs en 20 mercados (correcto, mercados eficientes)

Code Review + Fixes aplicados (commit 8cf88f0):

  • 🔴 C-1: Range check 0.001–0.999 en precios CLOB → rechaza precios 0.0 y stale (falsos positivos eliminados)
  • 🔴 C-2: Patrón _raw/wrapper en todos los métodos @retry → tenacity ahora reintenta realmente
  • 🟠 H-1: Rate limiting enrich_markets_with_clob: delay 0.2s + cap 50 mercados
  • 🟠 H-2: _get_best_ask_from_clob delega a PolymarketClient → cero duplicación
  • 🟠 H-3: _15MIN_PATTERN dead code eliminada
  • 🟡 M-1/M-2: import json/math movidos al top; search_markets con max_pages=3
  • 🟡 M-3: CRYPTO_CATEGORIES set con todas las variantes crypto
  • 🟡 M-4: raw: Optional[dict] = None typing correcto
  • 🟡 M-5: Docstring liquidity con nota sobre liquidez total vs profundidad
  • 🟢 L-1/L-2/L-3: logger.warning, imports al top, docstrings actualizados

Validación final:

  • 200/200 tests passing (7.17s)
  • Smoke tests: C-1 fix OK, arb detection matemáticamente correcta

FASE 2: External Anchors ✅ COMPLETADA

Objetivo: Comparar odds de Polymarket con fuentes externas para detectar edge real Completada: 2026-02-26 | Tests: 269/269 passing (+69 nuevos)

Contexto y decisiones de diseño

El edge del bot no es velocidad — es información. Necesitamos saber cuándo Polymarket está mal valorado respecto a evidencia externa. Para eso necesitamos "anclas" por categoría:

Categoría Ancla primaria Ancla secundaria
Deportes (Polymarket) The Odds API (bookmakers) Metaculus / Manifold
Política / macro Metaculus + Manifold Kalshi
Cultural (Oscars, Goyas) Metaculus Brave Search (expertos)
Tecnología / empresa Metaculus Manifold

Regla de oro: solo llamar al LLM si discrepancia ≥ 8pp vs ancla. Calculamos ancla primero (barato), LLM solo si merece la pena.

Tareas

2.1 — The Odds API integration (src/data/odds_api.py — nuevo)

Wrapper para The Odds API (free tier: 500 req/mes = ~16/día, suficiente).

# Interface objetivo:
client = OddsAPIClient(api_key=os.getenv("ODDS_API_KEY"))

# Buscar sport key por nombre
sport = client.find_sport("Premier League")  # → "soccer_epl"

# Obtener implied probability sin vig (método Pinnacle/power)
prob = client.get_no_vig_probability(
    sport_key="soccer_epl",
    team="Manchester City",
    outcome="win"
)  # → {"probability": 0.72, "source": "odds_api", "bookmakers_used": 8}

Campos clave de la API: sport_key, commence_time, home_team, away_team, bookmakers[].markets[].outcomes[].price

Devig method (Pinnacle/power): p_fair = p_raw^k / sum(p_raw_i^k) donde k se itera hasta sum(p_fair) = 1

2.2 — Añadir Kalshi a prediction_markets.py

Kalshi tiene markets de política y macro similares a Polymarket.

# Endpoint público de Kalshi (no auth para lectura):
# GET https://api.elections.kalshi.com/v1/markets/?status=open&limit=100
# o GET https://trading-api.kalshi.com/trade-api/v2/markets

class KalshiClient:
    BASE = "https://api.elections.kalshi.com/v1"
    
    def find_similar_market(self, question: str) -> Optional[KalshiMarket]:
        """Busca mercado en Kalshi similar a la pregunta dada (match por keywords)."""
        ...
    
    def get_probability(self, market_id: str) -> Optional[float]:
        """Devuelve yes_bid midpoint como probabilidad implícita."""
        ...

Añadir KalshiClient a prediction_markets.py junto a MetaculusClient y ManifoldClient.

  • Archivos: src/data/prediction_markets.py

2.3 — Calculadora de discrepancia (src/signals/discrepancy.py — nuevo)

El motor central de detección de edge:

@dataclass
class AnchorResult:
    source: str           # "odds_api", "metaculus", "manifold", "kalshi"
    probability: float    # fair probability según la fuente
    confidence: str       # "high" | "medium" | "low"
    metadata: dict        # bookmakers usados, nº predicciones, etc.

@dataclass  
class DiscrepancyResult:
    market_question: str
    polymarket_price: float         # YES best_ask del CLOB
    anchor: AnchorResult
    discrepancy_pp: float           # diferencia en puntos porcentuales
    direction: str                  # "polymarket_too_high" | "polymarket_too_low"
    actionable: bool                # discrepancy_pp >= threshold
    suggested_side: str             # "BUY_YES" | "BUY_NO" | "SKIP"

def calculate_discrepancy(
    market: Market,
    threshold_pp: float = 8.0,     # mínimo para ser actionable
    friction_pp: float = 4.0,      # onramp + spread + fees (~3-5%)
) -> Optional[DiscrepancyResult]:
    """
    Encuentra la mejor ancla para el mercado y calcula discrepancia.
    
    Lógica:
    1. Si es deportivo → intenta The Odds API primero
    2. Si es político/macro → intenta Metaculus → Manifold → Kalshi
    3. Si es cultural → intenta Metaculus → Manifold
    4. Calcula discrepancy_pp = abs(polymarket_price - anchor_probability) * 100
    5. actionable = discrepancy_pp >= (threshold_pp + friction_pp)
    """
  • Archivos: src/signals/discrepancy.py

2.4 — Resolution criteria checker (src/signals/resolution_checker.py — nuevo)

Detecta riesgos en la definición del mercado. Muchos edges se pierden por leer mal la pregunta.

@dataclass
class ResolutionRisk:
    risk_level: str          # "low" | "medium" | "high"
    flags: list[str]         # lista de riesgos detectados
    notes: str               # resumen human-readable

RISK_PATTERNS = {
    "ambiguous_source": [r"official", r"announced by", r"per Reuters"],
    "timezone_risk": [r"\d{1,2}:\d{2}", r"by end of", r"before midnight"],
    "definition_risk": [r"nominat", r"appoin", r"elect", r"confirm"],
    "resolution_date_risk": [r"extended", r"delayed", r"postponed"],
}

def check_resolution_risk(market: Market) -> ResolutionRisk:
    """
    Analiza la descripción del mercado en busca de ambigüedades.
    Ejemplos de flags:
    - "nominado" vs "confirmado" — el mercado puede resolver antes de lo esperado
    - Fuente ambigua ("per major media") — quién decide?
    - Timezone no especificada
    - Fecha de resolución futura ambigua
    """
  • Archivos: src/signals/resolution_checker.py

2.5 — Pipeline de candidatos integrado (actualizar market_scanner.py)

Añadir método get_candidates_with_anchors() que orqueste Fase 1 + Fase 2:

def get_candidates_with_anchors(
    self,
    max_pages: int = 3,
    min_discrepancy_pp: float = 8.0,
) -> list[tuple[Market, DiscrepancyResult]]:
    """
    Pipeline completo:
    1. Shortlist (Fase 1): scanner → score → top 10
    2. Para cada candidato: calcular discrepancia vs ancla externa
    3. Devolver solo los que superen el threshold
    """
  • Archivos: src/data/market_scanner.py

Criterios de aceptación Fase 2 ✅

  • OddsAPIClient.get_no_vig_probability() con power devig — validado con mocks
  • KalshiClient integrado — endpoint: api.elections.kalshi.com/trade-api/v2/markets
  • calculate_discrepancy() con routing por categoría (sports→OddsAPI, politics→Metaculus/Kalshi...)
  • check_resolution_risk() detecta: ambiguous_source, timezone_risk, definition_risk, date_ambiguity
  • get_candidates_with_anchors() pipeline integrado Fase 1+2
  • 269/269 tests passing (objetivo era ≥220)

Variables configuradas ✅

  • ODDS_API_KEY en config/.env local + VPS your-vps — free tier 500 req/mes

FASE 3: Research Engine ✅ COMPLETADA

Objetivo: Análisis profundo on-demand de candidatos con discrepancia real Completada: 2026-02-27 | Commits: 88ec244, a36842c, 4128d57, 4d9d95f | Tests: 269 → 336 passing (+67)

Tareas

3.1 — Brave Search integration

  • Usar la Brave Search API ya configurada en OpenClaw (rate limit: 1 req/s)
  • Para cada candidato: buscar noticias recientes + análisis + base rates
  • Búsqueda categórica: política → expertos + encuestas, deportes → stats + lesiones, cultural → campañas + buzz
  • Archivos: src/data/brave_search.py (nuevo)

3.2 — Mejorar event_researcher.py

  • Integrar Brave Search para contexto real (ya no usa solo Metaculus/Manifold)
  • Añadir base rate calculation: "¿con qué frecuencia ocurre esto históricamente?"
  • Añadir source credibility scoring
  • Archivos: src/signals/event_researcher.py

3.3 — Prompt LLM profesional

  • Prompt fijo con estructura de superforecasting (Tetlock framework)
  • Campos obligatorios: base rate, inside view, outside view, pre-mortem, argumento contrario
  • Output estructurado: {"probability": float, "confidence": int, "edge": float, "reasoning": str, "counter_argument": str, "resolution_risk": str}
  • Máximo 1 llamada LLM/día de forma proactiva; ilimitado si Sergio lo pide
  • Archivos: src/signals/llm_prompt.py (nuevo), src/signals/event_researcher.py

3.4 — Combinatorial arb detector (LLM)

  • Para mercados con misma end_date_iso y tema similar: detectar inconsistencias lógicas
  • Ejemplo: "¿Ganará equipo X?" no puede costar menos que "¿Ganará equipo X por >10 puntos?"
  • Agrupar por topic + end_date → pasar al LLM para truth table
  • Archivos: src/signals/combinatorial_arb.py (nuevo)

Criterios de aceptación:

  • research_market() retorna ResearchResult completo con queue file en data/event_queue/ (LLM async, < 2 min end-to-end)
  • Prompt Tetlock con todos los campos requeridos: base rate, inside view, outside view, pre-mortem, counter-argument, JSON schema explícito
  • find_combinatorial_arb() agrupa por fecha (±3 días), keywords (≥2 compartidas), llama LLM solo para pares relacionados

Code Review (2026-02-27) — sin blockers:

  • 🔴 Ningún bug CRITICAL o HIGH
  • 🟡 M-1: MAX_GROUP_SIZE=5 código muerto (topic_markets siempre es par de 2) — aceptable, previsto para extensión futura
  • 🟡 M-2: regex r"\{.*\}" en _parse_llm_inconsistency_response — funciona para el JSON simple esperado; json.loads directo como mejora futura
  • 🟢 L-1: sleep(1) en primer request — conservador, aceptable
  • 🟢 L-2: greedy grouping puede perder pares near-boundary — aceptable para el caso de uso
  • 🟢 L-3: confidence=0 en ResearchResult — intencional (LLM procesa async), documentado

FASE 4: Decision Engine + Alertas ✅ COMPLETADA

Objetivo: Convertir research en decisión accionable con tamaño de posición Completada: 2026-02-27 | Commits: 89c7b40, 7e39915, bb3811a, 3bdf06c, 7478893 | Tests: 336 → 383 passing (+47)

Tareas

4.1 — Mejorar decision_engine.py

  • Integrar todos los signals: arb_spread + discrepancy_score + LLM probability + whale_boost
  • Edge calculation: our_probability - market_price - friction_cost
  • Friction cost: ~3-5% total (onramp + spread + fees) — hardcoded como constante
  • BUY solo si edge > MIN_EDGE (configurable, default 10pp)
  • Archivos: src/execution/decision_engine.py

4.2 — Kelly sizing con friction adjustment

  • Mejorar sizing.py: descontar friction del EV antes de calcular Kelly
  • Cap adicional: "si el upside en $ < $2 para $200 de capital → SKIP"
  • Archivos: src/utils/sizing.py

4.3 — Alert format profesional (Telegram)

  • Formato: mercado + odds Polymarket + ancla + edge + confianza + razonamiento en 3 líneas + contra-argumento + tamaño sugerido
  • Botones inline: BUY / SKIP / ANALIZAR MÁS
  • Archivos: src/execution/alerter.py

4.4 — Polymarket CLI setup

  • Instalar CLI oficial (ARM64 confirmado)
  • Usarlo para orderbook depth (complementa nuestro polymarket_client.py)
  • Preparar para ejecución futura
  • Archivos: scripts/setup_cli.sh, documentación

Criterios de aceptación:

  • ResearchDecisionEngine.decide() produce BUY_YES/BUY_NO/SKIP con sizing — funciona con solo discrepancy_result (sin research)
  • format_research_alert() → mensaje Telegram + botones inline OpenClaw (str, list[list[dict]])
  • calculate_research_size() proporcional con cap 5%, independiente de calculate_kelly_size()
  • scripts/setup_notes.md documenta estado ARM64 + opciones de instalación
  • 47 nuevos tests, todos passing (383 total, 0 failed)

Code Review (2026-02-27) — commit 66f26a2 — sin blockers:

  • 🟡 M-1: Docstring _resolve_probability() engañoso → reescrito para reflejar comportamiento real
  • 🟡 M-2: Sin test para research(low_conf) + discrepancy(actionable) → +3 tests en TestResolvePriorityBehavior
  • 🟢 L-1: +{edge_net_pp:.1f}pp hardcodeado → {edge_net_pp:+.1f}pp (sign formatting correcto)
  • 🟢 L-2: Bug pre-existente format_daily_summary() retornaba "" cuando sin sym_lines → fix + test de regresión
  • Tests finales: 388 passing, 0 failed (+5 del review)

FASE 5: Tracking y Aprendizaje ✅ COMPLETADA

Objetivo: Medir si nuestras predicciones son buenas y aprender Completada: 2026-02-27 | Tests: 388 → 445 passing (+57, 0 failed)

Commits

  • feat(phase5.1) 99fec26 — 8 campos nuevos en Decision + migraciones SQLite
  • feat(phase5.1b) 5ff2f3fsave_research_decision() + get_research_decisions() en TradeDB
  • feat(phase5.3) 6b5c11bcalibration.py nuevo (~280 LOC)
  • feat(phase5.4) 5fbf53ecalibration_report() en ReflectionEngine + get_category_performance() en OutcomeTracker
  • fix(phase5) 9508c96 — edge_calibration en unidades pp, schema incluye columnas nuevas, +57 tests

Code Review (2026-02-27) — commit 9508c96:

  • 🟡 M-1: edge_calibration() usaba ROI% vs pp → fix: (actual_outcome - market_price)*100
  • 🟡 M-2: test_phase5.py no creado por sub-agente → escrito manualmente (57 tests)
  • 🟢 L-1: Alias RD importado pero no usado → eliminado, comentario explicativo
  • 🟢 L-2: SCHEMA sin columnas nuevas → añadidas para bases de datos desde cero

Tareas

5.1 — Trade tracker completo

  • Registrar cada decisión: mercado, fecha, odds al entrar, our_probability, edge calculado, confidence, fuente de anchor, LLM usado
  • Archivos: src/audit/trade_db.py (mejorar el existente)

5.2 — Outcome tracker

  • Verificar resolución de mercados donde tomamos posición
  • Calcular P&L real vs esperado
  • get_category_performance() añadido — delega a calibration.win_rate_by_category()
  • Archivos: src/audit/outcome_tracker.py (mejorar el existente)

5.3 — Calibración

  • Brier score: ¿nuestras probabilidades son correctas?
  • Win rate por categoría: ¿dónde somos mejores?
  • Edge realizado vs edge esperado (Pearson correlation incluida)
  • Archivos: src/audit/calibration.py (nuevo)

5.4 — Playbook automático

  • calibration_report() añadido a ReflectionEngine — report completo con una llamada
  • Archivos: src/audit/reflection.py

Criterios de aceptación:

  • Tras 5 decisiones, calibration.py devuelve métricas básicas
  • TradeDB.save_research_decision() funciona end-to-end
  • calibration.brier_score([])0.0 sin crash
  • calibration.summary_metrics(decisions) devuelve dict con todos los campos

FASE 6: Orquestación y Documentación ✅ COMPLETADA

Objetivo: Flujo completo ejecutable con un comando, documentación impecable Completada: 2026-02-27 | Tests: 472/472 passing (+27 nuevos)

Tareas

6.1 — Script de research diario

  • run_research.py --mode daily: scan → anchors → decide → alert Telegram
  • run_research.py --market <slug>: análisis on-demand por slug o condition_id
  • run_research.py --calibration: métricas de calibración del historial
  • run_research.py --status: estado del sistema (pending decisions, estadísticas)
  • Archivos: run_research.py

6.2 — Cron on-demand (no automático)

  • NO pipeline de alta frecuencia
  • scripts/setup_cron.sh: muestra configuración OpenClaw cron (9:00 AM Madrid)
  • Archivos: scripts/setup_cron.sh

6.3 — README y documentación final

  • README reescrito: estrategia, uso rápido, estructura, pipeline técnico, configuración
  • Archivos: README.md

6.4 — Tests de integración

  • 27 tests mockeados para run_research.py (sin llamadas reales a APIs)
  • TestRunDailyScan, TestRunMarketAnalysis, TestRunCalibration, TestRunStatus, TestMainCLI
  • Archivos: tests/test_phase6.py

Resumen de Fases

Fase Descripción Estado Tests
1 Market Discovery Engine ✅ Completada + reviewed 200
2 External Anchors ✅ Completada + reviewed 269
3 Research Engine ✅ Completada + reviewed 336
4 Decision Engine + Alertas ✅ Completada + reviewed 388
5 Tracking y Aprendizaje ✅ Completada + reviewed 445
6 Orquestación y Docs ✅ Completada 472

Pipeline completo: run_research.py --mode daily ejecuta las 6 fases de extremo a extremo. Test suite: 472 tests, 0 fallos.


Módulos entregados

Nuevos (creados en este proyecto)

  • src/signals/arb_detector.py — YES+NO < $1 con precios CLOB reales
  • src/signals/discrepancy.py — calculadora con routing por categoría
  • src/signals/resolution_checker.py — detecta ambigüedades en criterios
  • src/signals/llm_prompt.py — prompt Tetlock (Fase 3)
  • src/signals/combinatorial_arb.py — inconsistencias lógicas entre mercados
  • src/data/odds_api.py — The Odds API con power devig
  • src/data/brave_search.py — búsqueda web por categoría
  • src/audit/calibration.py — Brier score, win rate, edge calibration
  • run_research.py — script principal (4 modos)
  • scripts/setup_cron.sh — configuración cron OpenClaw
  • scripts/setup_notes.md — notas Polymarket CLI ARM64

Modificados (existentes extendidos)

  • src/data/market_scanner.py — paginación + scoring compuesto
  • src/data/polymarket_client.py — best ask via /price?side=sell
  • src/data/prediction_markets.py — Kalshi añadido
  • src/signals/event_researcher.py — Brave Search + research_market()
  • src/execution/decision_engine.py — ResearchDecisionEngine (Fase 4)
  • src/execution/alerter.py — format_research_alert() + botones inline
  • src/utils/sizing.py — calculate_research_size()
  • src/audit/trade_db.py — 8 campos research + save_research_decision()
  • src/audit/outcome_tracker.py — get_category_performance()
  • src/audit/reflection.py — calibration_report()

Próximos pasos operacionales

El pipeline de código está completo. Lo que queda es operacional, no de desarrollo.

🔑 Prerrequisito: API Keys

Servicio Estado Necesario para
Brave Search API ✅ Configurada Fase 3 (web context)
The Odds API ✅ Configurada Fase 2 sports anchor
Polymarket wallet ❌ Pendiente Ejecución real de trades

🧪 Paso 1: Validación read-only (sin dinero)

# Verificar que el pipeline funciona end-to-end con APIs reales
python run_research.py --mode daily --verbose

# Analizar un mercado concreto de interés
python run_research.py --market <slug>

Objetivo: confirmar que los candidatos BUY tienen edge real, no ruido. Criterio: ≥5 candidatos revisados manualmente y encontrados razonables.

📊 Paso 2: Paper trading activo (2-4 semanas)

  • Ejecutar daily scan cada día (manual o cron)
  • Registrar decisiones BUY en DB aunque no se ejecuten con dinero real
  • Cuando el mercado resuelva, OutcomeTracker.check_pending() actualiza WIN/LOSS
  • Revisar --calibration semanalmente: ¿el Brier score mejora? ¿win rate > 50%?

Criterio de avance: ≥10 mercados resueltos, win rate ≥ 55%.

💰 Paso 3: Live trading con presupuesto mínimo ($20)

  1. Crear cuenta Polymarket + depositar $20 USDC
  2. Ejecutar primera operación manual basada en señal del bot
  3. Verificar que el tracking de P&L funciona correctamente
  4. Escalar a $100 si los primeros 10 trades son consistentes

🏅 Fase futura: Betfair (deportes)

  • Crear cuenta Betfair (Sergio pendiente)
  • betfairlightweight es compatible con ARM64 — instalación directa
  • The Odds API ya tiene integración lista para anclar probabilidades

⚙️ Cron opcional (si se quiere automatizar)

Ver scripts/setup_cron.sh para configuración completa. Requiere añadir entry en ~/.openclaw/openclaw.json. Recomendación: empezar manual hasta validar que las alertas son accionables.