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-crossSource:
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