WEEK 1 · 15-WEEK TRACK · SESSION 1 · 9-SESSION TRACK

HFT Landscape & Market Microstructure

High-frequency trading is not forecasting. It is a race: two bots running the same idea finish in the order their messages reach the matching engine. Before you write a line of C++ you need to know what you are racing for — a place in a queue — and what it is worth.

Lab: build the client, land on the board, hand-compute the metrics No optimisation yet — orientation and vocabulary Speed buys queue position, nothing else
Part 1

1What is it? Four participants and one data structure

Every modern equity, futures and crypto venue runs the same thing: a central limit order book — resting orders on two sides, matched by rules that fit on an index card. Everything else in this course is a consequence of those rules and of the fact that several people are trying to obey them faster than you.

Who they are

—

Objective

Holding horizon
Latency sensitivity
Messages per fill
Capital at risk

What they optimise

    How they lose money

      The four numbers, and what each one leans on

      TermDefinitionWhat it is for
      best bid / askhighest buy, lowest sell restingthe touch — where you can trade now
      spreadask − bidthe price of immediacy, in ticks
      mid(bid + ask) / 2a reference, not a prediction
      microprice(ask·bid_sz + bid·ask_sz) / (bid_sz + ask_sz)size-weighted touch — leans toward the thin side
      obi(bid_sz − ask_sz) / (bid_sz + ask_sz)imbalance in [−1, 1]; +1 = all bids
      depthresting quantity per levelhow far a market order walks
      queue_aheadsame-side, same-price qty in front of youhow much must trade before you fill
      Note the cross-weighting in the microprice. The bid size multiplies the ask price. With 800 lots bid and 200 offered, buyers dominate: obi = +0.6 and the microprice sits at 100.016 — above the 100.010 mid, leaning toward the ask. Over a short horizon the microprice predicts the next mid better than the mid does, and that lean is the cheapest alpha in the building.
      // book_metrics.cpp — real, compilable C++20
      #include <algorithm>
      #include <cstdio>
      
      struct Metrics { double mid, spread, micro, obi; };
      
      constexpr Metrics compute(double bid_px, double bid_sz,
                                double ask_px, double ask_sz) noexcept {
          const double sz = bid_sz + ask_sz;
          return Metrics{
              .mid    = 0.5 * (bid_px + ask_px),
              .spread = std::max(0.0, ask_px - bid_px),   // locked/crossed -> 0
              .micro  = sz > 0.0 ? (ask_px * bid_sz + bid_px * ask_sz) / sz
                                 : 0.5 * (bid_px + ask_px),
              .obi    = sz > 0.0 ? (bid_sz - ask_sz) / sz : 0.0};
      }
      
      constexpr bool near(double a, double b, double eps = 1e-9) noexcept {
          return (a > b ? a - b : b - a) <= eps;
      }
      
      // the balanced-book sanity check, moved to compile time
      static_assert(compute(100.00, 500, 100.02, 500).obi == 0.0);
      // NOT ==: micro is 100.01000000000001, mid is 100.00999999999999
      static_assert(near(compute(100.00, 500, 100.02, 500).micro,
                         compute(100.00, 500, 100.02, 500).mid));
      
      int main() {
          const auto m = compute(100.00, 800, 100.02, 200);
          std::printf("mid=%.4f spread=%.4f micro=%.4f obi=%.4f\n",
                      m.mid, m.spread, m.micro, m.obi);
          // mid=100.0100 spread=0.0200 micro=100.0160 obi=0.6000
      }

      clang++ -std=c++20 -O2 book_metrics.cpp — and note the second static_assert. Writing it as micro == mid does not compile: the two expressions differ by one ulp (100.01000000000001 against 100.00999999999999) because they sum the same terms in a different order. Two computed doubles are never compared with ==. Week 6 is about moving much more of your checking here, to compile time.

      Order types: you may fix the price, or fix the fill, not both

      Every order type is a position on one trade-off. A limit order fixes your price and leaves the fill uncertain; a market order fixes the fill and leaves the price uncertain. Pick a type and see where it sits.

      The type

      —

      Price certainty
      Fill certainty
      Fee side
      Queue effect
      Part 2

      2How it works: run the book yourself

      This is a working central limit order book with price-time priority. Post a limit order and it joins the FIFO queue at its price — behind everything already there. Send a market order and it walks the other side, oldest order first at each level. Watch your queue_ahead, then press Reprice and watch what repricing costs you.

      Best bid
      —
      Best ask
      —
      Spread
      —
      Mid
      —
      Microprice
      —
      OBI
      —
      Your order
      none
      Queue ahead
      —
      Level qty
      —
      Your fills
      0

      Send an order

      The queue at your price

      Post an order to join a queue.

      Engine log

      The two rules, and nothing else. Price first — a more aggressive price always trades ahead of a worse one. Then time — at the same price, whoever arrived earlier fills first, strictly FIFO. That is the entire matching algorithm, and it is why speed is worth money: it is the only way to buy the second rule.
      Part 3

      3Why it matters: 10 µs versus 100 µs, in fills

      Here is the payoff, and it is arithmetic, not attitude. You and a rival see the same book update at the same instant and both decide to post the same limit order at the same price. The engine does not know who is cleverer. It timestamps arrivals. The slower order sits behind the faster one, and when a taker arrives with less quantity than the queue, only the front of the queue gets paid.

      Controls

      You win the race
      —
      Your fill probability
      —
      Expected filled qty
      —
      Rebate per 1,000 ticks
      —

      The arena is the live market, and the LATENCY board is the grade

      The venue, the feed and the matching engine are already running; your bot is just another client on the wire, speaking JSON over a WebSocket. You override one hook, on_book, and every order you send from inside it is timestamped from feed-decode to send. That microsecond number is what the LATENCY board ranks — at p50, p99 and p99.9.

      Your latency distribution

      p50
      —
      p99
      —
      p99.9 · the grade
      —
      Worst tick
      —

      What the board would say

      The mean is not on the board. Turn on one malloc per tick: the median barely moves and p99.9 explodes, because an allocator call is usually a few tens of nanoseconds and occasionally a lock, a free-list walk or a page fault. Weeks 3 and 4 exist to delete that line from your hot path; Week 14 teaches you to prove it is gone.
      // include/hft_bot.hpp — the one hook that matters
      void on_book(const std::string& symbol,
                   double bid, double ask, double mid,
                   double microprice, double obi) override {
          if (bid <= 0.0 || ask <= 0.0) return;      // need two sides
          if (ask - bid < 0.02) return;              // spread too tight
          // orders sent from here are latency-stamped for you
          if (obi > 0.4) buy_limit(symbol, 1, bid);
      }
      Part 4

      4Five ways week one goes wrong

      These are the mistakes that survive all fifteen weeks if nobody names them now. Open them.

      Treating the mid as fair value

      The mid is the average of two prices and ignores how much size stands behind each one. With 100.00 × 800 bid and 100.02 × 200 offered, the mid says 100.010 and the book says buyers are three times as eager: obi = +0.6, microprice 100.016. A strategy that mean-reverts to the mid here is systematically selling into pressure.

      Fix: use the microprice as your reference and the mid only as a display number. Both arrive pre-computed in on_book, so there is no excuse of cost — and Week 8 shows you how to update them in O(1) when you compute them yourself.

      Repricing on every tick, and never counting the queue you threw away

      Cancel plus re-post is two cheap messages and one expensive consequence: a new timestamp, which means the back of the queue. If 900 lots were ahead of you and you were 200 from the front, one reprice can cost you the whole session's fills at that level.

      The tell is a bot with an excellent order-to-trade ratio in the wrong direction: thousands of messages, almost no fills, and a P&L that is entirely fees.

      Fix: put a hysteresis band on requoting — only move when the microprice has left the band by more than the queue is worth — and track queue_ahead from the acknowledgement so the cost is a number in your log, not a feeling.

      Assuming a fill is good news

      Your resting bid fills instantly and in full. That usually means the market just decided your price was generous: somebody with better information crossed the spread to hit you. This is adverse selection, and it is why the fastest passive fills are often the worst ones.

      Fix: measure a markout — the mid 1 s, 5 s and 30 s after each fill — and grade yourself on markout-adjusted P&L, not on fill count. Speed's second job, after winning the queue, is letting you cancel before the bad fill happens.

      Optimising the mean because the mean is what your printf shows

      A 38 µs average with a 5.2 ms worst tick is a losing bot, and the average will never tell you. The races that pay are concentrated in the busiest microseconds of the session — exactly when queues are longest, when everyone fires at once, and when your tail shows up.

      Fix: collect a sample, sort it, and report p50 / p99 / p99.9 from the first week. The arena's harness already does; make your own benchmarks match it so the numbers are comparable.

      Measuring latency with the wrong clock — or with the print inside the timer

      std::chrono::system_clock is wall-clock: it can step backwards when NTP corrects it, which turns a percentile table into fiction. And a std::cout inside the timed region measures your terminal, not your strategy — a flush is easily tens of microseconds.

      Fix: std::chrono::steady_clock (monotonic) around the smallest region you mean to measure; log to a pre-allocated ring buffer and drain it off the hot path. Week 2 builds the harness properly, and Week 14 shows what the harness still hides.

      Part 5

      5Check yourself

      Three questions, instant feedback, nothing recorded.

      Q1Two buy orders rest at 100.02: yours for 500 lots, posted 40 µs later than a rival's 100-lot order. A taker sells 200 lots. Who trades?

      Q2The book is 100.00 × 800 bid, 100.02 × 200 offered. Where is the microprice, and what is the OBI?

      Q3Bot A: p50 = 38 µs, p99 = 71 µs, p99.9 = 210 µs. Bot B: p50 = 22 µs, p99 = 95 µs, p99.9 = 2.1 ms. Which single number tells you B loses races A wins?

      Part 6

      6Do the work

      In the lab (~60 min)

      • Build the arena C++ client — cmake -B build && cmake --build build — and run the grader once (make test) to see the whole term laid out in red.
      • Connect with your team id, confirm the [latency] tick->order = N us line, and find yourself on the LATENCY tab.
      • Write compute(bid_px, bid_sz, ask_px, ask_sz) yourself and check it on three snapshots: balanced 500/500, bid-heavy 900/100, ask-heavy 100/900.
      • Decide what spread should return on a locked or crossed book, and defend it.

      After the session

      • Formalise the metrics into a small header with static_assert coverage of the balanced case.
      • Run the offline replay harness and record your p50 / p99 / p99.9 — this is your baseline for the next fourteen topics.
      • Read the wire protocol end to end. You cannot parse quickly what you do not know the shape of.
      • In one paragraph: why does cancel-and-replace cost more than the two messages it sends?
      Get the code
      15-week track starter
      Header stubs, the autograder and the C++ arena client, one topic per session.
      Get the code
      9-session track starter
      The same stubs and the same grader, packaged for the condensed nine-session schedule.
      Course home
      The two-track map
      Where week 1 sits, and the tick-to-trade path every later topic makes cheaper.
      Next
      Week 2 · C++ Performance Foundations →
      Now find out what every operation on that hot path actually costs, in nanoseconds.