1#![allow(dead_code)]
18#![allow(unused_variables)]
19
20use 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
50pub 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 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 #[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 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 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 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 let mut to_add = data;
285 if sort {
286 to_add.sort_by_key(nautilus_model::data::HasTsInit::ts_init);
287 }
288
289 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 self.add_market_data_client_if_not_exists(instr_id.venue);
301 }
302 }
303
304 for item in to_add {
306 self.data.push_back(item);
307 }
308
309 if sort {
310 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 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 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; 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 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 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, 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 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 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), 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 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 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}