1#![allow(dead_code)]
18#![allow(unused_variables)]
19
20use 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
51pub 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 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 #[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, 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 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 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 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 let mut to_add = data;
293 if sort {
294 to_add.sort_by_key(HasTsInit::ts_init);
295 }
296
297 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 self.add_market_data_client_if_not_exists(instr_id.venue);
309 }
310 }
311
312 for item in to_add {
314 self.data.push_back(item);
315 }
316
317 if sort {
318 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 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 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; 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 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 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, 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 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 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), 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 assert_eq!(engine.venues.len(), 1);
575 assert!(engine.venues.contains_key(&venue));
576
577 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}