ML Infrastructure

Portfolio Risk Analytics

A streaming risk pipeline: Kafka market-tick ingestion, Spark Structured Streaming for rolling aggregates, a Delta bronze layer, and a FastAPI service computing historical-simulation VaR behind a Streamlit dashboard.

One thing to know before reading numbers off it. If no processed data is present, the API falls back to a seeded sample set so the dashboard still renders. On a fresh checkout that is what you are looking at. Run the producer and the consumer first if you want the figures to mean anything.
Kafka to Spark Structured Streaming 5s tumbling windows, 10s watermark Historical simulation VaR 95% and 99% confidence FastAPI risk endpoints
Apache Kafka Apache Spark FastAPI Streamlit Python Docker Pandas NumPy
5s tumbling window
10s watermark
95 / 99% VaR confidence levels
30-day historical window

The Problem

Traditional risk systems compute Value at Risk (VaR) overnight. A portfolio manager running an intraday position change doesn't know their updated risk exposure until the next morning. In volatile markets, that lag is not a workflow inconvenience - it's a risk management failure.

The secondary problem is architecture: financial data arrives as a firehose of price ticks. Computing portfolio-level VaR from streaming tick data requires joining live prices against position records, maintaining rolling historical windows, and running Monte Carlo or historical simulation on each update. Doing this in a batch system is fundamentally the wrong tool. Streaming with Spark changes the problem into a solvable one.

System Architecture

The pipeline has four distinct layers, with Kafka as the central nervous system.

Market Data Ingestion
Price Tick Simulator
Kafka: market_data
Risk Computation
Spark Structured Streaming
Rolling VaR Engine
Landing
Delta bronze layer
Processed data directory
API Layer
FastAPI (risk endpoints)
Seeded fallback when empty
Visualization
Streamlit dashboard

Market data ingestion

A Kafka producer simulates realistic equity price ticks: mid-price with configurable bid-ask spread, volume weighting, and random walk drift. Produces to a market_data topic partitioned by ticker symbol for ordering guarantees within each instrument.

Streaming VaR computation

Spark Structured Streaming joins live price ticks against a position snapshot loaded at startup. For each 5-second micro-batch, it computes mark-to-market P&L, updates the rolling 30-day return history, and recalculates Historical Simulation VaR at 95% confidence. A Delta writer lands the bronze layer alongside the streaming output.

API and dashboard

FastAPI exposes risk query endpoints: current VaR by portfolio or asset class, historical VaR timeseries, and position-level attribution. It reads the most recent processed file and drops back to a seeded sample when the directory is empty, so the dashboard is never blank on a cold start.

VaR Methodology: Historical Simulation

Historical Simulation VaR was chosen over parametric (variance-covariance) and Monte Carlo for a deliberate reason: it makes no distributional assumptions. Equity returns are fat-tailed and skewed. A parametric model that assumes normality will systematically underestimate tail risk. Historical simulation uses the actual observed return distribution, so the 2008 crash, the 2020 COVID selloff, and the 2022 rate shock all live in the historical window and inform the VaR calculation.

The implementation maintains a 30-day rolling window of daily returns per instrument. On each Spark micro-batch, it recomputes the P&L distribution by applying historical returns to the current position, sorts the distribution, and reads the 5th percentile as the 95% 1-day VaR. Component VaR is computed per asset, which allows attribution: which positions are contributing most to portfolio tail risk.

VaR timeseries chart - 30-day rolling 95% VaR vs realized P&L, showing backtesting accuracy with VaR breaches marked. Asset-level VaR attribution heatmap on the right.

Dashboard Design

The Streamlit surface reads the FastAPI endpoints and renders four panels:

  • Portfolio P&L curve - intraday cumulative P&L with entry price baseline
  • VaR gauge - current 1-day 95% VaR as percentage of portfolio NAV, with a red threshold line at 2%
  • Exposure breakdown - long/short gross exposure by asset class (equities, ETFs, futures)
  • Risk contributors - top 5 positions by component VaR contribution, sortable by delta or VaR
Panel layout: P&L curve (top left), VaR gauge (top right), asset class exposure bar chart (bottom left), top risk contributors table (bottom right).

What Worked, What Didn't

What worked

Kafka partitioning by ticker. Partitioning the market data topic by symbol guaranteed ordered delivery per instrument. This made the Spark join against position data deterministic - no race conditions between out-of-order price ticks for the same ticker.

What failed

Historical simulation window size. A 30-day window is too short to capture low-frequency stress events. In calm market periods, the VaR estimate will be systematically too low. Needed a secondary stressed VaR calculation using a 2008 or 2020 scenario window - didn't build this.

What worked

Loading positions once at startup. Holding the position snapshot in memory rather than re-reading it on every Spark micro-batch kept the join cheap. The caching layer that would back this at scale is part of the unbuilt half.

What failed

Designing the dashboard around polling. A 2-second poll against FastAPI adds load and refresh lag for no benefit. Server-sent events pushing on each micro-batch completion is the right shape, and choosing polling first meant designing the API around the wrong access pattern.

What worked

Component VaR attribution. Computing per-asset component VaR made the dashboard useful beyond just a total number. Seeing that a single position accounts for 40% of portfolio VaR is actionable - just seeing total VaR isn't.

What failed

Spark local mode for development. Running Spark in local mode masked memory and executor issues that appeared in distributed mode. Should have used a Docker Compose Spark cluster from day one - switching modes late in development required reconfiguring checkpoint paths and memory settings.

What I Would Build Next

  • Stressed VaR: secondary calculation using 2008, 2020, and 2022 historical stress periods as scenario overlays
  • WebSocket push: replace Streamlit polling with SSE or WebSocket from FastAPI, driving real-time dashboard updates on each Spark micro-batch completion
  • Greeks for options: extend the position model to include delta, gamma, and vega exposure if options are in the portfolio - changes the VaR computation, not just its inputs
  • Backtesting harness: replay 6-month historical data through the VaR engine and count breaches, validate the 95% confidence level is actually 95% over time
  • Multi-currency support: normalize positions to a base currency using FX rates from a secondary Kafka topic, enabling cross-currency portfolio risk