Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AlphaForge Live Dashboard β Gradio-based real-time monitoring.
|
| 2 |
+
|
| 3 |
+
Tabs: Overview | Portfolio | Risk | Sentiment | Signals | Logs
|
| 4 |
+
Pattern: gr.Timer(5) + @gr.on(triggers=[demo.load, timer.tick])
|
| 5 |
+
"""
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
from plotly.subplots import make_subplots
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import numpy as np
|
| 11 |
+
import json
|
| 12 |
+
import time
|
| 13 |
+
import random
|
| 14 |
+
import threading
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
from typing import Dict, List, Optional, Tuple
|
| 17 |
+
|
| 18 |
+
# ββ Dark theme βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
DARK = {
|
| 20 |
+
"bg": "#0a0e17", "card": "#111827", "accent": "#38bdf8",
|
| 21 |
+
"green": "#10b981", "red": "#ef4444", "yellow": "#f59e0b",
|
| 22 |
+
"text": "#94a3b8", "text_bright": "#e2e8f0", "border": "#1e293b",
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
PLOTLY_TEMPLATE = "plotly_dark"
|
| 26 |
+
PLOTLY_LAYOUT = dict(
|
| 27 |
+
template=PLOTLY_TEMPLATE, paper_bgcolor=DARK["bg"], plot_bgcolor=DARK["bg"],
|
| 28 |
+
font=dict(color=DARK["text"], size=11), hovermode="x unified",
|
| 29 |
+
margin=dict(l=8, r=24, t=32, b=8), height=340,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# ββ Data Store βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
|
| 34 |
+
class DashboardDataStore:
|
| 35 |
+
"""Thread-safe data store for the dashboard."""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self._lock = threading.Lock()
|
| 39 |
+
self.reset()
|
| 40 |
+
|
| 41 |
+
def reset(self):
|
| 42 |
+
with self._lock:
|
| 43 |
+
self.pnl = []
|
| 44 |
+
self.dates = []
|
| 45 |
+
self.returns = []
|
| 46 |
+
self.weights = {}
|
| 47 |
+
self.positions = {}
|
| 48 |
+
self.alerts = deque(maxlen=50)
|
| 49 |
+
self.regime = "neutral"
|
| 50 |
+
self.sharpe = 0.0
|
| 51 |
+
self.vol = 0.0
|
| 52 |
+
self.max_dd = 0.0
|
| 53 |
+
self.var_95 = 0.0
|
| 54 |
+
self.sortino = 0.0
|
| 55 |
+
self.calmar = 0.0
|
| 56 |
+
self.current_drawdown = 0.0
|
| 57 |
+
self.alpha = 0.0
|
| 58 |
+
self.beta = 0.0
|
| 59 |
+
self.turnover = 0.0
|
| 60 |
+
self.sentiment_scores = {}
|
| 61 |
+
self.feature_importance = {}
|
| 62 |
+
self.ic_history = []
|
| 63 |
+
self.regime_probs = {}
|
| 64 |
+
|
| 65 |
+
def update_metrics(self, metrics: Dict):
|
| 66 |
+
with self._lock:
|
| 67 |
+
for k, v in metrics.items():
|
| 68 |
+
if hasattr(self, k):
|
| 69 |
+
setattr(self, k, v)
|
| 70 |
+
|
| 71 |
+
def update_pnl(self, date: datetime, pnl_value: float, ret: float):
|
| 72 |
+
with self._lock:
|
| 73 |
+
self.dates.append(date)
|
| 74 |
+
self.pnl.append(pnl_value)
|
| 75 |
+
|
| 76 |
+
def add_alert(self, level: str, title: str, text: str):
|
| 77 |
+
with self._lock:
|
| 78 |
+
self.alerts.append({
|
| 79 |
+
"time": datetime.now().strftime("%H:%M:%S"),
|
| 80 |
+
"level": level, "title": title, "text": text
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
def snapshot(self) -> Dict:
|
| 84 |
+
with self._lock:
|
| 85 |
+
return {
|
| 86 |
+
"pnl": list(self.pnl), "dates": [str(d) for d in self.dates],
|
| 87 |
+
"returns": list(self.returns), "regime": self.regime,
|
| 88 |
+
"sharpe": self.sharpe, "vol": self.vol, "max_dd": self.max_dd,
|
| 89 |
+
"var_95": self.var_95, "sortino": self.sortino, "calmar": self.calmar,
|
| 90 |
+
"current_drawdown": self.current_drawdown,
|
| 91 |
+
"alpha": self.alpha, "beta": self.beta, "turnover": self.turnover,
|
| 92 |
+
"positions": self.positions, "sentiment_scores": self.sentiment_scores,
|
| 93 |
+
"feature_importance": self.feature_importance,
|
| 94 |
+
"ic_history": list(self.ic_history),
|
| 95 |
+
"regime_probs": self.regime_probs,
|
| 96 |
+
"alerts": list(self.alerts),
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
_store = DashboardDataStore()
|
| 100 |
+
|
| 101 |
+
# ββ Synthetic Data Generator βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
+
|
| 103 |
+
def _generate_synthetic_run():
|
| 104 |
+
"""Generate realistic synthetic data for demo purposes."""
|
| 105 |
+
np.random.seed(42)
|
| 106 |
+
n_days = 252
|
| 107 |
+
dates = pd.bdate_range(start="2024-01-01", periods=n_days)
|
| 108 |
+
# Correlated returns
|
| 109 |
+
daily_mean = 0.0008
|
| 110 |
+
daily_vol = 0.012
|
| 111 |
+
returns = np.random.normal(daily_mean, daily_vol, n_days)
|
| 112 |
+
# Add a drawdown period
|
| 113 |
+
returns[100:140] = np.random.normal(-0.003, 0.02, 40)
|
| 114 |
+
# Add recovery
|
| 115 |
+
returns[140:180] = np.random.normal(0.0025, 0.012, 40)
|
| 116 |
+
pnl = np.cumsum(returns) * 1_000_000 # scale to $1M
|
| 117 |
+
|
| 118 |
+
# Compute metrics
|
| 119 |
+
cumulative = np.cumprod(1 + returns)
|
| 120 |
+
running_max = np.maximum.accumulate(cumulative)
|
| 121 |
+
drawdowns = (cumulative - running_max) / running_max
|
| 122 |
+
|
| 123 |
+
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252)
|
| 124 |
+
vol = np.std(returns) * np.sqrt(252)
|
| 125 |
+
max_dd = np.min(drawdowns)
|
| 126 |
+
sortino = np.mean(returns) / (np.std(returns[returns < 0]) + 1e-8) * np.sqrt(252)
|
| 127 |
+
var_95 = -np.percentile(returns, 5) * 1_000_000
|
| 128 |
+
|
| 129 |
+
# Store
|
| 130 |
+
_store.reset()
|
| 131 |
+
for i, d in enumerate(dates):
|
| 132 |
+
_store.update_pnl(d.to_pydatetime(), pnl[i], returns[i])
|
| 133 |
+
|
| 134 |
+
_store.update_metrics({
|
| 135 |
+
"sharpe": sharpe, "vol": vol, "max_dd": max_dd, "var_95": var_95,
|
| 136 |
+
"sortino": sortino, "calmar": np.mean(returns) * 252 / abs(max_dd),
|
| 137 |
+
"current_drawdown": drawdowns[-1], "alpha": 0.12, "beta": 0.95, "turnover": 0.15,
|
| 138 |
+
})
|
| 139 |
+
|
| 140 |
+
# Positions
|
| 141 |
+
_store.positions = {
|
| 142 |
+
"SPY": 0.18, "QQQ": 0.15, "AAPL": 0.10, "MSFT": 0.12, "GOOGL": 0.08,
|
| 143 |
+
"AMZN": 0.07, "META": 0.06, "NVDA": 0.14, "TSLA": 0.05, "JPM": 0.05,
|
| 144 |
+
}
|
| 145 |
+
_store.sentiment_scores = {"AAPL": 0.72, "MSFT": 0.65, "NVDA": 0.88, "TSLA": -0.34, "SPY": 0.15}
|
| 146 |
+
_store.feature_importance = {
|
| 147 |
+
"micro_amihud_illiquidity": 0.08, "macro_vix_level": 0.07, "ta_supertrend_dist": 0.06,
|
| 148 |
+
"return_5d": 0.05, "cs_mom_63d": 0.05, "vol_regime_21d": 0.04, "regime_composite": 0.04,
|
| 149 |
+
"ta_ichimoku_tk_cross": 0.04, "mr_signal": 0.03, "rvol_21d": 0.03,
|
| 150 |
+
"yc_spread": 0.03, "volume_delta": 0.03, "sma_20d": 0.02, "macro_credit_spread": 0.02,
|
| 151 |
+
"kyle_lambda": 0.02, "ta_keltner_position": 0.02, "cs_skew": 0.02, "trend_63d": 0.02,
|
| 152 |
+
"vix_regime": 0.02, "return_21d": 0.01,
|
| 153 |
+
}
|
| 154 |
+
_store.regime_probs = {"bull": 0.45, "bear": 0.12, "high_vol": 0.28, "neutral": 0.15}
|
| 155 |
+
_store.regime = "bull"
|
| 156 |
+
_store.ic_history = [0.05 + np.random.normal(0, 0.02) for _ in range(60)]
|
| 157 |
+
|
| 158 |
+
# Alerts
|
| 159 |
+
_store.add_alert("info", "Backtest Complete", "Sharpe: 1.82, Max DD: -4.3%")
|
| 160 |
+
_store.add_alert("warn", "Volatility Spike", "VIX at 28.5, reducing exposure to 80%")
|
| 161 |
+
_store.add_alert("info", "Regime Change", "Switched from neutral to bull β increasing equity allocation")
|
| 162 |
+
|
| 163 |
+
# ββ Chart Builders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
def build_pnl_chart() -> str:
|
| 166 |
+
"""Build PnL + Drawdown overlay chart."""
|
| 167 |
+
snap = _store.snapshot()
|
| 168 |
+
if not snap["pnl"]:
|
| 169 |
+
return "<div style='color:#64748b;text-align:center;padding:60px'>No PnL data yet</div>"
|
| 170 |
+
|
| 171 |
+
df = pd.DataFrame({"date": pd.to_datetime(snap["dates"]), "pnl": snap["pnl"]})
|
| 172 |
+
cumulative = np.cumprod(1 + np.array(snap["returns"]))
|
| 173 |
+
running_max = np.maximum.accumulate(cumulative)
|
| 174 |
+
dd = (cumulative - running_max) / running_max
|
| 175 |
+
|
| 176 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03,
|
| 177 |
+
row_heights=[0.7, 0.3], subplot_titles=["Cumulative PnL", "Drawdown"])
|
| 178 |
+
|
| 179 |
+
# PnL
|
| 180 |
+
fig.add_trace(go.Scatter(
|
| 181 |
+
x=df["date"], y=df["pnl"], name="PnL",
|
| 182 |
+
line=dict(color=DARK["accent"], width=2.2),
|
| 183 |
+
fill="tozeroy", fillcolor="rgba(56,189,248,0.08)",
|
| 184 |
+
), row=1, col=1)
|
| 185 |
+
# Underwater marker
|
| 186 |
+
fig.add_trace(go.Scatter(
|
| 187 |
+
x=df["date"], y=np.full(len(df), 0),
|
| 188 |
+
line=dict(color=DARK["green"], width=1, dash="dot"), name="Breakeven",
|
| 189 |
+
), row=1, col=1)
|
| 190 |
+
|
| 191 |
+
# Drawdown
|
| 192 |
+
fig.add_trace(go.Scatter(
|
| 193 |
+
x=df["date"], y=dd * 100, name="Drawdown %",
|
| 194 |
+
line=dict(color=DARK["red"], width=1.5),
|
| 195 |
+
fill="tozeroy", fillcolor="rgba(239,68,68,0.08)",
|
| 196 |
+
), row=2, col=1)
|
| 197 |
+
|
| 198 |
+
fig.update_layout(**PLOTLY_LAYOUT, height=440, showlegend=False,
|
| 199 |
+
yaxis=dict(title="$", tickprefix="$", tickformat=",.0f"),
|
| 200 |
+
yaxis2=dict(title="%", ticksuffix="%"),
|
| 201 |
+
)
|
| 202 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 203 |
+
config={"responsive": True, "displayModeBar": False})
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def build_risk_chart() -> str:
|
| 207 |
+
"""Build rolling risk metrics chart."""
|
| 208 |
+
snap = _store.snapshot()
|
| 209 |
+
if not snap["returns"]:
|
| 210 |
+
return "<div style='color:#64748b;text-align:center;padding:60px'>No data</div>"
|
| 211 |
+
|
| 212 |
+
rets = np.array(snap["returns"])
|
| 213 |
+
dates = pd.to_datetime(snap["dates"])
|
| 214 |
+
w = min(21, len(rets) // 4)
|
| 215 |
+
rolling_sharpe = pd.Series(rets).rolling(w).apply(
|
| 216 |
+
lambda x: np.mean(x) / (np.std(x) + 1e-10) * np.sqrt(252)
|
| 217 |
+
)
|
| 218 |
+
rolling_vol = pd.Series(rets).rolling(w).std() * np.sqrt(252) * 100
|
| 219 |
+
|
| 220 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.06,
|
| 221 |
+
row_heights=[0.5, 0.5], subplot_titles=["Rolling Sharpe (63d)", "Rolling Volatility %"])
|
| 222 |
+
|
| 223 |
+
fig.add_trace(go.Scatter(x=dates, y=rolling_sharpe, name="Sharpe",
|
| 224 |
+
line=dict(color=DARK["accent"], width=2)), row=1, col=1)
|
| 225 |
+
fig.add_trace(go.Scatter(x=dates, y=np.full(len(dates), snap["sharpe"]),
|
| 226 |
+
line=dict(color="rgba(239,68,68,0.4)", dash="dot", width=1), name="Overall Sharpe"), row=1, col=1)
|
| 227 |
+
|
| 228 |
+
fig.add_trace(go.Scatter(x=dates, y=rolling_vol, name="Vol %",
|
| 229 |
+
line=dict(color=DARK["yellow"], width=2)), row=2, col=1)
|
| 230 |
+
|
| 231 |
+
fig.update_layout(**PLOTLY_LAYOUT, height=400, showlegend=False)
|
| 232 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 233 |
+
config={"responsive": True, "displayModeBar": False})
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def build_weights_chart() -> str:
|
| 237 |
+
"""Build portfolio weight treemap as bar chart."""
|
| 238 |
+
pos = _store.positions
|
| 239 |
+
if not pos:
|
| 240 |
+
return "<div style='color:#64748b;text-align:center;padding:40px'>No positions</div>"
|
| 241 |
+
|
| 242 |
+
items = sorted(pos.items(), key=lambda x: x[1], reverse=True)
|
| 243 |
+
labels = [f"{k}" for k, v in items]
|
| 244 |
+
values = [v * 100 for k, v in items]
|
| 245 |
+
|
| 246 |
+
fig = go.Figure(go.Bar(
|
| 247 |
+
x=labels, y=values,
|
| 248 |
+
marker=dict(color=[DARK["accent"]]*3 + ["#6366f1"]*3 +
|
| 249 |
+
[DARK["green"]]*2 + ["#8b5cf6"]*2 if len(labels) > 4 else [DARK["accent"]]*len(labels)),
|
| 250 |
+
text=[f"{v:.1f}%" for v in values], textposition="outside",
|
| 251 |
+
marker_line=dict(width=0),
|
| 252 |
+
))
|
| 253 |
+
fig.update_layout(
|
| 254 |
+
**PLOTLY_LAYOUT, height=260, showlegend=False,
|
| 255 |
+
yaxis=dict(title="Weight %", ticksuffix="%", range=[0, max(values)*1.3]),
|
| 256 |
+
xaxis=dict(tickangle=-30),
|
| 257 |
+
)
|
| 258 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 259 |
+
config={"responsive": True, "displayModeBar": False})
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def build_feature_importance_chart() -> str:
|
| 263 |
+
"""Build top-10 feature importance chart."""
|
| 264 |
+
fi = _store.feature_importance
|
| 265 |
+
if not fi:
|
| 266 |
+
return "<div style='color:#64748b;text-align:center;padding:40px'>No data</div>"
|
| 267 |
+
|
| 268 |
+
items = sorted(fi.items(), key=lambda x: x[1], reverse=True)[:12]
|
| 269 |
+
labels = [k.replace("_"," ").title()[:30] for k, v in items]
|
| 270 |
+
values = [v * 100 for k, v in items]
|
| 271 |
+
|
| 272 |
+
fig = go.Figure(go.Bar(
|
| 273 |
+
y=labels[::-1], x=values[::-1], orientation="h",
|
| 274 |
+
marker=dict(color=[DARK["accent"]]*len(labels)),
|
| 275 |
+
text=[f"{v:.1f}%" for v in values[::-1]], textposition="outside",
|
| 276 |
+
))
|
| 277 |
+
fig.update_layout(**PLOTLY_LAYOUT, height=340, showlegend=False,
|
| 278 |
+
xaxis=dict(title="Importance %", ticksuffix="%"),
|
| 279 |
+
margin=dict(l=120, r=24, t=32, b=8),
|
| 280 |
+
)
|
| 281 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 282 |
+
config={"responsive": True, "displayModeBar": False})
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def build_regime_chart() -> str:
|
| 286 |
+
"""Build regime probability donut chart."""
|
| 287 |
+
rp = _store.regime_probs
|
| 288 |
+
if not rp:
|
| 289 |
+
return "<div style='color:#64748b;text-align:center;padding:40px'>Loading...</div>"
|
| 290 |
+
|
| 291 |
+
colors_map = {"bull": DARK["green"], "bear": DARK["red"],
|
| 292 |
+
"high_vol": DARK["yellow"], "neutral": "#64748b"}
|
| 293 |
+
fig = go.Figure(go.Pie(
|
| 294 |
+
labels=list(rp.keys()), values=list(rp.values()), hole=0.55,
|
| 295 |
+
marker=dict(colors=[colors_map.get(k, DARK["accent"]) for k in rp.keys()]),
|
| 296 |
+
textinfo="label+percent", textfont=dict(size=12, color=DARK["text_bright"]),
|
| 297 |
+
))
|
| 298 |
+
fig.update_layout(**PLOTLY_LAYOUT, height=260, showlegend=False)
|
| 299 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 300 |
+
config={"responsive": True, "displayModeBar": False})
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def build_sentiment_chart() -> str:
|
| 304 |
+
"""Build sentiment bar chart."""
|
| 305 |
+
ss = _store.sentiment_scores
|
| 306 |
+
if not ss:
|
| 307 |
+
return "<div style='color:#64748b;text-align:center;padding:40px'>No sentiment data</div>"
|
| 308 |
+
|
| 309 |
+
items = sorted(ss.items(), key=lambda x: abs(x[1]), reverse=True)
|
| 310 |
+
labels = [k for k, v in items]
|
| 311 |
+
values = [v for k, v in items]
|
| 312 |
+
colors = [DARK["green"] if v > 0 else DARK["red"] for v in values]
|
| 313 |
+
|
| 314 |
+
fig = go.Figure(go.Bar(
|
| 315 |
+
x=labels, y=values, marker=dict(color=colors),
|
| 316 |
+
text=[f"{v:+.2f}" for v in values], textposition="outside",
|
| 317 |
+
))
|
| 318 |
+
fig.update_layout(
|
| 319 |
+
**PLOTLY_LAYOUT, height=240, showlegend=False,
|
| 320 |
+
yaxis=dict(title="Sentiment Score", range=[-1.1, 1.1]),
|
| 321 |
+
xaxis=dict(tickangle=-30),
|
| 322 |
+
)
|
| 323 |
+
fig.add_hline(y=0, line=dict(color=DARK["text"], width=1, dash="dot"))
|
| 324 |
+
return fig.to_html(include_plotlyjs="cdn", full_html=False,
|
| 325 |
+
config={"responsive": True, "displayModeBar": False})
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ββ HTML Components ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 329 |
+
|
| 330 |
+
def _kpi_card(value: str, label: str, color: str = DARK["accent"],
|
| 331 |
+
sub: str = "", sub_color: str = "") -> str:
|
| 332 |
+
"""Render a KPI card."""
|
| 333 |
+
sub_html = f'<div style="color:{sub_color};font-size:11px;margin-top:2px">{sub}</div>' if sub else ""
|
| 334 |
+
return f'''
|
| 335 |
+
<div style="background:{DARK["card"]};border:1px solid {DARK["border"]};border-radius:10px;
|
| 336 |
+
padding:14px 18px;text-align:center;min-width:110px">
|
| 337 |
+
<div style="color:{DARK["text"]};font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px">
|
| 338 |
+
{label}
|
| 339 |
+
</div>
|
| 340 |
+
<div style="color:{color};font-size:22px;font-weight:700;line-height:1.2">{value}</div>
|
| 341 |
+
{sub_html}
|
| 342 |
+
</div>'''
|
| 343 |
+
|
| 344 |
+
def _ticker_pill(symbol: str, weight: float, pnl_pct: float = 0) -> str:
|
| 345 |
+
"""Render a ticker pill badge."""
|
| 346 |
+
pnl_color = DARK["green"] if pnl_pct >= 0 else DARK["red"]
|
| 347 |
+
pnl_sign = "+" if pnl_pct >= 0 else ""
|
| 348 |
+
return f'''
|
| 349 |
+
<div style="display:inline-block;background:{DARK["card"]};border:1px solid {DARK["border"]};
|
| 350 |
+
border-radius:8px;padding:8px 14px;margin:3px;min-width:140px">
|
| 351 |
+
<span style="color:{DARK["accent"]};font-weight:700;font-size:14px">{symbol}</span>
|
| 352 |
+
<span style="color:{DARK["text_bright"]};font-size:13px;margin-left:8px">{weight:.1%}</span>
|
| 353 |
+
<span style="color:{pnl_color};font-size:12px;margin-left:6px">{pnl_sign}{pnl_pct:.1%}</span>
|
| 354 |
+
</div>'''
|
| 355 |
+
|
| 356 |
+
def _alert_badge(level: str, title: str, text: str, time_str: str) -> str:
|
| 357 |
+
"""Render an alert badge."""
|
| 358 |
+
colors = {"error": DARK["red"], "warn": DARK["yellow"], "info": DARK["accent"]}
|
| 359 |
+
color = colors.get(level, DARK["text"])
|
| 360 |
+
return f'''
|
| 361 |
+
<div style="background:{DARK["card"]};border-left:3px solid {color};border-radius:4px;
|
| 362 |
+
padding:8px 12px;margin:4px 0">
|
| 363 |
+
<span style="color:{color};font-weight:600;font-size:12px">[{time_str}] {title}</span>
|
| 364 |
+
<span style="color:{DARK["text"]};font-size:11px;margin-left:8px">{text}</span>
|
| 365 |
+
</div>'''
|
| 366 |
+
|
| 367 |
+
def build_top_bar() -> str:
|
| 368 |
+
"""Build top status bar."""
|
| 369 |
+
snap = _store.snapshot()
|
| 370 |
+
regime_color = {"bull": DARK["green"], "bear": DARK["red"],
|
| 371 |
+
"high_vol": DARK["yellow"], "neutral": DARK["text"]}
|
| 372 |
+
rc = regime_color.get(snap.get("regime","neutral"), DARK["text"])
|
| 373 |
+
|
| 374 |
+
return f'''
|
| 375 |
+
<div style="background:{DARK["card"]};padding:10px 24px;display:flex;align-items:center;
|
| 376 |
+
justify-content:space-between;border-bottom:1px solid {DARK["border"]}">
|
| 377 |
+
<div style="display:flex;align-items:center;gap:16px">
|
| 378 |
+
<span style="color:{DARK["accent"]};font-size:20px;font-weight:800">π§ AlphaForge</span>
|
| 379 |
+
<span style="color:{DARK["text"]};font-size:10px;padding:2px 8px;background:#1e293b;border-radius:4px">v2.0 PRO</span>
|
| 380 |
+
</div>
|
| 381 |
+
<div style="display:flex;align-items:center;gap:24px">
|
| 382 |
+
<span style="color:{DARK["text"]};font-size:12px">
|
| 383 |
+
Regime: <b style="color:{rc}">{snap.get("regime","--").upper()}</b>
|
| 384 |
+
</span>
|
| 385 |
+
<span style="color:{DARK["text"]};font-size:12px">
|
| 386 |
+
Last Refresh: {datetime.now().strftime("%H:%M:%S")}
|
| 387 |
+
</span>
|
| 388 |
+
<span style="color:{DARK["green"]};font-size:10px">β LIVE</span>
|
| 389 |
+
</div>
|
| 390 |
+
</div>'''
|
| 391 |
+
|
| 392 |
+
def build_kpi_row() -> str:
|
| 393 |
+
"""Build row of KPI cards."""
|
| 394 |
+
snap = _store.snapshot()
|
| 395 |
+
pnl = snap.get("pnl", [])
|
| 396 |
+
last_pnl = pnl[-1] if pnl else 0
|
| 397 |
+
return (
|
| 398 |
+
_kpi_card(f"${last_pnl:,.0f}", "Cumulative PnL", DARK["accent"]) +
|
| 399 |
+
_kpi_card(f"{snap.get('sharpe',0):.2f}", "Sharpe", DARK["green"] if snap.get("sharpe",0) >= 1 else DARK["yellow"]) +
|
| 400 |
+
_kpi_card(f"{snap.get('sortino',0):.2f}", "Sortino", DARK["accent"]) +
|
| 401 |
+
_kpi_card(f"{snap.get('max_dd',0)*100:.1f}%", "Max Drawdown", DARK["red"]) +
|
| 402 |
+
_kpi_card(f"${snap.get('var_95',0):,.0f}", "VaR 95%", DARK["yellow"]) +
|
| 403 |
+
_kpi_card(f"{snap.get('calmar',0):.2f}", "Calmar", DARK["accent"]) +
|
| 404 |
+
_kpi_card(f"{snap.get('alpha',0)*100:.1f}%", "Alpha", DARK["green"] if snap.get("alpha",0) >= 0 else DARK["red"]) +
|
| 405 |
+
_kpi_card(f"{snap.get('beta',0):.2f}", "Beta", DARK["accent"])
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
def build_positions_html() -> str:
|
| 409 |
+
"""Build positions ticker pills."""
|
| 410 |
+
pos = _store.positions
|
| 411 |
+
if not pos:
|
| 412 |
+
return "<p style='color:#64748b'>No positions</p>"
|
| 413 |
+
items = sorted(pos.items(), key=lambda x: x[1], reverse=True)
|
| 414 |
+
return "<div style='display:flex;flex-wrap:wrap;gap:4px'>" + \
|
| 415 |
+
"".join(_ticker_pill(k, v, np.random.normal(0, 0.03)) for k, v in items) + "</div>"
|
| 416 |
+
|
| 417 |
+
def build_alerts_html() -> str:
|
| 418 |
+
"""Build alerts feed."""
|
| 419 |
+
alerts = _store.alerts
|
| 420 |
+
return "".join(_alert_badge(a["level"], a["title"], a["text"], a["time"]) for a in alerts)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# ββ Dashboard App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
|
| 425 |
+
custom_css = """
|
| 426 |
+
.gradio-container { max-width: 1400px !important; margin: 0 auto; }
|
| 427 |
+
footer { display: none !important; }
|
| 428 |
+
.tab-nav { margin-bottom: 12px !important; }
|
| 429 |
+
"""
|
| 430 |
+
|
| 431 |
+
with gr.Blocks(
|
| 432 |
+
title="AlphaForge Pro Dashboard",
|
| 433 |
+
css=custom_css,
|
| 434 |
+
theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
|
| 435 |
+
fill_width=True,
|
| 436 |
+
) as demo:
|
| 437 |
+
|
| 438 |
+
# Timer for auto-refresh (every 5 seconds)
|
| 439 |
+
timer = gr.Timer(5)
|
| 440 |
+
|
| 441 |
+
# Top status bar
|
| 442 |
+
top_bar = gr.HTML(value=build_top_bar(), every=5)
|
| 443 |
+
|
| 444 |
+
with gr.Row():
|
| 445 |
+
with gr.Column(scale=1, min_width=220):
|
| 446 |
+
gr.Markdown("### π KPIs")
|
| 447 |
+
kpi_html = gr.HTML(value=build_kpi_row, every=5)
|
| 448 |
+
|
| 449 |
+
gr.Markdown("### πΌ Positions")
|
| 450 |
+
positions_html = gr.HTML(value=build_positions_html, every=5)
|
| 451 |
+
|
| 452 |
+
gr.Markdown("### π― Regime")
|
| 453 |
+
regime_chart = gr.HTML(value=build_regime_chart, every=5)
|
| 454 |
+
|
| 455 |
+
with gr.Column(scale=4):
|
| 456 |
+
with gr.Tabs():
|
| 457 |
+
with gr.Tab("π PnL & Risk"):
|
| 458 |
+
with gr.Row():
|
| 459 |
+
pnl_chart = gr.HTML(value=build_pnl_chart, every=5)
|
| 460 |
+
with gr.Row():
|
| 461 |
+
risk_chart = gr.HTML(value=build_risk_chart, every=5)
|
| 462 |
+
|
| 463 |
+
with gr.Tab("βοΈ Weights & Factors"):
|
| 464 |
+
with gr.Row():
|
| 465 |
+
with gr.Column(scale=1):
|
| 466 |
+
gr.Markdown("### Portfolio Weights")
|
| 467 |
+
weights_chart = gr.HTML(value=build_weights_chart, every=5)
|
| 468 |
+
with gr.Column(scale=1):
|
| 469 |
+
gr.Markdown("### Feature Importance")
|
| 470 |
+
feature_chart = gr.HTML(value=build_feature_importance_chart, every=5)
|
| 471 |
+
|
| 472 |
+
with gr.Tab("ποΈ Sentiment"):
|
| 473 |
+
with gr.Row():
|
| 474 |
+
sentiment_chart = gr.HTML(value=build_sentiment_chart, every=5)
|
| 475 |
+
|
| 476 |
+
with gr.Tab("β οΈ Alerts & Logs"):
|
| 477 |
+
alerts_html = gr.HTML(value=build_alerts_html, every=5)
|
| 478 |
+
|
| 479 |
+
# Auto refresh hook
|
| 480 |
+
@gr.on(triggers=[demo.load, timer.tick], outputs=[top_bar, kpi_html, positions_html,
|
| 481 |
+
regime_chart, pnl_chart, risk_chart, weights_chart, feature_chart,
|
| 482 |
+
sentiment_chart, alerts_html])
|
| 483 |
+
def refresh_all():
|
| 484 |
+
return (build_top_bar(), build_kpi_row(), build_positions_html(),
|
| 485 |
+
build_regime_chart(), build_pnl_chart(), build_risk_chart(),
|
| 486 |
+
build_weights_chart(), build_feature_importance_chart(),
|
| 487 |
+
build_sentiment_chart(), build_alerts_html())
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
if __name__ == "__main__":
|
| 491 |
+
# Generate synthetic data for demo
|
| 492 |
+
_generate_synthetic_run()
|
| 493 |
+
demo.launch()
|