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_common::timer::TimeEventHandlerV2;
31use nautilus_core::{UUID4, UnixNanos};
32use nautilus_data::client::DataClientAdapter;
33use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel, latency::LatencyModel};
34use nautilus_model::{
35    data::Data,
36    enums::{AccountType, BookType, OmsType},
37    identifiers::{AccountId, ClientId, InstrumentId, Venue},
38    instruments::{Instrument, InstrumentAny},
39    types::{Currency, Money},
40};
41use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
42use rust_decimal::Decimal;
43
44use crate::{
45    accumulator::TimeEventAccumulator, config::BacktestEngineConfig,
46    data_client::BacktestDataClient, exchange::SimulatedExchange,
47    execution_client::BacktestExecutionClient, modules::SimulationModule,
48};
49
50/// Core backtesting engine for running event-driven strategy backtests on historical data.
51///
52/// The `BacktestEngine` provides a high-fidelity simulation environment that processes
53/// historical market data chronologically through an event-driven architecture. It maintains
54/// simulated exchanges with realistic order matching and execution, allowing strategies
55/// to be tested exactly as they would run in live trading:
56///
57/// - Event-driven data replay with configurable latency models.
58/// - Multi-venue and multi-asset support.
59/// - Realistic order matching and execution simulation.
60/// - Strategy and portfolio performance analysis.
61/// - Seamless transition from backtesting to live trading.
62pub struct BacktestEngine {
63    instance_id: UUID4,
64    config: BacktestEngineConfig,
65    kernel: NautilusKernel,
66    accumulator: TimeEventAccumulator,
67    run_config_id: Option<UUID4>,
68    run_id: Option<UUID4>,
69    venues: HashMap<Venue, Rc<RefCell<SimulatedExchange>>>,
70    has_data: HashSet<InstrumentId>,
71    has_book_data: HashSet<InstrumentId>,
72    data: VecDeque<Data>,
73    index: usize,
74    iteration: usize,
75    run_started: Option<UnixNanos>,
76    run_finished: Option<UnixNanos>,
77    backtest_start: Option<UnixNanos>,
78    backtest_end: Option<UnixNanos>,
79}
80
81impl Debug for BacktestEngine {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct(stringify!(BacktestEngine))
84            .field("instance_id", &self.instance_id)
85            .field("run_config_id", &self.run_config_id)
86            .field("run_id", &self.run_id)
87            .finish()
88    }
89}
90
91impl BacktestEngine {
92    /// Create a new [`BacktestEngine`] instance.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the core `NautilusKernel` fails to initialize.
97    pub fn new(config: BacktestEngineConfig) -> anyhow::Result<Self> {
98        let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
99
100        Ok(Self {
101            instance_id: kernel.instance_id,
102            config,
103            accumulator: TimeEventAccumulator::new(),
104            kernel,
105            run_config_id: None,
106            run_id: None,
107            venues: HashMap::new(),
108            has_data: HashSet::new(),
109            has_book_data: HashSet::new(),
110            data: VecDeque::new(),
111            index: 0,
112            iteration: 0,
113            run_started: None,
114            run_finished: None,
115            backtest_start: None,
116            backtest_end: None,
117        })
118    }
119
120    /// # Errors
121    ///
122    /// Returns an error if initializing the simulated exchange for the venue fails.
123    #[allow(clippy::too_many_arguments)]
124    pub fn add_venue(
125        &mut self,
126        venue: Venue,
127        oms_type: OmsType,
128        account_type: AccountType,
129        book_type: BookType,
130        starting_balances: Vec<Money>,
131        base_currency: Option<Currency>,
132        default_leverage: Option<Decimal>,
133        leverages: HashMap<InstrumentId, Decimal>,
134        modules: Vec<Box<dyn SimulationModule>>,
135        fill_model: FillModel,
136        fee_model: FeeModelAny,
137        latency_model: Option<LatencyModel>,
138        routing: 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        allow_cash_borrowing: Option<bool>,
150        frozen_account: Option<bool>,
151        price_protection_points: Option<u32>,
152    ) -> anyhow::Result<()> {
153        let default_leverage: Decimal = default_leverage.unwrap_or_else(|| {
154            if account_type == AccountType::Margin {
155                Decimal::from(10)
156            } else {
157                Decimal::from(0)
158            }
159        });
160
161        let exchange = SimulatedExchange::new(
162            venue,
163            oms_type,
164            account_type,
165            starting_balances,
166            base_currency,
167            default_leverage,
168            leverages,
169            modules,
170            self.kernel.cache.clone(),
171            self.kernel.clock.clone(),
172            fill_model,
173            fee_model,
174            book_type,
175            latency_model,
176            bar_execution,
177            reject_stop_orders,
178            support_gtd_orders,
179            support_contingent_orders,
180            use_position_ids,
181            use_random_ids,
182            use_reduce_only,
183            use_message_queue,
184            allow_cash_borrowing,
185            frozen_account,
186            price_protection_points,
187        )?;
188        let exchange = Rc::new(RefCell::new(exchange));
189        self.venues.insert(venue, exchange.clone());
190
191        let account_id = AccountId::from(format!("{venue}-001").as_str());
192        let exec_client = BacktestExecutionClient::new(
193            self.config.trader_id(),
194            account_id,
195            exchange.clone(),
196            self.kernel.cache.clone(),
197            self.kernel.clock.clone(),
198            routing,
199            frozen_account,
200        );
201        let exec_client = Rc::new(exec_client);
202
203        exchange.borrow_mut().register_client(exec_client.clone());
204        self.kernel.exec_engine.register_client(exec_client)?;
205
206        log::info!("Adding exchange {venue} to engine");
207
208        Ok(())
209    }
210
211    pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModel) {
212        if let Some(exchange) = self.venues.get_mut(&venue) {
213            exchange.borrow_mut().set_fill_model(fill_model);
214        } else {
215            log::warn!(
216                "BacktestEngine::change_fill_model called for unknown venue {venue}. Ignoring."
217            );
218        }
219    }
220
221    /// Adds an instrument to the backtest engine for the specified venue.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if:
226    /// - The instrument's associated venue has not been added via `add_venue`.
227    /// - Attempting to add a `CurrencyPair` instrument for a single-currency CASH account.
228    ///
229    /// # Panics
230    ///
231    /// Panics if adding the instrument to the simulated exchange fails.
232    pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
233        let instrument_id = instrument.id();
234        if let Some(exchange) = self.venues.get_mut(&instrument.id().venue) {
235            // check if instrument is of variant CurrencyPair
236            if matches!(instrument, InstrumentAny::CurrencyPair(_))
237                && exchange.borrow().account_type != AccountType::Margin
238                && exchange.borrow().base_currency.is_some()
239            {
240                anyhow::bail!(
241                    "Cannot add a `CurrencyPair` instrument {} for a venue with a single-currency CASH account",
242                    instrument_id
243                )
244            }
245            exchange
246                .borrow_mut()
247                .add_instrument(instrument.clone())
248                .unwrap();
249        } else {
250            anyhow::bail!(
251                "Cannot add an `Instrument` object without first adding its associated venue {}",
252                instrument.id().venue
253            )
254        }
255
256        // Check client has been registered
257        self.add_market_data_client_if_not_exists(instrument.id().venue);
258
259        self.kernel
260            .data_engine
261            .borrow_mut()
262            .process(&instrument as &dyn Any);
263        log::info!(
264            "Added instrument {} to exchange {}",
265            instrument_id,
266            instrument_id.venue
267        );
268        Ok(())
269    }
270
271    pub fn add_data(
272        &mut self,
273        data: Vec<Data>,
274        client_id: Option<ClientId>,
275        validate: bool,
276        sort: bool,
277    ) {
278        if data.is_empty() {
279            log::warn!("add_data called with empty data slice – ignoring");
280            return;
281        }
282
283        // If requested, sort by ts_init so internal stream is monotonic.
284        let mut to_add = data;
285        if sort {
286            to_add.sort_by_key(nautilus_model::data::HasTsInit::ts_init);
287        }
288
289        // Instrument & book tracking using Data helpers
290        if validate {
291            for item in &to_add {
292                let instr_id = item.instrument_id();
293                self.has_data.insert(instr_id);
294
295                if item.is_order_book_data() {
296                    self.has_book_data.insert(instr_id);
297                }
298
299                // Ensure appropriate market data client exists
300                self.add_market_data_client_if_not_exists(instr_id.venue);
301            }
302        }
303
304        // Extend master data vector and ensure internal iterator (index) remains valid.
305        for item in to_add {
306            self.data.push_back(item);
307        }
308
309        if sort {
310            // VecDeque cannot be sorted directly; convert to Vec for sorting, then back.
311            let mut vec: Vec<Data> = self.data.drain(..).collect();
312            vec.sort_by_key(nautilus_model::data::HasTsInit::ts_init);
313            self.data = vec.into();
314        }
315
316        log::info!(
317            "Added {} data element{} to BacktestEngine",
318            self.data.len(),
319            if self.data.len() == 1 { "" } else { "s" }
320        );
321    }
322
323    pub fn add_actor(&mut self) {
324        todo!("implement add_actor")
325    }
326
327    pub fn add_actors(&mut self) {
328        todo!("implement add_actors")
329    }
330
331    pub fn add_strategy(&mut self) {
332        todo!("implement add_strategy")
333    }
334
335    pub fn add_strategies(&mut self) {
336        todo!("implement add_strategies")
337    }
338
339    pub fn add_exec_algorithm(&mut self) {
340        todo!("implement add_exec_algorithm")
341    }
342
343    pub fn add_exec_algorithms(&mut self) {
344        todo!("implement add_exec_algorithms")
345    }
346
347    pub fn reset(&mut self) {
348        todo!("implement reset")
349    }
350
351    pub fn clear_data(&mut self) {
352        todo!("implement clear_data")
353    }
354
355    pub fn clear_strategies(&mut self) {
356        todo!("implement clear_strategies")
357    }
358
359    pub fn clear_exec_algorithms(&mut self) {
360        todo!("implement clear_exec_algorithms")
361    }
362
363    pub fn dispose(&mut self) {
364        todo!("implement dispose")
365    }
366
367    pub fn run(&mut self) {
368        todo!("implement run")
369    }
370
371    pub fn end(&mut self) {
372        todo!("implement end")
373    }
374
375    pub fn get_result(&self) {
376        // TODO: implement full BacktestResult aggregation once portfolio analysis
377        // components are available in Rust. For now we simply log and return.
378        log::info!("BacktestEngine::get_result called – not yet implemented");
379    }
380
381    pub fn next(&mut self) {
382        self.data.pop_front();
383    }
384
385    pub fn advance_time(&mut self, _ts_now: UnixNanos) -> Vec<TimeEventHandlerV2> {
386        // TODO: integrate TestClock advancement when kernel clocks are exposed.
387        self.accumulator.drain()
388    }
389
390    pub fn process_raw_time_event_handlers(
391        &mut self,
392        handlers: Vec<TimeEventHandlerV2>,
393        ts_now: UnixNanos,
394        only_now: bool,
395        as_of_now: bool,
396    ) {
397        let mut last_ts_init: Option<UnixNanos> = None;
398
399        for handler in handlers {
400            let ts_event_init = handler.event.ts_event; // event time
401
402            if Self::should_skip_time_event(ts_event_init, ts_now, only_now, as_of_now) {
403                continue;
404            }
405
406            if last_ts_init != Some(ts_event_init) {
407                // First handler for this timestamp – process exchange queues beforehand.
408                for exchange in self.venues.values() {
409                    exchange.borrow_mut().process(ts_event_init);
410                }
411                last_ts_init = Some(ts_event_init);
412            }
413
414            handler.run();
415        }
416    }
417
418    pub fn log_pre_run(&self) {
419        todo!("implement log_pre_run_diagnostics")
420    }
421
422    pub fn log_run(&self) {
423        todo!("implement log_run")
424    }
425
426    pub fn log_post_run(&self) {
427        todo!("implement log_post_run")
428    }
429
430    pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
431        if self
432            .kernel
433            .data_engine
434            .borrow()
435            .registered_clients()
436            .contains(&client_id)
437        {
438            return;
439        }
440
441        // Create a generic, venue-agnostic backtest data client. We use a dummy
442        // venue derived from the client id for uniqueness.
443        let venue = Venue::from(client_id.as_str());
444        let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
445        let data_client_adapter = DataClientAdapter::new(
446            backtest_client.client_id,
447            None, // no specific venue association
448            false,
449            false,
450            Box::new(backtest_client),
451        );
452
453        self.kernel
454            .data_engine
455            .borrow_mut()
456            .register_client(data_client_adapter, None);
457    }
458
459    // Helper matching Cython semantics for determining whether to skip
460    // processing a time event.
461    fn should_skip_time_event(
462        ts_event_init: UnixNanos,
463        ts_now: UnixNanos,
464        only_now: bool,
465        as_of_now: bool,
466    ) -> bool {
467        if only_now {
468            ts_event_init != ts_now
469        } else if as_of_now {
470            ts_event_init > ts_now
471        } else {
472            ts_event_init >= ts_now
473        }
474    }
475
476    // TODO: We might want venue to be optional for multi-venue clients
477    pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
478        let client_id = ClientId::from(venue.as_str());
479        if !self
480            .kernel
481            .data_engine
482            .borrow()
483            .registered_clients()
484            .contains(&client_id)
485        {
486            let backtest_client =
487                BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
488            let data_client_adapter = DataClientAdapter::new(
489                client_id,
490                Some(venue), // TBD
491                false,
492                false,
493                Box::new(backtest_client),
494            );
495            self.kernel
496                .data_engine
497                .borrow_mut()
498                .register_client(data_client_adapter, None);
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use std::collections::HashMap;
506
507    use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel};
508    use nautilus_model::{
509        enums::{AccountType, BookType, OmsType},
510        identifiers::{ClientId, Venue},
511        instruments::{
512            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
513        },
514        types::Money,
515    };
516    use rstest::rstest;
517
518    use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
519
520    #[allow(clippy::missing_panics_doc)]
521    fn get_backtest_engine(config: Option<BacktestEngineConfig>) -> BacktestEngine {
522        let config = config.unwrap_or_default();
523        let mut engine = BacktestEngine::new(config).unwrap();
524        engine
525            .add_venue(
526                Venue::from("BINANCE"),
527                OmsType::Netting,
528                AccountType::Margin,
529                BookType::L2_MBP,
530                vec![Money::from("1_000_000 USD")],
531                None,
532                None,
533                HashMap::new(),
534                vec![],
535                FillModel::default(),
536                FeeModelAny::default(),
537                None,
538                None,
539                None,
540                None,
541                None,
542                None,
543                None,
544                None,
545                None,
546                None,
547                None,
548                None,
549                None,
550                None,
551                None,
552            )
553            .unwrap();
554        engine
555    }
556
557    #[rstest]
558    fn test_engine_venue_and_instrument_initialization(crypto_perpetual_ethusdt: CryptoPerpetual) {
559        let venue = Venue::from("BINANCE");
560        let client_id = ClientId::from(venue.as_str());
561        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
562        let instrument_id = instrument.id();
563        let mut engine = get_backtest_engine(None);
564        engine.add_instrument(instrument).unwrap();
565
566        // Check the venue and exec client has been added
567        assert_eq!(engine.venues.len(), 1);
568        assert!(engine.venues.contains_key(&venue));
569        assert!(engine.kernel.exec_engine.get_client(&client_id).is_some());
570
571        // Check the instrument has been added
572        assert!(
573            engine
574                .venues
575                .get(&venue)
576                .is_some_and(|venue| venue.borrow().get_matching_engine(&instrument_id).is_some())
577        );
578        assert_eq!(
579            engine
580                .kernel
581                .data_engine
582                .borrow()
583                .registered_clients()
584                .len(),
585            1
586        );
587        assert!(
588            engine
589                .kernel
590                .data_engine
591                .borrow()
592                .registered_clients()
593                .contains(&client_id)
594        );
595    }
596}