The current backtesting system handles many order-fill scenarios effectively, particularly with L2 data.
While the existing probabilistic FillModel serves its purpose for basic scenarios,
there are several significant limitations that prevent realistic simulation of market dynamics:
- Can't Simulate Competition for Liquidity
- The system cannot model how market participants compete for available liquidity, treating all visible volume as fully accessible.
- Can't simulate varying liquidity over time
- Unable to model varying liquidity conditions and spread patterns across different trading sessions.
- No Price Impact for Large Orders
- Large orders are processed similarly to small ones, failing to account for realistic market impact and slippage.
- No Partial Fills
- The system only supports complete fills, lacking the ability to simulate gradual execution of orders.
- Can't Simulate Order Queue Position
- Limited ability to model price-time priority and realistic fill patterns for limit orders.
- Too Simplistic L1 Data Simulation
- Basic probabilistic fill model doesn't capture market depth and liquidity nuances when working with less granular data.
We propose completely replacing the current FillModel with a single powerful method that both replicates all existing functionality and enables sophisticated fill simulation capabilities.
The core of the solution is this single method in the FillModel class.
Let's look at a simple example that demonstrates how this method can force all orders to get filled with exactly one tick of slippage:
class OneTickSlippageFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
tick = instrument.price_increment
return OrderBook(
bids=[
(best_bid, 0), # Top level - no volume
(best_bid - tick, 999999) # Second level - unlimited volume
],
asks=[
(best_ask, 0), # Top level - no volume
(best_ask + tick, 999999) # Second level - unlimited volume
]
)This simple example shows the power of the approach:
- By setting zero volume at the best prices, no fills can occur there
- By setting unlimited volume one tick away, all fills must occur there
- This creates guaranteed 1-tick slippage for all orders
- The behavior is crystal clear and deterministic from the code
This approach allows fill models to precisely control all aspects of fill simulation by returning a custom OrderBook that represents the expected market liquidity. The OrderMatchingEngine will use this simulated OrderBook to determine fills, maintaining its current L2 processing logic while enabling:
The beauty of this approach is that it:
- Maintains the current system's unified L2 processing
- Requires minimal changes to the matching engine
- Makes fill simulation explicit and easy to understand
The simulated OrderBook acts as a simple "contract" between the fill model and matching engine, allowing each fill model to implement its own assumptions about market liquidity and fill behavior.
This same mechanism can:
- Replicate all current
FillModelfunctionality (probabilistic fills, slippage) - Enable sophisticated new capabilities (partial fills, queue position, etc.)
- Make fill simulation explicit and easy to understand
- Give fill models complete control over fill behavior
The simulated OrderBook serves as a simple yet powerful interface between fill model and matching engine. The fill model precisely specifies available liquidity, and the matching engine executes fills according to normal L2 processing rules against this simulated book.
This section demonstrates various approaches to order fill simulation using the new FillModel implementations:
Simulates optimistic market conditions - every order gets filled immediately at the best available price. Ideal for testing basic strategy logic.
class BestPriceFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Basic fill simulation: All orders fill at best available price"""
UNLIMITED = 1_000_000 # Large enough to fill any order
return OrderBook(
bids=[(best_bid, UNLIMITED)], # For sell market orders
asks=[(best_ask, UNLIMITED)] # For buy market orders
)Simulates basic market depth behavior - first 10 contracts execute at best price, remaining quantity executes one tick worse. Provides realistic simulation of basic market impact.
class TwoTierFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Two-tier fill simulation:
- First 10 contracts at best price
- Remainder at 1 tick worse
"""
tick = instrument.price_increment
UNLIMITED = 1_000_000
return OrderBook(
bids=[
(best_bid, 10), # Top level - 10 contracts
(best_bid - tick, UNLIMITED) # Second level - remainder
],
asks=[
(best_ask, 10), # Top level - 10 contracts
(best_ask + tick, UNLIMITED) # Second level - remainder
]
)Simulates realistic market depth behavior - distributes 100-contract order fills across three price levels. Demonstrates how large orders interact with available market liquidity.
class ThreeTierFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Progressive fill simulation for 100-contract order:
- 50 contracts at best price
- 30 contracts 1 tick worse
- 20 contracts 2 ticks worse
"""
tick = instrument.price_increment
return OrderBook(
bids=[
(best_bid, 50), # Level 1: 50 contracts
(best_bid - tick, 30), # Level 2: 30 contracts
(best_bid - tick * 2, 20) # Level 3: 20 contracts
],
asks=[
(best_ask, 50), # Level 1: 50 contracts
(best_ask + tick, 30), # Level 2: 30 contracts
(best_ask + tick * 2, 20) # Level 3: 20 contracts
]
)Simulates uncertainty in fill prices - implements 50/50 chance between best price and one tick slippage. Replicates current FillModel's probabilistic behavior.
class ProbabilisticFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""50% chance of fill at best price, 50% chance of 1-tick slippage"""
tick = instrument.price_increment
UNLIMITED = 1_000_000
if random.random() < 0.5:
# Return order book with UNLIMITED volume at best price = simulating best fill
return OrderBook(
bids=[(best_bid, UNLIMITED)],
asks=[(best_ask, UNLIMITED)]
)
else:
# Return order book with 0 volume at best price = simulating order will be entirely filled at 2nd level price
return OrderBook(
bids=[
(best_bid, 0), # No volume at best price
(best_bid - tick, UNLIMITED) # All volume one tick worse
],
asks=[
(best_ask, 0), # No volume at best price
(best_ask + tick, UNLIMITED) # All volume one tick worse
]
)Simulates varying market conditions - implements wider spreads during low liquidity periods. Essential for strategies that trade across different market sessions.
class MarketHoursFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Simulates wider spreads during low liquidity periods"""
tick = instrument.price_increment
NORMAL_VOLUME = 500
if self.is_low_liquidity_period(): # e.g., outside market hours
return OrderBook(
bids=[(best_bid - tick, NORMAL_VOLUME)], # 1 tick wider
asks=[(best_ask + tick, NORMAL_VOLUME)] # 1 tick wider
)
return OrderBook(
bids=[(best_bid, NORMAL_VOLUME)],
asks=[(best_ask, NORMAL_VOLUME)]
)Simulates dynamic market depth - adjusts available liquidity based on recent trading volume. Creates realistic market depth based on actual market activity.
class VolumeSensitiveFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
tick = instrument.price_increment
UNLIMITED = 1_000_000
# Get last 5x 1minute bars
bars = self.cache.bars( ... )
# Calc average volume
avg_volume = sum(bar.volume for bar in bars) / len(bars)
# Available liquidity for our order is only 25%
available_volume = int(avg_volume * 0.25)
return OrderBook(
bids=[
(best_bid, available_volume), # Volume based on recent trading
(best_bid - tick, UNLIMITED) # Unlimited volume one tick worse
],
asks=[
(best_ask, available_volume), # Volume based on recent trading
(best_ask + tick, UNLIMITED) # Unlimited volume one tick worse
]
)Simulates market competition effects - makes only 30% of visible liquidity actually available. Reflects realistic conditions where multiple traders compete for same liquidity.
class CompetitionAwareFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Simulates that only 30% of visible liquidity is available"""
real_current_book = self.get_orderbook(instrument)
LIQUIDITY_FACTOR = 0.3 # Can access 30% of visible liquidity
available_bid = int(real_current_book.bids[0].volume * LIQUIDITY_FACTOR)
available_ask = int(real_current_book.asks[0].volume * LIQUIDITY_FACTOR)
return OrderBook(
bids=[(best_bid, available_bid)],
asks=[(best_ask, available_ask)]
)Simulates size-dependent market impact - applies different execution models based on order size. Reflects how market treats different order sizes.
class SizeAwareFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
"""Different liquidity profiles based on order size"""
tick = instrument.price_increment
if order.quantity <= 10: # Small orders: good liquidity
return OrderBook(
bids=[(best_bid, 50)],
asks=[(best_ask, 50)]
)
else: # Large orders: price impact
return OrderBook(
bids=[
(best_bid, 10),
(best_bid - tick, order.quantity - 10)
],
asks=[
(best_ask, 10),
(best_ask + tick, order.quantity - 10)
]
)Simulates realistic limit order behavior - fills only 5 contracts of order quantity when price touches limit level. Models typical limit order queue behavior.
class LimitOrderFillModel(FillModel):
def get_orderbook_for_fill_simulation(self, instrument, order, best_bid, best_ask):
tick = instrument.price_increment
UNLIMITED = 1_000_000
if order.type == OrderType.LIMIT:
return OrderBook(
bids=[
(best_bid, 5), # Max 5 contracts fill if market-price touch limit-price
(best_bid - tick, UNLIMITED), # Second level acts as price buffer
],
asks=[
(best_ask, 5), # Max 5 contracts fill if market-price touch limit-price
(best_ask + tick, UNLIMITED), # Second level acts as price buffer
]
)These examples demonstrate the flexibility and power of the new fill simulation approach. Each implementation targets a specific aspect of market behavior, from basic fills to complex market dynamics. Users can choose the most appropriate model for their needs or combine different approaches to create more sophisticated simulations.
The beauty of this design is that it unifies all fill simulation logic into a single, easy-to-understand method while maintaining the ability to create highly customized fill behaviors. This makes it both simple to use for basic cases and powerful enough for advanced scenarios.