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.
spread, mid, microprice, obi — and say which way each of them leans.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.
| Term | Definition | What it is for |
|---|---|---|
| best bid / ask | highest buy, lowest sell resting | the touch — where you can trade now |
| spread | ask − bid | the price of immediacy, in ticks |
| mid | (bid + ask) / 2 | a 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 |
| depth | resting quantity per level | how far a market order walks |
| queue_ahead | same-side, same-price qty in front of you | how much must trade before you fill |
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.
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.
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.
Post an order to join a queue.
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.
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.
// 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);
}
These are the mistakes that survive all fifteen weeks if nobody names them now. Open them.
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.
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.
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.
printf showsA 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.
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.
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?
cmake -B build && cmake --build build — and run
the grader once (make test) to see the whole term laid out in red.[latency] tick->order = N us line, and find
yourself on the LATENCY tab.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.spread should return on a locked or crossed book, and defend it.static_assert coverage of the
balanced case.