# Football Betting Analysis

> Análisis pre-partido de fútbol en 8 capas. Recibe una consulta en lenguaje natural, descubre el partido en FlashScore MCP, ejecuta build_match_context.py para obtener un contexto normalizado en JSON, y produce un informe estructurado con lenguaje probabilístico. Solo para partidos notstarted. No inventa datos, no suple la predicción con modelos heurísticos.

- Skill: `josearroyave/football-betting-analysis` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add josearroyave/football-betting-analysis`
- Raw SKILL.md: https://api.skillmd.com/api/skills/josearroyave/football-betting-analysis/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: josearroyave (https://skillmd.com/u/josearroyave)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/josearroyave/football-betting-analysis

---


# Football Betting Analysis

## 1. Overview

Análisis pre-partido de fútbol. Recibe una consulta en lenguaje natural, descubre
el partido en FlashScore MCP, ejecuta el script de preprocesamiento
`build_match_context.py` para obtener un contexto JSON normalizado, y produce un
informe estructurado con lenguaje probabilístico.

**Solo para partidos `notstarted` e `inprogress`.** Para partidos `finished` esta skill no aplica.

El modelo no llama endpoints directamente (salvo para match discovery).
Recibe datos ya limpios, normalizados y consolidados en `final_context`.

El análisis es una interpretación de la evidencia disponible. No es un pick.
No es una garantía. Siempre usa lenguaje como "podría", "señal", "sugiere".
Nunca: "va a ganar", "es fijo", "el over entra seguro".

---

## 2. When to Use / When Not to Use

### Usar cuando:

- El usuario pide análisis pre-partido de un partido específico de fútbol.
- El partido está en estado `notstarted`.

### No usar cuando:

- El partido ya está `finished` → esta skill no aplica.
- La consulta es sobre un torneo o equipo sin partido específico → no procede. Se puede ofrecer buscar partidos próximos de ese equipo.
- `final_context` no pudo generarse.

**Nota arquitectónica:** Esta versión no dispone de predicción basada en modelos
de aprendizaje. Por diseño, la capa predictiva es odds-driven — las probabilidades
numéricas SOLO viennent de odds. Los indicadores solo modulan confianza, no generan
porcentajes.

---

## 3. Fuente de Datos — FlashScore MCP

### Endpoints utilizados

| Endpoint                        | Uso                                                                                      |
| ------------------------------- | ---------------------------------------------------------------------------------------- |
| `Get_Match_Details`             | Evento completo + odds                                                                   |
| `Get_Match_H2H`                 | Head-to-head                                                                             |
| `Get_Match_Stats`               | Llamado en paralelo para cada partido del historial (matches[i]) — solo para construir form.advanced agregado |
| `Get_Match_Lineups`             | Alineaciones si existen + `missingPlayers` con nombre y motivo de ausencia               |
| `Get_Team_Results`              | Match discovery (fallback) + historial de resultados del equipo                          |
| `Get_Team_Fixtures`             | Match discovery (primario) — próximos partidos del equipo                                |
| `Get_Tournament_Standings`      | Contexto de forma/posición en torneo                                                     |
| `Get_Match_Standings_OverUnder` | Standings O/U                                                                            |
| `Get_Tournament_Top_Scorers`    | Goleadores del torneo                                                                    |
| `Get_Match_Standings_Form`      | Forma reciente dentro del partido                                                        |

### Lo que NO existe en FlashScore (y nunca se debe inventar)

| Qué no existe                                     | Consecuencia                                                                                                                                            |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Alineaciones confirmadas pre-partido              | El endpoint `Get_Match_Lineups` existe, pero las alineaciones confirmadas pueden no estar disponibles aún. Pueden venir vacías. No inventar alineación. Usar `predictedLineups` si está disponible — el script lo procesa automáticamente. |
| Histórico de lesiones pre-existentes              | No existe en la API. No inventar.                                                                                                                       |
| `missingPlayers` sin datos en `Get_Match_Lineups` | Si el endpoint no lo trae → no inventar motivo ni jugador.                                                                                              |

**Comportamiento de `normalize_lineups` en `build_match_context.py`:**

El script procesa las alineaciones en dos niveles de prioridad:

1. **Si `startingLineups` existe (alineaciones oficiales confirmadas):**
   - `starting_lineups` — alineación titular confirmada [API]
   - `missing_players` — jugadores ausentes con motivo [API]
   - `substitutes` — sustitutos disponibles [API]
   - `predicted_lineups` → vacío
   - `unsure_missing` — vacío

2. **Si `startingLineups` NO existe pero `predictedLineups` sí (alineación predicha/no confirmada):**
   - `unsure_missing` — jugadores cuya ausencia no está confirmada [API]
   - `missing_players` → jugadores ausentes con motivo [API]
   - `predicted_lineups` — alineación predicha [API]
   - `starting_lineups` → vacío
   - `substitutes` → vacío

**Nota:** `Get_Match_Lineups` puede aportar `missingPlayers` (nombre + motivo) incluso cuando las alineacionesConfirmadas no existen. Usar ese dato cuando esté disponible.

**Fuente de cada señal en el análisis:**

| Tag      | Significado                                         |
| -------- | --------------------------------------------------- |
| `[API]`  | Dato directo de un endpoint de FlashScore MCP       |
| `[ODDS]` | Calculado de cuotas de mercado                      |
| `[IND]`  | Indicador calculado a partir de los datos de la API |
| `[N/A]`  | Dato no disponible en la API                        |

---

## 4. Pipeline de Análisis

### 4.1 Parsing de la consulta

De la consulta en lenguaje natural extraer:

```
{
  home_team:  string,      // equipo mencionado primero
  away_team:  string,       // equipo mencionado segundo (o null si solo uno)
  date_from:  ISO 8601,    // fecha inicio del rango
  date_to:    ISO 8601,    // fecha fin del rango
  league:     string|null,  // competición mencionada (o null)
  analysis_mode: "prematch"  // único modo soportado
}
```

**Reglas de parsing:**

| Situación                                         | Regla                                                         |
| ------------------------------------------------- | ------------------------------------------------------------- |
| El usuario dice "hoy"                             | `date_from = hoy`, `date_to = hoy`                            |
| El usuario dice "mañana"                          | `date_from = mañana`, `date_to = mañana`                      |
| El usuario dice "este finde"                      | `date_from = viernes`, `date_to = domingo`                    |
| El usuario dice "esta semana" sin día             | `date_from = lunes`, `date_to = domingo`                      |
| Solo un equipo mencionado                         | Buscar el próximo partido de ese equipo en el rango de 7 días |
| Nombres con acento ("Atletico", "Inter")          | Normalizar quitando acentos antes de buscar                   |
| Nombres parciales ("Barça", "Atleti")             | Fuzzy match contra `home_team`/`away_team` del resultado      |
| Competición mencionada ("la league", "champions") | Filtrar por `league_id` tras buscar                           |

| `status = "notstarted"` | `analysis_mode = "prematch"` → proceed |
| `status = "inprogress"` | → Proceed con análisis en vivo. |
| `status = "finished"` | → No aplica. Notificar al usuario. |

**Si solo un equipo matchea pero hay múltiples candidatos del rival:**
→ Clarificación obligatoria. Preguntar: "¿Buscás el [equipo] vs [candidato A] o vs [candidato B]?"

### 4.2 Match Discovery (Fase 1 — SOLO esta fase usa MCP directamente)

**Paso 1 — Búsqueda por API de búsqueda de equipos:**

```
1. Normalizar nombres de ambos equipos (minúsculas, sin acentos, sin puntos)
2. Usar la API de búsqueda:
   GET https://s.livesport.services/api/v2/search/?q=<query>&lang-id=13&type-ids=1,2,3,4&project-id=202&project-type-id=1
3. Filtrar resultados: sport.id=1 (fútbol), type.id=2 (Team)
4. Retorna: team_id, name, url, country, country_id
   → Si 1 resultado: proceed con ese team_id
   → Si >1 resultado: clarificación obligatoria ("¿Buscás [equipo A] o [equipo B]?")
   → Si 0 resultados: ir a paso 5 (fallback a /data/main_teams.csv)
```

**Normalización para la API de búsqueda:**

- Minúsculas
- Quitar acentos: á→a, é→e, í→i, ó→o, ú→u
- Quitar puntos: "Atl." → "Atl"
- Quitar espacios extra

**Paso 2 — Lookup de equipos en /data/main_teams.csv (fallback):**

```
5. Si no se encontró el equipo en la API, buscar en /data/main_teams.csv:
   Normalizar nombres (mismas reglas que arriba)
6. Buscar match exacto en /data/main_teams.csv después de normalizar
   → Si 1 resultado: proceed
   → Si >1 resultado: clarificación obligatoria
7. Si 0 resultados exactos: intentar substring match
   → Si 1 resultado: proceed
   → Si >1 resultado: clarificación obligatoria
8. Si 0 resultados: buscar coincidencias cercanas (similitud string)
   → Calcular similaridad entre nombre buscado y todos los nombres en /data/main_teams.csv
   → Si hay candidatos con similitud ≥ umbral (ej: Levenshtein ratio ≥ 0.6):
      → Si 1 candidato cercano: "¿Quisiste decir [nombre]?"
      → Si >1 candidatos: mostrar top 3: "¿Buscás alguno de estos? [A], [B], [C]"
   → Si ningún candidato cercano: "No encontré '[equipo]'. Verificá el nombre."
```

**Sugerencia de candidatos cercanos:**

- Umbral de similaridad: ratio ≥ 0.6 (Levenshtein o similar)
- Mostrar máximo 3 candidatos ordenados por similaridad descendente
- Incluir el nombre original del CSV (sin normalizar) en la sugerencia

**Paso 3 — Búsqueda por overlap en fixtures:**

```
 9. Get_Team_Fixtures(team_id_home, page=1)
10. Get_Team_Fixtures(team_id_away, page=1)
11. Filtrar fixtures del rango date_from..date_to
12. Buscar partido donde home_team_id o away_team_id de un fixture
    coincida con el team_id del otro equipo (overlap)
    → Si 1 resultado: proceed
    → Si >1 resultado: priorizar por fecha más cercana al rango solicitado
    → Si 0: ir a paso 4
```

**Paso 4 — Fallback a results:**

```
13. Get_Team_Results(team_id_home, page=1)
14. Get_Team_Results(team_id_away, page=1)
15. Buscar overlap (misma lógica que fixtures)
    → Si 1 resultado: proceed
    → Si >1: priorizar por fecha más cercana
    → Si 0: "No encontré el partido [Home] vs [Away] en las fechas indicadas."
```

**Paso 5 — Verificación y extracción:**

```
16. Verificar estado del evento:
    → status = "notstarted" → proceed con análisis pre-partido
    → status = "inprogress" → proceed con análisis en vivo
    → status = "finished" → "El partido ya finalizó. Esta skill es solo pre-partido y en vivo."
17. Extraer IDs:
    → event_id = del evento encontrado
    → home_team_id = del equipo local
    → away_team_id = del equipo visitante
```

**Priorización cuando hay múltiples candidatos:**

1. Si el usuario mencionó liga → priorizar esa liga.
2. Si el usuario mencionó fecha exacta → solo considerar esa fecha.
3. Si hay exactamente 1 partido de cada equipo combinado → usar ese.
4. **Si múltiples partidos del mismo equipo → priorizar el más cercano en el tiempo al rango de fechas de la consulta.** Esto evita clarificaciones innecesarias.
5. Si no, clarificación obligatoria.

### 4.3 Preprocesamiento (Fase 2 — build_match_context.py)

**Esta es la única manera de obtener datos para el análisis. No usar endpoints MCP directamente para obtener datos del partido.**

```
python scripts/build_match_context.py <event_id> <home_team_id> <away_team_id>
```

**Qué hace el script:**

1. Ejecuta todas las llamadas a endpoints de FlashScore internamente
2. Construye H2H desde el historial de resultados de ambos equipos
3. Resume historial de forma (GF, GC, over, BTTS, home/away split) — `form.basic`
4. Extrae y normaliza ausencias desde `Get_Match_Lineups`
5. Valida qué mercados de odds existen realmente y bloquea los que no existan
6. Integra estadísticas avanzadas de cada partido del historial (tiros, posesión, xG, etc.) — solo para construir `form.advanced` (los datos individuales no se emiten en el JSON final)
7. Agrega player stats históricas por equipo (`player_stats.as_historical_home/away`)
8. Incorpora contexto de torneo (posición, forma, over/under standings)
9. Emite warnings de consistencia
10. Consolida toda la información en un único JSON (`final_context`)

**Output del script — `final_context`:**

```
{
  "meta": {
    event_id, home_team_id, away_team_id, generated_at
  },
  "match": {
    event_id,
    home_team: { id, name, event_participant_id },
    away_team: { id, name, event_participant_id },
    tournament: { id, stage_id, name },
    country, referee, timestamp, datetime, status,
    scores: {
      home, away,
      home_total, away_total,
      home_1st_half, away_1st_half,
      home_2nd_half, away_2nd_half,
      home_extra_time, away_extra_time,
      home_penalties, away_penalties
    },
    "lineups": {
      home: {
        formation,              // formación reportada (de predictedFormation)
        lineup_count,           // cantidad de jugadores en alineación
        // -- Si hay alineaciones oficiales (startingLineups existe): --
        starting_lineups: [...],       // alineación titular oficial [API]
        missing_players: [...],        // jugadores ausentes con motivo [API]
        substitutes: [...],            // sustitutos disponibles [API]
        predicted_lineups: [],        // vacío
        unsure_missing: [],           // vacío
        // -- Si NO hay oficiales pero hay predicción (predictedLineups): --
        starting_lineups: [],          // vacío
        missing_players: [],           // vacío
        substitutes: [],               // vacío
        predicted_lineups: [...],      // alineación predicha [API]
        unsure_missing: [...],         // ausencias no confirmadas [API]
      },
      away: { formation, lineup_count, starting_lineups, missing_players, substitutes, predicted_lineups, unsure_missing },
      warnings: string[] | null
    },
    "preview": string | null,  // texto de previa del partido — web scraping de FlashScore (DOM o contentParsed embebido)
    // --- inprogress or finished only ---
    "summary": object | null,    // eventos clave del partido (goles, tarjetas, etc.) [API]
    "commentary": object | null, // narración minuto a minuto [API]
    "match_stats": object | null, // estadísticas del partido (posesión, tiros, xG, etc.) [API]
    "player_stats": object | null, // stats de jugadores del partido actual [API]
    "live_analysis": {           // solo para status = "inprogress"
      total_goals_over_25: { probability, signal, confidence },
      total_goals_under_25: { ... },
      btts_yes: { ... },
      btts_no: { ... },
      next_goal_home: { ... },
      next_goal_away: { ... },
      next_goal_no_more: { ... },  // explicit class — no more goals in the match
      next_corner: { lean, signal, confidence, mode: "signal_only" },
      total_cards: { lean, signal, confidence, mode: "signal_only" },
      1x2_final: { probabilities: { home, draw, away }, model_version, calibrated, inputs_quality, top_factors, prematch_source, confidence, warnings },
      match_minute: int,
      current_score: { home: int, away: int },
      warnings: []
    } | null,
    warnings: string[] | null
  },
  "odds": {
    available_markets: string[],
    odds_home, odds_draw, odds_away,
    odds_over_25, odds_under_25,
    odds_btts_yes, odds_btts_no,
    warnings: string[]
  },
  "implied_probs": {
    prob_home, prob_draw, prob_away,
    prob_over_25, prob_btts_yes
  },
  "h2h": {
    total_matches,
    home_team_wins,   // partidos ganados por el equipo local del análisis
    away_team_wins,   // partidos ganados por el equipo visitante del análisis
    draws,
    home_team_goals_for,   // goles anotados por el equipo local del análisis en H2H
    away_team_goals_for,   // goles anotados por el equipo visitante del análisis en H2H
    total_goals, both_teams_scored,
    matches: [{
      match_id, timestamp,
      home_score, away_score,
      home_team, away_team,
      tournament_id, tournament_name,
      team_is_home, opponent
    }],
    basic_stats: {               // stats básicos del historial H2H
      // home_team = stats of the team that IS home in the CURRENT match (Bayern)
      // away_team = stats of the team that IS away in the CURRENT match (Real Madrid)
      // form_string/points/home_ppg/away_ppg excluded — not meaningful in h2h context
      home_team: {
        wins, losses, draws,
        gf_avg, gc_avg, total_goals
      },
      away_team: {
        wins, losses, draws,
        gf_avg, gc_avg, total_goals
      },
      both_teams_scored,
      over_25_freq, btts_freq,
      total_matches,
      total_goals
    },
    advanced_stats: {             // stats agregadas del historial (de advanced_stats de cada match)
      // For home_team_results / away_team_results: same flat structure
      // For h2h: split by current-match home/away identity
      // Each sub-dict has: { overall: {...}, first_half: {...}, second_half: {...} }
      home_team: {
        overall: { attack: {...}, defense: {...}, control: {...}, set_pieces_and_territory: {...}, discipline: {...}, duels_and_defending: {...}, efficiency: {...}, derived: {...} },
        first_half: { /* mismo formato que overall */ },
        second_half: { /* mismo formato que overall */ }
      },
      away_team: {
        overall: { attack: {...}, defense: {...}, control: {...}, set_pieces_and_territory: {...}, discipline: {...}, duels_and_defending: {...}, efficiency: {...}, derived: {...} },
        first_half: { /* mismo formato que overall */ },
        second_half: { /* mismo formato que overall */ }
      }
    },
    home_team_player_stats: {
      general: { minutes_total, goals_total, assists_total, shots_total, shots_on_target_total, key_passes_total, tackles_won_total, interceptions_total, ball_recoveries_total, yellow_cards_total, red_cards_total, goals_per_90, assists_per_90, shots_per_90, position_distribution, in_base_lineup_count, substitute_count },
      individual: {
        [player_name]: {
          "position": "Goalkeeper" | "Defender" | "Midfielder" | "Forward" | "Unknown",
          offense:    { GOALS, EXPECTED_GOALS, ASSISTS_GOAL, EXPECTED_ASSISTS, SHOTS_TOTAL, SHOTS_ON_TARGET, BIG_CHANCES_CREATED, BIG_CHANCES_MISSED },
          creation:   { KEY_PASSES, FINAL_THIRD_ENTRIES_TOTAL, BOX_ENTRIES, THROUGH_BALLS },
          possession: { TOUCHES_TOTAL, MATCH_MINUTES_PLAYED, PASSES_TOTAL },
          defense:    { DUELS_WON, DUELS_TOTAL, DUELS_EFFICIENCY, TACKLES_WON, INTERCEPTIONS, BALL_RECOVERIES },
          efficiency: { PASSES_ACCURACY, LONG_BALLS_ACCURACY, CROSSES_ACCURACY, DRIBBLES_EFFICIENCY },
          discipline: { FOULS_COMMITTED, FOULS_SUFFERED, CARDS_YELLOW, CARDS_RED, TURNOVERS, ERRORS_LEAD_TO_SHOT, ERRORS_LEAD_TO_GOAL },
          goalkeeping:{ SAVES_TOTAL, GOALS_CONCEDED, GOALS_PREVENTED, EXPECTED_GOALS_ON_TARGET_FACED, BIG_CHANCES_SAVED },
          derived:    { xg_per_shot, goals_minus_xg, xa_minus_assists, duels_win_pct, actions_per_90 }
        }, ...
      }
    },
    away_team_player_stats: { /* misma estructura */ },
    warnings: string[]
  },
  "home_team_results": {
    matches: [{
      match_id, timestamp,
      goals_for, goals_against, total_goals, both_teams_scored,
      tournament_id, tournament_name,
      team_is_home, opponent
    }],
    basic_stats: {               // stats básicos del historial
      all_matches, form_string, points,
      home_gf_avg, home_gc_avg,
      over_25_freq, btts_freq,
      home_ppg, home_gf_avg, home_gc_avg,
      away_ppg, away_team_gf_avg, away_team_gc_avg,
      total_matches, wins, draws, losses,
      goals_for, goals_against, total_goals, both_teams_scored
    },
    advanced_stats: {            // stats agregadas del historial (de advanced_stats de cada match)
      overall: {
        attack: {
          goals_for_avg, xg_for_avg, xgot_for_avg, xa_for_avg,
          shots_for_avg, shots_on_target_for_avg, shots_off_target_for_avg,
          blocked_shots_for_avg, shots_inside_box_for_avg, shots_outside_box_for_avg,
          big_chances_for_avg, touches_in_opposition_box_avg, hit_woodwork_avg
        },
        defense: {
          goals_against_avg, xg_against_avg, xgot_faced_avg,
          shots_against_avg, shots_on_target_against_avg, shots_off_target_against_avg,
          blocked_shots_against_avg, shots_inside_box_against_avg, shots_outside_box_against_avg,
          big_chances_against_avg, touches_in_opposition_box_against_avg,
          goalkeeper_saves_avg, errors_leading_to_shot_avg, errors_leading_to_goal_avg, goals_prevented_avg
        },
        control: {
          possession_avg, passes_accuracy_avg, passes_completed_avg, passes_attempted_avg,
          long_pass_accuracy_avg, long_passes_completed_avg, long_passes_attempted_avg,
          final_third_pass_accuracy_avg, final_third_passes_completed_avg, final_third_passes_attempted_avg,
          accurate_through_passes_avg
        },
        set_pieces_and_territory: {
          corners_for_avg, corners_against_avg, offsides_for_avg, offsides_against_avg,
          free_kicks_for_avg, free_kicks_against_avg, throw_ins_for_avg, throw_ins_against_avg,
          cross_accuracy_avg, crosses_completed_avg, crosses_attempted_avg
        },
        discipline: {
          yellow_cards_avg, red_cards_avg, cards_total_avg, fouls_committed_avg
        },
        duels_and_defending: {
          tackles_success_pct_avg, tackles_won_avg, tackles_attempted_avg,
          duels_won_avg, clearances_avg, interceptions_avg
        },
        efficiency: {
          shot_accuracy_pct, goal_conversion_pct, big_chance_conversion_pct,
          xg_per_shot, shots_on_target_faced_per_goal_against, save_pct,
          finishing_overperformance, conceding_overperformance
        },
        derived: {
          xg_balance_avg, xg_ratio, shots_share, shots_on_target_share,
          big_chances_balance_avg, corners_balance_avg, discipline_balance_avg
        }
      },
      first_half: { /* mismo formato que overall */ },
      second_half: { /* mismo formato que overall */ },
      warnings: []
    },
    player_stats_as_home: {
      general: { minutes_total, goals_total, assists_total, shots_total, shots_on_target_total, key_passes_total, tackles_won_total, interceptions_total, ball_recoveries_total, yellow_cards_total, red_cards_total, goals_per_90, assists_per_90, shots_per_90, position_distribution, in_base_lineup_count, substitute_count },
      individual: {
        [player_name]: {
          "position": "Goalkeeper" | "Defender" | "Midfielder" | "Forward" | "Unknown",
          offense:    { GOALS, EXPECTED_GOALS, ASSISTS_GOAL, EXPECTED_ASSISTS, SHOTS_TOTAL, SHOTS_ON_TARGET, BIG_CHANCES_CREATED, BIG_CHANCES_MISSED },
          creation:   { KEY_PASSES, FINAL_THIRD_ENTRIES_TOTAL, BOX_ENTRIES, THROUGH_BALLS },
          possession: { TOUCHES_TOTAL, MATCH_MINUTES_PLAYED, PASSES_TOTAL },
          defense:    { DUELS_WON, DUELS_TOTAL, DUELS_EFFICIENCY, TACKLES_WON, INTERCEPTIONS, BALL_RECOVERIES },
          efficiency: { PASSES_ACCURACY, LONG_BALLS_ACCURACY, CROSSES_ACCURACY, DRIBBLES_EFFICIENCY },
          discipline: { FOULS_COMMITTED, FOULS_SUFFERED, CARDS_YELLOW, CARDS_RED, TURNOVERS, ERRORS_LEAD_TO_SHOT, ERRORS_LEAD_TO_GOAL },
          goalkeeping:{ SAVES_TOTAL, GOALS_CONCEDED, GOALS_PREVENTED, EXPECTED_GOALS_ON_TARGET_FACED, BIG_CHANCES_SAVED },
          derived:    { xg_per_shot, goals_minus_xg, xa_minus_assists, duels_win_pct, actions_per_90 }
        }, ...
      }
    },
    player_stats_as_away: { /* misma estructura */ },
    warnings: string[] | null
  },
  "away_team_results": {
    matches: [{
      match_id, timestamp,
      goals_for, goals_against, total_goals, both_teams_scored,
      tournament_id, tournament_name,
      team_is_home, opponent
    }],
    basic_stats: {               // stats básicos del historial
      all_matches, form_string, points,
      home_gf_avg, home_gc_avg,
      over_25_freq, btts_freq,
      home_ppg, home_gf_avg, home_gc_avg,
      away_ppg, away_team_gf_avg, away_team_gc_avg,
      total_matches, wins, draws, losses,
      goals_for, goals_against, total_goals, both_teams_scored
    },
    advanced_stats: {            // stats agregadas del historial (de advanced_stats de cada match)
      overall: { /* mismo formato que en home_team_results */ },
      first_half: { /* mismo formato */ },
      second_half: { /* mismo formato */ },
      warnings: []
    },
    player_stats_as_home: {
      general: { minutes_total, goals_total, assists_total, shots_total, shots_on_target_total, key_passes_total, tackles_won_total, interceptions_total, ball_recoveries_total, yellow_cards_total, red_cards_total, goals_per_90, assists_per_90, shots_per_90, position_distribution, in_base_lineup_count, substitute_count },
      individual: {
        [player_name]: {
          "position": "Goalkeeper" | "Defender" | "Midfielder" | "Forward" | "Unknown",
          offense:    { GOALS, EXPECTED_GOALS, ASSISTS_GOAL, EXPECTED_ASSISTS, SHOTS_TOTAL, SHOTS_ON_TARGET, BIG_CHANCES_CREATED, BIG_CHANCES_MISSED },
          creation:   { KEY_PASSES, FINAL_THIRD_ENTRIES_TOTAL, BOX_ENTRIES, THROUGH_BALLS },
          possession: { TOUCHES_TOTAL, MATCH_MINUTES_PLAYED, PASSES_TOTAL },
          defense:    { DUELS_WON, DUELS_TOTAL, DUELS_EFFICIENCY, TACKLES_WON, INTERCEPTIONS, BALL_RECOVERIES },
          efficiency: { PASSES_ACCURACY, LONG_BALLS_ACCURACY, CROSSES_ACCURACY, DRIBBLES_EFFICIENCY },
          discipline: { FOULS_COMMITTED, FOULS_SUFFERED, CARDS_YELLOW, CARDS_RED, TURNOVERS, ERRORS_LEAD_TO_SHOT, ERRORS_LEAD_TO_GOAL },
          goalkeeping:{ SAVES_TOTAL, GOALS_CONCEDED, GOALS_PREVENTED, EXPECTED_GOALS_ON_TARGET_FACED, BIG_CHANCES_SAVED },
          derived:    { xg_per_shot, goals_minus_xg, xa_minus_assists, duels_win_pct, actions_per_90 }
        }, ...
      }
    },
    player_stats_as_away: { /* misma estructura */ },
    warnings: string[] | null
  },
  "standings": {
    teams: { [team_id]: { position, name, points, wins, draws, losses, goals, goal_difference } },
    warnings: string[] | null
  },
  "overunder_standings": {
    teams: { [team_id]: { over, under, average_goals } },
    warnings: string[] | null
  },
  "form_standings": {
    teams: { [team_id]: { points, form_string } },
    warnings: string[] | null
  },
  "top_scorers": {
    home_scorers: [{ name, player_id, team, goals, assists }],
    away_scorers: [...],
    warnings: string[] | null
  },
  "tournament_top_scorers": {
    home_scorers: [...],
    away_scorers: [...],
    warnings: string[] | null
  }
}
```

**Reglas de uso del script:**

- El modelo NO vuelve a llamar endpoints MCP después de recibir `final_context`.
- Todos los datos del análisis vienen del JSON.
- Si el script emite warnings → el modelo debe tomarlos en cuenta y no contradecirlos.
- Si el JSON marca algo como `[N/A]` → el modelo lo usa como `[N/A]`, no intenta inferir el dato.
- El script es determinista: dada la misma entrada, siempre produce la misma salida.
- El modelo no recalcula ni reinterpreta datos ya normalizados salvo para explicar su significado.

**Si el script falla o no puede ejecutarse:**
→ "No pude generar el contexto del partido. Análisis no viable."

---

## 5. Las 8 Capas — Definición Exacta

Cada capa tiene: inputs, cálculos, output obligatorio, degradación.

---

### Capa 1 — Contexto Base

**Inputs:** `odds_home`, `odds_draw`, `odds_away`, `odds_over_25`,
`odds_btts_yes`.

**Cálculos:**

- Probabilidad implícita de mercado: `1 / odds`
- Probabilidad implícita Over 2.5: `1 / odds_over_25`
- Probabilidad implícita BTTS: `1 / odds_btts_yes`
- Sesgo: favorito claro (>60%), favorito leve (50-60%), equilibrado (<50%)

**Output obligatorio:**

```
## 1. Contexto del Partido
- Partido: [Home] vs [Away]
- Competición: [Liga]
- Fecha/hora: [ISO 8601]
- Estado: [notstarted/inprogress]
---
Mercado dice [ODDS]:
- [Home]: [prob]% | Draw: [prob]% | [Away]: [prob]%
- Over 2.5: [cuota] → prob implícita [prob]%
- BTTS: [cuota] → prob implícita [prob]%
---
Indicadores históricos [IND]:
- Indicadores favorecen: [Home/Away/Equilibrado]
- Soporte histórico: [bajo/medio/alto]
Mercado vs datos: [alineados/parcialmente alineados/en conflicto] — [explicación breve]
```

**Degradación:**

- Si no hay odds → no usar diff mercado vs indicadores. Usar solo odds si disponibles.
- Si neither → capa 1 muy degradada.

**Output para `postmatch`:**

```
Mercado esperaba [ODDS]:
- [Home]: [prob]% | Draw: [prob]% | [Away]: [prob]%
- Over 2.5: [cuota] → prob implícita [prob]%
- BTTS: [cuota] → prob implícita [prob]%

Resultado vs expectativa [ODDS]/[IND]:
- El resultado [fue alineado / parcialmente alineado / sorpresivo] con las probabilidades del mercado
```

---

### Capa 2 — Descriptiva de Equipos

**Inputs:** `Get_Team_Results` (historial), `Get_Match_H2H`, `Get_Tournament_Standings` (si aplica), `final["preview"]` (web scraping — texto de previa del partido en FlashScore).

**Cálculos:**

- Puntos últimos N: de resultados del equipo
- Forma: resultados recientes (W/D/L)
- Goles avg: `goals_scored / matches_played` y `goals_conceded / matches_played`
- Over 2.5 freq: contar partidos con total > 2.5 en últimos N
- BTTS freq: contar partidos con gol de ambos en últimos N
- Perfil: según goles generados vs goles reales (sobre/sub-reperformance)
- H2H: de `Get_Match_H2H` si existe

**Output obligatorio:**

```
## 2. [Qué Viene Pasando / Lo Que Está Pasando]

[Adaptar según estado del partido:]
- status = notstarted → "Qué Viene Pasando"
- status = inprogress → "Lo Que Está Pasando" + marcador actual + minuto
- status = finished → "Lo Que Pasó"

[Home] [IND] [marcador si inprogress]:
  Forma: [form_string] — [pts] pts / [N] pts posibles
  Goles: [home_gf_avg]/[home_gc_avg]
  Over 2.5: [X/N] | BTTS: [X/N] (si hay datos)
  En casa: [home_ppg] ppg | [home_goals]GF / [home_goals]GC
  Perfil: [ofensivo/conservador/equilibrado/inestable]

[ away ] [IND]:
  [mismo formato]

H2H [IND]: [X]PJ — [home_team_wins]V-local [draws]E [away_team_wins]V-visitante | goles local [home_team_goals_for] / visitante [away_team_goals_for]
  [score reciente 1]
  [score reciente 2]

Nota: [si la forma se extrajo solo del evento actual (N=1), indicarlo]

[Para notstarted]: Previa del partido [IND]: [texto de previa de FlashScore — si no disponible: "Previa no disponible [N/A]"]
[Para inprogress]: Eventos recientes: [resumen de últimos eventos del partido — de summary.events]
[Para finished]: Resultado final: [home] [H] - [A] [away] | [marcador final]
[Para postmatch — analysis_mode = "postmatch"]:
  El resultado [confirma/rompe] la tendencia reciente de cada equipo
  Resultado vs forma: [el partido fue consistente/inconsistente con la forma previa]

**Nota:** La previa del partido debe ser concisa. Si excede 3-4 oraciones, resumir los puntos más relevantes para el análisis.
```

**Degradación:**

- Si `home_form` viene vacío → solo listar H2H disponible. Forma = N/A.
- Si no hay H2H → omitir sección H2H.
- Siempre incluir la nota si la muestra es N < 5.
- Si `preview` no disponible → texto "Previa no disponible [N/A]".
- Si status = inprogress → usar "Lo que Está Pasando" con marcador y minuto actual.
- Si status = finished → usar "Lo Que Pasó" con resultado final.
- Si `analysis_mode = "postmatch"`: incluir comparación de resultado vs forma previa y vs odds pre-partido.

---

### Capa 3 — Protagonistas

**Regla transversal (nueva):** Capa 3 = solo hechos + contexto mínimo. NO interpretación, NO peso, NO conclusiones, NO lenguaje causal.

```
Capa 3 NO puede contener:
- conclusiones
- inferencias
- lenguaje causal ("porque", "esto implica", "esto sugiere")

Solo descripción estructurada de datos.
```

**Pregunta única:** ¿qué hay? (NO: ¿qué significa?)

**Lógica de dos carriles (siempre intentar ambos):**

- **Carril A:** `Get_Match_Player_Stats` → stats individuales (gol, asistencia, tiros, etc.)
- **Carril B:** `Get_Match_Lineups` → `missingPlayers` (nombre + motivo de ausencia)

Si Carril A falla pero Carril B devuelve `missingPlayers` → usar Carril B.
Si ambos fallan → capa degradada con `[N/A]`.

---

**Carril A — Inputs directos de `Get_Match_Player_Stats`:**
`goals`, `assists`, `shots`, `shots_on_target`, `key_passes`,
`tackles_won`, `interceptions`, `ball_recoveries`, `yellow_cards`,
`red_cards`, `minutes`.

**Reglas — SIN fórmulas heurísticas:**

- No usar pesos (0.3, 0.5, etc.) ni normalizaciones inventadas.
- No calcular "impact score", "impacto ofensivo", ni ninguna métrica compuesta.
- **Sí se permite:** ranking directo por métrica individual (más goles, más pases clave, etc.).
- **Dependencia:** proporción directa de goles/producción del jugador vs total del equipo.

**Regla (nueva):** Los outputs de Capa 3 deben ser puramente descriptivos. No incluir frases como "esto afecta al equipo", "es una baja clave", "pierde poder ofensivo". Toda interpretación va en Capa 5.

**Outputs por jugador (datos directos, sin fórmulas):**

```
[Jugador X] ([pos]):
  Producción:
  - Goles: X
  - Asistencias: X

  Volumen ofensivo:
  - Tiros: X (X a puerta)

  Creación:
  - Pases clave: X

  Disciplina:
  - Amarillas: X | Rojas: X
```

**Top N por equipo (ranking directo por métrica — sin scores compuestos):**

```
Top generadores de gol [IND]:
1. [Jugador A] — [X] goles
2. [Jugador B] — [X] goles

Top creadores [IND]:
1. [Jugador A] — [X] pases clave
2. [Jugador B] — [X] pases clave
```

**Dependencia ofensiva:**

- Calcular: `(goles jugador / total goles equipo) * 100`
- Umbral: >40% → "Dependencia ofensiva alta [IND]"
- Si un equipo tiene >50% de producción en un solo jugador → alertar.

---

**Carril B — Fuentes desde `Get_Match_Lineups`:**

El script `build_match_context.py` procesa las alineaciones en dos niveles de prioridad (definidos en `normalize_lineups`):

- **Nivel 1 — Alineaciones oficiales (`startingLineups` existe):**
  - `missing_players`: lista de jugadores ausentes con `name`, `player_id`, `reason`, `country` [API]
  - `substitutes`: sustitutos disponibles [API]
  - `starting_lineups`: alineación titular oficial [API]
  - `predicted_lineups`: vacío
  - `unsure_missing`: vacío

- **Nivel 2 — Alineación predicha (`startingLineups` NO existe, `predictedLineups` sí):**
  - `unsure_missing`: jugadores cuya ausencia no está confirmada [API]
  - `predicted_lineups`: alineación predicha [API]
  - `missing_players`: vacío
  - `substitutes`: vacío
  - `starting_lineups`: vacío

Si `missing_players` contiene jugadores ausentes, reportarlos según los niveles de ausencia definidos más abajo.

**Niveles de ausencia (en orden de profundidad):**

1. **Ausencia observada [API]:** Nombre + motivo tal como los devuelve la API (Injury, Inactive, Leg Injury, etc.). Solo reportar lo que la API indica.

2. **Ausencia contextualizada [IND]:** Si además ese jugador aparece en `Get_Tournament_Top_Scorers` o como goleador/generador relevante en `Get_Team_Results` o `Get_Match_Player_Stats` de partidos previos → indicar: "[jugador] ausente — registrado como goleador/top del torneo [IND]." Solo reportar como hecho. No interpretar impacto.

3. **Influencia incierta [N/A/IND]:** Si un jugador está ausente pero no hay evidencia suficiente para estimar su peso → marcar como "influencia no cuantificable con precisión [N/A]". Nunca inventar impacto.

**Regla sobre Carril B solo:**

- Ausencias sin contexto adicional (no aparecen como goleadores relevantes, no hay ranking que los respalde) → reportar como simples hechos observados [API].
- No calcular cuánto baja el equipo por cada ausente.
- No estimar goles perdidos ni probabilidad de gol afectada.
- La acumulación de ausencias en un mismo equipo puede mencionarse como observación contextual (reduce certidumbre sobre el techo de rendimiento del equipo), pero sin cifras inventadas.

**Output obligatorio:**

```
## 3. Protagonistas

[Carril A — Si hay player-stats:]
[Jugador] ([pos]):
  Producción:
  - Goles: X
  - Asistencias: X
  Volumen ofensivo:
  - Tiros: X (X a puerta)
  Creación:
  - Pases clave: X
  Disciplina:
  - Amarillas: X | Rojas: X

[Top del partido]

Dependencia ofensiva:
- [Equipo]: [jugador] → [X]% de los goles del equipo [IND]

[Carril B — Si hay missingPlayers sin player-stats:]
Ausencias observadas [API]:
- [Equipo]: [Jugador] — [motivo de la API]
- [Equipo]: [Jugador] — [motivo de la API]

[Si hay ausencia contextualizada:]
- [Equipo]: [Jugador] — ausente [IND] — [goleador/top scorer del torneo / pieza habitual]

Lectura contextual [IND]:
- [Equipo] llega con [X] bajas registradas en la API.
- La influencia de las ausencias se considera [alta/moderada/baja] solo si coincide con
  otras señales disponibles (producción en排行榜, dependencia ofensiva, ranking histórico, etc.).
- Influencia no cuantificable con precisión [N/A]: [jugador(es)].

[Si commentary disponible Y coincide con indicadores:]
Patrón de estilo observado: [descripción]

[Si commentary disponible pero NO coincide con indicadores:]
Patrón de estilo: [N/A] — dato observacional no respaldado por stats.
```

**Degradación:**

- Si no hay player-stats NI missingPlayers → "Sin datos disponibles de protagonistas [N/A] — capa degradada." No inventar jugadores ni ausencias.
- Si solo hay missingPlayers (Carril B) → capa Carril A = N/A; Carril B según niveles de ausencia definidos arriba.
- Si hay ambos → usar Carril A y añadir Carril B como enriquecimiento.
- Si `analysis_mode = "postmatch"`: `player_stats` como fuente principal, `summary.events` como fuente secundaria, `commentary` como apoyo terciario — solo para describir secuencias, nunca para inventar superioridad estructural. Si no hay `player_stats` pero sí `summary.events` → basarse en eventos para describir el desarrollo.

---

### Capa 4 — Indicadores Compuestos

**Regla (nueva):** Capa 4 puede nombrar indicadores, pero no explicar por qué importan.

| Capa | Qué hace |
|------|----------|
| 4    | **Mide** |
| 5    | **Explica** |

**Pregunta única:** ¿cuánto? (NO: ¿por qué importa?)

```
Capa 4 NO puede contener:
- conclusiones
- inferencias
- lenguaje causal ("porque", "esto implica", "esto sugiere")
- "esto favorece a X"
- "esto indica partido abierto"

Solo descripción estructurada de datos comparables entre equipos.
```

**Inputs:** historial de equipos (`home_team_results`, `away_team_results`, `h2h`), odds.

**Cálculos:**

- Ventaja ofensiva: diferencia de goles avg entre equipos
- Fragilidad defensiva: goles recibidos / partidos jugados del equipo visitante
- Gap forma: puntos últimos N de cada equipo
- Gap casa/fuera: PPG home vs PPG away
- Riesgo Over 2.5: promedio de freq Over de ambos
- Riesgo BTTS: promedio de freq BTTS de ambos
- Disciplina: promedio de tarjetas de ambos equipos (de `Get_Match_Stats`)
- Volatilidad: desv. estándar de goles en últimos 5 de cada equipo
- Estabilidad muestra: N partidos disponibles vs mínimo 5

**Coherencia mercado-datos [IND]:**
- **Alta:** mercado y datos históricos favorecen el mismo lado Y con magnitudes similares.
- **Moderada:** coinciden en el lado, difieren en intensidad.
- **Baja:** favorecen lados distintos, o uno ve equilibrio y el otro no.

*(Medición de alineación — no interpretación de por qué.)*

**Output obligatorio:**

```
## 4. Indicadores Compuestos

Ventaja ofensiva: [Home] por [+/-X.XX] goles [IND]
Fragilidad: [Away] recibe [X.XX] goles/partido [IND]
Gap forma (últimos [N]): [Home] [+/-X pts] sobre [Away] [IND]
Gap casa/fuera: [Home] [+/-X.X] ppg [IND]
Riesgo Over 2.5: [X%] [IND]
Riesgo BTTS: [X%] [IND]
Volatilidad: [baja/media/alta] [IND]
Índice disciplina: [bajo (<1.0 yc/pp) / medio (1.0-1.5) / alto (>1.5)] [IND]
Mercado vs datos: [alta/moderada/baja]
Estabilidad muestra:

…(truncated)
