Spaces:
Sleeping
Sleeping
| import numpy as np | |
| def compute_var(returns, alpha=0.95): | |
| return float(-np.percentile(returns, (1.0 - alpha) * 100.0)) | |
| def compute_cvar(returns, alpha=0.95): | |
| cutoff = np.percentile(returns, (1.0 - alpha) * 100.0) | |
| tail_losses = -returns[returns < cutoff] | |
| if len(tail_losses) == 0: | |
| return compute_var(returns, alpha) | |
| return float(np.mean(tail_losses)) | |
| def build_cvar_garch_html(returns_df, w_risky, cfg, model_info): | |
| cvar_garch_html = "" | |
| _rm = returns_df.values if returns_df is not None else None | |
| _wa = w_risky.reindex(returns_df.columns).fillna(0.0).values if returns_df is not None else None | |
| if _rm is not None and (cfg.get('cvar_enabled') or cfg.get('garch_enabled')): | |
| cvar_rows = ""; garch_rows = "" | |
| if cfg.get('cvar_enabled'): | |
| _a = cfg.get('cvar_alpha', 0.95) | |
| _port_rets = _rm @ _wa | |
| cvar_rows = ( | |
| f"<tr><td>1-Day VaR ({_a:.0%})</td>" | |
| f"<td style='color:#e3b341'>{compute_var(_port_rets, _a):.2%}</td></tr>" | |
| f"<tr><td>1-Day CVaR ({_a:.0%})</td>" | |
| f"<td style='color:#f85149'>{compute_cvar(_port_rets, _a):.2%}</td></tr>" | |
| ) | |
| gi = model_info.get('garch_info', {}) | |
| if cfg.get('garch_enabled') and gi: | |
| for t, gd in gi.items(): | |
| if t == 'CASH': continue | |
| ratio = gd['garch_ann_vol'] / max(gd['hist_ann_vol'], 0.001) | |
| rc = "#f85149" if ratio > 1.15 else ("#3fb950" if ratio < 0.90 else "#e3b341") | |
| badge = "▲ Elevated" if ratio > 1.15 else ("▼ Compressed" if ratio < 0.90 else "≈ Normal") | |
| garch_rows += ( | |
| f"<tr><td><strong>{t}</strong></td>" | |
| f"<td>{gd['hist_ann_vol']:.1%}</td>" | |
| f"<td style='color:{rc}'>{gd['garch_ann_vol']:.1%}</td>" | |
| f"<td>{gd['persistence']:.3f}</td>" | |
| f"<td style='color:{rc}'>{badge}</td></tr>\n" | |
| ) | |
| if cvar_rows or garch_rows: | |
| cvar_garch_html = '<p class="st">CVaR & GARCH Volatility Regime</p><div class="two">' | |
| if cvar_rows: | |
| cvar_garch_html += ( | |
| '<div class="cc"><p style="font-size:.8rem;color:#8b949e;margin-bottom:8px">' | |
| '<strong>Tail Risk</strong></p>' | |
| f'<table><tbody>{cvar_rows}</tbody></table></div>' | |
| ) | |
| if garch_rows: | |
| cvar_garch_html += ( | |
| '<div class="cc" style="overflow-x:auto">' | |
| '<p style="font-size:.8rem;color:#8b949e;margin-bottom:8px">' | |
| '<strong>GARCH(1,1) Forecasts</strong></p>' | |
| '<table><thead><tr><th>Ticker</th><th>Hist</th><th>GARCH</th>' | |
| f'<th>Persist.</th><th>Regime</th></tr></thead>' | |
| f'<tbody>{garch_rows}</tbody></table></div>' | |
| ) | |
| cvar_garch_html += '</div>' | |
| return cvar_garch_html | |
| def build_risk_attr_html(mvar_series, cvar_components, factor_ret_attr): | |
| risk_attr_html = "" | |
| _has_risk_attr = any(x is not None for x in [mvar_series, cvar_components, factor_ret_attr]) | |
| if _has_risk_attr: | |
| ra_parts = [] | |
| if mvar_series is not None and not mvar_series.empty: | |
| mvar_rows = "" | |
| total_mvar = float(mvar_series.abs().sum()) or 1.0 | |
| for ticker, mv in mvar_series.sort_values(ascending=False).items(): | |
| mv_f = float(mv) | |
| mv_col = "#f85149" if mv_f > 0 else "#3fb950" | |
| pct_bar = abs(mv_f) / total_mvar | |
| bar_w = round(pct_bar * 80, 1) | |
| mvar_rows += ( | |
| f"<tr><td><strong>{ticker}</strong></td>" | |
| f"<td style='color:{mv_col};font-weight:700'>{mv_f*100:+.3f}%</td>" | |
| f"<td><div style='background:{mv_col};width:{bar_w}px;height:8px;border-radius:2px;opacity:.75'></div></td></tr>\n" | |
| ) | |
| ra_parts.append( | |
| '<div class="cc" style="overflow-x:auto"><p style="font-size:.8rem;color:#8b949e;margin-bottom:8px">' | |
| '<strong>Marginal VaR (95%)</strong> <span style="font-weight:400">— how much each asset adds to portfolio VaR for a 1% increase in its weight</span></p>' | |
| f'<table><thead><tr><th>Ticker</th><th>MVaR</th><th>Relative Size</th></tr></thead><tbody>{mvar_rows}</tbody></table></div>' | |
| ) | |
| if cvar_components is not None: | |
| comp_series, total_cvar = cvar_components | |
| if hasattr(comp_series, 'items') and total_cvar > 0: | |
| cvar_rows = "" | |
| for ticker, cv in comp_series.sort_values(ascending=False).items(): | |
| cv_f = float(cv) | |
| share = cv_f / total_cvar if total_cvar else 0.0 | |
| cv_col = "#f85149" if cv_f > 0 else "#3fb950" | |
| bar_w = round(abs(share) * 80, 1) | |
| cvar_rows += ( | |
| f"<tr><td><strong>{ticker}</strong></td>" | |
| f"<td style='color:{cv_col};font-weight:700'>{cv_f*100:+.3f}%</td>" | |
| f"<td style='color:#8b949e'>{share:.1%}</td>" | |
| f"<td><div style='background:{cv_col};width:{bar_w}px;height:8px;border-radius:2px;opacity:.75'></div></td></tr>\n" | |
| ) | |
| ra_parts.append( | |
| '<div class="cc" style="overflow-x:auto"><p style="font-size:.8rem;color:#8b949e;margin-bottom:8px">' | |
| f'<strong>CVaR Component Attribution</strong> <span style="font-weight:400">— each asset\'s contribution to the portfolio\'s expected loss on its worst days (total CVaR: <span style="color:#f85149">{total_cvar*100:.2f}%</span>)</span></p>' | |
| f'<table><thead><tr><th>Ticker</th><th>Component CVaR</th><th>Share of Total</th><th>Relative Size</th></tr></thead><tbody>{cvar_rows}</tbody></table></div>' | |
| ) | |
| if factor_ret_attr: | |
| fat_rows = "" | |
| tot_ret = factor_ret_attr["total_return"] | |
| fat_rows += f"<tr><td><strong>Total Return</strong></td><td colspan='2' style='font-weight:700'>{tot_ret*100:+.2f}%</td></tr>" | |
| for f_name, f_val in factor_ret_attr["factor_contributions"].items(): | |
| f_col = "#3fb950" if f_val > 0 else "#f85149" | |
| fat_rows += f"<tr><td>{f_name} Contribution</td><td style='color:{f_col}'>{f_val*100:+.2f}%</td><td></td></tr>" | |
| alpha_val = factor_ret_attr["alpha"] | |
| a_col = "#3fb950" if alpha_val > 0 else "#f85149" | |
| fat_rows += f"<tr><td><strong>Alpha (Idiosyncratic)</strong></td><td style='color:{a_col};font-weight:700'>{alpha_val*100:+.2f}%</td><td></td></tr>" | |
| ra_parts.append( | |
| '<div class="cc" style="overflow-x:auto"><p style="font-size:.8rem;color:#8b949e;margin-bottom:8px">' | |
| f'<strong>Factor-based Return Attribution (BHB Proxy)</strong></p>' | |
| f'<table><thead><tr><th>Factor</th><th>Contribution to Return</th><th></th></tr></thead><tbody>{fat_rows}</tbody></table></div>' | |
| ) | |
| if ra_parts: | |
| risk_attr_html = '<p class="st">Risk Attribution</p>' + "\n".join(ra_parts) | |
| return risk_attr_html | |
| def build_rc_html(model_info, weights, chart_data): | |
| rc_html = "" | |
| rc_series = model_info.get("risk_contributions") | |
| if rc_series is not None and not rc_series.empty: | |
| is_ml = ("XGBoost" in model_info.get('name', '')) | |
| rc_note = (" — <em>Machine Learning Asset Sizing</em>" if is_ml else "") | |
| rc_rows = "" | |
| for t in rc_series.index: | |
| rc_pct = float(rc_series[t])*100 | |
| w_pct = float(weights.get(t,0))*100 | |
| diff = rc_pct - w_pct | |
| d_col = "#f85149" if diff > 5 else ("#e3b341" if diff > 1 else "#3fb950") | |
| rc_rows += ( | |
| f"<tr><td><strong>{t}</strong></td>" | |
| f"<td>{w_pct:.1f}%</td>" | |
| f"<td style='color:{d_col}'>{rc_pct:.1f}%</td>" | |
| f"<td style='color:{d_col}'>{diff:+.1f}%</td></tr>\n" | |
| ) | |
| rc_html = ( | |
| f'<p class="st">Risk Contribution{rc_note}</p>' | |
| '<div class="two"><div class="cc" style="overflow-x:auto">' | |
| '<table><thead><tr><th>Ticker</th><th>Weight</th>' | |
| '<th>Risk Contrib.</th><th>Δ</th></tr></thead>' | |
| f'<tbody>{rc_rows}</tbody></table></div>' | |
| '<div class="cc"><canvas id="cj_rc"></canvas></div></div>' | |
| ) | |
| # Inject RC Chart Data into Chart.js | |
| _r = lambda x: round(x, 4) | |
| rc_labels = list(rc_series.index) | |
| rc_colors = ["#f85149" if (float(rc_series[t])*100 - float(weights.get(t,0))*100) > 5 else ("#e3b341" if (float(rc_series[t])*100 - float(weights.get(t,0))*100) > 1 else "#3fb950") for t in rc_labels] | |
| chart_data["rc_labels"] = rc_labels | |
| chart_data["rc_ds"] = [ | |
| {"label":"Risk Contrib %","data":[_r(float(rc_series[t])*100) for t in rc_labels],"backgroundColor":rc_colors,"borderRadius":4}, | |
| {"label":"Weight %", "data":[_r(float(weights.get(t,0))*100) for t in rc_labels],"backgroundColor":"rgba(88,166,255,0.4)","borderRadius":4} | |
| ] | |
| return rc_html | |