Spaces:
Running
Running
| 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) |