Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion data/calibration_params.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,19 @@
"hits_allowed": 0.4531,
"overall": 0.2586
},
"notes": "prop_min_prob_overrides: per-prop MIN_PROB gates stricter than global 0.57. fantasy_score set to 0.99 (effectively excluded). hits_allowed excluded pending more data (Brier 0.4531)."
"notes": "prop_min_prob_overrides: per-prop MIN_PROB gates stricter than global 0.57. fantasy_score set to 0.99 (effectively excluded). hits_allowed excluded pending more data (Brier 0.4531).",
"league_constants": {
"_comment": "60/40 blend 2026/2025 \u2014 May 15 2026. Source: BBRef totals through May 14.",
"woba": 0.31,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the other league constants in this file (which use three decimal places), woba should be explicitly set to 0.310.

Suggested change
"woba": 0.31,
"woba": 0.310,

"obp": 0.317,
"slg": 0.397,
"avg": 0.241,
"iso": 0.155,
"babip": 0.292,
"k_pct": 0.224,
"bb_pct": 0.091,
"era": 4.12,
"hr_fb_pct": 0.109,
"o_swing": 0.287
}
}
28 changes: 14 additions & 14 deletions fangraphs_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,25 +242,25 @@ def _pg_save_cache(season: int, batters: dict, pitchers: dict) -> None:
"pitcher": {
"csw_pct": 0.273, # FG 2026: ~27.3% through game 44
"swstr_pct": 0.110, # FG 2025: ~11.0% (unchanged)
"k_bb_pct": 0.130, # FG 2025: ~13.0% (unchanged)
"xfip": 4.10, # FG 2026: xFIP through game 44
"siera": 4.10, # FG 2026: SIERA through game 44
"fip": 4.10, # FG 2026: FIP through game 44
"hr_fb_pct": 0.119, # FG 2025: 11.9% (confirmed VSiN Feb 2026): ~11.8% (was 0.120)
"k_bb_pct": 0.133, # blended 60/40 2026/2025: 13.3%
"xfip": 4.12, # blended 60/40 2026/2025
"siera": 4.12, # blended 60/40 2026/2025
"fip": 4.12, # blended 60/40 2026/2025
"hr_fb_pct": 0.109, # blended 60/40 2026/2025 (2026 actual ~10.5%)
"lob_pct": 0.720, # unchanged
"babip": 0.288, # FG 2025: .289 (confirmed VSiN Feb 2026): ~0.298 (was 0.300)
"babip": 0.292, # blended 60/40 2026/2025 (2026 BBRef: .288)
},
"batter": {
"wrc_plus": 100.0, # by definition
"woba": 0.308, # FG 2025 (was 0.312)
"iso": 0.156, # FG 2025: elevated power (was 0.158)
"babip": 0.288, # FG 2025: .289 (confirmed VSiN Feb 2026)
"o_swing": 0.316, # FG 2025 (was 0.318)
"woba": 0.310, # blended 60/40 2026/2025 (2026 actual .307)
"iso": 0.155, # blended 60/40 2026/2025 (2026 actual .149)
"babip": 0.292, # blended 60/40 2026/2025 (2026 BBRef: .288)
"o_swing": 0.287, # blended 60/40 2026/2025 (ABS robot ump → fewer chases)
"z_contact": 0.848, # FG 2025: ~84.8% (was 0.850)
"hr_fb_pct": 0.119, # FG 2025: 11.9% (confirmed VSiN Feb 2026)
"k_pct": 0.223, # FG 2025: 22.2% (confirmed VSiN Feb 2026)
"bb_pct": 0.087, # FG 2025: 8.4% (confirmed VSiN Feb 2026)
"slg": 0.410, # FG 2025 (was 0.411)
"hr_fb_pct": 0.109, # blended 60/40 2026/2025 (2026 actual ~10.5%)
"k_pct": 0.224, # blended 60/40 2026/2025 (2026 actual 22.2%)
"bb_pct": 0.091, # blended 60/40 2026/2025 (2026 actual 9.5% — ABS effect)
"slg": 0.397, # blended 60/40 2026/2025 (2026 BBRef actual: .389)
"xbh_per_game": 0.50, # extra base hits per game — #1 feature for TB (45% importance)
},
}
Expand Down
4 changes: 2 additions & 2 deletions lineup_chase_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
logger = logging.getLogger(__name__)

# League average baselines (2024)
_LEAGUE_O_SWING = 0.316 # FG 2025: O-Swing% (was 0.318 in 2024)
_LEAGUE_K_PCT = 0.228 # FG 2026: 22.8% K% through game 44
_LEAGUE_O_SWING = 0.287 # blended 60/40 2026/2025 (ABS → fewer chases)
_LEAGUE_K_PCT = 0.224 # blended 60/40 2026/2025
_LEAGUE_Z_CONTACT = 0.850

# Chase thresholds
Expand Down
18 changes: 10 additions & 8 deletions pa_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@
# ── 2025 MLB league-average PA outcome rates ──────────────────────────────
# Source: FanGraphs 2025 season (confirmed via VSiN Feb 2026)
LEAGUE_RATES: dict[str, float] = {
"K": 0.228, # strikeout — FG 2026: 22.8% through game 44
"BB": 0.083, # walk — FG 2026: 8.3% through game 44
"HBP": 0.011, # hit by pitch
"HR": 0.033, # home run
"3B": 0.004, # triple
"2B": 0.047, # double
"1B": 0.143, # single
"OUT": 0.452, # field out (K+0.005 BB-0.004 net rounded)
# Blended 60/40 2026/2025 — May 15 2026 update
# 2026 actuals: BBRef totals through May 14 (49,542 PA, 30 teams)
"K": 0.224, # strikeout — blended (2026: 22.2%, 2025: 22.6%)
"BB": 0.091, # walk — blended (2026: 9.5% ABS effect, 2025: 8.5%)
"HBP": 0.011, # hit by pitch — unchanged
"HR": 0.030, # home run — blended (2026: 2.8%, 2025: 3.3%)
"3B": 0.004, # triple — unchanged
"2B": 0.044, # double — blended (2026: 4.1%, 2025: 4.7%)
"1B": 0.141, # single — blended (2026: 14.0%, 2025: 14.3%)
"OUT": 0.455, # field out — residual to sum=1.000
}
assert abs(sum(LEAGUE_RATES.values()) - 1.0) < 0.01, "LEAGUE_RATES must sum to 1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


Usage of assert statement in application logic is discouraged. assert is removed with compiling to optimized byte code. Consider raising an exception instead. Ideally, assert statement should be used only in tests.


Expand Down
22 changes: 11 additions & 11 deletions prop_enrichment_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,15 @@ def _get_fg_pitcher(name: str) -> dict:
from fangraphs_layer import get_pitcher # noqa: PLC0415
stats = get_pitcher(name) or {}
return {
"k_rate": stats.get("k_pct", stats.get("k_rate", 0.223)),
"bb_rate": stats.get("bb_pct", stats.get("bb_rate", 0.087)),
"k_rate": stats.get("k_pct", stats.get("k_rate", 0.224)),
"bb_rate": stats.get("bb_pct", stats.get("bb_rate", 0.091)),
"era": stats.get("xfip", stats.get("fip", 4.06)),
"whip": stats.get("whip", 1.28),
"csw_pct": stats.get("csw_pct", 0.275),
"swstr_pct": stats.get("swstr_pct", 0.110),
"xfip": stats.get("xfip", 4.06),
"siera": stats.get("siera", 4.06),
"k_bb_pct": stats.get("k_bb_pct", 0.139),
"k_bb_pct": stats.get("k_bb_pct", 0.133),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While k_bb_pct has been updated to 0.133, several other league fallbacks in this block (and throughout the file) remain inconsistent with the new blended constants. Specifically, era, xfip, and siera (lines 212, 216, 217) are still using 4.06 instead of the new 4.12 standard defined in fangraphs_layer.py and streak_agent.py. Additionally, babip (line 241) and z_contact (line 243) fallbacks in the batter block are also inconsistent with the updated defaults.

}
except Exception:
return {}
Expand All @@ -236,14 +236,14 @@ def _get_fg_batter(name: str) -> dict:
stats = get_batter(name) or {}
return {
"wrc_plus": stats.get("wrc_plus", 100.0),
"woba": stats.get("woba", 0.308),
"iso": stats.get("iso", 0.156),
"woba": stats.get("woba", 0.310),
"iso": stats.get("iso", 0.155),
"babip": stats.get("babip", 0.288),
"o_swing": stats.get("o_swing", 0.316),
"o_swing": stats.get("o_swing", 0.287),
"z_contact": stats.get("z_contact", 0.850),
"hr_fb_pct": stats.get("hr_fb_pct", 0.105),
"k_pct": stats.get("k_pct", 0.228),
"bb_pct": stats.get("bb_pct", 0.083),
"hr_fb_pct": stats.get("hr_fb_pct", 0.109),
"k_pct": stats.get("k_pct", 0.224),
"bb_pct": stats.get("bb_pct", 0.091),
}
except Exception:
return {}
Expand Down Expand Up @@ -550,14 +550,14 @@ def _get_form_adj(player_name: str, prop_type: str, hub: dict) -> float:

def _get_chase_score(opposing_team: str, hub: dict) -> dict:
default = {"k_prob_adjustment": 0.0, "lineup_difficulty": "NEUTRAL",
"avg_chase_rate": 0.316, "_opp_o_swing_avg": 0.316}
"avg_chase_rate": 0.287, "_opp_o_swing_avg": 0.287}
if not opposing_team:
return default
try:
from lineup_chase_layer import get_lineup_chase_score # noqa: PLC0415
lineups = hub.get("context", {}).get("lineups", [])
result = get_lineup_chase_score(opposing_team, lineups)
result["_opp_o_swing_avg"] = result.get("avg_chase_rate", 0.316)
result["_opp_o_swing_avg"] = result.get("avg_chase_rate", 0.287)
return result
except Exception as _lcs_err:
logger.debug("[Enrich] lineup_chase_score failed: %s", _lcs_err)
Expand Down
8 changes: 4 additions & 4 deletions streak_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ def evaluate_props_for_streaks(raw_props: list[dict]) -> list[StreakCandidate]:
# K-BB% quality adjustment
kbb_delta = (kbb - _LG_KP) * 0.5
# FIP quality: elite <3.50 adds small boost, high >5.00 subtracts
fip_delta = max(-0.04, min(0.04, (4.06 - fip) * 0.015))
fip_delta = max(-0.04, min(0.04, (4.12 - fip) * 0.015))
adj = csw_delta + kbb_delta + fip_delta
adj = max(-0.12, min(0.12, adj)) # cap at ±12pp

Expand All @@ -587,7 +587,7 @@ def evaluate_props_for_streaks(raw_props: list[dict]) -> list[StreakCandidate]:
era = stats.get("era", 4.06)
# Below-average ERA/FIP → more likely to go deeper (more outs)
# or give up fewer earned runs
quality = (4.06 - ((fip + era) / 2)) * 0.02
quality = (4.12 - ((fip + era) / 2)) * 0.02
adj = max(-0.08, min(0.08, quality))
if side == "Under":
adj = -adj # inverse for Under on ER
Expand All @@ -597,11 +597,11 @@ def evaluate_props_for_streaks(raw_props: list[dict]) -> list[StreakCandidate]:
stats = _fg_bat(cand.player_name)
if stats:
wrc = stats.get("wrc_plus", _LG_WRC)
woba = stats.get("woba", 0.308)
woba = stats.get("woba", 0.310)
xbh = stats.get("xbh_per_game", 0.50)
# wRC+ above 100 → above-average hitter
wrc_delta = (wrc - 100.0) * 0.001 # 120 wRC+ → +2pp
woba_delta = (woba - 0.308) * 0.15
woba_delta = (woba - 0.310) * 0.15
adj = wrc_delta + woba_delta
if pt in ("total_bases", "hits_runs_rbis"):
# extra-base hit rate adds more weight for TB props
Expand Down
8 changes: 4 additions & 4 deletions tasklets.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ def _sigmoid(x: float) -> float:
"S": 0.027, # switch: use favourable side
}
_STAT_DEFAULTS_PLATOON = {
"avg": 0.245, "obp": 0.315, "slg": 0.390, "ops": 0.705, "woba": 0.320
"avg": 0.241, "obp": 0.317, "slg": 0.397, "ops": 0.714, "woba": 0.310
}


Expand Down Expand Up @@ -3146,15 +3146,15 @@ def _clamp(v, lo=0.0, hi=1.0):

# slot 2: SLG for TB/power props (16% feature importance)
if _is_tb_prop:
_slg = float(prop.get("slg", 0.410) or 0.410)
_slg = float(prop.get("slg", 0.397) or 0.397)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The update of the slg fallback to 0.397 creates an inconsistency with the normalization logic in the following line (3150). The calculation (_slg - 0.250) / 0.400 and the associated comment 0.410=avg(0.40) are based on the previous league average. To maintain the same feature scaling for an "average" player, the normalization constants should be adjusted. Similarly, the babip fallback in line 3152 remains at 0.288 instead of the new 0.292 standard.

era = _clamp((_slg - 0.250) / 0.400) # 0.250=0, 0.410=avg(0.40), 0.650=elite(1.0)
else:
era = _clamp((float(prop.get("babip", 0.288) or 0.288) - 0.200) / 0.200)

# slot 3: batter bb_pct (plate discipline)
whip = _clamp(float(prop.get("bb_pct", 0.087) or 0.087) / 0.20)
whip = _clamp(float(prop.get("bb_pct", 0.091) or 0.091) / 0.20)
# slot 4: batter K% (inverse contact — higher K = worse contact)
shadow_whiff = _clamp(float(prop.get("k_pct", 0.223) or 0.223) / 0.35)
shadow_whiff = _clamp(float(prop.get("k_pct", 0.224) or 0.224) / 0.35)

# Zone integrity multiplier (pitcher K-props only, 1.0 for batters)
# Blended with pitcher type cluster: power=+0.05, command=-0.05, neutral=0
Expand Down