“The market” is not one place. The same share of Apple can trade on a lit exchange, inside a dark pool at the midpoint, against a wholesaler’s own book, or in a once-a-day auction — and the price you get, the information you leak and the counterparty you end up with are different in each. This page is the map. Read it before you write a router.
shared/orderbook.py
US equities settle T+1 since 28 May 2024
Quoted price ≠ net price
Market structure is the set of rules that decide who may trade with whom, at what price, and who finds out. Change one rule and the whole ecology moves: cap the access fee and volume migrates; publish the depth and large orders hide. Before the taxonomy, two questions worth more than the labels.
Either the price is discovered at the venue (many buyers and sellers meet in one book and their orders set it) or it is derived from somewhere else (a dealer marks off a reference, a dark pool matches at the lit midpoint). A venue that derives its prices is a free rider on the venue that discovers them — which is why lit exchanges care so much about how much volume goes dark.
exchange/price_engine.py makes the split explicit: the venue mark is a blend, weighted
MID_BLEND_WEIGHT = 0.85 toward the venue’s own depth-weighted microprice and
0.15 toward the shared fundamental. Discovery dominates; the common term is what keeps
two fragmented venues from drifting apart.On an exchange you never learn the name of your counterparty, because you do not need to: the trade is novated to a central counterparty (a CCP) that steps in as buyer to every seller and seller to every buyer. Bilateral OTC has no such thing — you have the credit of the dealer you faced, an ISDA master agreement, and a pile of collateral.
That single difference explains why an exchange can be anonymous and an OTC market cannot, why OTC has credit officers and exchanges have margin clerks, and why post-2008 regulation pushed standardised swaps onto clearing houses.
Pick a venue type. The map places it by pre-trade transparency (left = dark, right = fully lit depth) and by counterparty risk (bottom = CCP-cleared and anonymous, top = bilateral credit). The panel gives you the five facts you actually need about it.
Pre-trade: what the world can see before you trade.
Post-trade: how fast and how completely the print is published.
Three trading mechanisms cover almost everything: a continuous order-driven book, a quote-driven dealer, and a single-price call auction. Most real markets use all three, at different times of day, for the same instrument. Here they are, one at a time, with the same order flow poured through them.
Five client orders arrive. On the left they hit a CLOB and walk the ladder; on the right a dealer names one price for the whole clip and wears the inventory. Step through and watch which mechanism is cheaper — the answer flips with size.
Many participants post limits. Your market order consumes levels until it is done, so your average price is worse than the touch when you are large. The exchange is a matcher; it takes no risk and holds no inventory.
One dealer quotes a firm two-way price for your whole size. No slippage — one price, one fill — but the spread is wider, widens further with size, and skews as the dealer’s inventory builds. You are paying for immediacy and for the dealer’s risk.
inventory 0 · spread 4.0¢
An auction does not match orders one against one. It collects
them, then finds the single price that maximises crossed volume — and, among ties, the
one with the smallest imbalance. That is not a simplification: it is literally
auction_preview() in shared/orderbook.py, and it is how NYSE and Nasdaq open and
close every day. Add orders and watch the indicative price move.
| Candidate px | Demand ≥ p | Supply ≤ p | Volume | Imbalance | Pick |
|---|
A US equity session is not one regime. Move the clock: the matching rules, the spread, the depth and the share of the day’s volume all change, and a strategy tuned at 11:00 will be badly wrong at 09:31 and at 15:59.
One stock, a dozen places to trade it. That is not a bug — competition narrowed spreads and cut fees — but it moved a hard problem onto you: choosing where to send the order, and proving afterwards that you chose well. Then, once you have traded, something has to actually move the shares and the cash. Both halves are graded in this course.
Five venues quote the same name. The best quoted ask is not the best net ask once access fees and rebates land, and the midpoint venue is cheapest of all when it fills — which it does not always do. Choose a routing policy and a size, then read the net price.
Six firms traded 14 times among themselves this morning. Nobody is going to wire fourteen separate payments. Step through what a clearing house does to that graph: novate every trade to itself, then net. The obligation that actually settles the next morning is a small fraction of the notional that traded.
You do not need to be a lawyer. You do need to know which rule is about to reject your order, halt your symbol, or force your router’s hand. Filter by jurisdiction.
| Rule | Who | What it does | The number |
|---|
exchange/circuit_breaker.py three of them are
yours to enforce: a velocity halt at VELOCITY_HALT_PCT = 5% in 60 seconds, a session band at
SESSION_HALT_PCT = 25%, and market-wide breakers at -7% and -13%.
An LULD_BAND_PCT = 10% collar rejects the fat-finger print before it ever happens.All five have shown up in student submissions on a live arena session. Open them.
Two venues show the same $100.03 ask. One charges 30 mils per share to take, one pays you 4 mils. Your router picks by price, so it picks whichever came back first in the snapshot, and half the time it leaves 34 mils per share on the table — about 3.4 bps on a $100 stock, which is larger than most intraday alpha.
The arena makes this bigger on purpose. exchange/config.py ships
TAKER_FEE_RATE = 0.0015 and MAKER_REBATE_RATE = 0.0010 — 15 and 10 bps of
notional, roughly fifty times a real US access fee — so that in a 45-minute session fees are visible
in the standings rather than a rounding error.
Fix: rank on net price — quote + taker_fee when taking,
quote − maker_rebate when posting — and log both so your TCA report can show the gap you
avoided. Never compare a lit quote to a midpoint fill without adjusting both for fees.
The midpoint is half a spread better than the touch, so a naive router sends everything dark first. Then two things happen. The fill rate is not 100% — you sit unfilled while the price walks away, and that opportunity cost does not appear in your average-fill statistics because unfilled orders have no fill price. And the flow you do get filled against is selected: the counterparty who wanted the other side of your order at the midpoint often knew something.
Fix: measure fill rate and post-fill mark-out together, not average price alone. A venue
with a great average price and a 20% fill rate is a venue you are being adversely selected on. Report
fills / attempts and the 1-second and 10-second mark-out next to every venue’s average
price.
The closing auction is roughly a tenth of a US name’s consolidated volume on an ordinary day and much more on an index rebalance. A bot that keeps working a limit order through 15:59 is competing against every index fund in the world at 16:00:00 and does not know it. The mirror image happens at 09:30, when the first minutes carry a large slice of the day at several times the midday spread.
In the arena this is not hypothetical: with CLOSING_AUCTION=true the first close
request freezes continuous matching for CLOSING_AUCTION_TICKS and the official close is
one volume-maximising cross. A market order sitting in your queue when that happens does not do what
you think it does.
Fix: make the session phase an input to your strategy. Read the phase off the event stream, size differently in the first and last five minutes, and if you need the closing price, submit into the closing auction rather than trying to trade near it.
“The exchange guarantees my trade” is false, and the distinction bites when a counterparty fails. The exchange matched you; a CCP took the credit risk by novation, and it did that in exchange for initial and variation margin that it will call from your clearing member — possibly intraday, possibly at a very inconvenient moment.
The practical consequence for a systematic shop is that your funding and your position are two different constraints. You can be right about the price and still be closed out because a margin call landed before the P&L did.
Fix: model financing explicitly. The arena does —
MARGIN_HAIRCUT = 0.5 on long inventory, MARGIN_RATE_PER_TICK on borrowed
cash and BORROW_FEE_PER_TICK on shorts — so a strategy that ignores funding loses to one
that does not, on the same signal.
Week one you hardcode EXCHANGE_URL. Then a scenario turns on
multi_venue, a second exchange team opens with a different fee schedule, and your bot is
quoting into the venue with the worse rebate because that is the only URL it knows. Every price it
computes is now conditioned on a partial view of the market.
Fix: from the start, keep a list of venues, a per-venue book, and a consolidated top-of-book that your signal reads. One extra layer of indirection on day one; a rewrite mid-season if you skip it. This is exactly the boundary a smart order router lives at.
Three questions, instant feedback, nothing recorded.
Q1A call auction has collected its book. Which price does the engine choose?
Q2You must buy 40,000 shares of a name whose lit book shows 400 shares at the touch, right now, in one go. Which mechanism gives the better average price?
Q3Your exchange fill is anonymous — you never learn who sold to you. What makes that safe?
auction_preview().0.0015 / 0.0010 fee schedule, and sanity-check the sign.make install.