nautilus_backtest/
engine.rs

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