NautilusTrader
Getting Started
These docs track the unreleased nightly build and may change without notice. Switch to the latest stable docs.

Backtest (Low-Level API)

Use BacktestEngine for direct component access: load market data, wire up strategies and execution algorithms, and run backtests with full control over every step. This tutorial backtests an EMA cross strategy with a TWAP execution algorithm on a simulated Binance Spot exchange using historical trade tick data.

View source on GitHub.

Prerequisites

  • Python 3.12+
  • NautilusTrader latest release installed (pip install nautilus_trader)
from decimal import Decimal

from nautilus_trader.backtest import BacktestEngine
from nautilus_trader.common import LogLevel
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import ExecutionAlgorithmConfig
from nautilus_trader.config import LoggerConfig
from nautilus_trader.config import StrategyConfig
from nautilus_trader.indicators import ExponentialMovingAverage
from nautilus_trader.model import AccountType
from nautilus_trader.model import Bar
from nautilus_trader.model import BarType
from nautilus_trader.model import Currency
from nautilus_trader.model import ExecAlgorithmId
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import Money
from nautilus_trader.model import OmsType
from nautilus_trader.model import OrderSide
from nautilus_trader.model import TraderId
from nautilus_trader.model import Venue
from nautilus_trader.testkit.providers import TestDataProvider
from nautilus_trader.testkit.providers import TestInstrumentProvider
from nautilus_trader.trading import Strategy

Load data

Load bundled test data (ETHUSDT trades from Binance), initialize the matching instrument, and build Nautilus TradeTick objects from the CSV.

# Initialize the instrument which matches the data
ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance()

# Build Nautilus trade ticks from the bundled Binance CSV
ticks = TestDataProvider.trades_from_binance_csv(
    ETHUSDT_BINANCE,
    "binance/ethusdt-trades.csv",
)

See the Data concept guide for details on the data processing pipeline.

Initialize the engine

Pass a BacktestEngineConfig to configure the engine. Here we set a custom trader_id to show the pattern.

# Configure backtest engine
config = BacktestEngineConfig(
    trader_id=TraderId("BACKTESTER-001"),
    logging=LoggerConfig(stdout_level=LogLevel.ERROR),
)

# Build the backtest engine
engine = BacktestEngine(config=config)

Add a venue

Set up a simulated venue that matches the market data. Here we configure a Binance Spot exchange with a cash account.

# Add a trading venue (multiple venues possible)
BINANCE = Venue("BINANCE")
engine.add_venue(
    venue=BINANCE,
    oms_type=OmsType.NETTING,
    account_type=AccountType.CASH,  # Spot CASH account (not for perpetuals or futures)
    base_currency=None,  # Multi-currency account
    starting_balances=[
        Money(1_000_000.0, Currency.from_str("USDT")),
        Money(10.0, Currency.from_str("ETH")),
    ],
)

Add data

Add the instrument and trade ticks to the engine.

# Add instrument(s)
engine.add_instrument(ETHUSDT_BINANCE)

# Add data
engine.add_data(ticks)

You can add multiple data types (including custom types) and backtest across multiple venues.

Add strategies

The strategy extends Strategy and trades an EMA crossover on 250-tick bars, which the engine aggregates internally from the trade ticks. Entries are submitted with an exec_algorithm_id so the engine routes them to the TWAP execution algorithm for slicing.

class EMACrossTWAPConfig(StrategyConfig):
    _CUSTOM_FIELDS = (
        "instrument_id",
        "bar_type",
        "trade_size",
        "fast_ema_period",
        "slow_ema_period",
        "twap_horizon_secs",
        "twap_interval_secs",
    )

    def __new__(cls, *args, **kwargs):
        for field in cls._CUSTOM_FIELDS:
            kwargs.pop(field, None)
        return super().__new__(cls, *args, **kwargs)

    def __init__(
        self,
        instrument_id: InstrumentId,
        bar_type: BarType,
        trade_size: Decimal,
        fast_ema_period: int = 10,
        slow_ema_period: int = 20,
        twap_horizon_secs: float = 10.0,
        twap_interval_secs: float = 2.5,
        **_kwargs,
    ) -> None:
        super().__init__()
        self.instrument_id = instrument_id
        self.bar_type = bar_type
        self.trade_size = trade_size
        self.fast_ema_period = fast_ema_period
        self.slow_ema_period = slow_ema_period
        self.twap_horizon_secs = twap_horizon_secs
        self.twap_interval_secs = twap_interval_secs


class EMACrossTWAP(Strategy):
    def __init__(self, config: EMACrossTWAPConfig):
        super().__init__(config)
        self.fast_ema = ExponentialMovingAverage(config.fast_ema_period)
        self.slow_ema = ExponentialMovingAverage(config.slow_ema_period)
        self.exec_algorithm_id = ExecAlgorithmId("TWAP")
        self.exec_algorithm_params = {
            "horizon_secs": str(config.twap_horizon_secs),
            "interval_secs": str(config.twap_interval_secs),
        }

    def on_start(self):
        self.register_indicator_for_bars(self.config.bar_type, self.fast_ema)
        self.register_indicator_for_bars(self.config.bar_type, self.slow_ema)
        self.subscribe_bars(self.config.bar_type)

    def on_bar(self, bar: Bar):
        if not self.indicators_initialized():
            return

        if self.fast_ema.value >= self.slow_ema.value:
            if self.portfolio.is_net_flat(self.config.instrument_id):
                self.buy()
            elif self.portfolio.is_net_short(self.config.instrument_id):
                self.close_all_positions(self.config.instrument_id)
                self.buy()
        elif self.fast_ema.value < self.slow_ema.value:
            if self.portfolio.is_net_flat(self.config.instrument_id):
                self.sell()
            elif self.portfolio.is_net_long(self.config.instrument_id):
                self.close_all_positions(self.config.instrument_id)
                self.sell()

    def buy(self):
        self.submit_twap_order(OrderSide.BUY)

    def sell(self):
        self.submit_twap_order(OrderSide.SELL)

    def submit_twap_order(self, side: OrderSide):
        instrument = self.cache.instrument(self.config.instrument_id)
        order = self.order_factory.market(
            self.config.instrument_id,
            side,
            instrument.make_qty(self.config.trade_size),
            exec_algorithm_id=self.exec_algorithm_id,
            exec_algorithm_params=self.exec_algorithm_params,
        )
        self.submit_order(order)

    def on_stop(self):
        self.close_all_positions(self.config.instrument_id)
# Configure and add the strategy
strategy_config = EMACrossTWAPConfig(
    instrument_id=ETHUSDT_BINANCE.id,
    bar_type=BarType.from_str("ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL"),
    trade_size=Decimal("0.10"),
    fast_ema_period=10,
    slow_ema_period=20,
    twap_horizon_secs=10.0,
    twap_interval_secs=2.5,
)

strategy = EMACrossTWAP(config=strategy_config)
engine.add_strategy(strategy=strategy)

The strategy config carries the TWAP parameters, but the execution algorithm itself is a separate component.

Add execution algorithms

Register the built-in TWAP execution algorithm under the TWAP identifier the strategy references.

# Add the native TWAP execution algorithm
engine.add_native_exec_algorithm(
    "TwapAlgorithm",
    ExecutionAlgorithmConfig(exec_algorithm_id=ExecAlgorithmId("TWAP")),
)

Run the backtest

Call .run() to process all available data. The engine replays events in timestamp order with deterministic execution semantics.

# Run the engine (from start to end of data)
engine.run()

Post-run analysis

The engine retains data and execution objects in memory for generating reports. It also logs a tearsheet with default statistics; see the Portfolio statistics guide for custom statistics.

engine.generate_account_report(BINANCE)
engine.generate_order_fills_report()
engine.generate_positions_report()

Repeated runs

Reset the engine for repeated runs. Instruments, data, and loaded components persist across resets; loaded components have their internal state reset.

# For repeated backtest runs, reset the engine
engine.reset()

# Clear or remove loaded components before adding replacements.

Remove and add individual components (actors, strategies, execution algorithms) as required.

See the BacktestEngine API reference for the add and clear methods.

# Once done, good practice to dispose of the object if the script continues
engine.dispose()

On this page