From a single order book to many markets
In our earlier article on the
Rust SIMD order book
we explored how to represent and query a single limit-order book
efficiently. That work is a building block: once we can maintain a
fast, accurate view of one exchange, the natural next step is to
look at
several exchanges at once and ask whether we can
buy an asset cheaply on one venue and sell it for more on another.
In this article we walk through a small but realistic system that
does exactly that.
We will connect multiple synthetic exchanges over real WebSockets,
aggregate their order books in a Rust/Tokio backend, detect
arbitrage opportunities, send orders back to the venues, and
visualise the whole process in a modern Tauri + React desktop
application. The goal is not to present a production trading
system, but to show how the pieces fit together efficiently and
how modern Rust and web technologies make the interface feel
polished.
Why connect multiple exchanges?
The same asset often trades at slightly different prices on
different venues. Reasons include local supply and demand,
latency differences, fee structures and currency effects. If we
can observe two exchanges fast enough, we might find moments
where the best bid on one venue is higher than the best ask on
another. That crossed spread is a simple form of cross-exchange
arbitrage.
The challenge is that prices move quickly. By the time a human
notices the discrepancy, it is usually gone. So we want an
automated pipeline that receives market data, updates internal
books, scans for crossed spreads, and sends orders with minimal
delay. The architecture we describe here mirrors the shape of
such a pipeline while remaining small enough to run on a laptop.
System overview
The system has three layers:
- Synthetic exchange simulators written in Python. Each one maintains its own order book and serves it over a real WebSocket.
- A Rust/Tokio backend that connects to every exchange, rebuilds a local order book per venue, scans for arbitrage, routes orders and tracks PnL.
- A Tauri + React desktop UI that visualises live order books, executed trades and cumulative PnL.

A key design decision is that the simulators expose real
WebSocket servers and the backend opens real WebSocket clients,
even though everything runs locally. This forces us to handle
connection lifecycles, reconnections, message framing and
heartbeats honestly, just as we would with external exchanges.
WebSockets as the data backbone
WebSocket is the natural transport for streaming market data. It
starts as a normal HTTP request that is upgraded to a framed,
full-duplex protocol. After the upgrade, each message travels in
a lightweight frame with only a few bytes of overhead.

In our pipeline each exchange sends a full
snapshot
on connect, followed by a stream of
deltas.
Deltas describe level updates: a non-zero quantity sets or
replaces the quantity at a price, while a zero quantity removes
the level entirely. A
heartbeat is sent when the
book is quiet, so the backend can distinguish a silent market
from a dead connection.
We chose JSON text frames for clarity, but the same structure
works with compact binary payloads. For low-latency production
feeds, binary formats such as FlatBuffers or a simple fixed-size
frame are usually preferred because they remove parsing overhead
and reduce bandwidth.
Rust backend: concurrency without contention
The Rust backend is built on
Tokio. We spawn one
async
feed task per exchange. Each task owns its WebSocket,
applies incoming deltas to its local
OrderBook, and
forwards events into a single
tokio::sync::mpsc
channel. A single
engine task consumes that channel and
owns all trading state.

This pattern is important: the feed tasks do not share locks,
and the engine is the sole writer to PnL, exposure and the trade
log. That makes the system easier to reason about and avoids the
contention you would get from a shared-mutex design. When the
engine decides to trade, it sends orders back through per-exchange
channels to the feed tasks, which write them onto the same
WebSocket.
Internally, prices and quantities are stored as fixed-point
integers rather than floating-point numbers. This eliminates
rounding drift in the order books and PnL calculations.
Floating-point conversion happens only at display boundaries.
Finding arbitrage opportunities
With local order books for every connected exchange, the engine
scans ordered pairs of venues. If the best bid on one exchange
exceeds the best ask on another by enough to cover expected costs
such as fees and slippage, we have a candidate trade. The engine
then sells on the higher-priced venue and buys on the lower-priced
one, capping the size by the available liquidity on both sides.
Because this is a simulation, fills are modelled at the top of
book visible at detection time. In a live system we would walk
the book depth, model partial fills and apply the venue's fee
schedule, but the core detection logic remains the same.
A modern desktop UI with Tauri and React
The user interface is a React 18 + TypeScript app running inside
a Tauri webview. Tauri lets us ship a native-feeling desktop
window while writing the frontend with familiar web tooling. On
mount, the UI hydrates its state from the Rust backend and then
registers a typed Tauri
Channel for live updates.
The interface shows connection status for each exchange, the top
bid and ask levels of every book, a scrolling trade feed, and a
cumulative-PnL chart. We keep the UI fast by capping lists,
using stable React keys, and avoiding an external state library
at this scale. The result is a clean, responsive trading-terminal
view that stays usable even as events stream in.
Future work: scaling to many symbols
The architecture described so far assumes one trading symbol.
Extending it to many symbols means adding a real symbol field to
the wire protocol and routing each symbol's deltas to its own
arbitrage aggregator. Every exchange can stream multiple symbols,
and each symbol's aggregator sees the combined book across all
connected venues. A central risk and PnL aggregator then combines
positions across symbols and feeds the UI, where the user can
filter by symbol.

At higher throughput it becomes worthwhile to switch from JSON to
a binary wire format, add backpressure so the engine can drop
non-critical updates rather than buffer indefinitely, and pin
feed handlers to dedicated CPU cores.
Simulating latency and measuring its impact
Because the components use real sockets, we can stress-test the
system in a controlled environment. Tools such as Linux
tc/netem, Docker bridge networks or Toxiproxy let us
add latency, jitter and packet loss to the loopback traffic. By
replaying deterministic market data with different delay
profiles, we can measure how stale or delayed prices affect
performance.
The main risk is
adverse selection: a stale price makes
the engine believe an opportunity exists, but the market has
already moved by the time the order arrives. Useful diagnostics
include latency histograms, expected-versus-realised PnL,
per-trade slippage, stale-signal counts and hit rates under
different latency budgets.
Closing thoughts
Starting from the single-exchange order book work in our
SIMD order book article,
we have now seen how to connect multiple venues over WebSockets,
aggregate their books in Rust, detect simple arbitrage and
present the results through a modern desktop UI. The technologies
we used, Rust/Tokio for the backend and Tauri/React for the
interface, make it possible to build a responsive, good-looking
system without sacrificing the low-level control that trading
infrastructure demands.
The implementation is available at
github.com/Hoeppke/arb-sim.