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