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

Latency Arbitrage, Multi-Venue & the Tournament

The same instrument trades in two places at once, and for a few microseconds the two places disagree. That gap is a trade, and it belongs to whoever gets there first. Everything from the term arrives at once here — the parse, the book, the pool, the codec, the lock-free pipeline, the killed tail — and it is scored live on a composite, not on latency alone.

Finale scenario: multi-venue, futures, long auctions, an FPGA IPO mid-session Final project: a tournament-ready bot + a short performance writeup Final exam: on-site, closed-book, the whole term
Part 1

1What is it? One name, two books, a few microseconds of disagreement

Liquidity is fragmented: the same symbol has its own book, its own touch and its own queue on every venue. A trade or a cancel on venue A takes time to be reflected in what venue B is showing, and during that time the consolidated best bid can sit above the consolidated best offer. A crossed book across venues is not an error — it is a stale quote with a timer on it.

Reading a crossed consolidated book

 VENUE A          VENUE B (stale)
 bid     ask      bid     ask
 100.05  100.07   100.02  100.04   ← lagging

 NBBO bid = max(100.05, 100.02) = 100.05
 NBBO ask = min(100.07, 100.04) = 100.04
 → crossed: B.ask 100.04 < A.bid 100.05

 ACTION: buy B @ 100.04, sell A @ 100.05
 EDGE:   1.00 ¢ gross, minus BOTH sides' fees
 SIZE:   min(A.bid_sz, B.ask_sz) — the thin side

A locked market is best bid equal to best offer; a crossed one is bid above offer. Both are transient and both are public — which is exactly why they are a race and not an insight.

Why the window exists at all

Nothing is instantaneous. A print on A has to reach B's participants, who then have to decide, send, and be processed. Every one of those steps is a queue with a service time, so B's displayed quote is always a picture of a slightly older world. The window is the sum of those delays minus your own reaction time.

That is the entire economic content of low-latency trading: not prediction, but arriving inside a window somebody else's latency created. It is also why the edge per trade is tiny and the count is enormous, and why fees decide whether the trade exists at all.

In the arena. The finale scenario turns multi_venue on: the same symbols list on two venues, each forming its own price from its own book, and the exchange server timestamps who got to the stale side first. Set EXCHANGE_URLS to a comma-separated list and one process sees both books.

Interactive · the two-venue race

The mid moves on venue A at t = 0. B's quote is stale until the news propagates and B's maker cancels. You have to see it, decide, and land an order on B inside that window — before the competitor does. Move the latencies and watch who collects.

your path competitor stale-quote window
—

The decision, as a pure function

struct Top { double bid = 0.0, ask = 0.0, bid_sz = 0.0, ask_sz = 0.0; };

// +1 = buy on B, sell on A.  -1 = the mirror.  0 = no edge.
int cross_venue_arb(const Top& a, const Top& b, double fee_per_side) {
  if (a.bid <= 0.0 || a.ask <= 0.0 ||
      b.bid <= 0.0 || b.ask <= 0.0) return 0;
  const double edge_buy_b = a.bid - b.ask - 2.0 * fee_per_side;
  const double edge_buy_a = b.bid - a.ask - 2.0 * fee_per_side;
  if (edge_buy_b > 0.0 && edge_buy_b >= edge_buy_a) return +1;
  if (edge_buy_a > 0.0)                          return -1;
  return 0;                        // most ticks: do nothing
}

// never send for more than rests on the thin side
double tradable_size(const Top& a, const Top& b, int dir, double clip) {
  const double sz = (dir > 0) ? std::min(a.bid_sz, b.ask_sz)
                              : std::min(b.bid_sz, a.ask_sz);
  return std::min(sz, clip);
}
Part 2

2How it works — fastest, smartest, and the cost of every message

Being first is one way to win and it is not the only one. A latency-arbitrage window rewards pure speed; a market-making book rewards being right about fair value and being disciplined about inventory. Most of the term's engineering serves both, but the budget you spend on each is a real choice with a measurable optimum.

Interactive · speed versus alpha

You have one engineering budget. Spend it on latency and you win more races at a lower edge per race; spend it on the signal and you win fewer races but you are right more often — and being wrong quickly is the most expensive thing in this course.

—

Two different games

  • Pure latency arbitrage. The signal is arithmetic — the books are crossed or they are not. There is nothing to be clever about, so the whole contest is arrival time, and the winner takes all of it.
  • Market making. The signal is fair value, and speed buys you the ability to cancel before you are picked off. Here latency is defensive, and alpha is what decides whether you should have been quoting at all.
  • Everything in between is where the arena's composite score lives, which is why it refuses to rank you on one number.
The scoring consequence. The arena can charge your measured reaction latency against your P&L, so a slow-but-clever bot genuinely can lose to a fast-but-simple one — and an expensive signal that slows your p99 has to pay for the microseconds it consumed.

Interactive · the message budget and your order-to-trade ratio

Messages are a resource. The finale scenario cuts order_quota to roughly six messages per tick, and real venues police the order-to-trade ratio — orders and cancels sent per trade actually done — with fees, throttles and rejects. Spend messages where the edge is, not where the excitement is.

Where your messages went

—

The requote decision that saves messages

enum class Action { Hold, Requote, Cancel };

Action requote_decision(double my_px, double best_px,
                        long queue_ahead, long level_qty,
                        bool moved_against) {
  if (moved_against)               return Action::Cancel;  // stale: pull
  if (my_px < best_px)             return Action::Requote; // not at best
  if (queue_ahead > level_qty / 2) return Action::Requote; // buried
  return Action::Hold;               // good spot: keep the message
}

A cancel-and-repost costs a message and sends you to the back of the FIFO at the new price. The winning move on most ticks is Hold — which is the hardest thing to make a bot do.

Interactive · the market-maker score

Raw P&L flatters a maker: the rebate arrives immediately and the adverse selection arrives later. The score is built in the spirit of the arena's season math — a risk-adjusted quantity, penalised by drawdown — over the four components a maker actually controls. Move the weights and watch three plausible market makers change places.

#makerspreadrebateadverseinv. carrymax DDscore

The formula on the page

gross     = spread_captured + rebate
net       = gross − w_adv × adverse_selection
                  − w_inv × inventory_carry
risk_adj  = net / max(vol, vol_floor)
score     = risk_adj × max(0, 1 − pen × max_drawdown)
                     (pen defaults to 2.0: a 25% drawdown halves it)
—

The three makers

    Why divide by volatility. Without it, the way to win is leverage: quote enormous size, accept an enormous swing, and hope the session ends on a good tick. Dividing by realised volatility and then penalising the drawdown makes the same P&L earned smoothly beat the P&L earned by surviving a near-miss — and the penalty is applied by sign, so a big drawdown always hurts.
    Part 3

    3Why it matters — the tournament, and the whole course in one path

    The finale turns every market-structure flag on at once: two venues, futures, the longest opening and closing auctions of the season, the scarcest borrow, the tightest message quota and the widest position limit — plus an FPGA maker's IPO listing mid-session. There is nowhere left to hide a weak stage.

    The finale scenario, in numbers

    flagsettingwhat it does to you
    multi_venueonthe same symbol on two books — cross-venue races and a consolidated touch
    futures_enabledonan index future to hedge or to basis-trade against the constituents
    opening auction15the longest of the season: a single clearing price, not a queue to race
    closing auction12your last chance to flatten at a print everyone gets
    order_quota≈6 / tickmessages are the scarce resource; a wasted cancel is a missed quote
    position_limit1200the widest of the season — room to be wrong at scale
    short locate cap600the tightest borrow: shorting is a privilege you have to secure
    IPOmid-sessiona listing-day primary event, priced fast, against a hardware-floor field
    Auctions are not races. Everything you built for continuous trading — queue position, cancel latency, the pickoff — does nothing in a single-price auction. Submit into it on the merits and spend the microseconds elsewhere. Knowing when not to be fast is part of the grade.

    Ethics, briefly, and for real

    The case for. Tighter spreads, deeper books, continuous liquidity, and price discovery knitted across venues — measurably lower costs for everybody who is not racing.

    The case against. An arms race spends real resources to move wealth a few microseconds earlier, and the edge is partly purchased: colocation and proprietary feeds are for sale, so access is not equal.

    Stability. Fast, correlated withdrawal of liquidity is how a flash crash starts, which is why circuit breakers, LULD bands, tick sizes, speed bumps and kill switches exist — market structure is a design, and you are now someone who designs it.

    Your part. Fast code that respects its risk limits and does not destabilise a book. You should be able to argue both sides of this in the exam without reaching for a slogan.

    Interactive · the tournament scoreboard

    Five metrics, each normalised to 0–100 so they are commensurable, then weighted. Move the weights and watch the ranking move — that is not a bug in the rubric, it is the rubric telling you that "best bot" is a question about what you are measuring.

    #teamp99.9fill qualityP&LOTRthroughputcomposite
    —

    How each metric is normalised

    • p99.9 — tail latency, inverted and log-scaled: 20 µs scores 100, 20 ms scores 0.
    • fill quality — fill rate weighted by markout, so fills that lose money do not count.
    • P&L — risk-adjusted and drawdown-penalised, as in the season math.
    • OTR — orders and cancels per trade; inside the venue's tier scores 100, over it decays.
    • throughput — messages per second sustained when the whole class fires at once.

    Interactive · the course in one path

    Every stage of tick-to-trade was somebody's week. Click a stage — or tab to it and press Enter — to see which week owns it, what it costs, and what you did to it. This is the recap: fifteen weeks, one number.

    —

    —

    Part 4

    4Five ways the finale goes wrong

    Every one of these is a bot that works perfectly and loses.

    An arb that is gross-profitable and net-negative

    The books are crossed by a tick, you lift and hit, and you pay a taker fee on both legs. A one-cent gross edge against two aggressive fees is frequently a loss, and it is a loss you will make thousands of times before you notice, because each one is tiny and your fill rate looks superb.

    The same arithmetic kills the marginal arb under a scarce quota: a trade whose edge rounds to zero has still consumed a message that a real opportunity needed.

    Fix: compute the edge net of both sides' fees, require a margin above zero rather than a sign, and size to the thin side. Then check it against the markout — an arb whose second leg never fills is not an arb, it is a position.

    Cancelling your way to a perfect queue position

    Requote on every mid tick and you will always be at the best price and you will almost never trade: each cancel-and-repost puts you at the back of the FIFO at the new price, and you spend your entire message budget doing it.

    Worse, it looks like diligence. The dashboards show you at the touch constantly; the fill rate quietly collapses, the OTR climbs into a penalty tier, and the venue starts rejecting you.

    Fix: make Hold the default. Requote when you are no longer at best, or when you are buried behind more than about half the level; cancel when the price moved against you. Otherwise keep the spot and the message.

    Racing an auction

    An auction matches everybody at one clearing price. Queue position is irrelevant, cancel latency is irrelevant, and a burst of aggressive orders in the last milliseconds achieves nothing except spending your quota and possibly moving the indicative price against yourself.

    The finale has the longest auctions of the season at both ends of the session, so a bot that only knows how to race will spend a meaningful fraction of the day being wrong about what game it is playing.

    Fix: branch on the market state. In an auction, submit on the merits — size and limit — and use the microseconds you saved on the continuous session that follows.

    Trading two venues from one cached top of book

    on_book fires per venue. If you keep one Top per symbol instead of one per venue, every update overwrites the other venue's picture and your "crossed book" is you comparing venue A against venue A a moment ago. It will fire constantly and it will be wrong every time.

    The subtler version: you keep both, but you never age them. A quote from 400 ms ago is not a quote — it is a memory, and acting on it is how you become the stale side.

    Fix: touch_[symbol][venue], POD, no allocation, with a timestamp per entry and a staleness bound. If the other venue's picture is older than the window you are trying to exploit, there is no trade.

    Bringing a binary that does not build from your repo

    The tournament is a demo with a deadline. A bot that only builds on your laptop, needs a file that is in .gitignore, or depends on a local uncommitted patch does not enter — and a late submission cannot enter either. This is not a technicality; it is the same discipline as deterministic replay.

    Fix: clean clone, clean build, run the replay harness, check the release flags, and do it the day before. Then bring the numbers: p50, p99, p99.9, and one page on where your budget goes and what you would cut next.

    Part 5

    5Check yourself

    Three questions, instant feedback, nothing recorded.

    Q1A's bid is 100.05, B's ask is 100.04, both with size. Your taker fee is 0.6 ¢ per share per side. What is the trade?

    Q2Your bid is at the best price, queue_ahead is 120 of a 1000-share level, the mid has not moved, and your quota is six messages this tick. What is the right action?

    Q3Bot A has the best p99.9 in the room, a negative +1 s markout and an OTR in the venue's penalty tier. Bot B is second on the tail with a positive markout and a clean OTR. Who wins the composite?

    Part 6

    6Do the work — and then compete

    In the lab (~45–60 min)

    • Read the finale scenario before you code: two venues, auctions at both ends, a tight order_quota, an FPGA IPO mid-session.
    • Sketch the cross-venue stale-quote detector as a pure function, fees on both legs, sized to the thin side — then unit-test it without a socket in sight.
    • Sketch the queue-aware requote from on_ack / on_queue state, with Hold as the default branch.
    • Map every metric of the composite to the lever you built for it, and name the metric each lever hurts. Optimising one wrecks another; the grade rewards the balance.
    • Re-run latency_replay.py and latency_report.py one last time. Those are the numbers you bring.

    Final project & final exam

    • Tournament-ready bot. Your best binary for the finale scenario — multi-venue, quota-aware, tail-optimised, colocated if you decided the economics work.
    • Performance writeup (1–2 pages). Where your latency budget goes today, your p50/p99/p99.9, and what you would cut next.
    • Strict deadline. Both due before the finale session; a late bot cannot enter.
    • Bring a clean build from your repo. You compete with what compiles.
    • Final exam. On-site, closed-book, coding questions across the whole term: microstructure and the order book; memory hierarchy, alignment and benchmarking; pools, arenas and no-alloc hot paths; templates, CRTP and constexpr; cache-friendly books and ring buffers; memory order and SPSC queues; kernel bypass and zero-copy parsing; perf, flame graphs, tick-to-trade and the tail.
    Code
    Get the code
    Both tracks share one starter tree. The finale runs the full-structure scenario; latency_report.py prints the percentiles the composite consumes.
    Previous
    ← Week 14 · Profiling, the Tail & Production
    The tail you killed last week is the reason you are in this race at all.
    The whole course
    Home · start again →
    Fifteen weeks, one number: tick-to-trade. Measure, don't guess; kill the tail; own every cycle.