Syntrex commited on
Commit
82bd89f
·
verified ·
1 Parent(s): 994d0b6

Create upcoming_edge_stub.py

Browse files
Files changed (1) hide show
  1. models/upcoming_edge_stub.py +128 -0
models/upcoming_edge_stub.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+
7
+
8
+ def _safe_float(value: Any, default: float = 0.0) -> float:
9
+ try:
10
+ if value is None:
11
+ return default
12
+ text = str(value).strip().lower()
13
+ if text in {"", "nan", "none"}:
14
+ return default
15
+ return float(value)
16
+ except Exception:
17
+ return default
18
+
19
+
20
+ def probability_to_american(prob: float) -> int | None:
21
+ if prob <= 0 or prob >= 1:
22
+ return None
23
+
24
+ if prob >= 0.5:
25
+ return int(round(-(prob / (1 - prob)) * 100))
26
+ return int(round(((1 - prob) / prob) * 100))
27
+
28
+
29
+ def implied_prob_from_american(odds: int) -> float:
30
+ if odds > 0:
31
+ return 100 / (odds + 100)
32
+ return abs(odds) / (abs(odds) + 100)
33
+
34
+
35
+ def _stub_probs_for_batter(
36
+ batter_name: str,
37
+ statcast_df: pd.DataFrame,
38
+ game_row: dict[str, Any],
39
+ ) -> dict[str, float]:
40
+ hit_prob = 0.24
41
+ hr_prob = 0.045
42
+ tb2p_prob = 0.17
43
+
44
+ balls = _safe_float(game_row.get("balls"), 0.0)
45
+ strikes = _safe_float(game_row.get("strikes"), 0.0)
46
+ outs = _safe_float(game_row.get("outs"), 0.0)
47
+
48
+ runner_on_2b = bool(game_row.get("runner_on_2b", False))
49
+ runner_on_3b = bool(game_row.get("runner_on_3b", False))
50
+
51
+ if batter_name and not statcast_df.empty and "player_name" in statcast_df.columns:
52
+ batter_rows = statcast_df[statcast_df["player_name"].astype(str) == batter_name].copy()
53
+
54
+ if not batter_rows.empty:
55
+ if "launch_speed" in batter_rows.columns:
56
+ avg_ev = pd.to_numeric(batter_rows["launch_speed"], errors="coerce").dropna()
57
+ if not avg_ev.empty:
58
+ hit_prob += min(0.05, max(-0.03, (avg_ev.mean() - 88.0) * 0.002))
59
+ hr_prob += min(0.03, max(-0.015, (avg_ev.mean() - 90.0) * 0.0015))
60
+ tb2p_prob += min(0.04, max(-0.02, (avg_ev.mean() - 88.0) * 0.0015))
61
+
62
+ if balls >= 2:
63
+ hit_prob += 0.015
64
+ tb2p_prob += 0.01
65
+ if strikes >= 2:
66
+ hit_prob -= 0.015
67
+ hr_prob -= 0.005
68
+
69
+ if runner_on_2b or runner_on_3b:
70
+ hit_prob += 0.01
71
+ if outs >= 2:
72
+ hit_prob -= 0.005
73
+ tb2p_prob -= 0.005
74
+
75
+ hit_prob = max(0.05, min(0.50, hit_prob))
76
+ hr_prob = max(0.005, min(0.20, hr_prob))
77
+ tb2p_prob = max(0.03, min(0.40, tb2p_prob))
78
+
79
+ return {
80
+ "hit_prob": hit_prob,
81
+ "hr_prob": hr_prob,
82
+ "tb2p_prob": tb2p_prob,
83
+ }
84
+
85
+
86
+ def _market_row(label: str, batter_name: str, probs: dict[str, float]) -> dict[str, Any]:
87
+ fair_hit = probability_to_american(probs["hit_prob"])
88
+ fair_hr = probability_to_american(probs["hr_prob"])
89
+ fair_tb2p = probability_to_american(probs["tb2p_prob"])
90
+
91
+ book_hit = +135
92
+ book_hr = +425
93
+ book_tb2p = +165
94
+
95
+ return {
96
+ "slot": label,
97
+ "batter_name": batter_name,
98
+ "fair_hit_odds": fair_hit,
99
+ "fair_hr_odds": fair_hr,
100
+ "fair_tb2p_odds": fair_tb2p,
101
+ "book_hit_odds": book_hit,
102
+ "book_hr_odds": book_hr,
103
+ "book_tb2p_odds": book_tb2p,
104
+ "hit_edge": probs["hit_prob"] - implied_prob_from_american(book_hit),
105
+ "hr_edge": probs["hr_prob"] - implied_prob_from_american(book_hr),
106
+ "tb2p_edge": probs["tb2p_prob"] - implied_prob_from_american(book_tb2p),
107
+ }
108
+
109
+
110
+ def compute_upcoming_edge_rows(
111
+ game_row: dict[str, Any],
112
+ statcast_df: pd.DataFrame,
113
+ ) -> list[dict[str, Any]]:
114
+ rows: list[dict[str, Any]] = []
115
+
116
+ slots = [
117
+ ("On Deck", str(game_row.get("on_deck_name", "") or "").strip()),
118
+ ("In Hole", str(game_row.get("in_hole_name", "") or "").strip()),
119
+ ("3 Away", str(game_row.get("three_away_name", "") or "").strip()),
120
+ ]
121
+
122
+ for label, batter_name in slots:
123
+ if not batter_name:
124
+ continue
125
+ probs = _stub_probs_for_batter(batter_name, statcast_df=statcast_df, game_row=game_row)
126
+ rows.append(_market_row(label, batter_name, probs))
127
+
128
+ return rows