
Forward Risk
- 390 installs
- 161 repo stars
- Updated July 18, 2026
- joellewis/finance_skills
Model forward-looking market, credit, or portfolio risk before pricing products, hedging exposure, or approving trades and capital allocations.
About
The forward-risk skill from joellewis/finance_skills helps agents evaluate prospective financial exposure using forward-looking risk models, stress paths, and pricing implications before trades, launches, or commitments are finalized.
- Forward risk scenario modeling
- Stress and sensitivity analysis
- Pricing and hedge alignment
- Capital and exposure decisions
- Finance workflow guardrails
Forward Risk by the numbers
- 390 all-time installs (skills.sh)
- +15 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #275 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joellewis/finance_skills --skill forward-riskAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 390 |
|---|---|
| repo stars | ★ 161 |
| Last updated | July 18, 2026 |
| Repository | joellewis/finance_skills ↗ |
What it does
Model forward-looking market, credit, or portfolio risk before pricing products, hedging exposure, or approving trades and capital allocations.
Files
Forward-Looking Risk Analysis
Core Concepts
Parametric (Variance-Covariance) VaR
Assumes returns are normally distributed. For a single asset or portfolio in dollar terms (assuming zero expected return over short horizons):
VaR = W * z_alpha * sigma_pwhere:
- W = portfolio value
- z_alpha = z-score for confidence level (1.645 for 95%, 2.326 for 99%)
- sigma_p = portfolio volatility over the relevant horizon
More generally, including expected return:
VaR_alpha = mu - z_alpha * sigmaTo convert from 1-day VaR to h-day VaR (assuming i.i.d. returns):
VaR_h = VaR_1 * sqrt(h)Portfolio VaR (Multiple Assets)
For a portfolio with weight vector w and covariance matrix Sigma:
sigma_p = sqrt(w' * Sigma * w)
VaR_p = W * z_alpha * sqrt(w' * Sigma * w)The covariance matrix captures both individual volatilities and correlations between assets.
Monte Carlo VaR
Simulate a large number of portfolio return scenarios (e.g., 10,000+), then take the alpha-percentile of the simulated loss distribution.
Steps: 1. Estimate the return distribution parameters (mean vector, covariance matrix, or use a copula model). 2. Generate N random return scenarios (e.g., via Cholesky decomposition of the covariance matrix for multivariate normal). 3. Compute portfolio return for each scenario. 4. Sort results and identify the alpha-percentile loss.
Monte Carlo VaR can accommodate non-normal distributions, fat tails, path-dependent instruments, and nonlinear payoffs (e.g., options).
Conditional VaR (CVaR) / Expected Shortfall
CVaR answers: "Given that losses exceed VaR, what is the expected loss?"
ES_alpha = E[Loss | Loss > VaR_alpha]For a normal distribution:
ES_alpha = mu + sigma * phi(z_alpha) / (1 - alpha)where phi is the standard normal PDF.
CVaR is a coherent risk measure (unlike VaR) because it satisfies subadditivity: CVaR(A+B) <= CVaR(A) + CVaR(B). This means diversification always reduces or maintains CVaR, which is not guaranteed for VaR.
Component VaR
Decomposes total portfolio VaR into contributions from each position. Component VaRs sum to total VaR.
CVaR_i = w_i * beta_i * VaR_pwhere beta_i = Cov(R_i, R_p) / Var(R_p) is the asset's beta to the portfolio.
Equivalently:
CVaR_i = w_i * (partial VaR / partial w_i)
sum(CVaR_i) = VaR_pThis decomposition identifies which positions are the largest contributors to portfolio risk.
Marginal VaR
Measures the rate of change of portfolio VaR with respect to a small increase in a position's weight.
MVaR_i = partial(VaR_p) / partial(w_i) = z_alpha * (Sigma * w)_i / sigma_pMarginal VaR is used for position sizing: adding to a position with low marginal VaR reduces portfolio risk more efficiently.
Scenario Analysis
Apply specific historical or hypothetical market moves to the current portfolio to estimate P&L impact.
- Historical scenarios: Replay actual market events (e.g., 2008 GFC, 2020 COVID crash, 2022 rate hiking cycle) with current holdings.
- Hypothetical scenarios: Construct custom shocks (e.g., "equities -20%, rates +200bp, credit spreads +300bp, USD +10%").
Scenario P&L is computed by applying the scenario returns to current position exposures and revaluing.
Stress Testing
A structured framework for assessing portfolio resilience under extreme but plausible conditions.
Common stress scenarios:
- Equity crash: S&P 500 -30% to -40%
- Interest rate shock: +300bp parallel shift
- Credit crisis: investment-grade spreads +200bp, high-yield +800bp
- Liquidity freeze: bid-ask spreads widen 10x, forced selling at discount
- Currency shock: major currency pair moves 15-20%
- Stagflation: inflation +5%, GDP -3%, rates +200bp
Stress tests should include second-order effects: margin calls, liquidity demands, correlation spikes, counterparty risk.
Factor-Based Risk Decomposition
Separate total portfolio risk into systematic factor risk and idiosyncratic (security-specific) risk.
sigma^2_p = b' * Sigma_f * b + sum(w_i^2 * sigma^2_epsilon_i)where:
- b = vector of portfolio factor exposures
- Sigma_f = factor covariance matrix
- sigma^2_epsilon_i = idiosyncratic variance of asset i
Common factor models: Fama-French (market, size, value, momentum), Barra risk models, PCA-based statistical factors.
Key Formulas
| Formula | Expression | Use Case |
|---|---|---|
| Parametric VaR (single) | W z_alpha sigma | Simple position VaR |
| Portfolio VaR | W z_alpha sqrt(w' Sigma w) | Multi-asset VaR |
| Multi-day VaR | VaR_1 * sqrt(h) | Scale to h-day horizon |
| CVaR (normal) | mu + sigma * phi(z_alpha) / (1 - alpha) | Expected tail loss |
| Component VaR | w_i beta_i VaR_p | Risk contribution per position |
| Marginal VaR | z_alpha (Sigma w)_i / sigma_p | Sensitivity to weight change |
| Factor Risk | b' Sigma_f b | Systematic risk component |
| Idiosyncratic Risk | sum(w_i^2 * sigma^2_epsilon_i) | Security-specific risk |
Worked Examples
Example 1: Parametric 95% VaR
Given: A $1,000,000 equity portfolio with an annualized volatility of 15%.
Calculate: 1-day 95% parametric VaR (assuming 252 trading days and zero expected daily return).
Solution:
Daily volatility:
sigma_daily = 0.15 / sqrt(252) = 0.15 / 15.875 = 0.009451-day 95% VaR:
VaR = $1,000,000 * 1.645 * 0.00945 = $15,545Alternatively, computing directly from annual figures:
VaR_annual = $1,000,000 * 1.645 * 0.15 = $246,750
VaR_1day = $246,750 / sqrt(252) = $15,545Interpretation: There is a 5% chance of losing more than $15,545 in a single day under normal market conditions.
Example 2: Monte Carlo VaR
Given: A two-asset portfolio (60% equities, 40% bonds). Equities: mu = 10%, sigma = 18%. Bonds: mu = 4%, sigma = 5%. Correlation rho = -0.2. Portfolio value = $1,000,000.
Calculate: 95% annual VaR via Monte Carlo simulation (conceptual steps).
Solution:
1. Construct covariance matrix:
Sigma = | 0.0324 -0.0018 |
| -0.0018 0.0025 |2. Cholesky decomposition of Sigma to get lower triangular matrix L.
3. Simulate 10,000 scenarios: For each simulation, draw z ~ N(0, I), compute r = mu + Lz, then portfolio return R_p = w' r.
4. Compute portfolio P&L for each scenario: P&L = $1,000,000 * R_p.
5. Sort P&L from worst to best. The 500th worst (5th percentile) is the 95% VaR.
For this portfolio, the analytical answer provides a benchmark:
sigma_p = sqrt(0.6^2 * 0.0324 + 0.4^2 * 0.0025 + 2 * 0.6 * 0.4 * (-0.0018))
= sqrt(0.011664 + 0.0004 - 0.000864)
= sqrt(0.0112)
= 10.58%
VaR_95% = $1,000,000 * 1.645 * 0.1058 = $174,090The Monte Carlo result should converge to approximately this value for a multivariate normal assumption.
Example 3: Expected Shortfall
Given: From the Monte Carlo simulation above, the losses exceeding VaR (the worst 500 out of 10,000 scenarios) have an average loss of $225,000.
Calculate: 95% CVaR.
Solution:
CVaR_95% = $225,000Interpretation: When losses exceed the 95% VaR threshold, the average loss is $225,000. This is roughly 29% worse than the $174,090 VaR figure, highlighting the severity of tail events.
Common Pitfalls
- VaR says nothing about tail shape: VaR only identifies a threshold. Two portfolios with identical VaR can have vastly different tail losses. Always compute CVaR alongside VaR to understand tail severity.
- Parametric VaR assumes normality: Financial returns exhibit fat tails and skewness. Parametric VaR systematically underestimates tail risk. Use Monte Carlo with fat-tailed distributions or historical simulation for more realistic estimates.
- Correlation breakdown in crises: Correlations spike toward 1.0 during market stress, precisely when diversification is most needed. Stress tests should use crisis-period correlations, not calm-period correlations.
- Using too short a lookback for covariance estimation: Too short a window is noisy; too long a window includes stale data from different market regimes. A common compromise is 1-3 years of daily data, or use EWMA-weighted covariances.
- Not distinguishing between absolute VaR and relative VaR: Absolute VaR includes expected return (VaR = -mu + zsigma); relative VaR excludes it (VaR = zsigma). For short horizons (1-10 days), the expected return is negligible and the distinction is minor. For longer horizons, it matters.
- Square-root-of-time scaling limitations: VaR_h = VaR_1 * sqrt(h) assumes i.i.d. returns. With serial correlation or volatility clustering, this scaling is inaccurate.
Cross-References
- historical-risk: Historical VaR and realized volatility serve as non-parametric alternatives and calibration benchmarks for the forward-looking models in this skill.
- performance-metrics: VaR and CVaR can be used as risk denominators in modified risk-adjusted ratios (e.g., return/CVaR).
- volatility-modeling: EWMA and GARCH volatility forecasts provide the volatility inputs (sigma) for parametric and Monte Carlo VaR.
Running the Script
uv run scripts/forward_risk.py # run the demo (uses PEP 723 inline deps)
uv run scripts/forward_risk.py --verify # check demo outputs against the worked examples (exit 1 on mismatch)
python3 scripts/forward_risk.py # alternative (requires: pip install numpy scipy)The demo prints the calculations covered above; its values match the worked examples in this skill. Run --help for a list of the classes and functions. For programmatic use, import the module rather than running it — the demo only executes under python forward_risk.py.
# /// script
# dependencies = ["numpy", "scipy"]
# requires-python = ">=3.11"
# ///
"""
Forward-Looking Risk Analysis
==============================
Compute Value-at-Risk (parametric, historical, Monte Carlo), Conditional VaR
(Expected Shortfall), Component VaR, Marginal VaR, and stress testing scenario
analysis for portfolios.
Part of Layer 1b (Forward-Looking Risk) in the finance skills framework.
"""
import argparse
import sys
import numpy as np
from scipy import stats
class ForwardRisk:
"""Forward-looking risk computations for a portfolio.
Provides parametric, historical, and Monte Carlo VaR, Expected Shortfall,
risk decomposition (component and marginal VaR), and stress testing.
Parameters
----------
weights : np.ndarray
Portfolio weight vector (must sum to 1.0).
cov_matrix : np.ndarray
Annualized covariance matrix of asset returns (n x n).
portfolio_value : float, optional
Portfolio market value in dollars. Default is 1_000_000.
mean_returns : np.ndarray or None, optional
Annualized expected return vector. Default is None (assumes zero
expected return, appropriate for short-horizon VaR).
"""
def __init__(
self,
weights: np.ndarray,
cov_matrix: np.ndarray,
portfolio_value: float = 1_000_000.0,
mean_returns: np.ndarray | None = None,
):
self.weights = np.asarray(weights, dtype=np.float64)
self.cov_matrix = np.asarray(cov_matrix, dtype=np.float64)
self.portfolio_value = portfolio_value
self.mean_returns = (
np.asarray(mean_returns, dtype=np.float64)
if mean_returns is not None
else np.zeros(len(weights), dtype=np.float64)
)
n = len(self.weights)
if self.cov_matrix.shape != (n, n):
raise ValueError(
f"Covariance matrix shape {self.cov_matrix.shape} does not match "
f"weight vector length {n}."
)
def portfolio_volatility(self, annualized: bool = True) -> float:
"""Compute portfolio volatility from the weight vector and covariance matrix.
Parameters
----------
annualized : bool, optional
If True (default), return annualized volatility. If False, return
daily volatility assuming 252 trading days.
Returns
-------
float
Portfolio volatility: sigma_p = sqrt(w' * Sigma * w).
"""
variance = self.weights @ self.cov_matrix @ self.weights
vol = np.sqrt(variance)
if not annualized:
vol /= np.sqrt(252)
return float(vol)
def parametric_var(
self, confidence: float = 0.95, horizon_days: int = 1
) -> float:
"""Compute parametric (variance-covariance) VaR.
Assumes normally distributed returns. For short horizons the expected
return is assumed negligible (absolute VaR).
Parameters
----------
confidence : float, optional
Confidence level. Default is 0.95 (95% VaR).
horizon_days : int, optional
Holding period in trading days. Default is 1.
Returns
-------
float
Dollar VaR: W * z_alpha * sigma_p_daily * sqrt(h).
Positive value representing a potential loss.
"""
z_alpha = stats.norm.ppf(confidence)
sigma_daily = self.portfolio_volatility(annualized=False)
var = self.portfolio_value * z_alpha * sigma_daily * np.sqrt(horizon_days)
return float(var)
def historical_var(
self,
historical_returns: np.ndarray,
confidence: float = 0.95,
) -> float:
"""Compute historical (non-parametric) VaR from past portfolio returns.
Parameters
----------
historical_returns : np.ndarray
Array of historical portfolio returns (or a matrix of asset returns
with shape (T, n) which will be multiplied by weights).
confidence : float, optional
Confidence level. Default is 0.95.
Returns
-------
float
Dollar VaR from the empirical return distribution.
Positive value representing a potential loss.
"""
returns = np.asarray(historical_returns, dtype=np.float64)
if returns.ndim == 2:
portfolio_returns = returns @ self.weights
else:
portfolio_returns = returns
percentile = (1 - confidence) * 100
var_return = np.percentile(portfolio_returns, percentile)
return float(-var_return * self.portfolio_value)
def monte_carlo_var(
self,
confidence: float = 0.95,
horizon_days: int = 1,
n_simulations: int = 10_000,
seed: int | None = None,
) -> float:
"""Compute Monte Carlo VaR via multivariate normal simulation.
Uses Cholesky decomposition to generate correlated random returns.
Parameters
----------
confidence : float, optional
Confidence level. Default is 0.95.
horizon_days : int, optional
Holding period in trading days. Default is 1.
n_simulations : int, optional
Number of simulation paths. Default is 10,000.
seed : int or None, optional
Random seed for reproducibility. Default is None.
Returns
-------
float
Dollar VaR from the simulated loss distribution.
Positive value representing a potential loss.
"""
rng = np.random.default_rng(seed)
# Scale to daily parameters
daily_mean = self.mean_returns / 252
daily_cov = self.cov_matrix / 252
# Simulate h-day returns by scaling daily parameters
h_mean = daily_mean * horizon_days
h_cov = daily_cov * horizon_days
# Cholesky decomposition for correlated sampling
try:
chol = np.linalg.cholesky(h_cov)
except np.linalg.LinAlgError as exc:
raise ValueError(
"Covariance matrix is not positive definite, so Cholesky "
"decomposition failed. This usually means inconsistent or "
"estimated correlations. Repair the matrix first — e.g., "
"project to the nearest positive semi-definite matrix "
"(clip negative eigenvalues via np.linalg.eigh, or use "
"Higham's nearest-correlation-matrix algorithm) — then retry."
) from exc
# Generate random scenarios
z = rng.standard_normal((n_simulations, len(self.weights)))
simulated_returns = h_mean + z @ chol.T
# Portfolio returns for each scenario
portfolio_returns = simulated_returns @ self.weights
# VaR is the negative of the alpha-percentile
percentile = (1 - confidence) * 100
var_return = np.percentile(portfolio_returns, percentile)
return float(-var_return * self.portfolio_value)
def conditional_var_parametric(
self, confidence: float = 0.95, horizon_days: int = 1
) -> float:
"""Compute Conditional VaR (Expected Shortfall) under normality.
CVaR answers: given losses exceed VaR, what is the expected loss?
For a normal distribution:
ES_alpha = mu + sigma * phi(z_alpha) / (1 - alpha)
For short horizons with zero expected return:
ES_alpha = W * sigma * phi(z_alpha) / (1 - alpha)
Parameters
----------
confidence : float, optional
Confidence level. Default is 0.95.
horizon_days : int, optional
Holding period in trading days. Default is 1.
Returns
-------
float
Dollar CVaR (Expected Shortfall). Positive value.
"""
z_alpha = stats.norm.ppf(confidence)
phi_z = stats.norm.pdf(z_alpha)
sigma_daily = self.portfolio_volatility(annualized=False)
sigma_h = sigma_daily * np.sqrt(horizon_days)
es = self.portfolio_value * sigma_h * phi_z / (1 - confidence)
return float(es)
def conditional_var_empirical(
self,
historical_returns: np.ndarray,
confidence: float = 0.95,
) -> float:
"""Compute Conditional VaR (Expected Shortfall) from empirical data.
The average loss in the tail beyond VaR.
Parameters
----------
historical_returns : np.ndarray
Array of historical portfolio returns (or T x n asset return matrix).
confidence : float, optional
Confidence level. Default is 0.95.
Returns
-------
float
Dollar CVaR from the empirical tail. Positive value.
"""
returns = np.asarray(historical_returns, dtype=np.float64)
if returns.ndim == 2:
portfolio_returns = returns @ self.weights
else:
portfolio_returns = returns
percentile = (1 - confidence) * 100
var_threshold = np.percentile(portfolio_returns, percentile)
tail_returns = portfolio_returns[portfolio_returns <= var_threshold]
if len(tail_returns) == 0:
return self.historical_var(historical_returns, confidence)
return float(-np.mean(tail_returns) * self.portfolio_value)
def component_var(self, confidence: float = 0.95) -> np.ndarray:
"""Compute Component VaR for each position.
Component VaRs sum to total portfolio VaR.
CVaR_i = w_i * beta_i * VaR_p
where beta_i = (Sigma * w)_i / (w' * Sigma * w) is the asset's
beta to the portfolio.
Parameters
----------
confidence : float, optional
Confidence level. Default is 0.95.
Returns
-------
np.ndarray
Array of dollar Component VaR for each position.
sum(component_var) = parametric_var (1-day).
"""
z_alpha = stats.norm.ppf(confidence)
sigma_daily = self.portfolio_volatility(annualized=False)
daily_cov = self.cov_matrix / 252
# (Sigma * w)_i gives covariance of asset i with portfolio
cov_with_portfolio = daily_cov @ self.weights
# beta_i = cov(R_i, R_p) / var(R_p)
port_variance = self.weights @ daily_cov @ self.weights
betas = cov_with_portfolio / port_variance
# Component VaR_i = w_i * beta_i * VaR_p
var_p = self.portfolio_value * z_alpha * sigma_daily
c_var = self.weights * betas * var_p
return c_var
def marginal_var(self, confidence: float = 0.95) -> np.ndarray:
"""Compute Marginal VaR for each position.
Measures the rate of change of portfolio VaR with respect to a small
change in position weight.
MVaR_i = z_alpha * (Sigma * w)_i / sigma_p
Parameters
----------
confidence : float, optional
Confidence level. Default is 0.95.
Returns
-------
np.ndarray
Array of Marginal VaR per unit weight change for each asset.
Multiply by portfolio_value to get dollar Marginal VaR.
"""
z_alpha = stats.norm.ppf(confidence)
daily_cov = self.cov_matrix / 252
sigma_daily = self.portfolio_volatility(annualized=False)
# MVaR_i = z_alpha * (Sigma * w)_i / sigma_p
cov_w = daily_cov @ self.weights
m_var = z_alpha * cov_w / sigma_daily
return m_var
@staticmethod
def scale_var(var_1day: float, horizon_days: int) -> float:
"""Scale 1-day VaR to an h-day horizon using the square-root-of-time rule.
Assumes i.i.d. returns.
VaR_h = VaR_1 * sqrt(h)
Parameters
----------
var_1day : float
1-day VaR (dollar amount).
horizon_days : int
Target horizon in trading days.
Returns
-------
float
h-day VaR.
"""
return float(var_1day * np.sqrt(horizon_days))
@staticmethod
def stress_scenario(
weights: np.ndarray,
portfolio_value: float,
scenario_returns: np.ndarray,
) -> float:
"""Compute portfolio P&L under a stress scenario.
Parameters
----------
weights : np.ndarray
Portfolio weight vector.
portfolio_value : float
Current portfolio market value.
scenario_returns : np.ndarray
Hypothetical or historical return shocks per asset.
Returns
-------
float
Dollar P&L impact (negative = loss).
"""
weights = np.asarray(weights, dtype=np.float64)
scenario_returns = np.asarray(scenario_returns, dtype=np.float64)
portfolio_return = weights @ scenario_returns
return float(portfolio_value * portfolio_return)
@staticmethod
def factor_risk_decomposition(
weights: np.ndarray,
factor_loadings: np.ndarray,
factor_cov: np.ndarray,
idiosyncratic_var: np.ndarray,
) -> dict[str, float]:
"""Decompose portfolio variance into factor (systematic) and idiosyncratic risk.
sigma^2_p = b' * Sigma_f * b + sum(w_i^2 * sigma^2_epsilon_i)
where b = B' * w is the vector of portfolio factor exposures.
Parameters
----------
weights : np.ndarray
Portfolio weight vector (n,).
factor_loadings : np.ndarray
Factor loading matrix (n x k), where n = assets, k = factors.
factor_cov : np.ndarray
Factor covariance matrix (k x k).
idiosyncratic_var : np.ndarray
Idiosyncratic variance for each asset (n,).
Returns
-------
dict
Dictionary with keys: 'total_variance', 'factor_variance',
'idiosyncratic_variance', 'factor_pct', 'idiosyncratic_pct'.
"""
weights = np.asarray(weights, dtype=np.float64)
factor_loadings = np.asarray(factor_loadings, dtype=np.float64)
factor_cov = np.asarray(factor_cov, dtype=np.float64)
idiosyncratic_var = np.asarray(idiosyncratic_var, dtype=np.float64)
# Portfolio factor exposures: b = B' * w
port_factor_exposures = factor_loadings.T @ weights
# Factor variance: b' * Sigma_f * b
factor_var = port_factor_exposures @ factor_cov @ port_factor_exposures
# Idiosyncratic variance: sum(w_i^2 * sigma^2_epsilon_i)
idio_var = np.sum(weights**2 * idiosyncratic_var)
total_var = factor_var + idio_var
return {
"total_variance": float(total_var),
"factor_variance": float(factor_var),
"idiosyncratic_variance": float(idio_var),
"factor_pct": float(factor_var / total_var * 100) if total_var > 0 else 0.0,
"idiosyncratic_pct": float(idio_var / total_var * 100) if total_var > 0 else 0.0,
}
def _demo() -> None:
# ----------------------------------------------------------------
# Demo: Forward-looking risk analysis on a two-asset portfolio
# ----------------------------------------------------------------
np.random.seed(42)
# Portfolio setup: 60% equities, 40% bonds
weights = np.array([0.60, 0.40])
portfolio_value = 1_000_000.0
# Annualized parameters
equity_vol = 0.18
bond_vol = 0.05
correlation = -0.20
mean_returns = np.array([0.10, 0.04])
# Build covariance matrix
cov_matrix = np.array([
[equity_vol**2, correlation * equity_vol * bond_vol],
[correlation * equity_vol * bond_vol, bond_vol**2],
])
fr = ForwardRisk(
weights=weights,
cov_matrix=cov_matrix,
portfolio_value=portfolio_value,
mean_returns=mean_returns,
)
print("=" * 60)
print("Forward-Looking Risk Analysis - Demo")
print("=" * 60)
print(f"\nPortfolio: 60% Equities / 40% Bonds")
print(f"Portfolio Value: ${portfolio_value:,.0f}")
print(f"Equity Vol: {equity_vol:.0%} | Bond Vol: {bond_vol:.0%} | Corr: {correlation:.2f}")
# Portfolio volatility
ann_vol = fr.portfolio_volatility(annualized=True)
daily_vol = fr.portfolio_volatility(annualized=False)
print(f"\nPortfolio Volatility (annualized): {ann_vol:.4f} ({ann_vol*100:.2f}%)")
print(f"Portfolio Volatility (daily): {daily_vol:.6f} ({daily_vol*100:.4f}%)")
# Parametric VaR
print("\n--- Parametric VaR ---")
var_95_1d = fr.parametric_var(confidence=0.95, horizon_days=1)
var_99_1d = fr.parametric_var(confidence=0.99, horizon_days=1)
var_95_10d = fr.parametric_var(confidence=0.95, horizon_days=10)
print(f" 95% 1-day VaR: ${var_95_1d:,.0f}")
print(f" 99% 1-day VaR: ${var_99_1d:,.0f}")
print(f" 95% 10-day VaR: ${var_95_10d:,.0f}")
print(f" (sqrt-of-time check: ${ForwardRisk.scale_var(var_95_1d, 10):,.0f})")
# Monte Carlo VaR
print("\n--- Monte Carlo VaR (10,000 simulations) ---")
mc_var_95 = fr.monte_carlo_var(confidence=0.95, horizon_days=1, seed=42)
mc_var_99 = fr.monte_carlo_var(confidence=0.99, horizon_days=1, seed=42)
print(f" 95% 1-day MC VaR: ${mc_var_95:,.0f}")
print(f" 99% 1-day MC VaR: ${mc_var_99:,.0f}")
# Conditional VaR (Expected Shortfall)
print("\n--- Conditional VaR (Expected Shortfall, parametric) ---")
cvar_95 = fr.conditional_var_parametric(confidence=0.95, horizon_days=1)
cvar_99 = fr.conditional_var_parametric(confidence=0.99, horizon_days=1)
print(f" 95% 1-day CVaR: ${cvar_95:,.0f}")
print(f" 99% 1-day CVaR: ${cvar_99:,.0f}")
print(f" CVaR/VaR ratio (95%): {cvar_95/var_95_1d:.3f}")
# Historical VaR from synthetic data
print("\n--- Historical VaR (from synthetic daily returns) ---")
n_days = 504
daily_cov = cov_matrix / 252
chol = np.linalg.cholesky(daily_cov)
z = np.random.randn(n_days, 2)
hist_returns = (mean_returns / 252) + z @ chol.T
hist_portfolio_returns = hist_returns @ weights
hist_var_95 = fr.historical_var(hist_portfolio_returns, confidence=0.95)
hist_cvar_95 = fr.conditional_var_empirical(hist_portfolio_returns, confidence=0.95)
print(f" 95% Historical VaR: ${hist_var_95:,.0f}")
print(f" 95% Historical CVaR: ${hist_cvar_95:,.0f}")
# Component VaR
print("\n--- Component VaR (1-day, 95%) ---")
c_var = fr.component_var(confidence=0.95)
for i, label in enumerate(["Equities", "Bonds"]):
pct = c_var[i] / var_95_1d * 100
print(f" {label}: ${c_var[i]:,.0f} ({pct:.1f}% of total VaR)")
print(f" Sum: ${np.sum(c_var):,.0f} (Total VaR: ${var_95_1d:,.0f})")
# Marginal VaR
print("\n--- Marginal VaR (1-day, 95%) ---")
m_var = fr.marginal_var(confidence=0.95)
for i, label in enumerate(["Equities", "Bonds"]):
print(f" {label}: {m_var[i]:.6f} (per unit weight change)")
# Stress Testing
print("\n--- Stress Testing ---")
scenarios = {
"Equity Crash (-30%, bonds +5%)": np.array([-0.30, 0.05]),
"Rate Shock (equities -10%, bonds -8%)": np.array([-0.10, -0.08]),
"Stagflation (equities -20%, bonds -3%)": np.array([-0.20, -0.03]),
"Risk Rally (equities +15%, bonds +2%)": np.array([0.15, 0.02]),
}
for name, shock in scenarios.items():
pnl = ForwardRisk.stress_scenario(weights, portfolio_value, shock)
print(f" {name}: ${pnl:+,.0f}")
# Factor Risk Decomposition
print("\n--- Factor Risk Decomposition ---")
# Simple single-factor model (market factor)
factor_loadings = np.array([[1.0], [0.3]]) # equity beta=1.0, bond beta=0.3
factor_cov = np.array([[0.0225]]) # market factor variance (15% vol)
idio_var = np.array([0.0081, 0.0016]) # idiosyncratic variances
decomp = ForwardRisk.factor_risk_decomposition(
weights, factor_loadings, factor_cov, idio_var
)
print(f" Total Variance: {decomp['total_variance']:.6f}")
print(f" Factor (Systematic): {decomp['factor_variance']:.6f} ({decomp['factor_pct']:.1f}%)")
print(f" Idiosyncratic: {decomp['idiosyncratic_variance']:.6f} ({decomp['idiosyncratic_pct']:.1f}%)")
print("\n" + "=" * 60)
print("Demo complete.")
print("=" * 60)
def _check(failures: list, name: str, actual: float, expected: float, tol: float) -> None:
"""Record a verification check result."""
ok = abs(actual - expected) <= tol
status = "PASS" if ok else "FAIL"
print(f" [{status}] {name}: actual={actual:.6g}, expected={expected:.6g}, tol={tol:.2g}")
if not ok:
failures.append(name)
def _verify() -> None:
"""Verify key outputs against the SKILL.md worked examples."""
failures: list = []
# SKILL.md Example 1: $1M, 15% annual vol, 1-day 95% VaR ~= $15,545 (z=1.645)
fr1 = ForwardRisk(weights=np.array([1.0]), cov_matrix=np.array([[0.0225]]),
portfolio_value=1_000_000.0)
_check(failures, "Ex1 1-day 95% parametric VaR", fr1.parametric_var(0.95, 1), 15545.0, 10.0)
# SKILL.md Example 2: 60/40 portfolio analytical benchmark
cov = np.array([[0.0324, -0.0018], [-0.0018, 0.0025]])
fr2 = ForwardRisk(weights=np.array([0.60, 0.40]), cov_matrix=cov,
portfolio_value=1_000_000.0, mean_returns=np.array([0.10, 0.04]))
ann_vol = fr2.portfolio_volatility(annualized=True)
_check(failures, "Ex2 portfolio volatility", ann_vol, 0.105830, 1e-5)
_check(failures, "Ex2 annual 95% VaR (z=1.645)", 1_000_000.0 * 1.645 * ann_vol, 174090.0, 100.0)
# CVaR exceeds VaR at the same confidence level
var95 = fr2.parametric_var(0.95, 1)
cvar95 = fr2.conditional_var_parametric(0.95, 1)
_check(failures, "CVaR > VaR", 1.0 if cvar95 > var95 else 0.0, 1.0, 0)
# Component VaRs sum to total VaR
_check(failures, "component VaRs sum to total", float(np.sum(fr2.component_var(0.95))), var95, 1e-6)
if failures:
print(f"\n{len(failures)} check(s) FAILED: {', '.join(failures)}")
sys.exit(1)
print("\nAll checks passed.")
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__.strip().splitlines()[2] if __doc__ else "",
epilog=(
"Provides: ForwardRisk. "
"For programmatic use, import this module (forward_risk) instead of running it. "
"Bare run executes a demo whose printed values match the SKILL.md worked examples; "
"--verify asserts those values and exits nonzero on mismatch."
),
)
parser.add_argument(
"--verify",
action="store_true",
help="run the verification checks against the SKILL.md worked-example values",
)
args = parser.parse_args()
if args.verify:
_verify()
else:
_demo()
if __name__ == "__main__":
main()