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