StatSpace Nine is a public sports-modeling portfolio repo. The first flagship project inside it is a clean-room MLB Elo engine that tracks both team strength and starting-pitcher strength, supports walk-forward backtesting, and exposes a small CLI for building snapshots and daily predictions.
This public v1 is intentionally narrower than the internal research system it is based on. The goal here is to ship a runnable, inspectable, well-tested artifact with clean interfaces rather than a direct dump of private production code.
Elo is a strong baseline for baseball because it gives you a simple, interpretable rating system that updates one game at a time and remains useful even before you layer in richer features. For a public portfolio repo, that matters:
- the model logic is easy to audit
- the walk-forward workflow is explicit
- each design choice can be documented and tested
- advanced modules can be added later without breaking the core engine
- Team Elo ratings
- Starter Elo ratings
- Home-field advantage
- Margin-of-victory scaling
- Starter confidence shrinkage for small samples
- Season-to-season mean reversion toward baseline
- Walk-forward processing in chronological order
- Snapshot save/load support
- MLB Stats API-backed live schedule ingestion
- Cached public advanced-signal artifacts
- Rolling Statcast-backed xwOBA team batting signals
- Rolling Statcast-backed starter SIERA signals
- Static park-factor context signals
- MLB Stats API venue metadata for roof, dimensions, elevation, and timezone
- Walk-forward rest-day and time-zone travel signals
- Walk-forward bullpen fatigue signals
- CLI commands for
snapshot,backtest, andpredict - Pytest coverage, mypy, ruff, and GitHub Actions CI
These are explicit roadmap items, not hidden gaps:
- Market odds and season-target priors
- Probability calibration and classifier layers
- Scheduled production jobs and artifact publishing
flowchart LR
A["MLB Stats API"] --> B["Game Source<br/>normalize schedules + starters"]
A --> H["Advanced Signal Provider<br/>cached raw + derived artifacts"]
B --> C["MLBEloEngine<br/>team + starter ratings"]
H --> C
C --> D["Snapshot Artifacts<br/>JSON ratings + metadata"]
C --> E["Backtest Summary"]
C --> F["Predictions"]
G["CLI"] --> B
G --> H
G --> C
G --> D
G --> E
G --> F
src/statspace_nine/mlb_elo/
├── advanced.py # Advanced signal types and protocol
├── advanced_provider.py # Cached Statcast-backed advanced signal provider
├── advanced_repository.py # Raw + derived artifact repository
├── backtest.py # Aggregate metrics for walk-forward evaluation
├── cli.py # statspace-nine entrypoint
├── engine.py # EloConfig, MLBEloEngine, snapshot serialization
├── sources.py # GameSource protocol + MLB Stats API adapter
├── team_codes.py # Stable MLB team-code normalization
└── types.py # MLBGame, PredictionResult, BacktestSummary
The public engine currently uses these rules:
- All teams start at
1500 - All starters start at
1500 - Home field advantage is
+24 Elo - Team K-factor is
6 - Starter K-factor is
12 - Starter adjustment is scaled by
0.35 - Starter confidence ramps from
0.0to1.0over the first10starts - Team ratings revert
50%toward1500across seasons - Starter ratings revert
30%toward1500across seasons - Margin of victory uses a capped power formula with exponent
0.5
Phase 2 also computes additive public advanced signals:
- team xwOBA from cached Statcast plate-appearance-ending events
- starter SIERA from cached Statcast terminal plate appearances
- home-park run and home-run factor context from static public tables
- venue roof, dimensions, elevation, and timezone from cached MLB Stats API venue payloads
- team rest days and time zones crossed before each game
- bullpen fatigue from rolling bullpen innings over the trailing 3 days
- strict walk-forward rolling averages from prior completed games only
- explicit fallback flags when advanced signals are unavailable
The distinction matters: MLB Stats API venue payloads expose geometry and venue metadata, but not run or HR park factors. Those factor tables remain explicit manual inputs in the public repo.
These signals are exposed in predictions and artifacts but do not yet alter the headline Elo win probability.
The effective pregame gap is:
home_team_elo + home_field_advantage + home_starter_adjustment
- away_team_elo - away_starter_adjustment
The win probability is a standard Elo logistic transform:
P(home win) = 1 / (1 + 10^(-elo_gap / 400))
python3 -m pip install -e ".[dev]"python3 -m ruff check .
python3 -m mypy src
python3 -m pyteststatspace-nine snapshot \
--start-date 2025-03-18 \
--end-date 2025-09-30Advanced signals are enabled by default. To opt out:
statspace-nine snapshot \
--start-date 2025-03-18 \
--end-date 2025-09-30 \
--no-advanced-signalsDefault artifact output:
artifacts/snapshots/2025-09-30/
├── metadata.json
├── starter_ratings.json
├── summary.json
└── team_ratings.json
Advanced cache layout:
artifacts/advanced/
├── derived/
│ └── <cache-key>.json
└── raw/
├── game_feeds/
│ └── <game-id>.json
├── statcast_games/
└── <game-id>.json
└── venues/
└── <venue-id>.json
statspace-nine backtest --season 2025This writes:
artifacts/backtests/2025/
├── predictions.json
└── summary.json
statspace-nine predict --date 2026-04-11If you do not provide --snapshot-dir, the CLI bootstraps by replaying completed games starting from January 1 of the prior season.
If a snapshot includes advanced metadata, predict reuses the cached derived advanced state automatically.
Example prediction table:
Date Matchup Home Win % Elo Gap xwOBA Diff SIERA Diff
2026-04-10 CHW@KCR 53.412% 23.72 fallback fallback
2026-04-10 NYY@LAD 57.901% 40.28 0.041 0.82
Example backtest summary:
{
"seasons": [2025],
"games": 2430,
"accuracy": 0.538,
"brier_score": 0.246,
"average_home_win_prob": 0.534,
"home_win_rate": 0.529
}from statspace_nine.mlb_elo import EloConfig, MLBGame, MLBEloEngine
engine = MLBEloEngine(EloConfig())
prediction = engine.predict_game(
MLBGame(
date="2026-04-11",
away_team="NYY",
home_team="LAD",
season=2026,
away_starter_id="12345",
home_starter_id="67890",
)
)
print(prediction.home_win_prob)- The rating engine is pure model logic. It does not know how to call APIs.
- Live schedule ingestion is isolated behind a
GameSourceinterface. - Advanced metrics are isolated behind a separate provider and repository so raw fetch, cache reuse, and rolling computation are not embedded in the engine.
- Snapshot files are plain JSON so artifacts are easy to inspect and reuse.
- The CLI is intentionally thin: it orchestrates fetching, walk-forward processing, and artifact output without hiding the model flow.
- Tests use recorded fixtures for deterministic schedule normalization and smoke coverage.
A clean clone of the repo should be able to:
- install the package with dev tooling
- run lint, type checks, and tests
- build a ratings snapshot from live schedule data
- run a season backtest
- produce predictions for a given date
Near-term upgrades that fit the current interfaces:
- Add calibration and optional classifier promotion on top of raw Elo probabilities and advanced signals.
- Add market odds and season-target priors on top of the same artifact pipeline.
- Add richer reporting artifacts and eventually a lightweight public dashboard.
- Runtime data is live-fetch only in public v1.
- Generated artifacts live under
artifacts/and are intentionally ignored by git. - The package targets Python
3.11+.
MIT