value-at-risk / src /historical.py
kameshcodes's picture
fix: add environment-aware logging with prod/dev/test levels and fix CSS not applying in Gradio 6
e60df7c
"""
historical_var.py -- Historical Value at Risk calculation and analysis pipeline.
"""
import numpy as np
import pandas as pd
from loguru import logger
from src.utils import fetch_prices, compute_returns, plot_distribution
from src.excel_export import export_historical_var_report
def calculate_historical_var(returns: pd.Series, confidence: float) -> float:
"""Return VaR as a positive loss value.
VaR = (1 - confidence) percentile of returns, negated to express as loss.
"""
vals = returns.values
return -float(np.percentile(np.asarray(vals), (1.0 - confidence) * 100))
def calculate_historical_es(returns: pd.Series, confidence: float) -> float:
"""Return ES as a positive loss value.
ES = E[loss | loss > VaR], the mean of losses exceeding VaR.
"""
var = calculate_historical_var(returns, confidence)
losses = -np.asarray(returns.values)
tail = losses[losses > var]
return float(np.mean(tail)) if len(tail) > 0 else var
def compute_historical_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_historical_var(returns, var_confidence)
es_1d_pct = calculate_historical_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_historical_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 Historical 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="arithmetic")
result = compute_historical_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
logger.debug(
f"Stressed 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 historical_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 Historical VaR pipeline.
Returns a dict with all computed results.
PnL and VaR values are expressed in dollars based on *portfolio_value*.
If end_date is None, defaults to current date.
"""
# 1. Fetch data and compute returns
prices = fetch_prices(ticker, lookback, end_date)
daily_returns = compute_returns(prices, kind="arithmetic")
# 2. Compute normal VaR and ES
normal = compute_historical_var_es(
daily_returns, var_confidence, es_confidence, n_days, portfolio_value,
)
# 3. Compute Stressed VaR/ES
stressed = compute_stressed_historical_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_historical_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="Historical",
ticker=ticker,
)
logger.debug(
f"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,
"excel_path": excel_path,
"fig_dist": fig_dist,
}