| """AlphaForge Live Dashboard β Gradio-based real-time monitoring. |
| |
| Tabs: Overview | Portfolio | Risk | Sentiment | Signals | Logs |
| Pattern: gr.Timer(5) + @gr.on(triggers=[demo.load, timer.tick]) |
| """ |
| import gradio as gr |
| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
| import pandas as pd |
| import numpy as np |
| import json |
| import time |
| import random |
| import threading |
| from datetime import datetime, timedelta |
| from typing import Dict, List, Optional, Tuple |
|
|
| |
| DARK = { |
| "bg": "#0a0e17", "card": "#111827", "accent": "#38bdf8", |
| "green": "#10b981", "red": "#ef4444", "yellow": "#f59e0b", |
| "text": "#94a3b8", "text_bright": "#e2e8f0", "border": "#1e293b", |
| } |
|
|
| PLOTLY_TEMPLATE = "plotly_dark" |
| PLOTLY_LAYOUT = dict( |
| template=PLOTLY_TEMPLATE, paper_bgcolor=DARK["bg"], plot_bgcolor=DARK["bg"], |
| font=dict(color=DARK["text"], size=11), hovermode="x unified", |
| margin=dict(l=8, r=24, t=32, b=8), height=340, |
| ) |
|
|
| |
|
|
| class DashboardDataStore: |
| """Thread-safe data store for the dashboard.""" |
|
|
| def __init__(self): |
| self._lock = threading.Lock() |
| self.reset() |
|
|
| def reset(self): |
| with self._lock: |
| self.pnl = [] |
| self.dates = [] |
| self.returns = [] |
| self.weights = {} |
| self.positions = {} |
| self.alerts = deque(maxlen=50) |
| self.regime = "neutral" |
| self.sharpe = 0.0 |
| self.vol = 0.0 |
| self.max_dd = 0.0 |
| self.var_95 = 0.0 |
| self.sortino = 0.0 |
| self.calmar = 0.0 |
| self.current_drawdown = 0.0 |
| self.alpha = 0.0 |
| self.beta = 0.0 |
| self.turnover = 0.0 |
| self.sentiment_scores = {} |
| self.feature_importance = {} |
| self.ic_history = [] |
| self.regime_probs = {} |
|
|
| def update_metrics(self, metrics: Dict): |
| with self._lock: |
| for k, v in metrics.items(): |
| if hasattr(self, k): |
| setattr(self, k, v) |
|
|
| def update_pnl(self, date: datetime, pnl_value: float, ret: float): |
| with self._lock: |
| self.dates.append(date) |
| self.pnl.append(pnl_value) |
|
|
| def add_alert(self, level: str, title: str, text: str): |
| with self._lock: |
| self.alerts.append({ |
| "time": datetime.now().strftime("%H:%M:%S"), |
| "level": level, "title": title, "text": text |
| }) |
|
|
| def snapshot(self) -> Dict: |
| with self._lock: |
| return { |
| "pnl": list(self.pnl), "dates": [str(d) for d in self.dates], |
| "returns": list(self.returns), "regime": self.regime, |
| "sharpe": self.sharpe, "vol": self.vol, "max_dd": self.max_dd, |
| "var_95": self.var_95, "sortino": self.sortino, "calmar": self.calmar, |
| "current_drawdown": self.current_drawdown, |
| "alpha": self.alpha, "beta": self.beta, "turnover": self.turnover, |
| "positions": self.positions, "sentiment_scores": self.sentiment_scores, |
| "feature_importance": self.feature_importance, |
| "ic_history": list(self.ic_history), |
| "regime_probs": self.regime_probs, |
| "alerts": list(self.alerts), |
| } |
|
|
| _store = DashboardDataStore() |
|
|
| |
|
|
| def _generate_synthetic_run(): |
| """Generate realistic synthetic data for demo purposes.""" |
| np.random.seed(42) |
| n_days = 252 |
| dates = pd.bdate_range(start="2024-01-01", periods=n_days) |
| |
| daily_mean = 0.0008 |
| daily_vol = 0.012 |
| returns = np.random.normal(daily_mean, daily_vol, n_days) |
| |
| returns[100:140] = np.random.normal(-0.003, 0.02, 40) |
| |
| returns[140:180] = np.random.normal(0.0025, 0.012, 40) |
| pnl = np.cumsum(returns) * 1_000_000 |
|
|
| |
| cumulative = np.cumprod(1 + returns) |
| running_max = np.maximum.accumulate(cumulative) |
| drawdowns = (cumulative - running_max) / running_max |
|
|
| sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) |
| vol = np.std(returns) * np.sqrt(252) |
| max_dd = np.min(drawdowns) |
| sortino = np.mean(returns) / (np.std(returns[returns < 0]) + 1e-8) * np.sqrt(252) |
| var_95 = -np.percentile(returns, 5) * 1_000_000 |
|
|
| |
| _store.reset() |
| for i, d in enumerate(dates): |
| _store.update_pnl(d.to_pydatetime(), pnl[i], returns[i]) |
|
|
| _store.update_metrics({ |
| "sharpe": sharpe, "vol": vol, "max_dd": max_dd, "var_95": var_95, |
| "sortino": sortino, "calmar": np.mean(returns) * 252 / abs(max_dd), |
| "current_drawdown": drawdowns[-1], "alpha": 0.12, "beta": 0.95, "turnover": 0.15, |
| }) |
|
|
| |
| _store.positions = { |
| "SPY": 0.18, "QQQ": 0.15, "AAPL": 0.10, "MSFT": 0.12, "GOOGL": 0.08, |
| "AMZN": 0.07, "META": 0.06, "NVDA": 0.14, "TSLA": 0.05, "JPM": 0.05, |
| } |
| _store.sentiment_scores = {"AAPL": 0.72, "MSFT": 0.65, "NVDA": 0.88, "TSLA": -0.34, "SPY": 0.15} |
| _store.feature_importance = { |
| "micro_amihud_illiquidity": 0.08, "macro_vix_level": 0.07, "ta_supertrend_dist": 0.06, |
| "return_5d": 0.05, "cs_mom_63d": 0.05, "vol_regime_21d": 0.04, "regime_composite": 0.04, |
| "ta_ichimoku_tk_cross": 0.04, "mr_signal": 0.03, "rvol_21d": 0.03, |
| "yc_spread": 0.03, "volume_delta": 0.03, "sma_20d": 0.02, "macro_credit_spread": 0.02, |
| "kyle_lambda": 0.02, "ta_keltner_position": 0.02, "cs_skew": 0.02, "trend_63d": 0.02, |
| "vix_regime": 0.02, "return_21d": 0.01, |
| } |
| _store.regime_probs = {"bull": 0.45, "bear": 0.12, "high_vol": 0.28, "neutral": 0.15} |
| _store.regime = "bull" |
| _store.ic_history = [0.05 + np.random.normal(0, 0.02) for _ in range(60)] |
|
|
| |
| _store.add_alert("info", "Backtest Complete", "Sharpe: 1.82, Max DD: -4.3%") |
| _store.add_alert("warn", "Volatility Spike", "VIX at 28.5, reducing exposure to 80%") |
| _store.add_alert("info", "Regime Change", "Switched from neutral to bull β increasing equity allocation") |
|
|
| |
|
|
| def build_pnl_chart() -> str: |
| """Build PnL + Drawdown overlay chart.""" |
| snap = _store.snapshot() |
| if not snap["pnl"]: |
| return "<div style='color:#64748b;text-align:center;padding:60px'>No PnL data yet</div>" |
|
|
| df = pd.DataFrame({"date": pd.to_datetime(snap["dates"]), "pnl": snap["pnl"]}) |
| cumulative = np.cumprod(1 + np.array(snap["returns"])) |
| running_max = np.maximum.accumulate(cumulative) |
| dd = (cumulative - running_max) / running_max |
|
|
| fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03, |
| row_heights=[0.7, 0.3], subplot_titles=["Cumulative PnL", "Drawdown"]) |
|
|
| |
| fig.add_trace(go.Scatter( |
| x=df["date"], y=df["pnl"], name="PnL", |
| line=dict(color=DARK["accent"], width=2.2), |
| fill="tozeroy", fillcolor="rgba(56,189,248,0.08)", |
| ), row=1, col=1) |
| |
| fig.add_trace(go.Scatter( |
| x=df["date"], y=np.full(len(df), 0), |
| line=dict(color=DARK["green"], width=1, dash="dot"), name="Breakeven", |
| ), row=1, col=1) |
|
|
| |
| fig.add_trace(go.Scatter( |
| x=df["date"], y=dd * 100, name="Drawdown %", |
| line=dict(color=DARK["red"], width=1.5), |
| fill="tozeroy", fillcolor="rgba(239,68,68,0.08)", |
| ), row=2, col=1) |
|
|
| fig.update_layout(**PLOTLY_LAYOUT, height=440, showlegend=False, |
| yaxis=dict(title="$", tickprefix="$", tickformat=",.0f"), |
| yaxis2=dict(title="%", ticksuffix="%"), |
| ) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| def build_risk_chart() -> str: |
| """Build rolling risk metrics chart.""" |
| snap = _store.snapshot() |
| if not snap["returns"]: |
| return "<div style='color:#64748b;text-align:center;padding:60px'>No data</div>" |
|
|
| rets = np.array(snap["returns"]) |
| dates = pd.to_datetime(snap["dates"]) |
| w = min(21, len(rets) // 4) |
| rolling_sharpe = pd.Series(rets).rolling(w).apply( |
| lambda x: np.mean(x) / (np.std(x) + 1e-10) * np.sqrt(252) |
| ) |
| rolling_vol = pd.Series(rets).rolling(w).std() * np.sqrt(252) * 100 |
|
|
| fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.06, |
| row_heights=[0.5, 0.5], subplot_titles=["Rolling Sharpe (63d)", "Rolling Volatility %"]) |
|
|
| fig.add_trace(go.Scatter(x=dates, y=rolling_sharpe, name="Sharpe", |
| line=dict(color=DARK["accent"], width=2)), row=1, col=1) |
| fig.add_trace(go.Scatter(x=dates, y=np.full(len(dates), snap["sharpe"]), |
| line=dict(color="rgba(239,68,68,0.4)", dash="dot", width=1), name="Overall Sharpe"), row=1, col=1) |
|
|
| fig.add_trace(go.Scatter(x=dates, y=rolling_vol, name="Vol %", |
| line=dict(color=DARK["yellow"], width=2)), row=2, col=1) |
|
|
| fig.update_layout(**PLOTLY_LAYOUT, height=400, showlegend=False) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| def build_weights_chart() -> str: |
| """Build portfolio weight treemap as bar chart.""" |
| pos = _store.positions |
| if not pos: |
| return "<div style='color:#64748b;text-align:center;padding:40px'>No positions</div>" |
|
|
| items = sorted(pos.items(), key=lambda x: x[1], reverse=True) |
| labels = [f"{k}" for k, v in items] |
| values = [v * 100 for k, v in items] |
|
|
| fig = go.Figure(go.Bar( |
| x=labels, y=values, |
| marker=dict(color=[DARK["accent"]]*3 + ["#6366f1"]*3 + |
| [DARK["green"]]*2 + ["#8b5cf6"]*2 if len(labels) > 4 else [DARK["accent"]]*len(labels)), |
| text=[f"{v:.1f}%" for v in values], textposition="outside", |
| marker_line=dict(width=0), |
| )) |
| fig.update_layout( |
| **PLOTLY_LAYOUT, height=260, showlegend=False, |
| yaxis=dict(title="Weight %", ticksuffix="%", range=[0, max(values)*1.3]), |
| xaxis=dict(tickangle=-30), |
| ) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| def build_feature_importance_chart() -> str: |
| """Build top-10 feature importance chart.""" |
| fi = _store.feature_importance |
| if not fi: |
| return "<div style='color:#64748b;text-align:center;padding:40px'>No data</div>" |
|
|
| items = sorted(fi.items(), key=lambda x: x[1], reverse=True)[:12] |
| labels = [k.replace("_"," ").title()[:30] for k, v in items] |
| values = [v * 100 for k, v in items] |
|
|
| fig = go.Figure(go.Bar( |
| y=labels[::-1], x=values[::-1], orientation="h", |
| marker=dict(color=[DARK["accent"]]*len(labels)), |
| text=[f"{v:.1f}%" for v in values[::-1]], textposition="outside", |
| )) |
| fig.update_layout(**PLOTLY_LAYOUT, height=340, showlegend=False, |
| xaxis=dict(title="Importance %", ticksuffix="%"), |
| margin=dict(l=120, r=24, t=32, b=8), |
| ) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| def build_regime_chart() -> str: |
| """Build regime probability donut chart.""" |
| rp = _store.regime_probs |
| if not rp: |
| return "<div style='color:#64748b;text-align:center;padding:40px'>Loading...</div>" |
|
|
| colors_map = {"bull": DARK["green"], "bear": DARK["red"], |
| "high_vol": DARK["yellow"], "neutral": "#64748b"} |
| fig = go.Figure(go.Pie( |
| labels=list(rp.keys()), values=list(rp.values()), hole=0.55, |
| marker=dict(colors=[colors_map.get(k, DARK["accent"]) for k in rp.keys()]), |
| textinfo="label+percent", textfont=dict(size=12, color=DARK["text_bright"]), |
| )) |
| fig.update_layout(**PLOTLY_LAYOUT, height=260, showlegend=False) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| def build_sentiment_chart() -> str: |
| """Build sentiment bar chart.""" |
| ss = _store.sentiment_scores |
| if not ss: |
| return "<div style='color:#64748b;text-align:center;padding:40px'>No sentiment data</div>" |
|
|
| items = sorted(ss.items(), key=lambda x: abs(x[1]), reverse=True) |
| labels = [k for k, v in items] |
| values = [v for k, v in items] |
| colors = [DARK["green"] if v > 0 else DARK["red"] for v in values] |
|
|
| fig = go.Figure(go.Bar( |
| x=labels, y=values, marker=dict(color=colors), |
| text=[f"{v:+.2f}" for v in values], textposition="outside", |
| )) |
| fig.update_layout( |
| **PLOTLY_LAYOUT, height=240, showlegend=False, |
| yaxis=dict(title="Sentiment Score", range=[-1.1, 1.1]), |
| xaxis=dict(tickangle=-30), |
| ) |
| fig.add_hline(y=0, line=dict(color=DARK["text"], width=1, dash="dot")) |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, |
| config={"responsive": True, "displayModeBar": False}) |
|
|
|
|
| |
|
|
| def _kpi_card(value: str, label: str, color: str = DARK["accent"], |
| sub: str = "", sub_color: str = "") -> str: |
| """Render a KPI card.""" |
| sub_html = f'<div style="color:{sub_color};font-size:11px;margin-top:2px">{sub}</div>' if sub else "" |
| return f''' |
| <div style="background:{DARK["card"]};border:1px solid {DARK["border"]};border-radius:10px; |
| padding:14px 18px;text-align:center;min-width:110px"> |
| <div style="color:{DARK["text"]};font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px"> |
| {label} |
| </div> |
| <div style="color:{color};font-size:22px;font-weight:700;line-height:1.2">{value}</div> |
| {sub_html} |
| </div>''' |
|
|
| def _ticker_pill(symbol: str, weight: float, pnl_pct: float = 0) -> str: |
| """Render a ticker pill badge.""" |
| pnl_color = DARK["green"] if pnl_pct >= 0 else DARK["red"] |
| pnl_sign = "+" if pnl_pct >= 0 else "" |
| return f''' |
| <div style="display:inline-block;background:{DARK["card"]};border:1px solid {DARK["border"]}; |
| border-radius:8px;padding:8px 14px;margin:3px;min-width:140px"> |
| <span style="color:{DARK["accent"]};font-weight:700;font-size:14px">{symbol}</span> |
| <span style="color:{DARK["text_bright"]};font-size:13px;margin-left:8px">{weight:.1%}</span> |
| <span style="color:{pnl_color};font-size:12px;margin-left:6px">{pnl_sign}{pnl_pct:.1%}</span> |
| </div>''' |
|
|
| def _alert_badge(level: str, title: str, text: str, time_str: str) -> str: |
| """Render an alert badge.""" |
| colors = {"error": DARK["red"], "warn": DARK["yellow"], "info": DARK["accent"]} |
| color = colors.get(level, DARK["text"]) |
| return f''' |
| <div style="background:{DARK["card"]};border-left:3px solid {color};border-radius:4px; |
| padding:8px 12px;margin:4px 0"> |
| <span style="color:{color};font-weight:600;font-size:12px">[{time_str}] {title}</span> |
| <span style="color:{DARK["text"]};font-size:11px;margin-left:8px">{text}</span> |
| </div>''' |
|
|
| def build_top_bar() -> str: |
| """Build top status bar.""" |
| snap = _store.snapshot() |
| regime_color = {"bull": DARK["green"], "bear": DARK["red"], |
| "high_vol": DARK["yellow"], "neutral": DARK["text"]} |
| rc = regime_color.get(snap.get("regime","neutral"), DARK["text"]) |
|
|
| return f''' |
| <div style="background:{DARK["card"]};padding:10px 24px;display:flex;align-items:center; |
| justify-content:space-between;border-bottom:1px solid {DARK["border"]}"> |
| <div style="display:flex;align-items:center;gap:16px"> |
| <span style="color:{DARK["accent"]};font-size:20px;font-weight:800">π§ AlphaForge</span> |
| <span style="color:{DARK["text"]};font-size:10px;padding:2px 8px;background:#1e293b;border-radius:4px">v2.0 PRO</span> |
| </div> |
| <div style="display:flex;align-items:center;gap:24px"> |
| <span style="color:{DARK["text"]};font-size:12px"> |
| Regime: <b style="color:{rc}">{snap.get("regime","--").upper()}</b> |
| </span> |
| <span style="color:{DARK["text"]};font-size:12px"> |
| Last Refresh: {datetime.now().strftime("%H:%M:%S")} |
| </span> |
| <span style="color:{DARK["green"]};font-size:10px">β LIVE</span> |
| </div> |
| </div>''' |
|
|
| def build_kpi_row() -> str: |
| """Build row of KPI cards.""" |
| snap = _store.snapshot() |
| pnl = snap.get("pnl", []) |
| last_pnl = pnl[-1] if pnl else 0 |
| return ( |
| _kpi_card(f"${last_pnl:,.0f}", "Cumulative PnL", DARK["accent"]) + |
| _kpi_card(f"{snap.get('sharpe',0):.2f}", "Sharpe", DARK["green"] if snap.get("sharpe",0) >= 1 else DARK["yellow"]) + |
| _kpi_card(f"{snap.get('sortino',0):.2f}", "Sortino", DARK["accent"]) + |
| _kpi_card(f"{snap.get('max_dd',0)*100:.1f}%", "Max Drawdown", DARK["red"]) + |
| _kpi_card(f"${snap.get('var_95',0):,.0f}", "VaR 95%", DARK["yellow"]) + |
| _kpi_card(f"{snap.get('calmar',0):.2f}", "Calmar", DARK["accent"]) + |
| _kpi_card(f"{snap.get('alpha',0)*100:.1f}%", "Alpha", DARK["green"] if snap.get("alpha",0) >= 0 else DARK["red"]) + |
| _kpi_card(f"{snap.get('beta',0):.2f}", "Beta", DARK["accent"]) |
| ) |
|
|
| def build_positions_html() -> str: |
| """Build positions ticker pills.""" |
| pos = _store.positions |
| if not pos: |
| return "<p style='color:#64748b'>No positions</p>" |
| items = sorted(pos.items(), key=lambda x: x[1], reverse=True) |
| return "<div style='display:flex;flex-wrap:wrap;gap:4px'>" + \ |
| "".join(_ticker_pill(k, v, np.random.normal(0, 0.03)) for k, v in items) + "</div>" |
|
|
| def build_alerts_html() -> str: |
| """Build alerts feed.""" |
| alerts = _store.alerts |
| return "".join(_alert_badge(a["level"], a["title"], a["text"], a["time"]) for a in alerts) |
|
|
|
|
| |
|
|
| custom_css = """ |
| .gradio-container { max-width: 1400px !important; margin: 0 auto; } |
| footer { display: none !important; } |
| .tab-nav { margin-bottom: 12px !important; } |
| """ |
|
|
| with gr.Blocks( |
| title="AlphaForge Pro Dashboard", |
| css=custom_css, |
| theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"), |
| fill_width=True, |
| ) as demo: |
|
|
| |
| timer = gr.Timer(5) |
|
|
| |
| top_bar = gr.HTML(value=build_top_bar(), every=5) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1, min_width=220): |
| gr.Markdown("### π KPIs") |
| kpi_html = gr.HTML(value=build_kpi_row, every=5) |
|
|
| gr.Markdown("### πΌ Positions") |
| positions_html = gr.HTML(value=build_positions_html, every=5) |
|
|
| gr.Markdown("### π― Regime") |
| regime_chart = gr.HTML(value=build_regime_chart, every=5) |
|
|
| with gr.Column(scale=4): |
| with gr.Tabs(): |
| with gr.Tab("π PnL & Risk"): |
| with gr.Row(): |
| pnl_chart = gr.HTML(value=build_pnl_chart, every=5) |
| with gr.Row(): |
| risk_chart = gr.HTML(value=build_risk_chart, every=5) |
|
|
| with gr.Tab("βοΈ Weights & Factors"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("### Portfolio Weights") |
| weights_chart = gr.HTML(value=build_weights_chart, every=5) |
| with gr.Column(scale=1): |
| gr.Markdown("### Feature Importance") |
| feature_chart = gr.HTML(value=build_feature_importance_chart, every=5) |
|
|
| with gr.Tab("ποΈ Sentiment"): |
| with gr.Row(): |
| sentiment_chart = gr.HTML(value=build_sentiment_chart, every=5) |
|
|
| with gr.Tab("β οΈ Alerts & Logs"): |
| alerts_html = gr.HTML(value=build_alerts_html, every=5) |
|
|
| |
| @gr.on(triggers=[demo.load, timer.tick], outputs=[top_bar, kpi_html, positions_html, |
| regime_chart, pnl_chart, risk_chart, weights_chart, feature_chart, |
| sentiment_chart, alerts_html]) |
| def refresh_all(): |
| return (build_top_bar(), build_kpi_row(), build_positions_html(), |
| build_regime_chart(), build_pnl_chart(), build_risk_chart(), |
| build_weights_chart(), build_feature_importance_chart(), |
| build_sentiment_chart(), build_alerts_html()) |
|
|
|
|
| if __name__ == "__main__": |
| |
| _generate_synthetic_run() |
| demo.launch() |