📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← Back to articles
July 22, 2026
5 min read

Smart Order Routing in Crypto: One Order, Twelve Venues, No NBBO

Smart Order Routing in Crypto: One Order, Twelve Venues, No NBBO
#execution
#smart order routing
#market microstructure
#liquidity fragmentation
#crypto
#quant
#python
#tca

You need to buy 400 BTC. Binance has the most depth. OKX and Bybit each show decent size one tick wider. Coinbase quotes a better headline price, but in USD, not USDT. Kraken's top of book looks fantastic and is 400 milliseconds stale. Upbit is better still, but your compliance sign-off for Korea does not exist and neither does your KRW. An equities trader looking at this mess would reach for a smart order router and stop thinking, because in equities the hard parts of routing were regulated into the plumbing two decades ago. In crypto you are the plumbing. There is no consolidated tape, no order-protection rule, no fee cap, no netted settlement — and, most fundamentally, no ability to trade anywhere your capital is not already sitting.

This article is about building the router anyway: the consolidated book and why naive best-price routing against it loses money, the allocation problem as a convex program you can actually solve inside a child-order deadline, the capital constraint that makes crypto SOR inseparable from treasury management, maker-aware routing across venues, and the TCA feedback loop that tells you whether any of it works. It is the engineering sibling of Complex Arbitrage Execution in Rust — that article covers the nanoseconds; this one covers the decisions.

What equities SOR gets for free

It is worth being precise about what US equities routing inherits from regulation, because every item on the list is a thing you must rebuild or consciously live without.

Regulation NMS (SEC, 2005) did three things that matter here. Rule 611, the Order Protection Rule, prohibits executing at a price worse than a protected quote displayed on another exchange — a "trade-through" — which forces every broker to either route to the best displayed price or sweep it with intermarket sweep orders. Rule 610 caps the access fee any venue can charge for taking a protected quote at $0.003 per share, so displayed prices are comparable across venues to within 30 mils. And the consolidated tape (the SIPs) publishes a National Best Bid and Offer, a single official answer to "what is the market."

The academic verdict on this architecture is that it works surprisingly well. O'Hara and Ye (2011), "Is market fragmentation harming market quality?" (Journal of Financial Economics 100(3), 459–474), examined US stocks across fragmentation levels and found that more fragmented stocks had lower transaction costs and faster executions, with prices closer to a random walk. Their summary is the key phrase: US equities behave as "a single virtual market with multiple points of entry." Fragmentation is harmless when smart routers plus trade-through protection stitch the fragments back together. Foucault and Menkveld (2008), "Competition for Order Flow and Smart Order Routing Systems" (Journal of Finance 63(1), 119–158), showed the mechanism in the Dutch market: when a second limit order book (LSE's EuroSETS) entered against Euronext, consolidated depth increased, and liquidity supply on a venue was directly damped by its trade-through rate — routers that ignore a venue kill its incentive to quote.

Even in this regulated world, the consolidated view is a lie at short horizons. Ding, Hanna, and Hendershott (2014), "How Slow Is the NBBO? A Comparison with Direct Exchange Feeds" (Financial Review 49(2), 313–332), measured the SIP NBBO against an NBBO built from direct exchange feeds in the same data center and found dislocations several times per second in active names, typically lasting one to two milliseconds — pure aggregation and transport latency. Keep that number in mind: it is the equities version of a problem that is one to two orders of magnitude worse in crypto.

Now delete all of it. Crypto has no NBBO because there is no SIP. No trade-through rule: a venue will happily print you at a price five ticks through another venue's quote, and nobody files anything. No fee cap: taker fees range from sub-basis-point negotiated tiers to 10 bps retail schedules, so displayed price ordering and net price ordering routinely disagree. And no consolidated clearing: every exchange is its own silo with prefunded balances. You are the SIP, the router, and the clearing firm, simultaneously.

Building the consolidated book, and why naive best-price routing fails

The engineering baseline is unglamorous: N WebSocket L2 feeds, per-venue sequence-number gap handling, symbol and tick-size normalization, and quote-currency normalization (a BTC-USD book and a BTC-USDT book differ by the USDT/USD rate, which is not identically 1.0 and occasionally very much not 1.0). Merge the normalized books into one price-sorted ladder, tagging every level with its venue and — critically — the age of the snapshot it came from. If your merged book does not carry per-venue quote age as a first-class field, you have built a screensaver, not a router.

Consolidated order book across venues with stale and phantom liquidity highlighted

The naive router walks this merged ladder greedily: best net price first. It fails for three distinct reasons, and it is worth keeping them distinct because the fixes are different.

Stale quotes and latency skew. Your venues do not deliver data at the same latency. A colocated feed might be 3 ms old when you act on it; a public WebSocket from a venue on another continent might be 300 ms old. The merged top of book is therefore a composite of different pasts. When BTC moves 10 bps in 200 ms — routine — the stale venue's quotes look systematically attractive on exactly the wrong side. Routing to them buys you a race you have already lost: the quote is gone, your IOC comes back empty or partially filled, and by the time you re-route, the fresh venues have repriced. This is Ding–Hanna–Hendershott's 1–2 ms SIP dislocation problem, except your dislocations last hundreds of milliseconds and nobody is obligated to honor anything.

Phantom liquidity. Summing displayed size across venues overcounts, because the same market maker inventory is quoted in several places at once. Van Kervel (2015), "Competition for Order Flow with Fast and Slow Traders" (Review of Financial Studies 28(7), 2094–2127), documented this in fragmented equities: a trade on one venue is followed within milliseconds by sizeable cancellations of limit orders on competing venues, exactly as predicted by a model where fast liquidity providers quote duplicate size everywhere and pull the copies once one gets hit. Crypto market makers run the same playbook across Binance/OKX/Bybit, so the accessible consolidated depth is materially less than the displayed consolidated depth, and the shortfall grows with how sequentially (rather than simultaneously) you hit the venues. If your router sends child orders one venue at a time, waiting for each fill confirmation, you are farming yourself: each fill signals the rest of the street to cancel.

Fees reorder the ladder. A venue showing the best raw price with a 7.5 bps taker fee is often the worst net price in the book. This sounds too obvious to state, yet "best displayed price" routing is exactly what most first-generation crypto routers (and several vendor products) implement. Net-of-fee comparison is the minimum bar; the sibling article on maker-taker fees and rebates covers the per-venue fee math, VIP-tier dynamics, and why your marginal fee tier — not the sticker — belongs in the router.

The routing optimization

Formalize the child-order problem. You must buy quantity QQ now, marketable, across venues v=1,,Vv = 1,\dots,V. Let Av(x)A_v(x) be venue vv's marginal ask price after eating xx units of its book (a non-decreasing step function from the L2 snapshot), fvf_v its taker fee, and λv\lambda_v a per-unit penalty for staleness and adverse selection on venue vv (calibrated below, from your own markouts). The allocation q=(q1,,qV)q = (q_1, \dots, q_V) solves

minq0,  vqv=Q    v=1VCv(qv),Cv(qv)=(1+fv)0qvAv(x)dx  +  λvqv.\min_{q \ge 0,\; \sum_v q_v = Q} \;\; \sum_{v=1}^{V} C_v(q_v), \qquad C_v(q_v) = (1+f_v)\int_0^{q_v} A_v(x)\,dx \;+\; \lambda_v\, q_v .

Each CvC_v is convex (integral of a non-decreasing function, plus a linear term), so the problem is convex, and the KKT conditions give the whole story: there exists a threshold μ\mu such that

Cv(qv)=(1+fv)Av(qv)+λv=μfor every venue with qv>0,C_v'(q_v^*) = (1+f_v)\,A_v(q_v^*) + \lambda_v = \mu \quad \text{for every venue with } q_v^* > 0,

and Cv(0)μC_v'(0) \ge \mu for venues receiving nothing. In words: pour the order across venues like water, equalizing marginal all-in cost everywhere you trade. A venue is excluded exactly when its first unit — best price, plus fee, plus staleness penalty — is worse than the marginal unit elsewhere.

Water-filling allocation equalizing marginal cost across venue cost curves

For step-function books, the water-filling solution is computed by a greedy walk over the fee-and-penalty-adjusted merged ladder — so the greedy depth-walk is not wrong per se; it is the exact solution provided you greedy-walk net marginal costs with haircut sizes, not raw displayed prices. That distinction is the entire difference between a router and a screensaver.

The canonical treatment of the general problem is Cont and Kukanov, "Optimal order placement in limit order markets" (Quantitative Finance 17(1), 21–39, 2017; arXiv:1210.1625). They formulate order placement across venues — including the split between limit and market orders, fees and rebates, and a penalty for execution risk — as a convex optimization, derive an explicit closed form for the single-venue limit/market split, and give a stochastic-approximation algorithm for the multi-venue case that computes an allocation across twelve exchanges in under 200 ms. The framework predates crypto but transfers almost untouched, because it never assumed an NBBO in the first place — it assumed only per-venue books, per-venue fees, and uncertainty about fills, which is precisely the crypto situation.

A minimal, honest sketch of the marketable side (the convex water-fill with staleness haircuts):

import math
from dataclasses import dataclass

@dataclass
class Venue:
    name: str
    asks: list[tuple[float, float]]  # (price, displayed size), sorted
    taker_fee: float                 # fractional, e.g. 0.0002 = 2 bps
    quote_age_ms: float
    kappa: float                     # phantom-liquidity decay rate, 1/ms
    lam: float                       # staleness/toxicity penalty, $ per unit

def allocate(venues: list[Venue], Q: float):
    ladder = []  # (marginal all-in cost, accessible qty, venue)
    for v in venues:
        surv = math.exp(-v.kappa * v.quote_age_ms)  # P(level still there)
        for price, size in v.asks:
            cost = price * (1.0 + v.taker_fee) + v.lam
            ladder.append((cost, size * surv, v.name))
    ladder.sort()

    fills, remaining = {}, Q
    for cost, qty, name in ladder:
        take = min(qty, remaining)
        fills[name] = fills.get(name, 0.0) + take
        remaining -= take
        if remaining <= 1e-12:
            break
    return fills, remaining  # remaining > 0 => book too thin: slice parent

Sixteen lines of logic; all the intelligence lives in the inputs. κ\kappa (how fast displayed size evaporates with quote age) is calibrated from your own IOC fill ratios as a function of quote age at send time. λv\lambda_v comes from per-venue markouts (last section). Both are measured, not guessed.

A worked example. Buy Q=10Q = 10 BTC across three venues:

Venue Taker fee Quote age Asks (price × size)
A (deep, fresh) 2.0 bps 10 ms 87,000 × 3.0; 87,010 × 4.0; 87,025 × 6.0
B (cheap fee, stale) 1.0 bps 250 ms 86,995 × 1.5; 87,015 × 2.0
C (best headline, fat fee) 7.5 bps 20 ms 86,990 × 2.0; 87,000 × 3.0

Raw tape says C has the best ask (86,990), then B. Fee-adjust and the ladder reorders completely: C's top level nets 86,990×1.00075=87,05586{,}990 \times 1.00075 = 87{,}055, the worst liquidity on screen. B's top nets 87,004, A's nets 87,017. Apply a 30% staleness haircut to B's displayed size (eκ250ms0.7e^{-\kappa \cdot 250\text{ms}} \approx 0.7) and water-fill: 1.05 BTC from B's first level, 3.0 from A's first, 1.4 from B's second, 4.0 from A's second, 0.55 from A's third. All-in average: **87,022.2perBTC.ThenaivebestdisplayedpricerouterCfirst,thenBatfacevaluesize,thenCssecondlevelpays87,022.2 per BTC**. The naive best-displayed-price router — C first, then B at face-value size, then C's second level — pays 87,038.8: 1.9 bps worse, about $166 on one 10-BTC child, compounding over every child of every parent all day. And note the punchline: the venue with the best displayed price on the tape received zero flow from the optimized router. In crypto, nobody forces you to trade there — routing through a "protected quote" is not a concept — and the correct allocation frequently ignores the apparent best price entirely.

What the convex program still ignores: simultaneity (fire all venue children in the same millisecond, or van Kervel's cancellations will re-price venues mid-execution), discrete lot sizes and minimum notionals (round the continuous solution, fix up greedily), and the option to not cross the spread at all — which is Section 5's topic. For the deeper question of how large parents should be sliced over time before any of this venue logic runs, see Almgren–Chriss optimal execution; SOR decides where a child goes, not when children happen.

The capital constraint: SOR is treasury management

Everything above silently assumed you can trade qvq_v on venue vv. In equities that assumption is free: one prime broker, netted settlement, trade now and move money later. In crypto it is the binding constraint of the whole system. Exchanges demand prefunded balances — you cannot lift Kraken's ask with USDT that is sitting on Binance. So the true problem is

minq0,  qv=QvCv(qv)s.t.qvBv    v,\min_{q \ge 0,\; \sum q_v = Q} \sum_v C_v(q_v) \quad \text{s.t.} \quad q_v \le B_v \;\; \forall v,

where BvB_v is your available balance on venue vv (in the quote asset for buys, base for sells). The KKT conditions now read Cv(qv)=μηvC_v'(q_v^*) = \mu - \eta_v with ηv0\eta_v \ge 0 the multiplier on the balance cap. On capped venues, ηv=μCv(Bv)>0\eta_v = \mu - C_v'(B_v) > 0: the marginal dollar there executes cheaper than the market-wide water level, and you are forced to push flow to more expensive venues. That multiplier is not an abstraction — ηv\eta_v is literally the dollars-per-unit you would save if one more unit of balance existed on venue vv right now. Summed over your forecast flow, it is your willingness-to-pay for a rebalancing transfer, and the rebalance decision becomes a comparison any treasury system can execute: move inventory when

E ⁣[future childrenηv]  >  transfer fee+expected adverse move during transfer latency.\mathbb{E}\!\left[\textstyle\sum_{\text{future children}} \eta_v \right] \;>\; \text{transfer fee} + \text{expected adverse move during transfer latency}.

The right-hand side is not small and not constant. On-chain BTC needs 2–6 confirmations (20–60 minutes) before exchanges credit it; ERC-20 transfers take minutes plus gas that spikes exactly when markets are busy; TRC-20 and Solana rails are faster and cheaper but not universally supported; and every exchange adds its own withdrawal-processing queue, which stretches from minutes to hours precisely during volatility events, when your router most wants the inventory moved. The full transfer-cost arithmetic — fees, latency distributions, and the price risk you carry mid-flight — is worked out in the funding-rate arbitrage article, and it transfers verbatim: an SOR rebalance is the same object as an arbitrage leg transfer, cost side included.

This is why the academic result to internalize here is not an execution paper but Makarov and Schoar (2020), "Trading and Arbitrage in Cryptocurrency Markets" (Journal of Financial Economics 135(2), 293–319). They documented cross-exchange price deviations that persist for days to weeks — including the Korean "kimchi premium" that exceeded 40% in early 2018 — and showed that transaction costs cannot explain them; slow-moving, capital-controlled arbitrage capital can. Crypto venues are not O'Hara–Ye's "single virtual market with multiple points of entry." They are partially segmented pools connected by slow, costly pipes, and your router lives inside that segmentation. A crypto SOR without a treasury model is an equities SOR cosplaying.

Fast routing loop allocating within balances, slow treasury loop repositioning inventory

In practice this becomes a two-timescale controller. The fast loop (milliseconds) solves the constrained water-fill within current balances, every child order. The slow loop (minutes to hours) watches the time series of shadow prices ηv\eta_v and forecast flow, and schedules transfers when the persistent component of ηv\eta_v clears the transfer-cost hurdle — with hysteresis, because ping-ponging inventory between venues on noise is how you donate your edge to the Tron network. Institutional desks compress the problem with off-exchange settlement (Copper ClearLoop, Ceffu MirrorX): collateral sits with a custodian and is mirrored to venues, which shrinks transfer latency dramatically for supported venues — it narrows the constraint, but does not delete it, and it introduces its own counterparty line-item.

Maker-aware routing and cross-venue queue games

A router that only crosses spreads is leaving the cheapest liquidity unbought: your own. The Cont–Kukanov framework already contains the answer — their single-venue closed form splits an order between posting and taking based on fees, queue position, and execution-risk aversion — and the multi-venue version generalizes it: post passively on venues where (maker fee, queue length, fill probability within the child deadline) dominate, take on venues where immediacy is cheap, and treat the unfilled passive remainder as flow that re-enters the taker optimization at the deadline.

Two crypto-specific wrinkles make this richer than the equities version.

Fee-chasing is a measured trap. Battalio, Corwin, and Jennings (2016), "Can Brokers Have It All? On the Relation between Make-Take Fees and Limit Order Execution Quality" (Journal of Finance 71(5)), showed that US brokers routing limit orders to the highest-rebate venues delivered measurably worse executions — lower fill rates, worse realized quality — because the rebate venue is where every other rebate-seeker's order also sits: longest queue, most adverse fills. The crypto analogue is exact. The venue paying the best maker rebate attracts every market maker's passive quotes; your order joins a deep queue and fills predominantly when the price is about to trade through you. Per-venue net-of-markout maker economics (next section) routinely rank venues opposite to their fee schedules.

Latency skew is a two-sided game. The same cross-venue information flow van Kervel documented as defensive cancellations is, from the other side, an offensive signal: a trade at Binance's top of book predicts trades and cancellations at OKX's corresponding level within milliseconds. A maker-aware router must therefore (a) re-price its resting orders on other venues' events — pegging to a cross-venue microprice, not the local mid — or it becomes the slow counterparty that cross-venue arbitrageurs pick off; and (b) it can play the game deliberately: post on the venue that lags, hedge on the venue that leads at the moment of fill. That is cross-venue queue arbitrage, and it is the same latency structure exploited in cross-exchange arbitrage execution, just embedded inside an execution mandate instead of a stat-arb book. The operational requirement is identical: the fill event on venue A and the hedge order to venue B must live in the same single-digit-millisecond code path, or the edge belongs to someone else.

Measuring routing quality: markouts and league tables

Equities routers are disciplined by Rule 605/606 disclosure. Nothing disciplines yours except your own TCA — the framework from implementation shortfall and TCA is the scoreboard; here is the router-specific slice of it.

The atomic measurement is the per-venue markout: for every fill, record the (consolidated, latency-corrected) mid at t+Δt + \Delta for Δ{100ms,1s,10s,1min}\Delta \in \{100\text{ms}, 1\text{s}, 10\text{s}, 1\text{min}\}, signed so that negative means the market moved against your fill. Aggregate into an all-in league table:

Venue Taker fill share Eff. spread (bps) Fee (bps) Markout 1s (bps) All-in cost (bps)
A 46% 1.4 2.0 −0.6 4.0
B 31% 1.9 1.0 −2.1 5.0
C 23% 1.2 7.5 −0.4 9.1

The fee column says B is the cheap venue. The all-in column says B is the expensive venue: its stale quotes fill you selectively when the market is already moving through them, and the 1-second markout collects the bill. This inversion — fee ranking versus effective-cost ranking — is the single most consistent finding when desks first build this table, and it is exactly the number that belongs back in the router: the λv\lambda_v penalty of the convex program is the persistent per-venue markout deficit, measured, smoothed, and updated. The router and the TCA form a closed loop, or neither works.

One methodological trap: league tables built from a live router are contaminated by selection bias. If the router already sends the hard, informed flow to the deep venue and the easy flow to the cheap one, the deep venue's markouts look unfairly bad. The clean fix is deliberate randomization — route a few percent of children uniformly at random (an ε\varepsilon-greedy router, which quants will recognize as a bandit with a convex-optimization prior) — so a counterfactual exists. It costs basis points on the randomized slice and it is the only way the other 95% of flow is provably routed well.

The stack, then: a latency-honest consolidated book; a convex water-fill over net marginal costs with measured evaporation and toxicity parameters; a balance-constrained fast loop whose shadow prices drive a transfer-cost-aware slow loop; passive placement that respects queues and cross-venue information flow; and randomized, markout-based TCA feeding every parameter back. None of these components is individually deep. The system is — because in crypto, unlike equities, no regulator built any layer of it for you, and the market charges 2 bps per child order, forever, until you do.

Disclaimer: The information provided in this article is for educational and informational purposes only and does not constitute financial, investment, or trading advice. Trading cryptocurrencies involves significant risk of loss.

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Stay Ahead of the Market

Subscribe to our newsletter for exclusive AI trading insights, market analysis, and platform updates.

We respect your privacy. Unsubscribe at any time.