StockViz: Real-Time Pairs Trading Dashboard with JPMorgan Perspective
Overview
StockViz is a real-time pairs trading ratio monitoring dashboard built during the JPMorgan Chase Software Engineering Virtual Experience. The system combines a Python-based synthetic order book simulator (threaded HTTP server generating 5-year bounded random walk market data) with a TypeScript React frontend that polls at 100ms intervals, computes dual-stock mid-price ratios from top-of-book quotes, and visualizes them against static ±5% threshold bands via JPMorgan’s Perspective.js — a WebAssembly-powered, GPU-accelerated streaming data visualization library (open-sourced under FINOS). The <perspective-viewer> web component renders a multi-series y_line chart with four overlaid layers (ratio, upper bound, lower bound, trigger alert), enabling traders to identify mean-reversion arbitrage signals when the price ratio breaches configurable bands.
System Architecture
[Python Order Book Simulator] ──HTTP /query──> [React TypeScript SPA]
│ │
server3.py App.tsx (polling)
├── bwalk() bounded random walk ├── DataStreamer.ts (sync XHR)
├── market() stochastic generator ├── DataManipulator.ts (analytics)
├── orders() limit order generation └── Graph.tsx (Perspective.js)
├── order_book() bid/ask matching │
└── ThreadingMixIn HTTP server <perspective-viewer>
(port 8080) (WebAssembly y_line chart)
The system operates in two layers: (1) backend — Python 3 http.server with ThreadingMixIn reads a pre-generated 5-year synthetic CSV, maintains per-stock order books with aging/clearing, and serves top-of-book JSON at /query; (2) frontend — React SPA polls every 100ms for 1000 iterations (~100s streaming window), computes mid-prices and ratio analytics in DataManipulator.ts, and feeds processed rows into a Perspective table for real-time chart updates.
Order Book Simulation Engine
Bounded Random Walk Market Data
The backend generates entirely synthetic market data using a stochastic bounded random walk — no external APIs:
# Bounded random walk: value bounces within [min, max] via modular arithmetic
def bwalk(min, max, std):
rng = max - min
while True:
max += normalvariate(0, std)
yield abs((max % (rng * 2)) - rng) + min
# Configuration: price $60-150 (σ=1), spread $2-6 (σ=0.1), freq 12-36h (σ=50)
SPD = (2.0, 6.0, 0.1)
PX = (60.0, 150.0, 1)
FREQ = (12, 36, 50)
The market() generator produces (timestamp, price, spread) tuples, and orders() converts these into random limit orders for stocks ABC and DEF with random side (buy/sell) and size. On first run, a 5-year simulated dataset (SIM_LENGTH = timedelta(days=365*5)) is written to CSV; subsequent runs read from this pre-generated file.
Limit Order Book with Aging and Clearing
def add_book(book, order, size, _age=10):
yield order, size, _age
for o, s, age in book:
if age > 0:
yield o, s, age - 1 # Decrement age, expire at 0
Each order has a configurable age counter (default 10). New orders decrement all existing orders by 1, simulating cancellation/expiration. clear_order() recursively matches orders against the opposite side using comparison operators (operator.ge for sells, operator.le for buys), and clear_book() iteratively resolves crossed books (best bid ≥ best ask).
Real-Time Simulation
When REALTIME = True, the server maps simulated historical time to wall-clock time: events are gated by sim_start + (datetime.now() - rt_start), creating the illusion of a live market feed. The first 10 events are skipped (read_10_first_lines) to allow order books to build sufficient depth before serving queries. On data exhaustion, self.__init__() reinitializes the server, effectively looping the 5-year dataset.
Frontend Analytics Pipeline
TypeScript React Application
| Component | Technology | Role |
|---|---|---|
| Framework | React 16.9 + TypeScript 3.2 | Component-based SPA |
| Visualization | Perspective.js 0.2.12 (FINOS/JPMorgan) | WebAssembly-powered streaming charts |
| Chart backend | perspective-viewer-highcharts | Highcharts rendering plugin |
| Styling | Bootstrap 4.2 | Layout and button styling |
| Build | Create React App 2.1 | Webpack + Babel toolchain |
Component hierarchy: index.tsx (entry point, global window.perspective declaration) → App.tsx (polling controller, setInterval at 100ms) → DataStreamer.ts (synchronous XHR to /query) → DataManipulator.ts (mid-price + ratio computation) → Graph.tsx (Perspective table + viewer configuration).
Mid-Price Ratio Computation
// DataManipulator.ts — core analytics engine
const priceABC = (serverResponds[0].top_ask.price + serverResponds[0].top_bid.price) / 2;
const priceDEF = (serverResponds[1].top_ask.price + serverResponds[1].top_bid.price) / 2;
const ratio = priceABC / priceDEF;
const upperBound = 1 + 0.05; // Static +5% band
const lowerBound = 1 - 0.05; // Static -5% band
trigger_alert: (ratio > upperBound || ratio < lowerBound) ? ratio : undefined,
The mid-price (midpoint of best bid and best ask) filters out spread noise, providing a fairer estimate of the “true” price. The price ratio (ABC/DEF) is the fundamental pairs trading signal — when historically correlated stocks diverge beyond the ±5% bands, a mean-reversion trade is signaled. The trigger alert fires by plotting the ratio only when it breaches a bound; undefined values leave gaps in the line, visually highlighting anomalous crossings.
Perspective.js Chart Configuration
// Graph.tsx — WebAssembly-powered streaming visualization
elem.setAttribute('view', 'y_line'); // Multi-series line chart
elem.setAttribute('row-pivots', '["timestamp"]'); // X-axis: time series
elem.setAttribute('columns', '["ratio", "lower_bound", "upper_bound", "trigger_alert"]');
elem.setAttribute('aggregates', JSON.stringify({
ratio: 'avg', upper_bound: 'avg', lower_bound: 'avg',
trigger_alert: 'avg', timestamp: 'distinct count',
}));
Four Perspective modules are loaded via CDN: (1) perspective — core Apache Arrow + WebAssembly engine for columnar data operations, (2) perspective-viewer — the <perspective-viewer> web component, (3) perspective-viewer-hypergrid — tabular/spreadsheet view plugin, (4) perspective-viewer-highcharts — Highcharts rendering backend. The typed schema defines 7 columns (price_abc, price_def, ratio, timestamp, upper_bound, lower_bound, trigger_alert) with float/date types, where duplicate-timestamp rows are aggregated via avg to prevent visual noise.
Pairs Trading Strategy
The dashboard implements the visualization layer for a mean-reversion pairs trading strategy:
- Hypothesis: Stocks ABC and DEF are cointegrated — their price ratio reverts to a stable mean of ~1.0
- Signal: When
ratio > 1.05(ABC relatively overvalued) → short ABC / long DEF; whenratio < 0.95→ long ABC / short DEF - Exit: Close position when ratio returns to the equilibrium range [0.95, 1.05]
The static threshold bands function as simplified Bollinger Band analogs — unlike true Bollinger Bands (rolling mean ± N standard deviations), these are fixed at ±5% around the assumed equilibrium ratio.
Server Response Format
The /query endpoint returns top-of-book quotes for both instruments:
[
{ "stock": "ABC", "timestamp": "...", "top_bid": {"price": 105.23, "size": 47}, "top_ask": {"price": 108.91, "size": 12} },
{ "stock": "DEF", "timestamp": "...", "top_bid": {"price": 98.44, "size": 81}, "top_ask": {"price": 101.17, "size": 33} }
]
CORS is enabled via Access-Control-Allow-Origin: *, and the server uses a decorator-based routing framework where routes are discovered by introspecting class methods for __route__ attributes with regex matching against request paths.
Demo
Tech Stack
Python 3 (http.server, ThreadingMixIn, csv, random, json), TypeScript (3.2), React (16.9), Perspective.js (0.2.12 — JPMorgan/FINOS), Perspective-viewer-highcharts, Bootstrap (4.2), Create React App, Webpack