|
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
9.21 kB
| # 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: | |
| ```bash | |
| 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: | |
| ```bash | |
| 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: | |
| ```bash | |
| 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 | |
| ```bash | |
| # 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 | |