Validation-grade market and credit risk engine
A model-risk–oriented risk analytics engine that measures, validates and documents market and credit risk models the way an institutional risk function would.
It is not a trading system and not a dashboard. The organising question is not "what is the number?" but "is the number any good, and how would you know?"
Three things follow from that:
- Every published figure is reproducible. The results in
docs/validation_results.mdare generated from the data committed indata/byscripts/generate_validation_results.py, and CI fails if the committed results drift from what the code produces. - Conventions are enforced, not documented. VaR ≥ 0 and ES ≥ VaR are runtime invariants that raise, not notes in a README.
- Assumptions travel with results.
summary()returns the risk numbers, the full configuration, and the assumptions actually in force for that configuration.
Market risk — VaR and Expected Shortfall by four methods (historical simulation, parametric Normal, Monte Carlo, Filtered Historical Simulation with a GARCH(1,1) filter); multi-α analysis; rolling out-of-sample backtesting with Kupiec POF, Christoffersen independence, and joint conditional coverage; Euler VaR decomposition (marginal and component); true incremental VaR; Equal Risk Contribution budgeting.
Credit risk — Expected Loss (PD × LGD × EAD) with portfolio and segment aggregation, multiplicative and additive scenario shocks, and data-quality reporting on out-of-range inputs.
Stress and scenarios — single-name shocks, parallel rate shocks via duration approximation, covariance scaling, correlation-breakdown stress with PSD projection, and historical window replay.
git clone https://github.com/sensor-aae/Integrated-Risk-App.git
cd Integrated-Risk-App
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
streamlit run app/app.pyUpload data/market_data.csv for market risk and data/credit_example.csv for
credit. To reproduce every published number:
pip install -r requirements-dev.txt
pytest -q
python scripts/generate_validation_results.pyimport numpy as np
from risklib.data import load_prices, to_returns
from risklib.market import MarketRiskConfig, MarketRiskModel, backtest_var_historical
returns = to_returns(load_prices("data/market_data.csv"), method="log")
weights = np.ones(returns.shape[1]) / returns.shape[1]
cfg = MarketRiskConfig(
alpha=0.99,
method="fhs", # historical | parametric | monte_carlo | fhs
horizon_days=1,
exposure=1_000_000,
fit_garch=True, # estimate omega, alpha, beta by MLE
)
model = MarketRiskModel(returns, weights, cfg).fit()
model.compute_var() # positive loss amount
model.compute_es()
model.summary() # VaR, ES, full config, assumptions in force
model.fit_info_["source"] # "MLE" or "fixed" — estimated vs assumed
bt = backtest_var_historical(returns, weights, alpha=0.99, window=250)
bt["kupiec_pvalue"], bt["christoffersen_pvalue"], bt["joint_pvalue"]MarketRiskConfig carries every parameter that changes a number — confidence
level, horizon, exposure, backtest window, Monte Carlo paths and seed,
covariance shrinkage, and the GARCH settings. It is one serialisable object you
can log, diff and attach to a result, which is the difference between "we ran a
VaR" and "we ran this VaR".
risklib/ # single source of truth for all modelling
data.py # ingestion: prices -> returns
market/
market_risk_model.py # estimators, MarketRiskConfig, MarketRiskModel
backtest.py # Kupiec + Christoffersen + joint CC; both backtests
garch.py # one GARCH(1,1) recursion: fixed and MLE
extras.py # Euler decomposition, incremental VaR, ERC
scenarios.py # stress testing and scenario library
credit/
credit_risk_model.py # EL pipeline: validate -> shock -> compute -> summarise
app/app.py # Streamlit interface — presentation only
scripts/
generate_validation_results.py # regenerates docs/validation_results.md
docs/
model_report.md # methodology, assumptions, limitations
validation_results.md # GENERATED — do not edit by hand
tests/ # 129 tests
data/ # demo market and credit data
Design rules
risklib/holds all modelling, estimation and validation logicapp/computes nothing — it collects inputs, callsrisklib, and visualises- Losses are positive throughout, enforced at
fit()time
Full tables — point measures, GARCH parameter comparison, backtests across three
confidence levels, Euler decomposition, ERC, stress scenarios, and credit EL —
are in docs/validation_results.md, generated
from the shipped demo portfolio (AAPL / TLT / MSFT, equal-weighted, $1,000,000
exposure, 1,257 daily observations from January 2020 to December 2024).
A few results worth calling out:
All three tests pass at every confidence level, for both the historical and FHS backtests. The independence p-values are the interesting ones — they confirm exceptions are distributed across the sample rather than concentrated in stress periods, which Kupiec alone cannot detect.
Estimated GARCH parameters differ materially from the convention. The fixed defaults (α = 0.05, β = 0.94) imply persistence of 0.9900; MLE estimates 0.9665 on this sample. The assumed parameters overstate volatility persistence, which is exactly the misspecification the MLE path exists to address.
Component VaR sums to portfolio VaR to 3.6 × 10⁻¹² — the Euler identity is exact, not approximate, because VaR is homogeneous of degree 1 in the weights.
ERC substantially rebalances the book. Under equal weighting the two equities contribute 93% of portfolio VaR between them; ERC moves the bond from 33% to 54% of the portfolio and cuts VaR by roughly 20%.
129 tests. The suite asserts model invariants rather than frozen numbers, so the assertions hold for any input and do not break when a default changes.
The test worth reading is test_christoffersen_rejects_clustering. It builds an
exception series with the correct total count but bunched into one contiguous
block, then asserts that Kupiec passes it with p > 0.9 and Christoffersen rejects
it at p < 0.001. That contrast is the entire argument for implementing the
independence test.
Also covered: no-look-ahead verification (the threshold at t is recomputed by hand from t−1 and compared), GARCH MLE parameter recovery on simulated data, the Euler summation identity, credit EL aggregation consistency, and a contract-test file pinning every dictionary key the Streamlit app reads — so a rename in the engine fails in CI rather than in front of a user.
CI runs the suite on Python 3.11 and 3.12, verifies the package installs cleanly, and re-runs the validation script to confirm the published results still reproduce.
| Model | Formula | Notes |
|---|---|---|
| VaR (historical) | −Q₁₋α(r_p) × E | Empirical quantile; no distributional assumption |
| VaR (parametric) | (−μ_p + z_α σ_p) × E | Closed form under normality |
| ES (parametric) | (−μ_p + σ_p φ(z_α)/(1−α)) × E | Closed-form tail expectation |
| VaR (Monte Carlo) | Empirical quantile of simulated paths | Multivariate normal, covariance shrinkage |
| VaR (FHS) | −q_z × σ_{t+1} × E | GARCH-standardised residuals, one-step-ahead |
| Component VaR | w_i · ∂VaR/∂w_i | Euler allocation; sums exactly to portfolio VaR |
| Expected Loss | PD × LGD × EAD | Per facility, aggregated to segment and portfolio |
Full derivations, assumptions and limitations: docs/model_report.md.
Documented in full in docs/model_report.md,
with resolved items retained as an audit trail. The open ones:
- i.i.d. and stationarity assumptions, violated across volatility regime changes
- Multivariate normality in Monte Carlo understates tails at high confidence
- √h horizon scaling understates risk when volatility is autocorrelated
- The rolling FHS backtest retains fixed GARCH parameters
- Zero conditional mean in the GARCH filter
- Duration approximation ignores convexity
- Credit model computes Expected Loss only — no loss distribution, no economic capital
- Results derive from a single demo portfolio and demonstrate reproducibility, not general model performance
Trading and portfolio optimisation, real-time production risk, regulatory capital calculation. Deferred: factor models, ALM, CVA, copula simulation, PD estimation from default history, multi-step GARCH forecasting.
| Layer | Libraries |
|---|---|
| Engine | NumPy, pandas, SciPy |
| Interface | Streamlit, Plotly |
| Tests | pytest |
The core VaR and backtesting math depends on neither SciPy nor statsmodels —
Normal quantiles come from statistics.NormalDist and χ² p-values from exact
closed forms (erfc(√(x/2)) for one degree of freedom, exp(−x/2) for two).
SciPy is required only for GARCH maximum likelihood.
Educational and demonstrative. Not intended for production use or investment decision-making, and not a regulatory-approved model.