2626 @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap');
2727 html, body, [class*="css"] { font-family: 'Space Grotesk', sans-serif; }
2828 :root {
29- --ink: #10212b;
30- --muted: #5a6b75;
31- --cream: #f6efe6;
32- --sand: #efe3d5;
33- --paper: #fffaf2;
34- --accent: #2f6f6b;
29+ --ink: #0b0f14;
30+ --muted: #4a5563;
31+ --bg: #f7f9fc;
32+ --paper: #ffffff;
33+ --panel: #f0f4fa;
34+ --accent: #2563eb;
35+ --accent-2: #3b82f6;
3536 }
36- body, .stApp {
37- background: radial-gradient(1200px 500px at 20% -10%, var(--sand), transparent),
38- linear-gradient(180deg, var(--cream) 0%, #f8f3ee 100%);
37+ body, .stApp, [data-testid="stAppViewContainer"] {
38+ background: var(--bg) !important;
3939 color: var(--ink);
4040 }
41+ .stSidebar, [data-testid="stSidebar"] {
42+ background: #eef2f7 !important;
43+ border-right: 1px solid #d5dde8;
44+ }
45+ .stSidebar *, [data-testid="stSidebar"] * {
46+ color: var(--ink) !important;
47+ }
4148 h1, h2, h3, h4, h5, h6, p, li, label, span, div {
4249 color: var(--ink) !important;
4350 }
4754 padding: 18px 22px;
4855 border-radius: 16px;
4956 background: var(--paper);
50- border: 1px solid #eadfd2 ;
51- box-shadow: 0 6px 20px rgba(16, 33, 43 , 0.06 );
57+ border: 1px solid #dbe3ef ;
58+ box-shadow: 0 8px 22px rgba(11, 15, 20 , 0.08 );
5259 }
5360 .card {
5461 padding: 14px 16px;
5562 border-radius: 12px;
56- background: var(--paper) ;
57- border: 1px solid #eadfd2 ;
58- box-shadow: 0 6px 16px rgba(16, 33, 43 , 0.06 );
63+ background: #eaf2ff ;
64+ border: 1px solid #c9dcff ;
65+ box-shadow: 0 6px 16px rgba(37, 99, 235 , 0.15 );
5966 }
6067 .pill {
6168 display: inline-block;
6269 padding: 2px 10px;
6370 border-radius: 999px;
6471 font-size: 12px;
65- background: var(--accent) ;
72+ background: #1d4ed8 ;
6673 color: #ffffff;
6774 margin-right: 6px;
75+ box-shadow: 0 6px 14px rgba(29, 78, 216, 0.3);
6876 }
6977 .muted { color: var(--muted); }
78+ a { color: var(--accent); }
79+ .stTextInput input,
80+ .stNumberInput input,
81+ .stSelectbox select,
82+ .stMultiSelect div,
83+ .stDateInput input {
84+ background: #ffffff !important;
85+ color: #0e1b24 !important;
86+ border: 1px solid #cdd9ee !important;
87+ }
88+ .stDateInput input {
89+ border-color: #2563eb !important;
90+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
91+ }
92+ .stDateInput div[role="button"] svg {
93+ color: #2563eb !important;
94+ fill: #2563eb !important;
95+ }
96+ [data-testid="stDateInputPicker"] {
97+ background: #ffffff !important;
98+ color: var(--ink) !important;
99+ border: 1px solid #dbe3ef !important;
100+ border-radius: 12px !important;
101+ box-shadow: 0 8px 20px rgba(11, 15, 20, 0.12);
102+ }
103+ [data-testid="stDateInputPicker"] * {
104+ color: var(--ink) !important;
105+ }
106+ [data-testid="stDateInputPicker"] button {
107+ color: var(--ink) !important;
108+ }
109+ [data-testid="stDateInputPicker"] [aria-selected="true"] {
110+ background: #2563eb !important;
111+ color: #ffffff !important;
112+ }
113+ .stSelectbox svg, .stMultiSelect svg {
114+ color: #0e1b24 !important;
115+ fill: #0e1b24 !important;
116+ }
117+ .stSlider label, .stSlider span {
118+ color: #0e1b24 !important;
119+ }
120+ .stSlider [data-testid="stThumbValue"] {
121+ color: #0e1b24 !important;
122+ background: #ffffff !important;
123+ border: 1px solid #cdd9ee !important;
124+ }
125+ .stSlider [data-testid="stTickBar"] {
126+ background: #cdd9ee !important;
127+ }
128+ .stSlider [data-testid="stTrack"] {
129+ background: #cfe0ff !important;
130+ }
131+ .stSlider [data-testid="stSliderThumb"] {
132+ background: var(--accent) !important;
133+ border: 2px solid #1d4ed8 !important;
134+ }
135+ .stSlider [data-testid="stSliderTrack"] {
136+ background: #cfe0ff !important;
137+ }
138+ .stDownloadButton button,
139+ .stButton button,
140+ .stForm button {
141+ background: var(--accent) !important;
142+ color: #ffffff !important;
143+ border: 1px solid #1d4ed8 !important;
144+ border-radius: 10px !important;
145+ }
146+ .stDownloadButton button:hover,
147+ .stButton button:hover,
148+ .stForm button:hover {
149+ background: var(--accent-2) !important;
150+ border-color: #1e40af !important;
151+ }
152+ .stSlider > div[data-baseweb="slider"] {
153+ color: var(--ink);
154+ }
70155 </style>
71156 """ ,
72157 unsafe_allow_html = True ,
@@ -261,6 +346,18 @@ def attach_elo_from_history(df: pd.DataFrame, history: pd.DataFrame) -> pd.DataF
261346pred ["odds_home_f" ] = to_float_series (pred , "odds_home" )
262347pred ["p_home_f" ] = to_float_series (pred , "p_home" ) # market implied probability
263348pred ["model_p_home_f" ] = to_float_series (pred , "model_p_home" ) # model probability
349+ pred ["model_p_draw_f" ] = to_float_series (pred , "model_p_draw" )
350+ pred ["model_p_away_f" ] = to_float_series (pred , "model_p_away" )
351+ pred ["model_favor" ] = np .where (
352+ pred ["model_p_home_f" ] >= pred ["model_p_away_f" ],
353+ pred .get ("home" , "" ),
354+ pred .get ("away" , "" ),
355+ )
356+ pred ["model_favor" ] = np .where (
357+ pred ["model_p_draw_f" ] > pred [["model_p_home_f" , "model_p_away_f" ]].max (axis = 1 ),
358+ "Draw" ,
359+ pred ["model_favor" ],
360+ )
264361
265362# compute edge
266363pred ["edge_home" ] = pred ["model_p_home_f" ] - pred ["p_home_f" ]
@@ -313,6 +410,16 @@ def attach_elo_from_history(df: pd.DataFrame, history: pd.DataFrame) -> pd.DataF
313410 "Candidate" ,
314411 "Skip" ,
315412)
413+ df ["model_favor" ] = np .where (
414+ df ["model_p_home_f" ] >= df ["model_p_away_f" ],
415+ df .get ("home" , "" ),
416+ df .get ("away" , "" ),
417+ )
418+ df ["model_favor" ] = np .where (
419+ df ["model_p_draw_f" ] > df [["model_p_home_f" , "model_p_away_f" ]].max (axis = 1 ),
420+ "Draw" ,
421+ df ["model_favor" ],
422+ )
316423
317424# -------------------------
318425# Simple guide
@@ -454,6 +561,8 @@ def attach_elo_from_history(df: pd.DataFrame, history: pd.DataFrame) -> pd.DataF
454561 probs = model .predict_proba (X .values )
455562 future = future .loc [X .index ].copy ()
456563 future ["model_p_home" ] = probs [:, 0 ]
564+ future ["model_p_draw" ] = probs [:, 1 ]
565+ future ["model_p_away" ] = probs [:, 2 ]
457566 future ["edge_home" ] = future ["model_p_home" ] - future ["p_home" ]
458567 future ["ev_home" ] = future .apply (
459568 lambda r : compute_ev (r .get ("odds_home" ), r .get ("model_p_home" )), axis = 1
@@ -463,11 +572,21 @@ def attach_elo_from_history(df: pd.DataFrame, history: pd.DataFrame) -> pd.DataF
463572 "Candidate" ,
464573 "Skip" ,
465574 )
575+ future ["model_favor" ] = np .where (
576+ future ["model_p_home" ] >= future ["model_p_away" ],
577+ future .get ("home" , "" ),
578+ future .get ("away" , "" ),
579+ )
580+ future ["model_favor" ] = np .where (
581+ future ["model_p_draw" ] > future [["model_p_home" , "model_p_away" ]].max (axis = 1 ),
582+ "Draw" ,
583+ future ["model_favor" ],
584+ )
466585 if "date" in future .columns :
467586 today = pd .Timestamp .today ().normalize ()
468587 future = future .loc [future ["date" ] >= today ].copy ()
469588 show_cols = [
470- c for c in ["date" , "home" , "away" , "p_home" , "model_p_home" , "edge_home" , "ev_home" , "simple_label" ]
589+ c for c in ["date" , "home" , "away" , "model_favor" , " p_home" , "model_p_home" , "edge_home" , "ev_home" , "simple_label" ]
471590 if c in future .columns
472591 ]
473592 future_sorted = future [show_cols ].sort_values ("edge_home" , ascending = False )
@@ -507,6 +626,10 @@ def attach_elo_from_history(df: pd.DataFrame, history: pd.DataFrame) -> pd.DataF
507626# Match explorer
508627# -------------------------
509628st .header ("Match explorer (filtered matches)" )
629+ st .markdown (
630+ '<div style="height:2px;background:#2563eb;border-radius:999px;margin:6px 0 14px 0;"></div>' ,
631+ unsafe_allow_html = True ,
632+ )
510633if df .empty :
511634 st .info ("No matches available with current filters." )
512635else :
@@ -523,16 +646,24 @@ def fmt(i):
523646 row = st .selectbox ("Pick match (index)" , indices , format_func = fmt )
524647 r = df .iloc [row ]
525648 st .subheader (f"{ r .get ('home' ,'?' )} vs { r .get ('away' ,'?' )} ({ r .get ('date' ,'' )} )" )
526- st .write ({
527- "Market p_home" : r .get ("p_home_f" ),
528- "Model p_home" : r .get ("model_p_home_f" ),
529- "Edge (model - market)" : r .get ("edge_home" ),
530- "Label" : r .get ("simple_label" ),
531- "Odds_home" : r .get ("odds_home_f" ),
532- "EV (per unit stake)" : r .get ("ev_home" ),
533- "Kelly fraction (capped)" : r .get ("kelly_frac_capped" ),
534- "Suggested stake ($)" : r .get ("stake_usd" )
535- })
649+ st .markdown (
650+ f"""
651+ <div class="card">
652+ <strong>Model favors:</strong> { r .get ("model_favor" , "" )} <br/>
653+ <strong>Market win chance:</strong> { r .get ("p_home_f" , 0 ):.3f} <br/>
654+ <strong>Model win chance:</strong> { r .get ("model_p_home_f" , 0 ):.3f} <br/>
655+ <strong>Model draw chance:</strong> { r .get ("model_p_draw_f" , 0 ):.3f} <br/>
656+ <strong>Model away win chance:</strong> { r .get ("model_p_away_f" , 0 ):.3f} <br/>
657+ <strong>Edge (model - market):</strong> { r .get ("edge_home" , 0 ):.3f} <br/>
658+ <strong>Label:</strong> { r .get ("simple_label" )} <br/>
659+ <strong>Odds (home):</strong> { r .get ("odds_home_f" , 0 ):.3f} <br/>
660+ <strong>EV (per $1):</strong> { r .get ("ev_home" , 0 ):.3f} <br/>
661+ <strong>Kelly (capped):</strong> { r .get ("kelly_frac_capped" , 0 ):.3f} <br/>
662+ <strong>Suggested stake:</strong> ${ r .get ("stake_usd" , 0 ):.2f}
663+ </div>
664+ """ ,
665+ unsafe_allow_html = True ,
666+ )
536667
537668# -------------------------
538669# Bankroll / Backtest plot
0 commit comments