Low Level DesignWorked problems
Design a stock exchange order book
Design a stock exchange order book step by step for a senior low level design interview. Grow from a list scan that trades at the wrong price to price-time priority matching with sorted price levels, cancel and modify that keep or lose a place in line, IOC, FOK and market orders with market depth, and a multi-symbol exchange with a lock per book and a journal that replays to the same trades, with code in Java, Python, C#, Go and TypeScript.
- Read first
- Object modelingThread safety basics
- Patterns
- Command
“Design the order book for a stock exchange: traders send buy and sell orders, and the exchange matches them.” Then: “two traders send orders for the same shares in the same microsecond. Who gets them?”
A stock exchange is where buyers and sellers of shares meet. Each buyer says the most they’ll pay, and each seller says the least they’ll accept. These are orders. An order book is where the exchange keeps every order that hasn’t traded yet. Whenever a buyer’s price reaches a seller’s price, the two trade, and the exchange’s matching engine decides who trades with whom.
The question looks like a data structure exercise, and the data structure matters. But what makes it hard is fairness. Every trader must be treated by the same rule, the rule must be exact, and the result must be the same every time. An exchange that can’t say why one order filled before another doesn’t stay an exchange for long.
The answer in one minute
Section titled “The answer in one minute”If you only have a minute, this is the design. The rest of the page explains how we get here.
- Price-time priority. The best price trades first, and at one price, the oldest order trades first. A trade happens at the price of the resting order, the one already waiting on the book.
- Each side of the book is a sorted map from price to a queue of orders, oldest first. The best price is the first key, so matching never looks at orders that can’t trade.
- An index from order id to order makes cancel fast. A change keeps the order’s place in line only when it shrinks, at the same price. Anything else goes to the back of the line.
- Order types are one field on one matching loop. Only what happens to the unfilled part differs.
- One book is matched by one thread at a time, with a lock per stock. Every accepted request is numbered in a journal, and replaying the journal gives exactly the same trades.
We’ll build this in four stages, then run many stocks at once from many gateways.
Scope it
Section titled “Scope it”Before drawing anything, we ask a few questions. Each answer changes a class. Anything that doesn’t, we assume and move on.
Design a stock exchange.
Whether you scope it to the matching engine. Accounts, clearing, settlement, market data feeds and risk checks are each their own system. The red flag is drawing a dozen services and never deciding which order trades first.
Here are the questions that matter:
- Which part: accounts, matching, or market data? The order book and the matching engine. Accounts and settlement (actually moving the money and shares) are a separate ledger problem.
- How are orders ranked? By price, then by time.
- Which order types? Limit orders first, then market, immediate-or-cancel and fill-or-kill.
- How are prices given? In whole ticks, the smallest price step, such as cents.
- One stock or many? Many symbols (like AAPL), each with its own book.
These answers give us one promise that matters more than the others: no share is ever sold twice, and every trade follows price-time priority. That holds even with orders arriving at the same instant. Two smaller promises come with it. Matching is deterministic: the same orders in the same sequence always give the same trades. And matching is fast. An order touches only the prices it can trade at, never the whole book.
We leave out accounts, settlement, risk checks and market data feeds.
Go deeperThree more questions, and what each changes1 min
| Question | We assume | If the answer were different |
|---|---|---|
| Can a trader trade with themselves? | Allowed | Self-trade prevention cancels one side before matching |
| Trading hours and auctions? | Continuous trading | An opening auction collects orders and finds one price |
| Do we check the trader can pay? | Before the engine | A risk check inside matching would slow every order |
Build it
Section titled “Build it”We’ll build the design in four stages. Each stage adds only the classes that a new requirement needs. In each diagram, the new parts are outlined, and the colors show what each class does (the key is under every diagram). Prices are stored in cents, but the examples use small whole numbers like 100, to keep them easy to read.
Stage 1: Two lists of orders
Section titled “Stage 1: Two lists of orders”Start simple: buy and sell orders, and match them when the prices cross.
Whether you can get something working quickly, and then say exactly what’s wrong with it. The problems of the simple version are the requirements of the real one.
Start with the smallest thing that works. An Order has an id, a Side (buy or sell), a
price, and the quantity still to fill. A Trade records who bought from whom, at what price,
and how many. The OrderBook keeps two lists. A new buy walks through the sell list, and
trades with anything priced at or below its limit. Whatever is left joins the buy list.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// Which way an order goes. A buy matches against sells, and a sell against buys.enum Side { BUY, SELL }
// Prices are whole ticks (cents here), never floating point: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly.class Order { final String id; final Side side; final long price; long remaining;
Order(String id, Side side, long price, long quantity) { this.id = id; this.side = side; this.price = price; this.remaining = quantity; }}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.record Trade(String buyId, String sellId, long price, long quantity) {}
class OrderBook { private final List<Order> buys = new ArrayList<>(); private final List<Order> sells = new ArrayList<>();
// flaw 1: it takes the first order that crosses, not the best price: a buyer pays 105 while // someone is offering at 100. // flaw 2: every order scans the whole other side, so a busy book gets slower with every order. // flaw 3: nothing can be cancelled. List<Trade> submit(Order o) { List<Trade> trades = new ArrayList<>(); List<Order> other = o.side == Side.BUY ? sells : buys; for (Iterator<Order> it = other.iterator(); it.hasNext() && o.remaining > 0; ) { Order resting = it.next(); boolean crosses = o.side == Side.BUY ? resting.price <= o.price : resting.price >= o.price; if (!crosses) continue; long qty = Math.min(o.remaining, resting.remaining); trades.add(o.side == Side.BUY ? new Trade(o.id, resting.id, resting.price, qty) : new Trade(resting.id, o.id, resting.price, qty)); o.remaining -= qty; resting.remaining -= qty; if (resting.remaining == 0) it.remove(); } if (o.remaining > 0) (o.side == Side.BUY ? buys : sells).add(o); return trades; }}# Which way an order goes. A buy matches against sells, and a sell against buys.class Side(Enum): BUY = auto() SELL = auto()
# Prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an exchange# must compare prices exactly.@dataclass(eq=False)class Order: id: str side: Side price: int remaining: int
# Two orders met: who bought, who sold, how many, and at what price. The price is always the# resting order's: it was there first.@dataclass(frozen=True)class Trade: buy_id: str sell_id: str price: int quantity: int
class OrderBook: def __init__(self): self._buys: list[Order] = [] self._sells: list[Order] = []
# flaw 1: it takes the first order that crosses, not the best price: a buyer pays 105 while # someone is offering at 100. # flaw 2: every order scans the whole other side, so a busy book gets slower with every order. # flaw 3: nothing can be cancelled. def submit(self, o: Order) -> list[Trade]: trades = [] other = self._sells if o.side is Side.BUY else self._buys for resting in list(other): if o.remaining == 0: break crosses = resting.price <= o.price if o.side is Side.BUY else resting.price >= o.price if not crosses: continue qty = min(o.remaining, resting.remaining) if o.side is Side.BUY: trades.append(Trade(o.id, resting.id, resting.price, qty)) else: trades.append(Trade(resting.id, o.id, resting.price, qty)) o.remaining -= qty resting.remaining -= qty if resting.remaining == 0: other.remove(resting) if o.remaining > 0: (self._buys if o.side is Side.BUY else self._sells).append(o) return trades// Which way an order goes. A buy matches against sells, and a sell against buys.public enum Side { Buy, Sell }
// Prices are whole ticks (cents here), never doubles: 0.1 + 0.2 isn't 0.3, and an exchange must// compare prices exactly.public sealed class Order{ public Order(string id, Side side, long price, long remaining) => (Id, Side, Price, Remaining) = (id, side, price, remaining);
public string Id { get; } public Side Side { get; } public long Price { get; } public long Remaining { get; set; }}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.public sealed record Trade(string BuyId, string SellId, long Price, long Quantity);
public sealed class OrderBook{ private readonly List<Order> _buys = new(); private readonly List<Order> _sells = new();
// Submit has three flaws. // flaw 1: it takes the first order that crosses, not the best price: a buyer pays 105 while // someone is offering at 100. // flaw 2: every order scans the whole other side, so a busy book gets slower with every order. // flaw 3: nothing can be cancelled. public List<Trade> Submit(Order o) { var trades = new List<Trade>(); var other = o.Side == Side.Buy ? _sells : _buys; foreach (var resting in other) { var crosses = o.Side == Side.Buy ? resting.Price <= o.Price : resting.Price >= o.Price; if (o.Remaining == 0 || !crosses) continue; var qty = Math.Min(o.Remaining, resting.Remaining); trades.Add(o.Side == Side.Buy ? new Trade(o.Id, resting.Id, resting.Price, qty) : new Trade(resting.Id, o.Id, resting.Price, qty)); o.Remaining -= qty; resting.Remaining -= qty; } other.RemoveAll(r => r.Remaining == 0); if (o.Remaining > 0) (o.Side == Side.Buy ? _buys : _sells).Add(o); return trades; }}// Side is which way an order goes. A buy matches against sells, and a sell against buys.type Side int
const ( Buy Side = iota Sell)
// Order prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly.type Order struct { ID string Side Side Price int64 Remaining int64}
// Trade is two orders meeting: who bought, who sold, how many, and at what price. The price is// always the resting order's: it was there first.type Trade struct { BuyID, SellID string Price, Quantity int64}
type OrderBook struct { buys, sells []*Order}
// Submit has three flaws.// flaw 1: it takes the first order that crosses, not the best price: a buyer pays 105 while// someone is offering at 100.// flaw 2: every order scans the whole other side, so a busy book gets slower with every order.// flaw 3: nothing can be cancelled.func (b *OrderBook) Submit(o *Order) []Trade { var trades []Trade other := &b.sells if o.Side == Sell { other = &b.buys } kept := (*other)[:0] for _, resting := range *other { crosses := (o.Side == Buy && resting.Price <= o.Price) || (o.Side == Sell && resting.Price >= o.Price) if o.Remaining > 0 && crosses { qty := min(o.Remaining, resting.Remaining) if o.Side == Buy { trades = append(trades, Trade{o.ID, resting.ID, resting.Price, qty}) } else { trades = append(trades, Trade{resting.ID, o.ID, resting.Price, qty}) } o.Remaining -= qty resting.Remaining -= qty } if resting.Remaining > 0 { kept = append(kept, resting) } } *other = kept if o.Remaining > 0 { if o.Side == Buy { b.buys = append(b.buys, o) } else { b.sells = append(b.sells, o) } } return trades}// Which way an order goes. A buy matches against sells, and a sell against buys.type Side = 'buy' | 'sell';
// Prices are whole ticks (cents here), never fractions: 0.1 + 0.2 isn't 0.3, and an exchange// must compare prices exactly.class Order { constructor( readonly id: string, readonly side: Side, readonly price: number, public remaining: number, ) {}}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.type Trade = { buyId: string; sellId: string; price: number; quantity: number };
class OrderBook { private buys: Order[] = []; private sells: Order[] = [];
// submit has three flaws. // flaw 1: it takes the first order that crosses, not the best price: a buyer pays 105 while // someone is offering at 100. // flaw 2: every order scans the whole other side, so a busy book gets slower with every order. // flaw 3: nothing can be cancelled. submit(o: Order): Trade[] { const trades: Trade[] = []; const other = o.side === 'buy' ? this.sells : this.buys; for (const resting of other) { const crosses = o.side === 'buy' ? resting.price <= o.price : resting.price >= o.price; if (o.remaining === 0 || !crosses) continue; const quantity = Math.min(o.remaining, resting.remaining); const [buyId, sellId] = o.side === 'buy' ? [o.id, resting.id] : [resting.id, o.id]; trades.push({ buyId, sellId, price: resting.price, quantity }); o.remaining -= quantity; resting.remaining -= quantity; } const kept = other.filter((r) => r.remaining > 0); if (o.side === 'buy') this.sells = kept; else this.buys = kept; if (o.remaining > 0) (o.side === 'buy' ? this.buys : this.sells).push(o); return trades; }}It works, but it has three problems, and we leave them in on purpose. It takes the first seller it finds, not the cheapest. With offers at 105 and then 100, a buyer at 110 pays 105, while someone was selling for 100. Every order looks through the whole other side, so the book gets slower as it fills. And an order can never be cancelled.
Stage 2: Price levels and price-time priority
Section titled “Stage 2: Price levels and price-time priority”A buyer should always get the cheapest shares first. And when two sellers ask the same price, who goes first?
Whether you know price-time priority. And whether you pick a structure where the best price is found at once, and the oldest order at that price is next. The red flag is one list sorted on every insert. So is a heap of single orders, which loses the order of arrival at one price.
The rule every major stock exchange uses is price-time priority. The best price trades first: the highest bid (buy price) and the lowest ask (sell price). Among orders at the same price, the one that arrived first trades first. A trader who offered a better price, or the same price sooner, has earned the trade.
That rule decides the structure. Each side of the book is a sorted map from price to a Level.
Each level is a queue of orders, oldest first (FIFO: first in, first out). Bids are sorted
highest first, and asks lowest first. So the best price on each side is the first key.
An incoming buy looks at the lowest ask. If its limit reaches that price, it fills against that level’s orders, from the front. When this level is empty, it moves to the next. It stops at the first price it can’t reach, without looking at anything beyond it.
Here is a book, and a new buy for 10 shares with a limit of 102:
| Bids, highest first | Asks, lowest first |
|---|---|
| 99: Ana 5, Ben 3 | 101: Eve 2, Fay 6 |
| 98: Cy 4 | 102: Gus 5 |
| 97: Dee 10 | 105: Hal 1 |
The buy takes Eve’s 2 shares at 101, then Fay’s 6 at 101. The 101 level is now empty, so it moves to 102 and takes 2 of Gus’s 5. That’s 10 shares. Gus keeps 3 on the book, and Hal at 105 is never looked at.
The trade happens at the resting order’s price. Eve asked 101, and the buyer was willing to pay 102, so the trade is at 101. The resting order set its price first, and the incoming order accepted it. The buyer keeps the difference: an incoming order never trades at a worse price than the best one on the book.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// All resting orders at one price, oldest first. A LinkedHashMap keeps arrival order and, from// stage 3, removes any order by id in constant time.class Level { final long price; final LinkedHashMap<String, Order> orders = new LinkedHashMap<>();
Level(long price) { this.price = price; }}
class OrderBook { // Each side is sorted best price first: the highest bid and the lowest ask. Finding the best // level is O(log n) in the number of prices, not in the number of orders. private final TreeMap<Long, Level> bids = new TreeMap<>(Comparator.reverseOrder()); private final TreeMap<Long, Level> asks = new TreeMap<>();
// Price-time priority: the best price trades first, and at one price, the oldest order. The // trade happens at the resting order's price, the one that was already on the book. List<Trade> submit(Order o) { List<Trade> trades = new ArrayList<>(); TreeMap<Long, Level> opposite = o.side == Side.BUY ? asks : bids; while (o.remaining > 0 && !opposite.isEmpty()) { Level best = opposite.firstEntry().getValue(); if (!crosses(o, best.price)) break; Iterator<Order> it = best.orders.values().iterator(); while (o.remaining > 0 && it.hasNext()) { Order resting = it.next(); long qty = Math.min(o.remaining, resting.remaining); trades.add(o.side == Side.BUY ? new Trade(o.id, resting.id, best.price, qty) : new Trade(resting.id, o.id, best.price, qty)); o.remaining -= qty; resting.remaining -= qty; if (resting.remaining == 0) it.remove(); } if (best.orders.isEmpty()) opposite.pollFirstEntry(); } if (o.remaining > 0) rest(o); return trades; }
Long bestBid() { return bids.isEmpty() ? null : bids.firstKey(); }
Long bestAsk() { return asks.isEmpty() ? null : asks.firstKey(); }
static boolean crosses(Order o, long price) { return o.side == Side.BUY ? price <= o.price : price >= o.price; }
private void rest(Order o) { (o.side == Side.BUY ? bids : asks).computeIfAbsent(o.price, Level::new).orders.put(o.id, o); }}# All resting orders at one price, oldest first. A dict keeps arrival order and, from# stage 3, removes any order by id in constant time.class Level: def __init__(self, price: int): self.price = price self.orders: dict[str, Order] = {}
# One side of the book: levels by price, and the prices kept sorted so the best is found# without scanning orders. Bids want the highest price first, asks the lowest.class BookSide: def __init__(self, highest_first: bool): self._levels: dict[int, Level] = {} self._prices: list[int] = [] # ascending self._highest_first = highest_first
# The best price level, or None when this side is empty. def best(self) -> Level | None: if not self._prices: return None return self._levels[self._prices[-1] if self._highest_first else self._prices[0]]
def best_first(self) -> list[Level]: prices = reversed(self._prices) if self._highest_first else self._prices return [self._levels[p] for p in prices]
# The level at this price, created (and its price slotted into order) if it is new. def level(self, price: int) -> Level: if price not in self._levels: self._levels[price] = Level(price) bisect.insort(self._prices, price) return self._levels[price]
def get(self, price: int) -> Level | None: return self._levels.get(price)
def remove(self, price: int) -> None: del self._levels[price] self._prices.pop(bisect.bisect_left(self._prices, price))
# A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it.def crosses(o: Order, price: int) -> bool: return price <= o.price if o.side is Side.BUY else price >= o.price
class OrderBook: def __init__(self): self._bids = BookSide(highest_first=True) self._asks = BookSide(highest_first=False)
# Price-time priority: the best price trades first, and at one price, the oldest order. # The trade happens at the resting order's price, the one that was already on the book. def submit(self, o: Order) -> list[Trade]: trades = [] opposite = self._asks if o.side is Side.BUY else self._bids while o.remaining > 0 and (best := opposite.best()) is not None and crosses(o, best.price): for resting in list(best.orders.values()): if o.remaining == 0: break qty = min(o.remaining, resting.remaining) if o.side is Side.BUY: trades.append(Trade(o.id, resting.id, best.price, qty)) else: trades.append(Trade(resting.id, o.id, best.price, qty)) o.remaining -= qty resting.remaining -= qty if resting.remaining == 0: del best.orders[resting.id] if not best.orders: opposite.remove(best.price) if o.remaining > 0: (self._bids if o.side is Side.BUY else self._asks).level(o.price).orders[o.id] = o return trades
def best_bid(self) -> int | None: best = self._bids.best() return best.price if best else None
def best_ask(self) -> int | None: best = self._asks.best() return best.price if best else Nonepublic sealed class OrderBook{ // Each side maps price to the orders resting there, oldest first. A SortedDictionary keeps // prices in order, so the best is always First(). Bids sort descending, so the highest // price comes first; asks ascending. private readonly SortedDictionary<long, LinkedList<Order>> _bids = new(Comparer<long>.Create((a, b) => b.CompareTo(a))); private readonly SortedDictionary<long, LinkedList<Order>> _asks = new();
// Submit uses price-time priority: the best price trades first, and at one price, the // oldest order. The trade happens at the resting order's price, the one that was already on // the book. public List<Trade> Submit(Order o) { var trades = new List<Trade>(); var opposite = o.Side == Side.Buy ? _asks : _bids; while (o.Remaining > 0 && opposite.Count > 0) { var (price, level) = opposite.First(); if (!Crosses(o, price)) break; while (level.Count > 0 && o.Remaining > 0) { var r = level.First!.Value; var qty = Math.Min(o.Remaining, r.Remaining); trades.Add(TradeOf(o, r, price, qty)); o.Remaining -= qty; r.Remaining -= qty; if (r.Remaining == 0) level.RemoveFirst(); } if (level.Count == 0) opposite.Remove(price); } if (o.Remaining > 0) Level(o.Side == Side.Buy ? _bids : _asks, o.Price).AddLast(o); return trades; }
public long? BestBid() => _bids.Count > 0 ? _bids.First().Key : null;
public long? BestAsk() => _asks.Count > 0 ? _asks.First().Key : null;
private static LinkedList<Order> Level(SortedDictionary<long, LinkedList<Order>> side, long price) { if (!side.TryGetValue(price, out var level)) side[price] = level = new LinkedList<Order>(); return level; }
private static bool Crosses(Order o, long price) => o.Side == Side.Buy ? price <= o.Price : price >= o.Price;
private static Trade TradeOf(Order incoming, Order resting, long price, long qty) => incoming.Side == Side.Buy ? new(incoming.Id, resting.Id, price, qty) : new(resting.Id, incoming.Id, price, qty);}// Level holds all resting orders at one price, oldest first.type Level struct { Price int64 Orders []*Order}
// BookSide is one side of the book: levels kept sorted best first, so the best is always// levels[0]. Bids want the highest price first, asks the lowest. A sorted slice is simple and// fast for the few dozen prices near the market; an engine with thousands uses a tree or an// array indexed by tick.type BookSide struct { levels []*Level highestFirst bool}
// better reports whether price a is better than b for this side.func (s *BookSide) better(a, b int64) bool { if s.highestFirst { return a > b } return a < b}
func (s *BookSide) Best() *Level { if len(s.levels) == 0 { return nil } return s.levels[0]}
// find returns where price is, or would go, in the sorted levels.func (s *BookSide) find(price int64) int { return sort.Search(len(s.levels), func(i int) bool { return !s.better(s.levels[i].Price, price) })}
// Level returns the level at this price, inserting a new one in sorted place if needed.func (s *BookSide) Level(price int64) *Level { i := s.find(price) if i < len(s.levels) && s.levels[i].Price == price { return s.levels[i] } l := &Level{Price: price} s.levels = slices.Insert(s.levels, i, l) return l}
func (s *BookSide) Remove(price int64) { if i := s.find(price); i < len(s.levels) && s.levels[i].Price == price { s.levels = slices.Delete(s.levels, i, i+1) }}
// crosses: a buy crosses a sell priced at or below it; a sell crosses a buy at or above it.func crosses(o *Order, price int64) bool { if o.Side == Buy { return price <= o.Price } return price >= o.Price}
type OrderBook struct { bids, asks *BookSide}
func NewOrderBook() *OrderBook { return &OrderBook{bids: &BookSide{highestFirst: true}, asks: &BookSide{}}}
func (b *OrderBook) side(s Side) *BookSide { if s == Buy { return b.bids } return b.asks}
func (b *OrderBook) opposite(s Side) *BookSide { return b.side(1 - s) }
// Submit uses price-time priority: the best price trades first, and at one price, the oldest// order. The trade happens at the resting order's price, the one that was already on the book.func (b *OrderBook) Submit(o *Order) []Trade { var trades []Trade opp := b.opposite(o.Side) for o.Remaining > 0 { best := opp.Best() if best == nil || !crosses(o, best.Price) { break } for len(best.Orders) > 0 && o.Remaining > 0 { r := best.Orders[0] qty := min(o.Remaining, r.Remaining) trades = append(trades, tradeOf(o, r, best.Price, qty)) o.Remaining -= qty r.Remaining -= qty if r.Remaining == 0 { best.Orders = best.Orders[1:] } } if len(best.Orders) == 0 { opp.Remove(best.Price) } } if o.Remaining > 0 { l := b.side(o.Side).Level(o.Price) l.Orders = append(l.Orders, o) } return trades}
func tradeOf(incoming, resting *Order, price, qty int64) Trade { if incoming.Side == Buy { return Trade{incoming.ID, resting.ID, price, qty} } return Trade{resting.ID, incoming.ID, price, qty}}
// BestBid returns the highest bid, and false if there is none.func (b *OrderBook) BestBid() (int64, bool) { return best(b.bids) }
func (b *OrderBook) BestAsk() (int64, bool) { return best(b.asks) }
func best(s *BookSide) (int64, bool) { if l := s.Best(); l != nil { return l.Price, true } return 0, false}// One side of the book. Prices are kept sorted best first, so the best is always prices[0]:// bids want the highest price first, asks the lowest. Each price maps to its orders, oldest// first. A sorted array is simple and fast for the few dozen prices near the market; an engine// with thousands uses a tree or an array indexed by tick.class BookSide { private prices: number[] = []; private levels = new Map<number, Order[]>();
constructor(private readonly highestFirst: boolean) {}
// The best price on this side, or undefined when it is empty. best(): number | undefined { return this.prices[0]; }
// bestFirst lists the prices from best to worst, for walking the book. bestFirst(): readonly number[] { return this.prices; }
orders(price: number): Order[] { return this.levels.get(price) ?? []; }
add(o: Order): void { let level = this.levels.get(o.price); if (!level) { level = []; this.levels.set(o.price, level); this.prices.splice(this.find(o.price), 0, o.price); } level.push(o); }
remove(price: number): void { this.levels.delete(price); this.prices.splice(this.find(price), 1); }
// find returns where price is, or would go, by binary search. private find(price: number): number { let lo = 0; let hi = this.prices.length; while (lo < hi) { const mid = (lo + hi) >> 1; const p = this.prices[mid]; if (this.highestFirst ? p > price : p < price) lo = mid + 1; else hi = mid; } return lo; }}
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it.function crosses(o: Order, price: number): boolean { return o.side === 'buy' ? price <= o.price : price >= o.price;}
// The trade, written buyer first whichever side came in.function tradeOf(incoming: Order, resting: Order, price: number, quantity: number): Trade { return incoming.side === 'buy' ? { buyId: incoming.id, sellId: resting.id, price, quantity } : { buyId: resting.id, sellId: incoming.id, price, quantity };}
class OrderBook { protected readonly bids = new BookSide(true); protected readonly asks = new BookSide(false);
// submit uses price-time priority: the best price trades first, and at one price, the oldest // order. The trade happens at the resting order's price, the one that was already on the book. submit(o: Order): Trade[] { const trades: Trade[] = []; const opposite = o.side === 'buy' ? this.asks : this.bids; for (let price = opposite.best(); price !== undefined && o.remaining > 0 && crosses(o, price); price = opposite.best()) { const level = opposite.orders(price); while (level.length > 0 && o.remaining > 0) { const r = level[0]; const quantity = Math.min(o.remaining, r.remaining); trades.push(tradeOf(o, r, price, quantity)); o.remaining -= quantity; r.remaining -= quantity; if (r.remaining === 0) level.shift(); } if (level.length === 0) opposite.remove(price); } if (o.remaining > 0) (o.side === 'buy' ? this.bids : this.asks).add(o); return trades; }
bestBid(): number | undefined { return this.bids.best(); }
bestAsk(): number | undefined { return this.asks.best(); }}Go deeperHow to keep the price levels2 min
Java uses a TreeMap, and C# a SortedDictionary. Python, Go and TypeScript keep a sorted array of
prices, searched with binary search, beside a map from price to level. All give the best price
straight away, and adding a new price costs a logarithmic search.
| Heap of orders | Sorted map of levels (our choice) | Array indexed by tick | |
|---|---|---|---|
| Best price | Top of the heap | First key | A pointer kept to the best |
| Time order at a price | Needs a tie-breaker | The level’s queue | The level’s queue |
| Cancel | Slow: find and remove | Fast with an index by id | Fast with an index by id |
| Depth for a screen | Take out and put back | Walk the first few keys | Walk from the best tick |
| Memory | One entry per order | One entry per live price | Every tick in the price band |
Real matching engines often use the array. Prices stay close to the market, and tick sizes are fixed, so a few thousand slots cover the day. The sorted map is the clearest to write in an interview, and behaves the same way.
Stage 3: Cancel and modify
Section titled “Stage 3: Cancel and modify”Traders cancel far more orders than they fill. How do you cancel fast? And if someone changes the quantity of their order, do they keep their place in line?
Whether you index orders by id, instead of searching the book. And whether you know the priority rule for changes. The red flag is keeping the order’s place when it grows, because that lets a trader jump the queue.
On a real exchange, most orders are cancelled, not filled, often more than ninety percent. So cancel
must not search the book. The book keeps an index, resting, from order id to the order.
Cancel looks the order up, removes it from its level, and removes the level if it’s now empty.
Filling an order completely removes it from the index too, so cancelling a filled order correctly
returns false.
Modify follows one fairness rule. An order that shrinks at the same price keeps its place: asking for fewer shares can only help the people behind it. Anything else, a new price or a bigger quantity, would take shares that the people behind it were there first to claim. So it becomes a cancel plus a new order at the back of the line. A new price can also reach the other side and trade at once, which is why modify returns trades.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
class OrderBook { private final TreeMap<Long, Level> bids = new TreeMap<>(Comparator.reverseOrder()); private final TreeMap<Long, Level> asks = new TreeMap<>(); // Every resting order by id, so cancel finds it without searching the book. private final Map<String, Order> resting = new HashMap<>();
List<Trade> submit(Order o) { List<Trade> trades = match(o); if (o.remaining > 0) rest(o); return trades; }
boolean cancel(String id) { Order o = resting.remove(id); if (o == null) return false; // never existed, already filled, or already cancelled TreeMap<Long, Level> side = sideOf(o.side); Level level = side.get(o.price); level.orders.remove(id); if (level.orders.isEmpty()) side.remove(o.price); return true; }
// Lowering the quantity at the same price keeps the order's place in line: it asks for less, // so nobody behind it is worse off. Anything else, a new price or more quantity, would jump // ahead of people who were there first, so it's a cancel and a new order at the back. List<Trade> modify(String id, long price, long quantity) { Order o = resting.get(id); if (o == null) return List.of(); if (price == o.price && quantity <= o.remaining) { if (quantity == 0) cancel(id); else o.remaining = quantity; return List.of(); } cancel(id); return submit(new Order(id, o.side, price, quantity)); }
Long bestBid() { return bids.isEmpty() ? null : bids.firstKey(); }
Long bestAsk() { return asks.isEmpty() ? null : asks.firstKey(); }
private List<Trade> match(Order o) { List<Trade> trades = new ArrayList<>(); TreeMap<Long, Level> opposite = sideOf(o.side == Side.BUY ? Side.SELL : Side.BUY); while (o.remaining > 0 && !opposite.isEmpty()) { Level best = opposite.firstEntry().getValue(); if (!crosses(o, best.price)) break; Iterator<Order> it = best.orders.values().iterator(); while (o.remaining > 0 && it.hasNext()) { Order r = it.next(); long qty = Math.min(o.remaining, r.remaining); trades.add(o.side == Side.BUY ? new Trade(o.id, r.id, best.price, qty) : new Trade(r.id, o.id, best.price, qty)); o.remaining -= qty; r.remaining -= qty; if (r.remaining == 0) { it.remove(); resting.remove(r.id); } } if (best.orders.isEmpty()) opposite.pollFirstEntry(); } return trades; }
private TreeMap<Long, Level> sideOf(Side s) { return s == Side.BUY ? bids : asks; }
static boolean crosses(Order o, long price) { return o.side == Side.BUY ? price <= o.price : price >= o.price; }
private void rest(Order o) { sideOf(o.side).computeIfAbsent(o.price, Level::new).orders.put(o.id, o); resting.put(o.id, o); }}class OrderBook: def __init__(self): self._bids = BookSide(highest_first=True) self._asks = BookSide(highest_first=False) self._resting: dict[str, Order] = {} # every resting order by id, so cancel needn't search
def submit(self, o: Order) -> list[Trade]: trades = self._match(o) if o.remaining > 0: self._rest(o) return trades
def cancel(self, id: str) -> bool: o = self._resting.pop(id, None) if o is None: return False # never existed, already filled, or already cancelled side = self._side(o.side) level = side.get(o.price) del level.orders[id] if not level.orders: side.remove(o.price) return True
# Lowering the quantity at the same price keeps the order's place in line: it asks for # less, so nobody behind it is worse off. Anything else, a new price or more quantity, # would jump ahead of people who were there first, so it's a cancel and a new order at the # back. def modify(self, id: str, price: int, quantity: int) -> list[Trade]: o = self._resting.get(id) if o is None: return [] if price == o.price and quantity <= o.remaining: if quantity == 0: self.cancel(id) else: o.remaining = quantity return [] self.cancel(id) return self.submit(Order(id, o.side, price, quantity))
def best_bid(self) -> int | None: best = self._bids.best() return best.price if best else None
def best_ask(self) -> int | None: best = self._asks.best() return best.price if best else None
def _side(self, side: Side) -> BookSide: return self._bids if side is Side.BUY else self._asks
def _match(self, o: Order) -> list[Trade]: trades = [] opposite = self._asks if o.side is Side.BUY else self._bids while o.remaining > 0 and (best := opposite.best()) is not None and crosses(o, best.price): for r in list(best.orders.values()): if o.remaining == 0: break qty = min(o.remaining, r.remaining) trades.append(Trade(o.id, r.id, best.price, qty) if o.side is Side.BUY else Trade(r.id, o.id, best.price, qty)) o.remaining -= qty r.remaining -= qty if r.remaining == 0: del best.orders[r.id] del self._resting[r.id] if not best.orders: opposite.remove(best.price) return trades
def _rest(self, o: Order) -> None: self._side(o.side).level(o.price).orders[o.id] = o self._resting[o.id] = o// One symbol's book. Bids are sorted highest first and asks lowest first, so the best price on// each side is always the first key. Each level is a queue of orders, oldest first.public class OrderBook{ protected readonly SortedDictionary<long, LinkedList<Order>> Bids = new(Comparer<long>.Create((a, b) => b.CompareTo(a))); protected readonly SortedDictionary<long, LinkedList<Order>> Asks = new(); // Each resting order's node in its level, by id, so cancel removes it without searching. protected readonly Dictionary<string, LinkedListNode<Order>> Resting = new();
// Match what crosses, then rest what is left. Virtual, so the time-in-force book can add its // rules. public virtual List<Trade> Submit(Order o) { var trades = Match(o); if (o.Remaining > 0) Rest(o); return trades; }
// Cancel returns false if the order never existed, was already filled, or was already // cancelled. public bool Cancel(string id) { if (!Resting.Remove(id, out var node)) return false; var o = node.Value; var level = node.List!; level.Remove(node); if (level.Count == 0) SideOf(o.Side).Remove(o.Price); return true; }
// Modify keeps the order's place in line only when it lowers the quantity at the same // price: it asks for less, so nobody behind it is worse off. Anything else, a new price or // more quantity, would jump ahead of people who were there first, so it's a cancel and a new // order at the back. public List<Trade> Modify(string id, long price, long quantity) { if (!Resting.TryGetValue(id, out var node)) return new(); var o = node.Value; if (price == o.Price && quantity <= o.Remaining) { if (quantity == 0) Cancel(id); else o.Remaining = quantity; return new(); } Cancel(id); return Submit(new Order(o.Id, o.Side, price, quantity)); }
public long? BestBid() => Bids.Count > 0 ? Bids.First().Key : null;
public long? BestAsk() => Asks.Count > 0 ? Asks.First().Key : null;
protected SortedDictionary<long, LinkedList<Order>> SideOf(Side s) => s == Side.Buy ? Bids : Asks;
protected SortedDictionary<long, LinkedList<Order>> Opposite(Side s) => s == Side.Buy ? Asks : Bids;
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. protected List<Trade> Match(Order o) { var trades = new List<Trade>(); var opposite = Opposite(o.Side); while (o.Remaining > 0 && opposite.Count > 0) { var (price, level) = opposite.First(); if (!Crosses(o, price)) break; while (level.Count > 0 && o.Remaining > 0) { var r = level.First!.Value; var qty = Math.Min(o.Remaining, r.Remaining); trades.Add(TradeOf(o, r, price, qty)); o.Remaining -= qty; r.Remaining -= qty; if (r.Remaining == 0) { level.RemoveFirst(); Resting.Remove(r.Id); } } if (level.Count == 0) opposite.Remove(price); } return trades; }
protected void Rest(Order o) { var side = SideOf(o.Side); if (!side.TryGetValue(o.Price, out var level)) side[o.Price] = level = new LinkedList<Order>(); Resting[o.Id] = level.AddLast(o); }
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it. protected static bool Crosses(Order o, long price) => o.Side == Side.Buy ? price <= o.Price : price >= o.Price;
private static Trade TradeOf(Order incoming, Order resting, long price, long qty) => incoming.Side == Side.Buy ? new(incoming.Id, resting.Id, price, qty) : new(resting.Id, incoming.Id, price, qty);}// OrderBook is one symbol's book: bids and asks, each sorted best first.type OrderBook struct { bids, asks *BookSide resting map[string]*Order // every resting order by id, so cancel needn't search the book}
func NewOrderBook() *OrderBook { return &OrderBook{bids: &BookSide{highestFirst: true}, asks: &BookSide{}, resting: map[string]*Order{}}}
func (b *OrderBook) side(s Side) *BookSide { if s == Buy { return b.bids } return b.asks}
func (b *OrderBook) opposite(s Side) *BookSide { return b.side(1 - s) }
func (b *OrderBook) Submit(o *Order) []Trade { trades := b.match(o) if o.Remaining > 0 { b.rest(o) } return trades}
// Cancel returns false if the order never existed, was already filled, or was already// cancelled. Removing it from its level scans that one level, which holds a handful of orders.// An empty price level is removed too, so Best is always a real price.func (b *OrderBook) Cancel(id string) bool { o, ok := b.resting[id] if !ok { return false } delete(b.resting, id) s := b.side(o.Side) l := s.Level(o.Price) l.Orders = slices.DeleteFunc(l.Orders, func(x *Order) bool { return x.ID == id }) if len(l.Orders) == 0 { s.Remove(o.Price) } return true}
// Modify keeps the order's place in line only when it lowers the quantity at the same price:// it asks for less, so nobody behind it is worse off. Anything else, a new price or more// quantity, would jump ahead of people who were there first, so it's a cancel and a new order// at the back.func (b *OrderBook) Modify(id string, price, quantity int64) []Trade { o, ok := b.resting[id] if !ok { return nil } if price == o.Price && quantity <= o.Remaining { if quantity == 0 { b.Cancel(id) } else { o.Remaining = quantity } return nil } b.Cancel(id) return b.Submit(&Order{ID: id, Side: o.Side, Price: price, Remaining: quantity})}
func (b *OrderBook) BestBid() (int64, bool) { return best(b.bids) }
func (b *OrderBook) BestAsk() (int64, bool) { return best(b.asks) }
// match is price first, then time: take the best price level, and in it the oldest order, until// the incoming order is filled or no longer crosses.func (b *OrderBook) match(o *Order) []Trade { var trades []Trade opp := b.opposite(o.Side) for o.Remaining > 0 { best := opp.Best() if best == nil || !crosses(o, best.Price) { break } for len(best.Orders) > 0 && o.Remaining > 0 { r := best.Orders[0] qty := min(o.Remaining, r.Remaining) trades = append(trades, tradeOf(o, r, best.Price, qty)) o.Remaining -= qty r.Remaining -= qty if r.Remaining == 0 { best.Orders = best.Orders[1:] delete(b.resting, r.ID) } } if len(best.Orders) == 0 { opp.Remove(best.Price) } } return trades}
func (b *OrderBook) rest(o *Order) { l := b.side(o.Side).Level(o.Price) l.Orders = append(l.Orders, o) b.resting[o.ID] = o}// One symbol's book: bids and asks, each sorted best first.class OrderBook { protected readonly bids = new BookSide(true); protected readonly asks = new BookSide(false); // Every resting order by id, so cancel needn't search the book. protected readonly resting = new Map<string, Order>();
// Match what crosses, then rest what is left. submit(o: Order): Trade[] { const trades = this.match(o); if (o.remaining > 0) this.rest(o); return trades; }
// cancel returns false if the order never existed, was already filled, or was already // cancelled. Removing it from its level scans that one level, which holds a handful of orders. cancel(id: string): boolean { const o = this.resting.get(id); if (!o) return false; this.resting.delete(id); const side = this.side(o.side); const level = side.orders(o.price); level.splice(level.indexOf(o), 1); if (level.length === 0) side.remove(o.price); return true; }
// modify keeps the order's place in line only when it lowers the quantity at the same price: // it asks for less, so nobody behind it is worse off. Anything else, a new price or more // quantity, would jump ahead of people who were there first, so it's a cancel and a new order // at the back. modify(id: string, price: number, quantity: number): Trade[] { const o = this.resting.get(id); if (!o) return []; if (price === o.price && quantity <= o.remaining) { if (quantity === 0) this.cancel(id); else o.remaining = quantity; return []; } this.cancel(id); return this.submit(new Order(id, o.side, price, quantity)); }
bestBid(): number | undefined { return this.bids.best(); }
bestAsk(): number | undefined { return this.asks.best(); }
protected side(s: Side): BookSide { return s === 'buy' ? this.bids : this.asks; }
protected opposite(s: Side): BookSide { return s === 'buy' ? this.asks : this.bids; }
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. protected match(o: Order): Trade[] { const trades: Trade[] = []; const opposite = this.opposite(o.side); for (let price = opposite.best(); price !== undefined && o.remaining > 0 && crosses(o, price); price = opposite.best()) { const level = opposite.orders(price); while (level.length > 0 && o.remaining > 0) { const r = level[0]; const quantity = Math.min(o.remaining, r.remaining); trades.push(tradeOf(o, r, price, quantity)); o.remaining -= quantity; r.remaining -= quantity; if (r.remaining === 0) { level.shift(); this.resting.delete(r.id); } } if (level.length === 0) opposite.remove(price); } return trades; }
protected rest(o: Order): void { this.side(o.side).add(o); this.resting.set(o.id, o); }}Go deeperRemoving an order from the middle of a level1 min
How fast this is depends on the queue. Java keeps each level as a LinkedHashMap by id, and C#
keeps the LinkedListNode, so removal is immediate. Python, Go and TypeScript use a list, and
search just that one level, which usually holds a handful of orders.
Stage 4: Order types and market depth
Section titled “Stage 4: Order types and market depth”Traders also want market orders, orders that must fill immediately or not at all, and a screen showing the best few prices. Where does each go?
Whether the order types become a field the matching loop respects, instead of new copies of the loop. And whether you see that fill-or-kill has to be checked before matching, since a trade can’t be undone.
Every order now carries a TimeInForce, which says what happens to the part that doesn’t fill
right away:
- GTC, good till cancelled: the rest waits on the book. This is the limit order we’ve had so far.
- IOC, immediate or cancel: trade what’s possible now, and drop the rest.
- FOK, fill or kill: fill the whole quantity now, or do nothing at all.
All three use the same matching loop. Only the end differs: GTC puts the rest on the book, IOC drops it. FOK is the one that can’t be fixed afterwards, because trades already reported can’t be taken back. So before matching, the book adds up what’s available at prices the order can reach, and stops counting once it’s enough. If it falls short, the order is refused with no trades.
A market order says “any price”. It’s a limit order with the worst possible price: the maximum for a buy, and zero for a sell, so it reaches every level. It’s always IOC. A market order waiting on the book at “any price” would be an offer anyone could take at an absurd price.
Depth is what the trading screen shows: the first few levels of a side, each with its price and
the total quantity there. With levels kept sorted, it’s a walk over the first few keys.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// How long an order may wait. GTC rests on the book until filled or cancelled. IOC takes what it// can right now and drops the rest. FOK fills completely right now, or does nothing at all.enum TimeInForce { GTC, IOC, FOK }
// Prices are whole ticks (cents here), never floating point: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly. remaining counts down as the order fills.class Order { final String id; final Side side; final long price; final long quantity; final TimeInForce tif; long remaining;
Order(String id, Side side, long price, long quantity, TimeInForce tif) { this.id = id; this.side = side; this.price = price; this.quantity = quantity; this.tif = tif; this.remaining = quantity; }
static Order limit(String id, Side side, long price, long quantity) { return new Order(id, side, price, quantity, TimeInForce.GTC); }
// A market order is a limit order at the worst possible price, so it crosses every level, and // IOC, so it never rests on the book at that absurd price. static Order market(String id, Side side, long quantity) { return new Order(id, side, side == Side.BUY ? Long.MAX_VALUE : 0, quantity, TimeInForce.IOC); }}
// One row of the market data screen: a price and the total quantity resting there.record Depth(long price, long quantity) {}
// One symbol's book. Bids are sorted highest first and asks lowest first, so the best price on// each side is always the first key. `resting` finds any order by id, so a cancel never searches.class OrderBook { private final TreeMap<Long, Level> bids = new TreeMap<>(Comparator.reverseOrder()); private final TreeMap<Long, Level> asks = new TreeMap<>(); private final Map<String, Order> resting = new HashMap<>();
// FOK checks first, because a fill can't be undone. Then match. What is left of a GTC order // rests on the book; IOC and FOK leftovers are dropped. List<Trade> submit(Order o) { if (o.tif == TimeInForce.FOK && fillable(o) < o.quantity) return List.of(); List<Trade> trades = match(o); if (o.remaining > 0 && o.tif == TimeInForce.GTC) rest(o); return trades; }
// An empty price level is removed too, so the first key is always a real price. boolean cancel(String id) { Order o = resting.remove(id); if (o == null) return false; TreeMap<Long, Level> side = sideOf(o.side); Level level = side.get(o.price); level.orders.remove(id); if (level.orders.isEmpty()) side.remove(o.price); return true; }
// Shrinking an order keeps its place in the queue. A new price or a bigger size is a cancel and // a new order, so it goes to the back of the line, as on real exchanges. List<Trade> modify(String id, long price, long quantity) { Order o = resting.get(id); if (o == null) return List.of(); if (price == o.price && quantity <= o.remaining) { if (quantity == 0) cancel(id); else o.remaining = quantity; return List.of(); } cancel(id); return submit(Order.limit(id, o.side, price, quantity)); }
Long bestBid() { return bids.isEmpty() ? null : bids.firstKey(); }
Long bestAsk() { return asks.isEmpty() ? null : asks.firstKey(); }
// The top levels of one side, best first, as a trading screen shows them. List<Depth> depth(Side side, int levels) { List<Depth> out = new ArrayList<>(); for (Level l : sideOf(side).values()) { if (out.size() == levels) break; out.add(new Depth(l.price, l.orders.values().stream().mapToLong(o -> o.remaining).sum())); } return out; }
// How much of this order could trade right now, stopping once it's enough. FOK needs the // answer before matching, because a fill can't be undone. private long fillable(Order o) { long total = 0; for (Level l : sideOf(o.side == Side.BUY ? Side.SELL : Side.BUY).values()) { if (!crosses(o, l.price) || total >= o.quantity) break; for (Order r : l.orders.values()) total += r.remaining; } return total; }
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. private List<Trade> match(Order o) { List<Trade> trades = new ArrayList<>(); TreeMap<Long, Level> opposite = sideOf(o.side == Side.BUY ? Side.SELL : Side.BUY); while (o.remaining > 0 && !opposite.isEmpty()) { Level best = opposite.firstEntry().getValue(); if (!crosses(o, best.price)) break; Iterator<Order> it = best.orders.values().iterator(); while (o.remaining > 0 && it.hasNext()) { Order r = it.next(); long qty = Math.min(o.remaining, r.remaining); trades.add(o.side == Side.BUY ? new Trade(o.id, r.id, best.price, qty) : new Trade(r.id, o.id, best.price, qty)); o.remaining -= qty; r.remaining -= qty; if (r.remaining == 0) { it.remove(); resting.remove(r.id); } } if (best.orders.isEmpty()) opposite.pollFirstEntry(); } return trades; }
private TreeMap<Long, Level> sideOf(Side s) { return s == Side.BUY ? bids : asks; }
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it. static boolean crosses(Order o, long price) { return o.side == Side.BUY ? price <= o.price : price >= o.price; }
private void rest(Order o) { sideOf(o.side).computeIfAbsent(o.price, Level::new).orders.put(o.id, o); resting.put(o.id, o); }}# How long an order may wait. GTC rests on the book until filled or cancelled. IOC takes# what it can right now and drops the rest. FOK fills completely right now, or does nothing.class TimeInForce(Enum): GTC = auto() IOC = auto() FOK = auto()
MAX_PRICE = 2**63 - 1 # what a market buy is willing to pay: anything
# Prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an exchange# must compare prices exactly. remaining counts down as the order fills.@dataclass(eq=False)class Order: id: str side: Side price: int quantity: int tif: TimeInForce = TimeInForce.GTC remaining: int = -1
def __post_init__(self): if self.remaining < 0: self.remaining = self.quantity
# A market order is a limit order at the worst possible price, so it crosses every # level, and IOC, so it never rests on the book at that absurd price. @staticmethod def market(id: str, side: Side, quantity: int) -> Order: return Order(id, side, MAX_PRICE if side is Side.BUY else 0, quantity, TimeInForce.IOC)
# One row of the market data screen: a price and the total quantity resting there.@dataclass(frozen=True)class Depth: price: int quantity: int
# One symbol's book: bids and asks, each sorted best first. _resting finds any order by id,# so a cancel never searches.class OrderBook: def __init__(self): self._bids = BookSide(highest_first=True) self._asks = BookSide(highest_first=False) self._resting: dict[str, Order] = {}
# FOK checks first, because a fill can't be undone. Then match. What is left of a GTC # order rests on the book; IOC and FOK leftovers are dropped. def submit(self, o: Order) -> list[Trade]: if o.tif is TimeInForce.FOK and self._fillable(o) < o.quantity: return [] trades = self._match(o) if o.remaining > 0 and o.tif is TimeInForce.GTC: self._rest(o) return trades
# An empty price level is removed too, so the best level is always a real price. def cancel(self, id: str) -> bool: o = self._resting.pop(id, None) if o is None: return False side = self._side(o.side) level = side.get(o.price) del level.orders[id] if not level.orders: side.remove(o.price) return True
# Shrinking an order keeps its place in the queue. A new price or a bigger size is a # cancel and a new order, so it goes to the back of the line, as on real exchanges. def modify(self, id: str, price: int, quantity: int) -> list[Trade]: o = self._resting.get(id) if o is None: return [] if price == o.price and quantity <= o.remaining: if quantity == 0: self.cancel(id) else: o.remaining = quantity return [] self.cancel(id) return self.submit(Order(id, o.side, price, quantity))
def best_bid(self) -> int | None: best = self._bids.best() return best.price if best else None
def best_ask(self) -> int | None: best = self._asks.best() return best.price if best else None
# The top levels of one side, best first, as a trading screen shows them. def depth(self, side: Side, levels: int) -> list[Depth]: return [ Depth(l.price, sum(o.remaining for o in l.orders.values())) for l in self._side(side).best_first()[:levels] ]
# How much of this order could trade right now, stopping once it's enough. FOK needs # the answer before matching, because a fill can't be undone. def _fillable(self, o: Order) -> int: total = 0 for l in self._side(Side.SELL if o.side is Side.BUY else Side.BUY).best_first(): if not crosses(o, l.price) or total >= o.quantity: break total += sum(r.remaining for r in l.orders.values()) return total
def _side(self, side: Side) -> BookSide: return self._bids if side is Side.BUY else self._asks
# Price first, then time: take the best price level, and in it the oldest order, until # the incoming order is filled or no longer crosses. def _match(self, o: Order) -> list[Trade]: trades = [] opposite = self._asks if o.side is Side.BUY else self._bids while o.remaining > 0 and (best := opposite.best()) is not None and crosses(o, best.price): for r in list(best.orders.values()): if o.remaining == 0: break qty = min(o.remaining, r.remaining) trades.append(Trade(o.id, r.id, best.price, qty) if o.side is Side.BUY else Trade(r.id, o.id, best.price, qty)) o.remaining -= qty r.remaining -= qty if r.remaining == 0: del best.orders[r.id] del self._resting[r.id] if not best.orders: opposite.remove(best.price) return trades
def _rest(self, o: Order) -> None: self._side(o.side).level(o.price).orders[o.id] = o self._resting[o.id] = o// How long an order may wait. Gtc rests on the book until filled or cancelled. Ioc takes what it// can right now and drops the rest. Fok fills completely right now, or does nothing at all.public enum TimeInForce { Gtc, Ioc, Fok }
// Prices are whole ticks (cents here), never floating point: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly. Remaining counts down as the order fills.public sealed class Order{ public Order(string id, Side side, long price, long quantity, TimeInForce tif = TimeInForce.Gtc) => (Id, Side, Price, Quantity, Tif, Remaining) = (id, side, price, quantity, tif, quantity);
public string Id { get; } public Side Side { get; } public long Price { get; } public long Quantity { get; } public TimeInForce Tif { get; } public long Remaining { get; set; }
// A market order is a limit order at the worst possible price, so it crosses every level, // and Ioc, so it never rests on the book at that absurd price. public static Order Market(string id, Side side, long quantity) => new(id, side, side == Side.Buy ? long.MaxValue : 0, quantity, TimeInForce.Ioc);}
// One row of the market data screen: a price and the total quantity resting there.public sealed record Depth(long Price, long Quantity);
// The book plus time in force. FOK checks first, because a fill can't be undone. What is left of// a Gtc order rests; Ioc and Fok leftovers are dropped.public sealed class TimedOrderBook : OrderBook{ public override List<Trade> Submit(Order o) { if (o.Tif == TimeInForce.Fok && Fillable(o) < o.Quantity) return new(); var trades = Match(o); if (o.Remaining > 0 && o.Tif == TimeInForce.Gtc) Rest(o); return trades; }
// Depth returns the top levels of one side, best first, as a trading screen shows them. public List<Depth> DepthOf(Side side, int levels) => SideOf(side).Take(levels).Select(kv => new Depth(kv.Key, kv.Value.Sum(o => o.Remaining))).ToList();
// Fillable is how much of this order could trade right now, stopping once it's enough. Fok // needs the answer before matching, because a fill can't be undone. private long Fillable(Order o) { long total = 0; foreach (var (price, level) in Opposite(o.Side)) { if (!Crosses(o, price) || total >= o.Quantity) break; total += level.Sum(r => r.Remaining); } return total; }}// TimeInForce is how long an order may wait. GTC rests on the book until filled or cancelled.// IOC takes what it can right now and drops the rest. FOK fills completely right now, or does// nothing at all.type TimeInForce int
const ( GTC TimeInForce = iota IOC FOK)
// Order prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an exchange// must compare prices exactly. Remaining counts down as the order fills.type Order struct { ID string Side Side Price int64 Quantity int64 TIF TimeInForce Remaining int64}
func NewOrder(id string, side Side, price, quantity int64, tif TimeInForce) *Order { return &Order{id, side, price, quantity, tif, quantity}}
func Limit(id string, side Side, price, quantity int64) *Order { return NewOrder(id, side, price, quantity, GTC)}
// Market is a limit order at the worst possible price, so it crosses every level, and IOC, so it// never rests on the book at that absurd price.func Market(id string, side Side, quantity int64) *Order { price := int64(0) if side == Buy { price = math.MaxInt64 } return NewOrder(id, side, price, quantity, IOC)}
// Depth is one row of the market data screen: a price and the total quantity resting there.type Depth struct{ Price, Quantity int64 }
// Submit checks FOK first, because a fill can't be undone. Then it matches. What is left of a GTC// order rests on the book; IOC and FOK leftovers are dropped.func (b *OrderBook) Submit(o *Order) []Trade { if o.TIF == FOK && b.fillable(o) < o.Quantity { return nil } trades := b.match(o) if o.Remaining > 0 && o.TIF == GTC { b.rest(o) } return trades}
// Modify keeps an order's place in the queue when it only shrinks. A new price or a bigger size// is a cancel and a new order, so it goes to the back of the line, as on real exchanges.func (b *OrderBook) Modify(id string, price, quantity int64) []Trade { o, ok := b.resting[id] if !ok { return nil } if price == o.Price && quantity <= o.Remaining { if quantity == 0 { b.Cancel(id) } else { o.Remaining = quantity } return nil } b.Cancel(id) return b.Submit(Limit(id, o.Side, price, quantity))}
// Depth returns the top levels of one side, best first, as a trading screen shows them.func (b *OrderBook) Depth(side Side, levels int) []Depth { var out []Depth for _, l := range b.side(side).levels { if len(out) == levels { break } var total int64 for _, o := range l.Orders { total += o.Remaining } out = append(out, Depth{l.Price, total}) } return out}
// fillable is how much of this order could trade right now, stopping once it's enough. FOK// needs the answer before matching, because a fill can't be undone.func (b *OrderBook) fillable(o *Order) int64 { var total int64 for _, l := range b.opposite(o.Side).levels { if !crosses(o, l.Price) || total >= o.Quantity { break } for _, r := range l.Orders { total += r.Remaining } } return total}// How long an order may wait. gtc rests on the book until filled or cancelled. ioc takes what it// can right now and drops the rest. fok fills completely right now, or does nothing at all.type TimeInForce = 'gtc' | 'ioc' | 'fok';
// Prices are whole ticks (cents here), never fractions: 0.1 + 0.2 isn't 0.3, and an exchange must// compare prices exactly. remaining counts down as the order fills.class Order { remaining: number;
constructor( readonly id: string, readonly side: Side, readonly price: number, readonly quantity: number, readonly tif: TimeInForce = 'gtc', ) { this.remaining = quantity; }
// A market order is a limit order at the worst possible price, so it crosses every level, and // ioc, so it never rests on the book at that absurd price. static market(id: string, side: Side, quantity: number): Order { return new Order(id, side, side === 'buy' ? Number.MAX_SAFE_INTEGER : 0, quantity, 'ioc'); }}
// One row of the market data screen: a price and the total quantity resting there.type Depth = { price: number; quantity: number };
// The book plus time in force. fok checks first, because a fill can't be undone. What is left of// a gtc order rests; ioc and fok leftovers are dropped.class TimedOrderBook extends OrderBook { submit(o: Order): Trade[] { if (o.tif === 'fok' && this.fillable(o) < o.quantity) return []; const trades = this.match(o); if (o.remaining > 0 && o.tif === 'gtc') this.rest(o); return trades; }
// depth returns the top levels of one side, best first, as a trading screen shows them. depth(side: Side, levels: number): Depth[] { const book = this.side(side); return book .bestFirst() .slice(0, levels) .map((price) => ({ price, quantity: sum(book.orders(price)) })); }
// fillable is how much of this order could trade right now, stopping once it's enough. fok // needs the answer before matching, because a fill can't be undone. private fillable(o: Order): number { const book = this.opposite(o.side); let total = 0; for (const price of book.bestFirst()) { if (!crosses(o, price) || total >= o.quantity) break; total += sum(book.orders(price)); } return total; }}
function sum(orders: Order[]): number { return orders.reduce((total, o) => total + o.remaining, 0);}In C# and TypeScript, these changes live in a TimedOrderBook subclass of the stage 3 book. In Java,
Python and Go, the book itself changes.
Go deeperThe same buy as GTC, IOC and FOK1 min
Take the book from stage 2 again, and a buy for 20 shares with a limit of 102. At 101 and 102 there are 2 + 6 + 5 = 13 shares.
- GTC: it buys all 13, and the other 7 rest on the book as the new best bid at 102.
- IOC: it buys all 13, and the other 7 are dropped. Nothing rests.
- FOK: before matching, the book counts 13 available, which is less than 20. The order is refused, with no trades, and the book is exactly as it was.
A market buy for 20 would go further. After 101 and 102, it would take Hal’s 1 share at 105. Then it would stop with 6 unfilled, and drop them, because it’s IOC.
This is enough for a mid-level answer. Matching by price, then time, using sorted levels. Cancel through an index by id. And order types as a field. The rest of the page is what makes it a senior answer.
The full design
Section titled “The full design”Here is everything together, including the Exchange that we add next. Gateways call the exchange.
The exchange finds the symbol’s book. The book matches against sorted levels, and returns trades.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
// The complete design, built from every stage on this page.
import java.util.*;import java.util.concurrent.*;import java.util.concurrent.atomic.*;
// Which way an order goes. A buy matches against sells, and a sell against buys.enum Side { BUY, SELL }
// Prices are whole ticks (cents here), never floating point: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly. remaining counts down as the order fills.class Order { final String id; final Side side; final long price; final long quantity; final TimeInForce tif; long remaining;
Order(String id, Side side, long price, long quantity, TimeInForce tif) { this.id = id; this.side = side; this.price = price; this.quantity = quantity; this.tif = tif; this.remaining = quantity; }
static Order limit(String id, Side side, long price, long quantity) { return new Order(id, side, price, quantity, TimeInForce.GTC); }
// A market order is a limit order at the worst possible price, so it crosses every level, and // IOC, so it never rests on the book at that absurd price. static Order market(String id, Side side, long quantity) { return new Order(id, side, side == Side.BUY ? Long.MAX_VALUE : 0, quantity, TimeInForce.IOC); }}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.record Trade(String buyId, String sellId, long price, long quantity) {}
// One symbol's book. Bids are sorted highest first and asks lowest first, so the best price on// each side is always the first key. `resting` finds any order by id, so a cancel never searches.class OrderBook { private final TreeMap<Long, Level> bids = new TreeMap<>(Comparator.reverseOrder()); private final TreeMap<Long, Level> asks = new TreeMap<>(); private final Map<String, Order> resting = new HashMap<>();
// FOK checks first, because a fill can't be undone. Then match. What is left of a GTC order // rests on the book; IOC and FOK leftovers are dropped. List<Trade> submit(Order o) { if (o.tif == TimeInForce.FOK && fillable(o) < o.quantity) return List.of(); List<Trade> trades = match(o); if (o.remaining > 0 && o.tif == TimeInForce.GTC) rest(o); return trades; }
// An empty price level is removed too, so the first key is always a real price. boolean cancel(String id) { Order o = resting.remove(id); if (o == null) return false; TreeMap<Long, Level> side = sideOf(o.side); Level level = side.get(o.price); level.orders.remove(id); if (level.orders.isEmpty()) side.remove(o.price); return true; }
// Shrinking an order keeps its place in the queue. A new price or a bigger size is a cancel and // a new order, so it goes to the back of the line, as on real exchanges. List<Trade> modify(String id, long price, long quantity) { Order o = resting.get(id); if (o == null) return List.of(); if (price == o.price && quantity <= o.remaining) { if (quantity == 0) cancel(id); else o.remaining = quantity; return List.of(); } cancel(id); return submit(Order.limit(id, o.side, price, quantity)); }
Long bestBid() { return bids.isEmpty() ? null : bids.firstKey(); }
Long bestAsk() { return asks.isEmpty() ? null : asks.firstKey(); }
// The top levels of one side, best first, as a trading screen shows them. List<Depth> depth(Side side, int levels) { List<Depth> out = new ArrayList<>(); for (Level l : sideOf(side).values()) { if (out.size() == levels) break; out.add(new Depth(l.price, l.orders.values().stream().mapToLong(o -> o.remaining).sum())); } return out; }
// How much of this order could trade right now, stopping once it's enough. FOK needs the // answer before matching, because a fill can't be undone. private long fillable(Order o) { long total = 0; for (Level l : sideOf(o.side == Side.BUY ? Side.SELL : Side.BUY).values()) { if (!crosses(o, l.price) || total >= o.quantity) break; for (Order r : l.orders.values()) total += r.remaining; } return total; }
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. private List<Trade> match(Order o) { List<Trade> trades = new ArrayList<>(); TreeMap<Long, Level> opposite = sideOf(o.side == Side.BUY ? Side.SELL : Side.BUY); while (o.remaining > 0 && !opposite.isEmpty()) { Level best = opposite.firstEntry().getValue(); if (!crosses(o, best.price)) break; Iterator<Order> it = best.orders.values().iterator(); while (o.remaining > 0 && it.hasNext()) { Order r = it.next(); long qty = Math.min(o.remaining, r.remaining); trades.add(o.side == Side.BUY ? new Trade(o.id, r.id, best.price, qty) : new Trade(r.id, o.id, best.price, qty)); o.remaining -= qty; r.remaining -= qty; if (r.remaining == 0) { it.remove(); resting.remove(r.id); } } if (best.orders.isEmpty()) opposite.pollFirstEntry(); } return trades; }
private TreeMap<Long, Level> sideOf(Side s) { return s == Side.BUY ? bids : asks; }
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it. static boolean crosses(Order o, long price) { return o.side == Side.BUY ? price <= o.price : price >= o.price; }
private void rest(Order o) { sideOf(o.side).computeIfAbsent(o.price, Level::new).orders.put(o.id, o); resting.put(o.id, o); }}
// All resting orders at one price, oldest first. A LinkedHashMap keeps arrival order and, from// stage 3, removes any order by id in constant time.class Level { final long price; final LinkedHashMap<String, Order> orders = new LinkedHashMap<>();
Level(long price) { this.price = price; }}
// How long an order may wait. GTC rests on the book until filled or cancelled. IOC takes what it// can right now and drops the rest. FOK fills completely right now, or does nothing at all.enum TimeInForce { GTC, IOC, FOK }
// One row of the market data screen: a price and the total quantity resting there.record Depth(long price, long quantity) {}
// The two kinds of request the journal records.enum Action { SUBMIT, CANCEL }
// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.record Command(long seq, String symbol, Action action, String id, Side side, long price, long quantity, TimeInForce tif) {}
// Many gateways send orders at once. Matching must see one symbol's orders one at a time, in a// single agreed order, or two orders can both take the same resting quantity. So each symbol's// book has its own lock: orders for different symbols never wait for each other. The sequence// number is taken under that lock too, so sequence order is exactly the order the book saw.class Exchange { // One symbol's book and its trades. Its monitor is that symbol's lock. private static final class Book { final OrderBook book = new OrderBook(); final List<Trade> trades = new ArrayList<>(); }
private final ConcurrentHashMap<String, Book> books = new ConcurrentHashMap<>(); private final AtomicLong sequence = new AtomicLong(); private final ConcurrentLinkedQueue<Command> journal = new ConcurrentLinkedQueue<>();
List<Trade> submit(String symbol, Order o) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { journal.add(new Command(sequence.incrementAndGet(), symbol, Action.SUBMIT, o.id, o.side, o.price, o.quantity, o.tif)); List<Trade> trades = b.book.submit(o); b.trades.addAll(trades); return trades; } }
boolean cancel(String symbol, String id) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { journal.add(new Command(sequence.incrementAndGet(), symbol, Action.CANCEL, id, null, 0, 0, null)); return b.book.cancel(id); } }
List<Trade> trades(String symbol) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { return List.copyOf(b.trades); } }
// Two symbols add to the queue at the same time, so entries can land slightly out of order. // Sorting by sequence number gives the one true order. List<Command> journal() { return journal.stream().sorted(Comparator.comparingLong(Command::seq)).toList(); }
// After a crash, or on a standby machine: the same commands in the same order give the same // books and the same trades, because matching depends on nothing else. static Exchange replay(List<Command> commands) { Exchange e = new Exchange(); for (Command c : commands) { if (c.action() == Action.SUBMIT) e.submit(c.symbol(), new Order(c.id(), c.side(), c.price(), c.quantity(), c.tif())); else e.cancel(c.symbol(), c.id()); } return e; }}# The complete design, built from every stage on this page.
from __future__ import annotations
import bisectimport itertoolsimport threadingfrom dataclasses import dataclass, fieldfrom enum import Enum, auto
# Which way an order goes. A buy matches against sells, and a sell against buys.class Side(Enum): BUY = auto() SELL = auto()
# How long an order may wait. GTC rests on the book until filled or cancelled. IOC takes# what it can right now and drops the rest. FOK fills completely right now, or does nothing.class TimeInForce(Enum): GTC = auto() IOC = auto() FOK = auto()
# Prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an exchange# must compare prices exactly. remaining counts down as the order fills.@dataclass(eq=False)class Order: id: str side: Side price: int quantity: int tif: TimeInForce = TimeInForce.GTC remaining: int = -1
def __post_init__(self): if self.remaining < 0: self.remaining = self.quantity
# A market order is a limit order at the worst possible price, so it crosses every # level, and IOC, so it never rests on the book at that absurd price. @staticmethod def market(id: str, side: Side, quantity: int) -> Order: return Order(id, side, MAX_PRICE if side is Side.BUY else 0, quantity, TimeInForce.IOC)
# Two orders met: who bought, who sold, how many, and at what price. The price is always the# resting order's: it was there first.@dataclass(frozen=True)class Trade: buy_id: str sell_id: str price: int quantity: int
# One symbol's book: bids and asks, each sorted best first. _resting finds any order by id,# so a cancel never searches.class OrderBook: def __init__(self): self._bids = BookSide(highest_first=True) self._asks = BookSide(highest_first=False) self._resting: dict[str, Order] = {}
# FOK checks first, because a fill can't be undone. Then match. What is left of a GTC # order rests on the book; IOC and FOK leftovers are dropped. def submit(self, o: Order) -> list[Trade]: if o.tif is TimeInForce.FOK and self._fillable(o) < o.quantity: return [] trades = self._match(o) if o.remaining > 0 and o.tif is TimeInForce.GTC: self._rest(o) return trades
# An empty price level is removed too, so the best level is always a real price. def cancel(self, id: str) -> bool: o = self._resting.pop(id, None) if o is None: return False side = self._side(o.side) level = side.get(o.price) del level.orders[id] if not level.orders: side.remove(o.price) return True
# Shrinking an order keeps its place in the queue. A new price or a bigger size is a # cancel and a new order, so it goes to the back of the line, as on real exchanges. def modify(self, id: str, price: int, quantity: int) -> list[Trade]: o = self._resting.get(id) if o is None: return [] if price == o.price and quantity <= o.remaining: if quantity == 0: self.cancel(id) else: o.remaining = quantity return [] self.cancel(id) return self.submit(Order(id, o.side, price, quantity))
def best_bid(self) -> int | None: best = self._bids.best() return best.price if best else None
def best_ask(self) -> int | None: best = self._asks.best() return best.price if best else None
# The top levels of one side, best first, as a trading screen shows them. def depth(self, side: Side, levels: int) -> list[Depth]: return [ Depth(l.price, sum(o.remaining for o in l.orders.values())) for l in self._side(side).best_first()[:levels] ]
# How much of this order could trade right now, stopping once it's enough. FOK needs # the answer before matching, because a fill can't be undone. def _fillable(self, o: Order) -> int: total = 0 for l in self._side(Side.SELL if o.side is Side.BUY else Side.BUY).best_first(): if not crosses(o, l.price) or total >= o.quantity: break total += sum(r.remaining for r in l.orders.values()) return total
def _side(self, side: Side) -> BookSide: return self._bids if side is Side.BUY else self._asks
# Price first, then time: take the best price level, and in it the oldest order, until # the incoming order is filled or no longer crosses. def _match(self, o: Order) -> list[Trade]: trades = [] opposite = self._asks if o.side is Side.BUY else self._bids while o.remaining > 0 and (best := opposite.best()) is not None and crosses(o, best.price): for r in list(best.orders.values()): if o.remaining == 0: break qty = min(o.remaining, r.remaining) trades.append(Trade(o.id, r.id, best.price, qty) if o.side is Side.BUY else Trade(r.id, o.id, best.price, qty)) o.remaining -= qty r.remaining -= qty if r.remaining == 0: del best.orders[r.id] del self._resting[r.id] if not best.orders: opposite.remove(best.price) return trades
def _rest(self, o: Order) -> None: self._side(o.side).level(o.price).orders[o.id] = o self._resting[o.id] = o
# All resting orders at one price, oldest first. A dict keeps arrival order and, from# stage 3, removes any order by id in constant time.class Level: def __init__(self, price: int): self.price = price self.orders: dict[str, Order] = {}
# One side of the book: levels by price, and the prices kept sorted so the best is found# without scanning orders. Bids want the highest price first, asks the lowest.class BookSide: def __init__(self, highest_first: bool): self._levels: dict[int, Level] = {} self._prices: list[int] = [] # ascending self._highest_first = highest_first
# The best price level, or None when this side is empty. def best(self) -> Level | None: if not self._prices: return None return self._levels[self._prices[-1] if self._highest_first else self._prices[0]]
def best_first(self) -> list[Level]: prices = reversed(self._prices) if self._highest_first else self._prices return [self._levels[p] for p in prices]
# The level at this price, created (and its price slotted into order) if it is new. def level(self, price: int) -> Level: if price not in self._levels: self._levels[price] = Level(price) bisect.insort(self._prices, price) return self._levels[price]
def get(self, price: int) -> Level | None: return self._levels.get(price)
def remove(self, price: int) -> None: del self._levels[price] self._prices.pop(bisect.bisect_left(self._prices, price))
# A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it.def crosses(o: Order, price: int) -> bool: return price <= o.price if o.side is Side.BUY else price >= o.price
MAX_PRICE = 2**63 - 1 # what a market buy is willing to pay: anything
# One row of the market data screen: a price and the total quantity resting there.@dataclass(frozen=True)class Depth: price: int quantity: int
# The two kinds of request the journal records.class Action(Enum): SUBMIT = auto() CANCEL = auto()
# One accepted request, numbered. The journal of these is the exchange's source of truth:# replaying it in sequence order rebuilds every book and every trade exactly.@dataclass(frozen=True)class Command: seq: int symbol: str action: Action id: str side: Side | None = None price: int = 0 quantity: int = 0 tif: TimeInForce | None = None
# One symbol's book, its trades, and the lock that guards both.@dataclass(eq=False)class _Book: book: OrderBook = field(default_factory=OrderBook) trades: list[Trade] = field(default_factory=list) lock: threading.Lock = field(default_factory=threading.Lock)
# Many gateways send orders at once. Matching must see one symbol's orders one at a time,# in a single agreed order, or two orders can both take the same resting quantity. So each# symbol's book has its own lock: orders for different symbols never wait for each other. The# sequence number is taken under that lock too, so sequence order is exactly the order the# book saw.class Exchange: def __init__(self): self._books: dict[str, _Book] = {} self._books_lock = threading.Lock() self._seq = itertools.count(1) self._journal: list[Command] = []
def submit(self, symbol: str, o: Order) -> list[Trade]: b = self._book(symbol) with b.lock: self._journal.append(Command(next(self._seq), symbol, Action.SUBMIT, o.id, o.side, o.price, o.quantity, o.tif)) trades = b.book.submit(o) b.trades.extend(trades) return trades
def cancel(self, symbol: str, id: str) -> bool: b = self._book(symbol) with b.lock: self._journal.append(Command(next(self._seq), symbol, Action.CANCEL, id)) return b.book.cancel(id)
def trades(self, symbol: str) -> list[Trade]: b = self._book(symbol) with b.lock: return list(b.trades)
# Two symbols add entries at the same time, so they can land slightly out of order. # Sorting by sequence number gives the one true order. def journal(self) -> list[Command]: return sorted(self._journal, key=lambda c: c.seq)
# After a crash, or on a standby machine: the same commands in the same order give the # same books and the same trades, because matching depends on nothing else. @staticmethod def replay(commands: list[Command]) -> Exchange: e = Exchange() for c in commands: if c.action is Action.SUBMIT: e.submit(c.symbol, Order(c.id, c.side, c.price, c.quantity, c.tif)) else: e.cancel(c.symbol, c.id) return e
def _book(self, symbol: str) -> _Book: with self._books_lock: return self._books.setdefault(symbol, _Book())// The complete design, built from every stage on this page.
using System.Collections.Concurrent;
// Which way an order goes. A buy matches against sells, and a sell against buys.public enum Side { Buy, Sell }
// Prices are whole ticks (cents here), never floating point: 0.1 + 0.2 isn't 0.3, and an// exchange must compare prices exactly. Remaining counts down as the order fills.public sealed class Order{ public Order(string id, Side side, long price, long quantity, TimeInForce tif = TimeInForce.Gtc) => (Id, Side, Price, Quantity, Tif, Remaining) = (id, side, price, quantity, tif, quantity);
public string Id { get; } public Side Side { get; } public long Price { get; } public long Quantity { get; } public TimeInForce Tif { get; } public long Remaining { get; set; }
// A market order is a limit order at the worst possible price, so it crosses every level, // and Ioc, so it never rests on the book at that absurd price. public static Order Market(string id, Side side, long quantity) => new(id, side, side == Side.Buy ? long.MaxValue : 0, quantity, TimeInForce.Ioc);}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.public sealed record Trade(string BuyId, string SellId, long Price, long Quantity);
// One symbol's book. Bids are sorted highest first and asks lowest first, so the best price on// each side is always the first key. Each level is a queue of orders, oldest first.public class OrderBook{ protected readonly SortedDictionary<long, LinkedList<Order>> Bids = new(Comparer<long>.Create((a, b) => b.CompareTo(a))); protected readonly SortedDictionary<long, LinkedList<Order>> Asks = new(); // Each resting order's node in its level, by id, so cancel removes it without searching. protected readonly Dictionary<string, LinkedListNode<Order>> Resting = new();
// Match what crosses, then rest what is left. Virtual, so the time-in-force book can add its // rules. public virtual List<Trade> Submit(Order o) { var trades = Match(o); if (o.Remaining > 0) Rest(o); return trades; }
// Cancel returns false if the order never existed, was already filled, or was already // cancelled. public bool Cancel(string id) { if (!Resting.Remove(id, out var node)) return false; var o = node.Value; var level = node.List!; level.Remove(node); if (level.Count == 0) SideOf(o.Side).Remove(o.Price); return true; }
// Modify keeps the order's place in line only when it lowers the quantity at the same // price: it asks for less, so nobody behind it is worse off. Anything else, a new price or // more quantity, would jump ahead of people who were there first, so it's a cancel and a new // order at the back. public List<Trade> Modify(string id, long price, long quantity) { if (!Resting.TryGetValue(id, out var node)) return new(); var o = node.Value; if (price == o.Price && quantity <= o.Remaining) { if (quantity == 0) Cancel(id); else o.Remaining = quantity; return new(); } Cancel(id); return Submit(new Order(o.Id, o.Side, price, quantity)); }
public long? BestBid() => Bids.Count > 0 ? Bids.First().Key : null;
public long? BestAsk() => Asks.Count > 0 ? Asks.First().Key : null;
protected SortedDictionary<long, LinkedList<Order>> SideOf(Side s) => s == Side.Buy ? Bids : Asks;
protected SortedDictionary<long, LinkedList<Order>> Opposite(Side s) => s == Side.Buy ? Asks : Bids;
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. protected List<Trade> Match(Order o) { var trades = new List<Trade>(); var opposite = Opposite(o.Side); while (o.Remaining > 0 && opposite.Count > 0) { var (price, level) = opposite.First(); if (!Crosses(o, price)) break; while (level.Count > 0 && o.Remaining > 0) { var r = level.First!.Value; var qty = Math.Min(o.Remaining, r.Remaining); trades.Add(TradeOf(o, r, price, qty)); o.Remaining -= qty; r.Remaining -= qty; if (r.Remaining == 0) { level.RemoveFirst(); Resting.Remove(r.Id); } } if (level.Count == 0) opposite.Remove(price); } return trades; }
protected void Rest(Order o) { var side = SideOf(o.Side); if (!side.TryGetValue(o.Price, out var level)) side[o.Price] = level = new LinkedList<Order>(); Resting[o.Id] = level.AddLast(o); }
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it. protected static bool Crosses(Order o, long price) => o.Side == Side.Buy ? price <= o.Price : price >= o.Price;
private static Trade TradeOf(Order incoming, Order resting, long price, long qty) => incoming.Side == Side.Buy ? new(incoming.Id, resting.Id, price, qty) : new(resting.Id, incoming.Id, price, qty);}
// How long an order may wait. Gtc rests on the book until filled or cancelled. Ioc takes what it// can right now and drops the rest. Fok fills completely right now, or does nothing at all.public enum TimeInForce { Gtc, Ioc, Fok }
// One row of the market data screen: a price and the total quantity resting there.public sealed record Depth(long Price, long Quantity);
// The book plus time in force. FOK checks first, because a fill can't be undone. What is left of// a Gtc order rests; Ioc and Fok leftovers are dropped.public sealed class TimedOrderBook : OrderBook{ public override List<Trade> Submit(Order o) { if (o.Tif == TimeInForce.Fok && Fillable(o) < o.Quantity) return new(); var trades = Match(o); if (o.Remaining > 0 && o.Tif == TimeInForce.Gtc) Rest(o); return trades; }
// Depth returns the top levels of one side, best first, as a trading screen shows them. public List<Depth> DepthOf(Side side, int levels) => SideOf(side).Take(levels).Select(kv => new Depth(kv.Key, kv.Value.Sum(o => o.Remaining))).ToList();
// Fillable is how much of this order could trade right now, stopping once it's enough. Fok // needs the answer before matching, because a fill can't be undone. private long Fillable(Order o) { long total = 0; foreach (var (price, level) in Opposite(o.Side)) { if (!Crosses(o, price) || total >= o.Quantity) break; total += level.Sum(r => r.Remaining); } return total; }}
// Not called Action, which would clash with System.Action.public enum CommandType { Submit, Cancel }
// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.public sealed record Command(long Seq, string Symbol, CommandType Type, string Id, Side Side = Side.Buy, long Price = 0, long Quantity = 0, TimeInForce Tif = TimeInForce.Gtc);
// Orders arrive from many gateways at once. Matching must see one symbol's orders one at a time,// in a single agreed order, or two orders can both take the same resting quantity. So each// symbol's book has its own lock: orders for different symbols never wait for each other. The// sequence number is taken under that lock too, so sequence order is exactly the order the book// saw.public sealed class Exchange{ // One symbol's book and its trades. Locking the Book object is that symbol's lock. private sealed class Book { public readonly TimedOrderBook Orders = new(); public readonly List<Trade> Trades = new(); }
private readonly ConcurrentDictionary<string, Book> _books = new(); private readonly ConcurrentQueue<Command> _journal = new(); private long _sequence;
public List<Trade> Submit(string symbol, Order o) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) { _journal.Enqueue(new Command(Interlocked.Increment(ref _sequence), symbol, CommandType.Submit, o.Id, o.Side, o.Price, o.Quantity, o.Tif)); var trades = book.Orders.Submit(o); book.Trades.AddRange(trades); return trades; } }
public bool Cancel(string symbol, string id) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) { _journal.Enqueue(new Command(Interlocked.Increment(ref _sequence), symbol, CommandType.Cancel, id)); return book.Orders.Cancel(id); } }
public List<Trade> Trades(string symbol) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) return new List<Trade>(book.Trades); }
// Two symbols take sequence numbers in parallel, so the queue can hold them slightly out of // order. Sorting by number gives the true order. public List<Command> Journal() => _journal.OrderBy(c => c.Seq).ToList();
// Replay rebuilds an exchange after a crash, or on a standby machine: the same commands in // the same order give the same books and the same trades, because matching depends on nothing // else. public static Exchange Replay(IEnumerable<Command> commands) { var e = new Exchange(); foreach (var c in commands) { if (c.Type == CommandType.Submit) e.Submit(c.Symbol, new Order(c.Id, c.Side, c.Price, c.Quantity, c.Tif)); else e.Cancel(c.Symbol, c.Id); } return e; }}// The complete design, built from every stage on this page.
package stockexchange
import ( "cmp" "math" "slices" "sort" "sync" "sync/atomic")
// Side is which way an order goes. A buy matches against sells, and a sell against buys.type Side int
const ( Buy Side = iota Sell)
// Trade is two orders meeting: who bought, who sold, how many, and at what price. The price is// always the resting order's: it was there first.type Trade struct { BuyID, SellID string Price, Quantity int64}
// Level holds all resting orders at one price, oldest first.type Level struct { Price int64 Orders []*Order}
// BookSide is one side of the book: levels kept sorted best first, so the best is always// levels[0]. Bids want the highest price first, asks the lowest. A sorted slice is simple and// fast for the few dozen prices near the market; an engine with thousands uses a tree or an// array indexed by tick.type BookSide struct { levels []*Level highestFirst bool}
// better reports whether price a is better than b for this side.func (s *BookSide) better(a, b int64) bool { if s.highestFirst { return a > b } return a < b}
func (s *BookSide) Best() *Level { if len(s.levels) == 0 { return nil } return s.levels[0]}
// find returns where price is, or would go, in the sorted levels.func (s *BookSide) find(price int64) int { return sort.Search(len(s.levels), func(i int) bool { return !s.better(s.levels[i].Price, price) })}
// Level returns the level at this price, inserting a new one in sorted place if needed.func (s *BookSide) Level(price int64) *Level { i := s.find(price) if i < len(s.levels) && s.levels[i].Price == price { return s.levels[i] } l := &Level{Price: price} s.levels = slices.Insert(s.levels, i, l) return l}
func (s *BookSide) Remove(price int64) { if i := s.find(price); i < len(s.levels) && s.levels[i].Price == price { s.levels = slices.Delete(s.levels, i, i+1) }}
// crosses: a buy crosses a sell priced at or below it; a sell crosses a buy at or above it.func crosses(o *Order, price int64) bool { if o.Side == Buy { return price <= o.Price } return price >= o.Price}
func tradeOf(incoming, resting *Order, price, qty int64) Trade { if incoming.Side == Buy { return Trade{incoming.ID, resting.ID, price, qty} } return Trade{resting.ID, incoming.ID, price, qty}}
func best(s *BookSide) (int64, bool) { if l := s.Best(); l != nil { return l.Price, true } return 0, false}
// OrderBook is one symbol's book: bids and asks, each sorted best first.type OrderBook struct { bids, asks *BookSide resting map[string]*Order // every resting order by id, so cancel needn't search the book}
func NewOrderBook() *OrderBook { return &OrderBook{bids: &BookSide{highestFirst: true}, asks: &BookSide{}, resting: map[string]*Order{}}}
func (b *OrderBook) side(s Side) *BookSide { if s == Buy { return b.bids } return b.asks}
func (b *OrderBook) opposite(s Side) *BookSide { return b.side(1 - s) }
// Cancel returns false if the order never existed, was already filled, or was already// cancelled. Removing it from its level scans that one level, which holds a handful of orders.// An empty price level is removed too, so Best is always a real price.func (b *OrderBook) Cancel(id string) bool { o, ok := b.resting[id] if !ok { return false } delete(b.resting, id) s := b.side(o.Side) l := s.Level(o.Price) l.Orders = slices.DeleteFunc(l.Orders, func(x *Order) bool { return x.ID == id }) if len(l.Orders) == 0 { s.Remove(o.Price) } return true}
func (b *OrderBook) BestBid() (int64, bool) { return best(b.bids) }
func (b *OrderBook) BestAsk() (int64, bool) { return best(b.asks) }
// match is price first, then time: take the best price level, and in it the oldest order, until// the incoming order is filled or no longer crosses.func (b *OrderBook) match(o *Order) []Trade { var trades []Trade opp := b.opposite(o.Side) for o.Remaining > 0 { best := opp.Best() if best == nil || !crosses(o, best.Price) { break } for len(best.Orders) > 0 && o.Remaining > 0 { r := best.Orders[0] qty := min(o.Remaining, r.Remaining) trades = append(trades, tradeOf(o, r, best.Price, qty)) o.Remaining -= qty r.Remaining -= qty if r.Remaining == 0 { best.Orders = best.Orders[1:] delete(b.resting, r.ID) } } if len(best.Orders) == 0 { opp.Remove(best.Price) } } return trades}
func (b *OrderBook) rest(o *Order) { l := b.side(o.Side).Level(o.Price) l.Orders = append(l.Orders, o) b.resting[o.ID] = o}
// TimeInForce is how long an order may wait. GTC rests on the book until filled or cancelled.// IOC takes what it can right now and drops the rest. FOK fills completely right now, or does// nothing at all.type TimeInForce int
const ( GTC TimeInForce = iota IOC FOK)
// Order prices are whole ticks (cents here), never floats: 0.1 + 0.2 isn't 0.3, and an exchange// must compare prices exactly. Remaining counts down as the order fills.type Order struct { ID string Side Side Price int64 Quantity int64 TIF TimeInForce Remaining int64}
func NewOrder(id string, side Side, price, quantity int64, tif TimeInForce) *Order { return &Order{id, side, price, quantity, tif, quantity}}
func Limit(id string, side Side, price, quantity int64) *Order { return NewOrder(id, side, price, quantity, GTC)}
// Market is a limit order at the worst possible price, so it crosses every level, and IOC, so it// never rests on the book at that absurd price.func Market(id string, side Side, quantity int64) *Order { price := int64(0) if side == Buy { price = math.MaxInt64 } return NewOrder(id, side, price, quantity, IOC)}
// Depth is one row of the market data screen: a price and the total quantity resting there.type Depth struct{ Price, Quantity int64 }
// Submit checks FOK first, because a fill can't be undone. Then it matches. What is left of a GTC// order rests on the book; IOC and FOK leftovers are dropped.func (b *OrderBook) Submit(o *Order) []Trade { if o.TIF == FOK && b.fillable(o) < o.Quantity { return nil } trades := b.match(o) if o.Remaining > 0 && o.TIF == GTC { b.rest(o) } return trades}
// Modify keeps an order's place in the queue when it only shrinks. A new price or a bigger size// is a cancel and a new order, so it goes to the back of the line, as on real exchanges.func (b *OrderBook) Modify(id string, price, quantity int64) []Trade { o, ok := b.resting[id] if !ok { return nil } if price == o.Price && quantity <= o.Remaining { if quantity == 0 { b.Cancel(id) } else { o.Remaining = quantity } return nil } b.Cancel(id) return b.Submit(Limit(id, o.Side, price, quantity))}
// Depth returns the top levels of one side, best first, as a trading screen shows them.func (b *OrderBook) Depth(side Side, levels int) []Depth { var out []Depth for _, l := range b.side(side).levels { if len(out) == levels { break } var total int64 for _, o := range l.Orders { total += o.Remaining } out = append(out, Depth{l.Price, total}) } return out}
// fillable is how much of this order could trade right now, stopping once it's enough. FOK// needs the answer before matching, because a fill can't be undone.func (b *OrderBook) fillable(o *Order) int64 { var total int64 for _, l := range b.opposite(o.Side).levels { if !crosses(o, l.Price) || total >= o.Quantity { break } for _, r := range l.Orders { total += r.Remaining } } return total}
// Action is one of the two kinds of request the journal records.type Action int
const ( SubmitAction Action = iota CancelAction)
// Command is one accepted request, numbered. The journal of these is the exchange's source of// truth: replaying it in sequence order rebuilds every book and every trade exactly.type Command struct { Seq int64 Symbol string Action Action ID string Side Side Price int64 Quantity int64 TIF TimeInForce}
// symbolBook is one symbol's book and its trades, with the mutex that guards both.type symbolBook struct { mu sync.Mutex book *OrderBook trades []Trade}
// Exchange takes orders from many gateways at once. Matching must see one symbol's orders one at// a time, in a single agreed order, or two orders can both take the same resting quantity. So// each symbol's book has its own mutex: orders for different symbols never wait for each other.// The sequence number is taken under that mutex too, so sequence order is exactly the order the// book saw.type Exchange struct { mu sync.Mutex // guards books and journal, never held while matching books map[string]*symbolBook sequence atomic.Int64 journal []Command}
func NewExchange() *Exchange { return &Exchange{books: map[string]*symbolBook{}} }
func (e *Exchange) book(symbol string) *symbolBook { e.mu.Lock() defer e.mu.Unlock() b, ok := e.books[symbol] if !ok { b = &symbolBook{book: NewOrderBook()} e.books[symbol] = b } return b}
func (e *Exchange) record(c Command) { e.mu.Lock() e.journal = append(e.journal, c) e.mu.Unlock()}
func (e *Exchange) Submit(symbol string, o *Order) []Trade { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() e.record(Command{e.sequence.Add(1), symbol, SubmitAction, o.ID, o.Side, o.Price, o.Quantity, o.TIF}) trades := b.book.Submit(o) b.trades = append(b.trades, trades...) return trades}
func (e *Exchange) Cancel(symbol, id string) bool { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() e.record(Command{Seq: e.sequence.Add(1), Symbol: symbol, Action: CancelAction, ID: id}) return b.book.Cancel(id)}
func (e *Exchange) Trades(symbol string) []Trade { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() return slices.Clone(b.trades)}
// Journal sorts by sequence number: two symbols append at the same time, so entries can land// slightly out of order.func (e *Exchange) Journal() []Command { e.mu.Lock() defer e.mu.Unlock() out := slices.Clone(e.journal) slices.SortFunc(out, func(a, b Command) int { return cmp.Compare(a.Seq, b.Seq) }) return out}
// Replay rebuilds an exchange after a crash, or on a standby machine: the same commands in the// same order give the same books and the same trades, because matching depends on nothing else.func Replay(commands []Command) *Exchange { e := NewExchange() for _, c := range commands { if c.Action == SubmitAction { e.Submit(c.Symbol, NewOrder(c.ID, c.Side, c.Price, c.Quantity, c.TIF)) } else { e.Cancel(c.Symbol, c.ID) } } return e}// The complete design, built from every stage on this page.
// Which way an order goes. A buy matches against sells, and a sell against buys.type Side = 'buy' | 'sell';
// Prices are whole ticks (cents here), never fractions: 0.1 + 0.2 isn't 0.3, and an exchange must// compare prices exactly. remaining counts down as the order fills.class Order { remaining: number;
constructor( readonly id: string, readonly side: Side, readonly price: number, readonly quantity: number, readonly tif: TimeInForce = 'gtc', ) { this.remaining = quantity; }
// A market order is a limit order at the worst possible price, so it crosses every level, and // ioc, so it never rests on the book at that absurd price. static market(id: string, side: Side, quantity: number): Order { return new Order(id, side, side === 'buy' ? Number.MAX_SAFE_INTEGER : 0, quantity, 'ioc'); }}
// Two orders met: who bought, who sold, how many, and at what price. The price is always the// resting order's: it was there first.type Trade = { buyId: string; sellId: string; price: number; quantity: number };
// One symbol's book: bids and asks, each sorted best first.class OrderBook { protected readonly bids = new BookSide(true); protected readonly asks = new BookSide(false); // Every resting order by id, so cancel needn't search the book. protected readonly resting = new Map<string, Order>();
// Match what crosses, then rest what is left. submit(o: Order): Trade[] { const trades = this.match(o); if (o.remaining > 0) this.rest(o); return trades; }
// cancel returns false if the order never existed, was already filled, or was already // cancelled. Removing it from its level scans that one level, which holds a handful of orders. cancel(id: string): boolean { const o = this.resting.get(id); if (!o) return false; this.resting.delete(id); const side = this.side(o.side); const level = side.orders(o.price); level.splice(level.indexOf(o), 1); if (level.length === 0) side.remove(o.price); return true; }
// modify keeps the order's place in line only when it lowers the quantity at the same price: // it asks for less, so nobody behind it is worse off. Anything else, a new price or more // quantity, would jump ahead of people who were there first, so it's a cancel and a new order // at the back. modify(id: string, price: number, quantity: number): Trade[] { const o = this.resting.get(id); if (!o) return []; if (price === o.price && quantity <= o.remaining) { if (quantity === 0) this.cancel(id); else o.remaining = quantity; return []; } this.cancel(id); return this.submit(new Order(id, o.side, price, quantity)); }
bestBid(): number | undefined { return this.bids.best(); }
bestAsk(): number | undefined { return this.asks.best(); }
protected side(s: Side): BookSide { return s === 'buy' ? this.bids : this.asks; }
protected opposite(s: Side): BookSide { return s === 'buy' ? this.asks : this.bids; }
// Price first, then time: take the best price level, and in it the oldest order, until the // incoming order is filled or no longer crosses. protected match(o: Order): Trade[] { const trades: Trade[] = []; const opposite = this.opposite(o.side); for (let price = opposite.best(); price !== undefined && o.remaining > 0 && crosses(o, price); price = opposite.best()) { const level = opposite.orders(price); while (level.length > 0 && o.remaining > 0) { const r = level[0]; const quantity = Math.min(o.remaining, r.remaining); trades.push(tradeOf(o, r, price, quantity)); o.remaining -= quantity; r.remaining -= quantity; if (r.remaining === 0) { level.shift(); this.resting.delete(r.id); } } if (level.length === 0) opposite.remove(price); } return trades; }
protected rest(o: Order): void { this.side(o.side).add(o); this.resting.set(o.id, o); }}
// One side of the book. Prices are kept sorted best first, so the best is always prices[0]:// bids want the highest price first, asks the lowest. Each price maps to its orders, oldest// first. A sorted array is simple and fast for the few dozen prices near the market; an engine// with thousands uses a tree or an array indexed by tick.class BookSide { private prices: number[] = []; private levels = new Map<number, Order[]>();
constructor(private readonly highestFirst: boolean) {}
// The best price on this side, or undefined when it is empty. best(): number | undefined { return this.prices[0]; }
// bestFirst lists the prices from best to worst, for walking the book. bestFirst(): readonly number[] { return this.prices; }
orders(price: number): Order[] { return this.levels.get(price) ?? []; }
add(o: Order): void { let level = this.levels.get(o.price); if (!level) { level = []; this.levels.set(o.price, level); this.prices.splice(this.find(o.price), 0, o.price); } level.push(o); }
remove(price: number): void { this.levels.delete(price); this.prices.splice(this.find(price), 1); }
// find returns where price is, or would go, by binary search. private find(price: number): number { let lo = 0; let hi = this.prices.length; while (lo < hi) { const mid = (lo + hi) >> 1; const p = this.prices[mid]; if (this.highestFirst ? p > price : p < price) lo = mid + 1; else hi = mid; } return lo; }}
// A buy crosses a sell priced at or below it; a sell crosses a buy priced at or above it.function crosses(o: Order, price: number): boolean { return o.side === 'buy' ? price <= o.price : price >= o.price;}
// The trade, written buyer first whichever side came in.function tradeOf(incoming: Order, resting: Order, price: number, quantity: number): Trade { return incoming.side === 'buy' ? { buyId: incoming.id, sellId: resting.id, price, quantity } : { buyId: resting.id, sellId: incoming.id, price, quantity };}
// How long an order may wait. gtc rests on the book until filled or cancelled. ioc takes what it// can right now and drops the rest. fok fills completely right now, or does nothing at all.type TimeInForce = 'gtc' | 'ioc' | 'fok';
// One row of the market data screen: a price and the total quantity resting there.type Depth = { price: number; quantity: number };
// The book plus time in force. fok checks first, because a fill can't be undone. What is left of// a gtc order rests; ioc and fok leftovers are dropped.class TimedOrderBook extends OrderBook { submit(o: Order): Trade[] { if (o.tif === 'fok' && this.fillable(o) < o.quantity) return []; const trades = this.match(o); if (o.remaining > 0 && o.tif === 'gtc') this.rest(o); return trades; }
// depth returns the top levels of one side, best first, as a trading screen shows them. depth(side: Side, levels: number): Depth[] { const book = this.side(side); return book .bestFirst() .slice(0, levels) .map((price) => ({ price, quantity: sum(book.orders(price)) })); }
// fillable is how much of this order could trade right now, stopping once it's enough. fok // needs the answer before matching, because a fill can't be undone. private fillable(o: Order): number { const book = this.opposite(o.side); let total = 0; for (const price of book.bestFirst()) { if (!crosses(o, price) || total >= o.quantity) break; total += sum(book.orders(price)); } return total; }}
function sum(orders: Order[]): number { return orders.reduce((total, o) => total + o.remaining, 0);}
// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.type Command = | { seq: number; symbol: string; action: 'submit'; id: string; side: Side; price: number; quantity: number; tif: TimeInForce } | { seq: number; symbol: string; action: 'cancel'; id: string };
// Orders arrive from many gateways at once. Matching must see one symbol's orders one at a time,// in a single agreed order, or two orders can both take the same resting quantity. JavaScript// runs one callback at a time, and submit has no await inside it, so each order is matched// completely before the next one starts. No lock is needed, and the sequence number is simply// the order the calls ran in.//// That also means one process matches every symbol. A real exchange runs one such single-threaded// matcher per symbol, or per group of symbols, often in a worker or process of its own.class Exchange { private readonly books = new Map<string, TimedOrderBook>(); private readonly tradesBySymbol = new Map<string, Trade[]>(); private readonly log: Command[] = []; private sequence = 0;
submit(symbol: string, o: Order): Trade[] { this.log.push({ seq: ++this.sequence, symbol, action: 'submit', id: o.id, side: o.side, price: o.price, quantity: o.quantity, tif: o.tif }); const trades = this.book(symbol).submit(o); this.trades(symbol).push(...trades); return trades; }
cancel(symbol: string, id: string): boolean { this.log.push({ seq: ++this.sequence, symbol, action: 'cancel', id }); return this.book(symbol).cancel(id); }
trades(symbol: string): Trade[] { let trades = this.tradesBySymbol.get(symbol); if (!trades) { trades = []; this.tradesBySymbol.set(symbol, trades); } return trades; }
journal(): readonly Command[] { return this.log; }
// replay rebuilds an exchange after a crash, or on a standby machine: the same commands in // the same order give the same books and the same trades, because matching depends on nothing // else. static replay(commands: readonly Command[]): Exchange { const e = new Exchange(); for (const c of commands) { if (c.action === 'submit') e.submit(c.symbol, new Order(c.id, c.side, c.price, c.quantity, c.tif)); else e.cancel(c.symbol, c.id); } return e; }
private book(symbol: string): TimedOrderBook { let book = this.books.get(symbol); if (!book) { book = new TimedOrderBook(); this.books.set(symbol, book); } return book; }}Three ideas hold it together:
- Price-time priority is the structure, not a check. Sorted levels with FIFO queues mean the matching loop can only take the next fair order. There’s no rule to forget.
- The book is pure. It has no clock, no threads and no input or output, so it’s easy to test, and replays exactly. A market order is built as a limit order with the right settings, by a small factory method.
- The journal is the truth. Books and trades can always be rebuilt from the numbered requests, stored as Commands. Gateways call one object, safely, from many threads (Facade).
Stress it
Section titled “Stress it”So far, one stock and one caller. Now orders for thousands of stocks arrive from many gateways at once.
Two buys for the last 100 shares
Section titled “Two buys for the last 100 shares”Orders for thousands of stocks arrive from many gateways at once. Two gateways send a buy for the last 100 shares at the same moment. Walk me through what happens. And if the machine crashes, how do you rebuild the books?
Whether you keep matching one at a time per book, while letting different books run in parallel. And whether you see that the sequence of accepted orders is the real state of the exchange. The red flags: a lock per order, one global lock, or a sequence number taken outside the lock.
The Exchange holds one book per symbol, and is what gateways call.
Two orders, one resting quantity. Both buys look at the best ask, both see 100 shares, and both take them. The shares are sold twice, the resting order’s remaining quantity goes negative, and two trades are reported for one order. It’s a check and a change that must be one step, as always. But here the step is the whole match: it may read and change dozens of orders across several levels.
The clearly correct fix: one book is matched by one thread at a time. Each symbol’s book has its own lock, held for the whole submit or cancel. Orders for one stock are matched strictly one after another. That’s what price-time priority means anyway: there must be one agreed order of arrival. Orders for different stocks take different locks, and run in parallel. So a busy stock never slows a quiet one.
The better fix adds a journal you can replay. Each accepted request becomes a Command
(submit or cancel, with every detail of the order), and gets a sequence number. The commands, in
sequence order, are the journal. Matching depends on nothing but the commands and their order.
There’s no clock, and no randomness. So replaying the journal on an empty exchange rebuilds every
book and every trade exactly. That’s how an exchange recovers from a crash. It’s also how a standby
machine stays ready to take over. This is the Command pattern used for what it’s best at: a
request as data, so it can be recorded, numbered and run again.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
The sequence number must be taken under the book’s lock. Suppose gateway A took number 7, and gateway B took number 8, but B reached the book first. Replay would run A first, and produce different trades. So take the number and record the command while holding the book’s lock. Then the sequence order, for each symbol, is exactly the order the book processed.
- Color key
- Facade
| Option | How | Correct? | Cost |
|---|---|---|---|
| 1. A lock per book (our choice) | One lock held for each whole match | Yes | One stock’s orders go one at a time, as they must |
| 2. One lock for the exchange | Simplest | Yes | Every stock waits for every other |
| 3. A lock per order or level | Lock what you touch | No | A match touches many; deadlocks and half-done trades |
| 4. A thread per book with a queue | Gateways add to a queue; one thread empties it | Yes | The usual shape in real engines; more machinery |
| 5. Lock-free compare-and-set | Retry on conflict | Hard | Changes to many orders don’t fit one atomic step |
// The two kinds of request the journal records.enum Action { SUBMIT, CANCEL }
// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.record Command(long seq, String symbol, Action action, String id, Side side, long price, long quantity, TimeInForce tif) {}
// Many gateways send orders at once. Matching must see one symbol's orders one at a time, in a// single agreed order, or two orders can both take the same resting quantity. So each symbol's// book has its own lock: orders for different symbols never wait for each other. The sequence// number is taken under that lock too, so sequence order is exactly the order the book saw.class Exchange { // One symbol's book and its trades. Its monitor is that symbol's lock. private static final class Book { final OrderBook book = new OrderBook(); final List<Trade> trades = new ArrayList<>(); }
private final ConcurrentHashMap<String, Book> books = new ConcurrentHashMap<>(); private final AtomicLong sequence = new AtomicLong(); private final ConcurrentLinkedQueue<Command> journal = new ConcurrentLinkedQueue<>();
List<Trade> submit(String symbol, Order o) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { journal.add(new Command(sequence.incrementAndGet(), symbol, Action.SUBMIT, o.id, o.side, o.price, o.quantity, o.tif)); List<Trade> trades = b.book.submit(o); b.trades.addAll(trades); return trades; } }
boolean cancel(String symbol, String id) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { journal.add(new Command(sequence.incrementAndGet(), symbol, Action.CANCEL, id, null, 0, 0, null)); return b.book.cancel(id); } }
List<Trade> trades(String symbol) { Book b = books.computeIfAbsent(symbol, s -> new Book()); synchronized (b) { return List.copyOf(b.trades); } }
// Two symbols add to the queue at the same time, so entries can land slightly out of order. // Sorting by sequence number gives the one true order. List<Command> journal() { return journal.stream().sorted(Comparator.comparingLong(Command::seq)).toList(); }
// After a crash, or on a standby machine: the same commands in the same order give the same // books and the same trades, because matching depends on nothing else. static Exchange replay(List<Command> commands) { Exchange e = new Exchange(); for (Command c : commands) { if (c.action() == Action.SUBMIT) e.submit(c.symbol(), new Order(c.id(), c.side(), c.price(), c.quantity(), c.tif())); else e.cancel(c.symbol(), c.id()); } return e; }}# The two kinds of request the journal records.class Action(Enum): SUBMIT = auto() CANCEL = auto()
# One accepted request, numbered. The journal of these is the exchange's source of truth:# replaying it in sequence order rebuilds every book and every trade exactly.@dataclass(frozen=True)class Command: seq: int symbol: str action: Action id: str side: Side | None = None price: int = 0 quantity: int = 0 tif: TimeInForce | None = None
# One symbol's book, its trades, and the lock that guards both.@dataclass(eq=False)class _Book: book: OrderBook = field(default_factory=OrderBook) trades: list[Trade] = field(default_factory=list) lock: threading.Lock = field(default_factory=threading.Lock)
# Many gateways send orders at once. Matching must see one symbol's orders one at a time,# in a single agreed order, or two orders can both take the same resting quantity. So each# symbol's book has its own lock: orders for different symbols never wait for each other. The# sequence number is taken under that lock too, so sequence order is exactly the order the# book saw.class Exchange: def __init__(self): self._books: dict[str, _Book] = {} self._books_lock = threading.Lock() self._seq = itertools.count(1) self._journal: list[Command] = []
def submit(self, symbol: str, o: Order) -> list[Trade]: b = self._book(symbol) with b.lock: self._journal.append(Command(next(self._seq), symbol, Action.SUBMIT, o.id, o.side, o.price, o.quantity, o.tif)) trades = b.book.submit(o) b.trades.extend(trades) return trades
def cancel(self, symbol: str, id: str) -> bool: b = self._book(symbol) with b.lock: self._journal.append(Command(next(self._seq), symbol, Action.CANCEL, id)) return b.book.cancel(id)
def trades(self, symbol: str) -> list[Trade]: b = self._book(symbol) with b.lock: return list(b.trades)
# Two symbols add entries at the same time, so they can land slightly out of order. # Sorting by sequence number gives the one true order. def journal(self) -> list[Command]: return sorted(self._journal, key=lambda c: c.seq)
# After a crash, or on a standby machine: the same commands in the same order give the # same books and the same trades, because matching depends on nothing else. @staticmethod def replay(commands: list[Command]) -> Exchange: e = Exchange() for c in commands: if c.action is Action.SUBMIT: e.submit(c.symbol, Order(c.id, c.side, c.price, c.quantity, c.tif)) else: e.cancel(c.symbol, c.id) return e
def _book(self, symbol: str) -> _Book: with self._books_lock: return self._books.setdefault(symbol, _Book())// Not called Action, which would clash with System.Action.public enum CommandType { Submit, Cancel }
// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.public sealed record Command(long Seq, string Symbol, CommandType Type, string Id, Side Side = Side.Buy, long Price = 0, long Quantity = 0, TimeInForce Tif = TimeInForce.Gtc);
// Orders arrive from many gateways at once. Matching must see one symbol's orders one at a time,// in a single agreed order, or two orders can both take the same resting quantity. So each// symbol's book has its own lock: orders for different symbols never wait for each other. The// sequence number is taken under that lock too, so sequence order is exactly the order the book// saw.public sealed class Exchange{ // One symbol's book and its trades. Locking the Book object is that symbol's lock. private sealed class Book { public readonly TimedOrderBook Orders = new(); public readonly List<Trade> Trades = new(); }
private readonly ConcurrentDictionary<string, Book> _books = new(); private readonly ConcurrentQueue<Command> _journal = new(); private long _sequence;
public List<Trade> Submit(string symbol, Order o) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) { _journal.Enqueue(new Command(Interlocked.Increment(ref _sequence), symbol, CommandType.Submit, o.Id, o.Side, o.Price, o.Quantity, o.Tif)); var trades = book.Orders.Submit(o); book.Trades.AddRange(trades); return trades; } }
public bool Cancel(string symbol, string id) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) { _journal.Enqueue(new Command(Interlocked.Increment(ref _sequence), symbol, CommandType.Cancel, id)); return book.Orders.Cancel(id); } }
public List<Trade> Trades(string symbol) { var book = _books.GetOrAdd(symbol, _ => new Book()); lock (book) return new List<Trade>(book.Trades); }
// Two symbols take sequence numbers in parallel, so the queue can hold them slightly out of // order. Sorting by number gives the true order. public List<Command> Journal() => _journal.OrderBy(c => c.Seq).ToList();
// Replay rebuilds an exchange after a crash, or on a standby machine: the same commands in // the same order give the same books and the same trades, because matching depends on nothing // else. public static Exchange Replay(IEnumerable<Command> commands) { var e = new Exchange(); foreach (var c in commands) { if (c.Type == CommandType.Submit) e.Submit(c.Symbol, new Order(c.Id, c.Side, c.Price, c.Quantity, c.Tif)); else e.Cancel(c.Symbol, c.Id); } return e; }}// Action is one of the two kinds of request the journal records.type Action int
const ( SubmitAction Action = iota CancelAction)
// Command is one accepted request, numbered. The journal of these is the exchange's source of// truth: replaying it in sequence order rebuilds every book and every trade exactly.type Command struct { Seq int64 Symbol string Action Action ID string Side Side Price int64 Quantity int64 TIF TimeInForce}
// symbolBook is one symbol's book and its trades, with the mutex that guards both.type symbolBook struct { mu sync.Mutex book *OrderBook trades []Trade}
// Exchange takes orders from many gateways at once. Matching must see one symbol's orders one at// a time, in a single agreed order, or two orders can both take the same resting quantity. So// each symbol's book has its own mutex: orders for different symbols never wait for each other.// The sequence number is taken under that mutex too, so sequence order is exactly the order the// book saw.type Exchange struct { mu sync.Mutex // guards books and journal, never held while matching books map[string]*symbolBook sequence atomic.Int64 journal []Command}
func NewExchange() *Exchange { return &Exchange{books: map[string]*symbolBook{}} }
func (e *Exchange) book(symbol string) *symbolBook { e.mu.Lock() defer e.mu.Unlock() b, ok := e.books[symbol] if !ok { b = &symbolBook{book: NewOrderBook()} e.books[symbol] = b } return b}
func (e *Exchange) record(c Command) { e.mu.Lock() e.journal = append(e.journal, c) e.mu.Unlock()}
func (e *Exchange) Submit(symbol string, o *Order) []Trade { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() e.record(Command{e.sequence.Add(1), symbol, SubmitAction, o.ID, o.Side, o.Price, o.Quantity, o.TIF}) trades := b.book.Submit(o) b.trades = append(b.trades, trades...) return trades}
func (e *Exchange) Cancel(symbol, id string) bool { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() e.record(Command{Seq: e.sequence.Add(1), Symbol: symbol, Action: CancelAction, ID: id}) return b.book.Cancel(id)}
func (e *Exchange) Trades(symbol string) []Trade { b := e.book(symbol) b.mu.Lock() defer b.mu.Unlock() return slices.Clone(b.trades)}
// Journal sorts by sequence number: two symbols append at the same time, so entries can land// slightly out of order.func (e *Exchange) Journal() []Command { e.mu.Lock() defer e.mu.Unlock() out := slices.Clone(e.journal) slices.SortFunc(out, func(a, b Command) int { return cmp.Compare(a.Seq, b.Seq) }) return out}
// Replay rebuilds an exchange after a crash, or on a standby machine: the same commands in the// same order give the same books and the same trades, because matching depends on nothing else.func Replay(commands []Command) *Exchange { e := NewExchange() for _, c := range commands { if c.Action == SubmitAction { e.Submit(c.Symbol, NewOrder(c.ID, c.Side, c.Price, c.Quantity, c.TIF)) } else { e.Cancel(c.Symbol, c.ID) } } return e}// One accepted request, numbered. The journal of these is the exchange's source of truth:// replaying it in sequence order rebuilds every book and every trade exactly.type Command = | { seq: number; symbol: string; action: 'submit'; id: string; side: Side; price: number; quantity: number; tif: TimeInForce } | { seq: number; symbol: string; action: 'cancel'; id: string };
// Orders arrive from many gateways at once. Matching must see one symbol's orders one at a time,// in a single agreed order, or two orders can both take the same resting quantity. JavaScript// runs one callback at a time, and submit has no await inside it, so each order is matched// completely before the next one starts. No lock is needed, and the sequence number is simply// the order the calls ran in.//// That also means one process matches every symbol. A real exchange runs one such single-threaded// matcher per symbol, or per group of symbols, often in a worker or process of its own.class Exchange { private readonly books = new Map<string, TimedOrderBook>(); private readonly tradesBySymbol = new Map<string, Trade[]>(); private readonly log: Command[] = []; private sequence = 0;
submit(symbol: string, o: Order): Trade[] { this.log.push({ seq: ++this.sequence, symbol, action: 'submit', id: o.id, side: o.side, price: o.price, quantity: o.quantity, tif: o.tif }); const trades = this.book(symbol).submit(o); this.trades(symbol).push(...trades); return trades; }
cancel(symbol: string, id: string): boolean { this.log.push({ seq: ++this.sequence, symbol, action: 'cancel', id }); return this.book(symbol).cancel(id); }
trades(symbol: string): Trade[] { let trades = this.tradesBySymbol.get(symbol); if (!trades) { trades = []; this.tradesBySymbol.set(symbol, trades); } return trades; }
journal(): readonly Command[] { return this.log; }
// replay rebuilds an exchange after a crash, or on a standby machine: the same commands in // the same order give the same books and the same trades, because matching depends on nothing // else. static replay(commands: readonly Command[]): Exchange { const e = new Exchange(); for (const c of commands) { if (c.action === 'submit') e.submit(c.symbol, new Order(c.id, c.side, c.price, c.quantity, c.tif)); else e.cancel(c.symbol, c.id); } return e; }
private book(symbol: string): TimedOrderBook { let book = this.books.get(symbol); if (!book) { book = new TimedOrderBook(); this.books.set(symbol, book); } return book; }}Why is this correct? Under the book’s lock, one match reads and changes the resting orders, and finishes, before the next match starts. The second buy sees the quantity the first one left, so no share is sold twice. And since each command is numbered inside the same lock, the journal holds the book’s commands in the exact order the book ran them.
What does it cost? One stock’s orders go one at a time, which price-time priority needs anyway. Different symbols may record their commands slightly out of numeric order, so the journal is sorted by number when read. Commands for different symbols never affect each other, so their relative order doesn’t change any trade.
Go deeperHow each language does it1 min
In Java, Python, C# and Go, gateways run on their own threads, so each book’s lock is needed.
TypeScript runs one callback at a time, and submit has no await inside it. So each order is
matched completely before the next starts, and no lock is needed. That’s option 4 in miniature. Real
engines are single-threaded per book on purpose. A thread that never waits for a lock is the
fastest way to match.
How you’d prove it works
Section titled “How you’d prove it works”The book is pure, so most tests are a few orders and an expected list of trades. Tiny books whose answer is obvious test the priority rules. The tests that matter:
- With asks at 105 and then 100, a buy at 110 trades at 100: the best price, and the resting price.
- At one price, the older order fills first.
- A large buy walks several levels, and rests the rest at its limit.
- Orders that don’t cross both rest, and the best bid and ask are right.
- Cancel works once, and the cancelled order can’t trade.
- Reducing an order keeps its place. Growing it sends it behind newer orders.
- IOC and market orders never rest, and a market order takes the best price, however high.
- FOK fills completely or not at all, and leaves the book untouched when refused.
- Depth adds up each level, best first.
- Eight gateways send 400 orders each for two symbols, with cancels mixed in. No order fills more than its size, and replaying the journal gives exactly the same trades.
Go deeperThe tests in five languages2 min
The last test fails in Java, Python and Go if the book’s lock is removed: orders fill beyond their size, or the replayed trades differ. Replay is a strong test, because it checks determinism and the journal at once.
static Order buy(String id, long price, long qty) { return Order.limit(id, Side.BUY, price, qty); }
static Order sell(String id, long price, long qty) { return Order.limit(id, Side.SELL, price, qty); }
@Testvoid theBestPriceTradesFirstAtTheRestingPrice() { OrderBook book = new OrderBook(); book.submit(sell("s1", 105, 10)); book.submit(sell("s2", 100, 10)); List<Trade> trades = book.submit(buy("b1", 110, 10)); assertEquals(List.of(new Trade("b1", "s2", 100, 10)), trades); // 100, not the first-listed 105 assertEquals(Long.valueOf(105), book.bestAsk());}
@Testvoid atOnePriceTheOldestOrderTradesFirst() { OrderBook book = new OrderBook(); book.submit(sell("early", 100, 5)); book.submit(sell("late", 100, 5)); assertEquals("early", book.submit(buy("b", 100, 5)).get(0).sellId());}
@Testvoid aLargeOrderWalksTheBookAndRestsTheRest() { OrderBook book = new OrderBook(); book.submit(sell("s1", 100, 3)); book.submit(sell("s2", 101, 3)); book.submit(sell("s3", 103, 3)); List<Trade> trades = book.submit(buy("b", 101, 10)); assertEquals(List.of(new Trade("b", "s1", 100, 3), new Trade("b", "s2", 101, 3)), trades); assertEquals(Long.valueOf(101), book.bestBid()); // 4 left, resting at its limit assertEquals(Long.valueOf(103), book.bestAsk());}
@Testvoid ordersThatDontCrossRest() { OrderBook book = new OrderBook(); assertTrue(book.submit(buy("b", 99, 5)).isEmpty()); assertTrue(book.submit(sell("s", 101, 5)).isEmpty()); assertEquals(Long.valueOf(99), book.bestBid()); assertEquals(Long.valueOf(101), book.bestAsk());}
@Testvoid cancelRemovesARestingOrderOnce() { OrderBook book = new OrderBook(); book.submit(sell("s1", 100, 5)); assertTrue(book.cancel("s1")); assertFalse(book.cancel("s1")); assertNull(book.bestAsk()); assertTrue(book.submit(buy("b", 100, 5)).isEmpty());}
@Testvoid reducingKeepsPriorityAndRepricingLosesIt() { OrderBook book = new OrderBook(); book.submit(sell("a", 100, 10)); book.submit(sell("b", 100, 10)); book.modify("a", 100, 4); // smaller: keeps its place assertEquals("a", book.submit(buy("x", 100, 4)).get(0).sellId()); book.submit(sell("c", 100, 10)); book.modify("b", 100, 20); // bigger: to the back, behind c assertEquals("c", book.submit(buy("y", 100, 1)).get(0).sellId());}
@Testvoid iocAndMarketOrdersNeverRest() { OrderBook book = new OrderBook(); book.submit(sell("s", 100, 5)); List<Trade> ioc = book.submit(new Order("i", Side.BUY, 100, 8, TimeInForce.IOC)); assertEquals(5, ioc.get(0).quantity()); assertNull(book.bestBid()); // the other 3 were dropped book.submit(sell("s2", 120, 5)); assertEquals(120, book.submit(Order.market("m", Side.BUY, 2)).get(0).price());}
@Testvoid fillOrKillIsAllOrNothing() { OrderBook book = new OrderBook(); book.submit(sell("s1", 100, 3)); book.submit(sell("s2", 101, 3)); assertTrue(book.submit(new Order("f1", Side.BUY, 101, 7, TimeInForce.FOK)).isEmpty()); assertEquals(List.of(new Depth(100, 3), new Depth(101, 3)), book.depth(Side.SELL, 5)); // untouched assertEquals(2, book.submit(new Order("f2", Side.BUY, 101, 6, TimeInForce.FOK)).size());}
@Testvoid depthSumsEachLevelBestFirst() { OrderBook book = new OrderBook(); book.submit(buy("a", 99, 5)); book.submit(buy("b", 99, 2)); book.submit(buy("c", 98, 4)); book.submit(buy("d", 97, 1)); assertEquals(List.of(new Depth(99, 7), new Depth(98, 4)), book.depth(Side.BUY, 2));}
@Testvoid concurrentOrdersMatchExactlyAsTheJournalReplays() throws Exception { Exchange exchange = new Exchange(); Map<String, Long> sizes = new ConcurrentHashMap<>(); List<Thread> threads = new ArrayList<>(); for (int t = 0; t < 8; t++) { int gateway = t; Thread th = new Thread(() -> { Random random = new Random(gateway); for (int i = 0; i < 400; i++) { String id = gateway + "-" + i; long qty = 1 + random.nextInt(10); sizes.put(id, qty); Side side = random.nextBoolean() ? Side.BUY : Side.SELL; String symbol = i % 2 == 0 ? "ACME" : "GLOBEX"; exchange.submit(symbol, Order.limit(id, side, 99 + random.nextInt(3), qty)); if (i % 7 == 0) exchange.cancel(symbol, gateway + "-" + (i - 3)); } }); th.start(); threads.add(th); } for (Thread th : threads) th.join(); Exchange replayed = Exchange.replay(exchange.journal()); for (String symbol : List.of("ACME", "GLOBEX")) { List<Trade> trades = exchange.trades(symbol); assertFalse(trades.isEmpty()); Map<String, Long> filled = new HashMap<>(); for (Trade tr : trades) { filled.merge(tr.buyId(), tr.quantity(), Long::sum); filled.merge(tr.sellId(), tr.quantity(), Long::sum); } filled.forEach((id, qty) -> assertTrue(qty <= sizes.get(id))); // nothing overfilled assertEquals(trades, replayed.trades(symbol)); }}import sys
def buy(id: str, price: int, qty: int) -> Order: return Order(id, Side.BUY, price, qty)
def sell(id: str, price: int, qty: int) -> Order: return Order(id, Side.SELL, price, qty)
def test_the_best_price_trades_first_at_the_resting_price(): book = OrderBook() book.submit(sell("s1", 105, 10)) book.submit(sell("s2", 100, 10)) assert book.submit(buy("b1", 110, 10)) == [Trade("b1", "s2", 100, 10)] # 100, not the first-listed 105 assert book.best_ask() == 105
def test_at_one_price_the_oldest_order_trades_first(): book = OrderBook() book.submit(sell("early", 100, 5)) book.submit(sell("late", 100, 5)) assert book.submit(buy("b", 100, 5))[0].sell_id == "early"
def test_a_large_order_walks_the_book_and_rests_the_rest(): book = OrderBook() book.submit(sell("s1", 100, 3)) book.submit(sell("s2", 101, 3)) book.submit(sell("s3", 103, 3)) assert book.submit(buy("b", 101, 10)) == [Trade("b", "s1", 100, 3), Trade("b", "s2", 101, 3)] assert book.best_bid() == 101 # 4 left, resting at its limit assert book.best_ask() == 103
def test_orders_that_dont_cross_rest(): book = OrderBook() assert book.submit(buy("b", 99, 5)) == [] assert book.submit(sell("s", 101, 5)) == [] assert (book.best_bid(), book.best_ask()) == (99, 101)
def test_cancel_removes_a_resting_order_once(): book = OrderBook() book.submit(sell("s1", 100, 5)) assert book.cancel("s1") assert not book.cancel("s1") assert book.best_ask() is None assert book.submit(buy("b", 100, 5)) == []
def test_reducing_keeps_priority_and_repricing_loses_it(): book = OrderBook() book.submit(sell("a", 100, 10)) book.submit(sell("b", 100, 10)) book.modify("a", 100, 4) # smaller: keeps its place assert book.submit(buy("x", 100, 4))[0].sell_id == "a" book.submit(sell("c", 100, 10)) book.modify("b", 100, 20) # bigger: to the back, behind c assert book.submit(buy("y", 100, 1))[0].sell_id == "c"
def test_ioc_and_market_orders_never_rest(): book = OrderBook() book.submit(sell("s", 100, 5)) assert book.submit(Order("i", Side.BUY, 100, 8, TimeInForce.IOC))[0].quantity == 5 assert book.best_bid() is None # the other 3 were dropped book.submit(sell("s2", 120, 5)) assert book.submit(Order.market("m", Side.BUY, 2))[0].price == 120
def test_fill_or_kill_is_all_or_nothing(): book = OrderBook() book.submit(sell("s1", 100, 3)) book.submit(sell("s2", 101, 3)) assert book.submit(Order("f1", Side.BUY, 101, 7, TimeInForce.FOK)) == [] assert book.depth(Side.SELL, 5) == [Depth(100, 3), Depth(101, 3)] # untouched assert len(book.submit(Order("f2", Side.BUY, 101, 6, TimeInForce.FOK))) == 2
def test_depth_sums_each_level_best_first(): book = OrderBook() for id, price, qty in [("a", 99, 5), ("b", 99, 2), ("c", 98, 4), ("d", 97, 1)]: book.submit(buy(id, price, qty)) assert book.depth(Side.BUY, 2) == [Depth(99, 7), Depth(98, 4)]
def test_concurrent_orders_match_exactly_as_the_journal_replays(): old = sys.getswitchinterval() sys.setswitchinterval(1e-6) # switch threads often, so a missing lock shows up try: exchange = Exchange() sizes: dict[str, int] = {}
def gateway(g: int) -> None: rnd = random.Random(g) for i in range(300): id, qty = f"{g}-{i}", rnd.randint(1, 10) sizes[id] = qty side = Side.BUY if rnd.random() < 0.5 else Side.SELL symbol = "ACME" if i % 2 == 0 else "GLOBEX" exchange.submit(symbol, Order(id, side, rnd.randint(99, 101), qty)) if i % 7 == 0: exchange.cancel(symbol, f"{g}-{i - 3}")
with ThreadPoolExecutor(8) as pool: list(pool.map(gateway, range(8))) replayed = Exchange.replay(exchange.journal()) for symbol in ("ACME", "GLOBEX"): trades = exchange.trades(symbol) assert trades filled: dict[str, int] = defaultdict(int) for t in trades: filled[t.buy_id] += t.quantity filled[t.sell_id] += t.quantity assert all(qty <= sizes[id] for id, qty in filled.items()) # nothing overfilled assert trades == replayed.trades(symbol) finally: sys.setswitchinterval(old)private static Order Buy(string id, long price, long qty) => new(id, Side.Buy, price, qty);
private static Order Sell(string id, long price, long qty) => new(id, Side.Sell, price, qty);
[Fact]public void TheBestPriceTradesFirstAtTheRestingPrice(){ var b = new TimedOrderBook(); b.Submit(Sell("s1", 105, 10)); b.Submit(Sell("s2", 100, 10)); Assert.Equal(new[] { new Trade("b1", "s2", 100, 10) }, b.Submit(Buy("b1", 110, 10))); // not the first-listed 105 Assert.Equal(105, b.BestAsk());}
[Fact]public void AtOnePriceTheOldestOrderTradesFirst(){ var b = new TimedOrderBook(); b.Submit(Sell("early", 100, 5)); b.Submit(Sell("late", 100, 5)); Assert.Equal("early", b.Submit(Buy("b", 100, 5))[0].SellId);}
[Fact]public void ALargeOrderWalksTheBookAndRestsTheRest(){ var b = new TimedOrderBook(); b.Submit(Sell("s1", 100, 3)); b.Submit(Sell("s2", 101, 3)); b.Submit(Sell("s3", 103, 3)); Assert.Equal(new[] { new Trade("b", "s1", 100, 3), new Trade("b", "s2", 101, 3) }, b.Submit(Buy("b", 101, 10))); Assert.Equal(101, b.BestBid()); // 4 left, resting at its limit Assert.Equal(103, b.BestAsk());}
[Fact]public void OrdersThatDontCrossRest(){ var b = new TimedOrderBook(); Assert.Empty(b.Submit(Buy("b", 99, 5))); Assert.Empty(b.Submit(Sell("s", 101, 5))); Assert.Equal(99, b.BestBid()); Assert.Equal(101, b.BestAsk());}
[Fact]public void CancelRemovesARestingOrderOnce(){ var b = new TimedOrderBook(); b.Submit(Sell("s1", 100, 5)); Assert.True(b.Cancel("s1")); Assert.False(b.Cancel("s1")); Assert.Null(b.BestAsk()); Assert.Empty(b.Submit(Buy("b", 100, 5)));}
[Fact]public void ReducingKeepsPriorityAndRepricingLosesIt(){ var b = new TimedOrderBook(); b.Submit(Sell("a", 100, 10)); b.Submit(Sell("b", 100, 10)); b.Modify("a", 100, 4); // smaller: keeps its place Assert.Equal("a", b.Submit(Buy("x", 100, 4))[0].SellId); b.Submit(Sell("c", 100, 10)); b.Modify("b", 100, 20); // bigger: to the back, behind c Assert.Equal("c", b.Submit(Buy("y", 100, 1))[0].SellId);}
[Fact]public void IocAndMarketOrdersNeverRest(){ var b = new TimedOrderBook(); b.Submit(Sell("s", 100, 5)); Assert.Equal(5, b.Submit(new Order("i", Side.Buy, 100, 8, TimeInForce.Ioc))[0].Quantity); Assert.Null(b.BestBid()); // the other 3 were dropped b.Submit(Sell("s2", 120, 5)); Assert.Equal(120, b.Submit(Order.Market("m", Side.Buy, 2))[0].Price);}
[Fact]public void FillOrKillIsAllOrNothing(){ var b = new TimedOrderBook(); b.Submit(Sell("s1", 100, 3)); b.Submit(Sell("s2", 101, 3)); Assert.Empty(b.Submit(new Order("f1", Side.Buy, 101, 7, TimeInForce.Fok))); Assert.Equal(new[] { new Depth(100, 3), new Depth(101, 3) }, b.DepthOf(Side.Sell, 5)); // untouched Assert.Equal(2, b.Submit(new Order("f2", Side.Buy, 101, 6, TimeInForce.Fok)).Count);}
[Fact]public void DepthSumsEachLevelBestFirst(){ var b = new TimedOrderBook(); b.Submit(Buy("a", 99, 5)); b.Submit(Buy("b", 99, 2)); b.Submit(Buy("c", 98, 4)); b.Submit(Buy("d", 97, 1)); Assert.Equal(new[] { new Depth(99, 7), new Depth(98, 4) }, b.DepthOf(Side.Buy, 2));}
[Fact]public void ConcurrentOrdersMatchExactlyAsTheJournalReplays(){ var e = new Exchange(); var sizes = new ConcurrentDictionary<string, long>(); Parallel.For(0, 8, g => { var r = new Random(g); for (var i = 0; i < 400; i++) { var id = $"{g}-{i}"; var qty = 1 + r.Next(10); sizes[id] = qty; var symbol = i % 2 == 0 ? "ACME" : "GLOBEX"; e.Submit(symbol, new Order(id, r.Next(2) == 0 ? Side.Buy : Side.Sell, 99 + r.Next(3), qty)); if (i % 7 == 0) e.Cancel(symbol, $"{g}-{i - 3}"); } }); var replayed = Exchange.Replay(e.Journal()); foreach (var symbol in new[] { "ACME", "GLOBEX" }) { var trades = e.Trades(symbol); Assert.NotEmpty(trades); var filled = new Dictionary<string, long>(); foreach (var t in trades) { filled[t.BuyId] = filled.GetValueOrDefault(t.BuyId) + t.Quantity; filled[t.SellId] = filled.GetValueOrDefault(t.SellId) + t.Quantity; } foreach (var (id, qty) in filled) Assert.True(qty <= sizes[id]); // nothing overfilled Assert.Equal(trades, replayed.Trades(symbol)); }}func buy(id string, price, qty int64) *Order { return Limit(id, Buy, price, qty) }
func sell(id string, price, qty int64) *Order { return Limit(id, Sell, price, qty) }
func expect[T comparable](t *testing.T, want, got T) { t.Helper() if want != got { t.Fatalf("want %v, got %v", want, got) }}
func show(v any) string { return fmt.Sprint(v) }
func TestTheBestPriceTradesFirstAtTheRestingPrice(t *testing.T) { b := NewOrderBook() b.Submit(sell("s1", 105, 10)) b.Submit(sell("s2", 100, 10)) expect(t, show([]Trade{{"b1", "s2", 100, 10}}), show(b.Submit(buy("b1", 110, 10)))) // not the first-listed 105 ask, _ := b.BestAsk() expect(t, 105, ask)}
func TestAtOnePriceTheOldestOrderTradesFirst(t *testing.T) { b := NewOrderBook() b.Submit(sell("early", 100, 5)) b.Submit(sell("late", 100, 5)) expect(t, "early", b.Submit(buy("b", 100, 5))[0].SellID)}
func TestALargeOrderWalksTheBookAndRestsTheRest(t *testing.T) { b := NewOrderBook() b.Submit(sell("s1", 100, 3)) b.Submit(sell("s2", 101, 3)) b.Submit(sell("s3", 103, 3)) expect(t, show([]Trade{{"b", "s1", 100, 3}, {"b", "s2", 101, 3}}), show(b.Submit(buy("b", 101, 10)))) bid, _ := b.BestBid() ask, _ := b.BestAsk() expect(t, 101, bid) // 4 left, resting at its limit expect(t, 103, ask)}
func TestOrdersThatDontCrossRest(t *testing.T) { b := NewOrderBook() expect(t, 0, len(b.Submit(buy("b", 99, 5)))) expect(t, 0, len(b.Submit(sell("s", 101, 5)))) bid, _ := b.BestBid() ask, _ := b.BestAsk() expect(t, "99 101", fmt.Sprint(bid, ask))}
func TestCancelRemovesARestingOrderOnce(t *testing.T) { b := NewOrderBook() b.Submit(sell("s1", 100, 5)) expect(t, true, b.Cancel("s1")) expect(t, false, b.Cancel("s1")) _, ok := b.BestAsk() expect(t, false, ok) expect(t, 0, len(b.Submit(buy("b", 100, 5))))}
func TestReducingKeepsPriorityAndRepricingLosesIt(t *testing.T) { b := NewOrderBook() b.Submit(sell("a", 100, 10)) b.Submit(sell("b", 100, 10)) b.Modify("a", 100, 4) // smaller: keeps its place expect(t, "a", b.Submit(buy("x", 100, 4))[0].SellID) b.Submit(sell("c", 100, 10)) b.Modify("b", 100, 20) // bigger: to the back, behind c expect(t, "c", b.Submit(buy("y", 100, 1))[0].SellID)}
func TestIOCAndMarketOrdersNeverRest(t *testing.T) { b := NewOrderBook() b.Submit(sell("s", 100, 5)) expect(t, 5, b.Submit(NewOrder("i", Buy, 100, 8, IOC))[0].Quantity) _, ok := b.BestBid() expect(t, false, ok) // the other 3 were dropped b.Submit(sell("s2", 120, 5)) expect(t, 120, b.Submit(Market("m", Buy, 2))[0].Price)}
func TestFillOrKillIsAllOrNothing(t *testing.T) { b := NewOrderBook() b.Submit(sell("s1", 100, 3)) b.Submit(sell("s2", 101, 3)) expect(t, 0, len(b.Submit(NewOrder("f1", Buy, 101, 7, FOK)))) expect(t, show([]Depth{{100, 3}, {101, 3}}), show(b.Depth(Sell, 5))) // untouched expect(t, 2, len(b.Submit(NewOrder("f2", Buy, 101, 6, FOK))))}
func TestDepthSumsEachLevelBestFirst(t *testing.T) { b := NewOrderBook() b.Submit(buy("a", 99, 5)) b.Submit(buy("b", 99, 2)) b.Submit(buy("c", 98, 4)) b.Submit(buy("d", 97, 1)) expect(t, show([]Depth{{99, 7}, {98, 4}}), show(b.Depth(Buy, 2)))}
func TestConcurrentOrdersMatchExactlyAsTheJournalReplays(t *testing.T) { e := NewExchange() var sizes sync.Map var wg sync.WaitGroup for g := range 8 { wg.Add(1) go func() { defer wg.Done() r := rand.New(rand.NewPCG(uint64(g), 0)) for i := range 400 { id := fmt.Sprintf("%d-%d", g, i) qty := 1 + r.Int64N(10) sizes.Store(id, qty) side := Side(r.IntN(2)) symbol := []string{"ACME", "GLOBEX"}[i%2] e.Submit(symbol, Limit(id, side, 99+r.Int64N(3), qty)) if i%7 == 0 { e.Cancel(symbol, fmt.Sprintf("%d-%d", g, i-3)) } } }() } wg.Wait() replayed := Replay(e.Journal()) for _, symbol := range []string{"ACME", "GLOBEX"} { trades := e.Trades(symbol) expect(t, true, len(trades) > 0) filled := map[string]int64{} for _, tr := range trades { filled[tr.BuyID] += tr.Quantity filled[tr.SellID] += tr.Quantity } for id, qty := range filled { size, _ := sizes.Load(id) expect(t, true, qty <= size.(int64)) // nothing overfilled } expect(t, show(trades), show(replayed.Trades(symbol))) }}import { test } from 'node:test';import assert from 'node:assert/strict';
const buy = (id: string, price: number, quantity: number) => new Order(id, 'buy', price, quantity);const sell = (id: string, price: number, quantity: number) => new Order(id, 'sell', price, quantity);
test('the best price trades first, at the resting price', () => { const b = new TimedOrderBook(); b.submit(sell('s1', 105, 10)); b.submit(sell('s2', 100, 10)); assert.deepEqual(b.submit(buy('b1', 110, 10)), [{ buyId: 'b1', sellId: 's2', price: 100, quantity: 10 }]); // not the first-listed 105 assert.equal(b.bestAsk(), 105);});
test('at one price, the oldest order trades first', () => { const b = new TimedOrderBook(); b.submit(sell('early', 100, 5)); b.submit(sell('late', 100, 5)); assert.equal(b.submit(buy('b', 100, 5))[0].sellId, 'early');});
test('a large order walks the book and rests the rest', () => { const b = new TimedOrderBook(); b.submit(sell('s1', 100, 3)); b.submit(sell('s2', 101, 3)); b.submit(sell('s3', 103, 3)); assert.deepEqual(b.submit(buy('b', 101, 10)), [ { buyId: 'b', sellId: 's1', price: 100, quantity: 3 }, { buyId: 'b', sellId: 's2', price: 101, quantity: 3 }, ]); assert.equal(b.bestBid(), 101); // 4 left, resting at its limit assert.equal(b.bestAsk(), 103);});
test("orders that don't cross rest", () => { const b = new TimedOrderBook(); assert.deepEqual(b.submit(buy('b', 99, 5)), []); assert.deepEqual(b.submit(sell('s', 101, 5)), []); assert.deepEqual([b.bestBid(), b.bestAsk()], [99, 101]);});
test('cancel removes a resting order once', () => { const b = new TimedOrderBook(); b.submit(sell('s1', 100, 5)); assert.equal(b.cancel('s1'), true); assert.equal(b.cancel('s1'), false); assert.equal(b.bestAsk(), undefined); assert.deepEqual(b.submit(buy('b', 100, 5)), []);});
test('reducing keeps priority, repricing loses it', () => { const b = new TimedOrderBook(); b.submit(sell('a', 100, 10)); b.submit(sell('b', 100, 10)); b.modify('a', 100, 4); // smaller: keeps its place assert.equal(b.submit(buy('x', 100, 4))[0].sellId, 'a'); b.submit(sell('c', 100, 10)); b.modify('b', 100, 20); // bigger: to the back, behind c assert.equal(b.submit(buy('y', 100, 1))[0].sellId, 'c');});
test('ioc and market orders never rest', () => { const b = new TimedOrderBook(); b.submit(sell('s', 100, 5)); assert.equal(b.submit(new Order('i', 'buy', 100, 8, 'ioc'))[0].quantity, 5); assert.equal(b.bestBid(), undefined); // the other 3 were dropped b.submit(sell('s2', 120, 5)); assert.equal(b.submit(Order.market('m', 'buy', 2))[0].price, 120);});
test('fill or kill is all or nothing', () => { const b = new TimedOrderBook(); b.submit(sell('s1', 100, 3)); b.submit(sell('s2', 101, 3)); assert.deepEqual(b.submit(new Order('f1', 'buy', 101, 7, 'fok')), []); assert.deepEqual(b.depth('sell', 5), [ { price: 100, quantity: 3 }, { price: 101, quantity: 3 }, ]); // untouched assert.equal(b.submit(new Order('f2', 'buy', 101, 6, 'fok')).length, 2);});
test('depth sums each level, best first', () => { const b = new TimedOrderBook(); b.submit(buy('a', 99, 5)); b.submit(buy('b', 99, 2)); b.submit(buy('c', 98, 4)); b.submit(buy('d', 97, 1)); assert.deepEqual(b.depth('buy', 2), [ { price: 99, quantity: 7 }, { price: 98, quantity: 4 }, ]);});
test('interleaved gateways match exactly as the journal replays', async () => { const e = new Exchange(); const sizes = new Map<string, number>(); let seed = 7; const rand = (n: number) => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed % n; }; const gateway = async (g: number) => { for (let i = 0; i < 400; i++) { await new Promise((r) => setImmediate(r)); // let the other gateways in between orders const id = `${g}-${i}`; const quantity = 1 + rand(10); sizes.set(id, quantity); const symbol = i % 2 === 0 ? 'ACME' : 'GLOBEX'; e.submit(symbol, new Order(id, rand(2) === 0 ? 'buy' : 'sell', 99 + rand(3), quantity)); if (i % 7 === 0) e.cancel(symbol, `${g}-${i - 3}`); } }; await Promise.all([0, 1, 2, 3, 4, 5, 6, 7].map(gateway)); const replayed = Exchange.replay(e.journal()); for (const symbol of ['ACME', 'GLOBEX']) { const trades = e.trades(symbol); assert.ok(trades.length > 0); const filled = new Map<string, number>(); for (const t of trades) { filled.set(t.buyId, (filled.get(t.buyId) ?? 0) + t.quantity); filled.set(t.sellId, (filled.get(t.sellId) ?? 0) + t.quantity); } for (const [id, quantity] of filled) assert.ok(quantity <= sizes.get(id)!); // nothing overfilled assert.deepEqual(trades, replayed.trades(symbol)); }});Go deeperEvery edge case, and where it is handled2 min
| Case | What happens | Where |
|---|---|---|
| A buy below the best ask | Rests; no trade | submit |
| A buy that crosses several levels | Fills best first, rests the rest at its limit | match |
| Two orders at one price | The older fills first | the level’s queue |
| Cancel a filled or unknown order | Returns false | resting index |
| Cancel twice | The second returns false | resting index |
| The last order at a price is removed | The level is removed, so the best price moves on | cancel, match |
| Modify to a smaller size, same price | Keeps its place | modify |
| Modify to a new price that crosses | Trades at once | modify via submit |
| IOC with nothing to match | No trades, nothing rests | submit |
| FOK with not enough available | No trades at all; the book is untouched | fillable |
| A market order on an empty side | No trades; it never rests | IOC |
| Two gateways racing for the last shares | One fills, the other rests | the book’s lock |
| A crash | Replay the journal | replay |
Wrap up
Section titled “Wrap up”The decisions, and what each one costs
Section titled “The decisions, and what each one costs”| Decision | We chose | We gave up |
|---|---|---|
| Priority | Price, then time; trade at the resting price | Pro-rata and other ways of sharing |
| The book | A sorted map of price levels, each a FIFO queue | The memory-hungry speed of a tick array |
| Cancel | An index by id | Nothing: it’s cheap and essential |
| Modify | Keep priority only when smaller at the same price | Letting traders grow orders in place |
| Order types | A time-in-force field on one matching loop | Separate classes per order type |
| Concurrency | A lock per book; sequence numbers under it | Parallel matching inside one book |
| Recovery | A numbered journal that replays to the same trades | Storing the books themselves |
Go deeperGenuinely different designs, and when to pick them2 min
A single-threaded engine per group of symbols. Real exchanges, such as the LMAX design, run each group of books on one thread. It reads commands from a ring buffer, with no locks at all. The journal is written before matching, and copied to a standby machine, which replays the same commands. It’s the fastest design there is. It’s option 4 from the table, taken seriously.
An array of levels indexed by tick. Say the tick size is fixed, and prices stay within a band. Then an array slot per tick gives constant-time access to any level, and the best price is a moving pointer. It’s faster than a tree, and uses more memory.
Event sourcing everything. The journal already is event sourcing for the books. Extending it, so trades, cancels and market data are all worked out from the journal, gives a perfect record of everything, which regulators require.
How to spend 45 minutes
Section titled “How to spend 45 minutes”- 0 to 3. Scope to matching. Price-time priority. Order types. Prices in ticks.
- 3 to 5. The promises. Name “no share sold twice, fair order” as the hard one.
- 5 to 23. Two lists and their problems. Sorted levels with queues. Cancel with an index, and the
modify rule. Then time in force, and depth. Code
submitand its matching loop. - 23 to 25. Walk through the full diagram.
- 25 to 31. The race for the last shares, a lock per book, the journal, and the sequence number under the lock.
- 31 to 45. Stop orders or another extension, then your own questions.
If time is short, describe the order types in a sentence, and spend the time on the matching loop and the lock. Those decide this question for most interviewers.
Follow-up questions
Section titled “Follow-up questions”Add stop orders: sell if the price falls to 95. Where do they live?
Whether you keep stops out of the book, since they aren’t offers to trade yet, and trigger them from trades. And whether you see that triggered stops must go through the same sequence as any order, or replay breaks.
Most extensions fit into one place:
| Extension | What changes | Touches matching? |
|---|---|---|
| Stop orders | A trigger map beside the book, checked after each trade | No |
| Iceberg orders | Show part of the size; refill from the hidden part, at the back of line | Slightly |
| Self-trade prevention | Before trading with your own order, cancel one of them | One check |
| Market data feed | Each trade and level change becomes an event for subscribers | No |
| Opening auction | Collect orders, then find the one price that trades the most | A separate step |
| Pro-rata matching | A level splits a fill by size instead of by time | The level only |
Go deeperAdding stop orders, step by step2 min
A stop order says “if the price falls to 95, sell”. Until then, it isn’t an offer to trade.
- Keep stops out of the book. If a stop rested in the book, others could trade with it before it was triggered. So stops wait in their own sorted map, by trigger price, beside the book.
- Check the triggers after each trade. The last trade’s price is the market price. After each match, the book looks at the stops whose trigger that price has crossed. Because the map is sorted, this looks only at stops that fire, never at the rest.
- A triggered stop becomes a normal order. That’s usually a market order (sell at any price). Or a limit order, if the trader set a limit too.
- Submit it as a new command, in sequence. It goes into the journal with its own number, like any order from a gateway. That’s what makes replay trigger it at exactly the same moment. If the book just matched it quietly inside the current match, replay would still work, but the journal would no longer show what happened.
- Watch for chains. A triggered stop’s trade can move the price again, and trigger more stops. Each one is a new command, handled in turn, so the chain is processed in a clear order rather than all at once.
The matching loop doesn’t change. Stops are a layer that watches trades, and feeds new orders in.
Go deeperFour more follow-up questions2 min
Interviewer asksHow fast is this?
An order costs a logarithmic lookup for its price level, plus a constant amount for each order it fills. Cancel is a hash lookup. In memory, that’s a few microseconds per order. Real engines get under a microsecond by using arrays and one thread per book, with no locks.
Interviewer asksWhat if the gateway sends the same order twice?
Order ids are unique per trader, so the exchange keeps the ids it has seen, and refuses a repeat. The book’s resting index already refuses a second cancel. A submit needs the same check before matching, or a network retry would buy the shares twice.
Interviewer asksWhy is the trade at the resting price, not the midpoint?
The resting order made a firm offer at its price, and the incoming order chose to accept it. Trading at the incoming order’s limit would punish it for being willing to pay more. Trading at the midpoint would give the resting order a price it never asked for. Every major exchange uses the resting price.
Interviewer asksHow do you publish the book to traders?
Every trade and every change to a level is an event: price, side, and new total quantity. Subscribers apply the changes to their own copy of the depth. The engine never waits for them: events go to a queue after matching, in sequence order, so a slow subscriber can’t slow the book.
Further reading
Section titled “Further reading”- Command pattern, requests as data that can be recorded and replayed
- Design a movie ticket booking system, another design where two people race for the same thing
- Design a task scheduler, another engine ordered by priority, then time
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.