Auctions¶
Objective¶
Run opening and closing auctions, understand equilibrium price calculation, and observe how auction orders are collected and matched in a single uncrossing event.
Background reading
Prerequisites¶
- Chapters 01–06 completed.
pm-engine, both trader consoles, the market-maker gateway and the operator console running, and nopm-scheduler(you drive the phases by hand).
Background¶
Auctions concentrate liquidity at a single price. All orders are collected during the auction phase, then matched at the equilibrium price — the price that maximises the quantity traded.
Key properties:
- No partial information leakage (indicative prices may be shown).
- All fills happen at the same price.
- Prevents manipulation via timing advantages.
Exercise 1: Set Up for an Opening Auction¶
Stop the scheduler and use manual phase control instead (see
06 — Freeze/Advance Procedure).
You want to be sitting in OPENING_AUCTION — not PRE_OPEN — before
sending the TIF=ATO orders below: the engine only accepts ATO orders
while the session state is exactly OPENING_AUCTION and rejects them during
PRE_OPEN even though PRE_OPEN accepts orders generally. PRE_OPEN can only
be entered from CLOSED, so first check where you are:
Chapter 06 left the market in CONTINUOUS. From there, close the day first —
SESSION|STATE=CLOSING_AUCTION, then SESSION|STATE=CLOSED (CLOSED alone is
also accepted from CONTINUOUS; a request for
PRE_OPEN from CONTINUOUS is ignored and the console only prints
TIMEOUT). From CLOSED go on:
Before the auction starts, empty the AAPL book. The market-maker quotes from
chapter 02 would otherwise take part in the auction too, and the calculation
in Exercise 2 assumes only the four orders below. Stop any pm-mm-bot and AI
traders, then cancel everything resting on AAPL — orders and quotes — and
check the book is empty:
[GW_ADMIN|ADMIN]> CANCEL_SYM|SYM=AAPL
[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL
[GW_ADMIN|ADMIN]> SESSION|STATE=OPENING_AUCTION
From two gateways, enter orders while in OPENING_AUCTION:
TRADER01:
[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=300|PRICE=150.50|TIF=ATO
[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=200|PRICE=150.20|TIF=ATO
TRADER02:
[TRADER02]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=400|PRICE=149.80|TIF=ATO
[TRADER02]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=100|PRICE=150.30|TIF=ATO
Add one order that cannot cross, for Exercise 3 — a bid for MSFT far below any offer (the MSFT market maker, when it is quoting, asks about 420):
Checkpoint: orders accepted, resting in auction book.
Exercise 2: Trigger the Auction Uncrossing¶
When the session leaves OPENING_AUCTION for CONTINUOUS, all crossing orders match at the equilibrium price. Nothing has traded yet — the orders from Exercise 1 are collected, not matched. Work out the result by hand first, then fire the uncrossing:
For the exact orders entered in Exercise 1, here is the full calculation
(verified against engine/auction.py):
| Candidate price | Cumulative buy qty (bids ≥ price) | Cumulative sell qty (asks ≤ price) | Matched qty | Surplus |
|---|---|---|---|---|
| 149.80 | 500 (300+200) | 400 | 400 | 100 |
| 150.20 | 500 | 400 | 400 | 100 |
| 150.30 | 300 | 500 | 300 | 200 |
| 150.50 | 300 | 500 | 300 | 200 |
Maximum matched quantity is 400, tied between 149.80 and 150.20 with equal surplus (100). The engine scans candidate prices from lowest to highest and only replaces the current best on a strict improvement, so the first price reached with the best (qty, surplus) pair wins. That means the equilibrium price here is 149.80 — the lower of the two tied prices — not a value in between.
Expected: all fills print execution price 149.80, for a total matched
quantity of 400 shares. TRADER01's second order fills 100 of its 200 shares
and reports [PARTIAL]; its unfilled 100, and TRADER02's sell at 150.30, then
expire, because an ATO order only lives for the opening auction.
What to observe: identify the one common execution price printed across all
fill events, and confirm it equals 149.80 and the total filled quantity
equals 400 (300 from TRADER01's first order + 100 from the
second, against TRADER02's 400-share sell). On TRADER01 you see:
FILL <first order id> qty=300 @149.8 remaining=0 [FILLED]
FILL <second order id> qty=100 @149.8 remaining=100 [PARTIAL]
EXPIRED <second order id> (DAY order — trading day ended)
Checkpoint: all fills report execution price 149.80 and matched quantity 400.
Exercise 3: Unfilled Auction Orders¶
An ATO order that does not cross expires when the auction ends, instead of
carrying on into continuous trading. You entered one in Exercise 1: the MSFT
bid at 400.00. The uncrossing in Exercise 2 left it unfilled — no MSFT seller
was anywhere near 400 (the day's market-maker quotes were gone after chapter
06's close, and a quote at about 420 would be far above 400 anyway) — and the
console reported it EXPIRED in the same burst as the AAPL fills. Confirm it:
The MSFT order is listed with status EXPIRED. (The console's explanation
reads Opening auction ended, identifying why the ATO remainder expired.
Checkpoint: the unmatched ATO order expired at the start of CONTINUOUS.
Exercise 4: Closing Auction¶
Move the session to the closing auction:
Then enter:
[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.80|TIF=ATC
[TRADER02]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=100|PRICE=150.60|TIF=ATC
Close the day; on the transition to CLOSED the auction uncrosses and fills are generated:
Both orders fill in full at one price, 150.60: the candidates 150.60 and
150.80 both match 100 shares with no surplus, and the engine keeps the lower of
two tied prices, exactly as in Exercise 2. Each console prints
FILL … qty=100 @150.6 remaining=0 [FILLED].
Remember to reopen the market (PRE_OPEN, then CONTINUOUS) before the next
chapter.
Checkpoint: closing auction produces fills.
Exercise 5: GTC Orders in Auctions¶
GTC orders participate in auctions alongside ATO/ATC orders. Prove it for the
opening auction. After Exercise 4 the market is CLOSED; move to PRE_OPEN
(which accepts GTC but not ATO), clear AAPL with CANCEL_SYM|SYM=AAPL, and
rest a GTC bid:
[GW_ADMIN|ADMIN]> SESSION|STATE=PRE_OPEN
[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=50|PRICE=150.00|TIF=GTC
Move to OPENING_AUCTION, offer 50 shares at the same price as an ATO order,
and uncross:
[GW_ADMIN|ADMIN]> SESSION|STATE=OPENING_AUCTION
[TRADER02]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=50|PRICE=150.00|TIF=ATO
[GW_ADMIN|ADMIN]> SESSION|STATE=CONTINUOUS
Both consoles report FILL … qty=50 @150.0 [FILLED]: the GTC bid, placed
before the auction began, was matched in it. The same holds in the closing
auction — rest a GTC bid during CONTINUOUS, send an ATC sell at the same
price after SESSION|STATE=CLOSING_AUCTION, and they trade when the session
moves to CLOSED.
Checkpoint: the GTC bid filled in the opening auction against an ATO order.
Equilibrium Price Rules¶
The auction algorithm scans candidate prices from lowest to highest and selects the price that:
- Maximises traded volume — most shares can match.
- Minimises surplus — least shares left unexecuted at that price.
- Ties resolved by scan order — if multiple prices tie on both volume and surplus, the engine keeps the first (lowest) one it found, since it only replaces the current best on a strict improvement. There is no separate "nearest to last trade" tie-break — the tie always resolves to the lower of the tied candidate prices.
How real exchanges break the tie
Most real exchanges use more steps after rules 1 and 2. If the surplus is on the buy side at every tied price, they pick the highest tied price (buyers are pressing, so the price moves up); if it is on the sell side, the lowest; and only if there is no clear pressure do they fall back to the price closest to a reference price, such as the last trade. In this chapter's example the surplus is on the buy side, so a typical exchange would uncross at 150.20, not 149.80. EduMatcher keeps the simpler rule so the result is easy to compute by hand — a good discussion point for the Reflection below.
Reflection¶
Why does an auction match all crossing orders at a single equilibrium price, instead of walking the book and filling each order at its own limit price (as CONTINUOUS trading does)? What problem would arise at the open if every order filled at its own price instead?