Skip to content

Repository files navigation

predball — an autonomous World Cup forecasting bot

predball fields Tommy, a bot that competes in the SportsPredict ProbabilityCup: before each match the platform publishes ~15–20 yes/no questions ("Will Spain win?", "Will there be 10+ corners?", "Will Messi score or assist?"). Tommy reads the questions, prices every one from a single generative model of the match, and submits a probability through the official bot API — fully unattended, never abstaining.

It just finished its first full tournament: the 2026 World Cup edition, group stage through the final.


🏆 How Tommy did

Final rank 268 / 4,011 — top 6.7%
Final score 1,702 RBP
Competitors outperformed 3,743 (93.3% of the field)
#1 of all Montenegro 🇲🇪 entrants (national leaderboard)
Questions graded 455, across 30 matches
Directional hit rate 58.5% — picked the more-likely side 3 times in 5
Brier skill vs coin-flip +0.086 (eliminated ~9% of baseline error)

Best performances

Matchday Round RBP gained Running total → rank
Jul 14 Semi-finals +267.7 🥇 1,265 → #303
Jul 18 Semis settle + slates +263.1 🥈 1,528 → #273
Jul 19 The Final (all-in) +173.2 1,702 → #268
Jul 9 Quarter-finals +148.4 969 → #342
Jul 7 Quarter-finals +140.5 821 → #376

Tommy climbed from a low of rank ~940 mid-tournament to 268 by the final — the edge compounded as the knockout multipliers (×2, ×3) rewarded being sharper than the field exactly when it mattered most.

vs. the crowd. RBP is scored relative to the field averageRBP = (field-average Brier − your Brier) × multiplier × 100. A positive total is, by construction, proof of being sharper than the average competitor; Tommy finished +1,702 while the median player finished at −11 and fewer than half the field (46%) finished positive at all. The edge came from systematically fading markets the crowd reliably over-bid — star scorers, penalties, "a goal before the hydration break" — where Tommy's calibration was near-perfect (in the 10–50% band, predicted rate 0.22–0.41 vs. realised 0.22–0.42).


The tournament, briefly

The ProbabilityCup is a probability-forecasting contest, not a betting game. For every match you answer a fixed slate of yes/no questions with a probability in [1, 99], and you are scored on the Brier score (squared error of your probability vs. the 0/1 outcome) relative to the whole field's average. You gain points only by being more accurate than the crowd; knockout matches count double, the final triple. With ~4,000 competitors and a purely relative metric, the game is won by consistent, well-calibrated edges — not the occasional bold call.


System design, briefly

Two halves separated by a hard brain/app firewall: footy/ is pure mathematics and does no I/O (no HTTP, DB, or files); probcup/ does all the talking to the outside world. They meet at one seam (footy/contracts), which keeps every probability reproducible and independently testable. Every question carries two numbersp_honest (the calibrated, truthful belief that models train against) and p_submit (what's actually sent, after an optional rank-chasing tilt) — so leaderboard tactics never contaminate the honest model.

flowchart LR
    SP[("SportsPredict<br/>bot API")]
    ODDS[("The Odds API<br/>24 bookmakers")]

    subgraph APP["probcup/ — the APP (plumbing + I/O)"]
        direction TB
        COL["collectors<br/>odds · questions · crowd"] --> WH[("warehouse<br/>bitemporal Postgres")]
        WH --> PROV["provider<br/>assemble_inputs as-of"]
        PUB["publisher<br/>outbox + dry-run/live submit"]
        ORCH["orchestration<br/>scheduler · passes · archive"]
    end

    subgraph BRAIN["footy/ — the BRAIN (pure maths, no I/O)"]
        direction TB
        MODELS["models M0–M5<br/>de-vig · goals · players"] --> DERIV["derivation<br/>question to probability"]
        DERIV --> CALIB["calibration<br/>honest belief p_honest"]
        CALIB --> OVER["overlay<br/>strategic tilt to p_submit"]
    end

    ODDS --> COL
    SP --> COL
    PROV -->|FeatureInputs| MODELS
    OVER -->|p_submit| PUB
    PUB --> SP
    ORCH -.drives.-> PROV
    ORCH -.drives.-> PUB

    classDef app fill:#e8f0fe,stroke:#4285f4;
    classDef brain fill:#fce8e6,stroke:#ea4335;
    class COL,WH,PROV,PUB,ORCH app;
    class MODELS,DERIV,CALIB,OVER brain;
Loading
  • collectors → warehouse ingest odds, question slates, and post-match crowd/results into a bitemporal store (everything stamped observed_at, so backtests can't peek ahead).
  • provider assembles an as-of-correct snapshot of inputs for a given match + time.
  • models → derivation build one match model and read every question off it.
  • calibration → overlay turn raw model output into the honest belief, then into the submitted number; orchestration schedules the passes and the post-match archive.

The mathematical model, briefly

One generative stack produces a single joint distribution of the match; every question is just a functional of it (directive: output full distributions).

team strengths (Elo / Poisson-GLM)
        │  λ_home, λ_away
        ▼
Dixon-Coles score grid  P(i,j) = Poisson(λ_h)·Poisson(λ_a)·τ(i,j)      ← goals/result
        │                                     + M2 negative-binomial count models  ← shots/corners/cards
        ▼
one seeded simulator (N=20k joint draws)  ← comparatives, halves, first goal, player props
        │
        ▼   p_honest = isotonic_calibrate( P(question) )
        ▼   p_submit = clip( p_honest + k·(p_honest − ĉ) ),  k gated by a logged γ-schedule
  • Dixon-Coles models correlated home/away goals with a low-score correction τ; strengths come from bookmaker-odds inversion live, and an odds-free Poisson-GLM fit on a 49k-match corpus for backtests.
  • Calibration is per-family isotonic regression, shrunk toward identity while data is thin, so probabilities match observed frequencies.
  • Overlay is the strategic shell: an M5 crowd model estimates where the field will land (ĉ), and Tommy presses its edge away from the crowd by a magnitude the γ-schedule sets from the rank deficit — bounded so the honest call's direction is never flipped.

Docs & running it

uv sync && uv run python scripts/devdb.py && uv run alembic upgrade head   # setup
uv run python -m probcup.orchestration.run --match <id>                    # dry-run a match
uv run pytest footy/tests/coherence -q                                     # the merge gate

Submissions stay dry-run unless both PROBCUP_LIVE=1 and --live are set — a deliberate double safety catch.

About

Worldcup football prediction project for https://sportspredict.com/probabilitycup

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages