NautilusTrader
Getting Started

Nightly docs

These docs track unreleased changes and may change without notice. Their code examples can use APIs that the latest release lacks, so run them with a development wheel or switch to the latest release 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-3.14
  • NautilusTrader 2.x installed (pip install -U --pre nautilus_trader --extra-index-url=https://packages.nautechsystems.io/simple). The --pre flag is required while 2.x ships as 2.0.0rcN.
  • pandas (pip install pandas), used by the reports at the end. The wheel declares no runtime dependencies.
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.execution import MakerTakerFeeModel
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 sample test data (ETHUSDT trades from Binance), initialize the matching instrument, and build Nautilus TradeTick objects from the CSV. TestDataProvider reads the CSV from the local test_data/ directory in a source checkout and downloads it from GitHub otherwise, so a wheel install needs network access.

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

# Build Nautilus trade ticks from the sample 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")),
    ],
    fee_model=MakerTakerFeeModel(
        maker_rate=Decimal("0.0001"),
        taker_rate=Decimal("0.0001"),
    ),
)

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):
    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: object,
    ) -> 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) -> None:
        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) -> None:
        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) -> None:
        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) -> None:
        self.submit_twap_order(OrderSide.BUY)

    def sell(self) -> None:
        self.submit_twap_order(OrderSide.SELL)

    def submit_twap_order(self, side: OrderSide) -> None:
        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) -> None:
        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 loaded components before adding replacements.

Clear and add 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