Backtesting
We are currently working on this guide.
Backtesting with NautilusTrader is a methodical simulation process that replicates trading
activities using a specific system implementation. This system is composed of various components
including the built-in engines, Cache
, MessageBus, Portfolio
, Actors, Strategies, Execution Algorithms,
and other user-defined modules. The entire trading simulation is predicated on a stream of historical data processed by a
BacktestEngine
. Once this data stream is exhausted, the engine concludes its operation, producing
detailed results and performance metrics for in-depth analysis.
It's important to recognize that NautilusTrader offers two distinct API levels for setting up and conducting backtests:
- High-level API: Uses a
BacktestNode
and configuration objects (BacktestEngine
s are used internally). - Low-level API: Uses a
BacktestEngine
directly with more "manual" setup.
Choosing an API level
Consider using the low-level API when:
- Your entire data stream can be processed within the available machine resources (e.g., RAM).
- You prefer not to store data in the Nautilus-specific Parquet format.
- You have a specific need or preference to retain raw data in its original format (e.g., CSV, binary, etc.).
- You require fine-grained control over the
BacktestEngine
, such as the ability to re-run backtests on identical datasets while swapping out components (e.g., actors or strategies) or adjusting parameter configurations.
Consider using the high-level API when:
- Your data stream exceeds available memory, requiring streaming data in batches.
- You want to leverage the performance and convenience of the
ParquetDataCatalog
for storing data in the Nautilus-specific Parquet format. - You value the flexibility and functionality of passing configuration objects to define and manage multiple backtest runs across various engines simultaneously.
Low-level API
The low-level API centers around a BacktestEngine
, where inputs are initialized and added manually via a Python script.
An instantiated BacktestEngine
can accept the following:
- Lists of
Data
objects, which are automatically sorted into monotonic order based onts_init
. - Multiple venues, manually initialized.
- Multiple actors, manually initialized and added.
- Multiple execution algorithms, manually initialized and added.
This approach offers detailed control over the backtesting process, allowing you to manually configure each component.
High-level API
The high-level API centers around a BacktestNode
, which orchestrates the management of multiple BacktestEngine
instances,
each defined by a BacktestRunConfig
. Multiple configurations can be bundled into a list and processed by the node in one run.
Each BacktestRunConfig
object consists of the following:
- A list of
BacktestDataConfig
objects. - A list of
BacktestVenueConfig
objects. - A list of
ImportableActorConfig
objects. - A list of
ImportableStrategyConfig
objects. - A list of
ImportableExecAlgorithmConfig
objects. - An optional
ImportableControllerConfig
object. - An optional
BacktestEngineConfig
object, with a default configuration if not specified.
Data
Data provided for backtesting drives the execution flow. Since a variety of data types can be used, it's crucial that your venue configurations align with the data being provided for backtesting. Mismatches between data and configuration can lead to unexpected behavior during execution.
NautilusTrader is primarily designed and optimized for order book data, which provides a complete representation of every price level or order in the market, reflecting the real-time behavior of a trading venue. This ensures the highest level of execution granularity and realism. However, if granular order book data is either not available or necessary, then the platform has the capability of processing market data in the following descending order of detail:
-
Order Book Data/Deltas (L3 market-by-order):
- Providing comprehensive market depth and detailed order flow, with visibility of all individual orders.
-
Order Book Data/Deltas (L2 market-by-price):
- Providing market depth visibility across all price levels.
-
Quote Ticks (L1 market-by-price):
- Representing the "top of the book" by capturing only the best bid and ask prices and sizes.
-
Trade Ticks:
- Reflecting actual executed trades, offering a precise view of transaction activity.
-
Bars:
- Aggregating trading activity - typically over fixed time intervals, such as 1-minute, 1-hour, or 1-day.
Venues
When initializing a venue for backtesting, you must specify its internal order book_type
for execution processing from the following options:
L1_MBP
: Level 1 market-by-price (default). Only the top level of the order book is maintained.L2_MBP
: Level 2 market-by-price. Order book depth is maintained, with a single order aggregated per price level.L3_MBO
: Level 3 market-by-order. Order book depth is maintained, with all individual orders tracked as provided by the data.
The granularity of the data must match the specified order book_type
. Nautilus cannot generate higher granularity data (L2 or L3) from lower-level data such as quotes, trades, or bars.
If you specify L2_MBP
or L3_MBO
as the venue’s book_type
, all non-order book data (such as quotes, trades, and bars) will be ignored for execution processing.
This may cause orders to appear as though they are never filled. We are actively working on improved validation logic to prevent configuration and data mismatches.
When providing Level 2 or higher order book data, ensure that the book_type
is updated to reflect the data's granularity.
Failing to do so will result in data aggregation: L2 data will be reduced to a single order per level, and L1 data will reflect only top-of-book levels.
Execution
Bar based execution
Bar data provides a summary of market activity with four key prices for each time period (assuming bars are aggregated by trades):
- Open: opening price (first trade)
- High: highest price traded
- Low: lowest price traded
- Close: closing price (last trade)
While this gives us an overview of price movement, we lose some important details that we'd have with more granular data:
- We don't know in what order the market hit the high and low prices.
- We can't see exactly when prices changed within the time period.
- We don't know the actual sequence of trades that occurred.
This is why Nautilus processes bar data through a sophisticated system that attempts to maintain the most realistic yet conservative market behavior possible, despite these limitations. At its core, the platform always maintains an order book simulation - even when you provide less granular data such as quotes, trades or bars (although the simulation will only have a top level book).
Processing bar data: Time / Prices / Executions
Even when you provide bar data, Nautilus maintains an internal order book for each instrument - just like a real venue would.
-
Time Processing:
- Nautilus has a specific way of handling the timing of bar data for execution that's crucial for accurate simulation.
- Bar timestamps (
ts_event
) are expected to represent the close time of the bar. This approach is most logical because it represents the moment when the bar is fully formed and its aggregation is complete. - The initialization time (
ts_init
) can be controlled using thets_init_delta
parameter inBarDataWrangler
, which should typically be set to the bar's step size (timeframe) in nanoseconds. - The platform ensures all events happen in the correct sequence based on these timestamps, preventing any possibility of look-ahead bias in your backtests.
-
Price Processing:
- The platform converts each bar's OHLC prices into a sequence of market updates.
- These updates always follow the same order: Open → High → Low → Close.
- If you provide multiple timeframes (like both 1-minute and 5-minute bars), the platform uses the more granular data for highest accuracy.
-
Executions:
- When you place orders, they interact with the simulated order book just like they would on a real venue.
- For MARKET orders, execution happens at the current simulated market price plus any configured latency.
- For LIMIT orders working in the market, they'll execute if any of the bar's prices reach or cross your limit price (see below).
- The matching engine continuously processes orders as OHLC prices move, rather than waiting for complete bars.
Slippage and Spread Handling
When backtesting with different types of data, NautilusTrader implements specific handling for slippage and spread simulation:
For L2 (market-by-price) or L3 (market-by-order) data, slippage is simulated with high accuracy by:
- Filling orders against actual order book levels
- Matching available size at each price level sequentially
- Maintaining realistic order book depth impact (per order fill)
For L1 data types (e.g., L1 orderbook, trades, quotes, bars), slippage is handled through:
Initial Fill Slippage (prob_slippage
)
- Controlled by the
prob_slippage
parameter of theFillModel
. - Determines if the initial fill will occur one tick away from current market price.
- Example: With
prob_slippage=0.5
, a market BUY has 50% chance of filling one tick above the best ask price.
When backtesting with bar data, be aware that the reduced granularity of price information affects the slippage mechanism. For the most realistic backtesting results, consider using higher granularity data sources such as L2 or L3 order book data when available.
Fill Model
The FillModel
helps simulate order queue position and execution in a simple probabilistic way during backtesting.
It addresses a fundamental challenge: even with perfect historical market data, we can't fully simulate how orders may have interacted with other
market participants in real-time.
The FillModel
simulates two key aspects of trading that exist in real markets regardless of data quality:
-
Queue Position for Limit Orders
- When multiple traders place orders at the same price level, the order's position in the queue affects if and when it gets filled.
-
Market Impact and Competition
- When taking liquidity with market orders, you compete with other traders for available liquidity, which can affect your fill price.
Configuration and Parameters
from nautilus_trader.backtest.models import FillModel
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.backtest.engine import BacktestEngineConfig
# Create a custom fill model with your desired probabilities
fill_model = FillModel(
prob_fill_on_limit=0.2, # Chance a limit order fills when price matches (applied to bars/trades/quotes + L1/L2/L3 orderbook)
prob_fill_on_stop=0.95, # [DEPRECATED] Will be removed in a future version, use `prob_slippage` instead
prob_slippage=0.5, # Chance of 1-tick slippage (applied to bars/trades/quotes + L1 orderbook only)
random_seed=None, # Optional: Set for reproducible results
)
# Add the fill model to your engine configuration
engine = BacktestEngine(
config=BacktestEngineConfig(
trader_id="TESTER-001",
fill_model=fill_model, # Inject your custom fill model here
)
)
prob_fill_on_limit (default: 1.0
)
- Purpose:
- Simulates the probability of a limit order getting filled when its price level is reached in the market.
- Details:
- Simulates your position in the order queue at a given price level.
- Applies to all data types (e.g., L3/L2/L1 orderbook, quotes, trades, bars).
- New random probability check occurs each time market price touches your order price (but does not move through it).
- On successful probability check, fills entire remaining order quantity.
- Examples:
- With
prob_fill_on_limit=0.0
:- Limit buy orders never fill when best ask reaches the limit price
- Limit sell orders never fill when best bid reaches the limit price
- This simulates being at the very back of the queue and never reaching the front
- With
prob_fill_on_limit=0.5
:- Limit buy orders have 50% chance of filling when best ask reaches the limit price
- Limit sell orders have 50% chance of filling when best bid reaches the limit price
- This simulates being in the middle of the queue
- With
prob_fill_on_limit=1.0
(default):- Limit buy orders always fill when best ask reaches the limit price
- Limit sell orders always fill when best bid reaches the limit price
- This simulates being at the front of the queue with guaranteed fills
- With
prob_slippage (default: 0.0
)
- Purpose:
- Simulates the probability of experiencing price slippage when executing market orders.
- Details:
- Only applies to L1 data types (e.g., quotes, trades, bars).
- When triggered, moves fill price one tick against your order direction.
- Affects all market-type orders (
MARKET
,MARKET_TO_LIMIT
,MARKET_IF_TOUCHED
,STOP_MARKET
). - Not utilized with L2/L3 data where order book depth can determine slippage.
- Example:
- With
prob_slippage=0.0
(default):- No artificial slippage is applied, representing an idealized scenario where you always get filled at the current market price
- With
prob_slippage=0.5
:- Market BUY orders have 50% chance of filling one tick above the best ask price, and 50% chance at the best ask price
- Market SELL orders have 50% chance of filling one tick below the best bid price, and 50% chance at the best bid price
- With
prob_slippage=1.0
:- Market BUY orders always fill one tick above the best ask price
- Market SELL orders always fill one tick below the best bid price
- This simulates consistent adverse price movement against your orders
- With
prob_fill_on_stop (default: 1.0
)
- DEPRECATED: This parameter will be removed in a future version.
- Stop order is just shorter name for stop-market order, that convert to market orders when market-price touches the stop-price
- That means, stop order order-fill mechanics is simply market-order mechanics, that is controlled by the
prob_slippage
parameter.
How Simulation Varies by Data Type
The behavior of the FillModel
adapts based on the order book type being used:
L2/L3 Orderbook data
With full order book depth, the FillModel
focuses purely on simulating queue position for limit orders through prob_fill_on_limit
. The order book itself handles slippage naturally based on available liquidity at each price level.
prob_fill_on_limit
is active - simulates queue positionprob_slippage
is not used - real order book depth determines price impact
L1 Orderbook data
With only best bid/ask prices available, the FillModel
provides additional simulation:
prob_fill_on_limit
is active - simulates queue positionprob_slippage
is active - simulates basic price impact since we lack real depth information
Bar/Quote/Trade data
When using less granular data, the same behaviors apply as L1:
prob_fill_on_limit
is active - simulates queue positionprob_slippage
is active - simulates basic price impact
Important Considerations
The FillModel
has certain limitations to keep in mind:
- Partial fills are not simulated - orders either fill completely or not at all
- With L1 data, slippage is limited to a fixed 1-tick, at which entire order's quantity is filled
The FillModel
continues to evolve, future versions may introduce more sophisticated simulation of order execution dynamics, including:
- Partial fill simulation
- Variable slippage based on order size
- More complex queue position modeling