Market Making and High-Frequency Trading · Market making
10The Quoting Engine
A market maker changes its mind about its price tens of thousands of times a day in a busy instrument. Every change is a cancel and a new order, or a modification; each can cross a fill already on its way, and the venue counts them all. The component that turns “where I want to be” into the fewest messages that get there is the quoting engine. In this chapter’s simulated market, a one-level quoter sends 758 messages an hour for 250 fills. A three-level ladder sends 980 and earns more, because its orders are already in the queue when the price moves. A hysteresis that looks like thrift, keeping an order one tick from its target, turns the mark-out negative.
10.1 From target quotes to orders
The policies of chapters 3 to 9 produce targets: on each side, the prices and sizes the market maker wants to show. The engine owns the orders: which are resting, which are on their way, which the venue has not yet acknowledged, which it has been asked to cancel. It compares the target with the orders and sends the difference. Its rules (Listing 10.1):
Method 10.1 (Diffing a target against the working orders)
On each side, visit the working orders in price priority. An order at a target price covers it: if it holds more than the target, amend it down; if less, add a new order for the difference, never increase it. An order at no target price is cancelled, unless it is within a hysteresis of a target it can cover. Every target left uncovered gets a new order. Leave orders awaiting acknowledgement alone. Send cancels first, then amendments, then new orders.
The order matters. Cancels go first because a cancel protects capital and a new order commits it; when messages must be rationed, the ones that remove risk are sent. Orders in flight are left alone because the engine does not yet know whether they are in the book; acting on them twice is how engines produce duplicate orders.
Definition 10.2 (Quote ladder)
A quote ladder is a set of orders resting at several consecutive prices on one side, each intended to trade: the best level earns the spread now, and the levels behind it hold places in queues that become the best when the price moves.
In a large-tick book (chapter 5) a ladder’s deeper levels rarely fill where they are; their value is the place they hold. When the price ticks up, a one-level quoter cancels its bid and joins the back of the new best queue; a ladder’s second level is already in that queue.
10.2 Cancel-replace, priority and in-flight states
Definition 10.3 (Cancel-replace)
A cancel-replace changes a resting order’s price or size in one message, the venue cancelling the old order and entering a new one; under the usual venue rule the order keeps its place in the queue only if its size is reduced at the same price, and goes to the back for any other change.
One Quant Book 10, chapter 26 specifies the simulator’s replace message with exactly this rule: a size decrease at the same price is published as a partial cancel and keeps priority; anything else loses it. That is why the engine amends only downwards and adds a separate order to increase size.
An order has a life the engine must follow (Figure 10.1): sent, then acknowledged; asked to change or cancel, then acknowledged again. A fill can arrive at any point, including after the engine has decided to cancel the order: the cancel is still travelling, or reached the venue just after the fill, which answers “too late to cancel”. This cancel–fill race is not an error; it is a certainty at high message rates, and the engine counts it. In the chapter’s runs a one-level quoter meets 19.5 such races an hour.
firm.quoteengine.10.3 Throttles, message limits and order-to-trade ratios
Definition 10.4 (Message throttle)
A message throttle is a limit on the rate of messages a trading system sends, imposed by the venue on each session or by the firm on itself, and the policy for what is sent when the limit binds: which messages go first, which are deferred and which are dropped.
The engine’s throttle is a token bucket (One Quant Book 13, chapter 21 builds one for the gateway): tokens refill at the permitted rate up to a burst depth, each message takes one, and when none is left the remaining actions are dropped. Dropping, not queueing, is the right policy for quotes: the next update recomputes the difference from the latest target, so a stale action is never sent late.
Venues limit messages in three ways: a hard rate per session (a throttle rejects the excess), a charge on messages beyond a ratio to trades, and the regulatory order-to-trade ratio of One Quant Book 10, chapter 24.
As of September 2026 — Message limits in rules and fee schedules
Under Commission Delegated Regulation (EU) 2017/566, each trading venue computes, for each member and instrument at least at the end of every session, the ratio of unexecuted orders to transactions in volume terms and in number terms (orders over transactions, minus one), counting each order type as its annex prescribes, and a member exceeding the venue’s maximum ratio in either measure has exceeded it. CME Group’s messaging efficiency programme, as filed with the CFTC in 2019, weights messages by type into a messaging score, divides it by the firm’s traded volume in a product group during regular trading hours, and compares the resulting volume ratio with product-group benchmarks set each quarter.
10.4 Self-trade prevention and multiple strategies
A firm running several strategies in one instrument can find its own bid crossing its own offer: a quoting strategy resting at the ask and a momentum strategy buying. The trade is real on the venue’s books but moves no risk, pays two fees, and prints a wash trade that surveillance will see (One Quant Book 9, chapter 29). Venues offer self-trade prevention (One Quant Book 10, chapter 26): orders tagged with the same group are never matched against each other, and the venue cancels the newer, the older or both, as the firm chose. The engine’s side of the problem is to know every strategy’s resting orders and to route internal crossings to the netting logic of chapter 11 before they reach the venue.
10.5 Quote ladders and the line not to cross
A ladder of orders on one side, cancelled and moved as the market moves, looks from the outside like the layering of One Quant Book 9, chapter 29: orders at several prices that rarely trade. The difference is intent. The statute under which the Seventh Circuit upheld Coscia’s conviction in 2017 defines spoofing as “bidding or offering with the intent to cancel the bid or offer before execution”. The court rejected his argument that the definition was vague because high-frequency traders cancel 98% of their orders. Cancelling most orders is normal; placing them never meaning to let them trade is not. A market maker’s ladder is defensible when each level is priced and sized to trade, fills are taken when they come, both sides are quoted, and the cancellation logic is the policy’s, documented. The engine is where that evidence is produced: it logs, for every order, the target it served.
10.6 Tutorial: one engine, four configurations
Goal. Drive the engine from a quoting policy on the simulated market and measure messages, fills, races, queue places and edge. End state: the table below.
The diff (Method 10.1), in
firm.quoteengine.QuoteEngine.update; its C++20 and Rust twins produce the same 1 154 actions on a 3 000-event fixture.def update(self, t: float, side: int, targets) -> list: targets = [(int(p), int(q)) for p, q in targets if q > 0] covered = [False] * len(targets) cancels, amends, news = [], [], [] mine = sorted((o for o in self.orders.values() if o.side == side), key=lambda o: (-side * o.price, o.oid)) for o in mine: if o.state != "live": if o.state != "pending_cancel": # in flight: it still covers its price for k, (p, _) in enumerate(targets): if not covered[k] and p == o.price: covered[k] = True break continue k = next((k for k, (p, _) in enumerate(targets) if not covered[k] and p == o.price), None) if k is not None: covered[k] = True q = targets[k][1] if o.leaves - q >= self.min_size: amends.append(("amend", o.oid, q)) elif q - o.leaves >= self.min_size: news.append((side, o.price, q - o.leaves)) continue near = [k for k, (p, _) in enumerate(targets) if not covered[k] and abs(p - o.price) < self.min_move] if near: k = min(near, key=lambda k: (abs(targets[k][0] - o.price), k)) covered[k] = True continue cancels.append(("cancel", o.oid)) for k, (p, q) in enumerate(targets): if not covered[k]: news.append((side, p, q))Listing 10.1. Cover targets with working orders, amend down, add, cancel, or keep within the hysteresis. code/firm/quoteengine/firm_quoteengine.py Issuing under a budget: cancels first, then amendments, then new orders, one token each.
out = [] for a in cancels + amends: if not self.bucket.take(t): self.stats["dropped"] += 1 continue o = self.orders[a[1]] if a[0] == "cancel": o.state = "pending_cancel" self.stats["cancel"] += 1 else: o.state, o.leaves = "pending_amend", a[2] self.stats["amend"] += 1 out.append(a) for sd, p, q in news: if not self.bucket.take(t): self.stats["dropped"] += 1 continue self.next_oid += 1 self.orders[self.next_oid] = Order(self.next_oid, sd, p, q, "pending_new") self.stats["new"] += 1 out.append(("new", self.next_oid, sd, p, q)) return outListing 10.2. The token bucket decides what is sent; the rest is re-derived at the next update. code/firm/quoteengine/firm_quoteengine.py - The quoter.
hf_engine.EngineQuotertargets the others’ best prices (and the prices behind them for a ladder), one lot per level, and translates the engine’s actions into orders; onfirm.tape, which has no modify message, an amendment becomes a cancel and a new order. - Compare four configurations on four twenty-minute sessions with
compare().
What to change next. Run the same quoter on firm.exchsim, whose replace message keeps priority on a size decrease; add a second strategy in the same instrument and count the self-trades prevented.
| configuration | messages | fills | races | 10-s mark-out | edge | median age |
| an hour | an hour | an hour | (ticks a share) | ($ an hour) | of a fill (s) | |
| one level | 758 | 250 | 19.5 | 0.141 | 35.25 | 9.6 |
| three levels | 980 | 282 | 20.3 | 0.144 | 40.50 | 17.3 |
| three levels, hysteresis of two ticks | 806 | 364 | 1.5 | 9.6 | ||
| three levels, 2 messages a second | 884 | 282 | 9.8 | 0.154 | 43.50 | 17.7 |
Four lessons (Figure 10.2). The ladder sends 29% more messages and earns 15% more edge an hour, because its fills come from orders that waited in the queue (a median of 17 seconds against 10). The hysteresis saves messages but keeps orders a tick away from where the policy wants them, including a bid one tick above a best bid that has fallen: that order is alone at a stale price and gets picked off, and the mark-out turns negative; hysteresis in a one-tick book must only ever keep orders behind the target, never in front. The throttle of two messages a second drops 1 396 derived actions an hour and loses nothing: the actions dropped were re-derived and sent when a token was free, and the budget cut the races by half. The messages per fill, 3.0 to 3.5, are what a venue’s order-to-trade ratio would count.
hf_engine.compare.10.7 Build: the quoting engine
Purpose. Turn target quotes into the fewest messages under a venue’s priority rules and a message budget, and track every order’s state.
Interface. QuoteEngine(min_move, min_size, rate, burst): update(t, side, targets) -> actions ((’new’, oid, side, price, qty), (’amend’, oid, qty), (’cancel’, oid)); ack(oid), fill(oid, qty), orders, stats (new, amend, cancel, dropped, fills, races); TokenBucket(rate, burst).take(t). C++20: cpp/firm_quoteengine.hpp; Rust: rust/.
Rules. Amend only downwards; orders in flight are untouched; cancels before amendments before new orders; an empty bucket drops actions, never queues them; a fill during a pending cancel is counted as a race.
Acceptance tests. code/firm/quoteengine/tests/: no message when the book already matches the target; a price move cancels one order and adds one; a size decrease amends, an increase adds; the hysteresis keeps an order within one tick; the throttle drops and the next update re-derives; a cancel–fill race is counted; the Python, C++20 and Rust engines produce the same action log on the fixture.
Stretch. Replace messages with the venue’s priority rule on firm.exchsim; per-strategy self-trade groups; an order-to-trade budget per instrument and session.
Sources and further reading
- Commission Delegated Regulation (EU) 2017/566 on the ratio of unexecuted orders to transactions.
- CME Group, CME Globex Messaging Efficiency Program, rule filing with the CFTC, November 2019.
- United States v. Coscia, 866 F.3d 782 (7th Cir. 2017).
10.8 Exercises
Exercise 10.1 ★
The engine rests 300 shares at 100 and the target becomes 100 shares at 100. What does it send, and does the order keep its place?
Solution
Solution of Exercise 10.1.
One amendment of the order down to 100 shares. A size decrease at the same price keeps its place in the queue.
Exercise 10.2 ★
A member sent 12 000 orders and made 400 transactions in an instrument in a session. What is its ratio of unexecuted orders to transactions in number terms?
Solution
Solution of Exercise 10.2.
.
Exercise 10.3 ★
A token bucket refills at 2 tokens a second with a depth of 5. It is full at and five actions are sent at once. How many can be sent at ?
Solution
Solution of Exercise 10.3.
The bucket is empty after the five actions and refills tokens: two actions.
Exercise 10.4 ★★
Why should a throttled engine drop actions rather than queue them?
Solution
Solution of Exercise 10.4.
A queued action was derived from a target that is already old; sent late, it may cancel an order the new target wants or add one it does not. The next update recomputes the difference between the current target and the orders, so everything still needed is sent again as soon as a token is free.
Exercise 10.5 ★★
Why did the two-tick hysteresis turn the mark-out negative, and how would you fix it without losing its message savings?
Solution
Solution of Exercise 10.5.
The hysteresis keeps an order within one tick of a target in either direction. When the best bid falls by a tick, the old bid is kept one tick above the new best: it is now the best bid alone, at a price the market has left, and the next seller takes it. Apply the hysteresis only to orders behind the target (a bid below it, an ask above it) and cancel at once any order in front of it; the saving comes mostly from orders behind.
Exercise 10.6 ★★
What makes a quote ladder defensible against an accusation of layering?
Solution
Solution of Exercise 10.6.
Evidence that every level was meant to trade: prices and sizes set by the quoting policy, both sides quoted, fills accepted when they came, no pattern of large orders on one side cancelled as the other side trades, and a log linking every order to the target that produced it.
Exercise 10.7 ★★★
Coding. Replay the component’s fixture through an engine with rate=5 and burst=2 (hf_engine.fixture_stats), report the counts, and explain why they say nothing about what a tighter throttle would do.
Solution
Solution of Exercise 10.7.
71 new orders, 1 cancel, no amendment, 195 dropped actions, 1 fill. The fixture’s acknowledgements and fills name the order ids of the engine that produced it, with the fixture’s parameters; an engine that issues different actions numbers its orders differently, so almost none of its orders is ever acknowledged, the engine leaves them alone as in flight, and it stops quoting. A fixture of this kind tests that twin engines agree; to measure a throttle, run it in closed loop with a market, as compare() does.
Exercise 10.8 ★★★
Find the flaw. “To save messages we increase an order’s size with a modify instead of sending a second order.”
Solution
Solution of Exercise 10.8.
On most venues an increase in size loses the order’s queue priority, so the “saving” sends the whole order to the back of the queue. A second order at the same price keeps the first one’s place.
10.9 Problem: Ten Thousand Changes of Mind
Problem 10.1
Weekend problem — ten thousand changes of mind
A market maker’s engine must keep a three-level ladder current in a busy one-tick stock without exceeding its message budget or its venue’s order-to-trade ratio.
Part I — The engine.
- State the diff of Method 10.1 and justify the order in which actions are sent.
- Define a quote ladder and say what its deeper levels are for in a one-tick book.
- Define a cancel-replace and the priority rule, and explain why the engine never amends upwards.
- What is a cancel–fill race, and how often did it happen here?
Part II — Limits.
- Define a message throttle and describe the token bucket.
- Summarise the two message limits of the dated box.
- What is self-trade prevention and why does a multi-strategy firm need it?
- Where is the line between a ladder and layering, and what did the Coscia court say about cancellation rates?
Part III — Measurements.
- Give messages, fills and edge for the one-level and three-level quoters.
- Why do the ladder’s fills come from older orders?
- What did the two-tick hysteresis do, and why?
- What did the throttle drop, and what did it cost?
Part IV — The verdict.
- State the named result: messages an hour and the median age of a fill under the one-level, three-level and throttled configurations, against the edge each earns.
- Which configuration would you run, and with what change to the hysteresis?
- What would the messages-per-fill figures mean for a venue’s order-to-trade ratio?
- How would results change on a venue whose replace keeps priority on size decreases?
- Why must the Python, C++ and Rust engines agree action for action?
- What does the engine log to defend the firm’s quoting?
- How would a second strategy in the same stock change the engine?
- In one sentence: what does a quoting engine optimise?
Solution
Solution of Problem 10.1.
- See Method 10.1; cancels first because they remove risk and the budget may not reach the rest, orders in flight untouched because their state is unknown.
- See Definition 10.2; the deeper levels hold places in queues that become the best when the price moves.
- See Definition 10.3; an increase would lose priority, so size is added by a second order.
- A fill arriving while a cancel is in flight; 19.5 an hour for one level, 20.3 for three levels.
- See Definition 10.4; tokens refill at the rate up to the burst depth and each message takes one.
- The EU venue computes each member’s ratio of unexecuted orders to transactions in volume and number terms at least at the end of each session; CME’s programme weights messages by type and divides by traded volume, against quarterly benchmarks.
- The venue does not match two orders of the same firm or group; a firm with several strategies in one instrument would otherwise trade with itself, paying fees and printing wash trades.
- Intent to cancel before execution; the court held that cancelling 98% of orders, as high-frequency traders do, does not make the statute vague.
- One level: 758 messages, 250 fills, $35.25 an hour; three levels: 980, 282, $40.50.
- The deeper levels waited in the queue and are at the front when the price moves: a median age of 17 seconds at the fill against 10.
- It saved messages but kept orders in front of a falling market: 364 fills an hour, mark-out ticks, an hour.
- 1 396 derived actions an hour; nothing: the edge was $43.50 an hour and the races fell to 9.8.
- 758, 980 and 884 messages an hour; median fill ages 9.6, 17.3 and 17.7 seconds; edge $35.25, $40.50 and $43.50 an hour.
- The throttled ladder, with the hysteresis restricted to orders behind the target.
- Messages per fill of 3.0 to 3.5 (2.2 with the hysteresis); if every message counted as an order and every fill as a transaction, the number-terms ratio of the dated box would be about 2.0 to 2.5, a number to compare with the venue’s maximum.
- Amendments would keep priority instead of costing a cancel and a new order: fewer messages and older fills.
- The simulator, the backtest and production must make the same decisions; a fixture replayed in all three proves it.
- For every order, the target and policy state that produced it, its states and times, and the fills.
- It must share the order state across strategies, net internal crossings and tag orders for self-trade prevention.
- The fewest messages that keep the orders where the policy wants them, without losing queue places it has earned.
10.10 Interview questions
Interview question 10.1 ★ developer
Your engine sends a cancel and immediately receives a fill for the same order. What states must it handle, and what does it tell the strategy?
Solution
Solution of Interview question 10.1.
Pending cancel then fill: reduce the leaves, count a race, and treat the cancel acknowledgement (or a “too late” reject) as closing the order. Tell the strategy the fill at once; its position has changed whatever the cancel does.
What the interviewer is looking for: the in-flight state machine and position first.
Interview question 10.2 ★★ developer
Design the data structure for working orders so that the diff against a ten-level ladder runs in constant time per level.
Solution
Solution of Interview question 10.2.
An array indexed by price offset from a reference price per side, each slot holding the order at that price; the ladder’s targets map to slots directly, so each level is one lookup; recentre the array when the price drifts far.
What the interviewer is looking for: price-indexed arrays, not trees.
Interview question 10.3 ★★ trader
Your venue caps you at 50 messages a second and the market is moving fast. Which messages do you send first?
Solution
Solution of Interview question 10.3.
Cancels of orders the market has moved through first, then cancels of other stale orders, then amendments down, then new orders at the best, then deeper levels; drop rather than queue.
What the interviewer is looking for: risk-reducing messages first.
Interview question 10.4 ★★ risk
A regulator asks why your firm cancels 97% of its orders. What do you show them?
Solution
Solution of Interview question 10.4.
The quoting policy and its parameters, the log linking each order to the target that produced it, fill rates by level, both-sided quoting, and the absence of patterns of one-sided orders cancelled as the other side trades.
What the interviewer is looking for: intent evidenced by logs.
Interview question 10.5 ★★ researcher
How would you measure the value of a place in the queue that your ladder preserves when the price moves?
Solution
Solution of Interview question 10.5.
Compare the mark-out and fill probability of orders that were already queued when the price moved with orders placed after it, at the same level; or value the queue place with the queue-reactive model of chapter 5.
What the interviewer is looking for: a controlled comparison or a model.
Interview question 10.6 ★★★ developer
Prove that a token bucket of rate and depth never lets more than messages through in any window of length .
Solution
Solution of Interview question 10.6.
Let be the tokens at time , at most . Over the tokens used are at most the tokens held at plus those refilled, , and each message uses one.
What the interviewer is looking for: a conservation argument.