Probabilistic match outcome prediction powered by Elo, Glicko-2, Bivariate Poisson, and Plackett-Luce models — built for football, cricket, F1, and beyond.
OddsEngine is a production-grade, multi-sport match prediction platform. At its core is a C++ statistical engine implementing four different mathematical rating and prediction models, exposed via a Java/Spring Boot REST API with a Next.js frontend.
The engine was designed from the ground up with a strict separation between:
- Statistical computation — high-performance C++ numerical solvers
- Orchestration & persistence — Java platform with JPA and SQLite
- API surface — RESTful endpoints and gRPC contracts
- Presentation — Next.js web dashboard
A live demonstration of the system predicted the 2026 FIFA World Cup semi-final (England vs Argentina) using real tournament match data, producing calibrated win/draw/loss probabilities via the Bivariate Poisson model.
The classical Elo rating system, extended for N-way competitions:
-
Home-field advantage offset (
$H = 100$ rating points) -
K-factor experience decay:
$K = 32$ for provisional entities (< 30 matches),$K = 16$ for established - Works for any sport with a pairwise win/loss/draw outcome
Marks Glickman's full Glicko-2 implementation with volatility tracking:
- Converts to/from the internal Glicko-2 scale (
$\mu$ ,$\phi$ ,$\sigma$ ) - Illinois algorithm (regula falsi) root-finding for volatility updates — O(1) convergence, iteration-guarded against infinite loops
- Tracks rating deviation (confidence intervals) and volatility (consistency)
- Verified against Glickman's worked example:
$r \approx 1464.06$ ,$RD \approx 151.52$ ,$\sigma \approx 0.05999$
Implements the Karlis & Ntzoufras (2003) bivariate Poisson formulation for football/soccer:
Where:
-
$\lambda_A = \alpha_A \cdot \beta_B \cdot \gamma$ — expected home goals (attack × opponent defence × home advantage) -
$\lambda_B = \alpha_B \cdot \beta_A$ — expected away goals -
$\lambda_3$ — score correlation coefficient (captures low-scoring game dependency)
Features:
- Time-decay weighting on historical matches (exponential decay, recent = higher weight)
- MLE parameter fitting via numerical gradient descent
- Identifiability normalisation: attack strengths scaled to average 1.0
- Full
$10 \times 10$ scoreline probability matrix → aggregated Win / Draw / Loss
Purpose-built for F1 and N-way ranking sports:
Where participant strength is factored as:
-
$\theta_D$ — driver skill parameter -
$\theta_C$ — constructor/car performance parameter -
$\alpha$ — grid position penalty (fitted from data, bounded$[0.01, 0.20]$ ) - Constructor-specific DNF reliability rates modelled separately from pace
- Parameters normalised post-fit for identifiability
OddsEngine/
├── proto/ # gRPC & Protobuf contracts
│ ├── domain.proto # Entity, Event, Participant, RatingSnapshot
│ └── services.proto # RatingService, PredictionService, SimulationService
│
├── engine/ # C++ Statistical Core
│ ├── src/
│ │ ├── elo.{hpp,cpp} # Generalised Elo-for-N
│ │ ├── glicko2.{hpp,cpp} # Glicko-2 with Illinois volatility solver
│ │ ├── poisson.{hpp,cpp} # Bivariate Poisson scoreline model
│ │ ├── plackett_luce.{hpp,cpp} # Plackett-Luce F1/N-way ranking model
│ │ └── main.cpp # CLI entrypoint + unit test suite
│ └── Makefile # MinGW g++ build (C++17)
│
├── platform/ # Java/Spring Boot Platform
│ └── src/main/java/com/oddsengine/
│ ├── model/ # JPA entities (SportEntity, Event, Participant, RatingSnapshot)
│ ├── repository/ # Spring Data JPA repositories
│ ├── service/
│ │ ├── EngineClient.java # C++ subprocess bridge (ProcessBuilder → JSON)
│ │ └── RatingOrchestrator.java # Orchestrates ratings + snapshots
│ └── controller/
│ └── PredictionController.java # REST API endpoints
│
├── frontend/ # Next.js 15 Web Dashboard
├── scripts/ # Data scraping + DB seeding (Python)
│ ├── fetch_football.py
│ ├── fetch_cricket.py
│ ├── fetch_f1.py
│ └── db_seed.py
└── OddsEngine_Implementation_Tasksheet.md # Full implementation roadmap
REST Request
│
▼
PredictionController (Java)
│
▼
RatingOrchestrator
├── Fetches current ratings from SQLite (JPA)
├── Calculates matches played (K-factor / RD scaling)
└── Invokes EngineClient
│
▼ JSON over stdin/stdout
engine.exe (C++)
├── Routes by "model_name"
├── Runs statistical computation
└── Returns JSON result
│
▼
RatingOrchestrator
└── Persists RatingSnapshot to SQLite
- C++: MinGW-w64 / g++ (C++17) on Windows, or GCC/Clang on Linux/macOS
- Java: JDK 21+, Gradle
- Frontend: Node.js 20+
cd engine
mingw32-make # Build engine.exe
mingw32-make test # Build + run all unit tests
mingw32-make clean # Remove build artifactsExpected test output:
All statistical core tests passed successfully!
The test suite covers:
- Pairwise Elo updates (established + provisional K-factors)
- N-way Elo with home advantage
- Glicko-2 parameter recovery (vs Glickman's paper)
- Poisson scoreline matrix validation
- Plackett-Luce parameter recovery for F1-style results
cd platform
./gradlew bootRun # Start Spring Boot server (port 8080)
./gradlew test # Run integration testsThe engine accepts a single JSON line on stdin and writes a JSON result to stdout.
echo '{"model_name":"elo","participants":[{"entity_id":"TeamA","finish_rank":1,"current_rating":1500.0},{"entity_id":"TeamB","finish_rank":2,"current_rating":1500.0}]}' | ./engine.exe --cliecho '{"model_name":"glicko2","participants":[{"entity_id":"P1","finish_rank":1,"current_rating":1500.0,"rating_deviation":200.0,"volatility":0.06},...]}' | ./engine.exe --cliecho '{
"model_name": "poisson",
"history": [
{"home_id":"ENG","away_id":"ARG","home_goals":4,"away_goals":2,"weight":0.95},
...
],
"predict_match": {"home_id":"ENG","away_id":"ARG"}
}' | ./engine.exe --cli
# → {"probabilities": {"win": 0.547431, "draw": 0.274523, "loss": 0.178047}}echo '{
"model_name": "plackett_luce",
"history": [...],
"predict_entrants": [
{"entity_id":"VER","driver_id":"VER","constructor_id":"RBR","grid_position":1},
...
]
}' | ./engine.exe --cli
# → {"probabilities": {"VER": 0.38, "HAM": 0.22, ...}}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/events/process |
Process an event and update ratings |
GET |
/api/ratings/{entityId} |
Fetch rating history for an entity |
POST |
/api/ingest/batch |
Ingest bulk JSON payloads of EventWrappers |
You can ingest bulk match data from external providers (football-data.co.uk, Cricsheet, Jolpica F1) using the included Python adapters:
python scripts/fetch_football_api.py # Premier League CSV -> JSON Ingestion
python scripts/fetch_cricket_api.py # Cricsheet match data
python scripts/fetch_f1_api.py # Jolpica F1 race resultsThis pushes JSON EventWrapper payloads to the Spring Boot backend, automatically deduplicating fixtures, mapping entities, and triggering the underlying statistical model updates.
POST /api/events/process
{
"eventId": "wc2026-sf-1",
"sportId": "football",
"modelName": "poisson",
"venue": "Atlanta Stadium",
"participants": [
{ "entityId": "ENG", "finishRank": 1 },
{ "entityId": "ARG", "finishRank": 2 }
]
}Using the Bivariate Poisson model with 27 real match records (2026 World Cup tournament data + 2025 qualifiers + 2024 Copa América), the engine produced the following prediction for the 2026 FIFA World Cup Semi-Final:
| 🏴 England | ➖ Draw | 🇦🇷 Argentina |
|---|---|---|
| 54.7% | 27.5% | 17.8% |
Data was weighted by recency (exponential decay: WC knockouts 0.97–0.99, qualifiers 0.55–0.68), and attack/defense parameters were fitted via MLE gradient descent.
| Phase | Description | Status |
|---|---|---|
| 0 | Architecture, Protobuf contracts, DB schema | ✅ Complete |
| 0.3 | Data feasibility spike (scraping scripts) | ✅ Complete |
| 1–2 | Java platform, JPA models, REST API | ✅ Complete |
| 1.1 | Elo refinements (home advantage, K-decay) | ✅ Complete |
| 1.2 | Glicko-2 module (volatility, Illinois solver) | ✅ Complete |
| 1.3 | Bivariate Poisson scoreline model | ✅ Complete |
| 1.3b | Plackett-Luce F1/N-way ranking model | ✅ Complete |
| 1.3c | Cricket format-aware model | ✅ Complete |
| 1.4 | Bayesian updating layer | 🔜 Upcoming |
| 1.5 | Monte Carlo season simulator | 🔜 Upcoming |
| 1.6 | Model evaluation & backtesting utilities | ✅ Complete |
| 1.7 | gRPC service layer | ✅ Complete |
| 5a | Chat: Fixture Resolution & Prediction | ✅ Complete |
| 5b | Chat: Conversational Analytics | 🔜 Upcoming |
| 6 | Context-Aware Feature Layer | 🔜 Upcoming |
| 7 | UI Redesign (hybrid chat/dashboard) | 🔜 Upcoming |
| Layer | Technology |
|---|---|
| Statistical Core | C++17, MinGW-w64 |
| Platform | Java 21, Spring Boot 3, Gradle |
| Database | SQLite (via JPA/Hibernate) |
| Migrations | Flyway |
| Contracts | gRPC, Protocol Buffers |
| Frontend | Next.js 15 |
| Data Pipeline | Python 3 (requests, sqlite3) |
This project follows strict micro-commit discipline:
- All commits are < 100 lines of changes
- Features are developed on sub-branches, merged with
--no-ff - Branch topology is preserved for clean history inspection
Built with rigorous statistical foundations and an eye toward production-grade architecture.
Elo · Glicko-2 · Bivariate Poisson · Plackett-Luce