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
NO es arb sistemático. Es research edge:
- Encontrar mercados donde el precio de Polymarket diverge de la evidencia pública disponible
- Analizar con LLM + fuentes externas solo cuando hay discrepancia real (≥8-10pp)
- 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)
Objetivo: Scanner profesional que encuentra candidatos reales Completada: 2026-02-26 | Commit: e05bad5 | Tests: 200/200 passing
1.1 — Arreglar paginación del scanner
- Problema actual:
market_scanner.pysolo obtiene top 100 por volumen. Polymarket tiene miles. - Fix: implementar paginación completa con
offseten 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
outcomePricesde Gamma API (midpoints/estimados) - Fix: para candidatos, obtener
best_askdel CLOB orderbook - Esto es crítico para el arb real:
best_ask_yes + best_ask_novs $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_spreaden 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_askse obtiene del CLOB endpoint/price?side=sell(fix crítico:/bookdaba falsos positivos) -
arb_spreadcalculado 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_clobdelega aPolymarketClient→ cero duplicación - 🟠 H-3:
_15MIN_PATTERNdead code eliminada - 🟡 M-1/M-2:
import json/mathmovidos al top;search_marketsconmax_pages=3 - 🟡 M-3:
CRYPTO_CATEGORIESset con todas las variantes crypto - 🟡 M-4:
raw: Optional[dict] = Nonetyping correcto - 🟡 M-5: Docstring
liquiditycon 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
Objetivo: Comparar odds de Polymarket con fuentes externas para detectar edge real Completada: 2026-02-26 | Tests: 269/269 passing (+69 nuevos)
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.
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
- Archivos:
src/data/odds_api.py - Env var:
ODDS_API_KEY(free tier en https://the-odds-api.com)
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
-
OddsAPIClient.get_no_vig_probability()con power devig — validado con mocks -
KalshiClientintegrado — 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)
ODDS_API_KEYenconfig/.envlocal + VPS your-vps — free tier 500 req/mes
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)
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_isoy 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()retornaResearchResultcompleto con queue file endata/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=5có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=0en ResearchResult — intencional (LLM procesa async), documentado
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)
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 solodiscrepancy_result(sin research) -
format_research_alert()→ mensaje Telegram + botones inline OpenClaw (str, list[list[dict]]) -
calculate_research_size()proporcional con cap 5%, independiente decalculate_kelly_size() -
scripts/setup_notes.mddocumenta 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 enTestResolvePriorityBehavior - 🟢 L-1:
+{edge_net_pp:.1f}pphardcodeado →{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)
Objetivo: Medir si nuestras predicciones son buenas y aprender Completada: 2026-02-27 | Tests: 388 → 445 passing (+57, 0 failed)
feat(phase5.1)99fec26— 8 campos nuevos en Decision + migraciones SQLitefeat(phase5.1b)5ff2f3f—save_research_decision()+get_research_decisions()en TradeDBfeat(phase5.3)6b5c11b—calibration.pynuevo (~280 LOC)feat(phase5.4)5fbf53e—calibration_report()en ReflectionEngine +get_category_performance()en OutcomeTrackerfix(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.pyno creado por sub-agente → escrito manualmente (57 tests) - 🟢 L-1: Alias
RDimportado pero no usado → eliminado, comentario explicativo - 🟢 L-2: SCHEMA sin columnas nuevas → añadidas para bases de datos desde cero
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 acalibration.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 aReflectionEngine— report completo con una llamada- Archivos:
src/audit/reflection.py
Criterios de aceptación:
- Tras 5 decisiones,
calibration.pydevuelve métricas básicas -
TradeDB.save_research_decision()funciona end-to-end -
calibration.brier_score([])→0.0sin crash -
calibration.summary_metrics(decisions)devuelve dict con todos los campos
Objetivo: Flujo completo ejecutable con un comando, documentación impecable Completada: 2026-02-27 | Tests: 472/472 passing (+27 nuevos)
6.1 — Script de research diario ✅
run_research.py --mode daily: scan → anchors → decide → alert Telegramrun_research.py --market <slug>: análisis on-demand por slug o condition_idrun_research.py --calibration: métricas de calibración del historialrun_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
| 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.
- ✅
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
- ✅
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()
El pipeline de código está completo. Lo que queda es operacional, no de desarrollo.
| 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 |
# 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.
- 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
--calibrationsemanalmente: ¿el Brier score mejora? ¿win rate > 50%?
Criterio de avance: ≥10 mercados resueltos, win rate ≥ 55%.
- Crear cuenta Polymarket + depositar $20 USDC
- Ejecutar primera operación manual basada en señal del bot
- Verificar que el tracking de P&L funciona correctamente
- Escalar a $100 si los primeros 10 trades son consistentes
- Crear cuenta Betfair (Sergio pendiente)
betfairlightweightes compatible con ARM64 — instalación directa- The Odds API ya tiene integración lista para anclar probabilidades
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.