Download PROJECT_DETAILS.md from chetanmitra12/tennis-model: direct link, hf CLI and curl.
- Browser
- Download file 9.21 kB
-
https://huggingface.co/chetanmitra12/tennis-model/resolve/main/PROJECT_DETAILS.md
- Command line
-
hf download hf://chetanmitra12/tennis-model/PROJECT_DETAILS.md
-
curl -L -o PROJECT_DETAILS.md https://huggingface.co/chetanmitra12/tennis-model/resolve/main/PROJECT_DETAILS.md
ATP Tennis Match Prediction Model
ATP match prediction using point-in-time Elo ratings, serve-return matchup features, recent form, and calibrated probability models.
Current Model
The current feature rebuild contains 42 features and 81,547 chronological feature rows (through 2026-06-28). The saved artifact is trained and calibrated only before 2026-01-01; 2026 is the held-out test year. It contains calibrated XGBoost and regularized logistic candidates. Calibrated logistic is the current default because it was the strongest 2026 holdout candidate; set MODEL_KIND=xgboost to run the tree challenger.
Baseline Feature Reference
| # | Feature | Weight | Description |
|---|---|---|---|
| 1 | elo_diff_x_expected | 28.18% | elo_diff Γ sigmoid β auto-downweights close matchups |
| 2 | surface_elo_weighted | 13.51% | surface_elo_diff Γ sigmoid Γ min(surf_matches,30)/30 |
| 3 | elo_volatility_diff | 3.44% | Std dev of recent Elo changes |
| 4 | diff_win_rate_weighted | 5.62% | (P1_wr - P2_wr) Γ min(min_matches,200)/200 |
| 5 | diff_form_5 | 4.65% | Last 5 match win rate diff |
| 6 | diff_form_10 | 3.35% | Last 10 match win rate diff |
| 7 | diff_ace_rate | 3.56% | EMA ace rate diff |
| 8 | diff_bpSave | 3.35% | EMA break point save diff |
| 9 | diff_bpConv | 3.42% | EMA break point conversion diff |
| 10 | ret_vs_1stServe | 4.61% | P1_retWonΓ(1-P2_1stWon) - reverse (5Γ feature_weight) |
| 11 | ret_vs_2ndServe | 3.86% | P1_retWonΓ(1-P2_2ndWon) - reverse (5Γ feature_weight) |
| 12 | h2h_close | 3.82% | (h2h_wr-0.5)Γmin(total,10)/10 Γ closeness |
| 13 | surface_wr_close | 4.50% | diff_surface_wr Γ closeness |
| 14 | ret_matchup_close | 4.05% | (ret_vs_1st+ret_vs_2nd) Γ closeness |
| 15 | best_of | 6.39% | 3 or 5 sets |
| 16 | indoor | 3.69% | Indoor/outdoor flag |
Added Serve/Return Features
The five direct serve/return features added in the earlier rebuild are diff_first_serve_in, diff_first_serve_points_won, diff_second_serve_points_won, diff_service_hold, and diff_return_points_won. The current 42-feature rebuild adds 21 more leakage-safe features: 20-match, surface, and 52-week form; rest and 14/30-day workload; a bounded fatigue score; point-weighted Bayesian first-serve, second-serve, return, and total-point rates; opponent-adjusted serve and return residuals; surface Bayesian rates; log ranking context; serve sample size; stat coverage; and handedness matchup. Tennis Abstract match statistics, when available, update those estimates only after the match.
Betting Strategy
- Live selection: pre-match higher-Elo favorite, probability >= 0.60, net edge >= 0.01
- Price: executable YES ask, less any configured per-contract fee
- Live sizing: favorite-ask price only, multiplied by the configured sizing fraction and capped at 1% of bankroll
- Live matches: always skipped
The historical backtest is a separate favorite-only evaluation: every matched settled row is a bet on the lower-priced sportsbook favorite. The displayed test is 2026 only; all feature rows before 2026 are training/calibration history and are never included as 2026 test rows. It does not emit BET/SKIP rows or use model edge to decide inclusion. Sizing starts with $100 and compounds after every bet using the current bankroll. The frozen sizing method is min(0.5% of bankroll, 0.5% / favorite decimal odds), selected before the 2026 test period. The current 2026 result is 717W-316L, -2.79% ROI, $89.89 final bankroll, and 17.19% maximum drawdown across 1,033 matched bets. This is historical sportsbook diagnostics, not proof of executable positive edge.
Evaluation
The previous sportsbook ROI backtest was disabled because it used final tracker state for historical matches and had no trustworthy pre-match execution timestamps. Use the leakage-safe walk-forward evaluator for model metrics.
New odds polls are appended to odds_snapshots.csv with event and market tickers, observation time, bid/ask, and pre-match status. ROI should only be computed from that immutable snapshot history.
Tennis Abstract Enrichment
prod_core/tennisabstract_import.py downloads the embedded matchmx arrays from Tennis Abstract player-classic pages, parses match-level serve/return counts and duration, and writes data/tennisabstract_match_stats.csv. It can also create data/tml-data/all_matches_ta_enriched.csv; core_model.py uses that derived file automatically when present and never modifies the raw CSV.
The importer is cached and resumable. The model-targeted crawl uses the players
and match keys in data/core_meta.csv (the 81k rows actually used by the
model), checkpoints the canonical sidecar, and keeps player failures in a
progress file so it can be continued safely:
python scripts/tennis_model/prod_core/tennisabstract_import.py --target-meta data/core_meta.csv --replace --merge --delay 2.0 --retries 3 --retry-failures
The crawl is intentionally serial and rate-limited. It may take a long time;
rerunning the same command without --replace resumes from
data/tennisabstract_match_stats.progress.json and reuses
data/tennisabstract_cache/. TA player pages return career history, so the
fetcher may read more rows than the target, but the sidecar retains only the
year-aware keys from core_meta.csv. The raw match file remains unchanged and
continues to supply non-target history needed for rolling pre-match state.
The sidecar also retains target rows for which TA reports no serve stats
(stat_count=0), letting the audit distinguish match coverage from complete
stat coverage.
Run the coverage audit after a crawl, before rebuilding the model:
python scripts/tennis_model/prod_core/audit_tennisabstract_coverage.py
It writes output/tennisabstract_coverage.json and
output/tennisabstract_coverage_by_year.csv. Tennis Abstract reports tournament
start dates for many historical rows, so the importer uses a
tournament/surface/round/player join when an exact row date is unavailable.
These statistics are used only to update rolling state after a completed match;
they are never features for that same match. The source is rate-limited, so
failed players are recorded in .failures.csv and can be retried later with
--retry-failures. A TA page may still legitimately lack match-level stats;
those rows remain visible in the audit rather than being misrepresented as
fully enhanced.
The backtest dashboard is available locally with:
node web/backtest/server.js
It uses walk_forward_bookmaker_predictions.csv, walk_forward_sized_trades.csv, and train_test_split_audit.json; it does not deploy to Fly.
Project Structure
tennis-model/
βββ scripts/
β βββ prod_core/core_model.py # Builds chronological features from raw data
β βββ prod_core/core_backtest.py # Trains calibrated model candidates
βββ models/
β βββ models_elo.pkl # Trained XGBoost model (requires EloSystem class)
β βββ tracker_state_elo.pkl # Player stats, Elo ratings, name lookup
βββ data/
β βββ clean_data.pkl # Preprocessed 195,614 matches
β βββ X_elo.npy # Feature matrix (73,405 Γ 16)
β βββ y_elo.npy # Labels
β βββ dates_elo.npy # Match dates
β βββ sportsbook/2025.xlsx # Sportsbook odds 2025
β βββ sportsbook/2026.xlsx # Sportsbook odds 2026
β βββ tml-data/ # Raw yearly CSVs + ATP_Database.csv
βββ output/
β βββ all_baseline_bets.csv # All conf>0.50 bets with Kelly
β βββ conf60_bets.csv # Filtered conf>=0.60 bets
βββ README.md
Running
# Step 1: Feature engineering (builds X.npy, y.npy, dates.npy, tracker)
python scripts/tennis_model/prod_core/core_model.py
# Step 2: Train calibrated candidates
python scripts/tennis_model/prod_core/core_backtest.py
# Step 3: Leakage-safe walk-forward metrics
python scripts/tennis_model/prod_core/walk_forward_eval.py
# Step 4: Favorite-only historical price backtest and odds-only sizing
python scripts/tennis_model/prod_core/walk_forward_bookmaker_backtest.py
python scripts/tennis_model/prod_core/walk_forward_sizing_eval.py
# Step 5: Independent train/test boundary audit
python scripts/tennis_model/prod_core/verify_train_test_split.py
Key Technical Notes
- EloSystem class must be defined before unpickling models/tracker (pickle dependency)
- Feature order in every make_fv() must exactly match prod_core/core_model.py
- Challengers update player stats but don't generate training rows
- P1 is always the higher-Elo player during training and prediction
- Walk-forward training rows are strictly before each calendar-year evaluation window; calibration is a later slice inside training history
- feature_weights param: 5Γ for ret_vs features, 3Γ for ret_matchup_close
- Closeness factor: 1.0 - abs(elo_expected - 0.5) * 2.0
- Elo time-weighting is anchored to the data cutoff, not the machine clock
- Tennis Abstract enrichment is a post-match sidecar; it cannot provide reliable event-time execution timestamps