Nautilus Trader - Getting Started

NautilusTrader is “an open-source, production-grade, Rust-native engine for multi-asset, multi-venue trading systems.” The system “spans research, deterministic simulation, and live execution within a single event-driven architecture, with Python serving as the control plane for strategy logic, configuration, and orchestration.”

This page captures the install + first-backtest path for someone evaluating Nautilus as a candidate engine.

Prerequisites

Per the docs, to begin using NautilusTrader you need:

  • “Python 3.12–3.14 with the nautilus_trader package installed”
  • A method to execute Python scripts or Jupyter notebooks

Supported Platforms

NautilusTrader officially supports “Python 3.12-3.14 on the following 64-bit platforms”:

  • Linux (Ubuntu) 22.04 and later (x86_64 and ARM64)
  • macOS 15.0 and later (ARM64)
  • Windows Server 2022 and later (x86_64)

On Linux, “confirm your glibc version with ldd --version and ensure it reports 2.35 or newer before proceeding.”

Installation

Primary: pip (via uv)

The standard install - single command, prebuilt wheels, no Rust toolchain required for end users:

uv pip install nautilus_trader

A virtual environment is strongly recommended.

Optional Extras

Pull integration-specific deps with extras. Available extras: betfair, docker, ib, polymarket, visualization.

uv pip install "nautilus_trader[docker,ib]"

The ib extra is the relevant one for this project (Interactive Brokers).

Alternative Index (Nautech Systems)

Stable releases:

uv pip install nautilus_trader --index-url=https://packages.nautechsystems.io/simple

Pre-release / development wheels:

uv pip install nautilus_trader --pre --index-url=https://packages.nautechsystems.io/simple

Docker Alternative

“A self-contained Jupyter notebook server is available as a Docker image, no local setup required.” This is the lowest-friction path for a first backtest.

Optional: Redis

“The minimum supported Redis version is 6.2.” Used for cache/state:

docker run -d --name redis -p 6379:6379 redis:latest

Building from Source (Rust toolchain)

End users do NOT need Rust - wheels are prebuilt. Rust is only required if building from source (contributors, custom forks, or platforms with no wheel).

# Rust toolchain
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
 
# Linux build deps
sudo apt-get install clang lld
 
# uv (Python package/env manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
 
# Clone + sync
git clone --branch develop --depth 1 https://github.com/nautechsystems/nautilus_trader
cd nautilus_trader
uv sync --all-extras

Linux/macOS PyO3 environment variables are required for the in-tree build (see installation page). Mac Sequoia ARM64 is officially supported.

Two API Levels

The docs split backtesting into:

  • High-Level API (BacktestNode / TradingNode): “Recommended for production, easier transition to live trading; requires a Parquet data catalog”
  • Low-Level API (BacktestEngine): “For library development, direct component access; no live-trading path”

Hard constraint: “One node per process” - multiple instances in a single process are not supported. Sequential execution with cleanup is fine.

First Backtest (Quickstart)

The quickstart promises a runnable backtest in under five minutes using an EMA-cross strategy on synthetic EUR/USD bars via the low-level BacktestEngine API. Three pieces: strategy class, synthetic data, engine config.

Strategy

from decimal import Decimal
from nautilus_trader.config import StrategyConfig
from nautilus_trader.indicators import ExponentialMovingAverage
from nautilus_trader.model.data import Bar, BarType
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.trading.strategy import Strategy
 
class EMACrossConfig(StrategyConfig, frozen=True):
    instrument_id: InstrumentId
    bar_type: BarType
    trade_size: Decimal
    fast_ema_period: int = 10
    slow_ema_period: int = 20
 
class EMACross(Strategy):
    def __init__(self, config: EMACrossConfig):
        super().__init__(config)
        self.fast_ema = ExponentialMovingAverage(config.fast_ema_period)
        self.slow_ema = ExponentialMovingAverage(config.slow_ema_period)
 
    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_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_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):
        instrument = self.cache.instrument(self.config.instrument_id)
        order = self.order_factory.market(
            self.config.instrument_id,
            OrderSide.BUY,
            instrument.make_qty(self.config.trade_size),
        )
        self.submit_order(order)
 
    def sell(self):
        instrument = self.cache.instrument(self.config.instrument_id)
        order = self.order_factory.market(
            self.config.instrument_id,
            OrderSide.SELL,
            instrument.make_qty(self.config.trade_size),
        )
        self.submit_order(order)
 
    def on_stop(self):
        self.close_all_positions(self.config.instrument_id)

Notes for our project:

  • Config is a frozen pydantic-style dataclass - StrategyConfig is the base.
  • Indicator registration via register_indicator_for_bars - engine pushes bars into the indicator automatically, no manual update() calls.
  • on_bar is the event handler; indicators_initialized() gates trading until warmup completes.
  • Order construction via self.order_factory.market(...) then submit_order. Quantity passed through instrument.make_qty(...) for venue rounding.

Synthetic Data + Wrangler

import numpy as np
import pandas as pd
from nautilus_trader.persistence.wranglers import BarDataWrangler
from nautilus_trader.test_kit.providers import TestInstrumentProvider
 
EURUSD = TestInstrumentProvider.default_fx_ccy("EUR/USD")
 
rng = np.random.default_rng(42)
n = 10_000
price = 1.10 + np.cumsum(rng.normal(0, 0.0002, n))
spread = np.abs(rng.normal(0, 0.0003, n))
bars_df = pd.DataFrame(
    {
        "open": price,
        "high": price + spread,
        "low": price - spread,
        "close": price + rng.normal(0, 0.00005, n),
    },
    index=pd.date_range("2024-01-01", periods=n, freq="1min", tz="UTC"),
)
bars_df["high"] = bars_df[["open", "high", "close"]].max(axis=1)
bars_df["low"] = bars_df[["open", "low", "close"]].min(axis=1)
 
bar_type = BarType.from_str("EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL")
bars = BarDataWrangler(bar_type, EURUSD).process(bars_df)

BarType.from_str(...) uses the canonical SYMBOL.VENUE-AGGREGATION-PRICE_TYPE-SOURCE format. BarDataWrangler converts a tz-aware pandas dataframe into engine-native Bar objects.

Engine Setup + Run

from decimal import Decimal
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig, LoggingConfig
from nautilus_trader.model.currencies import USD
from nautilus_trader.model.enums import AccountType, OmsType
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.objects import Money
 
engine = BacktestEngine(
    config=BacktestEngineConfig(
        logging=LoggingConfig(log_level="ERROR"),
    ),
)
 
SIM = Venue("SIM")
engine.add_venue(
    venue=SIM,
    oms_type=OmsType.NETTING,
    account_type=AccountType.MARGIN,
    starting_balances=[Money(1_000_000, USD)],
    base_currency=USD,
    default_leverage=Decimal(1),
)
 
engine.add_instrument(EURUSD)
engine.add_data(bars)
 
strategy = EMACross(
    EMACrossConfig(
        instrument_id=EURUSD.id,
        bar_type=bar_type,
        trade_size=Decimal(100000),
    ),
)
engine.add_strategy(strategy)
 
engine.run()

Reports

engine.trader.generate_account_report(SIM)
engine.trader.generate_positions_report()
engine.trader.generate_order_fills_report()
engine.dispose()

“The engine processes all bars in order, updating indicators and executing trades based on the crossover signal.”

Where the Examples Live

Per the getting-started index, learning materials are organized as:

ResourceLocation
Standalone examplesexamples/ (GitHub)
Notebook tutorialsdocs/tutorials/
Conceptual guides w/ codedocs/concepts/
Built-in strategies/indicatorsnautilus_trader/examples/
Test coveragetests/unit_tests/

Learning Curve - Non-Developer + AI Agent Pairing

For someone non-technical pairing with an AI agent (this project’s setup):

  • Day 1 - Install + first backtest: Real. The single pip install plus the quickstart snippet runs end-to-end. Docker notebook image collapses this to “open browser, paste code.” TTHW (time to hello world) is plausibly under 30 minutes with an agent driving copy/paste.
  • Week 1 - Custom strategy on synthetic data: Reasonable. Strategy class shape is small (config + 4–5 lifecycle hooks). Most friction is data shape (BarType string format, wrangler tz-awareness), not strategy logic.
  • Week 2–4 - Real data + IBKR live path: Where the slope steepens. Live requires the high-level TradingNode API, Parquet catalog, venue adapter config, and IBKR Gateway/TWS pairing - each of which is its own learning surface. The [ib] extra installs the adapter but venue setup is multi-step.
  • Month 2+ - Production-grade with Redis cache, multi-strategy, risk controls: This is the genuine engineering work. Plausible with an AI agent, but the “one node per process” constraint and the event-driven model require thinking carefully about concurrency boundaries.

Net: a non-developer can ship a working backtest in days and a paper-traded strategy in weeks with a competent AI pair, but the live-trading transition (catalog, venue adapters, IBKR auth, runtime ops) is real engineering work that compresses into months, not days.

See also

  • nautilus-concepts.md
  • nautilus-how-to.md
  • nautilus-integrations.md (for IBKR setup)