A multi-asset market-risk system for a sell-side trading book, implemented end to end across nine phases: from raw market data through sensitivities, VaR/ES, FRTB backtesting, stress testing, risk attribution, and finally a single regulatory capital number under both Basel III and FRTB. It is built around one organising idea — a closed loop in which every measure feeds the next, so the capital figure at the end is consistent with the risk factors at the start.
On a real 14-position multi-asset book the engine produces one consistent capital number end to end — and, the part that matters for a risk seat, the backtesting catches what an average-rate test misses: Christoffersen's independence test flags exception clustering (the failure mode that shows up inside a single stress window) even when Kupiec's unconditional rate looks acceptable. The dashboard below shows 1-day VaR/ES across four methods, the P&L-attribution check (Spearman 0.9985 between Taylor and full-reval P&L), 500-day backtesting with the breach count and traffic-light, the Basel-vs-FRTB capital comparison, stress-scenario P&L, and the component-VaR attribution.
End-to-end risk dashboard on the US book, regenerated by the pipeline from cached market data. FRTB IMA capital $22M vs Basel 2.5 $63M; 9 breaches over 500 days (amber); component-VaR concentration HHI 0.20.
The reference book is a 14-position US trading book: five long single-name equities (AAPL, MSFT, NVDA, GOOGL, and a JPM short), two long equity options, the 2y/5y/10y UST curve, a short US IG credit position, and three FX pairs. An EU book of identical structure is also provided — geography lives only in the data fetchers and the book definition, not in the engine.
| Phase | Module | Status |
|---|---|---|
| 1 | Data layer & portfolio definition | Complete |
| 2 | Greeks & risk-theoretical P&L | Complete |
| 3 | VaR engine (parametric, historical, Monte Carlo, filtered HS) | Complete |
| 4 | FRTB Expected Shortfall with liquidity horizons | Complete |
| 5 | P&L Attribution test + SbM standardised charge | Complete |
| 6 | Backtesting suite (Kupiec, Christoffersen, traffic light) | Complete |
| 7 | Stress testing (historical & hypothetical scenarios) | Complete |
| 8 | Component VaR, marginal VaR, concentration | Complete |
| 9 | Regulatory capital (Basel III vs FRTB) + dashboard | Complete |
positions + market data (Phase 1)
│
▼
sensitivities Δ Γ V ρ θ (Phase 2)
│
├──────────────► RTPL (Taylor / risk-theoretical P&L)
│ │
full revaluation ──► HPL │ (Phase 2 / 5)
│ │
▼ ▼
VaR (4 methods) + FRTB ES ◄───────┘ (Phase 3 / 4)
│
▼
backtesting → traffic-light multiplier (Phase 6)
│
▼
stressed-period calibration (worst 250d) (Phase 7)
│
▼
component / marginal VaR + concentration (Phase 8)
│
▼
regulatory capital: Basel 2.5 vs FRTB IMA vs SA (Phase 9)
Two P&L streams run through the whole system. The RTPL (risk-theoretical P&L)
is the Taylor/sensitivities reconstruction in greeks/pnl_attribution.py; the
HPL (hypothetical P&L) is the full front-office revaluation in
frtb/actual_pnl.py. The FRTB PLA test compares them; the VaR/ES engines run on
the RTPL distribution; the capital layer consumes the backtesting multiplier, the
stressed ES, and the SbM charge that the earlier phases produce.
market-risk-engine/
├── data/ # Risk factors, instruments, portfolios, market data
│ ├── risk_factors.py # RiskFactor / RiskFactorSet, native-unit conventions
│ ├── portfolio.py # Instrument hierarchy, Position, Portfolio
│ ├── market_data.py # FRED / yfinance fetchers (+ on-disk cache)
│ ├── book_us.py # US trading book (14 positions)
│ └── book_eu.py # EU trading book (parallel structure)
├── greeks/ # Sensitivities + risk-theoretical P&L
│ ├── sensitivities.py # Δ, Γ, V, ρ, θ per (position, risk factor)
│ └── pnl_attribution.py # Taylor-expansion P&L = RTPL (+ by-greek/-position/-RF)
├── var/ # VaR engines, common 250d window
│ ├── types.py # Shared VaRResult schema
│ ├── parametric.py # Delta-normal
│ ├── historical.py # Historical simulation
│ ├── monte_carlo.py # Multivariate-normal / multivariate-t (shared χ² mixing)
│ ├── filtered_hs.py # GARCH(1,1) filtered HS
│ └── var_engine.py # Unified facade (run_all_methods)
├── frtb/ # FRTB measures
│ ├── liquidity_horizons.py # LH band partitioning
│ ├── expected_shortfall.py # 97.5% ES, LH-scaled to the 10-day base horizon
│ ├── actual_pnl.py # HPL via full revaluation (compute_hpl)
│ ├── pla_test.py # P&L Attribution test (Spearman + KS, MAR32)
│ ├── stressed_calibration.py# Rolling-window stressed-period search
│ └── sbm_capital.py # Standardised approach (SbM), 3 correlation scenarios
├── backtest/ # Backtesting & multiplier
│ ├── exceptions.py # Rolling VaR exception stream
│ ├── coverage_tests.py # Kupiec POF + Christoffersen independence/CC
│ ├── traffic_light.py # Basel & FRTB multipliers from exception count
│ └── backtest_engine.py # One VaR, tested vs RTPL and HPL
├── stress/ # Scenario analysis
│ ├── scenarios.py # GFC / COVID / 2022 / supervisory shocks
│ └── scenario_engine.py # Full-reval headline + greek decomposition + residual
├── attribution/ # Risk attribution
│ ├── component_var.py # Euler component VaR + marginal + incremental
│ ├── concentration.py # Herfindahl, effective-N, diversification ratio
│ └── attribution_engine.py # Parametric vs historical, side by side
├── capital/ # Capital aggregation & reporting
│ ├── regulatory_capital.py # Basel 2.5 vs FRTB IMA vs SA
│ └── dashboard.py # Six-panel matplotlib desk view
├── phase4_test.py … phase9_test.py # Per-phase integration tests
├── verify_fred.py # FRED API-key check
├── risk_dashboard.png # Sample dashboard output
├── requirements.txt
├── sources.md # Data provenance and methodology references
├── data_cache/ # Cached RF history for offline runs (use_cache=True)
├── LICENSE
└── README.md
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Reproduce every phase end to end (uses the bundled data_cache, no keys needed):
python phase4_test.py # FRTB ES + stressed calibration
python phase5_test.py # PLA test + SbM standardised charge
python phase6_test.py # backtesting + traffic-light multiplier
python phase7_test.py # stress scenarios
python phase8_test.py # component/marginal VaR + concentration
python phase9_test.py # Basel vs FRTB capital + dashboardFor a live data pull instead of the cache, sign up for a free FRED key
(https://fredaccount.stlouisfed.org/apikeys), export FRED_API_KEY=..., run
python verify_fred.py, and call the builders with use_cache=False.
from data.market_data import build_us_risk_factor_set
from data.book_us import build_book
from greeks.sensitivities import build_sensitivities
from capital.regulatory_capital import compute_regulatory_capital
rfs = build_us_risk_factor_set(start="2010-01-01", end="2024-12-31", use_cache=True)
pf = build_book(rfs)
sens = build_sensitivities(pf)
print(compute_regulatory_capital(pf, sens).summary())| Measure | Value |
|---|---|
| VaR, 1-day 99% (historical sim) | ~$1.8M |
| VaR, 10-day 99% | $5.8M |
| Stressed VaR, 10-day (COVID window) | $10.6M |
| FRTB ES, 10-day 97.5% | $6.0M |
| Stressed ES / IMCC, 10-day | $11.4M |
| Backtest (trailing 250d, 99%) | Amber, 9 exceptions, IMA retained |
| Basel multiplier / FRTB multiplier | 3.85 / 1.92 |
| Basel 2.5 capital | $63M |
| FRTB IMA capital | $22M (35% of Basel) |
| FRTB SA (SbM, high-correlation scenario) | $15M |
Three findings are worth calling out, because they are what the engine exists to surface:
-
The regime shift cuts capital here. FRTB IMA is
35% of Basel 2.5 for this book, because Basel double-counts (VaR plus stressed VaR, each ×3.85) while FRTB charges a single stressed ES at a multiplier that roughly halved (3.0 → 1.5). The well-documented industry-level capital increases under FRTB come mostly from desks pushed onto the punitive Standardised Approach, not from desks that retain model approval. -
Stress finds a risk that VaR cannot. The worst named scenario is the 2022 rate shock (−$13.6M), ahead of GFC (−$11.3M) and COVID (−$9.4M), even though GFC has the larger raw equity loss. The reason is a sign flip on the UST book: bonds rally in GFC/COVID (flight to quality, a hedge) but sell off in 2022 (a headwind) — an ~$8M swing on the same positions.
-
Diversification depends on which tail you look at. NVDA alone carries ~32% of VaR and the top three names 68% (Herfindahl 0.198, an effective 5 of 14 positions). And the parametric and historical component-VaR decompositions disagree on the sign of the bonds, the JPM short, and the IG short: small risk adders under the Gaussian/average-correlation view, but hedges in the empirical loss tail (where flight-to-quality and risk-off make them pay off). The long option positions flip the same way through convexity.
These are deliberate scope choices, documented inline at each site, not oversights:
- SbM is one bucket per risk class. It applies the prescribed MAR21 within-bucket correlations (GIRR tenor matrix, 25% large-cap equity, 60% FX) and the three correlation scenarios, but omits the full bucket taxonomy, the curvature charge, and most vega. The SA is therefore understated, so the SA-below-IMA ordering for this book should not be over-read; in production SA is usually a conservative floor above IMA.
VaR_avg60/IMCC_avg60are proxied by the current value rather than a trailing-60-day average of the measure; since the multipliers exceed 1, the multiplier term binds andK ≈ m × (current measure).- VIX is used as a single-name equity-vol proxy for the option vega.
- Component/marginal VaR is delta-normal — options enter through delta only (the historical decomposition does carry their convexity).
- No default risk charge (DRC), NMRF add-on, or residual risk (RRAO) — the reference book has no non-modellable factors and no defaultable single names beyond the IG index.
See sources.md for the full list. Key sources: FRTB (BCBS d457 / MAR), Basel III
VaR-based capital (BCBS d128), Kupiec (1995) and Christoffersen (1998) backtests,
and RiskMetrics (1996) for EWMA volatility.
What the numbers can and cannot tell you:
- Window dependence. VaR/ES are estimated on a trailing 250-day window; historical simulation can never exceed the worst day in that window, so genuinely unprecedented tail risk is understated — mitigated, not solved, by the stressed-calibration and ES phases.
- Taylor P&L. Sensitivities are a delta-gamma-vega-theta-rho approximation; the P&L-attribution panel (Spearman 0.9985) shows it tracks full revaluation closely on this book, but large moves and exotic payoffs would diverge.
- Backtest power. Exception counts are sample-dependent and the coverage tests have limited power in short windows — a clean traffic light is necessary, not sufficient.
- Liquidity horizons are simplified. RF-to-LH assignment follows the MAR33 buckets but does not model name-level liquidity or market impact beyond the horizon scaling.
- Single daily-close vendor. No intraday data, no bid-ask, and no liquidity/valuation adjustment beyond the LH treatment.
The tests/ directory holds a pytest suite asserting the regulatory and
mathematical identities at the core of the engine. It is driven by fixed-seed
synthetic books, so it runs offline with no market-data calls.
- VaR / ES — ES ≥ VaR at matched confidence, exact √-time horizon scaling, and an ES/VaR ratio matching the Gaussian closed form on a normal P&L.
- FRTB ES — for a single-liquidity-horizon book the aggregated ES equals the 1-day band ES lifted to the 10-day base horizon by √10 (MAR33).
- Attribution — the delta-normal Euler decomposition is exact (component VaR sums to portfolio VaR); concentration (HHI / effective-N) is well-formed and orders correctly between a balanced and a concentrated book.
- Backtesting — Kupiec POF is zero when the exception rate matches the VaR
level and rejects an excess; Christoffersen conditional coverage decomposes
exactly as
LR_cc = LR_uc + LR_ind; the Basel/FRTB traffic-light zones and multipliers and the FRTB IMA hard gate map correctly.
pip install -r requirements-dev.txt
pytest tests/ -q # 12 testsTests run automatically on every push via GitHub Actions (.github/workflows/tests.yml).
examples/vstests/— the phase-by-phase walkthrough scripts now live inexamples/and are run from the project root, e.g.python -m examples.phase5_demo. They are illustrative demos, not assertions; the automated suite istests/.
