Dugout Value
Baseball research on pitching, hitting, and projections, plus tools for comparing players, contracts, and trade value.
Live, backtested football predictions: goals O/U and player shots/SOT.
Translation pending
Sports & fitnessPython · TypeScriptGitHub @tanamsethi31OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Stats-first football betting research: Dixon-Coles + lineup-aware models, walk-forward backtested for real edge vs. bookmaker closing odds.
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗A stats-first football betting research project: does positive expected value (ROI) vs. bookmaker closing odds exist anywhere in these markets, and if so, where exactly? Not "does this predict football well" — a model can be accurate and still lose money once the market's own margin is priced in.
Every claimed edge here has been walk-forward backtested (never trained on
future data) and calibration-checked before being trusted. Most markets
tested show no edge, and that's reported just as loudly as the ones that
do — see Results at a glance and the full
RESULTS.md.
footymodel.vercel.app — public view of the Premier League SofaScore paper-trading pipeline: player shots/SOT and goals Over/Under 2.5. Tabs: Predictions, Goals O/U, Track record (graded actuals + €100 paper bankroll when synced from the repo), Features, and Glossary. Paper-trade only; no login.
Operating docs: docs/SHOTS_SOT_OPERATING.md
(match-week playbook), docs/PAPER_PROPS_EXPERIMENT.md
(staking rules).
| Market / feature | Verdict | Key number |
|---|---|---|
| Over/Under, 1X2 (goals model) | ❌ No edge | CLV ≈ −0.2%, yield ≈ −7% |
| Asian Handicap (all 7 leagues) | ❌ No edge | best config +2.05% yield but −1.52% CLV — the "positive yield, negative CLV" noise trap |
| Lineup-aware total-goals accuracy | ✅ Confirmed | pooled t = 3.04 across all big-5 leagues |
| Closing-line goals O/U (historical) | ❌ No edge | see row above; live path is lineup-window paper only |
| Lineup-edge → live goals paper | 🔴 Active (hypothesis) | SofaScore match O/U + shared €100 ledger — live_recommendations.csv |
| Player shots/SOT calibration | ✅ Well-calibrated | gaps mostly within ±0.03, full PL + Bundesliga seasons |
| Player shots/SOT → live paper | 🔴 Active | same bankroll — dashboard + paper_bankroll_bets.csv |
Full detail, per-league tables, and every rejected hypothesis (rest-days,
mid-tier lineup extension, etc.) are in RESULTS.md.
Betting favorites gives a high hit rate and negative returns. We optimize for edge vs. the bookmaker's margin-stripped ("fair") probability. Profit comes from calibration + value detection + staking discipline, on top of a principled statistical core (Dixon-Coles / Poisson).
Production (live) — player shots/SOT + goals O/U 2.5, zero paid live APIs:
flowchart LR
subgraph Free_cloud
FPL[FPL fixture feed]
US[Understat rosters<br/>post-match grade]
MP[matches.parquet<br/>goals grade]
GH1[live_poll.yml<br/>calendar + watchlists]
GH2[grade_props.yml<br/>grade props + settle]
GH3[grade_results.yml<br/>grade goals + settle]
end
subgraph Home_network
SS[SofaScore Playwright<br/>confirmed XI]
MD[sofascore_matchday]
PB[paper_bankroll]
end
FPL --> CAL[(fixture_calendar.json)]
CAL --> GH1
GH1 --> REPO[(GitHub repo)]
SS --> MD
MD --> PROPS[(live_player_props.csv)]
MD --> RECS[(live_recommendations.csv)]
MD --> PB
PB --> BETS[(paper_bankroll_bets.csv)]
PROPS --> REPO
RECS --> REPO
BETS --> REPO
US --> GH2
MP --> GH3
GH2 --> REPO
GH3 --> REPO
REPO --> DASH[Next.js dashboard<br/>Vercel]
Research / legacy (not used in production): Dixon-Coles closing-line backtests,
recommend.py, API-Football / RapidAPI / Apify engines — see RESULTS.md.
The dashboard reads committed files from the repo (no separate database).
Props polling must run on a home IP (laptop script or self-hosted runner);
see docs/SHOTS_SOT_OPERATING.md.
footymodel/
config.py # leagues, seasons, data source URLs, column maps
data.py # Phase 1 — download + normalize football-data.co.uk CSVs
players.py # Phase A — LineupModel (confirmed-lineup goals prediction)
live/ # Phase B — live detection/paper-trade pipeline
sofascore_matchday.py # **production** — one poll: goals then props
sofascore_props_engine.py # PL player props + paper bets
sofascore_engine.py # lineup-aware goals O/U + paper bets
paper_bankroll.py # shared €100 ledger + staking
paper_props_bankroll.py # re-export shim
sofascore_props_odds.py # SofaScore player-prop odds (when exposed)
shots_engine.py # shots/SOT models + row builder
calendar.py # FPL calendar + idle-poll gate
fpl_client.py # free PL fixture feed
match_detail.py # JSONL: lineups + breakdown
engine.py / rapidapi_engine.py / apify_engine.py # legacy, not production
docs/
SHOTS_SOT_OPERATING.md # match-week playbook (start here)
PAPER_PROPS_EXPERIMENT.md # € bankroll rules
SELF_HOSTED_PROPS.md # laptop vs self-hosted runner
MATCHDAY_AUTOMATION.md # cron + calendar gate
dashboard/ # Next.js app (Vercel) — props + goals O/U public UI
data/
raw/ # downloaded CSVs (gitignored)
processed/ # normalized tidy tables + live logs (mostly gitignored,
# tracked exceptions listed in .gitignore for the cron to
# commit back to the repo)
cd ~/footymodel
python3.11 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
requirements.txt is the production lock file. Update dependency ranges in
requirements.in, then regenerate it with
uv pip compile requirements.in --output-file requirements.txt --python-version 3.11.
SofaScore props polling also needs Playwright Chromium:
playwright install chromium
Scope: Premier League only. Markets: player shots (1+/2+/3+),
SOT (1+/2+), and Over/Under 2.5 goals (lineup-aware). One internal
€100 paper bankroll (paper_props_bankroll.json, ledger paper_bankroll_bets.csv).
| What | Command / workflow |
|---|---|
| Match-week schedule (Irish time) | PYTHONPATH=. python scripts/matchday_schedule.py |
| Poll + paper bets (home Wi‑Fi) | ./scripts/matchday_laptop.sh loop 900 --push |
| Bankroll summary | python scripts/paper_props_summary.py |
| Cloud calendar | .github/workflows/live_poll.yml |
| Cloud grade props + settle | .github/workflows/grade_props.yml |
| Cloud grade goals + settle | .github/workflows/grade_results.yml |
| Optional always-on home PC | .github/workflows/live_poll_props_selfhosted.yml |
Full playbook: docs/SHOTS_SOT_OPERATING.md. Automation:
docs/MATCHDAY_CLOUD_AUTOMATION.md. Goals plan:
docs/GOALS_OU_PAPER_PLAN.md.
Dry-run (no network):
python scripts/sofascore_props_dryrun.py
python scripts/paper_props_bankroll_test.py
python scripts/goals_ou_math_audit.py
. .venv/bin/activate
python -m footymodel.data # download + normalize default leagues/seasons
python -m footymodel.data --leagues E0 E1 N1 --seasons 2021 2022 2023 2024
Output: data/processed/matches.parquet — one tidy row per match.
Not part of the live product. Kept for backtests and
paper_trades.csvdemos on historical football-data.co.uk odds.
The recommender fits the model on all history and outputs O/U value bets with fractional-Kelly stakes for upcoming fixtures, logging them for paper-trading.
. .venv/bin/activate
# Demo on the latest matchday in the dataset (grades vs actual results):
python -m footymodel.recommend --league E1
# Live use next season — supply current fixtures + odds:
# fixtures.csv columns: home_team,away_team,odds_over25,odds_under25
python -m footymodel.recommend --league E1 --fixtures fixtures.csv
Every run appends to data/processed/paper_trades.csv. Team names must match
football-data.co.uk spellings (e.g. "Sheffield United", "Nott'm Forest").
HONEST STATUS. The goals/xG statistical model shows no edge vs. the market (
RESULTS.md): O/U yield ~−7%, CLV ~−0.2%. The one confirmed exception is the lineup-aware model (Phase A below) — knowing the actual starting XI (attack + defence) measurably improves O/U prediction accuracy, replicated significantly across all 5 big-5 leagues (pooled t=3.04). Live lineup-window goals O/U is paper-traded on SofaScore (see Live SofaScore paper trading); closing-line backtests above remain the honest baseline.
players.py's LineupModel predicts total goals from the actual starting XI
(both attack AND defence ratings, not team averages) using scraped Understat
per-match rosters. This is the single model improvement that has held up under
rigorous cross-league testing in this project — see RESULTS.md for the full
big-5 confirmation (every league improved in the same direction, unlike an
earlier half-built version that didn't replicate).
python -m footymodel.understat # already run — big-5 xG cached
python scripts/build_players.py E0,SP1,D1,I1,F1 # scrape rosters (slow, cached)
python scripts/lineup_test.py E0,SP1,D1,I1,F1 # reproduce the t=3.04 confirmation
Goals O/U live polling (sofascore_engine.py, rapidapi_engine.py, etc.) and
paid API keys are legacy / local recovery only. Production live data is:
live_poll.yml (calendar + watchlists), grade_props.yml (Understat grade + € settle).sofascore_props_engine.py via matchday_laptop.sh or
live_poll_props_selfhosted.yml.See docs/SELF_HOSTED_PROPS.md. Optional .env
keys (API_FOOTBALL_KEY, RAPIDAPI_KEY, APIFY_TOKEN) are documented in
.env.example but not required for the props product.
Legacy dry-runs (mocked APIs, no quota):
python scripts/shots_live_dryrun.py
python scripts/live_dryrun.py # goals engine only
python scripts/run_all_dryrun.py
python scripts/paper_props_summary.pypython scripts/live_summary.py (legacy live_recommendations.csv)data.py)model.py)understat.py)backtest.py) → no edgestrategy.py)staking.py)recommend.py)players.py) → confirmed edge, t=3.04live/) → live in production, polling
via GitHub Actions every 20minmodel.py predict_ah,
scripts/ah_backtest.py) → no edge (best config: +2.05% yield but
CLV −1.52%, the "positive yield/negative CLV = noise" trap). Mid-tier
lineup extension via FBref researched and confirmed not viable (no
per-player xG for Championship/Eredivisie) — see RESULTS.md.footymodel/fbref.py,
scripts/fbref_ingest_batch.py, scripts/fbref_sot_calibration_test.py)
→ well-calibrated on one PL season (gaps within ±0.03 in populated
buckets). See RESULTS.md Phase D.footymodel/whoscored.py, scripts/whoscored_calibration_test.py)
→ well-calibrated, full 380/380 PL 2023/24 season. See RESULTS.md
Phase E.Most of this project's real correctness checks are backtests, not unit tests — a walk-forward calibration table catches more than an assert would. But a handful of non-trivial, data-independent logic paths do have standalone pure-Python checks, run on every push via GitHub Actions:
python scripts/ah_settlement_test.py
python scripts/goals_odds_parse_test.py
python scripts/props_odds_parse_test.py
python scripts/sofascore_odds_parse_test.py
python scripts/rapidapi_odds_parse_test.py
python scripts/apify_odds_parse_test.py
python scripts/grade_results_datetime_test.py
python scripts/grade_results_columns_test.py
python scripts/match_detail_test.py
python scripts/kelly_simulation_test.py
python scripts/calendar_test.py
Everything under live/ additionally has a dry-run mode (mocked API
responses, no key or quota spent) — see
Before spending API quota above.
The dashboard (TypeScript, no test framework) is verified via npx tsc --noEmit and npm run build on every change.
Copyright © 2026 Tanam Sethi. All rights reserved. No license is granted to copy, modify, or redistribute this code or its contents.
live/run_all.py) → goals and
player-props engines now share ONE fixtures+lineups fetch per poll
instead of two independent ones, which would have doubled API-Football
usage past the Free-tier daily quota.shots_engine.py, same
API-Football key as everything else. Forward paper-trade only (no
historical prop-odds archive is reachable).rapidapi_engine.py
and sofascore_engine.py as fallbacks after API-Football's free tier
was suspended once without warning (all three log to the same CSV,
tagged by source); moved production polling from a local crontab to a
GitHub Actions cron (live_poll.yml) so nothing depends on a machine
staying on. SofaScore's fallback currently blocked by bot-detection on
GitHub's shared runner IPs specifically — tracked, non-blocking.dashboard/) → Next.js app on Vercel,
reading live data straight from this (private) repo via the GitHub
Contents API. Five tabs (Track record, Goals O/U, Player props,
Staking, Glossary); expandable match cards showing the team-model vs.
lineup-model breakdown and starting XI behind each Goals O/U
prediction (live/match_detail.py logs the extra data, previously
computed and discarded); mobile-responsive; past-kickoff predictions
collapse into a "show N past predictions" disclosure instead of either
cluttering the upcoming list or disappearing entirely.refresh_training_data.yml)
→ nothing was keeping matches.parquet/matches_xg.parquet/
player_match.parquet current as the season progressed. Now runs
daily in GitHub Actions, no laptop involved: full big-5 season history
for match results/base xG (cheap, one request per league-season), but
build_players.py's per-match Understat roster scrape only refreshes
the current season - re-scraping the full history daily would be
thousands of redundant requests. Required fixing build_players.py to
merge the refreshed season into the existing file instead of
overwriting it wholesale (merge_player_dataset(), unit tested).live/calendar.py) → daily
10-day kickoff snapshot so the dashboard can preview next weekend
during a quiet midweek, idle live_poll ticks skip Playwright/API
lineup fetches, and a delayed poll still queries yesterday when an
unseen fixture sits in the 24h backward window. Missing/stale
calendar fails open (runs the engines) rather than risking a miss.