WEEK 3

Anatomy of a Real-Time Trading System

Five processes, one order book, and a clock. This is the first week the system is real: your bot connects to a venue somebody else wrote, and from now on the only honest description of how well it works is a latency distribution with a tail.

Lab: run a local venue, a maker and your bot; plot the tail HW3: order-book invariants + your own latency histogram Project P0: teams, roles and a $1M budget
Part 1

1What is it? Five processes that only ever exchange messages

An event-driven architecture has no main loop that “does the work”. Every process sits blocked on its socket until something arrives, handles it, and blocks again. There is no polling, no schedule, and no central brain — which is exactly why the arrows, not the boxes, are what you have to design, measure and defend.

The rule that makes it a system

Every process owns its state and publishes changes; nobody reaches into anybody else’s memory. The venue owns the book and the authoritative portfolios. Your bot owns its strategy state and a cache of the book. When those two disagree, the venue is right and you have a bug — that asymmetry is the whole reason the design works.

Corollary. Anything you cannot see in a message, you cannot debug after the fact. Design the messages for the post-mortem you will write in week 10.

Why event-driven, and not a loop that polls

A polling loop asks “anything new?” on a schedule you invented. On average it is half an interval late; at worst a full interval; and if two things happen inside one interval it sees one of them. An event-driven handler is woken by the kernel when the data is actually there.

Part 3 makes both of those claims measurable. Polling is not slower in some vague way — it adds a known, quantifiable delay you did not need to pay.

Interactive · click a component

The five processes of a live session and the messages between them. Click any box — or tab to it and press Enter — to see what it owns, what it sends, which file it lives in, and how it fails. Then filter the arrows down to one flow.

The exchange

—

Owns

    Speaks

      Lives in

      Part 2

      2How does it work? Price-time priority, by hand

      The central limit order book is the one piece of this system with no I/O, no clock and no opinions — which is why it is the piece that must be exactly right. Two sorted queues and one rule: best price first, then earliest arrival. Post orders below and watch the rule decide who gets filled.

      Best bid
      —
      Best ask
      —
      Mid
      —
      Spread
      —
      Microprice
      —
      Imbalance
      —
      Your position
      0
      Your cash
      0.00
      Fees paid
      0.00
      Rebates
      0.00

      Trade tape · newest first

      Send an order

      My resting orders

      # shared/orderbook.py — no sockets, no clock, 100% testable
      def add_order(self, o: Order) -> list[Trade]:
          book = self._asks if o.side == 'buy' else self._bids
          trades = []
          while o.remaining and book and self._crosses(o, book[0]):
              r = book[0]                       # best price, earliest arrival
              qty = min(o.remaining, r.remaining)
              trades.append(Trade(price=r.price, qty=qty))   # RESTING price
              o.remaining -= qty; r.remaining -= qty
              if not r.remaining: book.pop(0)
          if o.remaining and o.order_type == 'limit':
              self._rest(o)                    # market/ioc remainder is cancelled
          return trades
      Part 3

      3Why it matters: the tail is the system

      Here is the habit this course is really about. You will be asked, every week from now on, what your p50 and p99 are — and you will never again be allowed to report a mean. Add work to on_tick below and watch the two numbers move by completely different amounts.

      p50
      —
      p99
      —
      Max
      —
      Mean
      —
      Deadline miss
      —
      StageTypicalOwnerCan you fix it?

      What did you put in on_tick?

      # The only instrumentation you need, and it is four lines.
      t0 = time.perf_counter_ns()
      sig = self.on_tick(market, portfolio)
      t1 = time.perf_counter_ns()
      self.hist.append((t1 - t0) / 1e6)     # ms, per tick
      
      # At session close, print the percentiles — never the mean.
      p50, p99 = statistics.quantiles(self.hist, n=100)[49::49]

      Interactive · event-driven versus polling, on the same events

      Same market, same twelve book updates. The top lane polls on a fixed interval; the bottom lane is woken by the kernel when the bytes arrive. Step through and watch the delay bars — and watch the polling lane miss updates entirely when two land inside one interval.

      Controls

      Poll · mean delay
      —
      Poll · worst
      —
      Poll · missed
      —
      Poll · wasted
      —
      Event · mean delay
      —
      Event · worst
      —
      Event · missed
      0
      Event · wasted
      0
      Part 4

      4Five ways this goes wrong

      These are the five that cost marks in week 3, every time. Open them.

      Reporting a mean latency

      A pipeline that averages 3 ms against a 5 ms deadline sounds healthy and can still miss one deadline in fifty. The mean is a summary of the cases that went fine; the deadline only ever sees the cases that did not.

      The second half of the same mistake is a percentile computed over the wrong window — a p99 across the whole session hides a two-minute period where it was p50. Report percentiles per window, and report the max separately, because the max is the one your safety case has to survive.

      Fix: keep the raw samples, report p50/p99/max, and plot the histogram. If you only ever look at one number, look at p99.

      Trusting your local copy of the book

      Your bot keeps a local book so on_tick is fast. Fine — until you miss a snapshot, mishandle a cancel, or reconnect without re-syncing. Now you are computing a signal from a book that does not exist, and the venue will happily fill you at prices your model says are impossible.

      The failure is silent and self-reinforcing: a wrong local book produces confident orders, and the fills you get back look like evidence that the book was right.

      Fix: the venue is the source of truth. Reconcile on every snapshot, log the disagreement, and on reconnect throw your local state away and ask for a fresh snapshot rather than resuming.

      Confusing the ack with the fill

      OrderAck means “accepted, and here is your order id”. It does not mean you traded. A limit order that rests gets an ack and no fill, possibly forever. Treating the ack as a fill gives you a phantom position, and every risk check after that is computed on a number that is wrong.

      The inverse also bites: fills can arrive in pieces. One order, four TradeExecution messages, four different prices — your average entry price is a weighted average, not the last one you saw.

      Fix: position changes only on TradeExecution. Track resting orders separately from positions, accumulate partials, and reconcile both against PortfolioUpdate.

      Doing real work inside on_tick

      Recomputing a 500-point rolling standard deviation in pure Python, building a DataFrame, writing a log line to a slow terminal — each is a couple of milliseconds that used to be a rounding error and is now your whole budget. Worse, each one is bursty: it is the p99 that moves, not the p50, so a casual look at the average tells you nothing happened.

      Fix: keep incremental state instead of recomputing (a running sum, a ring buffer), batch your logging, and move anything genuinely heavy off the hot path with run_in_executor. Then measure again — the histogram in Part 3 is the deliverable precisely because it makes this visible.

      An order book that is a list you re-sort

      self.bids.append(o); self.bids.sort(key=lambda o: -o.price) is correct and is O(n log n) on every single order. It also loses time priority the moment two orders share a price and your sort is not stable in the way you assumed — and time priority is the fairness rule of the venue, so getting it wrong is not a performance bug, it is a correctness bug that pays somebody else your fill.

      Fix: keep price levels in sorted order and a FIFO queue within each level; insert in the right place instead of re-sorting. Then write the invariant tests: best bid never exceeds best ask, total quantity is conserved across a match, and the earliest order at a price always fills first.

      Part 5

      5Check yourself

      Three questions, instant feedback, nothing recorded.

      Q1A sell limit rests at 184.00. Your buy limit arrives at 184.05. At what price does the trade print?

      Q2You add a rolling-variance recompute to on_tick. It costs 2 ms, but only on the 1 tick in 20 where a price level changed. What happens to your numbers?

      Q3Instead of handling pushed updates, your bot polls the venue for the book every 100 ms. What is the worst-case delay you have added, and what else have you lost?

      Part 6

      6Do the work

      In the lab (~75 min)

      • Run a local venue, a house market maker and your own bot — three processes on your laptop, talking only in messages.
      • Read the book: print best bid, best ask, spread, microprice and imbalance every second, and sanity-check them by hand against the ladder.
      • Instrument tick → decision → ack, collect a few thousand samples, and plot the tail. Not the average. The tail.

      Homework 3 & Project P0

      • HW3: order-book invariant tests — best bid never crosses best ask, quantity conserved through a match, earliest order at a price fills first.
      • HW3: your own latency histogram with p50, p99 and max, plus one paragraph on which stage dominates and why.
      • P0: team, roles, and a defended split of the $1M budget across exchange licence, broker desks and trader seats; starter repo generated and connected.
      • One-line AI-USE note on each deliverable.
      Code
      Get the code
      Labs and homework live in your course’s own site; the starter repo is public. Use it as a template, clone, make install.
      Previous
      ← Week 2 · Concurrency & Networking
      The loop and the wire underneath everything on this page.
      Next
      Week 4 · Intelligence in the Loop →
      Now put a signal inside the budget you just measured — and defend the accuracy you traded for it.