Galyari
Reads live Level 2 market depth from a Schwab brokerage stream and paints it as a heatmap, with wall, iceberg and pull detection across several symbols. Two renderers share the same data: matplotlib, and a browser canvas fed over WebSocket. MIT licensed.
- Python 3.13
- Schwab API
- WebSocket
- matplotlib
- HTML Canvas
Commercial order-flow tools assume a Windows box and a paid data subscription. Galyari is the version I wanted on Fedora. A producer thread (the Schwab stream, a simulator, or a replay of a recorded tape) writes each new book into a lock-guarded shared state, and either renderer reads from it: matplotlib, or a WebSocket bridge to a canvas client in the browser with no dependencies. Brighter bands are larger resting size, so liquidity that stays put stands out and fleeting quotes fade.
It has 101 unit tests.
Measured performance
Measured with the --bench flag on an Intel Core Ultra 7 258V, Fedora 42, Python 3.13.13, with headless Chromium at 1600×900; 60 s per run after a 3 s warm-up (30 s for the 60 Hz run). Producer→paint runs from the producer storing a new book to the first frame that shows it.
| Renderer | Source | FPS | Frame p50 / p95 (ms) | Producer→paint p50 / p95 (ms) |
|---|---|---|---|---|
| browser | simulator, 1 symbol | 3.98 | 7.8 / 19.3 | 131.6 / 195.1 |
| browser | simulator, 4-symbol grid | 3.96 | 22.8 / 32.6 | 110.5 / 170.8 |
| browser | replay, GOOG tape | 3.98 | 2.7 / 4.5 | 173.6 / 252.9 |
| browser | simulator, --hz 60 | 57.29 | 6.8 / 11.1 | 34.3 / 45.9 |
| matplotlib | simulator | 3.33 | 86.8 / 93.5 | 124.1 / 238.3 |
| matplotlib | replay, GOOG tape | 3.33 | 82.3 / 86.5 | 162.7 / 287.9 |
FPS is capped by design: a 4 Hz broadcast in the browser and a 300 ms timer in matplotlib. At 4 Hz most of the latency is waiting for the next broadcast tick; the WebSocket transfer itself is about 1–2 ms. Not measured: the live socket, and the exchange → Schwab → machine path.
Signal study
Do the signals Galyari draws predict anything? I tested 18 order-flow signals (plus 16 context splits) on 3,045 events from three stocks, one micro-cap, one small-cap and one large-cap, recorded over a single afternoon, with 65.5% of the events after 16:00 ET. For each signal I compared the forward mid-price return at 30 s, 2 min and 5 min against a 10,000-resample null drawn from that stock’s own events, corrected with Holm across all 138 tests, and required the sign to agree across stocks.
No signal passes. Two of the 138 cells survive the correction, but both reverse sign between stocks. An uncorrected first pass had flagged two signals; both fail once the correction is applied.
Gallery
Code
# ── Install ──────────────────────────────────────────────
# Requires Python 3.10+ (developed on 3.13).
pip install -r requirements.txt
# ── Run offline (start here — no credentials, no market hours) ──
python schwab_orderflow_heatmap.py --simulate
# Drives a synthetic random-walk book with decaying liquidity
# walls. The always-available test harness.
# ── Go live ──────────────────────────────────────────────
# Drop a .env in this folder (a template ships as .env; the real
# one is gitignored — never commit it):
# SCHWAB_API_KEY=... # App Key from developer.schwab.com
# SCHWAB_APP_SECRET=... # App Secret
# SCHWAB_CALLBACK_URL=https://127.0.0.1:8182/ # MUST match the
# # app registration char-for-char, trailing slash included
# SCHWAB_ACCOUNT_ID=... # brokerage account id (digits)
# SCHWAB_TOKEN_PATH=/abs/path/to/token.json # reused per run
python schwab_orderflow_heatmap.py --symbol AAPL
# First live run prints an OAuth URL: log in, then paste the full
# redirect URL (https://127.0.0.1:8182/?code=...) back in. The
# browser page looks broken — that's expected; the code is in the
# URL. token.json is written and reused (~7 days between re-auths).
# ── Switch symbols live, no restart ──────────────────────
python schwab_orderflow_heatmap.py --symbol AAPL \
--watchlist AAPL,GOOG,TSLA,NVDA
# n / p cycle the watchlist; the "go to ▸" box jumps to any symbol.
# ── Record & replay (identical renderer across all modes) ──
python schwab_orderflow_heatmap.py --simulate --record # -> recordings/SIM_*.jsonl
python schwab_orderflow_heatmap.py --replay recordings/AAPL_*.jsonl --speed 4
# ── Browser renderer: same data, streamed to a canvas ────
pip install websockets
python bridge.py --simulate # then open the printed URL
python bridge.py --symbol AAPL --watchlist AAPL,GOOG,TSLA,NVDA # 2x2 live grid
# Open http://127.0.0.1:8080 — scroll to zoom (lossless), click a
# tile to expand, Esc to return to the grid.




