NautilusTrader
How-To Guides

Run a Backtest (Rust)

Nautilus provides two Rust APIs for backtesting: BacktestEngine (low-level) and BacktestNode (high-level with catalog streaming). This guide covers both.

For background on backtesting concepts, fill models, and matching engine behavior, see the Backtesting concept guide. For project setup and feature flags, see the Rust concept guide.

Dependencies

Add the following to your Cargo.toml. The streaming and nautilus-persistence entries are only needed for the high-level BacktestNode API.

[dependencies]
nautilus-backtest = { version = "0.54", features = ["streaming"] }
nautilus-execution = "0.54"
nautilus-model = { version = "0.54", features = ["stubs"] }
nautilus-persistence = "0.54"
nautilus-trading = { version = "0.54", features = ["examples"] }

ahash = "0.8"
anyhow = "1"
tempfile = "3"
ustr = "1"

If you only need the low-level BacktestEngine, drop streaming, nautilus-persistence, tempfile, and ustr.

BacktestEngine (low-level API)

The low-level API gives direct control: you build the engine, add venues and instruments, load data in memory, register strategies, and run.

Create the engine

use nautilus_backtest::{config::BacktestEngineConfig, engine::BacktestEngine};

let mut engine = BacktestEngine::new(BacktestEngineConfig::default())?;

Add a venue

use ahash::AHashMap;
use nautilus_execution::models::{fee::FeeModelAny, fill::FillModelAny};
use nautilus_model::{
    enums::{AccountType, BookType, OmsType},
    identifiers::Venue,
    types::Money,
};

engine.add_venue(
    Venue::from("SIM"),
    OmsType::Hedging,
    AccountType::Margin,
    BookType::L1_MBP,
    vec![Money::from("1_000_000 USD")],
    None,            // base_currency
    None,            // default_leverage
    AHashMap::new(), // per-instrument leverages
    None,            // margin_model
    vec![],          // simulation modules
    FillModelAny::default(),
    FeeModelAny::default(),
    None, // latency_model
    None, // routing
    None, // reject_stop_orders
    None, // support_gtd_orders
    None, // support_contingent_orders
    None, // use_position_ids
    None, // use_random_ids
    None, // use_reduce_only
    None, // use_message_queue
    None, // use_market_order_acks
    None, // bar_execution
    None, // bar_adaptive_high_low_ordering
    None, // trade_execution
    None, // liquidity_consumption
    None, // allow_cash_borrowing
    None, // frozen_account
    None, // queue_position
    None, // oto_full_trigger
    None, // price_protection_points
)?;

Add instruments and data

use nautilus_model::instruments::{
    Instrument, InstrumentAny, stubs::audusd_sim,
};

let instrument = InstrumentAny::CurrencyPair(audusd_sim());
let instrument_id = instrument.id();
engine.add_instrument(&instrument)?;

let quotes = generate_quotes(instrument_id); // Your data loading function
engine.add_data(quotes, None, true, true);

Register a strategy and run

use nautilus_model::types::Quantity;
use nautilus_trading::examples::strategies::EmaCross;

let strategy = EmaCross::new(
    instrument_id,
    Quantity::from("100000"),
    10, // fast EMA period
    20, // slow EMA period
);

engine.add_strategy(strategy)?;
engine.run(None, None, None, false)?;

Run the full example

cargo run -p nautilus-backtest --features examples --example engine-ema-cross

Source: crates/backtest/examples/engine_ema_cross.rs

BacktestNode (high-level API)

The high-level API loads data from a ParquetDataCatalog and streams in configurable chunk sizes. Requires the streaming feature on nautilus-backtest.

Write data to a catalog

use nautilus_model::instruments::{
    Instrument, InstrumentAny, stubs::audusd_sim,
};
use nautilus_persistence::backend::catalog::ParquetDataCatalog;
use tempfile::TempDir;

let instrument = InstrumentAny::CurrencyPair(audusd_sim());
let instrument_id = instrument.id();
let quotes = generate_quotes(instrument_id);

let temp_dir = TempDir::new()?;
let catalog_path = temp_dir.path().to_str()
    .context("temp dir path is not valid UTF-8")?
    .to_string();
let catalog = ParquetDataCatalog::new(
    temp_dir.path(), None, None, None, None,
);

catalog.write_instruments(vec![instrument])?;
catalog.write_to_parquet(quotes, None, None, None)?;

Configure the run

use nautilus_backtest::config::{
    BacktestDataConfig, BacktestEngineConfig,
    BacktestRunConfig, BacktestVenueConfig, NautilusDataType,
};
use nautilus_model::enums::{AccountType, BookType, OmsType};
use ustr::Ustr;

let venue_config = BacktestVenueConfig::new(
    Ustr::from("SIM"),
    OmsType::Hedging,
    AccountType::Margin,
    BookType::L1_MBP,
    None, // routing
    None, // frozen_account
    None, // reject_stop_orders
    None, // support_gtd_orders
    None, // support_contingent_orders
    None, // use_position_ids
    None, // use_random_ids
    None, // use_reduce_only
    None, // bar_execution
    None, // bar_adaptive_high_low_ordering
    None, // trade_execution
    None, // use_market_order_acks
    None, // liquidity_consumption
    None, // allow_cash_borrowing
    None, // queue_position
    None, // oto_trigger_mode
    vec!["1_000_000 USD".to_string()],
    None, // base_currency
    None, // default_leverage
    None, // leverages
    None, // price_protection_points
);

let data_config = BacktestDataConfig::new(
    NautilusDataType::QuoteTick,
    catalog_path,
    None, // catalog_fs_protocol
    None, // catalog_fs_storage_options
    Some(instrument_id),
    None, // instrument_ids
    None, // start_time
    None, // end_time
    None, // filter_expr
    None, // client_id
    None, // metadata
    None, // bar_spec
    None, // bar_types
    None, // optimize_file_loading
);

let run_config = BacktestRunConfig::new(
    Some("ema-cross-run".to_string()),
    vec![venue_config],
    vec![data_config],
    BacktestEngineConfig::default(),
    Some(100), // Stream in chunks of 100
    None,      // dispose_on_completion
    None,      // start
    None,      // end
);

Build, add strategies, and run

use nautilus_backtest::node::BacktestNode;
use nautilus_model::types::Quantity;
use nautilus_trading::examples::strategies::EmaCross;

let mut node = BacktestNode::new(vec![run_config])?;
node.build()?;

let engine = node.get_engine_mut("ema-cross-run")
    .context("engine not found for run config ID")?;
let strategy = EmaCross::new(
    instrument_id,
    Quantity::from("100000"),
    10,
    20,
);
engine.add_strategy(strategy)?;

node.run()?;

Run the full example

cargo run -p nautilus-backtest --features examples,streaming --example node-ema-cross

Source: crates/backtest/examples/node_ema_cross.rs

On this page