diff --git a/prop_enrichment_layer.py b/prop_enrichment_layer.py index d3f4af2..9a385f0 100644 --- a/prop_enrichment_layer.py +++ b/prop_enrichment_layer.py @@ -1730,6 +1730,62 @@ def _dampen(base_prob_pct, adjustments, **kw): # noqa: E731 except Exception as _def_err: logger.debug("[Enrichment] Defense OAA skipped for %s: %s", player, _def_err) + # ── Batted-ball profile signal (hits + total_bases) ────────────────── + # Uses statcast_static_layer.get_batter_batted_ball() which reads + # batted-ball.csv (bbe, gb_rate, air_rate, fb_rate, ld_rate, pull_rate…) + # + # hits: LD rate drives BABIP; GB heavy = infield-hit bonus + # total_bases: FB rate + pull rate = XBH/HR upside; GB heavy = drag + # + # Max effect: ±4pp per leg; flows through adjustment dampener. + if prop_type in ("hits", "total_bases") and is_batter_prop: + _b_id_bb = prop.get("player_id") or prop.get("mlbam_id") + if _b_id_bb: + try: + from statcast_static_layer import get_batter_batted_ball as _gbb # noqa: PLC0415 + _bb_prof = _gbb(int(_b_id_bb)) + if _bb_prof: + _gb_r = float(_bb_prof.get("gb_rate") or 0) + _fb_r = float(_bb_prof.get("fb_rate") or 0) + _ld_r = float(_bb_prof.get("ld_rate") or 0) + _pull_r = float(_bb_prof.get("pull_rate") or 0) + _bb_adj = 0.0 + + if prop_type == "hits": + # LD rate is strongest BABIP driver; MLB avg ~22% + # ±3pp per 6pp deviation from average + if _ld_r > 0: + _bb_adj += (_ld_r - 0.22) / 0.06 * 0.030 + # GB-heavy batters (>48%) get slight infield-hit bonus + if _gb_r > 0.48: + _bb_adj += (_gb_r - 0.48) / 0.10 * 0.010 + + elif prop_type == "total_bases": + # High FB rate = more fly balls = more XBH/HRs + # MLB avg air_rate ~0.38 (includes LD + FB) + if _fb_r > 0: + _bb_adj += (_fb_r - 0.22) / 0.08 * 0.030 # FB avg ~22% + # High pull rate = pull-side power = more XBH + if _pull_r > 0: + _bb_adj += (_pull_r - 0.38) / 0.10 * 0.020 + # GB-heavy batters suppress total bases + if _gb_r > 0: + _bb_adj -= (_gb_r - 0.40) / 0.10 * 0.015 + + _bb_adj = round(max(-0.040, min(0.040, _bb_adj)), 4) + if abs(_bb_adj) >= 0.005: + prop["_bb_profile_adj"] = _bb_adj + logger.debug( + "[Enrichment] %s %s bb_profile_adj=%.3f " + "(gb=%.2f fb=%.2f ld=%.2f pull=%.2f)", + player, prop_type, _bb_adj, + _gb_r, _fb_r, _ld_r, _pull_r, + ) + except Exception as _bb_err: + logger.debug( + "[Enrichment] batted_ball skipped for %s: %s", player, _bb_err + ) + # ── FIX: Bridge enrichment keys → simulation engine underscore-prefixed keys ── # prop_enrichment_layer sets k_rate/k_pct, bb_rate/bb_pct, woba, wrc_plus (no prefix). # regardless of who the player is. Chase Burns and a AAA call-up were identical. @@ -1965,6 +2021,7 @@ def _dampen(base_prob_pct, adjustments, **kw): # noqa: E731 ("_arm_angle_adj", "arm_angle_deception"), ("_swing_path_k_adj", "swing_path_k"), ("_chase_discipline_k_adj", "chase_discipline_k"), + ("_bb_profile_adj", "bb_profile"), ]: _v = float(prop.get(_adj_key, 0.0) or 0.0) if _v != 0.0: @@ -2033,6 +2090,7 @@ def _dampen(base_prob_pct, adjustments, **kw): # noqa: E731 "arm_angle": round(float(prop.get("_arm_angle_adj", 0.0) or 0.0), 4), "swing_path_k": round(float(prop.get("_swing_path_k_adj", 0.0) or 0.0), 4), "chase_disc_k": round(float(prop.get("_chase_discipline_k_adj",0.0) or 0.0), 4), + "bb_profile": round(float(prop.get("_bb_profile_adj", 0.0) or 0.0), 4), } enriched_count += 1