DepthCharge
A physical limit-order-book display: an ESP32-S3 driving a 64×64 LED matrix behind smoked acrylic, rendering live depth first from my own matching engine (Anvil), then from Kraken and Binance — bids stacking green, asks red, trades flashing white at the touch, grey when the data can't be trusted. The portable C++20 book engine is proven on a desktop replay harness before it ever touches the microcontroller; custom carrier PCB and printed enclosure.
// Overview
DepthCharge is a desk-top market-data terminal built as an object: an ESP32-S3 driving a 64×64 HUB75 LED matrix behind smoked acrylic, rendering a live limit-order-book ladder. Asks stack red above the touch, bids green below, the spread is the dark gap between them, trade prints flash white where they land, and a last-price sparkline runs along the bottom. When the data can't be trusted the panel greys out and says why — a frozen ladder that still looks live is the one output the design refuses to produce.
It consumes three venues in ascending order of wire difficulty. First Anvil — my own C++20 matching engine, live at anvil.garethcooke.com — which publishes full top-N snapshots and per-fill trades; DepthCharge is the second independent client of its versioned wire contract, and building it needs zero changes to Anvil. Then Kraken, whose real L2 deltas self-verify against a CRC32 checksum over the top ten levels. Then Binance: a buffered diff stream bracketed against a REST snapshot with sequence-gap recovery, which is the graduation exercise. One book engine, one FeedEvent vocabulary, three adapters.
That engine is portable C++20 with no microcontroller headers in it at all. Transport lives outside the library; adapters take received frames as bytes and emit events, so the identical adapter logic runs under a captured replay file, a host WebSocket client, or esp_websocket_client on the target. The seam is where correctness lives: a desktop replay harness drives the engine from captured wire traces under ctest and renders the result as a console ladder, so behaviour is provable on the desk before it touches hardware. The trace worth watching is the reconnect one — 382 frames in, the feed goes quiet for 4.5 seconds, the ladder greys with a STALE — disconnect banner, and the resync snapshot brings it back. Nothing merges without a replay covering it.
Working against a real venue surfaced what a spec wouldn't. Anvil stamps every frame with a seq, but it's a single global engine counter shared across all tickers and frame types, so one socket's received subsequence is sparse and non-monotonic — M0 measured 42 backward steps in five minutes of healthy data. An adapter written to trust it would have declared a gap roughly nine times a minute. The adapter now synthesises its own monotonic sequence from receive order and never raises a sequence gap, which is safe precisely because Anvil's book frames are idempotent full replaces. Anvil also publishes no tick size or quantity step, so DepthCharge declares them per symbol and verifies every wire price is exactly representable at that scale — a mismatch is a reported error, never a silent rounding.
Where it is now: the engine shipped at M1 — Anvil adapter, phase-1 book and console ladder all green off captured traces — and the bench caught up at M2, with the DevKit, panel and PSU wired and the HUB75 DMA demo running. M3 is under way: the hand-off that carries a rendered snapshot from the feed task to the render task is now a real wait-free three-slot mailbox, built and ThreadSanitizer-clean, rather than the design intent it had been since the constitution was written. Ahead of it is live Anvil on the panel with the pull-the-Wi-Fi test, then the Kraken and Binance adapters, a custom KiCad carrier board around a WROOM-1-N16R8, and a printed enclosure with a smoked acrylic front and a rotary encoder for symbol, venue and price zoom.
// Key decisions
Why a separate repo from Anvil?
The boundary between them is a versioned wire contract, not shared code. Keeping DepthCharge out of Anvil's repo is exactly what makes it Anvil's second independent client — the claim worth being able to make — and it keeps the portfolio's one-repo-per-deploy pattern intact. Anvil stays untouched in v1; the things DepthCharge would like from it (a chaos flag for deterministic gap testing, a sequenced incremental L2 feed) sit on Anvil's own backlog rather than blocking here.
Why one repo for engine, firmware and hardware?
Firmware, carrier board and enclosure versions have to travel together — the same reasoning as MorayGlow. The thing that normally makes that hurt is untestable firmware-coupled logic, and the host-buildable engine/ seam removes it: the book never learns it's on a microcontroller.
Why is FeedEvent the only type crossing the adapter boundary?
Three venues means three JSON dialects, three sequencing schemes and one checksum. Funnelling all of it into a single flat, trivially copyable event type quarantines the venue mess inside the adapters and lets the book be written once against one vocabulary. Snapshot levels are conveyed as borrowed spans into adapter-owned storage rather than owned containers, which is what keeps the boundary allocation-free with a single boundary type — the cost being an explicit lifetime rule: a consumer that defers must copy.
Why does no floating-point number ever touch book data?
Prices and quantities are integers scaled by a per-symbol tick size and quantity step. Exact integer equality is the point — keys never drift, replays stay deterministic, and a price that isn't representable at the declared scale is reported rather than silently rounded. Floats appear only at the display-formatting edge. Anvil's rule, inherited verbatim.
Why is “stale” a rendered state rather than an error?
Disconnects, ring overflow, checksum failures and sequence gaps all reach the book as the same Gap event, and any of them greys the panel until a fresh snapshot arrives. Gap is data, not an exception. A ladder frozen on last-known-good looks exactly like a quiet market, and for an object that lives on a desk being glanced at, that's the one unacceptable output — the honesty of the whole thing rests on it.
Why prove everything on a desktop harness first?
engine/ builds on the host with zero ESP-IDF, FreeRTOS or Arduino includes, so every line of book logic is exercisable by ctest at the desk, and captured wire traces are the ground-truth artefacts — no adapter behaviour or book logic merges without a trace and a golden expectation. Sessions converge only when red and green are objective, and debugging a sequencing bug through a 64×64 LED panel is not a debugging strategy.
Why crypto venues for the real-data leg?
24/7 markets suit something permanently powered on a desk, and free unauthenticated L2 depth means no credentials live on the microcontroller. It also makes Anvil's synthetic order flow a non-issue: the panel consumes wire semantics, not market truth, so a synthetic feed exercises the same code paths a real one does.