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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +321 -498
app.py CHANGED
@@ -1,551 +1,374 @@
1
- import streamlit as st
 
 
 
2
  import pandas as pd
3
- import numpy as np
4
- import plotly.express as px
5
  import plotly.graph_objects as go
6
- import torch
7
- import torch.nn as nn
8
- import requests
9
- import time
10
- from datetime import datetime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # -------------------------------------------------
13
- # APP CONFIG
14
- # -------------------------------------------------
15
 
16
  st.set_page_config(
17
- page_title="WBC Analytics Assistant",
18
  layout="wide",
19
- page_icon="⚾"
20
  )
21
 
22
- # -------------------------------------------------
23
- # GLOBAL SETTINGS
24
- # -------------------------------------------------
25
-
26
- REFRESH_RATE = 30
27
-
28
- SPORTSBOOKS = [
29
- "DraftKings",
30
- "FanDuel",
31
- "BetMGM",
32
- "Caesars",
33
- "Bet365",
34
- "Pinnacle"
35
- ]
36
-
37
- # -------------------------------------------------
38
- # DARK UI STYLE
39
- # -------------------------------------------------
40
-
41
- st.markdown("""
42
- <style>
43
-
44
- body {
45
- background-color: #0e1117;
46
- }
47
-
48
- .stApp {
49
- background-color: #0e1117;
50
- color: white;
51
- }
52
-
53
- h1,h2,h3 {
54
- color:white;
55
- }
56
-
57
- .card{
58
- background-color:#161b22;
59
- padding:20px;
60
- border-radius:10px;
61
- box-shadow:0px 0px 10px rgba(0,0,0,0.6);
62
- }
63
-
64
- </style>
65
- """, unsafe_allow_html=True)
66
-
67
- # -------------------------------------------------
68
- # DATA INGESTION
69
- # -------------------------------------------------
70
-
71
- def fetch_statcast():
72
-
73
- df = pd.DataFrame({
74
- "velocity": np.random.normal(95,3,500),
75
- "spin_rate": np.random.normal(2300,200,500),
76
- "vertical_break": np.random.normal(15,4,500),
77
- "horizontal_break": np.random.normal(-8,4,500),
78
- "exit_velocity": np.random.normal(92,5,500),
79
- "launch_angle": np.random.normal(15,10,500),
80
- "bat_speed": np.random.normal(72,4,500)
81
- })
82
-
83
- return df
84
-
85
-
86
- def fetch_odds():
87
-
88
- odds = np.random.randint(-150,200,len(SPORTSBOOKS))
89
-
90
- prob = []
91
-
92
- for o in odds:
93
-
94
- if o > 0:
95
- prob.append(100/(o+100))
96
- else:
97
- prob.append(abs(o)/(abs(o)+100))
98
-
99
- return pd.DataFrame({
100
- "sportsbook":SPORTSBOOKS,
101
- "odds":odds,
102
- "prob":prob
103
- })
104
-
105
- # -------------------------------------------------
106
- # EDGE + VIG REMOVAL
107
- # -------------------------------------------------
108
-
109
- def remove_vig(probs):
110
-
111
- total = sum(probs)
112
-
113
- return [p/total for p in probs]
114
-
115
-
116
- def calculate_edge(model_prob, market_prob):
117
-
118
- return model_prob - market_prob
119
-
120
-
121
- def kelly(edge, odds):
122
-
123
- if odds > 0:
124
- b = odds/100
125
- else:
126
- b = 100/abs(odds)
127
-
128
- return edge / b
129
-
130
-
131
- # -------------------------------------------------
132
- # PITCH AI MODEL
133
- # -------------------------------------------------
134
-
135
- class PitchModel(nn.Module):
136
-
137
- def __init__(self):
138
-
139
- super().__init__()
140
-
141
- self.net = nn.Sequential(
142
- nn.Linear(4,32),
143
- nn.ReLU(),
144
- nn.Linear(32,32),
145
- nn.ReLU(),
146
- nn.Linear(32,3),
147
- nn.Sigmoid()
148
- )
149
-
150
- def forward(self,x):
151
-
152
- return self.net(x)
153
-
154
-
155
- model = PitchModel()
156
-
157
-
158
- def predict_pitch(velocity, spin, vbreak, hbreak):
159
-
160
- x = torch.tensor([[velocity,spin,vbreak,hbreak]],dtype=torch.float32)
161
-
162
- out = model(x).detach().numpy()[0]
163
-
164
- return {
165
- "strike":out[0],
166
- "whiff":out[1],
167
- "damage":out[2]
168
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- # -------------------------------------------------
171
- # MONTE CARLO SIMULATION
172
- # -------------------------------------------------
173
-
174
- def simulate_game():
175
-
176
- home = []
177
- away = []
178
-
179
- for i in range(10000):
180
-
181
- home_runs = np.random.poisson(4.5)
182
- away_runs = np.random.poisson(4.2)
183
-
184
- home.append(home_runs)
185
- away.append(away_runs)
186
-
187
- return home, away
188
 
189
 
190
- # -------------------------------------------------
191
- # BET TRACKER
192
- # -------------------------------------------------
 
 
 
193
 
194
- if "bets" not in st.session_state:
195
- st.session_state.bets = []
196
 
197
- def log_bet(book, odds, stake, bet):
 
 
 
 
 
198
 
199
- st.session_state.bets.append({
200
- "date":datetime.now(),
201
- "sportsbook":book,
202
- "odds":odds,
203
- "stake":stake,
204
- "bet":bet,
205
- "result":"open"
206
- })
207
 
 
 
 
 
 
 
208
 
209
- def settle_bet(index,result):
210
 
211
- st.session_state.bets[index]["result"] = result
 
 
212
 
213
 
214
- # -------------------------------------------------
215
- # VISUALIZATIONS
216
- # -------------------------------------------------
 
 
217
 
218
- def pitch_movement_chart(df):
219
 
220
- fig = px.scatter(
221
- df,
222
- x="horizontal_break",
223
- y="vertical_break",
224
- title="Pitch Movement"
225
  )
226
-
227
- return fig
228
-
229
-
230
- def velocity_distribution(df):
231
-
232
- fig = px.histogram(
233
- df,
234
- x="velocity",
235
- title="Velocity Distribution"
236
  )
 
237
 
238
- return fig
239
 
 
 
240
 
241
- def exit_velocity_chart(df):
 
 
 
242
 
243
- fig = px.histogram(
244
- df,
245
- x="exit_velocity",
246
- title="Exit Velocity Distribution"
247
- )
248
-
249
- return fig
250
 
 
 
 
 
 
 
 
 
 
 
251
 
252
- def launch_angle_chart(df):
 
 
 
 
 
 
253
 
254
- fig = px.histogram(
255
- df,
256
- x="launch_angle",
257
- title="Launch Angle Distribution"
258
- )
259
 
260
- return fig
 
261
 
 
 
 
262
 
263
- def spray_chart():
 
 
 
264
 
265
- x = np.random.normal(0,120,200)
266
- y = np.random.normal(150,40,200)
 
 
 
267
 
268
- fig = px.scatter(x=x,y=y,title="Spray Chart")
269
 
270
- return fig
 
 
271
 
 
 
272
 
273
- def edge_chart(df):
 
 
274
 
275
- fig = px.bar(
276
- df,
277
- x="sportsbook",
278
- y="edge",
279
- title="Edge by Sportsbook"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  )
281
 
282
- return fig
283
-
284
-
285
- def bankroll_chart():
286
-
287
- profits = []
288
- profit = 0
289
-
290
- for b in st.session_state.bets:
291
-
292
- if b["result"]=="win":
293
-
294
- if b["odds"]>0:
295
- profit += b["stake"]*(b["odds"]/100)
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  else:
298
- profit += b["stake"]*(100/abs(b["odds"]))
299
-
300
- elif b["result"]=="loss":
301
-
302
- profit -= b["stake"]
303
-
304
- profits.append(profit)
305
-
306
- fig = px.line(
307
- x=list(range(len(profits))),
308
- y=profits,
309
- title="Bankroll Growth"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  )
311
 
312
- return fig
313
-
314
-
315
- # -------------------------------------------------
316
- # SIDEBAR NAV
317
- # -------------------------------------------------
318
-
319
- st.sidebar.title("⚾ WBC Assistant")
320
-
321
- page = st.sidebar.selectbox(
322
-
323
- "Navigation",
324
-
325
- [
326
- "Live Dashboard",
327
- "Matchup Analyzer",
328
- "Player Analytics",
329
- "Betting Intelligence",
330
- "Bet Tracker",
331
- "Simulation Center",
332
- "Algorithm Breakdown"
333
- ]
334
- )
335
-
336
- # -------------------------------------------------
337
- # LIVE DASHBOARD
338
- # -------------------------------------------------
339
-
340
- if page == "Live Dashboard":
341
-
342
- st.title("Live Statcast Dashboard")
343
 
344
- df = fetch_statcast()
 
345
 
346
- col1,col2 = st.columns(2)
347
-
348
- with col1:
349
- st.plotly_chart(pitch_movement_chart(df),use_container_width=True)
350
-
351
- with col2:
352
- st.plotly_chart(velocity_distribution(df),use_container_width=True)
353
-
354
- st.plotly_chart(exit_velocity_chart(df),use_container_width=True)
355
-
356
-
357
- # -------------------------------------------------
358
- # MATCHUP ANALYZER
359
- # -------------------------------------------------
360
-
361
- elif page == "Matchup Analyzer":
362
-
363
- st.title("Pitch-by-Pitch Matchup Model")
364
-
365
- velocity = st.slider("Velocity",80,102,95)
366
- spin = st.slider("Spin Rate",1800,3000,2300)
367
- vbreak = st.slider("Vertical Break",-20,25,15)
368
- hbreak = st.slider("Horizontal Break",-20,20,-8)
369
-
370
- result = predict_pitch(velocity,spin,vbreak,hbreak)
371
-
372
- col1,col2,col3 = st.columns(3)
373
-
374
- col1.metric("Strike Probability",round(result["strike"],2))
375
- col2.metric("Whiff Probability",round(result["whiff"],2))
376
- col3.metric("Damage Probability",round(result["damage"],2))
377
-
378
-
379
- # -------------------------------------------------
380
- # PLAYER ANALYTICS
381
- # -------------------------------------------------
382
-
383
- elif page == "Player Analytics":
384
-
385
- st.title("Player Analytics")
386
-
387
- df = fetch_statcast()
388
-
389
- col1,col2 = st.columns(2)
390
-
391
- with col1:
392
- st.plotly_chart(exit_velocity_chart(df),use_container_width=True)
393
-
394
- with col2:
395
- st.plotly_chart(launch_angle_chart(df),use_container_width=True)
396
-
397
- st.plotly_chart(spray_chart(),use_container_width=True)
398
-
399
-
400
- # -------------------------------------------------
401
- # BETTING INTELLIGENCE
402
- # -------------------------------------------------
403
-
404
- elif page == "Betting Intelligence":
405
-
406
- st.title("Betting Intelligence")
407
-
408
- odds_df = fetch_odds()
409
-
410
- model_prob = np.random.uniform(0.45,0.65)
411
-
412
- odds_df["edge"] = odds_df["prob"].apply(
413
- lambda p: calculate_edge(model_prob,p)
414
  )
415
 
416
- st.dataframe(odds_df)
417
-
418
- st.plotly_chart(edge_chart(odds_df),use_container_width=True)
419
-
420
-
421
- # -------------------------------------------------
422
- # BET TRACKER
423
- # -------------------------------------------------
424
-
425
- elif page == "Bet Tracker":
426
-
427
- st.title("Bet Tracker")
428
-
429
- col1,col2,col3,col4 = st.columns(4)
430
-
431
- with col1:
432
- book = st.selectbox("Sportsbook",SPORTSBOOKS)
433
-
434
- with col2:
435
- odds = st.number_input("Odds")
436
-
437
- with col3:
438
- stake = st.number_input("Stake")
439
-
440
- with col4:
441
- bet = st.text_input("Bet Type")
442
-
443
- if st.button("Log Bet"):
444
- log_bet(book,odds,stake,bet)
445
 
446
- if len(st.session_state.bets) > 0:
447
-
448
- bets_df = pd.DataFrame(st.session_state.bets)
449
-
450
- st.dataframe(bets_df)
451
-
452
- st.plotly_chart(bankroll_chart(),use_container_width=True)
453
-
454
-
455
- # -------------------------------------------------
456
- # SIMULATION CENTER
457
- # -------------------------------------------------
458
-
459
- elif page == "Simulation Center":
460
-
461
- st.title("Monte Carlo Game Simulation")
462
-
463
- home,away = simulate_game()
464
-
465
- fig1 = px.histogram(home,nbins=15,title="Home Run Distribution")
466
- fig2 = px.histogram(away,nbins=15,title="Away Run Distribution")
467
-
468
- st.plotly_chart(fig1,use_container_width=True)
469
- st.plotly_chart(fig2,use_container_width=True)
470
-
471
-
472
- # -------------------------------------------------
473
- # ALGORITHM BREAKDOWN
474
- # -------------------------------------------------
475
-
476
- elif page == "Algorithm Breakdown":
477
-
478
- st.title("Algorithm Breakdown")
479
-
480
- st.markdown("""
481
-
482
- ### Data Sources
483
-
484
- Statcast pitch data
485
- Sportsbook odds APIs
486
- Weather data
487
-
488
- ---
489
-
490
- ### Feature Engineering
491
-
492
- EV90
493
- Barrel rate
494
- Pitch movement
495
- Spin efficiency
496
- Bat speed
497
- Attack angle
498
-
499
- ---
500
-
501
- ### Pitch-by-Pitch AI Model
502
-
503
- Neural network predicts:
504
-
505
- Strike probability
506
- Whiff probability
507
- Damage probability
508
-
509
- ---
510
-
511
- ### Matchup Engine
512
-
513
- Compares pitcher arsenal vs hitter strengths.
514
-
515
- ---
516
-
517
- ### Simulation Engine
518
-
519
- 10,000 Monte Carlo simulations per game.
520
-
521
- ---
522
-
523
- ### Edge Detection
524
-
525
- Edge = Model Probability − Market Probability
526
-
527
- ---
528
-
529
- ### Bet Sizing
530
-
531
- Kelly Criterion.
532
-
533
- ---
534
-
535
- ### Continuous Training
536
-
537
- Historical Statcast training
538
- Weighted recent data retraining
539
- Reinforcement learning updates
540
-
541
- """)
542
-
543
- # -------------------------------------------------
544
- # AUTO REFRESH
545
- # -------------------------------------------------
546
-
547
- st.caption(f"Auto refresh every {REFRESH_RATE} seconds")
548
 
549
- time.sleep(REFRESH_RATE)
550
 
551
- st.experimental_rerun()
 
 
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()