Christoph M. Hoeppke

Connecting multiple exchanges with WebSockets and Rust for cross-market arbitrage

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. Full arb-sim desktop application

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.
System architecture: simulators, Rust backend and Tauri UI 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. WebSocket frame layout 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. Feed tasks fan into a single engine task via channels 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. Multiple symbols from multiple exchanges feeding one arbitrage aggregator per 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.