Source code for pyvallocation.utils.functions

"""Portfolio Value-at-Risk (VaR) and Conditional VaR (CVaR) functions."""

from __future__ import annotations
import numpy as np
import pandas as pd

[docs] def portfolio_cvar( w: np.ndarray, R: np.ndarray | pd.DataFrame, p: np.ndarray | None = None, alpha: float | None = None, demean: bool | None = None, ) -> float | np.ndarray: """ Computes portfolio Conditional Value-at-Risk (CVaR or Expected Shortfall). Args: w: Portfolio weights matrix (N, M). N=instruments, M=portfolios. R: Instrument P&L or returns matrix (T, N). T=scenarios. p: Scenario probability vector (T, 1). Defaults to uniform. alpha: Confidence level for CVaR. Defaults to 0.95. demean: If True, uses demeaned P&L. Defaults to False. Returns: The portfolio's alpha-CVaR, returned as a positive float or a 1xM array. """ alpha = 0.95 if alpha is None else float(alpha) if not 0.0 < alpha < 1.0: raise ValueError("alpha must be a float in the interval (0, 1).") if demean is None: demean = False elif not isinstance(demean, bool): raise ValueError("demean must be either True or False.") R_arr = np.asarray(R, dtype=float) p = np.full((R_arr.shape[0], 1), 1.0 / R_arr.shape[0]) if p is None else np.asarray(p, dtype=float).reshape(-1, 1) if demean: R_arr -= p.T @ R_arr pf_pnl = R_arr @ w if pf_pnl.ndim == 1: pf_pnl = pf_pnl.reshape(-1, 1) order = np.argsort(pf_pnl, axis=0) sorted_pnl = np.take_along_axis(pf_pnl, order, axis=0) sorted_p = np.take_along_axis(np.broadcast_to(p, pf_pnl.shape), order, axis=0) var_indices = (np.cumsum(sorted_p, axis=0) >= (1.0 - alpha)).argmax(axis=0) var = np.take_along_axis(sorted_pnl, var_indices[np.newaxis, :], axis=0) tail_mask = pf_pnl <= var denominator = np.sum(p * tail_mask, axis=0) numerator = np.sum(p * pf_pnl * tail_mask, axis=0) cvar = np.full_like(denominator, np.nan) np.divide(numerator, denominator, out=cvar, where=denominator != 0) risk = -cvar.reshape(1, -1) return risk.item() if risk.size == 1 else risk
[docs] def portfolio_var( w: np.ndarray, R: np.ndarray | pd.DataFrame, p: np.ndarray | None = None, alpha: float | None = None, demean: bool | None = None, ) -> float | np.ndarray: """ Computes portfolio Value-at-Risk (VaR). Args: w: Portfolio weights matrix (N, M). N=instruments, M=portfolios. R: Instrument P&L or returns matrix (T, N). T=scenarios. p: Scenario probability vector (T, 1). Defaults to uniform. alpha: Confidence level for VaR. Defaults to 0.95. demean: If True, uses demeaned P&L. Defaults to False. Returns: The portfolio's alpha-VaR, returned as a positive float or a 1xM array. """ alpha = 0.95 if alpha is None else float(alpha) if not 0.0 < alpha < 1.0: raise ValueError("alpha must be a float in the interval (0, 1).") if demean is None: demean = False elif not isinstance(demean, bool): raise ValueError("demean must be either True or False.") R_arr = np.asarray(R, dtype=float) p = np.full((R_arr.shape[0], 1), 1.0 / R_arr.shape[0]) if p is None else np.asarray(p, dtype=float).reshape(-1, 1) if demean: R_arr -= p.T @ R_arr pf_pnl = R_arr @ w if pf_pnl.ndim == 1: pf_pnl = pf_pnl.reshape(-1, 1) order = np.argsort(pf_pnl, axis=0) sorted_pnl = np.take_along_axis(pf_pnl, order, axis=0) sorted_p = np.take_along_axis(np.broadcast_to(p, pf_pnl.shape), order, axis=0) var_indices = (np.cumsum(sorted_p, axis=0) >= (1.0 - alpha)).argmax(axis=0) var = np.take_along_axis(sorted_pnl, var_indices[np.newaxis, :], axis=0) risk = -var return risk.item() if risk.size == 1 else risk