from __future__ import annotations from typing import Any def build_live_context(game_row: dict[str, Any], weather_row: dict[str, Any] | None = None) -> dict[str, Any]: weather_row = weather_row or {} def _safe_int(value: Any, default: int = 0) -> int: try: if value is None: return default text = str(value).strip().lower() if text in {"", "nan", "none"}: return default return int(float(value)) except Exception: return default def _safe_float(value: Any, default: float | None = None) -> float | None: try: if value is None: return default text = str(value).strip().lower() if text in {"", "nan", "none"}: return default return float(value) except Exception: return default away_score = _safe_int(game_row.get("away_score"), 0) home_score = _safe_int(game_row.get("home_score"), 0) return { "inning": _safe_int(str(game_row.get("status", "")).split()[-1] if game_row.get("status") else 0, 0), "status": str(game_row.get("status", "") or ""), "outs": _safe_int(game_row.get("outs"), 0), "balls": _safe_int(game_row.get("balls"), 0), "strikes": _safe_int(game_row.get("strikes"), 0), "runner_on_1b": bool(game_row.get("runner_on_1b", False)), "runner_on_2b": bool(game_row.get("runner_on_2b", False)), "runner_on_3b": bool(game_row.get("runner_on_3b", False)), "score_diff_away": away_score - home_score, "temperature_f": _safe_float(weather_row.get("temperature_f")), "wind_speed_mph": _safe_float(weather_row.get("wind_speed_mph")), } def compute_context_adjustment(context: dict[str, Any]) -> dict[str, Any]: hit_adj = 0.0 hr_adj = 0.0 tb2p_adj = 0.0 reason_tags: list[str] = [] balls = int(context.get("balls", 0)) strikes = int(context.get("strikes", 0)) outs = int(context.get("outs", 0)) runner_on_2b = bool(context.get("runner_on_2b", False)) runner_on_3b = bool(context.get("runner_on_3b", False)) if balls >= 2: hit_adj += 0.012 tb2p_adj += 0.010 reason_tags.append("Hitter-friendly count") if strikes >= 2: hit_adj -= 0.014 hr_adj -= 0.006 tb2p_adj -= 0.010 reason_tags.append("Two-strike penalty") if runner_on_2b or runner_on_3b: hit_adj += 0.008 tb2p_adj += 0.008 reason_tags.append("Run-scoring pressure") if outs >= 2: hit_adj -= 0.004 tb2p_adj -= 0.004 # Temperature and wind adjustments are intentionally absent here. # environment_model.py owns all weather signals via continuous formulas. # Adding them here would double-count the same physical effect. return { "hit_adj": hit_adj, "hr_adj": hr_adj, "tb2p_adj": tb2p_adj, "reason_tags": reason_tags, }