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_traderpackage 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_traderA 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/simplePre-release / development wheels:
uv pip install nautilus_trader --pre --index-url=https://packages.nautechsystems.io/simpleDocker 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:latestBuilding 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-extrasLinux/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 -
StrategyConfigis the base. - Indicator registration via
register_indicator_for_bars- engine pushes bars into the indicator automatically, no manualupdate()calls. on_baris the event handler;indicators_initialized()gates trading until warmup completes.- Order construction via
self.order_factory.market(...)thensubmit_order. Quantity passed throughinstrument.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:
| Resource | Location |
|---|---|
| Standalone examples | examples/ (GitHub) |
| Notebook tutorials | docs/tutorials/ |
| Conceptual guides w/ code | docs/concepts/ |
| Built-in strategies/indicators | nautilus_trader/examples/ |
| Test coverage | tests/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 installplus 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
(
BarTypestring format, wrangler tz-awareness), not strategy logic. - Week 2–4 - Real data + IBKR live path: Where the slope steepens. Live
requires the high-level
TradingNodeAPI, 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)