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