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
49pub struct BacktestEngine {
50    instance_id: UUID4,
51    config: BacktestEngineConfig,
52    kernel: NautilusKernel,
53    accumulator: TimeEventAccumulator,
54    run_config_id: Option<UUID4>,
55    run_id: Option<UUID4>,
56    venues: HashMap<Venue, Rc<RefCell<SimulatedExchange>>>,
57    has_data: HashSet<InstrumentId>,
58    has_book_data: HashSet<InstrumentId>,
59    data: VecDeque<Data>,
60    index: usize,
61    iteration: usize,
62    run_started: Option<UnixNanos>,
63    run_finished: Option<UnixNanos>,
64    backtest_start: Option<UnixNanos>,
65    backtest_end: Option<UnixNanos>,
66}
67
68impl Debug for BacktestEngine {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct(stringify!(BacktestEngine))
71            .field("instance_id", &self.instance_id)
72            .field("run_config_id", &self.run_config_id)
73            .field("run_id", &self.run_id)
74            .finish()
75    }
76}
77
78impl BacktestEngine {
79    /// Create a new [`BacktestEngine`] instance.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if the core `NautilusKernel` fails to initialize.
84    pub fn new(config: BacktestEngineConfig) -> anyhow::Result<Self> {
85        let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
86
87        Ok(Self {
88            instance_id: kernel.instance_id,
89            config,
90            accumulator: TimeEventAccumulator::new(),
91            kernel,
92            run_config_id: None,
93            run_id: None,
94            venues: HashMap::new(),
95            has_data: HashSet::new(),
96            has_book_data: HashSet::new(),
97            data: VecDeque::new(),
98            index: 0,
99            iteration: 0,
100            run_started: None,
101            run_finished: None,
102            backtest_start: None,
103            backtest_end: None,
104        })
105    }
106
107    /// # Errors
108    ///
109    /// Returns an error if initializing the simulated exchange for the venue fails.
110    #[allow(clippy::too_many_arguments)]
111    pub fn add_venue(
112        &mut self,
113        venue: Venue,
114        oms_type: OmsType,
115        account_type: AccountType,
116        book_type: BookType,
117        starting_balances: Vec<Money>,
118        base_currency: Option<Currency>,
119        default_leverage: Option<Decimal>,
120        leverages: HashMap<InstrumentId, Decimal>,
121        modules: Vec<Box<dyn SimulationModule>>,
122        fill_model: FillModel,
123        fee_model: FeeModelAny,
124        latency_model: Option<LatencyModel>,
125        routing: Option<bool>,
126        frozen_account: Option<bool>,
127        reject_stop_orders: Option<bool>,
128        support_gtd_orders: Option<bool>,
129        support_contingent_orders: Option<bool>,
130        use_position_ids: Option<bool>,
131        use_random_ids: Option<bool>,
132        use_reduce_only: Option<bool>,
133        use_message_queue: Option<bool>,
134        bar_execution: Option<bool>,
135        bar_adaptive_high_low_ordering: Option<bool>,
136        trade_execution: Option<bool>,
137    ) -> anyhow::Result<()> {
138        let default_leverage: Decimal = default_leverage.unwrap_or_else(|| {
139            if account_type == AccountType::Margin {
140                Decimal::from(10)
141            } else {
142                Decimal::from(0)
143            }
144        });
145
146        let exchange = SimulatedExchange::new(
147            venue,
148            oms_type,
149            account_type,
150            starting_balances,
151            base_currency,
152            default_leverage,
153            leverages,
154            modules,
155            self.kernel.cache.clone(),
156            self.kernel.clock.clone(),
157            fill_model,
158            fee_model,
159            book_type,
160            latency_model,
161            frozen_account,
162            bar_execution,
163            reject_stop_orders,
164            support_gtd_orders,
165            support_contingent_orders,
166            use_position_ids,
167            use_random_ids,
168            use_reduce_only,
169            use_message_queue,
170        )?;
171        let exchange = Rc::new(RefCell::new(exchange));
172        self.venues.insert(venue, exchange.clone());
173
174        let account_id = AccountId::from(format!("{venue}-001").as_str());
175        let exec_client = BacktestExecutionClient::new(
176            self.config.trader_id(),
177            account_id,
178            exchange.clone(),
179            self.kernel.cache.clone(),
180            self.kernel.clock.clone(),
181            routing,
182            frozen_account,
183        );
184        let exec_client = Rc::new(exec_client);
185
186        exchange.borrow_mut().register_client(exec_client.clone());
187        self.kernel.exec_engine.register_client(exec_client)?;
188
189        log::info!("Adding exchange {venue} to engine");
190
191        Ok(())
192    }
193
194    pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModel) {
195        todo!("implement change_fill_model")
196    }
197
198    /// Adds an instrument to the backtest engine for the specified venue.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if:
203    /// - The instrument's associated venue has not been added via `add_venue`.
204    /// - Attempting to add a `CurrencyPair` instrument for a single-currency CASH account.
205    ///
206    /// # Panics
207    ///
208    /// Panics if adding the instrument to the simulated exchange fails.
209    pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
210        let instrument_id = instrument.id();
211        if let Some(exchange) = self.venues.get_mut(&instrument.id().venue) {
212            // check if instrument is of variant CurrencyPair
213            if matches!(instrument, InstrumentAny::CurrencyPair(_))
214                && exchange.borrow().account_type != AccountType::Margin
215                && exchange.borrow().base_currency.is_some()
216            {
217                anyhow::bail!(
218                    "Cannot add a `CurrencyPair` instrument {} for a venue with a single-currency CASH account",
219                    instrument_id
220                )
221            }
222            exchange
223                .borrow_mut()
224                .add_instrument(instrument.clone())
225                .unwrap();
226        } else {
227            anyhow::bail!(
228                "Cannot add an `Instrument` object without first adding its associated venue {}",
229                instrument.id().venue
230            )
231        }
232
233        // Check client has been registered
234        self.add_market_data_client_if_not_exists(instrument.id().venue);
235
236        self.kernel.data_engine.process(&instrument as &dyn Any);
237        log::info!(
238            "Added instrument {} to exchange {}",
239            instrument_id,
240            instrument_id.venue
241        );
242        Ok(())
243    }
244
245    pub fn add_data(
246        &mut self,
247        data: Vec<Data>,
248        client_id: Option<ClientId>,
249        validate: bool,
250        sort: bool,
251    ) {
252        todo!("implement add_data")
253    }
254
255    pub fn add_actor(&mut self) {
256        todo!("implement add_actor")
257    }
258
259    pub fn add_actors(&mut self) {
260        todo!("implement add_actors")
261    }
262
263    pub fn add_strategy(&mut self) {
264        todo!("implement add_strategy")
265    }
266
267    pub fn add_strategies(&mut self) {
268        todo!("implement add_strategies")
269    }
270
271    pub fn add_exec_algorithm(&mut self) {
272        todo!("implement add_exec_algorithm")
273    }
274
275    pub fn add_exec_algorithms(&mut self) {
276        todo!("implement add_exec_algorithms")
277    }
278
279    pub fn reset(&mut self) {
280        todo!("implement reset")
281    }
282
283    pub fn clear_data(&mut self) {
284        todo!("implement clear_data")
285    }
286
287    pub fn clear_strategies(&mut self) {
288        todo!("implement clear_strategies")
289    }
290
291    pub fn clear_exec_algorithms(&mut self) {
292        todo!("implement clear_exec_algorithms")
293    }
294
295    pub fn dispose(&mut self) {
296        todo!("implement dispose")
297    }
298
299    pub fn run(&mut self) {
300        todo!("implement run")
301    }
302
303    pub fn end(&mut self) {
304        todo!("implement end")
305    }
306
307    pub fn get_result(&self) {
308        todo!("implement get_result")
309    }
310
311    pub fn next(&mut self) {
312        todo!("implement next")
313    }
314
315    pub fn advance_time(&mut self) {
316        todo!("implement advance_time")
317    }
318
319    pub fn process_raw_time_event_handlers(&mut self) {
320        todo!("implement process_raw_time_event_handlers")
321    }
322
323    pub fn log_pre_run(&self) {
324        todo!("implement log_pre_run_diagnostics")
325    }
326
327    pub fn log_run(&self) {
328        todo!("implement log_run")
329    }
330
331    pub fn log_post_run(&self) {
332        todo!("implement log_post_run")
333    }
334
335    pub fn add_data_client_if_not_exists(&mut self) {
336        todo!("implement add_data_client_if_not_exists")
337    }
338
339    // TODO: We might want venue to be optional for multi-venue clients
340    pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
341        let client_id = ClientId::from(venue.as_str());
342        if !self
343            .kernel
344            .data_engine
345            .registered_clients()
346            .contains(&client_id)
347        {
348            let backtest_client =
349                BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
350            let data_client_adapter = DataClientAdapter::new(
351                client_id,
352                Some(venue), // TBD
353                false,
354                false,
355                Box::new(backtest_client),
356            );
357            self.kernel
358                .data_engine
359                .register_client(data_client_adapter, None);
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use std::collections::HashMap;
367
368    use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel};
369    use nautilus_model::{
370        enums::{AccountType, BookType, OmsType},
371        identifiers::{ClientId, Venue},
372        instruments::{
373            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
374        },
375        types::Money,
376    };
377    use rstest::rstest;
378
379    use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
380
381    #[allow(clippy::missing_panics_doc)] // OK for testing
382    fn get_backtest_engine(config: Option<BacktestEngineConfig>) -> BacktestEngine {
383        let config = config.unwrap_or_default();
384        let mut engine = BacktestEngine::new(config).unwrap();
385        engine
386            .add_venue(
387                Venue::from("BINANCE"),
388                OmsType::Netting,
389                AccountType::Margin,
390                BookType::L2_MBP,
391                vec![Money::from("1_000_000 USD")],
392                None,
393                None,
394                HashMap::new(),
395                vec![],
396                FillModel::default(),
397                FeeModelAny::default(),
398                None,
399                None,
400                None,
401                None,
402                None,
403                None,
404                None,
405                None,
406                None,
407                None,
408                None,
409                None,
410                None,
411            )
412            .unwrap();
413        engine
414    }
415
416    #[rstest]
417    fn test_engine_venue_and_instrument_initialization(crypto_perpetual_ethusdt: CryptoPerpetual) {
418        pyo3::prepare_freethreaded_python();
419
420        let venue = Venue::from("BINANCE");
421        let client_id = ClientId::from(venue.as_str());
422        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
423        let instrument_id = instrument.id();
424        let mut engine = get_backtest_engine(None);
425        engine.add_instrument(instrument).unwrap();
426
427        // Check the venue and exec client has been added
428        assert_eq!(engine.venues.len(), 1);
429        assert!(engine.venues.get(&venue).is_some());
430        assert!(engine.kernel.exec_engine.get_client(&client_id).is_some());
431
432        // Check the instrument has been added
433        assert!(
434            engine
435                .venues
436                .get(&venue)
437                .is_some_and(|venue| venue.borrow().get_matching_engine(&instrument_id).is_some())
438        );
439        assert_eq!(engine.kernel.data_engine.registered_clients().len(), 1);
440        assert!(
441            engine
442                .kernel
443                .data_engine
444                .registered_clients()
445                .contains(&client_id)
446        );
447    }
448}