Markets5 min read
One book, many venues
Building a perpetual futures aggregator at Print.World that merges 21 venues into one order book, routes against real depth, and never places the same order twice.
From Print.World · Aug 2025 – Present
For the last stretch at Print.World I've been working on perps: an aggregator that pulls 21 perpetual futures venues into one unified order book, routes orders across them, and shows the result in the terminal. The idea is simple to say. A user wants to buy some amount of BTC perp, and we should find them the best real execution across every venue we support.
Most of the work is in the word "real".
Venues don't agree on what a market looks like
Some venues publish a full order book. Others only give you a quote: here's a price for this size, take it or leave it. A unified book has to merge both, so quote-only venues get represented as levels they'll actually honour rather than pretending to be something they're not.
Each venue gets its own adapter, and all adapters sit behind a single staleness policy and a single error boundary. That was a deliberate choice. With 21 venues, something is always slightly broken somewhere, and I wanted one place that decides "this data is too old to route against" instead of 21 slightly different opinions. If one adapter throws, it drops out of the book; it doesn't take the rest down.
Walking the book
The router walks book depth level by level, which tells you three things a top-of-book price can't: the average fill price for your size, the price impact, and whether a venue runs out of depth before your order is filled.
Fees are a separate step. Depth walking answers "what price would I get", and a fee-aware quote step answers "what does that actually cost me". Keeping them apart made both easier to test, and it meant that when a venue changed its fee schedule, the depth logic didn't have to move.
Route state is frozen per tick. Each tick takes a consistent snapshot of the book and computes routes from it, so any replica can serve a quote and two replicas given the same tick will agree. That made horizontal scaling boring, which is what you want.
Never place an order twice
This is the part I spent the most time on, because it's where money can go wrong.
Every order is placed at most once per client order ID. If a request is retried, by the user, a network blip, or our own code, the second attempt finds the first and doesn't submit again.
Take-profit and stop-loss legs were trickier. Sometimes you send one, the connection drops, and you genuinely don't know whether the venue accepted it. The tempting move is to resend. That's how people end up with two stop-losses. Instead, an ambiguous leg is stored with an explicit status, roughly:
PLACED | REJECTED | UNKNOWN
UNKNOWN is reconciled against the venue's actual open orders before anything else happens. It's never resent blindly.
Rate limits across a fleet
One venue enforces its rate limit per IP, and we run several instances. Each instance tracking its own budget would collectively blow through the limit. So there's a shared token bucket in Redis that every instance draws from.
Two rules on that bucket mattered a lot. It fails open: if Redis is unreachable, we don't stop trading because a rate limiter is down. And it only gates reads, never order submission. A limiter that could block a user's order or a stop-loss is a limiter that could cause real damage.
Funding carry
One feature I enjoyed was ranking funding-rate carry pairs, where you're long on one venue and short on another to collect the difference in funding. The naive version ranks by funding spread. That's misleading, because entering two legs costs money. So pairs are ranked by net carry after both legs' entry costs, with a break-even time ("this pays for itself after roughly this long"), and sized to the depth that's actually available on both sides.
A great-looking spread on a venue with no depth isn't an opportunity. It's a screenshot.
Pushing the book to users
The terminal gets the book over a websocket push layer. Sending the full book on every update was wasteful, so updates carry deltas: only the levels that changed. On the BTC book that took traffic from 132 KB/s to 6.4 KB/s. Choosing the server for that push layer was its own debate, which I wrote up in fix our websocket server, or adopt one?.
For state streams, where the newest message fully replaces the previous one, I used bounded queues that drop the oldest message when full. A slow client doesn't need every intermediate state; it needs the latest one. Unbounded queues on a slow consumer are just a memory leak with extra steps.
What I took from it
The pattern across all of this is being explicit about what you don't know. A stale venue is marked stale. An ambiguous order leg is UNKNOWN, not guessed. A rate limiter that can't reach its store says so and steps aside. Every time I was tempted to pick the optimistic default, the honest one turned out to be simpler to reason about.
The principle I took from it: build the reconciliation path alongside the happy path, not after it. The interesting engineering in a trading system lives in the space between "we sent it" and "we know what happened". The same lesson showed up again in bridging.