Spaces:
Paused
Paused
Commit Β·
3a83eb9
1
Parent(s): cbc2d9e
feat: Cat 38 venue-conditional matchup features (14 new) + doc updates
Browse filesEngine v3.0-38cat: 6149 features. Venue-specific home/away comparisons
(WP edge, margin edge, ORtg/DRtg cross-matchup) at 5/10/20 game windows.
Updated CLAUDE.md and HF README to reflect current architecture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README.md +4 -3
- features/engine.py +45 -1
README.md
CHANGED
|
@@ -13,8 +13,9 @@ pinned: true
|
|
| 13 |
|
| 14 |
Always-running agentic loop for NBA quantitative prediction models.
|
| 15 |
|
| 16 |
-
- **
|
| 17 |
- **8 seasons** of NBA data (9,551+ games, 2018-2026)
|
| 18 |
-
- **
|
| 19 |
- **Walk-forward backtesting** with Kelly criterion sizing
|
| 20 |
-
- **
|
|
|
|
|
|
| 13 |
|
| 14 |
Always-running agentic loop for NBA quantitative prediction models.
|
| 15 |
|
| 16 |
+
- **5 tree-based models** evolved via NSGA-II genetic algorithm (CPU-optimized)
|
| 17 |
- **8 seasons** of NBA data (9,551+ games, 2018-2026)
|
| 18 |
+
- **Up to 200 features** (island-specific: 55-80) from v3.0-37cat engine with MOVDA
|
| 19 |
- **Walk-forward backtesting** with Kelly criterion sizing
|
| 20 |
+
- **Feature engine**: v3.0 + Cat36 EWMA + Cat37 MOVDA (deployed 2026-03-25)
|
| 21 |
+
- **MAX_FEATURES=200** hard cap, mutation capped at 0.15, xgboost_brier fixed
|
features/engine.py
CHANGED
|
@@ -63,7 +63,7 @@ from typing import Dict, List, Tuple, Optional
|
|
| 63 |
import math
|
| 64 |
|
| 65 |
# ββ Engine Version ββ
|
| 66 |
-
ENGINE_VERSION = "v3.0-
|
| 67 |
|
| 68 |
# ββ Team mappings ββ
|
| 69 |
TEAM_MAP = {
|
|
@@ -2233,6 +2233,20 @@ class NBAFeatureEngine:
|
|
| 2233 |
"movda_win_prob", # MOVDA-derived win probability
|
| 2234 |
])
|
| 2235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2236 |
self.feature_names = names
|
| 2237 |
|
| 2238 |
def build(self, games, market_data=None, referee_data=None, player_data=None, quarter_data=None):
|
|
@@ -5044,6 +5058,36 @@ class NBAFeatureEngine:
|
|
| 5044 |
row.append(_movda_dr / 400.0) # movda_diff
|
| 5045 |
row.append(_movda_wp) # movda_win_prob
|
| 5046 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5047 |
X.append(row)
|
| 5048 |
y.append(1 if hs > as_ else 0)
|
| 5049 |
|
|
|
|
| 63 |
import math
|
| 64 |
|
| 65 |
# ββ Engine Version ββ
|
| 66 |
+
ENGINE_VERSION = "v3.0-38cat"
|
| 67 |
|
| 68 |
# ββ Team mappings ββ
|
| 69 |
TEAM_MAP = {
|
|
|
|
| 2233 |
"movda_win_prob", # MOVDA-derived win probability
|
| 2234 |
])
|
| 2235 |
|
| 2236 |
+
# 38. VENUE-CONDITIONAL MATCHUP FEATURES (14 features)
|
| 2237 |
+
# Home team's home-only stats vs away team's road-only stats
|
| 2238 |
+
# This is the true matchup signal: how does home team perform AT HOME
|
| 2239 |
+
# vs how does away team perform ON THE ROAD β not combined records
|
| 2240 |
+
for w in [5, 10, 20]:
|
| 2241 |
+
names.append(f"venue_wp_edge_{w}") # h_home_wp - a_road_wp
|
| 2242 |
+
names.append(f"venue_margin_edge_{w}") # h_home_margin - a_road_margin
|
| 2243 |
+
names.append(f"venue_ortg_edge_{w}") # h_home_ortg - a_road_drtg
|
| 2244 |
+
names.append(f"venue_drtg_edge_{w}") # h_home_drtg - a_road_ortg
|
| 2245 |
+
names.extend([
|
| 2246 |
+
"venue_home_boost", # h_home_wp - h_overall_wp (home court effect)
|
| 2247 |
+
"venue_road_penalty", # a_overall_wp - a_road_wp (road penalty)
|
| 2248 |
+
])
|
| 2249 |
+
|
| 2250 |
self.feature_names = names
|
| 2251 |
|
| 2252 |
def build(self, games, market_data=None, referee_data=None, player_data=None, quarter_data=None):
|
|
|
|
| 5058 |
row.append(_movda_dr / 400.0) # movda_diff
|
| 5059 |
row.append(_movda_wp) # movda_win_prob
|
| 5060 |
|
| 5061 |
+
# ββ 38. VENUE-CONDITIONAL MATCHUP FEATURES (14 features) ββ
|
| 5062 |
+
# Use true venue-specific records: home team at home vs away team on road
|
| 5063 |
+
_h_home_tr = team_home_results.get(home, [])
|
| 5064 |
+
_a_away_tr = team_away_results.get(away, [])
|
| 5065 |
+
for _w38 in [5, 10, 20]:
|
| 5066 |
+
# WP edge: home team's home record vs away team's road record
|
| 5067 |
+
_h_home_wp = self._wp(_h_home_tr, _w38) if _h_home_tr else self._wp(hr_, _w38)
|
| 5068 |
+
_a_road_wp = self._wp(_a_away_tr, _w38) if _a_away_tr else self._wp(ar_, _w38)
|
| 5069 |
+
row.append(_h_home_wp - _a_road_wp)
|
| 5070 |
+
# Margin edge: home margin at home vs away margin on road
|
| 5071 |
+
_h_home_mg = self._pd(_h_home_tr, _w38) if _h_home_tr else self._pd(hr_, _w38)
|
| 5072 |
+
_a_road_mg = self._pd(_a_away_tr, _w38) if _a_away_tr else self._pd(ar_, _w38)
|
| 5073 |
+
row.append(_h_home_mg - _a_road_mg)
|
| 5074 |
+
# ORtg edge: home team's home offense vs away team's road defense
|
| 5075 |
+
_h_home_or = self._ortg(_h_home_tr, _w38) if _h_home_tr else self._ortg(hr_, _w38)
|
| 5076 |
+
_a_road_dr = self._drtg(_a_away_tr, _w38) if _a_away_tr else self._drtg(ar_, _w38)
|
| 5077 |
+
row.append((_h_home_or - _a_road_dr) / 10.0)
|
| 5078 |
+
# DRtg edge: home team's home defense vs away team's road offense
|
| 5079 |
+
_h_home_dr = self._drtg(_h_home_tr, _w38) if _h_home_tr else self._drtg(hr_, _w38)
|
| 5080 |
+
_a_road_or = self._ortg(_a_away_tr, _w38) if _a_away_tr else self._ortg(ar_, _w38)
|
| 5081 |
+
row.append((_h_home_dr - _a_road_or) / 10.0)
|
| 5082 |
+
# Home court boost: how much better the home team is at home vs overall
|
| 5083 |
+
_h_overall_wp = self._wp(hr_, len(hr_)) if hr_ else 0.5
|
| 5084 |
+
_h_home_wp_82 = self._wp(_h_home_tr, len(_h_home_tr)) if _h_home_tr else _h_overall_wp
|
| 5085 |
+
row.append(_h_home_wp_82 - _h_overall_wp)
|
| 5086 |
+
# Road penalty: how much worse away team is on road vs overall
|
| 5087 |
+
_a_overall_wp = self._wp(ar_, len(ar_)) if ar_ else 0.5
|
| 5088 |
+
_a_road_wp_82 = self._wp(_a_away_tr, len(_a_away_tr)) if _a_away_tr else _a_overall_wp
|
| 5089 |
+
row.append(_a_overall_wp - _a_road_wp_82)
|
| 5090 |
+
|
| 5091 |
X.append(row)
|
| 5092 |
y.append(1 if hs > as_ else 0)
|
| 5093 |
|