Source code for wraquant.risk.scenarios
"""Scenario analysis and Monte Carlo simulation for portfolio risk."""
from __future__ import annotations
import numpy as np
import pandas as pd
[docs]
def monte_carlo_var(
returns: pd.DataFrame,
weights: np.ndarray,
n_sims: int = 10000,
confidence: float = 0.95,
) -> float:
"""Estimate portfolio VaR via Monte Carlo simulation.
Draws from a multivariate normal distribution fitted to historical
returns.
Parameters:
returns: DataFrame of asset returns (columns = assets).
weights: Portfolio weight vector.
n_sims: Number of simulation draws.
confidence: Confidence level.
Returns:
VaR as a positive float.
"""
from wraquant.core._coerce import coerce_array
weights = coerce_array(weights, name="weights")
mu = returns.mean().values
cov = returns.cov().values
rng = np.random.default_rng()
sims = rng.multivariate_normal(mu, cov, size=n_sims)
portfolio_returns = sims @ weights
var_quantile = np.percentile(portfolio_returns, (1 - confidence) * 100)
return float(-var_quantile)
[docs]
def stress_test(
returns: pd.DataFrame,
weights: np.ndarray,
shocks: dict[str, float],
) -> float:
"""Compute the portfolio loss under a stress scenario.
Each entry in *shocks* maps an asset name to a shocked return.
Assets not in *shocks* use their historical mean.
Parameters:
returns: DataFrame of asset returns (columns = assets).
weights: Portfolio weight vector.
shocks: Mapping of asset name to shocked return value.
Returns:
Portfolio return under the stress scenario (typically negative).
"""
from wraquant.core._coerce import coerce_array
weights = coerce_array(weights, name="weights")
scenario = returns.mean().copy()
for asset, shock_val in shocks.items():
if asset in scenario.index:
scenario[asset] = shock_val
return float(scenario.values @ weights)