The Limit Order Book

Finance

The LIMIT ORDER BOOK (LOB) is the actual mechanism by which prices are formed in modern electronic markets. Every order placed by every market participant lives in the LOB until it is matched against an incoming order or cancelled. The continuous double auction — orders crossing against each other under PRICE-TIME PRIORITY — produces the bid-ask spread, the apparent depth of the market, and the small-scale price dynamics that algorithmic traders care about. This page covers the mechanics of the LOB, the standard order types, and the first-order quantities a trader needs to understand to interact with one productively.

A working LOB model also underpins more advanced execution theory: Almgren-Chriss is an aggregated, continuous-time abstraction of LOB dynamics; market-making models (Avellaneda-Stoikov 2008, Cartea-Jaimungal-Penalva) take the LOB seriously as the trading interface and optimize quote placement directly; modern execution algorithms (VWAP, IS, POV) submit limit and market orders into the LOB and care about FILL RATES, QUEUE POSITION, and MICROSTRUCTURE noise.

What the LOB actually contains

Two queues for each price level: the BID side and the ASK side. The bid side lists prices someone is willing to BUY at; the ask side lists prices someone is willing to SELL at. At each price, orders are queued in time-of-arrival order. The best bid is the highest price on the bid side; the best ask is the lowest price on the ask side. By construction (in a non-locked market), .

Key derived quantities:

Order types

Matching

The matching engine enforces PRICE-TIME PRIORITY: incoming orders match against resting orders by (1) BEST PRICE first, (2) WITHIN a price level, FIRST-IN-FIRST-OUT. This is the standard for US equities, futures, and most listed derivatives. Some markets use PRO-RATA matching at each price (orders share the inbound flow in proportion to size); the variant matters for liquidity-provision incentives but the core mechanics are the same.

A market BUY of 250 shares against an ask book with 100 @ 100.00, 400 @ 100.02, 800 @ 100.05: matches 100 @ 100.00 (clearing that level), then 150 @ 100.02 (partial fill of the next level). Average execution price 100.012 — 3.7 cents above the original midpoint of 99.975. That 3.7 cents is TEMPORARY IMPACT — the cost of demanding immediate liquidity.

Code: a minimal LOB

# Minimal limit-order-book simulator with price-time priority matching.
# Demonstrates: bid-ask spread, midpoint vs microprice, market-order
# slippage as it walks through book depth.

class LimitOrderBook:
    def __init__(self):
        self.bids = {}       # price -> list of (order_id, size)
        self.asks = {}
        self.next_id = 1
        self.trades = []

    def submit_limit(self, side, price, size):
        oid = self.next_id; self.next_id += 1
        book = self.bids if side == 'buy' else self.asks
        # Cross any marketable portion before resting
        size = self._match(side, price, size)
        if size > 0:
            book.setdefault(price, []).append((oid, size))
        return oid

    def submit_market(self, side, size):
        self._match(side, None, size)

    def _match(self, side, limit_price, size):
        opposite = self.asks if side == 'buy' else self.bids
        # Best price first: ascending asks for a buyer, descending bids for a seller
        prices = sorted(opposite, reverse=(side == 'sell'))
        for px in prices:
            if size <= 0: break
            if limit_price is not None:
                if side == 'buy'  and px > limit_price: break
                if side == 'sell' and px < limit_price: break
            queue = opposite[px]
            while queue and size > 0:
                oid, qty = queue[0]
                if qty <= size:
                    self.trades.append((side, px, qty))
                    size -= qty
                    queue.pop(0)
                else:
                    self.trades.append((side, px, size))
                    queue[0] = (oid, qty - size)
                    size = 0
            if not queue:
                del opposite[px]
        return size

    def best_bid(self): return max(self.bids) if self.bids else None
    def best_ask(self): return min(self.asks) if self.asks else None
    def spread(self):
        if self.bids and self.asks: return self.best_ask() - self.best_bid()
        return None
    def microprice(self):
        """Volume-weighted midprice between best bid and ask.
        Bid-side liquidity → midprice tilts UP toward the ask, and vice versa."""
        if not (self.bids and self.asks): return None
        bb, ba = self.best_bid(), self.best_ask()
        bsize = sum(q for _, q in self.bids[bb])
        asize = sum(q for _, q in self.asks[ba])
        return (ba*bsize + bb*asize) / (bsize + asize)

# Build a small book with several levels of depth
lob = LimitOrderBook()
lob.submit_limit('buy',   99.95,  200)
lob.submit_limit('buy',   99.94,  500)
lob.submit_limit('buy',   99.92, 1000)
lob.submit_limit('sell', 100.00,  100)
lob.submit_limit('sell', 100.02,  400)
lob.submit_limit('sell', 100.05,  800)

print(f"Initial book state:")
print(f"  Best bid: {lob.best_bid()}  Best ask: {lob.best_ask()}")
print(f"  Quoted spread:   {lob.spread():.4f}")
print(f"  Midprice:        {(lob.best_bid() + lob.best_ask()) / 2:.4f}")
print(f"  Microprice:      {lob.microprice():.4f}")
print(f"  (Microprice tilts toward the side with LESS liquidity — here the ask")
print(f"   has only 100 shares vs 200 on the bid, so price tilts UP.)")

# Submit a market buy of 250 shares — walks through two ask levels
print(f"\nMarket buy of 250 shares:")
lob.submit_market('buy', 250)
for trade in lob.trades:
    print(f"  Filled {trade[2]:>4} shares @ {trade[1]}")
print(f"\nAfter the trade:")
print(f"  Best bid: {lob.best_bid()}  Best ask: {lob.best_ask()}")
print(f"  Quoted spread: {lob.spread():.4f}")

# Slippage analysis
fills = lob.trades
total_size  = sum(q for _, _, q in fills)
total_cost  = sum(px*q for _, px, q in fills)
avg_fill    = total_cost / total_size
print(f"\nSlippage:")
print(f"  Average fill price:   {avg_fill:.4f}")
print(f"  Original midpoint:    99.9750")
print(f"  Total slippage:       {avg_fill - 99.9750:.4f}")
print(f"  (This is temporary impact — the cost of demanding immediate liquidity.)")

Output:

Initial book state:
  Best bid: 99.95  Best ask: 100.0
  Quoted spread:   0.0500
  Midprice:        99.9750
  Microprice:      99.9833
  (Microprice tilts toward the side with LESS liquidity — here the ask
   has only 100 shares vs 200 on the bid, so price tilts UP.)

Market buy of 250 shares:
  Filled  100 shares @ 100.0
  Filled  150 shares @ 100.02

After the trade:
  Best bid: 99.95  Best ask: 100.02
  Quoted spread: 0.0700

Slippage:
  Average fill price:   100.0120
  Original midpoint:    99.9750
  Total slippage:       0.0370
  (This is temporary impact — the cost of demanding immediate liquidity.)

Three things to read off. (1) The microprice (99.983) is above the midprice (99.975) because the BEST ASK has less size (100) than the best bid (200). The microprice "leans" toward the side with less depth, which is closer to the fair-value direction the market is likely to move. (2) The 250-share market buy fills two levels deep, paying 100.00 for the first 100 shares and 100.02 for the next 150. Total slippage of 3.7 cents on a 5-cent spread market — about 70% of the quoted spread, plus a few extra cents from walking through depth. (3) After the trade, the new best ask has moved to 100.02 (with the residual 250 shares from the partially-filled level); the spread WIDENS to 7 cents temporarily. Real markets re-fill quickly as market makers post new bids and asks at the cleared levels.

Spread, queue, and the cost of immediacy

The bid-ask spread is the PRIMARY COST of trading. Cross half of it on entry, half on exit; if the spread is 5 cents on a $100 stock, every round-trip costs 5 cents (5 basis points) just for liquidity. For a strategy with edge of 10 bps per trade, the spread is the difference between profit and loss.

Three ways to reduce the cost of immediacy:

Queue position dynamics

For a limit-order trader, QUEUE POSITION at a given price level determines fill probability. Posting LATE puts you at the back of the queue; you fill ONLY if everyone ahead of you fills first OR cancels. Posting EARLY (or RECEIVING priority via rebates) is the difference between filling and not filling.

Queue dynamics are surprisingly intricate. The expected fill probability for an order at position in a queue of total size depends on:

Cartea-Jaimungal-Penalva (2015) and Cont-Stoikov-Talreja (2010) develop stochastic models for these dynamics; the resulting QUEUE-VALUE functions are key inputs to optimal market-making algorithms.

Microstructure effects

The LOB produces small-scale price dynamics that violate the constant-vol log-normal assumption of derivatives pricing models:

What this enables

Production execution-algorithm software lives and breathes on LOB data. Standard production capabilities:

Related