Syntrex commited on
Commit
2a91e3a
·
verified ·
1 Parent(s): 7a40912

Update config/settings.py

Browse files
Files changed (1) hide show
  1. config/settings.py +372 -57
config/settings.py CHANGED
@@ -1,59 +1,374 @@
1
  from __future__ import annotations
2
 
3
- import os
4
-
5
- APP_TITLE = "WBC Analytics Assistant"
6
- REFRESH_TTL_SECONDS = 30
7
-
8
- DUCKDB_PATH = "data/wbc.duckdb"
9
-
10
- DEFAULT_EDGE_THRESHOLD = 0.05
11
- DEFAULT_CONFIDENCE_THRESHOLD = 0.70
12
-
13
- STATCAST_SEARCH_URL = "https://baseballsavant.mlb.com/statcast_search/csv"
14
- MLB_SCHEDULE_URL = "https://statsapi.mlb.com/api/v1/schedule"
15
- MLB_TEAMS_URL = "https://statsapi.mlb.com/api/v1/teams"
16
- MLB_GAME_FEED_URL_TEMPLATE = "https://statsapi.mlb.com/api/v1.1/game/{game_pk}/feed/live"
17
-
18
- OPENWEATHER_URL = "https://api.openweathermap.org/data/2.5/weather"
19
-
20
- ODDS_SPORT_KEY = "baseball_mlb"
21
- ODDS_BASE_URL = "https://api.the-odds-api.com/v4"
22
- ODDS_REGIONS = "us"
23
- ODDS_FEATURED_MARKETS = "h2h,spreads,totals"
24
- ODDS_FORMAT = "american"
25
-
26
- ODDS_API_KEY = os.getenv("ODDS_API_KEY", "")
27
- OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
28
-
29
- SUPPORTED_BOOKS = [
30
- "DraftKings",
31
- "FanDuel",
32
- "BetMGM",
33
- "Caesars",
34
- "bet365",
35
- "Pinnacle",
36
- ]
37
-
38
- WBC_TEAMS = [
39
- "Japan",
40
- "United States",
41
- "Dominican Republic",
42
- "Mexico",
43
- "Puerto Rico",
44
- "Venezuela",
45
- "Korea",
46
- "Chinese Taipei",
47
- "Netherlands",
48
- "Cuba",
49
- "Australia",
50
- "Italy",
51
- "Panama",
52
- "Colombia",
53
- "Czech Republic",
54
- "Israel",
55
- "Nicaragua",
56
- "Great Britain",
57
- "Canada",
58
- "China",
59
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ from datetime import date, timedelta
4
+
5
+ import pandas as pd
6
+ import plotly.graph_objects as go
7
+ import streamlit as st
8
+
9
+ from analytics.bankroll import bankroll_curve, grade_profit, summary_metrics
10
+ from analytics.edge import (
11
+ american_to_implied_prob,
12
+ calculate_edge,
13
+ kelly_fraction,
14
+ remove_vig_two_way,
15
+ )
16
+ from config.settings import (
17
+ APP_TITLE,
18
+ DEFAULT_EDGE_THRESHOLD,
19
+ ODDS_API_KEY,
20
+ OPENWEATHER_API_KEY,
21
+ REFRESH_TTL_SECONDS,
22
+ )
23
+ from data.odds import fetch_featured_odds
24
+ from data.rosters import fetch_mlb_teams
25
+ from data.schedule import fetch_schedule_for_date
26
+ from data.statcast import fetch_statcast_range, normalize_statcast
27
+ from data.weather import fetch_weather_for_venue
28
+ from database.db import (
29
+ get_connection,
30
+ insert_bet,
31
+ next_bet_id,
32
+ read_table,
33
+ update_bet_result,
34
+ upsert_dataframe,
35
+ )
36
+ from utils.helpers import utc_now_iso
37
+ from visualization.batter import create_exit_velocity_chart, create_launch_angle_chart
38
+ from visualization.betting import create_bankroll_chart, create_edge_chart
39
+ from visualization.pitcher import create_pitch_movement_chart
40
+
41
+
42
+ st.set_page_config(
43
+ page_title=APP_TITLE,
44
+ layout="wide",
45
+ page_icon="",
46
+ )
47
+
48
+ st.markdown(
49
+ """
50
+ <style>
51
+ .stApp {
52
+ background: linear-gradient(180deg, #0b1020 0%, #0f172a 100%);
53
+ }
54
+ .block-container {
55
+ padding-top: 1.25rem;
56
+ padding-bottom: 2rem;
57
+ max-width: 1500px;
58
+ }
59
+ div[data-testid="stMetric"] {
60
+ background: rgba(255,255,255,0.04);
61
+ border: 1px solid rgba(255,255,255,0.08);
62
+ border-radius: 16px;
63
+ padding: 12px;
64
+ }
65
+ </style>
66
+ """,
67
+ unsafe_allow_html=True,
68
+ )
69
+
70
+ conn = get_connection()
71
+
72
+
73
+ @st.cache_data(ttl=REFRESH_TTL_SECONDS)
74
+ def load_schedule_for_today() -> pd.DataFrame:
75
+ df = fetch_schedule_for_date(date.today().isoformat())
76
+ if not df.empty:
77
+ upsert_dataframe(conn, "cached_schedule", df, replace=True)
78
+ return df
79
+
80
+
81
+ @st.cache_data(ttl=REFRESH_TTL_SECONDS)
82
+ def load_odds() -> pd.DataFrame:
83
+ df = fetch_featured_odds()
84
+ if not df.empty:
85
+ upsert_dataframe(conn, "cached_odds", df, replace=True)
86
+ return df
87
+
88
+
89
+ @st.cache_data(ttl=REFRESH_TTL_SECONDS)
90
+ def load_statcast_recent() -> pd.DataFrame:
91
+ end_date = date.today()
92
+ start_date = end_date - timedelta(days=7)
93
+ raw = fetch_statcast_range(start_date.isoformat(), end_date.isoformat())
94
+ return normalize_statcast(raw)
95
+
96
+
97
+ @st.cache_data(ttl=3600)
98
+ def load_teams() -> pd.DataFrame:
99
+ return fetch_mlb_teams()
100
+
101
+
102
+ def load_weather(venue_name: str) -> pd.DataFrame:
103
+ df = fetch_weather_for_venue(venue_name)
104
+ if not df.empty:
105
+ upsert_dataframe(conn, "cached_weather", df, replace=False)
106
+ return df
107
+
108
+
109
+ def render_header() -> None:
110
+ st.title("⚾ WBC Analytics Assistant")
111
+ st.caption(
112
+ "Real-data app using MLB schedule/statcast-style pulls, The Odds API, weather overlays, "
113
+ "DuckDB storage, and a modern Streamlit UI."
114
+ )
115
+ secret_status = []
116
+ secret_status.append("ODDS_API_KEY ✓" if ODDS_API_KEY else "ODDS_API_KEY missing")
117
+ secret_status.append(
118
+ "OPENWEATHER_API_KEY ✓" if OPENWEATHER_API_KEY else "OPENWEATHER_API_KEY missing"
119
+ )
120
+ st.caption(" | ".join(secret_status))
121
+
122
+
123
+ def render_dashboard() -> None:
124
+ st.subheader("Live Dashboard")
125
+
126
+ schedule_df = load_schedule_for_today()
127
+ if schedule_df.empty:
128
+ st.warning("No schedule data returned for today.")
129
+ return
130
+
131
+ st.dataframe(schedule_df, use_container_width=True, hide_index=True)
132
+
133
+ venue_name = schedule_df["venue"].dropna().astype(str).iloc[0] if not schedule_df.empty else ""
134
+ if venue_name:
135
+ weather_df = load_weather(venue_name)
136
+ if not weather_df.empty:
137
+ row = weather_df.iloc[0]
138
+ c1, c2, c3, c4 = st.columns(4)
139
+ c1.metric("Venue", row["location_name"])
140
+ c2.metric("Temp °F", f"{row['temperature_f']:.1f}")
141
+ c3.metric("Wind mph", f"{row['wind_speed_mph']:.1f}" if pd.notna(row["wind_speed_mph"]) else "N/A")
142
+ c4.metric("Conditions", row["description"])
143
+
144
+ statcast_df = load_statcast_recent()
145
+ if not statcast_df.empty:
146
+ col1, col2 = st.columns(2)
147
+ with col1:
148
+ st.plotly_chart(create_pitch_movement_chart(statcast_df), use_container_width=True)
149
+ with col2:
150
+ st.plotly_chart(create_exit_velocity_chart(statcast_df), use_container_width=True)
151
+
152
+
153
+ def render_players() -> None:
154
+ st.subheader("Player Analytics")
155
+
156
+ teams_df = load_teams()
157
+ if not teams_df.empty:
158
+ st.dataframe(teams_df, use_container_width=True, hide_index=True)
159
+
160
+ statcast_df = load_statcast_recent()
161
+ if statcast_df.empty:
162
+ st.info("No recent statcast data available.")
163
+ return
164
+
165
+ col1, col2 = st.columns(2)
166
+ with col1:
167
+ st.plotly_chart(create_exit_velocity_chart(statcast_df), use_container_width=True)
168
+ with col2:
169
+ st.plotly_chart(create_launch_angle_chart(statcast_df), use_container_width=True)
170
+
171
+
172
+ def compute_market_edges(odds_df: pd.DataFrame) -> pd.DataFrame:
173
+ if odds_df.empty:
174
+ return odds_df
175
+
176
+ out = odds_df.copy()
177
+ out["implied_prob"] = out["price"].apply(american_to_implied_prob)
178
+
179
+ grouped_rows: list[dict] = []
180
+ for (event_id, sportsbook, market_key), group in out.groupby(["event_id", "sportsbook", "market_key"]):
181
+ temp = group.copy().reset_index(drop=True)
182
+
183
+ if len(temp) == 2:
184
+ p1, p2 = temp.loc[0, "implied_prob"], temp.loc[1, "implied_prob"]
185
+ nv1, nv2 = remove_vig_two_way(p1, p2)
186
+ temp.loc[0, "no_vig_prob"] = nv1
187
+ temp.loc[1, "no_vig_prob"] = nv2
188
+ else:
189
+ total = temp["implied_prob"].sum()
190
+ temp["no_vig_prob"] = temp["implied_prob"] / total if total else temp["implied_prob"]
191
+
192
+ for _, row in temp.iterrows():
193
+ model_prob = float(row["no_vig_prob"]) + 0.03
194
+ edge = calculate_edge(model_prob, float(row["no_vig_prob"]))
195
+ grouped_rows.append(
196
+ {
197
+ **row.to_dict(),
198
+ "model_prob": model_prob,
199
+ "edge": edge,
200
+ "kelly": kelly_fraction(model_prob, int(row["price"])) if pd.notna(row["price"]) else 0.0,
201
+ }
202
+ )
203
+
204
+ return pd.DataFrame(grouped_rows)
205
+
206
+
207
+ def render_betting() -> None:
208
+ st.subheader("Betting Intelligence")
209
+
210
+ odds_df = load_odds()
211
+ if odds_df.empty:
212
+ st.warning("No odds returned. Check ODDS_API_KEY or free-tier usage limits.")
213
+ return
214
+
215
+ edges_df = compute_market_edges(odds_df)
216
+ if edges_df.empty:
217
+ st.info("No edge rows computed.")
218
+ return
219
+
220
+ top_edges = edges_df.sort_values("edge", ascending=False).head(30)
221
+
222
+ c1, c2, c3 = st.columns(3)
223
+ c1.metric("Markets loaded", len(edges_df))
224
+ c2.metric("Top edge", f"{top_edges['edge'].max():.2%}")
225
+ c3.metric("Threshold", f"{DEFAULT_EDGE_THRESHOLD:.0%}")
226
+
227
+ st.plotly_chart(create_edge_chart(top_edges), use_container_width=True)
228
+ st.dataframe(
229
+ top_edges[
230
+ [
231
+ "sportsbook",
232
+ "home_team",
233
+ "away_team",
234
+ "market_key",
235
+ "outcome_name",
236
+ "price",
237
+ "no_vig_prob",
238
+ "model_prob",
239
+ "edge",
240
+ "kelly",
241
+ ]
242
+ ],
243
+ use_container_width=True,
244
+ hide_index=True,
245
+ )
246
+
247
+
248
+ def render_bet_tracker() -> None:
249
+ st.subheader("Bet Tracker")
250
+
251
+ with st.form("bet_form", clear_on_submit=True):
252
+ c1, c2, c3 = st.columns(3)
253
+ sportsbook = c1.text_input("Sportsbook", value="DraftKings")
254
+ market = c2.text_input("Market", value="h2h")
255
+ selection = c3.text_input("Selection", value="Example Team")
256
+
257
+ c4, c5, c6 = st.columns(3)
258
+ odds = c4.number_input("Odds", min_value=-1000, max_value=1000, value=120, step=1)
259
+ stake = c5.number_input("Stake", min_value=0.0, value=10.0, step=1.0)
260
+ game_id = c6.text_input("Game ID", value="")
261
+
262
+ notes = st.text_input("Notes", value="")
263
+ submitted = st.form_submit_button("Log Bet")
264
+
265
+ if submitted:
266
+ bet_id = next_bet_id(conn)
267
+ insert_bet(
268
+ conn=conn,
269
+ bet_id=bet_id,
270
+ created_at=utc_now_iso(),
271
+ sportsbook=sportsbook,
272
+ market=market,
273
+ selection=selection,
274
+ odds=int(odds),
275
+ stake=float(stake),
276
+ result="open",
277
+ profit=0.0,
278
+ game_id=game_id,
279
+ notes=notes,
280
+ )
281
+ st.success(f"Logged bet #{bet_id}")
282
+
283
+ bets_df = read_table(conn, "bets")
284
+ if bets_df.empty:
285
+ st.info("No bets logged yet.")
286
+ return
287
+
288
+ st.dataframe(bets_df, use_container_width=True, hide_index=True)
289
+
290
+ with st.expander("Grade a bet"):
291
+ bet_id_to_grade = st.number_input("Bet ID", min_value=1, step=1, value=1)
292
+ result = st.selectbox("Result", options=["win", "loss"])
293
+ if st.button("Apply Grade"):
294
+ row = bets_df[bets_df["bet_id"] == bet_id_to_grade]
295
+ if row.empty:
296
+ st.error("Bet ID not found.")
297
+ else:
298
+ stake = float(row.iloc[0]["stake"])
299
+ odds = int(row.iloc[0]["odds"])
300
+ profit = grade_profit(stake, odds, result)
301
+ update_bet_result(conn, int(bet_id_to_grade), result, profit)
302
+ st.success(f"Updated bet #{bet_id_to_grade} to {result}")
303
+
304
+ bets_df = read_table(conn, "bets")
305
+ metrics = summary_metrics(bets_df)
306
+ c1, c2, c3, c4 = st.columns(4)
307
+ c1.metric("Graded Bets", metrics["bets"])
308
+ c2.metric("Profit", f"${metrics['profit']:.2f}")
309
+ c3.metric("ROI", f"{metrics['roi']:.2%}")
310
+ c4.metric("Win Rate", f"{metrics['win_rate']:.2%}")
311
+
312
+ curve_df = bankroll_curve(bets_df)
313
+ st.plotly_chart(create_bankroll_chart(curve_df), use_container_width=True)
314
+
315
+
316
+ def render_algorithm_breakdown() -> None:
317
+ st.subheader("Algorithm Breakdown")
318
+ st.markdown(
319
+ """
320
+ ### Data inputs
321
+ - MLB schedule feed
322
+ - Baseball Savant statcast search CSV
323
+ - The Odds API featured odds
324
+ - OpenWeather venue conditions
325
+
326
+ ### Market math
327
+ 1. Convert American odds to implied probability
328
+ 2. Remove vig for 2-way markets
329
+ 3. Compare model probability to no-vig probability
330
+ 4. Report edge and Kelly fraction
331
+
332
+ ### Current demo model
333
+ The current app uses a simple research baseline:
334
+ - no-vig market probability + fixed model uplift
335
+ - this keeps the edge pipeline real and testable
336
+ - later you can replace the uplift with your matchup model output
337
+
338
+ ### Persistence
339
+ - DuckDB stores bets and cached snapshots
340
+ - all storage remains local to the Space container
341
+ """
342
+ )
343
+
344
+
345
+ def main() -> None:
346
+ render_header()
347
+
348
+ page = st.sidebar.radio(
349
+ "Navigation",
350
+ options=[
351
+ "Dashboard",
352
+ "Players",
353
+ "Betting",
354
+ "Bet Tracker",
355
+ "Algorithm Breakdown",
356
+ ],
357
+ )
358
+
359
+ st.sidebar.caption(f"Refresh TTL: {REFRESH_TTL_SECONDS}s")
360
+
361
+ if page == "Dashboard":
362
+ render_dashboard()
363
+ elif page == "Players":
364
+ render_players()
365
+ elif page == "Betting":
366
+ render_betting()
367
+ elif page == "Bet Tracker":
368
+ render_bet_tracker()
369
+ else:
370
+ render_algorithm_breakdown()
371
+
372
+
373
+ if __name__ == "__main__":
374
+ main()