nautilus_backtest/
engine.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16// Under development
17#![allow(dead_code)]
18#![allow(unused_variables)]
19
20//! The core `BacktestEngine` for backtesting on historical data.
21
22use std::{
23    any::Any,
24    cell::RefCell,
25    collections::{HashMap, HashSet, VecDeque},
26    fmt::Debug,
27    rc::Rc,
28};
29
30use nautilus_core::{UUID4, UnixNanos};
31use nautilus_data::client::DataClientAdapter;
32use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel, latency::LatencyModel};
33use nautilus_model::{
34    data::Data,
35    enums::{AccountType, BookType, OmsType},
36    identifiers::{AccountId, ClientId, InstrumentId, Venue},
37    instruments::{Instrument, InstrumentAny},
38    types::{Currency, Money},
39};
40use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
41use rust_decimal::Decimal;
42
43use crate::{
44    accumulator::TimeEventAccumulator, config::BacktestEngineConfig,
45    data_client::BacktestDataClient, exchange::SimulatedExchange,
46    execution_client::BacktestExecutionClient, modules::SimulationModule,
47};
48
49/// Core backtesting engine for running event-driven strategy backtests on historical data.
50///
51/// The `BacktestEngine` provides a high-fidelity simulation environment that processes
52/// historical market data chronologically through an event-driven architecture. It maintains
53/// simulated exchanges with realistic order matching and execution, allowing strategies
54/// to be tested exactly as they would run in live trading:
55///
56/// - Event-driven data replay with configurable latency models.
57/// - Multi-venue and multi-asset support.
58/// - Realistic order matching and execution simulation.
59/// - Strategy and portfolio performance analysis.
60/// - Seamless transition from backtesting to live trading.
61pub struct BacktestEngine {
62    instance_id: UUID4,
63    config: BacktestEngineConfig,
64    kernel: NautilusKernel,
65    accumulator: TimeEventAccumulator,
66    run_config_id: Option<UUID4>,
67    run_id: Option<UUID4>,
68    venues: HashMap<Venue, Rc<RefCell<SimulatedExchange>>>,
69    has_data: HashSet<InstrumentId>,
70    has_book_data: HashSet<InstrumentId>,
71    data: VecDeque<Data>,
72    index: usize,
73    iteration: usize,
74    run_started: Option<UnixNanos>,
75    run_finished: Option<UnixNanos>,
76    backtest_start: Option<UnixNanos>,
77    backtest_end: Option<UnixNanos>,
78}
79
80impl Debug for BacktestEngine {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct(stringify!(BacktestEngine))
83            .field("instance_id", &self.instance_id)
84            .field("run_config_id", &self.run_config_id)
85            .field("run_id", &self.run_id)
86            .finish()
87    }
88}
89
90impl BacktestEngine {
91    /// Create a new [`BacktestEngine`] instance.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the core `NautilusKernel` fails to initialize.
96    pub fn new(config: BacktestEngineConfig) -> anyhow::Result<Self> {
97        let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
98
99        Ok(Self {
100            instance_id: kernel.instance_id,
101            config,
102            accumulator: TimeEventAccumulator::new(),
103            kernel,
104            run_config_id: None,
105            run_id: None,
106            venues: HashMap::new(),
107            has_data: HashSet::new(),
108            has_book_data: HashSet::new(),
109            data: VecDeque::new(),
110            index: 0,
111            iteration: 0,
112            run_started: None,
113            run_finished: None,
114            backtest_start: None,
115            backtest_end: None,
116        })
117    }
118
119    /// # Errors
120    ///
121    /// Returns an error if initializing the simulated exchange for the venue fails.
122    #[allow(clippy::too_many_arguments)]
123    pub fn add_venue(
124        &mut self,
125        venue: Venue,
126        oms_type: OmsType,
127        account_type: AccountType,
128        book_type: BookType,
129        starting_balances: Vec<Money>,
130        base_currency: Option<Currency>,
131        default_leverage: Option<Decimal>,
132        leverages: HashMap<InstrumentId, Decimal>,
133        modules: Vec<Box<dyn SimulationModule>>,
134        fill_model: FillModel,
135        fee_model: FeeModelAny,
136        latency_model: Option<LatencyModel>,
137        routing: Option<bool>,
138        frozen_account: Option<bool>,
139        reject_stop_orders: Option<bool>,
140        support_gtd_orders: Option<bool>,
141        support_contingent_orders: Option<bool>,
142        use_position_ids: Option<bool>,
143        use_random_ids: Option<bool>,
144        use_reduce_only: Option<bool>,
145        use_message_queue: Option<bool>,
146        bar_execution: Option<bool>,
147        bar_adaptive_high_low_ordering: Option<bool>,
148        trade_execution: Option<bool>,
149    ) -> anyhow::Result<()> {
150        let default_leverage: Decimal = default_leverage.unwrap_or_else(|| {
151            if account_type == AccountType::Margin {
152                Decimal::from(10)
153            } else {
154                Decimal::from(0)
155            }
156        });
157
158        let exchange = SimulatedExchange::new(
159            venue,
160            oms_type,
161            account_type,
162            starting_balances,
163            base_currency,
164            default_leverage,
165            leverages,
166            modules,
167            self.kernel.cache.clone(),
168            self.kernel.clock.clone(),
169            fill_model,
170            fee_model,
171            book_type,
172            latency_model,
173            frozen_account,
174            bar_execution,
175            reject_stop_orders,
176            support_gtd_orders,
177            support_contingent_orders,
178            use_position_ids,
179            use_random_ids,
180            use_reduce_only,
181            use_message_queue,
182        )?;
183        let exchange = Rc::new(RefCell::new(exchange));
184        self.venues.insert(venue, exchange.clone());
185
186        let account_id = AccountId::from(format!("{venue}-001").as_str());
187        let exec_client = BacktestExecutionClient::new(
188            self.config.trader_id(),
189            account_id,
190            exchange.clone(),
191            self.kernel.cache.clone(),
192            self.kernel.clock.clone(),
193            routing,
194            frozen_account,
195        );
196        let exec_client = Rc::new(exec_client);
197
198        exchange.borrow_mut().register_client(exec_client.clone());
199        self.kernel.exec_engine.register_client(exec_client)?;
200
201        log::info!("Adding exchange {venue} to engine");
202
203        Ok(())
204    }
205
206    pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModel) {
207        todo!("implement change_fill_model")
208    }
209
210    /// Adds an instrument to the backtest engine for the specified venue.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if:
215    /// - The instrument's associated venue has not been added via `add_venue`.
216    /// - Attempting to add a `CurrencyPair` instrument for a single-currency CASH account.
217    ///
218    /// # Panics
219    ///
220    /// Panics if adding the instrument to the simulated exchange fails.
221    pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
222        let instrument_id = instrument.id();
223        if let Some(exchange) = self.venues.get_mut(&instrument.id().venue) {
224            // check if instrument is of variant CurrencyPair
225            if matches!(instrument, InstrumentAny::CurrencyPair(_))
226                && exchange.borrow().account_type != AccountType::Margin
227                && exchange.borrow().base_currency.is_some()
228            {
229                anyhow::bail!(
230                    "Cannot add a `CurrencyPair` instrument {} for a venue with a single-currency CASH account",
231                    instrument_id
232                )
233            }
234            exchange
235                .borrow_mut()
236                .add_instrument(instrument.clone())
237                .unwrap();
238        } else {
239            anyhow::bail!(
240                "Cannot add an `Instrument` object without first adding its associated venue {}",
241                instrument.id().venue
242            )
243        }
244
245        // Check client has been registered
246        self.add_market_data_client_if_not_exists(instrument.id().venue);
247
248        self.kernel
249            .data_engine
250            .borrow_mut()
251            .process(&instrument as &dyn Any);
252        log::info!(
253            "Added instrument {} to exchange {}",
254            instrument_id,
255            instrument_id.venue
256        );
257        Ok(())
258    }
259
260    pub fn add_data(
261        &mut self,
262        data: Vec<Data>,
263        client_id: Option<ClientId>,
264        validate: bool,
265        sort: bool,
266    ) {
267        todo!("implement add_data")
268    }
269
270    pub fn add_actor(&mut self) {
271        todo!("implement add_actor")
272    }
273
274    pub fn add_actors(&mut self) {
275        todo!("implement add_actors")
276    }
277
278    pub fn add_strategy(&mut self) {
279        todo!("implement add_strategy")
280    }
281
282    pub fn add_strategies(&mut self) {
283        todo!("implement add_strategies")
284    }
285
286    pub fn add_exec_algorithm(&mut self) {
287        todo!("implement add_exec_algorithm")
288    }
289
290    pub fn add_exec_algorithms(&mut self) {
291        todo!("implement add_exec_algorithms")
292    }
293
294    pub fn reset(&mut self) {
295        todo!("implement reset")
296    }
297
298    pub fn clear_data(&mut self) {
299        todo!("implement clear_data")
300    }
301
302    pub fn clear_strategies(&mut self) {
303        todo!("implement clear_strategies")
304    }
305
306    pub fn clear_exec_algorithms(&mut self) {
307        todo!("implement clear_exec_algorithms")
308    }
309
310    pub fn dispose(&mut self) {
311        todo!("implement dispose")
312    }
313
314    pub fn run(&mut self) {
315        todo!("implement run")
316    }
317
318    pub fn end(&mut self) {
319        todo!("implement end")
320    }
321
322    pub fn get_result(&self) {
323        todo!("implement get_result")
324    }
325
326    pub fn next(&mut self) {
327        todo!("implement next")
328    }
329
330    pub fn advance_time(&mut self) {
331        todo!("implement advance_time")
332    }
333
334    pub fn process_raw_time_event_handlers(&mut self) {
335        todo!("implement process_raw_time_event_handlers")
336    }
337
338    pub fn log_pre_run(&self) {
339        todo!("implement log_pre_run_diagnostics")
340    }
341
342    pub fn log_run(&self) {
343        todo!("implement log_run")
344    }
345
346    pub fn log_post_run(&self) {
347        todo!("implement log_post_run")
348    }
349
350    pub fn add_data_client_if_not_exists(&mut self) {
351        todo!("implement add_data_client_if_not_exists")
352    }
353
354    // TODO: We might want venue to be optional for multi-venue clients
355    pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
356        let client_id = ClientId::from(venue.as_str());
357        if !self
358            .kernel
359            .data_engine
360            .borrow()
361            .registered_clients()
362            .contains(&client_id)
363        {
364            let backtest_client =
365                BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
366            let data_client_adapter = DataClientAdapter::new(
367                client_id,
368                Some(venue), // TBD
369                false,
370                false,
371                Box::new(backtest_client),
372            );
373            self.kernel
374                .data_engine
375                .borrow_mut()
376                .register_client(data_client_adapter, None);
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::collections::HashMap;
384
385    use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel};
386    use nautilus_model::{
387        enums::{AccountType, BookType, OmsType},
388        identifiers::{ClientId, Venue},
389        instruments::{
390            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
391        },
392        types::Money,
393    };
394    use rstest::rstest;
395
396    use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
397
398    #[allow(clippy::missing_panics_doc)] // OK for testing
399    fn get_backtest_engine(config: Option<BacktestEngineConfig>) -> BacktestEngine {
400        let config = config.unwrap_or_default();
401        let mut engine = BacktestEngine::new(config).unwrap();
402        engine
403            .add_venue(
404                Venue::from("BINANCE"),
405                OmsType::Netting,
406                AccountType::Margin,
407                BookType::L2_MBP,
408                vec![Money::from("1_000_000 USD")],
409                None,
410                None,
411                HashMap::new(),
412                vec![],
413                FillModel::default(),
414                FeeModelAny::default(),
415                None,
416                None,
417                None,
418                None,
419                None,
420                None,
421                None,
422                None,
423                None,
424                None,
425                None,
426                None,
427                None,
428            )
429            .unwrap();
430        engine
431    }
432
433    #[rstest]
434    fn test_engine_venue_and_instrument_initialization(crypto_perpetual_ethusdt: CryptoPerpetual) {
435        pyo3::prepare_freethreaded_python();
436
437        let venue = Venue::from("BINANCE");
438        let client_id = ClientId::from(venue.as_str());
439        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
440        let instrument_id = instrument.id();
441        let mut engine = get_backtest_engine(None);
442        engine.add_instrument(instrument).unwrap();
443
444        // Check the venue and exec client has been added
445        assert_eq!(engine.venues.len(), 1);
446        assert!(engine.venues.contains_key(&venue));
447        assert!(engine.kernel.exec_engine.get_client(&client_id).is_some());
448
449        // Check the instrument has been added
450        assert!(
451            engine
452                .venues
453                .get(&venue)
454                .is_some_and(|venue| venue.borrow().get_matching_engine(&instrument_id).is_some())
455        );
456        assert_eq!(
457            engine
458                .kernel
459                .data_engine
460                .borrow()
461                .registered_clients()
462                .len(),
463            1
464        );
465        assert!(
466            engine
467                .kernel
468                .data_engine
469                .borrow()
470                .registered_clients()
471                .contains(&client_id)
472        );
473    }
474}