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 += (
'
'
)
if garch_rows:
cvar_garch_html += (
'
'
'
'
'GARCH(1,1) Forecasts
'
'
| Ticker | Hist | GARCH | '
f'Persist. | Regime |
'
f'{garch_rows}
'
)
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'
| Ticker | MVaR | Relative Size |
{mvar_rows}
'
)
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'
| Ticker | Component CVaR | Share of Total | Relative Size |
{cvar_rows}
'
)
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'
| Factor | Contribution to Return | |
{fat_rows}
'
)
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}
'
''
'
| Ticker | Weight | '
'Risk Contrib. | Δ |
'
f'{rc_rows}
'
'
'
)
# 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