Skip to content

Commit 7544267

Browse files
committed
Add validation-grade MarketRiskModel with explicit loss conventions
1 parent a827698 commit 7544267

3 files changed

Lines changed: 103 additions & 0 deletions

File tree

risklib/__init__.py

Whitespace-only changes.

risklib/market/__init__.py

Whitespace-only changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from dataclasses import dataclass
2+
from typing import Dict, Any
3+
import numpy as np
4+
import pandas as pd
5+
6+
7+
@dataclass
8+
class MarketRiskConfig:
9+
alpha: float = 0.99
10+
window: int = 250
11+
method: str = "historical" # historical | parametric | monte_carlo | fhs
12+
13+
14+
class MarketRiskModel:
15+
"""
16+
Market Risk Model for VaR / ES estimation.
17+
18+
Loss convention:
19+
- Losses are positive
20+
- VaR and ES are reported as positive numbers
21+
"""
22+
23+
def __init__(self, returns: pd.Series, config: MarketRiskConfig):
24+
self.returns = returns.dropna()
25+
self.config = config
26+
27+
# Enforce loss convention
28+
self.losses = -self.returns
29+
30+
self.var_ = None
31+
self.es_ = None
32+
33+
# ---------- public API ----------
34+
35+
def fit(self) -> None:
36+
if self.config.method == "historical":
37+
self._fit_historical()
38+
elif self.config.method == "parametric":
39+
self._fit_parametric()
40+
elif self.config.method == "monte_carlo":
41+
self._fit_monte_carlo()
42+
elif self.config.method == "fhs":
43+
self._fit_filtered_historical()
44+
else:
45+
raise ValueError(f"Unknown method: {self.config.method}")
46+
47+
def compute_var(self) -> float:
48+
if self.var_ is None:
49+
raise RuntimeError("Model must be fitted before computing VaR")
50+
return self.var_
51+
52+
def compute_es(self) -> float:
53+
if self.es_ is None:
54+
raise RuntimeError("Model must be fitted before computing ES")
55+
return self.es_
56+
57+
def summary(self) -> Dict[str, Any]:
58+
return {
59+
"VaR": self.var_,
60+
"ES": self.es_,
61+
"alpha": self.config.alpha,
62+
"window": self.config.window,
63+
"method": self.config.method,
64+
"assumptions": self.assumptions(),
65+
}
66+
67+
def assumptions(self) -> list:
68+
assumptions = ["iid returns", "stationarity within rolling window"]
69+
if self.config.method == "parametric":
70+
assumptions.append("normality")
71+
return assumptions
72+
73+
# ---------- model implementations ----------
74+
75+
def _fit_historical(self):
76+
window_losses = self.losses[-self.config.window :]
77+
self.var_ = np.quantile(window_losses, self.config.alpha)
78+
self.es_ = window_losses[window_losses >= self.var_].mean()
79+
80+
def _fit_parametric(self):
81+
from scipy.stats import norm
82+
83+
mu = self.losses.mean()
84+
sigma = self.losses.std(ddof=1)
85+
86+
z = norm.ppf(self.config.alpha)
87+
self.var_ = mu + z * sigma
88+
self.es_ = mu + sigma * norm.pdf(z) / (1 - self.config.alpha)
89+
90+
def _fit_monte_carlo(self):
91+
simulated = np.random.normal(
92+
self.losses.mean(),
93+
self.losses.std(ddof=1),
94+
size=100_000,
95+
)
96+
self.var_ = np.quantile(simulated, self.config.alpha)
97+
self.es_ = simulated[simulated >= self.var_].mean()
98+
99+
def _fit_filtered_historical(self):
100+
# Placeholder: plug in existing GARCH-lite logic here
101+
filtered_losses = self.losses
102+
self.var_ = np.quantile(filtered_losses, self.config.alpha)
103+
self.es_ = filtered_losses[filtered_losses >= self.var_].mean()

0 commit comments

Comments
 (0)