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