2026_MLB_Model / visualization /recommendation_panels.py
Syntrex's picture
Tier 5A execution layer + Alpha Release tab + edge strip fix
dba351a
raw
history blame
6.27 kB
from __future__ import annotations
from typing import Any
import streamlit.components.v1 as components
def _fmt_odds(value: Any) -> str:
if value is None:
return "—"
try:
iv = int(value)
return f"+{iv}" if iv > 0 else str(iv)
except Exception:
return str(value)
def _fmt_edge(value: Any) -> str:
if value is None:
return "—"
try:
edge = float(value)
except Exception:
return str(value)
pct = edge * 100
if pct >= 5:
color = "#22c55e"
elif pct >= 2:
color = "#84cc16"
elif pct >= 0:
color = "#eab308"
elif pct >= -3:
color = "#f97316"
else:
color = "#ef4444"
return f'<span style="color:{color};font-weight:800;">{pct:.1f}%</span>'
def _fmt_confidence(value: Any) -> str:
try:
conf = float(value)
except Exception:
return "—"
if conf >= 80:
color = "#22c55e"
elif conf >= 60:
color = "#eab308"
else:
color = "#ef4444"
return f'<span style="color:{color};font-weight:800;">{conf:.0f}</span>'
def _fmt_tier(value: Any) -> str:
tier = str(value or "").strip().lower()
if tier == "bet":
color = "#22c55e"
bg = "rgba(34,197,94,0.12)"
label = "BET"
elif tier == "watch":
color = "#eab308"
bg = "rgba(234,179,8,0.12)"
label = "WATCH"
else:
color = "#94a3b8"
bg = "rgba(148,163,184,0.10)"
label = "PASS"
return (
f'<span style="color:{color};background:{bg};padding:2px 8px;'
f'border-radius:999px;font-weight:900;">{label}</span>'
)
def _fmt_badges(values: Any) -> str:
badges = values or []
if not isinstance(badges, list):
return ""
parts = []
for badge in badges[:3]:
text = str(badge or "").strip()
if not text:
continue
if "BEST" in text.upper():
color = "#fbbf24"
bg = "rgba(251,191,36,0.14)"
elif text.upper() == "BET":
color = "#22c55e"
bg = "rgba(34,197,94,0.14)"
elif text.upper() == "WATCH":
color = "#eab308"
bg = "rgba(234,179,8,0.14)"
else:
color = "#94a3b8"
bg = "rgba(148,163,184,0.10)"
parts.append(
f'<span style="display:inline-block;padding:2px 8px;'
f'margin-right:6px;border-radius:999px;'
f'background:{bg};color:{color};font-size:10px;'
f'font-weight:900;letter-spacing:0.02em;">{text}</span>'
)
return "".join(parts)
def render_recommendation_panels(rows: list[dict[str, Any]]) -> None:
if not rows:
return
body_rows = []
for row in rows:
ev90 = row.get("ev90")
ev90_text = f" • EV90 {float(ev90):.1f}" if ev90 is not None else ""
reason_tags = row.get("reason_tags", []) or []
reason_text = " • ".join(str(tag) for tag in reason_tags[:3])
badges_html = _fmt_badges(row.get("opportunity_badges", []))
_book_src = str(row.get("book_hr_odds_source") or "placeholder")
_book_odds_raw = _fmt_odds(row.get("book_hr_odds"))
if _book_src == "placeholder":
_book_display = (
f'<span title="Reference odds (market data unavailable)" '
f'style="color:#64748b;">~{_book_odds_raw}</span>'
)
else:
_book_display = _book_odds_raw
body_rows.append(
f"""
<div class="row-wrap">
<div class="grid-row">
<div>
<div class="batter-line">{row.get("slot", "")}: {row.get("batter_name", "")}{ev90_text}</div>
<div class="badge-line">{badges_html}</div>
<div class="reason-line">{reason_text}</div>
</div>
<div>{_fmt_odds(row.get("fair_hr_odds"))}</div>
<div>{_book_display}</div>
<div>{_fmt_edge(row.get("adjusted_edge", row.get("hr_edge")))}</div>
<div>{_fmt_confidence(row.get("confidence"))}</div>
<div>{_fmt_tier(row.get("recommendation_tier"))}</div>
</div>
</div>
"""
)
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {{
margin: 0;
padding: 0;
background: transparent;
font-family: Arial, sans-serif;
}}
.panel {{
margin-top: -4px;
margin-bottom: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(148,163,184,0.14);
border-radius: 16px;
padding: 12px;
box-sizing: border-box;
color: #e2e8f0;
}}
.title {{
color: #94a3b8;
font-size: 12px;
font-weight: 700;
margin-bottom: 8px;
}}
.grid-header, .grid-row {{
display: grid;
grid-template-columns: 2.2fr 0.6fr 0.6fr 0.7fr 0.6fr 0.7fr;
gap: 8px;
align-items: center;
}}
.grid-header {{
color: #94a3b8;
font-size: 11px;
font-weight: 700;
margin-bottom: 6px;
}}
.row-wrap {{
padding: 8px 0;
border-top: 1px solid rgba(148,163,184,0.08);
}}
.row-wrap:first-of-type {{
border-top: none;
padding-top: 2px;
}}
.grid-row {{
color: #e2e8f0;
font-size: 13px;
font-weight: 700;
}}
.batter-line {{
color: #f8fafc;
font-size: 13px;
font-weight: 800;
line-height: 1.25;
margin-bottom: 3px;
}}
.badge-line {{
min-height: 18px;
margin-bottom: 3px;
}}
.reason-line {{
color: #94a3b8;
font-size: 11px;
font-weight: 600;
line-height: 1.2;
}}
</style>
</head>
<body>
<div class="panel">
<div class="title">UPCOMING BATTER HR RECOMMENDATIONS • EV90 SIM MODEL V4.1</div>
<div class="grid-header">
<div>BATTER / REASONS</div>
<div>FAIR</div>
<div>BOOK</div>
<div>EDGE</div>
<div>CONF</div>
<div>TIER</div>
</div>
{''.join(body_rows)}
</div>
</body>
</html>
"""
height = 96 + (len(rows) * 70)
components.html(html, height=height, scrolling=False)