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"1-Day VaR ({_a:.0%})" f"{compute_var(_port_rets, _a):.2%}" f"1-Day CVaR ({_a:.0%})" f"{compute_cvar(_port_rets, _a):.2%}" ) 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"{t}" f"{gd['hist_ann_vol']:.1%}" f"{gd['garch_ann_vol']:.1%}" f"{gd['persistence']:.3f}" f"{badge}\n" ) if cvar_rows or garch_rows: cvar_garch_html = '

CVaR & GARCH Volatility Regime

' if cvar_rows: cvar_garch_html += ( '

' 'Tail Risk

' f'{cvar_rows}
' ) if garch_rows: cvar_garch_html += ( '
' '

' 'GARCH(1,1) Forecasts

' '' f'' f'{garch_rows}
TickerHistGARCHPersist.Regime
' ) cvar_garch_html += '
' 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"{ticker}" f"{mv_f*100:+.3f}%" f"
\n" ) ra_parts.append( '

' 'Marginal VaR (95%)  — how much each asset adds to portfolio VaR for a 1% increase in its weight

' f'{mvar_rows}
TickerMVaRRelative Size
' ) 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"{ticker}" f"{cv_f*100:+.3f}%" f"{share:.1%}" f"
\n" ) ra_parts.append( '

' f'CVaR Component Attribution  — each asset\'s contribution to the portfolio\'s expected loss on its worst days (total CVaR: {total_cvar*100:.2f}%)

' f'{cvar_rows}
TickerComponent CVaRShare of TotalRelative Size
' ) if factor_ret_attr: fat_rows = "" tot_ret = factor_ret_attr["total_return"] fat_rows += f"Total Return{tot_ret*100:+.2f}%" for f_name, f_val in factor_ret_attr["factor_contributions"].items(): f_col = "#3fb950" if f_val > 0 else "#f85149" fat_rows += f"{f_name} Contribution{f_val*100:+.2f}%" alpha_val = factor_ret_attr["alpha"] a_col = "#3fb950" if alpha_val > 0 else "#f85149" fat_rows += f"Alpha (Idiosyncratic){alpha_val*100:+.2f}%" ra_parts.append( '

' f'Factor-based Return Attribution (BHB Proxy)

' f'{fat_rows}
FactorContribution to Return
' ) if ra_parts: risk_attr_html = '

Risk Attribution

' + "\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 = (" — Machine Learning Asset Sizing" 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"{t}" f"{w_pct:.1f}%" f"{rc_pct:.1f}%" f"{diff:+.1f}%\n" ) rc_html = ( f'

Risk Contribution{rc_note}

' '
' '' '' f'{rc_rows}
TickerWeightRisk Contrib.Δ
' '
' ) # 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