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 ) -> anyhow::Result<()> {
152 let default_leverage: Decimal = default_leverage.unwrap_or_else(|| {
153 if account_type == AccountType::Margin {
154 Decimal::from(10)
155 } else {
156 Decimal::from(0)
157 }
158 });
159
160 let exchange = SimulatedExchange::new(
161 venue,
162 oms_type,
163 account_type,
164 starting_balances,
165 base_currency,
166 default_leverage,
167 leverages,
168 modules,
169 self.kernel.cache.clone(),
170 self.kernel.clock.clone(),
171 fill_model,
172 fee_model,
173 book_type,
174 latency_model,
175 bar_execution,
176 reject_stop_orders,
177 support_gtd_orders,
178 support_contingent_orders,
179 use_position_ids,
180 use_random_ids,
181 use_reduce_only,
182 use_message_queue,
183 allow_cash_borrowing,
184 frozen_account,
185 )?;
186 let exchange = Rc::new(RefCell::new(exchange));
187 self.venues.insert(venue, exchange.clone());
188
189 let account_id = AccountId::from(format!("{venue}-001").as_str());
190 let exec_client = BacktestExecutionClient::new(
191 self.config.trader_id(),
192 account_id,
193 exchange.clone(),
194 self.kernel.cache.clone(),
195 self.kernel.clock.clone(),
196 routing,
197 frozen_account,
198 );
199 let exec_client = Rc::new(exec_client);
200
201 exchange.borrow_mut().register_client(exec_client.clone());
202 self.kernel.exec_engine.register_client(exec_client)?;
203
204 log::info!("Adding exchange {venue} to engine");
205
206 Ok(())
207 }
208
209 pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModel) {
210 if let Some(exchange) = self.venues.get_mut(&venue) {
211 exchange.borrow_mut().set_fill_model(fill_model);
212 } else {
213 log::warn!(
214 "BacktestEngine::change_fill_model called for unknown venue {venue}. Ignoring."
215 );
216 }
217 }
218
219 pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
231 let instrument_id = instrument.id();
232 if let Some(exchange) = self.venues.get_mut(&instrument.id().venue) {
233 if matches!(instrument, InstrumentAny::CurrencyPair(_))
235 && exchange.borrow().account_type != AccountType::Margin
236 && exchange.borrow().base_currency.is_some()
237 {
238 anyhow::bail!(
239 "Cannot add a `CurrencyPair` instrument {} for a venue with a single-currency CASH account",
240 instrument_id
241 )
242 }
243 exchange
244 .borrow_mut()
245 .add_instrument(instrument.clone())
246 .unwrap();
247 } else {
248 anyhow::bail!(
249 "Cannot add an `Instrument` object without first adding its associated venue {}",
250 instrument.id().venue
251 )
252 }
253
254 self.add_market_data_client_if_not_exists(instrument.id().venue);
256
257 self.kernel
258 .data_engine
259 .borrow_mut()
260 .process(&instrument as &dyn Any);
261 log::info!(
262 "Added instrument {} to exchange {}",
263 instrument_id,
264 instrument_id.venue
265 );
266 Ok(())
267 }
268
269 pub fn add_data(
270 &mut self,
271 data: Vec<Data>,
272 client_id: Option<ClientId>,
273 validate: bool,
274 sort: bool,
275 ) {
276 if data.is_empty() {
277 log::warn!("add_data called with empty data slice – ignoring");
278 return;
279 }
280
281 let mut to_add = data;
283 if sort {
284 to_add.sort_by_key(nautilus_model::data::HasTsInit::ts_init);
285 }
286
287 if validate {
289 for item in &to_add {
290 let instr_id = item.instrument_id();
291 self.has_data.insert(instr_id);
292
293 if item.is_order_book_data() {
294 self.has_book_data.insert(instr_id);
295 }
296
297 self.add_market_data_client_if_not_exists(instr_id.venue);
299 }
300 }
301
302 for item in to_add {
304 self.data.push_back(item);
305 }
306
307 if sort {
308 let mut vec: Vec<Data> = self.data.drain(..).collect();
310 vec.sort_by_key(nautilus_model::data::HasTsInit::ts_init);
311 self.data = vec.into();
312 }
313
314 log::info!(
315 "Added {} data element{} to BacktestEngine",
316 self.data.len(),
317 if self.data.len() == 1 { "" } else { "s" }
318 );
319 }
320
321 pub fn add_actor(&mut self) {
322 todo!("implement add_actor")
323 }
324
325 pub fn add_actors(&mut self) {
326 todo!("implement add_actors")
327 }
328
329 pub fn add_strategy(&mut self) {
330 todo!("implement add_strategy")
331 }
332
333 pub fn add_strategies(&mut self) {
334 todo!("implement add_strategies")
335 }
336
337 pub fn add_exec_algorithm(&mut self) {
338 todo!("implement add_exec_algorithm")
339 }
340
341 pub fn add_exec_algorithms(&mut self) {
342 todo!("implement add_exec_algorithms")
343 }
344
345 pub fn reset(&mut self) {
346 todo!("implement reset")
347 }
348
349 pub fn clear_data(&mut self) {
350 todo!("implement clear_data")
351 }
352
353 pub fn clear_strategies(&mut self) {
354 todo!("implement clear_strategies")
355 }
356
357 pub fn clear_exec_algorithms(&mut self) {
358 todo!("implement clear_exec_algorithms")
359 }
360
361 pub fn dispose(&mut self) {
362 todo!("implement dispose")
363 }
364
365 pub fn run(&mut self) {
366 todo!("implement run")
367 }
368
369 pub fn end(&mut self) {
370 todo!("implement end")
371 }
372
373 pub fn get_result(&self) {
374 log::info!("BacktestEngine::get_result called – not yet implemented");
377 }
378
379 pub fn next(&mut self) {
380 self.data.pop_front();
381 }
382
383 pub fn advance_time(&mut self, _ts_now: UnixNanos) -> Vec<TimeEventHandlerV2> {
384 self.accumulator.drain()
386 }
387
388 pub fn process_raw_time_event_handlers(
389 &mut self,
390 handlers: Vec<TimeEventHandlerV2>,
391 ts_now: UnixNanos,
392 only_now: bool,
393 as_of_now: bool,
394 ) {
395 let mut last_ts_init: Option<UnixNanos> = None;
396
397 for handler in handlers {
398 let ts_event_init = handler.event.ts_event; if Self::should_skip_time_event(ts_event_init, ts_now, only_now, as_of_now) {
401 continue;
402 }
403
404 if last_ts_init != Some(ts_event_init) {
405 for exchange in self.venues.values() {
407 exchange.borrow_mut().process(ts_event_init);
408 }
409 last_ts_init = Some(ts_event_init);
410 }
411
412 handler.run();
413 }
414 }
415
416 pub fn log_pre_run(&self) {
417 todo!("implement log_pre_run_diagnostics")
418 }
419
420 pub fn log_run(&self) {
421 todo!("implement log_run")
422 }
423
424 pub fn log_post_run(&self) {
425 todo!("implement log_post_run")
426 }
427
428 pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
429 if self
430 .kernel
431 .data_engine
432 .borrow()
433 .registered_clients()
434 .contains(&client_id)
435 {
436 return;
437 }
438
439 let venue = Venue::from(client_id.as_str());
442 let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
443 let data_client_adapter = DataClientAdapter::new(
444 backtest_client.client_id,
445 None, false,
447 false,
448 Box::new(backtest_client),
449 );
450
451 self.kernel
452 .data_engine
453 .borrow_mut()
454 .register_client(data_client_adapter, None);
455 }
456
457 fn should_skip_time_event(
460 ts_event_init: UnixNanos,
461 ts_now: UnixNanos,
462 only_now: bool,
463 as_of_now: bool,
464 ) -> bool {
465 if only_now {
466 ts_event_init != ts_now
467 } else if as_of_now {
468 ts_event_init > ts_now
469 } else {
470 ts_event_init >= ts_now
471 }
472 }
473
474 pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
476 let client_id = ClientId::from(venue.as_str());
477 if !self
478 .kernel
479 .data_engine
480 .borrow()
481 .registered_clients()
482 .contains(&client_id)
483 {
484 let backtest_client =
485 BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
486 let data_client_adapter = DataClientAdapter::new(
487 client_id,
488 Some(venue), false,
490 false,
491 Box::new(backtest_client),
492 );
493 self.kernel
494 .data_engine
495 .borrow_mut()
496 .register_client(data_client_adapter, None);
497 }
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use std::collections::HashMap;
504
505 use nautilus_execution::models::{fee::FeeModelAny, fill::FillModel};
506 use nautilus_model::{
507 enums::{AccountType, BookType, OmsType},
508 identifiers::{ClientId, Venue},
509 instruments::{
510 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
511 },
512 types::Money,
513 };
514 use rstest::rstest;
515
516 use crate::{config::BacktestEngineConfig, engine::BacktestEngine};
517
518 #[allow(clippy::missing_panics_doc)] fn get_backtest_engine(config: Option<BacktestEngineConfig>) -> BacktestEngine {
520 let config = config.unwrap_or_default();
521 let mut engine = BacktestEngine::new(config).unwrap();
522 engine
523 .add_venue(
524 Venue::from("BINANCE"),
525 OmsType::Netting,
526 AccountType::Margin,
527 BookType::L2_MBP,
528 vec![Money::from("1_000_000 USD")],
529 None,
530 None,
531 HashMap::new(),
532 vec![],
533 FillModel::default(),
534 FeeModelAny::default(),
535 None,
536 None,
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 )
550 .unwrap();
551 engine
552 }
553
554 #[rstest]
555 fn test_engine_venue_and_instrument_initialization(crypto_perpetual_ethusdt: CryptoPerpetual) {
556 let venue = Venue::from("BINANCE");
557 let client_id = ClientId::from(venue.as_str());
558 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
559 let instrument_id = instrument.id();
560 let mut engine = get_backtest_engine(None);
561 engine.add_instrument(instrument).unwrap();
562
563 assert_eq!(engine.venues.len(), 1);
565 assert!(engine.venues.contains_key(&venue));
566 assert!(engine.kernel.exec_engine.get_client(&client_id).is_some());
567
568 assert!(
570 engine
571 .venues
572 .get(&venue)
573 .is_some_and(|venue| venue.borrow().get_matching_engine(&instrument_id).is_some())
574 );
575 assert_eq!(
576 engine
577 .kernel
578 .data_engine
579 .borrow()
580 .registered_clients()
581 .len(),
582 1
583 );
584 assert!(
585 engine
586 .kernel
587 .data_engine
588 .borrow()
589 .registered_clients()
590 .contains(&client_id)
591 );
592 }
593}