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.

Dashboard screenshot


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.

Risk analytics — Monte Carlo distribution, correlation matrix, stress scenarios

The minimum-variance hedge and the per-currency scenario calculator (here under a stylised USD surge), above the 30-day indexed FX-performance chart:

Hedge and scenario calculator

Rolling and annualised volatility per pair, and the full position book marked to USD with per-name FX P&L and portfolio weights:

Volatility and positions


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) where e is the USD exposure vector and Σ the return covariance. Component VaR uses the marginal decomposition eᵢ·(Σ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).

License

MIT — see LICENSE.

Languages
HTML 54.8%
Python 45.2%