Spaces:
Running
Running
File size: 5,815 Bytes
7bb0af0 e60df7c 7bb0af0 e60df7c 7bb0af0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """
parametric_var.py -- Parametric (Variance-Covariance) Value at Risk calculation and analysis pipeline.
"""
import numpy as np
import pandas as pd
from scipy import stats
from loguru import logger
from src.utils import fetch_prices, compute_returns, plot_distribution
from src.excel_export import export_parametric_var_report
def estimate_distribution(returns: pd.Series) -> tuple[float, float]:
"""Estimate mean and standard deviation of daily returns.
Uses an unbiased sample standard deviation (ddof=1).
Returns (mu, sigma).
"""
mu = float(returns.mean())
sigma = float(returns.std(ddof=1))
return mu, sigma
def calculate_parametric_var(returns: pd.Series, confidence: float) -> float:
"""Return VaR as a positive loss value using the normal model.
VaR = -(mu - z * sigma) where z = norm.ppf(confidence).
"""
mu, sigma = estimate_distribution(returns)
z = float(stats.norm.ppf(confidence))
return -(mu - z * sigma)
def calculate_parametric_es(returns: pd.Series, confidence: float) -> float:
"""Return ES as a positive loss value using the normal model.
ES = -(mu - sigma * phi(z) / (1 - confidence)).
"""
mu, sigma = estimate_distribution(returns)
z = float(stats.norm.ppf(confidence))
alpha = 1.0 - confidence
return -(mu - sigma * float(stats.norm.pdf(z)) / alpha)
def compute_parametric_var_es(
returns: pd.Series,
var_confidence: float,
es_confidence: float,
n_days: int,
portfolio_value: float,
) -> dict:
"""Compute VaR and ES from returns and scale to n-day horizon.
Returns a dict with 1-day and n-day dollar VaR and ES.
"""
var_1d_pct = calculate_parametric_var(returns, var_confidence)
es_1d_pct = calculate_parametric_es(returns, es_confidence)
var_1d = var_1d_pct * portfolio_value
es_1d = es_1d_pct * portfolio_value
scaling_factor = np.sqrt(n_days)
var_nd = var_1d * scaling_factor
es_nd = es_1d * scaling_factor
return {
"var_1d": var_1d,
"var_nd": var_nd,
"es_1d": es_1d,
"es_nd": es_nd,
}
def compute_stressed_parametric_var_es(
ticker: str,
var_confidence: float,
es_confidence: float,
n_days: int,
portfolio_value: float,
stress_start: str,
stress_end: str,
stress_label: str,
) -> dict:
"""Compute Stressed Parametric VaR and ES over a defined stress window."""
prices = fetch_prices(ticker, start_date=stress_start, end_date=stress_end)
daily_returns = compute_returns(prices, kind="log")
result = compute_parametric_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
logger.debug(
f"Stressed Parametric VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
)
return {
**result,
"stress_start": stress_start,
"stress_end": stress_end,
"stress_label": stress_label,
"prices": prices,
}
def parametric_var_es_pipeline(
ticker: str,
var_confidence: float,
es_confidence: float,
lookback: int,
n_days: int,
portfolio_value: float,
end_date: pd.Timestamp | None = None,
stress_start: str = "2008-01-01",
stress_end: str = "2008-12-31",
stress_label: str = "Global Financial Crisis (2008)",
):
"""Execute the full Parametric VaR pipeline.
Returns a dict with all computed results.
VaR and ES values are expressed as positive dollar losses based on *portfolio_value*.
If end_date is None, defaults to the last business day.
"""
# 1. Fetch data and compute returns
prices = fetch_prices(ticker, lookback, end_date)
daily_returns = compute_returns(prices, kind="log")
mu, sigma = estimate_distribution(daily_returns)
# 2. Compute normal VaR and ES
normal = compute_parametric_var_es(
daily_returns, var_confidence, es_confidence, n_days, portfolio_value,
)
# 3. Compute Stressed VaR/ES
stressed = compute_stressed_parametric_var_es(
ticker, var_confidence, es_confidence, n_days, portfolio_value,
stress_start, stress_end, stress_label,
)
# 4. Generate Excel report (normal + stressed sheets)
excel_path = export_parametric_var_report(
prices=prices,
ticker=ticker,
n_days=n_days,
portfolio_value=portfolio_value,
var_date=end_date,
lookback=lookback,
stressed_prices=stressed["prices"],
stress_start=stressed["stress_start"],
stress_end=stressed["stress_end"],
stress_label=stressed["stress_label"],
var_confidence=var_confidence,
es_confidence=es_confidence,
)
# 5. Generate distribution plot
var_date_str = end_date.strftime("%Y-%m-%d") if end_date else ""
var_conf_pct = f"{var_confidence * 100:g}"
es_conf_pct = f"{es_confidence * 100:g}"
fig_dist = plot_distribution(
returns=daily_returns * portfolio_value,
var_cutoff=-normal["var_nd"],
var_label=f"VaR ({var_conf_pct}%, {n_days}d)",
es_cutoff=-normal["es_nd"],
es_label=f"ES ({es_conf_pct}%, {n_days}d)",
var_date=var_date_str,
method="Parametric",
ticker=ticker,
)
logger.debug(
f"Parametric VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
)
return {
**normal,
"stressed_var_nd": stressed["var_nd"],
"stressed_es_nd": stressed["es_nd"],
"prices": prices,
"daily_returns": daily_returns,
"mu": mu,
"sigma": sigma,
"excel_path": excel_path,
"fig_dist": fig_dist,
}
|