# Worldcup Prediction

> World Cup football match prediction pipeline producing calibrated probability distributions (1X2, totals, top scorelines), edge analysis vs de-vigged market odds, fractional-Kelly sizing with a strict no-bet rule, and qualification scenario simulation. Use this skill whenever the user asks to predict, analyse, or preview a World Cup (or other football) match, asks about win/draw/loss chances, expected goals or scorelines, odds value, line movement, betting edge, Kelly staking, group standings or qualification scenarios, or asks to run the agent pipeline / generate a match prediction report — even if they just say "分析这场比赛" or "这场怎么看".

- Skill: `ovohao/worldcup-prediction` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add ovohao/worldcup-prediction`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ovohao/worldcup-prediction/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: ovohao (https://skillmd.com/u/ovohao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ovohao/worldcup-prediction

---


# World Cup Prediction Skill

A five-agent, market-anchored prediction pipeline. Core design rules (do
not violate):

1. **One frozen snapshot.** Every run starts by building a timestamped
   bundle via `scripts/snapshot.py`. All agents read that file only. Never
   fetch live data mid-pipeline.
2. **Numbers come from scripts, not debate.** Agents emit structured JSON;
   `scripts/aggregator.py` produces the final probabilities. The
   consistency review may force factual corrections and re-emission — it
   may never negotiate the fused number.
3. **Market is the anchor.** De-vig snapshot odds with Shin's method;
   market weight 0.5, agents 1/4/5 split the rest equally (fixed by spec).
4. **Distributions, never point picks.** Output 1X2 probabilities, totals
   lines, top-5 scorelines with probabilities. No single-score "prediction",
   no corners market.
5. **Read-only.** No order placement, no automated betting, no
   circumvention of bookmaker controls. Kelly output is informational.
6. **Every run is scored.** Log all vectors with `scripts/scorekeeper.py`
   before kickoff; settle after; report Brier/RPS/logloss/CLV.
7. **Skill changes are decided by FULL-SAMPLE evidence, never a single match.**
   Before adding/removing/reverting any rule (totals lean, guard, vote
   construction, draw-boost…), backtest it across ALL settled results — win-rate
   AND ROI for bets (1X2, totals/over-under, scorelines, cards) and mean Brier
   for probabilities — and keep whatever wins on the accumulated record. One
   result is noise: do NOT flip-flop a rule because the last game went against
   it (lesson: the favourite-vs-minnow OVER lean was added → reverted on ONE
   loss → the full sample showed it was +EV all along, 5/8, ROI +13.5%). Re-
   evaluate only as n grows meaningfully, not per match.

## Requirements & conventions

- Python deps: `pip install numpy scipy` (everything else is stdlib).
- API keys via env vars (see references/data_sources.md); never commit keys.
- Suggested layout: snapshots in `~/worldcup/snapshots/`, scorekeeper db at
  `~/worldcup/logs/predictions.jsonl`, fitted params in `~/worldcup/`.
- If The Odds API returns `matched: false`, read `all_fixture_names` in the
  snapshot and add the variant to assets/team_aliases.json.
- **In-tournament form outranks everything.** This World Cup's already-played
  matches are the single most representative data on a team's current squad,
  form, and conditions — they carry the HIGHEST weight, and EVERY agent must
  reference them from matchday 2 on:
  - models: after each matchday, append the completed WC results to the
    training CSV with the top `importance` (~2.0, above any friendly/qualifier)
    and refit (`dixon_coles.py --importance-pow 1`, `elo.py`). Time-decay +
    importance then both favour them.
  - agent3: read HOW each side actually set up and performed in their WC game
    (formation, set-piece events, who started) over pre-tournament friendlies.
  - agent_benham: prefer WC npxG once available over friendly/qualifier npxG.
  - agent4/agent5: a team's WC result/performance is A-tier evidence and the
    first reference point; friendlies are discounted next to it.
  Matchday 1 has no prior WC games — say so and fall back to the 24-month base.
  MD1 openers also run draw-heavy (rust/caginess/lopsided sides easing off):
  turn on `--opener-draw-boost` at aggregate (see step 12) and expect favourites
  to convert less often than the 24m base or the market implies.

## Modes

- **fast_mode** (kickoff within 90 min, or user asks a quick view): snapshot
  → market de-vig → injuries/news one-paragraph summary → weather → edge +
  no-bet check. No five-agent run; say explicitly that it is the quick view.
- **full_mode** (default for pre-match reports): the complete pipeline below.
- **in_play_mode** (match is live; user asks for a live read). ⚠️ This is the
  ONE mode that BREAKS rule #1 on purpose — it uses live data, not a frozen
  snapshot, and never feeds the pre-match scorekeeper log. Workflow:
  1. Carry the pre-match `lambda_home`/`lambda_away` from the last
     dixon_coles predict as the prior (do NOT re-fit live).
  2. Pull LIVE state: current score from The Odds API /scores (scores are
     populated while `completed:false`); minute + red cards from a live feed
     (ESPN live); in-play h2h odds from /odds.
  3. python scripts/in_play.py --lambda-home .. --lambda-away ..
       --minute M --home-score H --away-score A [--red-home/--red-away]
       [--live-odds H D A]
  4. Report live 1X2 / totals / top final scorelines + edge, LABELLED live
     and timestamped. Live Kelly uses a SMALLER fraction than pre-match and
     respects suspensions/latency (state it). Game-state effects beyond red
     cards (leader killing the game) are not modelled — flag, don't fake.
- **scenario_mode** (display / mock-up only). For presentations where a
  probability is hand-set for illustration, use
  `scripts/scenario_report.py --base REAL.json --set score:0-1=.. --set
  under_1.5=..`. It emits a bundle hard-flagged `scenario:true do_not_log:true`
  with `overrides_applied` (showing each real→set value), written ONLY under
  `scenarios/`. RULES: the rendered report MUST keep one short footer marking
  it a scenario/展示版; a scenario bundle is NEVER passed to scorekeeper log,
  NEVER settled, and NEVER presented as the model's real output. This is how
  "free to adjust for a demo" and "never pollute the settled record" stay
  true at once.

## Pipeline (run in this order)

```
0. Read references/data_sources.md (keys, endpoints, free-tier limits)
1. SNAPSHOT   python scripts/snapshot.py --fixture-id .. --home .. --away ..
              --venue-id .. --kickoff-utc .. --out snapshots/
              Before this, gather news via web tools (ESPN/theScore/RotoWire
              injury+lineup pages) AND player ratings. For player data use the
              FotMob semi-auto pipeline (FBref/Sofascore block bots even in a
              real browser; FotMob works — see data_sources.md):
                scripts/fotmob_ratings.sh <home-slug> --name "Home"
                scripts/fotmob_ratings.sh <away-slug> --name "Away"
              (slugs in assets/fotmob_team_ids.json). RELIABLE: squad heights +
              aerial threats → grounds agent3 key_duels; VARIABLE: rating/xG/xA
              leaders (present if FotMob tracked the team's recent comp). Merge
              into player_ratings; summarise news+ratings into a JSON file,
              pass via --extra-json so everything is frozen in the bundle.
              FRESHNESS: a snapshot built >90 min out uses PREDICTED XIs —
              its bets are provisional. Before acting on a bet, take a
              SECOND snapshot at ~T-75min once official XIs drop, with
              --lineups-confirmed, and diff the two with line_movement.py.
              The bundle records minutes_to_kickoff + lineups_confirmed so
              kelly.py / the report can flag provisional sizing automatically.
1b. AGENT QUALIFIER (runs FIRST; every matchday). For EACH side, set the
              qualification STANCE that shapes how they play this match.
              ALWAYS build the group state from the LIVE data layer first — it
              pulls current standings + remaining fixtures from football-data.org
              and fills each remaining fixture's probs/λ from the REFIT DC, so
              the sim is never run on stale or hand-typed numbers (re-run after
              every result; needs FOOTBALL_DATA_TOKEN):
                python scripts/build_group_state.py --params ~/worldcup/dc_params.json
                  --out group_state.json --target <Home> --bracket-out bH.json
                python scripts/build_group_state.py --params ~/worldcup/dc_params.json
                  --target <Away> --bracket-out bA.json   # reuse same state
                python scripts/bracket_sim.py --state bH.json   # per side
              NEVER hand-author group.json — if build_group_state emits warnings
              (unmapped team / missing score), fix the alias, don't fake a number.
              bracket_sim returns finish_probs + the R32 opponent TIER per finish (1st→a
              3rd-placer=easiest, 2nd→runner-up, 3rd→a winner=hardest) +
              recommended_target_rank. Pick each side's target rank (ability ×
              opponent tier) and derive `this_match_intent` (win_and_max_GD /
              win / damage_limit_or_steal_point / rotate_rest). Stance feeds
              agent2/agent3 and agent1's bounded λ only — NEVER the fused number.
              Stance exists from matchday 1 (GD is the tiebreaker). See
              references/agents.md "Agent Qualifier".
2. AGENT 2    Off-field intel report. Classify every news item strictly per
              references/source_credibility.md. From matchday 2-3, when the
              table makes results matter, run the stakes tools (matchday 1
              openers have no standings — skip). Feed the SAME live
              group_state.json built in step 1b (build_group_state.py) — do not
              rebuild it by hand:
                python scripts/qualification_sim.py --state group_state.json  # qualify% / winner%
                python scripts/strategic_scenario.py --state group_state.json
                  --r32-from-bracket bracket_<team>.json   # last-round value
                  model; --r32-from-bracket auto-derives the R32 opponent
                  strength per finishing slot from the bracket_sim output above
                  (tier1 winner=0.68 / tier2=0.55 / tier3 third-placer=0.42) —
                  no hand-typed r32_map
              ⭐ REQUIRED — this is ONE value model, not a set of named cases.
              A side maximises EXPECTED TOURNAMENT VALUE: each finishing slot has
              value V = qualify-base + a bonus that DECREASES with the R32
              opponent that slot inherits (easier path = more value, so V(1st)
              can be < V(2nd) when topping the group draws the favourite); 0 if
              eliminated. The script reports per side, AGAINST THE CURRENT TABLE:
                - ev_by_result {win,draw,loss}  and  target_result = argmax EV
                - chase_value  = EV(win) − EV(draw)   (≤0 ⇒ no reason to win)
                - defend_value = EV(draw) − EV(loss)  (large ⇒ must not lose)
                - posture (a region of that space, with a one-line read):
                  must_win | press_for_win (chase≫defend) | secure_first
                  (defend≫chase ⇒ bank the point, win only if cheap) |
                  protect_result | prefer_not_to_win (dodge the favourite) |
                  win_or_out (only a win survives) | chase_seed_no_fear |
                  indifferent_safe (rotate).
              READ the gradients against the live form, then state the totals/
              draw implication: secure_first / protect_result / prefer_not_to_win
              / indifferent_safe ⇒ lower tempo, UNDER + DRAW lean; win_or_out /
              must_win / press_for_win ⇒ higher tempo. Then apply the modifiers
              before trusting it: teams rarely actively LOSE (prefer_not_to_win =
              a softening, not放水); the OPPONENT's posture too (both content ⇒
              mutual low-event draw); feasibility (can agent3 execute it?); and
              downstream certainty (earlier MD / unfinished groups ⇒ discount the
              dodge). Fold into strategic_pressure + the totals/draw read as
              CONTEXT only — never let it move the fused number. best-3rd & GD
              tiebreaks need qualification_sim (enumerator flags gd_can_decide).
3. AGENT 3    Tactical features (structured fields only; essay = appendix).
              ⭐⭐ DATA-DRIVEN (required, esp. KNOCKOUTS where tactics decide):
              GROUND the tactical read in OFFICIAL FIFA team statistics, not
              narrative. Pull them first:
                python scripts/fifa_fetch.py teams   # 48 teams, ~32 metrics each
                python scripts/fifa_fetch.py matchup --home X --away Y
              The matchup prints both sides' value+[tournament rank/48] for
              possession, xG, shots, on-target, CONVERSION% (finishing
              efficiency = goals vs xG), shots-in-box, headed shots, crosses +
              accuracy, corners, defensive_pressures, forced_turnovers,
              ball_recovery_time (lower=higher press), clean_sheets, GK save%.
              Read the REAL style from the numbers — they often OVERTURN the
              narrative (e.g. NED-MAR: Morocco out-possesses & out-xGs the
              "attacking" Dutch, but NL convert 24% [rank 2] vs Morocco 12%
              [23] and NL keep 0 clean sheets → the true edges are NL finishing
              vs Morocco control+waste, NOT "Dutch attack vs Moroccan bus").
              Derive style_matchup / press-vs-build / wide-vs-central /
              finishing-efficiency / set-piece threat (corners+headed shots)
              from the stats; combine with the predicted XIs + formations +
              positional coords from api.fifa.com (snapshot/lineups) for the
              key_duels. (`fifa_fetch.py players --team X` adds player-level
              goals/assists/shots when the rate-limit allows.)
              Write structured fields to agent3_features.json, then HARD-GATE
              before agent1 (set_piece for BOTH sides is mandatory — a Bosnia
              corner was the only goal missed in the Canada-Bosnia review):
                python scripts/validate_features.py --inputs agent3_features.json
              Knockouts: weight agent3 MORE — single-elimination, no rotation,
              tactical matchup + finishing efficiency + set-pieces decide tight
              games. Still CONTEXT: colours style/tempo/key_duels and feeds
              agent1 ONLY via the bounded λ channel — never the fused number.
4. AGENT 1    Fit/refresh models if needed:
                python scripts/dixon_coles.py fit --matches data.csv --out dc.json
                python scripts/elo.py fit --matches data.csv --out elo.json
              Predict with bounded adjustments derived from agents 2/3:
                python scripts/dixon_coles.py predict --params dc.json
                  --home X --away Y --home-adj -0.08
                python scripts/elo.py predict --ratings elo.json --home X --away Y
              Neutral venue is the DEFAULT; add --hosts only when USA,
              Mexico or Canada play at home.
              ⭐ TOTALS correction is now BUILT IN (--totals-boost, default 1.10,
              TOTALS ONLY — 1X2/scorelines/E[goals] stay pure DC). backtest.py
              on 2098 intl matches, TWO independent hold-outs, found the pure
              model SYSTEMATICALLY UNDER-PREDICTS overs (60-75% fav band: pred
              0.448 vs actual 0.614); +10% lambda was the best correction in
              BOTH windows. (Symmetric over-dispersion --totals-dispersion did
              ~nothing — the bias is in the mean, not the variance; kept only as
              an option.) The boost ALSO holds on 233 real major-tournament
              matches (StatsBomb open data): actual over-2.5 0.468 vs boost-off
              0.405 vs boost-1.10 0.460. Re-validate as the WC sample grows:
                python scripts/backtest.py --matches ~/worldcup/intl_matches_24m.csv
                  --cutoff <ISO>   # 1X2+totals OOS scoring, bias-by-fav-strength
                  diagnosis, dispersion/game-state/boost sweeps
                python scripts/statsbomb_fetch.py matches   # free tournament
                  results; `events` mode pulls xG/set-pieces for tournament-only
                  feature builds (no scraping; research-licensed)
                python scripts/statsbomb_features.py   # per-team xG/set-piece
                python scripts/statsbomb_validate.py   # go/no-go on a feature
              ⭐ VALIDATED on 262 major-tournament matches: a per-team SET-PIECE
              rating is NOISE (split-half repeatability r=0.12 vs open-play 0.53)
              — do NOT build one / do not weight a team's past set-piece output
              as a stable prior (it's ~45% of pattern-xG but unpredictable per
              team). xG > goals as a forward signal (0.285 vs 0.203) → feed xG
              via the existing Benham xg_home/xg_away correction, not a new model.
              ⭐ HEAT also tested (heat_backfill.py + heat_backtest.py, 1381
              matches): NO general heat->goals effect — raw corr is a climate-
              zone CONFOUND, within-country detrended r=-0.004. Do NOT add a heat
              lambda term. (Only >=34C noon extremes might matter; needs 2026
              exact-venue data to confirm — track prospectively, don't model now.)
              Optionally compute context edges first (rest/travel/altitude):
                python scripts/context_factors.py --home X --away Y --venue-id V
                  --kickoff-utc T [--home-last-utc.. --away-last-utc..
                  --home-venue-last.. --away-venue-last..]
              Apply npxG corrections (references/metrics.md) via
                --xg-home/--xg-away, and context via --home-adj/--away-adj.
              Optionally walk the match (phase-by-phase + fitness/game-state):
                python scripts/tactical_progression.py --lambda-home .. --lambda-away ..
                  [--fade-side .. --fade-from-min .. --fade-strength ..
                   --fragile-side ..]
              It returns half/phase xG, first-goal timing, and a CAPPED
              (±0.06) fitness nudge for --home-adj/--away-adj. Its
              gamestate_caveat is CONTEXT only. Confidence guard: progression
              detail never raises 1X2 confidence (the pro report it imitates
              was detailed and still wrong — Qatar drew the 81% favourite).
              Then blend via script (never by hand). ALWAYS pass the shin-
              devigged market probs + the Elo-underdog guard: when DC agrees
              with the market on the favourite but Elo alone flips to the
              underdog by ≥0.15, the guard down-weights Elo (Elo has been wrong
              every such time — USA-Paraguay, NED-JPN, SWE-TUN; backtest −0.07
              Brier, zero effect on consensus / both-models-dissent games):
                python scripts/blend_models.py --dc dc_out.json --elo elo_out.json
                  --market <Hs> <Ds> <As> --underdog-elo-guard
              (guard only fires on the lone-Elo-dissent pattern; it leaves
              50/50 untouched otherwise. Devig the snapshot odds early for this;
              it does not pre-empt the formal MARKET step.)
5. AGENT BENHAM  Underlying-performance vector (npxG/structure, not goals
              or reputation). Joins the fusion. See references/agents.md +
              references/metrics.md.
6. AGENT 4    Own vector, anchored on market; every >3pp deviation cites
              A/B-tier evidence.
7. AGENT 5    Red team: structured challenges first, then its own vector.
7b. AGENT CARDS  Discipline/bookings analyst (NON-voting; separate output
              value, never touches 1X2). Fetch the assigned referee + their
              cards/game average AND both teams' recent cards/game (web/FotMob;
              fouls/game as a flagged proxy if exact cards are paywalled);
              missing data → neutral baseline + low_confidence, never faked.
              Build cards_state.json with bounded tactical multipliers from
              agent3 (physical/grind) + qualifier (must-win tension), then:
                python scripts/cards_model.py --state cards_state.json
              Edge only if a cards market exists (most WC fixtures: none →
              context). See references/agents.md "Agent Cards".
7c. AGENT FLOW  Cross-venue money-flow analyst (NON-voting; never touches 1X2).
              Aggregates REAL-MONEY signals — Polymarket + on-chain (Azuro/
              Overtime/SX) prediction markets + Betfair Exchange order flow +
              book bets%/handle% splits — into a qualitative read that FEEDS
              agent_bloom.
              ⚠️ POLYMARKET IS MANDATORY every prediction (it needs NO key — there
              is no reason to skip it). One resolver builds the state (finds the
              event by slug, falls back to title-matching, maps the 3 sub-markets
              to home/draw/away "Yes" tokens), then flow_analyzer pulls live mids:
                python scripts/poly_lookup.py --home France --away Senegal
                  --date 2026-06-16 --emit-flow-state
                  --market-fair <Hs> <Ds> <As> --dc-totals <O15> <O25> <O35>
                  --bet-candidate <our_value_outcome> --out flow_state.json
                python scripts/flow_analyzer.py --state flow_state.json
              (<Hs Ds As> = Pinnacle shin-fair probs; <O15 O25 O35> = DC's
              over_1.5/2.5/3.5. poly_lookup ALSO pulls the GOALS money — full-match
              O/U totals, spread, BTTS, exact-score (the '-more-markets' and
              '-exact-score' sibling events) — and flow_analyzer's goals_flow
              compares the money's P(over) to DC's. missing_legs → partial, never
              faked.)
              Betfair (strongest order-flow signal) needs an ACTIVATED LIVE app key
              + cert login (delayed key has no matched volume) — env BETFAIR_APP_KEY
              /USERNAME/PASSWORD/CERT/KEY; if those are unset, say "Betfair blind"
              explicitly (do not silently drop it). On-chain subgraphs config-driven.
              ⭐ In the report, ALWAYS state whether the real money CONFIRMS or
              CONTRADICTS our edge — a liquid Polymarket agreeing with Pinnacle is
              evidence AGAINST a model-only lean (e.g. France-Senegal: $3.79M PM
              priced the draw at 21.4% ≈ Pinnacle 20.9%, killing a model draw edge).
              Read-only: NEVER places bets / evades limits/KYC/geo. Degrades
              cleanly per venue; missing data → skipped + low confidence, never
              faked. See references/agents.md "Agent Flow" + data_sources.md.
8. AGENT BLOOM  Market/execution audit (no vector): price deviation vs shin
              fair probs, cross-market coherence, line-movement/steam,
              agent_flow's money-flow read, execution feasibility. Emits a
              verdict, may raise a challenge.
9. REVIEW     ≤2 rounds. Factual errors → targeted agent corrects & re-emits.
10. MARKET     python scripts/odds_utils.py devig --odds H D A --method shin
              (prefer Pinnacle from the snapshot). For totals use
              --method auto (2-way markets use power, not shin). If an earlier snapshot of this
              fixture exists, also run:
              python scripts/line_movement.py --old OLD.json --new NEW.json
              And cross-book best-price / dispersion:
              python scripts/line_shopping.py --snapshot SNAP.json --market h2h
11. VALIDATE   python scripts/validate_vector.py --inputs vectors.json
              (hard-fails on malformed vectors; >3pp market deviations
              without documented evidence are flagged)
12. AGGREGATE python scripts/aggregator.py --inputs vectors.json --market-weight 0.5
              [--opener-draw-boost 0.02-0.03 — RECOMMENDED ON for any
               matchday-1 opener, NOT just cagey ones. WC2026 MD1 ran very
               draw-heavy: 3/8 early + the 6/15 slate Spain 0-0 / Belgium 1-1
               / Saudi 1-1 (3/4 draws). It applies EVEN to lopsided openers —
               Spain (89% fav) drew Cape Verde 0-0; do NOT disable it because
               "the favourite is too strong" (that was a settled miss). Cap
               0.05; still off for knockout/MD2+ games where the market
               re-prices on real form. Small-n: a prior, not a guarantee.]
              The market entry's probs = market_snapshot.fair_probs_shin
              from the snapshot bundle.
12b. BET PREFLIGHT (only if an edge looks bettable AND kickoff is near).
              Gather the CONFIRMED XIs from the fastest source (data_sources.md
              speed table: Sofascore/FotMob/official X), write them into a new
              extra-json, then run the one-command ~T-75min refresh:
                scripts/refresh_and_diff.sh FIXTURE HOME AWAY VENUE KO \
                  NEW_EXTRA OLD_SNAPSHOT
              It rebuilds the snapshot (--lineups-confirmed), diffs it, and
              runs the NEWS RADAR. If the radar says MATERIAL MOVE and you
              cannot explain it from news, STOP — informed money saw something
              you haven't; find it before betting. Re-run EDGE on the fresh
              snapshot's market.
12b. RULE 1   ALWAYS run the DC-relative betting signals (totals + 1X2) — the
              single most discriminating per-match signal on the settled sample
              (88% vs 43-44% split). Needs the agent1 DC predict JSON + the shin
              market probs:
                python scripts/rule1.py --dc dc_out.json --market <Hs> <Ds> <As>
              Returns: TOTALS — bet OVER 2.5 only if DC over_2.5 >= 0.45 (else
              skip); 1X2 — gap = market_fav - dc_fav: gap<0.15 & fav>=0.55 →
              back the favourite, gap>=0.15 → FADE it (over-priced favourite DC
              distrusts; won only 43%). It INFORMS the EDGE/bet decision below
              (and the report's bet section), never overrides the fused 1X2.
              IN-SAMPLE caveat + small n are in its output — keep it small and
              re-calibrate as the settled count grows.
13. EDGE      For any outcome with positive edge:
              python scripts/kelly.py --prob P --prob-low BAND_LOW --odds O
                --market-type {1x2|asian_handicap|totals|btts|player_props}
                [--no-lineup --minutes-to-kickoff N]
              Respect no_bet / provisional_only verbatim. Lineup-dependent
              markets without a confirmed XI (>90 min out) are provisional.
              ⭐ When logging the bet, TAG it AND record the bet-time market
              price (FIXED PROCESS — from the money-detection step):
                bet_ledger.py bet .. --kind edge|challenge [--edge-pct E] --mkt-odds M
              edge = Kelly/Rule-1 gave a genuine positive edge; challenge = the
              forced bet-every-match minimum with no real edge (size at floor).
              ⭐⭐ CLV FIXED PROCESS (do NOT skip — CLV is the only low-variance
              edge proof): the CLOSING price (line right before KO) must be
              recorded for EVERY bet. Capture it as close to kickoff as possible
              via line_read.py / poly_lookup, then:
                bet_ledger.py close --match F --closing-odds <closing price>
              If not active at KO, obtain the close at settle — capture via the
              tools or ASK THE USER for it; never settle a bet with no closing
              price (settle prints a loud warning if missing).
              Cross-check against RULE 1: prefer bets where Rule 1 and Kelly
              agree (e.g. Rule-1 "back favourite" + positive Kelly edge); when
              Rule 1 says FADE the favourite, do not back it even if some model
              shows a thin edge.
13b. AGENT DESK  Closing synthesis & recommendation (NON-voting; runs LAST so
              it sees the fused numbers + kelly + every soft layer). HYBRID:
              the script enforces discipline, Claude writes the prose. Build
              desk_state.json (fused 1X2 + band + totals + kelly/no_bet +
              soft: qualifier/intel/tactical/cards/flow/bloom), then:
                python scripts/desk_synthesizer.py --state desk_state.json
              It returns a synthesis scaffold + signal-coherence (aligned vs
              CONFLICTS) + typed recommendations (main / alt_angle / watch /
              stand_down). HARD: a leg Kelly/band killed is NEVER a `main`
              (→ watch/stand_down); confidence capped by evidence tier;
              conflicts surfaced, not hidden. Owns the report's closing
              "研判 + 推荐" section. See references/agents.md "Agent Desk".
14. LOG       scorekeeper.py log for market/agent1/agent_benham/agent4/agent5/final.
              Also log each agent_desk recommendation:
                scorekeeper.py log-rec --fixture-id F --rec-type {main|alt_angle|
                  watch|stand_down|context} --market .. --selection .. [--ref-price]
15. REPORT    Render the report template below.
16. SETTLE    After full time: scorekeeper.py settle --closing-odds H D A;
              settle the recommendations too (hit-rate / ROI / shadow-P&L of
              what the discipline skipped):
                scorekeeper.py settle-rec --fixture-id F --result R [--total-goals N]
              ⭐ MANDATORY — record the closing price so CLV is captured (use
              `bet_ledger.py close` pre-KO, or pass --closing-odds here; if the
              close is unknown, ASK THE USER before settling) —
              over <~50 bets W/L is mostly noise; a +mean CLV (beating the
              close) is the low-variance proof the process has edge:
                bet_ledger.py settle --match F --result R --closing-odds <price>
              periodically show scorekeeper.py report (prob/Brier) AND
              scorekeeper.py rec-report (recommendation hit-rate by type) AND
              bet_ledger.py status (now: CLV mean + beat-close% + edge-vs-
              challenge ROI split).
```

Agent role contracts, I/O schemas and the review protocol:
**references/agents.md** (read before running agents).
Model theory and World Cup-specific adjustments: **references/models.md**.

## Report template (final output to user)

1. Match context — fixture, kickoff (UTC + venue local), venue + roof,
   stage, standings pressure, qualification probabilities if relevant
1b. Qualification & stance (REQUIRED, from agent_qualifier) — for BOTH sides:
   finish_probs, the target rank + which R32 opponent TIER each finish gives
   (1st→3rd-placer easiest …), and the resulting this_match_intent
   (win_and_max_GD / damage_limit_or_steal_point / rotate_rest …). State the
   GD/suspension angle. NOTE it is context that colours tactics — it does NOT
   move the fused 1X2. Matchday 1: ambition-driven; matchday 3: concrete result.
   ⭐ W/D/L → value → behaviour (REQUIRED on matchday 2-3, from
   strategic_scenario — ONE value model, no named cases): for each side give
   ev_by_result + target_result, the two gradients chase_value / defend_value,
   and the posture with its read (e.g. secure_first = a win is best but not-
   losing matters far more → bank the point; prefer_not_to_win = winning tops
   into a harder R32 path). Tie the totals & draw read to the posture
   explicitly, then note the modifiers actually applied (opponent's posture,
   feasibility, downstream certainty, "teams rarely actively lose"). Flag if
   best-3rd / GD tiebreaks apply (cite qualification_sim, not the enumerator).
2. Team news — injuries/suspensions/lineups, each with tier + freshness
3. Market — snapshot odds, shin-fair probabilities, best price across books
   + consensus + dispersion flags (line_shopping), line movement since open
   if available, money-flow signals (labelled per data_sources.md)
3b. Money flow (from agent_flow, if any venue data) — cross-venue implied probs
   (Pinnacle / Betfair / Polymarket / on-chain), dispersion, money-vs-tickets,
   flow_read + recommendation + confidence. State which venues were live vs
   skipped (esp. Betfair needs a live key). Read-only signal — does NOT move the
   fused 1X2; it informs the Bloom view / bet decision only.
   ALSO REQUIRED — goals/score money (goals_flow): a DC-totals-vs-money-totals
   table (over_1.5/2.5/3.5: DC P(over) | Polymarket P(over) | Δ | vol), the
   spread money, and the top exact-score money. Two distinct signals:
   (a) the money-vs-DC DIVERGENCE ("lean toward money") = INFO ONLY, ~coin-flip
       (2/3 this WC), do NOT bet off it;
   (b) the favourite-vs-minnow OVER 2.5 lean (over_lean, fav>=0.65) = a SMALL,
       FULL-SAMPLE +EV bet (5/8 = 62.5%, break-even 54%, ROI ~+13.5%). Decide on
       the ACCUMULATED record, not single games — the POR-COD 1-1 loss is within
       variance. Size small (high variance, bimodal goals); re-evaluate only as
       n grows; do NOT flip-flop the rule on one result.
4. Weather — values + impact score, scaled by roof relevance
5. Probabilities — fused 1X2 + fair odds + disagreement band; totals;
   Asian handicap line (from the score matrix); top-5 scorelines;
   per-agent vectors in an appendix
5c. Cards / discipline (REQUIRED, from agent_cards) — expected total cards +
   most-likely count, over/under lines, per-team split, P(≥1 red), and the
   referee name + their cards/game average that drives it. State whether team
   averages are exact or estimated (low_confidence) and cite sources. Cards are
   high-variance and this value does NOT affect the 1X2 / scorekeeper Brier.
6. Edge & sizing — edge vs market, Kelly output incl. no_bet flag/reason.
   REQUIRED — state RULE 1's two signals for this match: TOTALS (DC over_2.5 vs
   0.45 → bet OVER / skip) and 1X2 (gap = market_fav - dc_fav → back / fade the
   favourite), with the historical split (88% vs 43-44%) and the in-sample/small-n
   caveat. Bets should align with Rule 1 (and Kelly); flag any disagreement.
7. Two-school view — REQUIRED closing perspectives, each 2-4 sentences:
   - **Benham (underlying-performance) view**: what the npxG/structure say,
     where they diverge from goals/reputation, and which side that favours.
   - **Bloom (market/execution) view**: is the price actually wrong, which
     market leg is mispriced, is the edge get-on-able, or is the honest
     answer "no value, do not force it". If agent_bloom said
     model_likely_wrong / no value, this view leads the edge section.
     Fold in agent_flow's read: does the real money confirm our edge
     (align_with_flow) or fade it (fade_or_stand_down)?
   These are method-based perspectives, NOT quotes from real individuals;
   never attribute fabricated statements to Tony Bloom or Matthew Benham.
8. Caveats — missing sources, stale items, low-data teams, and:
   "Informational analysis only, not betting advice. Probabilities are
   estimates. Check the legality of sports betting in your jurisdiction."
9. 研判 + 推荐 (REQUIRED, from agent_desk) — the closing synthesis that
   integrates ALL soft info (tactical/intel/stance/cards/money-flow/bloom) +
   the fused numbers + the discipline result into: a short consolidated read,
   the signal-coherence map (where signals agree vs CONFLICT — surface
   conflicts), and one or more concrete recommendations each typed (main /
   alt_angle / watch / stand_down) with price ref, soft rationale, confidence,
   and its guard. A band/Kelly-killed leg appears only as watch/stand_down,
   never as a "main" pick. This is a judgement layer on top of the model — it
   does NOT change the fused 1X2. Keep the no-certainty + informational footer.

## Hard behavioural constraints

- Never present a pick as certain ("必买", "稳了", "lock") — always
  probabilities with the disagreement band.
- If `no_bet` is true, the report must say so prominently; do not soften it.
- If snapshot sources failed/skipped, list them in Caveats — never
  fabricate odds, lineups, news, or weather.
- C-tier-only claims never adjust model inputs.
- Do not place bets, automate wagering, or advise on evading bookmaker
  limits/KYC. If asked, decline that part and continue the analysis.
- If signs of problem gambling appear (chasing losses, distress,
  bet-everything requests), pause the analysis and respond with care;
  mention responsible-gambling support.

