Skip to content
Trading

Building a limit-order execution engine that respects the order book

execution engine — man's eye view of mansion

Backtesting a high-frequency or market-making strategy using simple daily or even bar-by-bar data is a fast track to losing capital. In my early days building market-making models for liquid crypto pairs, my backtests showed an eye-watering Sharpe ratio of 4.2. When we went live, the strategy lost 6% of its allocation in forty-eight hours.

The cause was simple: naive limit-order fill assumptions.

Most retail backtesters make the fatal assumption that if the market price touches your limit order price, you are filled. In reality, you sit at the very back of the queue at that price level. If the price touches your level and immediately bounces, you will not get filled. Even worse, when you do get filled, it is often because aggressive market orders swept the book completely through your level. This is classic adverse selection: you only get filled when the price is highly likely to keep moving against you.

To solve this, I built a high-fidelity limit-order execution engine that runs locally, processes Level 2 (L2) incremental updates, tracks our exact queue position at every price level, and models fills based on actual market trades and order book cancellations.

The naive simulation trap and why it fails

When you place a limit order in a real matching engine, your order is placed at the end of a queue for that specific price level (assuming a standard Pro-Rata or Price-Time priority matching algorithm, the latter being the standard for almost all crypto and equity exchanges).

flowchart TD
 MD["L2 MD Feed"] --> EE["Execution Engine"]
 SO["Strategy Orders"] --> EE
 EE --> QPT["Queue Tracker"]
 QPT --> FE["Fill Engine"]
 FE --> SM["State Manager"]

If you place a buy limit order for 1.0 BTC at \$60,000, and there are already 4.5 BTC sitting at that price level, your queue position is 4.5 BTC.
For your order to fill:
1. Trade executions must occur at \$60,000 for a cumulative volume of at least 4.5 BTC.
2. Other market participants ahead of you in the queue must cancel their orders, moving you closer to the front.

If the price hits \$60,000, executes 1.2 BTC of volume, and then climbs back to \$60,010, a naive backtester reports a 100% fill. A queue-aware engine correctly reports a 0% fill, with your order having moved 1.2 BTC closer to the front of the queue.

Designing the queue-tracking simulator

To model this accurately, our engine needs to consume two streams of market data:
1. L2 Order Book Updates: To track the depth of each price level and detect cancellations.
2. Trade Events: To decrement the volume ahead of us in the queue.

When we place an order, we query the current volume at that price level in the local order book. That volume becomes our initial queue_ahead value.

As subsequent L2 updates arrive, if the total volume at our price level decreases without a trade occurring, we must assume a portion of that reduction was due to order cancellations ahead of us. Since we cannot know the exact physical order layout without a Level 3 (L3) order-by-order feed (which is unavailable or prohibitively expensive on many exchanges), we use a standard linear cancellation assumption: cancellations are distributed proportionally across all orders in the queue.

Let’s translate this logic into high-performance Python code.

The Implementation

Below is the complete implementation of our queue-aware execution engine. It manages an internal L2 book state, tracks active limit orders, adjusts queue positions when trades and cancellations occur, and triggers fills.

import bisect
from typing import Dict, List, Optional, Tuple

class Order:
def __init__(self, order_id: str, side: str, price: float, qty: float):
self.order_id = order_id
self.side = side # "buy" or "sell"
self.price = price
self.qty = qty
self.queue_ahead = 0.0
self.filled_qty = 0.0
self.status = "PENDING"

def __repr__(self) -> str:
return (
f"Order(id={self.order_id}, side={self.side}, price={self.price}, "
f"qty={self.qty}, filled={self.filled_qty}, queue_ahead={self.queue_ahead}, status={self.status})"
)

class LocalOrderBook:
def __init__(self):
# We store bids and asks as sorted lists of [price, qty]
# bids are sorted descending, asks ascending
self.bids: List[List[float]] = []
self.asks: List[List[float]] = []

def update_level(self, side: str, price: float, qty: float):
book = self.bids if side == "buy" else self.asks
prices = [x[0] for x in book]

# Binary search for the price level
if side == "buy":
# Bids are descending, so we use a custom search or invert for bisect
idx = bisect.bisect_left([p for p in prices], price)
else:
idx = bisect.bisect_left(prices, price)

if idx < len(book) and book[idx][0] == price:
if qty == 0.0:
book.pop(idx)
else:
book[idx][1] = qty
elif qty > 0.0:
book.insert(idx, [price, qty])

def get_volume_at_price(self, side: str, price: float) -> float:
book = self.bids if side == "buy" else self.asks
for p, qty in book:
if p == price:
return qty
return 0.0

class ExecutionEngine:
def __init__(self):
self.book = LocalOrderBook()
self.active_orders: Dict[str, Order] = {}
# Keep track of previous book state to estimate cancellations
self.prev_depths: Dict[Tuple[str, float], float] = {}

def place_limit_order(self, order_id: str, side: str, price: float, qty: float) -> Order:
order = Order(order_id, side, price, qty)

# Get current volume ahead of us
current_vol = self.book.get_volume_at_price(side, price)
order.queue_ahead = current_vol
order.status = "OPEN"

self.active_orders[order_id] = order
return order

def cancel_order(self, order_id: str):
if order_id in self.active_orders:
self.active_orders[order_id].status = "CANCELLED"
del self.active_orders[order_id]

def process_book_update(self, side: str, price: float, new_qty: float):
key = (side, price)
old_qty = self.prev_depths.get(key, 0.0)
self.book.update_level(side, price, new_qty)
self.prev_depths[key] = new_qty

# If the size decreased, handle potential queue updates due to cancellations
if new_qty < old_qty:
reduction = old_qty new_qty
self._apply_cancellation_reduction(side, price, reduction)

def process_trade(self, side: str, price: float, trade_qty: float):
# A buy trade matches against sell limit orders. A sell trade matches against buy limit orders.
# side parameter here represents the trade execution side (e.g., "sell" trade hits bids)
target_side = "buy" if side == "sell" else "sell"

completed_fills = []

for order_id, order in self.active_orders.items():
if order.side == target_side and order.price == price:
if order.queue_ahead > 0:
# Trade execution eats into the queue ahead of us first
executed_against_queue = min(order.queue_ahead, trade_qty)
order.queue_ahead -= executed_against_queue
remaining_trade_qty = trade_qty executed_against_queue
else:
remaining_trade_qty = trade_qty

if order.queue_ahead <= 0 and remaining_trade_qty > 0:
# Our order starts filling
fill_amount = min(order.qty order.filled_qty, remaining_trade_qty)
order.filled_qty += fill_amount
if order.filled_qty >= order.qty:
order.status = "FILLED"
completed_fills.append(order_id)

for order_id in completed_fills:
del self.active_orders[order_id]

def _apply_cancellation_reduction(self, side: str, price: float, reduction_qty: float):
for order in self.active_orders.values():
if order.side == side and order.price == price:
if order.queue_ahead > 0:
# We assume cancellations are distributed proportionally.
# A conservative assumption is that cancellations happen uniformly.
# We reduce our queue position by a proportional factor of the reduction.
# For safety, we assume 50% of cancellations happen ahead of us.
estimated_cancels_ahead = reduction_qty * 0.5
order.queue_ahead = max(0.0, order.queue_ahead estimated_cancels_ahead)

# Verification script showing the mechanics under load
if __name__ == "__main__":
engine = ExecutionEngine()

# 1. Initialize order book state
print("— Initializing Order Book —")
engine.process_book_update("buy", 59000.0, 10.0)
engine.process_book_update("buy", 58900.0, 15.0)

# Show depth
print(f"Volume at 59000.0: {engine.book.get_volume_at_price('buy', 59000.0)}")

# 2. Place our limit order at 59000.0 for 2.0 units
# Since there are already 10.0 units there, our queue_ahead must be 10.0
my_order = engine.place_limit_order("order_001", "buy", 59000.0, 2.0)
print(f"\nPlaced Order: {my_order}")

# 3. Process a book update where depth shrinks (cancellation ahead of us)
# 59000.0 shrinks from 10.0 to 6.0 (reduction of 4.0)
print("\n— Order Book updates (Cancellations occur) —")
engine.process_book_update("buy", 59000.0, 6.0)
print(f"Updated Order Status: {my_order}")

# 4. Process a trade that doesn't reach us yet
# A sell trade of 3.0 units hits the buy side at 59000.0
print("\n— Trade of 3.0 units at 59000.0 —")
engine.process_trade("sell", 59000.0, 3.0)
print(f"Updated Order Status: {my_order}")

# 5. Process another trade of 6.0 units at 59000.0
# Remaining queue_ahead is 5.0. This trade should clear the queue and partially fill our order.
print("\n— Trade of 6.0 units at 59000.0 —")
engine.process_trade("sell", 59000.0, 6.0)
print(f"Updated Order Status: {my_order}")

Running the Verification

When executing the script, we see the engine process events dynamically:

— Initializing Order Book —
Volume at 59000.0: 10.0

Placed Order: Order(id=order_001, side=buy, price=59000.0, qty=2.0, filled=0.0, queue_ahead=10.0, status=OPEN)

— Order Book updates (Cancellations occur) —
Updated Order Status: Order(id=order_001, side=buy, price=59000.0, qty=2.0, filled=0.0, queue_ahead=8.0, status=OPEN)

— Trade of 3.0 units at 59000.0 —
Updated Order Status: Order(id=order_001, side=buy, price=59000.0, qty=2.0, filled=0.0, queue_ahead=5.0, status=OPEN)

— Trade of 6.0 units at 59000.0 —
Updated Order Status: Order(id=order_001, side=buy, price=59000.0, qty=2.0, filled=1.0, queue_ahead=0.0, status=OPEN)

Comparative results

To measure the impact of this change, we ran a backtest of our high-frequency market-making strategy on BTC-USDT over a 7-day period. We compared three different execution simulators:

  1. Naive Touch Simulator: Fills immediately if the trade price matches or crosses the limit price.
  2. Strict Queue Simulator (No Cancellation Credit): Tracks queue position, but ignores order cancellations (only trades decrement queue size).
  3. Queue-Aware Simulator with 50% Cancellation Estimation: Our implemented approach.
Simulator Model Fill Rate (%) Realized Sharpe Ratio Net PnL (USD) Max Drawdown (%)
Naive Touch 94.2% 4.12 +\$42,150 -1.2%
Strict Queue (No Cancels) 21.5% -1.82 -\$11,400 -8.4%
Queue-Aware (50% Cancels) 41.1% 1.84 +\$12,300 -3.1%
Production Live Performance 39.5% 1.71 +\$10,850 -3.5%

The results show how dangerous naive backtesters are. The Naive Touch simulator yielded an unrealistically high 94.2% fill rate because it assumed we were always at the front of the queue. This led to highly profitable but fictitious round-trips.

Conversely, ignoring cancellations entirely (Strict Queue) was overly conservative, underestimating our actual fills and killing the strategy’s viability by missing profitable trade executions.

Our hybrid model, which estimates that 50% of book cancellations occur ahead of us in the queue, tracked our live production metrics with remarkable accuracy (41.1% simulated fill rate vs 39.5% actual live fill rate).

Key takeaways and trade-offs

  • The 50% Cancellation Rule is a Heuristic: Unless you have L3 ITCH data where every individual order is tagged with an ID, you cannot know if a cancellation happened in front of or behind your order. Setting this parameter to 50% is a balanced approach, but during periods of heavy market stress, cancellations often spike at the front of the book as algorithmic market makers pull liquidity.
  • Latency Matters: Running L2 state-tracking in Python is sufficient for backtesting historical data up to 100,000 events per second. If you require real-time hardware-in-the-loop simulation or sub-millisecond execution modeling, this logic must be implemented in C++ or Rust using flat arrays instead of dictionaries.
  • Adverse Selection is Real: A low fill rate is not just a volume issue; it means your orders are only being filled when toxic flow sweeps the book. When building strategies, always evaluate the performance of your limit orders conditional on the price continuing to move through your level versus reversing immediately after filling you.

Join the conversation

Your email address will not be published. Required fields are marked *