Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.3 KiB
FX Risk Terminal
A real-time foreign-exchange risk engine for a multi-currency equity book, served as a clean browser dashboard. It values the portfolio in USD live, then runs the same risk analytics a desk would: Value-at-Risk by three independent methods, Expected Shortfall, Monte-Carlo simulation, stress testing, and minimum-variance hedging — with the entire quant core implemented in pure Python (no numpy, no pandas).
The engine is currency-agnostic: it reads whatever currencies appear in your positions and builds the full covariance/VaR/correlation pipeline around them. The committed demo book spans 8 currencies — EUR, SEK, GBP, CHF, JPY, KRW, BRL, MXN — across European, Asian-tech and LatAm names.
Why this exists
I hold equities denominated in several currencies but think in USD. The P&L on that book has two drivers — the stocks and the currency — and the FX leg is the one most retail tools ignore. This dashboard isolates and quantifies the currency risk: how much of the book is exposed to each pair, how much I can expect to lose on a bad day, where that risk actually concentrates, whether the mix is genuinely diversified, and how cheaply it can be hedged.
It's deliberately built from first principles. The covariance pipeline, the Cholesky decomposition that correlates the Monte-Carlo shocks, and all three VaR estimators are written out in plain Python so the mathematics is visible rather than hidden behind a library call.
What it computes
| Capability | Method |
|---|---|
| Live valuation | Every position's CCY/USD rate pulled every 60 s (ECB via frankfurter.app), book marked to USD, FX P&L vs. previous close, >0.5% move flags |
| Value-at-Risk | Three independent estimators — parametric (variance-covariance), historical simulation, and Monte Carlo (50k correlated paths) — at 95% and 99% |
| Expected Shortfall (CVaR) | Mean loss in the tail beyond VaR, from both historical and simulated distributions |
| Risk decomposition | Component VaR — each currency's additive contribution to total risk (sums to the parametric total), showing where risk lives vs. where the money sits |
| Diversification ratio | Weighted-average sleeve vol ÷ portfolio vol; >1 means correlations are doing real work |
| Monte Carlo engine | Correlated daily shocks via Cholesky decomposition of the N×N return covariance matrix; full P&L distribution rendered as a histogram with VaR/ES markers |
| Stress testing | Six stylised historical regimes (GFC '08, Euro crisis '11, USD bull '14, COVID '20, USD surge '22, risk-on) with per-currency shocks — including JPY/CHF safe-haven behaviour — replayed against the live book |
| Minimum-variance hedge | Searches every pair for the single forward that minimises residual variance, reporting its notional and hedge effectiveness (R² = variance reduction via cross-correlation) |
| Volatility & correlation | 30-day annualised vol per pair (√252 scaling) and the full N×N rolling correlation matrix as a heatmap |
| Scenario calculator | A slider per currency + regime presets → instant per-position and portfolio USD impact |
| Interactive portfolio builder | Add/edit/remove positions in the browser and re-run the entire risk engine live. The book lives only in localStorage and is sent to a stateless endpoint — never persisted server-side |
| Indexed performance | 30-day FX performance rebased to 100, so any number of pairs is comparable on one axis |
| Horizon scaling | Toggle 1-day ↔ 10-day (Basel-style) VaR; everything scales by √h |
The three VaR methods are shown side by side on purpose: convergence across them is the validation, and divergence flags non-normal (fat-tailed) risk the parametric model would miss.
Tech stack
| Layer | Choice | Why |
|---|---|---|
| Backend | Python / Flask | Thin API layer; minimal and deployable |
| Quant core | Pure Python (stdlib only) | Cholesky, covariance, VaR/ES, Monte Carlo all from scratch — no numpy/pandas |
| FX data | frankfurter.app (ECB) | Free, reliable, no API key; open.er-api.com fallback |
| Charts | Chart.js 4 (CDN) | No build step; custom plugin for VaR marker lines |
| Frontend | Vanilla JS + CSS | No framework, no Bootstrap — fast and dependency-light |
| Tests | unittest |
20 tests validating the maths offline (no network) |
Quick start
git clone <repo-url>
cd fx-dashboard
cp positions.json.example positions.json # then edit with your holdings
pip install -r requirements.txt
python app.py # → http://localhost:5050
positions.json is git-ignored — your holdings never enter the repo. With no file present the app falls back to the committed example data, so it runs out of the box. To force the demo book even when a private file exists (e.g. for screenshots):
FX_DEMO=1 python app.py
Run the test suite:
python -m unittest -v # 20 tests, no network required
Position file format
[
{"ticker": "ASML", "shares": 40, "avg_cost": 620.00, "currency": "EUR", "exchange": "Amsterdam"},
{"ticker": "000660","shares": 200, "avg_cost": 180000, "currency": "KRW", "exchange": "Seoul"}
]
The currency universe is derived automatically from the file — no code change needed to add a pair. Supported currencies are the ECB reference set (EUR, SEK, GBP, CHF, NOK, DKK, JPY, BRL, MXN, KRW, AUD, CAD, CNY, INR, ZAR, …). Currencies outside that set (e.g. ARS, TWD) have no free feed and are skipped with a warning.
Architecture
fx-dashboard/
├── app.py ← Flask routes + JSON error handling
├── fx_engine.py ← rate fetching, caching, positions, FX P&L
├── risk_engine.py ← VaR · ES · Monte Carlo · Cholesky · stress · hedging
├── test_risk_engine.py ← 20 offline unit tests
├── templates/
│ └── dashboard.html ← single-page risk terminal (Chart.js, vanilla CSS)
├── positions.json ← git-ignored — real holdings
├── positions.json.example ← committed — example data
├── requirements.txt
└── .gitignore
API
| Method | Path | Returns |
|---|---|---|
| GET | /api/health |
liveness + data-source status |
| GET | /api/rates |
live rates, previous close, % change, flags |
| GET | /api/positions |
enriched positions: USD value, weights, FX P&L, exposure |
| GET | /api/historical |
30-day series + per-currency vol + correlation matrix |
| GET | /api/risk?horizon=N |
VaR (3 methods), ES, component VaR, diversification ratio, MC distribution, best hedge |
| GET | /api/stress |
stress-scenario impacts |
| POST | /api/analyze |
{positions, horizon} → full bundle for a custom book (stateless; powers the in-browser builder) |
| POST | /api/scenario |
{moves:{CCY:pct}, positions?} → per-position and portfolio impact |
Methodology notes
- Returns are daily log returns of the FX rate; VaR/ES are reported as positive loss numbers.
- Parametric VaR =
z · √(eᵀ Σ e)whereeis the USD exposure vector andΣthe return covariance. Component VaR uses the marginal decompositioneᵢ·(Σe)ᵢ / σ · z, which sums to the total. - Historical VaR/ES re-prices the book under every observed daily return vector and reads the empirical tail.
- Monte Carlo draws correlated shocks
L·z(L= Cholesky ofΣ,z~ N(0,1)), re-prices, and takes the simulated tail. Seeded for reproducibility. - Best single hedge searches every pair for the forward that maximises variance reduction on the whole book: optimal short notional
H* = (Σe)ⱼ / Σⱼⱼ, effectiveness =Corr(book, rⱼ)². - Diversification ratio =
Σ|eᵢ|·σᵢ / √(eᵀΣe); 1.0 means the book moves as a single bet, higher means correlations are reducing risk. - Stress shocks are stylised representative magnitudes for scenario analysis, not exact historical replays. JPY and CHF appreciate in risk-off scenarios (safe-haven behaviour); EM currencies (BRL, MXN, KRW) carry the largest downside.
- Rates are ECB daily fixings — this models FX risk on the book, not intraday stock P&L (no equity price feed).

