Source code for scr_financial.risk.metrics

"""
Systemic risk metrics: CoVaR, ΔCoVaR, MES, SRISK.

These implement the standard tail-risk measures used as benchmarks against
the Spectral Coarse-Graining (SCG) risk indicator.

References:
  - Adrian & Brunnermeier (2016) — CoVaR
  - Acharya et al. (2017) — MES / SRISK
"""

from __future__ import annotations

import logging
from typing import Dict, List, Optional

import numpy as np

logger = logging.getLogger(__name__)


[docs] def compute_var(returns: np.ndarray, alpha: float = 0.05) -> float: """Value-at-Risk at level alpha (left tail).""" return float(np.percentile(returns, alpha * 100))
[docs] def compute_covar( bank_returns: np.ndarray, system_returns: np.ndarray, alpha: float = 0.05, ) -> float: """CoVaR: VaR of a bank conditional on the system being at its VaR. Uses the historical simulation approach: subset bank returns to periods where system return <= system VaR, then take the alpha-quantile. """ sys_var = compute_var(system_returns, alpha) # Periods when system is in distress mask = system_returns <= sys_var if mask.sum() < 3: return compute_var(bank_returns, alpha) return float(np.percentile(bank_returns[mask], alpha * 100))
[docs] def compute_delta_covar( bank_returns: np.ndarray, system_returns: np.ndarray, alpha: float = 0.05, ) -> float: """ΔCoVaR: difference between CoVaR under stress and CoVaR at median. Positive ΔCoVaR means the bank contributes more risk when the system is stressed. """ covar_stress = compute_covar(bank_returns, system_returns, alpha) # CoVaR at median state: system near its 50th percentile (±5pp) sys_med = np.median(system_returns) band = np.percentile(np.abs(system_returns - sys_med), 10) mask_median = np.abs(system_returns - sys_med) <= max(band, 1e-10) if mask_median.sum() < 3: covar_median = compute_var(bank_returns, 0.5) else: covar_median = float(np.percentile(bank_returns[mask_median], alpha * 100)) return covar_stress - covar_median
[docs] def compute_mes( bank_returns: np.ndarray, system_returns: np.ndarray, alpha: float = 0.05, ) -> float: """Marginal Expected Shortfall: E[r_bank | r_system < VaR_alpha(system)]. Measures a bank's expected loss conditional on a systemic tail event. """ sys_var = compute_var(system_returns, alpha) mask = system_returns <= sys_var if mask.sum() < 1: return 0.0 return float(np.mean(bank_returns[mask]))
[docs] def compute_srisk( mes: float, equity: float, liabilities: float, k: float = 0.08, ) -> float: """SRISK: expected capital shortfall of a bank in a systemic crisis. SRISK = max(0, k*(equity + liabilities) - equity*(1 + MES)) where k is the prudential capital ratio (default 8%). """ return max(0.0, k * (equity + liabilities) - equity * (1.0 + mes))
[docs] def compute_system_risk_metrics( bank_returns_dict: Dict[str, np.ndarray], alpha: float = 0.05, ) -> Dict[str, Dict[str, float]]: """Compute all risk metrics for a set of banks. Parameters ---------- bank_returns_dict : dict Maps bank_id → array of returns. alpha : float Tail probability. Returns ------- dict mapping bank_id → {var, covar, delta_covar, mes} """ # System return = equal-weighted average of all bank returns all_returns = np.array(list(bank_returns_dict.values())) system_returns = np.mean(all_returns, axis=0) results = {} for bank_id, returns in bank_returns_dict.items(): results[bank_id] = { "var": compute_var(returns, alpha), "covar": compute_covar(returns, system_returns, alpha), "delta_covar": compute_delta_covar(returns, system_returns, alpha), "mes": compute_mes(returns, system_returns, alpha), } return results