File size: 3,021 Bytes
d19bce3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21151ce
 
 
d19bce3
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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,
    }