← Back to roadmap
Phase 4Week 41~120 min read
0%·0/3

Market Microstructure

Spreads, queue position and adverse selection.

Mastery · Markets, Strategies & Getting Hired

Why this week

For forty weeks you've built the venue. Now look at it from the side of the people who trade on it. Market microstructure studies how the rules of a market (tick size, matching algorithm, fees, order types) and the behaviour of its participants shape prices and trading costs. Three questions decide whether a resting order makes or loses money:

  1. Where am I in the queue? Under price-time priority, being first at a price is worth a lot. Being last can be worth less than nothing.
  2. What does the spread pay me, and what does it have to cover? The spread pays for processing costs, for inventory risk, and above all for the losses to traders who know more than you.
  3. Who is on the other side? When a fill is followed by the price moving against you, that's adverse selection, and it's the main cost of providing liquidity.

You have an advantage real participants never get: your engine emits every event, so you can measure queue position, trade direction and markouts exactly. This week you learn the concepts, estimate them the way an outside participant has to, and then build exact versions on your own event stream.

⚠️
Warning

Weeks 41–45 are about the users of the exchange: microstructure, a market maker, statistical arbitrage, backtesting, and position keeping. They're here so you understand the systems those people need and can talk with them precisely. Nothing in them is trading advice. A strategy that does well against simulated order flow tells you about your simulator, not about markets.

Day 1 — The book through a trader's eyes

Day 01

Spread, depth, tick size and fees

The rules that shape the game

Open the matching sandbox and play both roles. Rest a bid, then send aggressive orders against it. Watch where your order sits as others join the level behind you and leave in front of you.

Matching sandboxprice-time priority · chips are resting orders in FIFO order
104
60
103
65
102
50
101
50
spread 1
100
50
99
45
98
50
97
70
Events (newest first)

Submit an order that crosses the spread to see trades.

The vocabulary, with what each term means for a participant:

  • Quoted spread (best ask − best bid) is what an impatient trader pays to cross immediately. Depth is how much size is resting at or near the touch, which tells you how far a large order will have to walk the book.
  • Tick size. In a large-tick instrument the spread is almost always exactly one tick, because the tick is big relative to the price. Nobody can improve the price without crossing the spread, so competition moves to time: whoever joins the level first is first in line. Speed and queue position become the game. In a small-tick instrument the spread varies, quotes can improve in tiny steps, and you can jump ahead of a queue for a negligible price concession.
  • Matching algorithm. Your engine is price-time (FIFO). Some futures markets, with interest-rate futures at CME the classic example, use pro-rata or hybrid allocation instead: an incoming order is shared among resting orders in proportion to their size. That rewards size more than speed and encourages over-quoting.
  • Fees. Many equity venues charge takers a fee and pay makers a rebate ("maker-taker"). Some do the reverse ("inverted"). For a market maker the rebate can be a large part of the economics, which you'll see in week 42's PnL drill.
Bookby Larry Harris
Trading and Exchanges: Market Microstructure for Practitioners

The practitioner's map of the territory. Read the chapters on orders and order properties, order-driven markets, dealers, and bid/ask spreads. Harris names the participants (informed traders, dealers, order anticipators, noise traders) whose interaction you'll simulate in weeks 42–44.

Recall#large-tick-queue

Why is queue position worth more in a large-tick instrument than in a small-tick one?

Day 2 — Where spreads come from

Day 02

Processing costs, inventory, adverse selection

The economics of providing liquidity

The classic decomposition splits the spread into three costs the liquidity provider has to recover:

  • Order-processing costs: fees, technology, the people running the desk.
  • Inventory-holding costs: a dealer who buys has to carry the position until someone sells, and is exposed to price moves while doing so. The more volatile the instrument and the longer the wait, the wider the spread.
  • Adverse selection: some counterparties know something you don't. Glosten and Milgrom (1985) showed that even a risk-neutral market maker with zero costs must quote a positive spread when some traders are informed. The spread is what makes trading with the uninformed pay for the losses to the informed. Kyle (1985) models an informed trader hiding among noise traders, with prices moving in proportion to order flow.

You measure these per trade. Let D be +1 for a buyer-initiated trade and −1 for a seller-initiated one, P the trade price, and M the midquote:

quoted spread      = ask − bid                       (at the time of the trade)
effective spread   = 2·D·(P − M_t)                   (what the taker actually paid)
realized spread    = 2·D·(P − M_{t+Δ})               (what the provider kept after Δ)
price impact       = 2·D·(M_{t+Δ} − M_t)             (how far the mid moved toward the taker)
effective spread   = realized spread + price impact

If the realized spread is near zero or negative, liquidity providers are being adversely selected: the price impact eats everything they charged. Roll (1984) showed how to estimate the spread from trade prices alone. If the only thing moving prices tick to tick is trades bouncing between bid and ask, consecutive price changes are negatively correlated, and spread ≈ 2·sqrt(−cov(ΔP_t, ΔP_{t−1})).

Paperby Lawrence Glosten and Paul Milgrom (Journal of Financial Economics, 1985)
Bid, Ask and Transaction Prices in a Specialist Market with Heterogeneously Informed Traders

Read for the mechanism, not the proofs. The market maker updates beliefs after every trade and sets bid and ask as conditional expectations, so the spread exists purely because of information.

Paperby Richard Roll (Journal of Finance, 1984)
A Simple Implicit Measure of the Effective Bid-Ask Spread in an Efficient Market

Four pages. Work through why bid-ask bounce produces negative autocorrelation in price changes, then think about why the estimate breaks when information moves prices too.

Recall#effective-vs-realized

A buyer lifts the ask at 100.02 when the mid is 100.00. One second later the mid is 100.015. Compute the effective spread, realized spread and price impact, and say what they mean for the market maker who sold.

Day 3 — Queue position

Day 03

Where you are in line

Tracking it from L2, and what it is worth

With an order-by-order (L3) feed such as Nasdaq TotalView-ITCH, each add, execute and delete message carries an order reference, so you can track exactly how much size is ahead of your order. With an aggregated (L2) feed you only see total size per price, and you have to estimate:

  • Trades at your price consume the queue from the front, so they're always ahead of you.
  • Cancels could have come from anywhere. You can assume they were all behind you (pessimistic), all ahead (optimistic), or spread in proportion to how much size is ahead versus behind (a common middle ground). Each assumption gives a different estimate, and backtests that use them come out very differently (week 44).

The value of a queue position comes from fill probability and from which fills you get. At the front, you're filled by ordinary flow. At the back, you're often filled only when a large aggressive order sweeps the whole level, and that's often exactly when the price is about to move through you. So the last order in the queue gets the fills it least wants. For a first model of fill probability, treat taker volume at your price as a Poisson process. Real order flow is clustered (one trade makes more trades likely), so treat Poisson as an approximation to improve on, not a truth.

Drill

Estimate fill probability from queue position

Target: 6/6 cases passNot yet run
/// Estimated queue position of ONE of our resting orders at one price
/// level, maintained from an L2 feed (you only see the total size of the
/// level, not the individual orders in it).
pub struct QueueEstimate {
    /// Estimated lots resting AHEAD of us at our price.
    ahead: f64,
    /// Our unfilled quantity.
    remaining: u64,
}

impl QueueEstimate {
    /// We joined the BACK of a level that already held `level_qty` lots.
    pub fn new(level_qty: u64, my_qty: u64) -> Self {
        todo!()
    }

    pub fn ahead(&self) -> f64 {
        todo!()
    }

    pub fn remaining(&self) -> u64 {
        todo!()
    }

    /// `traded` lots printed at our price. Price-time priority: trades
    /// consume the queue from the FRONT. Never let `ahead` go below 0.
    pub fn on_trade(&mut self, traded: u64) {
        todo!()
    }

    /// The level shrank by `cancelled` lots without a trade: someone
    /// ELSE cancelled. `level_qty_before` is the level size just before the
    /// cancel and includes our `remaining`. We can't see where the
    /// cancelled order was, so assume cancels are spread uniformly over
    /// the other orders:
    ///     ahead −= cancelled × ahead / (level_qty_before − remaining)
    /// (do nothing if there are no other orders).
    pub fn on_cancel(&mut self, cancelled: u64, level_qty_before: u64) {
        todo!()
    }

    /// Our own execution report: `qty` lots of our order filled. Under
    /// price-time priority, everything that was ahead of us is gone.
    pub fn on_my_fill(&mut self, qty: u64) {
        todo!()
    }
}

/// Probability our order fills COMPLETELY within a horizon, if taker
/// volume hitting our price arrives as a Poisson process with a mean of
/// `mu` lots over the horizon: P(N ≥ ahead + my_qty), N ~ Poisson(mu).
/// Hint: P(N ≥ k) = 1 − Σ_{i<k} e^(−mu) · mu^i / i!  — build each term
/// from the previous one instead of computing factorials.
pub fn fill_probability(ahead: u64, my_qty: u64, mu: f64) -> f64 {
    todo!()
}
Rust

Look at case 5 in the drill. With an expected 20 lots of taker flow, an order with 5 lots ahead fills completely 99.5% of the time. Put 25 lots ahead and it fills about 2% of the time. Same price, same instrument, and a fill probability fifty times lower. That's why firms spend so much on being first to a new price level.

Recall#back-of-queue

Why can a fill at the back of a long queue be worse news than no fill at all?

Day 4 — Adverse selection and markouts

Day 04

Who did you trade with?

Markout curves and stale quotes

A markout scores each fill by what the market did afterwards. For a fill at price P, with M the mid h after the fill:

buy  markout(h) = M_{t+h} − P
sell markout(h) = P − M_{t+h}

Average the markouts over all your passive fills at several horizons (for example 10 ms, 100 ms, 1 s, 10 s) and plot them against h. A typical passive curve starts at about half the spread, which is what you earned against the mid at the moment of the fill, and then falls as the price moves toward the takers who traded with you. If the curve goes negative and stays there, your fills are worth less than the spread you collected. You're the counterparty informed traders were looking for.

One common source of toxic flow is your own stale quotes. When public information changes, such as a related futures contract moving or a news headline, there's a window in which your quote is out of date. Anyone faster than you can trade against it before you cancel. Budish, Cramton and Shim (2015) argue that continuous limit order books make this race inevitable, because tiny speed advantages translate directly into profits taken from slower liquidity providers. They propose frequent batch auctions instead. You don't have to agree with the remedy to find their description of the problem very useful.

Paperby Eric Budish, Peter Cramton and John Shim (Quarterly Journal of Economics, 2015)
The High-Frequency Trading Arms Race: Frequent Batch Auctions as a Market Design Response

Focus on the mechanism of 'sniping' stale quotes and why it's a cost that liquidity providers pass on through wider spreads. It gives you vocabulary for latency-driven adverse selection that you'll use in weeks 42 and 44.

Recall#markout-reference

When you compute markouts, why must the reference mid at time zero be the mid just BEFORE the fill rather than just after it?

Design note#toxic-sim-flow

Your week 23 simulator has random takers. Design two kinds of simulated trader that would create measurable adverse selection for a passive order: what does each one know, when does it trade, and what markout curve do you expect its fills to produce?

0 chars

Day 5 — Order-flow imbalance

Day 05

What the book says about the next tick

Queue imbalance, weighted mid and OFI

Three short-horizon quantities appear throughout the microstructure literature:

  • Queue imbalance (bid_qty − ask_qty) / (bid_qty + ask_qty). In large-tick instruments it's a well-documented predictor of the direction of the next mid move. A thin ask queue is likely to be depleted first.
  • Weighted mid (bid_px·ask_qty + ask_px·bid_qty) / (bid_qty + ask_qty). Note the cross-weighting: a large bid pulls the estimate toward the ask, because the ask is more likely to give way. People often loosely call this "the microprice". Stoikov (2018) defines a more careful micro-price as the limit of expected future mids, and the two aren't the same.
  • Order-flow imbalance (OFI) from Cont, Kukanov and Stoikov (2014) aggregates every change at the top of the book (new bids, cancelled bids, asks eaten, asks added) into one signed number. Their finding is that short-horizon mid changes are approximately linear in OFI, with a slope inversely related to depth.
Drill

Order-book imbalance signal

Target: 5/5 cases passNot yet run
/// Top of book: best bid and best ask with their sizes. Prices in ticks.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Top {
    pub bid_px: i64,
    pub bid_qty: u64,
    pub ask_px: i64,
    pub ask_qty: u64,
}

/// Queue imbalance (bid_qty − ask_qty) / (bid_qty + ask_qty), in [−1, 1].
/// 0.0 when both sides are empty.
pub fn imbalance(t: &Top) -> f64 {
    todo!()
}

/// Size-weighted mid: (bid_px·ask_qty + ask_px·bid_qty) / (bid_qty + ask_qty).
/// Note the cross-weighting: a big bid pulls the estimate TOWARD the ask.
/// Falls back to the plain mid when both sizes are 0.
pub fn weighted_mid(t: &Top) -> f64 {
    todo!()
}

/// Order-flow imbalance of one top-of-book update (Cont, Kukanov &
/// Stoikov, 2014). With [c] = 1 if c is true, else 0:
///   e =  [bid_px ≥ prev.bid_px]·bid_qty − [bid_px ≤ prev.bid_px]·prev.bid_qty
///      − [ask_px ≤ prev.ask_px]·ask_qty + [ask_px ≥ prev.ask_px]·prev.ask_qty
/// Positive e = buying pressure (bids building or asks being eaten).
pub fn ofi(prev: &Top, cur: &Top) -> i64 {
    todo!()
}

/// Sum of ofi over consecutive pairs (tops[0], tops[1]), (tops[1], tops[2]), …
pub fn ofi_sum(tops: &[Top]) -> i64 {
    todo!()
}
Rust
⚠️
Warning

These are some of the most studied signals in market microstructure, which is exactly why they're not an edge in themselves. Their predictive power is real but small, it decays within seconds or less, and acting on them means paying spread and fees that usually exceed what they predict. In this curriculum they're inputs: features your market maker uses to decide where to quote (week 42), and quantities your backtester has to compute without peeking at the future (week 44).

Paperby Rama Cont, Arseniy Kukanov and Sasha Stoikov (Journal of Financial Econometrics, 2014)
The Price Impact of Order Book Events

Read the definition of OFI and the linear price-impact result. Notice the depth term: the same imbalance moves a thin book much more than a deep one.

Paperby Sasha Stoikov (Quantitative Finance, 2018)
The micro-price: a high-frequency estimator of future prices

Why the size-weighted mid is a crude estimator, and how conditioning on imbalance and spread gives a better one. Skim for the idea, not the estimation details.

Day 6 — Exact analytics on your own venue

Day 06

The view from inside the exchange

What you can measure exactly that outsiders must guess

Outside participants have to guess at things your engine knows exactly. Each Trade event carries the taker's side, so you never need a trade-classification heuristic like Lee–Ready, which infers direction from the trade price relative to the quotes. Each Rested, Cancelled, Reduced and Trade event names the order, so queue position is exact: the size of everything resting ahead of the order at its price, in FIFO order. And the sequence number the engine gives each command (Engine::seq(); events themselves carry none) gives you an exact event clock for markout horizons, alongside the simulator's time.

Design note#analytics-spec

Specify the analytics output before writing it: which series (spread, imbalance, weighted mid) at what sampling (every event, every N events, every simulated millisecond), which markout horizons and in what clock, and how you represent a one-sided book. Justify each choice in one line.

0 chars

Capstone

Build an analytics consumer in crates/sim that reads the engine's ordered event stream, during a week 23 simulation or afterwards from the journal, and computes:

  • Book series: best bid and ask, spread, top-of-book imbalance and weighted mid, maintained incrementally from Rested, Trade, Cancelled, Reduced and Expired events. You can reuse your week 10 L2 builder.
  • Exact queue position of a tracked order: the resting quantity ahead of it at its price, updated as orders ahead trade, cancel or reduce. New orders join behind it and never change it. Write a test that cross-checks it against the drill's L2 estimate, and see how far apart they drift.
  • Markouts for every fill of a tracked account at several horizons, using the pre-trade mid as the reference, plus the average markout curve.

In the repo this is sim::micro (src/micro.rs):

  • TopOfBook { bid, ask } reads the top of the book from the engine, from the week 10 L2Publisher or from a BookBuilder client. Those constructors are provided, and the tests check that all three agree. You write spread(), mid(), imbalance() and microprice(), each None unless the book is two-sided. microprice() is the size-weighted mid: Day 5's loose usage, not Stoikov's micro-price.
  • Queue position comes two ways, and the tests require them to agree exactly at every step. queue_position(&engine, id) reads the engine's L3 view of the order's level. QueueTracker starts once from Engine::level_orders and then follows the engine's events alone. Both give a QueuePosition { orders_ahead, qty_ahead, remaining }, and None once the order has filled or been cancelled.
  • effective_spread(taker_side, price, mid_before) and realized_spread(taker_side, price, mid_later) are Day 2's formulas.
  • Markouts::new(&horizons) takes times as any non-decreasing u64 (the tests use the command index). Feed it on_mid(t, mid) and on_fill(t, side, price, qty) in time order. A fill is marked at horizon h with the last mid recorded at a time ≤ t + h, so results trail the clock by one update. finish(t) resolves everything due by t and leaves later horizons pending instead of counting them as zero. stats() gives fills, quantity and total markout per horizon, and per_unit() the average.

Watch horizon 0. If you record the mid after each command, as the tests do, a fill's horizon-0 mark uses the post-trade mid, the one Day 4's check warns about. In the repo the pre-trade reference belongs to effective_spread, whose mid_before you read before the command runs.

Decisions to make deliberately:

  • Mid representation. (bid + ask) / 2 is often half a tick. Store bid + ask as an integer (the mid in half-ticks) so the analytics stay exact and convert only for display. The repo takes the other option and uses f64: the analytics are off the order path, and every value in its tests is a multiple of ½, which f64 holds exactly.
  • Horizon clock. Event time (N events later) is deterministic and independent of load. Simulated wall time is what traders mean. Pick one and label it everywhere.
  • One-sided books. With no bid or no ask there's no mid. Decide whether to skip the sample, carry the last mid forward, or emit an explicit gap, and document the choice. In the repo, mid() returns None, and the randomized test skips that sample, so Markouts carries the last mid forward.
  • Sweeps. One aggressive order produces several Trade events. For price impact, aggregate them per taker; for queue position, apply them one by one.

Pitfalls: markouts measured from the post-trade mid; inferring trade direction when the event already tells you; counting a self-trade as adverse selection; markout horizons that run past the end of the simulation (drop those fills, don't treat missing data as zero).

Capstone · in your exchange · crates/sim

Microstructure analytics from engine events

An analytics pass in crates/sim over the engine's event stream: spread, imbalance and weighted-mid series, the exact queue position of a tracked order, and markouts after fills at several horizons.

cargo xtask test --week 41      # tests named w41_*
cargo xtask bench --week 41     # p50 / p99 / p99.9 on your machine
cargo xtask report --upload  # update this page and your lab
  • All week 41 tests pass w41_*

Feeds into

  • Week 42 builds a market maker that quotes around the weighted mid, worries about queue position and judges itself by markouts.
  • Week 44 needs this week's fill and queue models. A backtester that replays L2 data has to use the queue-position estimate you wrote today as its passive-fill model, and its honesty depends on it. The repo's backtester replays the command log through the engine instead, so the strategy's orders take exact places in the engine's FIFO.