Match Limit Orders with Price-Time Priority
Two SigmaSoftware Engineer · Technical ScreenLast updated
Implement a limit order matching engine adhering to price-time priority (FIFO), resting-order execution pricing, and partial fill mechanics.
Problem Statement
Design and implement an in-memory limit order matching engine. The engine receives a sequence of limit orders and matches them based on standard price-time priority.
Each order has:
order_id: A unique string identifier.side: Either"BUY"or"SELL".price: An integer representing the limit price.quantity: A positive integer representing the number of units to trade.
Matching Rules
- Price-Time Priority:
- Buy orders (Bids): Prioritize highest price first. For matching prices, prioritize earlier arrival time (FIFO).
- Sell orders (Asks): Prioritize lowest price first. For matching prices, prioritize earlier arrival time (FIFO).
- Crossing Conditions:
- An incoming
BUYorder matches with restingSELLorders ifbuy_price >= sell_price. - An incoming
SELLorder matches with restingBUYorders ifsell_price <= buy_price.
- An incoming
- Execution Price:
- Trades always execute at the price of the resting order (the passive maker order already in the book), never the incoming taker order's price.
- Partial Fills & Residuals:
- An order matches as much quantity as possible against eligible resting orders.
- If the resting order is partially filled, it remains at the top of its price level with its remaining quantity and its original arrival priority.
- Any unmatched remaining quantity of the incoming order rests in the book.
Interface
Implement the following function:
def match_orders(orders: list[tuple[str, str, int, int]]) -> tuple[
list[tuple[str, str, int, int]],
list[tuple[str, int, int]],
list[tuple[str, int, int]]
]:
"""
Processes a list of orders in arrival order.
Args:
orders: List of (order_id, side, price, quantity) where side is 'BUY' or 'SELL'.
Returns:
A tuple of (trades, remaining_bids, remaining_asks):
- trades: List of (maker_order_id, taker_order_id, trade_price, trade_quantity)
in chronological order of execution.
- remaining_bids: List of (order_id, price, quantity) sorted by best bid first
(highest price, then earliest arrival).
- remaining_asks: List of (order_id, price, quantity) sorted by best ask first
(lowest price, then earliest arrival).
"""
pass
Constraints
1 <= len(orders) <= 10^51 <= price <= 10^91 <= quantity <= 10^9order_idconsists of alphanumeric characters and is unique per order.- Total execution time should scale efficiently (logarithmic insertion and matching per order level).
Example
orders = [
("B1", "BUY", 100, 10),
("B2", "BUY", 102, 5),
("S1", "SELL", 103, 10),
("S2", "SELL", 101, 8)
]
trades, bids, asks = match_orders(orders)
Step-by-step Trace:
B1rests: Bids:[B1: (100, 10)]B2rests: Bids:[B2: (102, 5), B1: (100, 10)]S1arrives (Ask 103): Does not cross (103 > 102). Asks:[S1: (103, 10)]S2arrives (Ask 101, qty 8): Crosses withB2(`101 100, so no further match.- Remaining
S2quantity (3) rests in book.
- Remaining
Output:
trades = [("B2", "S2", 102, 5)]
remaining_bids = [("B1", 100, 10)]
remaining_asks = [("S2", 101, 3), ("S1", 103, 10)]
Hints
Hint 1
Use two heaps or balanced tree structures: a max-heap for bids (keyed on -price and arrival sequence) and a min-heap for asks (keyed on price and arrival sequence).
Hint 2
Remember that the execution price is determined by the resting maker order, not the incoming taker order.
Hint 3
When a resting order is partially filled, update its remaining quantity in-place rather than generating a new arrival timestamp.
Follow-up questions
- How would you support order cancellations (e.g., `CANCEL `) with O(1) or O(log N) runtime?
- If millions of orders arrive per second, how would you design the data structures to minimize heap allocations and avoid pointer-chasing overhead?
- How would you extend this engine to support market orders, Stop-Loss triggers, and Immediate-or-Cancel (IOC) order types?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.