Skip to content

Latest commit

 

History

89 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FutbolTipster

Statistical football predictions (1X2 and derived markets) powered by Dixon-Coles and Negative-Binomial models, presented with confidence bands. Informative, not betting advice.

FutbolTipster — match list

What it is

A full-stack project that predicts football match outcomes from a statistical engine and shows them in a responsive web app:

  • 6 leagues: 5 European (Premier League, La Liga, Serie A, Bundesliga, Ligue 1) + the Ecuadorian Liga Pro (EC1)
  • ~20 derived markets per match: double chance, over/under, Asian handicap, HT and HT/FT, corners, bookings, shots on target, fouls, first goal/corner
  • Confidence bands on every prediction (Safe, Likely, Tight, Uncertain) so you know how much to trust each pick
  • Two model families: Dixon-Coles (bivariate Poisson for full-time and first-half scores) and Negative-Binomial for count markets, trained on 2014–2025 results with temporal decay and recent form (k=5)

FutbolTipster — expanded match with markets

How it works

ESPN (live fixtures)  →  server (Node/Express + SQLite)  →  ml-service (FastAPI models)  →  web (React)

The server syncs fixtures from ESPN on a cron schedule, resolves team names against the models, asks the ml-service for predictions, and persists everything in SQLite. The web app reads from /api — nothing else.

Tech stack

Service Stack Role
ml-service Python / FastAPI Statistical models and markets (/predict, /teams, /health)
server Node / Express + SQLite ESPN orchestration, team resolution, predictions, tracking
web React 19 / Vite / Tailwind v4 Responsive SPA that only reads /api

Architecture

ESPN scoreboard (fixtures, -2..+14 days)
   │  cron sync (SYNC_CRON, 06:00 local · TZ)
   ▼
server  Node/Express + SQLite  (:4000)
   │  runSync → refreshFixtures → resolveTeam → predictFixture → persist
   │  POST /predict
   ▼
ml-service  FastAPI  (:8001)
   │  models loaded from artifacts/*.npz
   │  (Dixon-Coles FT & HT, HT/FT conditional, Negative-Binomial counts)
   ▼
web  React 19 + Vite  (:5173)
   │  reads /api only (Vite proxy in dev)

Key design points:

  • One shared model for the 5 European leagues; Liga Pro (EC1) uses its own Dixon-Coles trained on ESPN history — EC1 data never touches the global model.
  • Artifacts are reproducible: ml-service/data/ and ml-service/artifacts/ are gitignored and regenerated with the download scripts + scripts/train.py.
  • Confidence bands have a single source of truth: the ml-service serves GET /bands and the server caches it with a fallback.
  • Auto re-prediction: if the model is retrained (trained_at changes), the server force-repredicts pending fixtures.

Repository structure

ml-service/              # Python / FastAPI — statistical models
  app/
    api.py               # /predict, /teams, /health, /models, /bands
    data.py              # loads historical CSVs (5 European leagues + EC1)
    models/
      dixon_coles.py     # Dixon-Coles for FT and HT scores
      count_model.py     # Negative-Binomial for count markets (recent form, k=5)
      markets.py         # ~20 derived markets
  scripts/               # download_data, download_espn_ecuador, train, validate, backtest
  tests/                 # pytest (37 tests)
  requirements.lock      # pinned Python dependencies

server/                  # Node / Express + SQLite — orchestration
  src/
    index.ts             # bootstrap, cron sync, health-check of ml-service
    config.ts            # env: PORT, ML_URL, SYNC_CRON, DB_PATH, REFRESH_TOKEN, CORS
    db.ts                # node:sqlite — fixtures, picks, stats, meta
    teams.ts             # resolves ESPN display names → model team names (fuzzy ≥ 0.8)
    dates.ts             # local dates (TZ) for the fixture window and filter
    providers/espn.ts    # ESPN scoreboard client
    routes/api.ts        # /api/leagues, /api/fixtures, /api/stats, /api/refresh
    services/predict.ts  # runSync, re-prediction by trained_at, backfill
    data/teamOverrides.ts   # ESPN → model aliases (single source)
    lib/json.ts          # safe JSON response helpers
  package.json

web/                     # React 19 / Vite / Tailwind v4 — frontend
  src/
    App.tsx              # league tabs, silent auto-refresh every 60s
    api.ts · bands.ts · heat.ts · utils.ts
    components/          # MatchCard, Markets, ProbabilityBar, ConfidenceBadge, BandLegend,
                         #   Countdown, MatchToolbar, SpotlightCard, Sidebar, Header,
                         #   ThemeToggle, Tooltip, ErrorBoundary
    components/ui/       # shadcn/ui primitives: badge, button, select, dropdown-menu, tooltip
    hooks/useTheme.ts    # light/dark theme
  e2e/                   # smoke (7 checks) + responsive (51 checks) via playwright-core
  scripts/               # verify-* checks and screenshot helper
  package.json

Key packages

ml-service (Python)

Package Purpose
fastapi + uvicorn API server
numpy · scipy · pandas model math and data
pydantic request/response validation
pytest · httpx tests (httpx is required by FastAPI's TestClient)

server (Node)

Package Purpose
express HTTP API
node-cron sync schedule
cors · dotenv middleware and env
node:sqlite (built-in) persistence
dev: tsx · typescript · vitest · eslint · prettier running and tooling

web (React)

Package Purpose
react · react-dom UI
@base-ui/react + shadcn/ui accessible primitives
lucide-react icons
motion animations
tailwindcss v4 (@tailwindcss/vite) styling
dev: vite · vitest · testing-library · playwright-core · shadcn build, tests, e2e

Testing & CI

# ml-service (Python)
cd ml-service && python -m ruff check app tests scripts && python -m ruff format --check . && python -m pytest tests -q   # 37 tests

# server (Node)
cd server && npm run lint && npm test && npm run typecheck                                                            # 23 tests

# web (React)
cd web && npm run lint && npm test && npm run typecheck                                                               # 28 tests

# e2e (requires the three services running)
cd web && npm run test:e2e && npm run test:e2e:responsive                                                              # 7 + 51 checks

GitHub Actions (.github/workflows/ci.yml) runs lint, typecheck, tests and pip-audit for all three services on every push.

Getting started

# ml-service (models)
cd ml-service
pip install -r requirements.lock
uvicorn app.api:app --port 8001

# server (Express + SQLite + ESPN)
cd server
cp ../.env.example .env
npm install && npm run dev

# web (frontend)
cd web
npm install && npm run dev   # http://localhost:5173

Environment variables are documented in .env.example; API endpoints and the data/training pipeline are documented in the source of each service (server/, ml-service/, web/).

Validation & honesty

Models are validated walk-forward on out-of-sample seasons (2023–2025, n≈5.4k): log-loss ≈ 0.99, RPS ≈ 0.20, ~52% accuracy for the FT model. A flat-betting backtest of all 14 markets with synthetic SBOBET-style odds shows no market beats the 7% bookmaker margin — derived markets are informative, not a betting system.

Roadmap

  • Band-level recalibration (deferred): only if the project is monetized.
  • Player-level markets (deferred): needs a scorer-per-match data source.

License

MIT — see LICENSE.

Author

Pablo Domínguez — GitHub · LinkedIn

About

Statistical football predictions (1X2 and ~20 derived markets) using Dixon-Coles and Negative-Binomial models with confidence bands. Full-stack monorepo: FastAPI ml-service, Express server, React web.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages