math-backend / report_builders /html_risk.py
engineportf's picture
Upload folder using huggingface_hub
558db1e verified
Raw
History Blame Contribute Delete
9.56 kB
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 &amp; 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> &nbsp;<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> &nbsp;<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