""" ui/components.py Reusable Streamlit UI blocks. Changes: - Metrics row: Ann Return compared vs SPY (not T-bill) - Max Daily DD: shows date it happened - Conviction panel: compact single-line ETF list (no big bars) - applymap → map (deprecation fix) """ import streamlit as st import pandas as pd import numpy as np from signals.conviction import conviction_color, conviction_icon # ── Freshness status ────────────────────────────────────────────────────────── def show_freshness_status(freshness: dict): if freshness.get("fresh"): st.success(freshness["message"]) else: st.warning(freshness["message"]) # ── Winner signal banner ────────────────────────────────────────────────────── def show_signal_banner(next_signal: str, next_date, approach_name: str): is_cash = next_signal == "CASH" bg = ("linear-gradient(135deg, #2d3436 0%, #1a1a2e 100%)" if is_cash else "linear-gradient(135deg, #00d1b2 0%, #00a896 100%)") label = ("⚠️ DRAWDOWN PROTECTION ACTIVE — CASH" if is_cash else f"🎯 {next_date.strftime('%Y-%m-%d')} → {next_signal}") st.markdown(f"""
{approach_name.upper()} · NEXT TRADING DAY SIGNAL

{label}

""", unsafe_allow_html=True) # ── All models signals panel ────────────────────────────────────────────────── def show_all_signals_panel(all_signals: dict, target_etfs: list, include_cash: bool, next_date, optimal_lookback: int): COLORS = {"Approach 1": "#00ffc8", "Approach 2": "#7c6aff", "Approach 3": "#ff6b6b"} st.subheader(f"🗓️ All Models — {next_date.strftime('%Y-%m-%d')} Signals") st.caption(f"📐 Lookback **{optimal_lookback}d** found optimal (auto-selected from 30 / 45 / 60d)") cols = st.columns(len(all_signals)) for col, (name, info) in zip(cols, all_signals.items()): color = COLORS.get(name, "#888") signal = info["signal"] top_prob = float(np.max(info["proba"])) * 100 badge = " ⭐" if info["is_winner"] else "" sig_col = "#aaa" if signal == "CASH" else "white" col.markdown(f"""
{name.upper()}{badge}
{signal}
Confidence: {top_prob:.1f}%
""", unsafe_allow_html=True) def _build_etf_badges(sorted_pairs: list, best_name: str, color: str) -> str: """Build compact ETF probability badges as HTML string.""" badges = [] for name, score in sorted_pairs: is_best = name == best_name bg = "#e8fdf7" if is_best else "#f8f8f8" border = color if is_best else "#ddd" txt_col = color if is_best else "#555" weight = "700" if is_best else "400" star = "★ " if is_best else "" badges.append( f'' f'{star}{name} {score:.3f}' ) return "".join(badges) # ── Signal conviction panel ─────────────────────────────────────────────────── def show_conviction_panel(conviction: dict): label = conviction["label"] z_score = conviction["z_score"] best_name = conviction["best_name"] sorted_pairs = conviction["sorted_pairs"] color = conviction_color(label) icon = conviction_icon(label) z_clipped = max(-3.0, min(3.0, z_score)) bar_pct = int((z_clipped + 3) / 6 * 100) # ── Header with Z-score gauge ───────────────────────────────────────────── st.markdown(f"""
{icon} Signal Conviction Z = {z_score:.2f} σ {label}
Weak −3σNeutral 0σStrong +3σ
MODEL PROBABILITY BY ETF
{_build_etf_badges(sorted_pairs, best_name, color)}
""", unsafe_allow_html=True) st.caption( "Z-score = std deviations the top ETF's probability sits above the mean of all ETF probabilities. " "Higher → model is more decisive. " "⚠️ CASH override triggers if 2-day cumulative return ≤ −15%, exits when Z ≥ 1.0." ) # ── Metrics row ─────────────────────────────────────────────────────────────── def show_metrics_row(result: dict, tbill_rate: float, spy_ann_return: float = None): c1, c2, c3, c4, c5 = st.columns(5) # Ann return vs SPY if spy_ann_return is not None: diff = (result['ann_return'] - spy_ann_return) * 100 sign = "+" if diff >= 0 else "" delta_str = f"vs SPY: {sign}{diff:.2f}%" else: delta_str = f"vs T-bill: {(result['ann_return'] - tbill_rate)*100:.2f}%" c1.metric("📈 Ann. Return", f"{result['ann_return']*100:.2f}%", delta=delta_str) c2.metric("📊 Sharpe", f"{result['sharpe']:.2f}", delta="Strong" if result['sharpe'] > 1 else "Weak") c3.metric("🎯 Hit Ratio 15d", f"{result['hit_ratio']*100:.0f}%", delta="Good" if result['hit_ratio'] > 0.55 else "Weak") c4.metric("📉 Max Drawdown", f"{result['max_dd']*100:.2f}%", delta="Peak to Trough") # Max daily DD with date (only show date if available) worst_date = result.get("max_daily_date", "N/A") dd_delta = f"on {worst_date}" if worst_date != "N/A" else "Worst Single Day" c5.metric("⚠️ Max Daily DD", f"{result['max_daily_dd']*100:.2f}%", delta=dd_delta) # ── Comparison table ────────────────────────────────────────────────────────── def show_comparison_table(comparison_df: pd.DataFrame): def _highlight(row): if "WINNER" in str(row.get("Winner", "")): return ["background-color: rgba(0,200,150,0.15); font-weight:bold"] * len(row) return [""] * len(row) styled = ( comparison_df.style .apply(_highlight, axis=1) .set_properties(**{"text-align": "center", "font-size": "14px"}) .set_table_styles([ {"selector": "th", "props": [("font-size", "14px"), ("font-weight", "bold"), ("text-align", "center")]}, {"selector": "td", "props": [("padding", "10px")]}, ]) ) st.dataframe(styled, use_container_width=True) # ── Audit trail ─────────────────────────────────────────────────────────────── def show_audit_trail(audit_trail: list): if not audit_trail: st.info("No audit trail data available.") return df = pd.DataFrame(audit_trail).tail(20) cols = [c for c in ["Date", "Signal", "Net_Return", "Z_Score"] if c in df.columns] df = df[cols] def _color_ret(val): return ("color: #00c896; font-weight:bold" if val > 0 else "color: #ff4b4b; font-weight:bold") fmt = {"Net_Return": "{:.2%}"} if "Z_Score" in df.columns: fmt["Z_Score"] = "{:.2f}" styled = ( df.style .map(_color_ret, subset=["Net_Return"]) .format(fmt) .set_properties(**{"font-size": "14px", "text-align": "center"}) .set_table_styles([ {"selector": "th", "props": [("font-size", "14px"), ("font-weight", "bold"), ("text-align", "center")]}, {"selector": "td", "props": [("padding", "10px")]}, ]) ) st.dataframe(styled, use_container_width=True, height=500)