1use std::{
19 any::Any,
20 cell::RefCell,
21 fmt::Debug,
22 rc::{Rc, Weak},
23 sync::Arc,
24};
25
26use ahash::{AHashMap, AHashSet};
27use nautilus_analysis::analyzer::PortfolioAnalyzer;
28use nautilus_common::{
29 actor::{DataActor, DataActorNative},
30 cache::Cache,
31 clock::{Clock, TestClock},
32 component::Component,
33 enums::LogColor,
34 log_info,
35 logging::{
36 logging_clock_set_realtime_mode, logging_clock_set_static_mode,
37 logging_clock_set_static_time,
38 },
39 runner::{
40 SyncDataCommandSender, SyncTradingCommandSender, data_cmd_queue_is_empty,
41 drain_data_cmd_queue, drain_trading_cmd_queue, replace_data_cmd_sender,
42 replace_exec_cmd_sender, trading_cmd_queue_is_empty,
43 },
44 timer::{TimeEvent, TimeEventCallback},
45};
46use nautilus_core::{
47 UUID4, UnixNanos, datetime::unix_nanos_to_iso8601, string::formatting::Separable,
48};
49use nautilus_data::client::DataClientAdapter;
50use nautilus_execution::models::fill::FillModelAny;
51use nautilus_model::{
52 accounts::{Account, AccountAny},
53 data::{Data, HasTsInit},
54 enums::{AccountType, AggregationSource, BookType},
55 identifiers::{AccountId, ClientId, InstrumentId, TraderId, Venue},
56 instruments::{Instrument, InstrumentAny},
57 position::Position,
58 types::Price,
59};
60use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
61use nautilus_trading::{
62 ExecutionAlgorithm, ExecutionAlgorithmNative,
63 strategy::{Strategy, StrategyNative},
64};
65
66use crate::{
67 accumulator::TimeEventAccumulator,
68 config::{BacktestEngineConfig, SimulatedVenueConfig},
69 data_client::BacktestDataClient,
70 data_iterator::BacktestDataIterator,
71 exchange::SimulatedExchange,
72 execution_client::BacktestExecutionClient,
73 result::BacktestResult,
74};
75
76pub struct BacktestEngine {
89 kernel: NautilusKernel,
90 instance_id: UUID4,
91 config: BacktestEngineConfig,
92 accumulator: TimeEventAccumulator,
93 run_config_id: Option<String>,
94 run_id: Option<UUID4>,
95 venues: AHashMap<Venue, Rc<RefCell<SimulatedExchange>>>,
96 exec_clients: Vec<BacktestExecutionClient>,
97 has_data: AHashSet<InstrumentId>,
98 has_book_data: AHashSet<InstrumentId>,
99 data_iterator: BacktestDataIterator,
100 data_len: usize,
101 data_stream_counter: usize,
102 ts_first: Option<UnixNanos>,
103 ts_last_data: Option<UnixNanos>,
104 sorted: bool,
105 iteration: usize,
106 force_stop: bool,
107 last_ns: UnixNanos,
108 last_module_ns: Option<UnixNanos>,
109 last_liquidation_ns: Option<UnixNanos>,
110 end_ns: UnixNanos,
111 run_started: Option<UnixNanos>,
112 run_finished: Option<UnixNanos>,
113 backtest_start: Option<UnixNanos>,
114 backtest_end: Option<UnixNanos>,
115}
116
117impl Debug for BacktestEngine {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct(stringify!(BacktestEngine))
120 .field("instance_id", &self.instance_id)
121 .field("run_config_id", &self.run_config_id)
122 .field("run_id", &self.run_id)
123 .finish_non_exhaustive()
124 }
125}
126
127impl BacktestEngine {
128 pub fn new(mut config: BacktestEngineConfig) -> anyhow::Result<Self> {
134 let mut cache_config = config.cache.unwrap_or_default();
137 cache_config.drop_instruments_on_reset = false;
138 config.cache = Some(cache_config);
139 let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
140 let instance_id = kernel.instance_id;
141
142 Ok(Self {
143 kernel,
144 instance_id,
145 config,
146 accumulator: TimeEventAccumulator::new(),
147 run_config_id: None,
148 run_id: None,
149 venues: AHashMap::new(),
150 exec_clients: Vec::new(),
151 has_data: AHashSet::new(),
152 has_book_data: AHashSet::new(),
153 data_iterator: BacktestDataIterator::new(),
154 data_len: 0,
155 data_stream_counter: 0,
156 ts_first: None,
157 ts_last_data: None,
158 sorted: true,
159 iteration: 0,
160 force_stop: false,
161 last_ns: UnixNanos::default(),
162 last_module_ns: None,
163 last_liquidation_ns: None,
164 end_ns: UnixNanos::default(),
165 run_started: None,
166 run_finished: None,
167 backtest_start: None,
168 backtest_end: None,
169 })
170 }
171
172 #[must_use]
174 pub const fn kernel(&self) -> &NautilusKernel {
175 &self.kernel
176 }
177
178 pub fn kernel_mut(&mut self) -> &mut NautilusKernel {
180 &mut self.kernel
181 }
182
183 #[must_use]
185 pub fn trader_id(&self) -> TraderId {
186 self.kernel.trader_id()
187 }
188
189 #[must_use]
191 pub fn machine_id(&self) -> &str {
192 self.kernel.machine_id()
193 }
194
195 #[must_use]
197 pub fn instance_id(&self) -> UUID4 {
198 self.instance_id
199 }
200
201 #[must_use]
203 pub fn iteration(&self) -> usize {
204 self.iteration
205 }
206
207 #[must_use]
209 pub fn run_config_id(&self) -> Option<&str> {
210 self.run_config_id.as_deref()
211 }
212
213 #[must_use]
215 pub const fn run_id(&self) -> Option<UUID4> {
216 self.run_id
217 }
218
219 #[must_use]
221 pub const fn run_started(&self) -> Option<UnixNanos> {
222 self.run_started
223 }
224
225 #[must_use]
227 pub const fn run_finished(&self) -> Option<UnixNanos> {
228 self.run_finished
229 }
230
231 #[must_use]
233 pub const fn backtest_start(&self) -> Option<UnixNanos> {
234 self.backtest_start
235 }
236
237 #[must_use]
239 pub const fn backtest_end(&self) -> Option<UnixNanos> {
240 self.backtest_end
241 }
242
243 #[must_use]
245 pub fn list_venues(&self) -> Vec<Venue> {
246 self.venues.keys().copied().collect()
247 }
248
249 pub fn add_venue(&mut self, config: SimulatedVenueConfig) -> anyhow::Result<()> {
253 let venue = config.venue;
256 let routing = Some(config.routing);
257 let frozen_account = Some(config.frozen_account);
258
259 let exchange =
260 SimulatedExchange::new(config, self.kernel.cache.clone(), self.kernel.clock.clone())?;
261 let exchange = Rc::new(RefCell::new(exchange));
262 SimulatedExchange::register_spread_quote_endpoint(&exchange);
263 self.venues.insert(venue, exchange.clone());
264
265 let account_id = AccountId::from(format!("{venue}-001").as_str());
266
267 let exec_client = BacktestExecutionClient::new(
268 self.config.trader_id(),
269 account_id,
270 &exchange,
271 self.kernel.cache.clone(),
272 self.kernel.clock.clone(),
273 routing,
274 frozen_account,
275 );
276
277 exchange
278 .borrow_mut()
279 .register_client(Rc::new(exec_client.clone()));
280
281 self.exec_clients.push(exec_client.clone());
282
283 self.kernel
284 .exec_engine
285 .borrow_mut()
286 .register_client(Box::new(exec_client))?;
287
288 log::info!("Adding exchange {venue} to engine");
289
290 Ok(())
291 }
292
293 pub fn set_settlement_price(
299 &mut self,
300 venue: Venue,
301 instrument_id: InstrumentId,
302 price: Price,
303 ) -> anyhow::Result<()> {
304 let exchange = self
305 .venues
306 .get_mut(&venue)
307 .ok_or_else(|| anyhow::anyhow!("Unknown venue {venue}"))?;
308 exchange
309 .borrow_mut()
310 .set_settlement_price(instrument_id, price);
311 Ok(())
312 }
313
314 pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModelAny) {
316 if let Some(exchange) = self.venues.get_mut(&venue) {
317 exchange.borrow_mut().set_fill_model(fill_model);
318 } else {
319 log::warn!(
320 "BacktestEngine::change_fill_model called for unknown venue {venue}, ignoring"
321 );
322 }
323 }
324
325 pub fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
334 let instrument_id = instrument.id();
335 if let Some(exchange) = self.venues.get(&instrument.id().venue) {
336 let previous_expiration_ns = exchange.borrow().instrument_expiration(instrument_id);
337
338 if matches!(
339 instrument,
340 InstrumentAny::CurrencyPair(_) | InstrumentAny::TokenizedAsset(_)
341 ) && exchange.borrow().account_type != AccountType::Margin
342 && exchange.borrow().base_currency.is_some()
343 {
344 anyhow::bail!(
345 "Cannot add a multi-currency spot instrument {instrument_id} for a venue with a single-currency CASH account"
346 )
347 }
348 exchange.borrow_mut().add_instrument(instrument.clone())?;
349 if let Some(expiration_ns) = instrument.expiration_ns() {
350 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
351 }
352
353 if let Some(previous_expiration_ns) = previous_expiration_ns
354 && instrument.expiration_ns() != Some(previous_expiration_ns)
355 && !exchange
356 .borrow()
357 .has_unprocessed_instrument_expiration(previous_expiration_ns)
358 {
359 let timer_name = Self::instrument_expiration_timer_name(
360 instrument_id.venue,
361 previous_expiration_ns,
362 );
363 self.kernel.clock.borrow_mut().cancel_timer(&timer_name);
364 }
365 } else {
366 anyhow::bail!(
367 "Cannot add an `Instrument` object without first adding its associated venue {}",
368 instrument.id().venue
369 )
370 }
371
372 self.add_market_data_client_if_not_exists(instrument.id().venue);
373
374 self.kernel
375 .data_engine
376 .borrow_mut()
377 .process(instrument as &dyn Any);
378 log::info!(
379 "Added instrument {} to exchange {}",
380 instrument_id,
381 instrument_id.venue
382 );
383 Ok(())
384 }
385
386 pub fn add_data(
397 &mut self,
398 data: Vec<Data>,
399 client_id: Option<ClientId>,
400 validate: bool,
401 sort: bool,
402 ) -> anyhow::Result<()> {
403 #[cfg(not(feature = "defi"))]
404 let _ = client_id;
405
406 anyhow::ensure!(!data.is_empty(), "data was empty");
407
408 let count = data.len();
409 let mut to_add = data;
410
411 if sort {
412 to_add.sort_by_key(HasTsInit::ts_init);
413 }
414
415 if validate {
416 let first = &to_add[0];
419 #[cfg(feature = "defi")]
420 let first_is_defi = matches!(first, Data::Defi(_));
421 #[cfg(not(feature = "defi"))]
422 let first_is_defi = false;
423
424 if !first_is_defi {
425 let first_instrument_id = first.instrument_id();
426 anyhow::ensure!(
427 self.kernel
428 .cache
429 .borrow()
430 .instrument(&first_instrument_id)
431 .is_some(),
432 "Instrument {first_instrument_id} for the given data not found in the cache. \
433 Add the instrument through `add_instrument()` prior to adding related data."
434 );
435
436 if let Data::Bar(bar) = first {
437 anyhow::ensure!(
438 bar.bar_type.aggregation_source() == AggregationSource::External,
439 "bar_type.aggregation_source must be External, was {:?}",
440 bar.bar_type.aggregation_source(),
441 );
442 }
443 }
444 }
445
446 let mut batch_min_ts: Option<UnixNanos> = None;
452 let mut batch_max_ts: Option<UnixNanos> = None;
453
454 #[cfg(feature = "defi")]
455 if to_add.iter().any(|item| matches!(item, Data::Defi(_))) {
456 self.add_defi_data_client_if_not_exists(client_id);
457 }
458
459 for item in &to_add {
460 #[cfg(feature = "defi")]
461 if matches!(item, Data::Defi(_)) {
462 let ts = item.ts_init();
463 batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
464 batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
465 continue;
466 }
467
468 let instr_id = item.instrument_id();
469 self.has_data.insert(instr_id);
470
471 if item.is_order_book_data() {
472 self.has_book_data.insert(instr_id);
473 }
474
475 self.add_market_data_client_if_not_exists(instr_id.venue);
476
477 let ts = item.ts_init();
478 batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
479 batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
480 }
481
482 if let Some(ts) = batch_min_ts
483 && self.ts_first.is_none_or(|t| ts < t)
484 {
485 self.ts_first = Some(ts);
486 }
487
488 if let Some(ts) = batch_max_ts
489 && self.ts_last_data.is_none_or(|t| ts > t)
490 {
491 self.ts_last_data = Some(ts);
492 }
493
494 self.data_len += count;
495 let stream_name = format!("backtest_data_{}", self.data_stream_counter);
496 self.data_stream_counter += 1;
497 self.data_iterator.add_data(&stream_name, to_add, true);
498
499 self.sorted = sort;
500
501 log::info!(
502 "Added {count} data element{} to BacktestEngine ({} total)",
503 if count == 1 { "" } else { "s" },
504 self.data_len,
505 );
506
507 Ok(())
508 }
509
510 pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
517 where
518 T: DataActor + DataActorNative + Component + Debug + 'static,
519 {
520 self.kernel.trader.borrow_mut().add_actor(actor)
521 }
522
523 pub fn add_actors<T>(&mut self, actors: Vec<T>) -> anyhow::Result<()>
529 where
530 T: DataActor + DataActorNative + Component + Debug + 'static,
531 {
532 for actor in actors {
533 self.add_actor(actor)?;
534 }
535 Ok(())
536 }
537
538 pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
545 where
546 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
547 {
548 let strategy_id = self
549 .kernel
550 .trader
551 .borrow()
552 .prepare_strategy_for_registration(&mut strategy)?;
553 let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
554
555 self.kernel.trader.borrow_mut().add_strategy(strategy)?;
556
557 if let Some(oms_type) = oms_type {
558 self.kernel
559 .exec_engine
560 .borrow_mut()
561 .register_oms_type(strategy_id, oms_type);
562 }
563
564 Ok(())
565 }
566
567 pub fn add_strategies<T>(&mut self, strategies: Vec<T>) -> anyhow::Result<()>
573 where
574 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
575 {
576 for strategy in strategies {
577 self.add_strategy(strategy)?;
578 }
579 Ok(())
580 }
581
582 pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
588 where
589 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
590 {
591 self.kernel
592 .trader
593 .borrow_mut()
594 .add_exec_algorithm(exec_algorithm)
595 }
596
597 pub fn add_exec_algorithms<T>(&mut self, exec_algorithms: Vec<T>) -> anyhow::Result<()>
604 where
605 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
606 {
607 for exec_algorithm in exec_algorithms {
608 self.add_exec_algorithm(exec_algorithm)?;
609 }
610 Ok(())
611 }
612
613 pub fn run(
630 &mut self,
631 start: Option<UnixNanos>,
632 end: Option<UnixNanos>,
633 run_config_id: Option<String>,
634 streaming: bool,
635 ) -> anyhow::Result<()> {
636 self.run_impl(start, end, run_config_id, streaming)?;
637
638 if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
643 self.end();
644 }
645
646 Ok(())
647 }
648
649 fn run_impl(
650 &mut self,
651 start: Option<UnixNanos>,
652 end: Option<UnixNanos>,
653 run_config_id: Option<String>,
654 streaming: bool,
655 ) -> anyhow::Result<()> {
656 anyhow::ensure!(
657 self.sorted,
658 "Data has been added but not sorted, call `engine.sort_data()` or use \
659 `engine.add_data(..., sort=true)` before running"
660 );
661
662 for exchange in self.venues.values() {
663 let exchange = exchange.borrow();
664 let book_type_has_depth = exchange.book_type() as u8 > BookType::L1_MBP as u8;
665 if !book_type_has_depth {
666 continue;
667 }
668
669 for instrument_id in exchange.instrument_ids() {
670 let has_data = self.has_data.contains(instrument_id);
671 let missing_book_data = !self.has_book_data.contains(instrument_id);
672 if has_data && missing_book_data {
673 anyhow::bail!(
674 "No order book data found for instrument '{instrument_id}' when `book_type` \
675 is '{:?}'. Set the venue `book_type` to 'L1_MBP' (for top-of-book data \
676 like quotes, trades, and bars) or provide order book data for this \
677 instrument.",
678 exchange.book_type()
679 );
680 }
681 }
682 }
683
684 let start_ns = start.unwrap_or_else(|| self.ts_first.unwrap_or_default());
686 let end_ns = end.unwrap_or_else(|| {
687 self.ts_last_data
688 .unwrap_or(UnixNanos::from(4_102_444_800_000_000_000u64))
689 });
690 anyhow::ensure!(start_ns <= end_ns, "start was > end");
691 self.end_ns = end_ns;
692 self.last_ns = start_ns;
693 self.last_module_ns = None;
694
695 let clocks = self.collect_all_clocks();
697 Self::set_all_clocks_time(&clocks, start_ns);
698
699 if self.iteration == 0 {
701 self.set_instrument_expiration_timers()?;
702
703 self.run_config_id = run_config_id;
704 self.run_id = Some(UUID4::new());
705 self.run_started = Some(UnixNanos::from(std::time::SystemTime::now()));
706 self.backtest_start = Some(start_ns);
707
708 for exchange in self.venues.values() {
709 let mut ex = exchange.borrow_mut();
710 ex.initialize_account();
711 ex.load_open_orders();
712 }
713
714 Self::set_all_clocks_time(&clocks, start_ns);
716
717 self.force_stop = false;
719 self.kernel.reset_shutdown_flag();
720
721 Self::init_command_senders();
723
724 logging_clock_set_static_mode();
726 logging_clock_set_static_time(start_ns.as_u64());
727
728 self.kernel.start();
730 if self.kernel.is_event_store_replay() {
731 self.log_pre_run();
732 return Ok(());
733 }
734
735 if self.kernel.is_event_store_replay_configured() {
736 anyhow::bail!("event-store replay did not start");
737 }
738 self.kernel.start_trader();
739
740 self.log_pre_run();
741 }
742
743 self.log_run();
744
745 let mut data = self.data_iterator.next_item();
747 while let Some(ref d) = data {
748 if d.ts_init() >= start_ns {
749 break;
750 }
751 data = self.data_iterator.next_item();
752 }
753
754 if let Some(ref d) = data {
756 let ts = d.ts_init();
757 self.last_ns = if ts.as_u64() > 0 {
758 UnixNanos::from(ts.as_u64() - 1)
759 } else {
760 UnixNanos::default()
761 };
762 } else {
763 self.last_ns = start_ns;
764 }
765
766 loop {
767 if self.kernel.is_shutdown_requested() {
768 log::info!("Shutdown requested via ShutdownSystem, ending backtest");
769 self.force_stop = true;
770 }
771
772 if self.force_stop {
773 log::info!("Force stop triggered, ending backtest");
774 break;
775 }
776
777 if data.is_none() {
778 if streaming {
779 break;
783 }
784 let done = self.process_next_timer(&clocks);
785 data = self.data_iterator.next_item();
786 if data.is_none() && done {
787 break;
788 }
789 continue;
790 }
791
792 let d = data.as_ref().unwrap();
793 let ts_init = d.ts_init();
794
795 if ts_init > end_ns {
796 break;
797 }
798
799 if ts_init > self.last_ns {
800 self.last_ns = ts_init;
801 self.advance_time_impl(ts_init, &clocks);
802 }
803
804 if self.kernel.is_shutdown_requested() {
807 self.force_stop = true;
808 break;
809 }
810
811 self.route_data_to_exchange(d);
812 self.kernel.data_engine.borrow_mut().process_data(d.clone());
813
814 self.drain_command_queues();
816 self.settle_venues(ts_init);
817
818 let prev_last_ns = self.last_ns;
819 data = self.data_iterator.next_item();
820
821 if data.is_none() || data.as_ref().unwrap().ts_init() > prev_last_ns {
823 self.flush_accumulator_events(&clocks, prev_last_ns);
824 self.run_venue_modules(prev_last_ns);
825 self.run_venue_liquidations(prev_last_ns);
826 }
827
828 self.iteration += 1;
829 }
830
831 let ts_now = self.kernel.clock.borrow().timestamp_ns();
833 self.settle_venues(ts_now);
834 self.run_venue_modules(ts_now);
835 self.run_venue_liquidations(ts_now);
836
837 let flush_ts = if streaming || self.force_stop || self.kernel.is_shutdown_requested() {
840 self.last_ns
841 } else {
842 end_ns
843 };
844 self.flush_accumulator_events(&clocks, flush_ts);
845
846 Ok(())
847 }
848
849 pub fn end(&mut self) {
851 if self.end_ns.as_u64() > 0 {
858 let clocks = self.collect_all_clocks();
859 let flush_ts = if self.force_stop || self.kernel.is_shutdown_requested() {
860 self.last_ns
861 } else {
862 self.end_ns
863 };
864
865 self.flush_accumulator_events(&clocks, flush_ts);
866 }
867
868 self.kernel.stop_trader();
869
870 let mut ts_now = self.kernel.clock.borrow().timestamp_ns();
873
874 self.drain_command_queues();
876
877 if let Some(max_inflight_ts) = self.max_inflight_command_ts()
880 && max_inflight_ts > ts_now
881 {
882 ts_now = max_inflight_ts;
883 let clocks = self.collect_all_clocks();
884 Self::set_all_clocks_time(&clocks, ts_now);
885 }
886
887 self.settle_venues(ts_now);
888
889 self.kernel.data_engine.borrow_mut().stop();
891 self.kernel.risk_engine.borrow_mut().stop();
892 self.kernel.exec_engine.borrow_mut().stop();
893
894 self.run_finished = Some(UnixNanos::from(std::time::SystemTime::now()));
895 self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
896
897 logging_clock_set_realtime_mode();
899
900 self.log_post_run();
901 }
902
903 pub fn reset(&mut self) {
908 log::debug!("Resetting");
909
910 if self.kernel.trader.borrow().is_running() {
911 self.end();
912 }
913
914 self.kernel.data_engine.borrow_mut().stop();
916 self.kernel.data_engine.borrow_mut().reset();
917
918 self.kernel.exec_engine.borrow_mut().stop();
919
920 for exchange in self.venues.values() {
923 exchange.borrow_mut().reset();
924 }
925 self.kernel.exec_engine.borrow_mut().reset();
926
927 self.kernel.risk_engine.borrow_mut().stop();
928 self.kernel.risk_engine.borrow_mut().reset();
929
930 self.kernel.order_emulator.reset();
931
932 if let Err(e) = self.kernel.trader.borrow_mut().reset() {
934 log::error!("Error resetting trader: {e:?}");
935 }
936
937 self.kernel.portfolio.borrow_mut().reset();
938
939 self.run_config_id = None;
941 self.run_id = None;
942 self.run_started = None;
943 self.run_finished = None;
944 self.backtest_start = None;
945 self.backtest_end = None;
946 self.iteration = 0;
947 self.force_stop = false;
948 self.last_ns = UnixNanos::default();
949 self.last_module_ns = None;
950 self.last_liquidation_ns = None;
951 self.end_ns = UnixNanos::default();
952
953 self.accumulator.clear();
954
955 self.data_iterator.reset_all_cursors();
957
958 log::info!("Reset");
959 }
960
961 pub fn sort_data(&mut self) {
966 self.sorted = true;
970 log::info!("Data sort requested (iterator merges streams by replay timestamp)");
971 }
972
973 pub fn clear_data(&mut self) {
975 self.has_data.clear();
976 self.has_book_data.clear();
977 self.data_iterator = BacktestDataIterator::new();
978 self.data_len = 0;
979 self.data_stream_counter = 0;
980 self.ts_first = None;
981 self.ts_last_data = None;
982 self.sorted = true;
983 }
984
985 pub fn clear_actors(&mut self) -> anyhow::Result<()> {
991 self.kernel.trader.borrow_mut().clear_actors()
992 }
993
994 pub fn clear_strategies(&mut self) -> anyhow::Result<()> {
1000 self.kernel.trader.borrow_mut().clear_strategies()
1001 }
1002
1003 pub fn clear_exec_algorithms(&mut self) -> anyhow::Result<()> {
1009 self.kernel.trader.borrow_mut().clear_exec_algorithms()
1010 }
1011
1012 pub fn dispose(&mut self) {
1014 self.clear_data();
1015 self.accumulator.clear();
1016 self.kernel.dispose();
1017 }
1018
1019 #[must_use]
1021 pub fn get_result(&self) -> BacktestResult {
1022 let elapsed_time_secs = match (self.backtest_start, self.backtest_end) {
1023 (Some(start), Some(end)) => (end.as_f64() - start.as_f64()) / 1_000_000_000.0,
1024 _ => 0.0,
1025 };
1026
1027 let cache = self.kernel.cache.borrow();
1028 let orders = cache.orders(None, None, None, None, None);
1029 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1030 let total_orders = orders.len();
1031 let positions: Vec<Position> = cache
1032 .positions(None, None, None, None, None)
1033 .into_iter()
1034 .map(|p| p.cloned())
1035 .collect();
1036 let cached_positions_count = positions.len();
1037 let snapshot_positions = cache.position_snapshots(None, None).len();
1038 let total_positions = Self::total_positions_with_snapshots(&cache, cached_positions_count);
1039 let summary = self.build_result_summary(
1040 &cache,
1041 total_events,
1042 total_orders,
1043 cached_positions_count,
1044 snapshot_positions,
1045 );
1046
1047 let stats = self.kernel.portfolio.borrow().statistics();
1048 let stats_pnls = stats.pnls;
1049 let stats_returns = stats.returns;
1050 let stats_general = stats.general;
1051
1052 BacktestResult {
1053 trader_id: self.config.trader_id().to_string(),
1054 machine_id: self.kernel.machine_id.clone(),
1055 instance_id: self.instance_id,
1056 run_config_id: self.run_config_id.clone(),
1057 run_id: self.run_id,
1058 run_started: self.run_started,
1059 run_finished: self.run_finished,
1060 backtest_start: self.backtest_start,
1061 backtest_end: self.backtest_end,
1062 elapsed_time_secs,
1063 iterations: self.iteration,
1064 total_events,
1065 total_orders,
1066 total_positions,
1067 summary,
1068 stats_pnls,
1069 stats_returns,
1070 stats_general,
1071 }
1072 }
1073
1074 fn build_result_summary(
1075 &self,
1076 cache: &Cache,
1077 total_events: usize,
1078 total_orders: usize,
1079 cached_positions_count: usize,
1080 snapshot_positions: usize,
1081 ) -> AHashMap<String, String> {
1082 let mut summary = AHashMap::new();
1083 summary.insert("iterations".to_string(), self.iteration.to_string());
1084 summary.insert("total_events".to_string(), total_events.to_string());
1085 summary.insert("orders.total".to_string(), total_orders.to_string());
1086 summary.insert(
1087 "orders.open".to_string(),
1088 cache
1089 .orders_open_count(None, None, None, None, None)
1090 .to_string(),
1091 );
1092 summary.insert(
1093 "orders.closed".to_string(),
1094 cache
1095 .orders_closed_count(None, None, None, None, None)
1096 .to_string(),
1097 );
1098 summary.insert(
1099 "orders.emulated".to_string(),
1100 cache
1101 .orders_emulated_count(None, None, None, None, None)
1102 .to_string(),
1103 );
1104 summary.insert(
1105 "orders.inflight".to_string(),
1106 cache
1107 .orders_inflight_count(None, None, None, None, None)
1108 .to_string(),
1109 );
1110 summary.insert(
1111 "positions.total".to_string(),
1112 cached_positions_count.to_string(),
1113 );
1114 summary.insert(
1115 "positions.open".to_string(),
1116 cache
1117 .positions_open_count(None, None, None, None, None)
1118 .to_string(),
1119 );
1120 summary.insert(
1121 "positions.closed".to_string(),
1122 cache
1123 .positions_closed_count(None, None, None, None, None)
1124 .to_string(),
1125 );
1126 summary.insert(
1127 "positions.snapshots".to_string(),
1128 snapshot_positions.to_string(),
1129 );
1130 summary.insert(
1131 "positions.total_with_snapshots".to_string(),
1132 (cached_positions_count + snapshot_positions).to_string(),
1133 );
1134
1135 let mut venues: Vec<Venue> = self.venues.keys().copied().collect();
1136 venues.sort_by_key(ToString::to_string);
1137 summary.insert("venues.total".to_string(), venues.len().to_string());
1138
1139 for venue in venues {
1140 let Some(account) = cache.account_for_venue(&venue) else {
1141 continue;
1142 };
1143
1144 let venue_key = venue.to_string();
1145 let account_key = format!("account.{venue_key}");
1146 summary.insert(format!("{account_key}.id"), account.id().to_string());
1147 summary.insert(
1148 format!("{account_key}.type"),
1149 account.account_type().to_string(),
1150 );
1151 summary.insert(
1152 format!("{account_key}.base_currency"),
1153 account
1154 .base_currency()
1155 .map_or_else(|| "None".to_string(), |currency| currency.code.to_string()),
1156 );
1157 summary.insert(
1158 format!("{account_key}.event_count"),
1159 account.event_count().to_string(),
1160 );
1161
1162 let mut balances: Vec<_> = account.balances().into_iter().collect();
1163 balances.sort_by_key(|(currency, _)| currency.code.to_string());
1164
1165 for (currency, balance) in balances {
1166 let balance_key = format!("{account_key}.balance.{}", currency.code);
1167 summary.insert(format!("{balance_key}.total"), balance.total.to_string());
1168 summary.insert(format!("{balance_key}.free"), balance.free.to_string());
1169 summary.insert(format!("{balance_key}.locked"), balance.locked.to_string());
1170 }
1171 }
1172
1173 summary
1174 }
1175
1176 fn route_data_to_exchange(&self, data: &Data) {
1177 if matches!(
1178 data,
1179 Data::MarkPriceUpdate(_)
1180 | Data::IndexPriceUpdate(_)
1181 | Data::OptionGreeks(_)
1182 | Data::Custom(_)
1183 ) {
1184 return;
1185 }
1186 #[cfg(feature = "defi")]
1187 if matches!(data, Data::Defi(_)) {
1188 return;
1189 }
1190
1191 let venue = data.instrument_id().venue;
1192 if let Some(exchange) = self.venues.get(&venue) {
1193 let mut exchange_ref = exchange.borrow_mut();
1194
1195 match data {
1196 Data::Delta(delta) => exchange_ref.process_order_book_delta(*delta),
1197 Data::Deltas(deltas) => exchange_ref.process_order_book_deltas(deltas),
1198 Data::Depth10(depth) => exchange_ref.process_order_book_depth10(depth),
1199 Data::Quote(quote) => exchange_ref.process_quote_tick(quote),
1200 Data::Trade(trade) => exchange_ref.process_trade_tick(trade),
1201 Data::Bar(bar) => exchange_ref.process_bar(*bar),
1202 Data::InstrumentStatus(status) => exchange_ref.process_instrument_status(*status),
1203 Data::InstrumentClose(close) => exchange_ref.process_instrument_close(*close),
1204 Data::FundingRateUpdate(funding) => {
1205 let settlement_ns = exchange_ref.process_funding_rate(*funding);
1206 drop(exchange_ref);
1207 self.schedule_funding_settlement_if_required(
1208 exchange,
1209 funding.instrument_id,
1210 settlement_ns,
1211 );
1212 }
1213 _ => {}
1214 }
1215 } else {
1216 log::warn!("No exchange found for venue {venue}, data not routed");
1217 }
1218 }
1219
1220 fn advance_time_impl(&mut self, ts_now: UnixNanos, clocks: &[Rc<RefCell<dyn Clock>>]) {
1221 for clock in clocks {
1222 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1223 }
1224
1225 let ts_before = if ts_now.as_u64() > 0 {
1227 UnixNanos::from(ts_now.as_u64() - 1)
1228 } else {
1229 UnixNanos::default()
1230 };
1231
1232 let mut ts_last: Option<UnixNanos> = None;
1233 let mut shutdown_at: Option<UnixNanos> = None;
1234
1235 while let Some(handler) = self.accumulator.pop_next_at_or_before(ts_before) {
1236 let ts_event = handler.event.ts_event;
1237
1238 if let Some(ts) = ts_last
1240 && ts != ts_event
1241 {
1242 self.settle_venues(ts);
1243 self.run_venue_modules(ts);
1244 self.run_venue_liquidations(ts);
1245 }
1246
1247 ts_last = Some(ts_event);
1248 Self::set_all_clocks_time(clocks, ts_event);
1249 logging_clock_set_static_time(ts_event.as_u64());
1250
1251 handler.run();
1252 self.drain_command_queues();
1253
1254 if self.kernel.is_shutdown_requested() {
1257 self.accumulator.clear();
1258 shutdown_at = Some(ts_event);
1259 break;
1260 }
1261
1262 for clock in clocks {
1264 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1265 }
1266 }
1267
1268 if let Some(ts) = ts_last {
1270 self.settle_venues(ts);
1271 self.run_venue_modules(ts);
1272 self.run_venue_liquidations(ts);
1273 }
1274
1275 if let Some(ts_event) = shutdown_at {
1278 self.last_ns = ts_event;
1279 } else {
1280 Self::set_all_clocks_time(clocks, ts_now);
1281 logging_clock_set_static_time(ts_now.as_u64());
1282 }
1283 }
1284
1285 fn flush_accumulator_events(&mut self, clocks: &[Rc<RefCell<dyn Clock>>], ts_now: UnixNanos) {
1286 if self.kernel.is_shutdown_requested() {
1288 self.accumulator.clear();
1289 return;
1290 }
1291
1292 for clock in clocks {
1293 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1294 }
1295
1296 let mut ts_last: Option<UnixNanos> = None;
1297
1298 while let Some(handler) = self.accumulator.pop_next_at_or_before(ts_now) {
1299 let ts_event = handler.event.ts_event;
1300
1301 if let Some(ts) = ts_last
1303 && ts != ts_event
1304 {
1305 self.settle_venues(ts);
1306 self.run_venue_modules(ts);
1307 self.run_venue_liquidations(ts);
1308 }
1309
1310 ts_last = Some(ts_event);
1311 Self::set_all_clocks_time(clocks, ts_event);
1312 logging_clock_set_static_time(ts_event.as_u64());
1313
1314 handler.run();
1315 self.drain_command_queues();
1316
1317 if self.kernel.is_shutdown_requested() {
1320 self.accumulator.clear();
1321 break;
1322 }
1323
1324 for clock in clocks {
1326 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1327 }
1328 }
1329
1330 if let Some(ts) = ts_last {
1332 self.settle_venues(ts);
1333 self.run_venue_modules(ts);
1334 self.run_venue_liquidations(ts);
1335 }
1336 }
1337
1338 fn process_next_timer(&mut self, clocks: &[Rc<RefCell<dyn Clock>>]) -> bool {
1339 self.flush_accumulator_events(clocks, self.last_ns);
1340
1341 let mut min_next_time: Option<UnixNanos> = None;
1343
1344 for clock in clocks {
1345 let clock_ref = clock.borrow();
1346 for name in clock_ref.timer_names() {
1347 if let Some(next_time) = clock_ref.next_time_ns(name)
1348 && next_time > self.last_ns
1349 {
1350 min_next_time = Some(match min_next_time {
1351 Some(current_min) => next_time.min(current_min),
1352 None => next_time,
1353 });
1354 }
1355 }
1356 }
1357
1358 match min_next_time {
1359 None => true,
1360 Some(t) if t > self.end_ns => true,
1361 Some(t) => {
1362 self.last_ns = t;
1363 self.flush_accumulator_events(clocks, t);
1364 false
1365 }
1366 }
1367 }
1368
1369 fn set_instrument_expiration_timers(&self) -> anyhow::Result<()> {
1370 for exchange in self.venues.values() {
1371 let expirations = exchange.borrow().instrument_expirations();
1372 for (instrument_id, expiration_ns) in expirations {
1373 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
1374 }
1375 }
1376
1377 Ok(())
1378 }
1379
1380 fn set_instrument_expiration_timer(
1381 &self,
1382 exchange: &Rc<RefCell<SimulatedExchange>>,
1383 instrument_id: InstrumentId,
1384 expiration_ns: UnixNanos,
1385 ) -> anyhow::Result<()> {
1386 if expiration_ns == UnixNanos::default() {
1387 return Ok(());
1388 }
1389
1390 let timer_name = Self::instrument_expiration_timer_name(instrument_id.venue, expiration_ns);
1391 let timer_key = ustr::Ustr::from(timer_name.as_str());
1392 if self.kernel.clock.borrow().timer_exists(&timer_key) {
1393 return Ok(());
1394 }
1395
1396 let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1397 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1398 if let Some(exchange) = exchange.upgrade() {
1399 exchange
1400 .borrow_mut()
1401 .process_instrument_expirations(event.ts_event);
1402 }
1403 });
1404 let mut clock = self.kernel.clock.borrow_mut();
1405
1406 clock.set_time_alert_ns(
1407 &timer_name,
1408 expiration_ns,
1409 Some(TimeEventCallback::from(callback)),
1410 None,
1411 )?;
1412
1413 Ok(())
1414 }
1415
1416 fn instrument_expiration_timer_name(venue: Venue, expiration_ns: UnixNanos) -> String {
1417 format!("INSTRUMENT-EXPIRATION:{venue}:{expiration_ns}")
1418 }
1419
1420 fn schedule_funding_settlement_if_required(
1421 &self,
1422 exchange: &Rc<RefCell<SimulatedExchange>>,
1423 instrument_id: InstrumentId,
1424 settlement_ns: Option<UnixNanos>,
1425 ) {
1426 let Some(settlement_ns) = settlement_ns else {
1427 return;
1428 };
1429
1430 if let Err(e) = self.set_funding_settlement_timer(exchange, instrument_id, settlement_ns) {
1431 log::error!("Cannot schedule funding settlement for {instrument_id}: {e}");
1432 }
1433 }
1434
1435 fn set_funding_settlement_timer(
1436 &self,
1437 exchange: &Rc<RefCell<SimulatedExchange>>,
1438 instrument_id: InstrumentId,
1439 settlement_ns: UnixNanos,
1440 ) -> anyhow::Result<()> {
1441 let timer_name = Self::funding_settlement_timer_name(instrument_id);
1442 let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1443 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1444 if let Some(exchange) = exchange.upgrade() {
1445 exchange
1446 .borrow_mut()
1447 .process_funding_settlement(instrument_id, event.ts_event);
1448 }
1449 });
1450 let mut clock = self.kernel.clock.borrow_mut();
1451
1452 clock.set_time_alert_ns(
1453 &timer_name,
1454 settlement_ns,
1455 Some(TimeEventCallback::from(callback)),
1456 None,
1457 )?;
1458
1459 Ok(())
1460 }
1461
1462 fn funding_settlement_timer_name(instrument_id: InstrumentId) -> String {
1463 format!("FUNDING-SETTLEMENT:{instrument_id}")
1464 }
1465
1466 fn collect_all_clocks(&self) -> Vec<Rc<RefCell<dyn Clock>>> {
1467 let mut clocks = vec![self.kernel.clock.clone()];
1468 clocks.extend(self.kernel.trader.borrow().get_component_clocks());
1469 clocks
1470 }
1471
1472 fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
1473 self.venues
1474 .values()
1475 .filter_map(|v| v.borrow().max_inflight_command_ts())
1476 .max()
1477 }
1478
1479 fn settle_venues(&self, ts_now: UnixNanos) {
1480 for exchange in self.venues.values() {
1483 exchange.borrow().set_clock_time(ts_now);
1484 }
1485
1486 loop {
1492 self.drain_command_queues();
1495
1496 let active_venues: Vec<Venue> = self
1497 .venues
1498 .iter()
1499 .filter(|(_, ex)| ex.borrow().has_pending_commands(ts_now))
1500 .map(|(id, _)| *id)
1501 .collect();
1502
1503 if active_venues.is_empty() {
1504 break;
1505 }
1506
1507 for venue_id in &active_venues {
1508 self.venues[venue_id].borrow_mut().process(ts_now);
1509 }
1510 self.drain_command_queues();
1511
1512 for venue_id in &active_venues {
1513 self.venues[venue_id]
1514 .borrow_mut()
1515 .iterate_matching_engines(ts_now);
1516 }
1517
1518 self.drain_command_queues();
1521 }
1522 }
1523
1524 fn run_venue_modules(&mut self, ts_now: UnixNanos) {
1525 if self.last_module_ns == Some(ts_now) {
1526 return;
1527 }
1528 self.last_module_ns = Some(ts_now);
1529
1530 self.drain_command_queues();
1532 self.settle_venues(ts_now);
1533
1534 for exchange in self.venues.values() {
1535 exchange.borrow_mut().process_modules(ts_now);
1536 }
1537
1538 self.drain_command_queues();
1540 self.settle_venues(ts_now);
1541 }
1542
1543 fn run_venue_liquidations(&mut self, ts_now: UnixNanos) {
1544 if self.last_liquidation_ns == Some(ts_now) {
1545 return;
1546 }
1547 self.last_liquidation_ns = Some(ts_now);
1548
1549 for exchange in self.venues.values() {
1550 exchange.borrow_mut().process_liquidations(ts_now);
1551 }
1552
1553 self.drain_command_queues();
1554 self.settle_venues(ts_now);
1555 }
1556
1557 fn drain_exec_client_events(&self) {
1558 for client in &self.exec_clients {
1559 client.drain_queued_events();
1560 }
1561 }
1562
1563 fn drain_command_queues(&self) {
1564 loop {
1568 drain_trading_cmd_queue();
1569 drain_data_cmd_queue();
1570 self.drain_exec_client_events();
1571
1572 if trading_cmd_queue_is_empty() && data_cmd_queue_is_empty() {
1573 break;
1574 }
1575 }
1576 }
1577
1578 fn init_command_senders() {
1579 replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1580 replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1581 }
1582
1583 fn advance_clock_on_accumulator(
1584 accumulator: &mut TimeEventAccumulator,
1585 clock: &Rc<RefCell<dyn Clock>>,
1586 to_time_ns: UnixNanos,
1587 set_time: bool,
1588 ) {
1589 let mut clock_ref = clock.borrow_mut();
1590 let test_clock = clock_ref
1591 .as_any_mut()
1592 .downcast_mut::<TestClock>()
1593 .expect("BacktestEngine requires TestClock");
1594 accumulator.advance_clock(test_clock, to_time_ns, set_time);
1595 }
1596
1597 fn set_all_clocks_time(clocks: &[Rc<RefCell<dyn Clock>>], ts: UnixNanos) {
1598 for clock in clocks {
1599 let mut clock_ref = clock.borrow_mut();
1600 let test_clock = clock_ref
1601 .as_any_mut()
1602 .downcast_mut::<TestClock>()
1603 .expect("BacktestEngine requires TestClock");
1604 test_clock.set_time(ts);
1605 }
1606 }
1607
1608 #[rustfmt::skip]
1609 fn log_pre_run(&self) {
1610 log_info!("=================================================================", color = LogColor::Cyan);
1611 log_info!(" BACKTEST PRE-RUN", color = LogColor::Cyan);
1612 log_info!("=================================================================", color = LogColor::Cyan);
1613
1614 let cache = self.kernel.cache.borrow();
1615 for exchange in self.venues.values() {
1616 let ex = exchange.borrow();
1617 log_info!("=================================================================", color = LogColor::Cyan);
1618 log::info!(" SimulatedVenue {} ({})", ex.id, ex.account_type);
1619 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1620
1621 if let Some(account) = cache.account_for_venue(&ex.id) {
1622 log::info!("Balances starting:");
1623 let account_ref: &dyn Account = match &*account {
1624 AccountAny::Margin(margin) => margin,
1625 AccountAny::Cash(cash) => cash,
1626 AccountAny::Betting(betting) => betting,
1627 };
1628
1629 for balance in account_ref.starting_balances().values() {
1630 log::info!(" {balance}");
1631 }
1632 }
1633 }
1634
1635 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1636 }
1637
1638 #[rustfmt::skip]
1639 fn log_run(&self) {
1640 let config_id = self.run_config_id.as_deref().unwrap_or("None");
1641 let id = format_optional_uuid(self.run_id.as_ref());
1642 let start = format_optional_nanos(self.backtest_start);
1643
1644 log_info!("=================================================================", color = LogColor::Cyan);
1645 log_info!(" BACKTEST RUN", color = LogColor::Cyan);
1646 log_info!("=================================================================", color = LogColor::Cyan);
1647 log::info!("Run config ID: {config_id}");
1648 log::info!("Run ID: {id}");
1649 log::info!("Backtest start: {start}");
1650 log::info!("Data elements: {}", self.data_len);
1651 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1652 }
1653
1654 #[rustfmt::skip]
1655 fn log_post_run(&self) {
1656 let cache = self.kernel.cache.borrow();
1657 let orders = cache.orders(None, None, None, None, None);
1658 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1659 let total_orders = orders.len();
1660 let positions: Vec<Position> = cache
1661 .positions(None, None, None, None, None)
1662 .into_iter()
1663 .map(|p| p.cloned())
1664 .collect();
1665 let total_positions = Self::total_positions_with_snapshots(&cache, positions.len());
1666
1667 let config_id = self.run_config_id.as_deref().unwrap_or("None");
1668 let id = format_optional_uuid(self.run_id.as_ref());
1669 let started = format_optional_nanos(self.run_started);
1670 let finished = format_optional_nanos(self.run_finished);
1671 let elapsed = format_optional_duration(self.run_started, self.run_finished);
1672 let bt_start = format_optional_nanos(self.backtest_start);
1673 let bt_end = format_optional_nanos(self.backtest_end);
1674 let bt_range = format_optional_duration(self.backtest_start, self.backtest_end);
1675 let iterations = self.iteration.separate_with_underscores();
1676 let events = total_events.separate_with_underscores();
1677 let num_orders = total_orders.separate_with_underscores();
1678 let num_positions = total_positions.separate_with_underscores();
1679
1680 log_info!("=================================================================", color = LogColor::Cyan);
1681 log_info!(" BACKTEST POST-RUN", color = LogColor::Cyan);
1682 log_info!("=================================================================", color = LogColor::Cyan);
1683 log::info!("Run config ID: {config_id}");
1684 log::info!("Run ID: {id}");
1685 log::info!("Run started: {started}");
1686 log::info!("Run finished: {finished}");
1687 log::info!("Elapsed time: {elapsed}");
1688 log::info!("Backtest start: {bt_start}");
1689 log::info!("Backtest end: {bt_end}");
1690 log::info!("Backtest range: {bt_range}");
1691 log::info!("Iterations: {iterations}");
1692 log::info!("Total events: {events}");
1693 log::info!("Total orders: {num_orders}");
1694 log::info!("Total positions: {num_positions}");
1695
1696 if !self.config.run_analysis {
1697 return;
1698 }
1699
1700 let accounts = cache.accounts_all_owned();
1701 let mut snapshots = Vec::new();
1702 for position in &positions {
1703 snapshots.extend(cache.position_snapshots(Some(&position.id), None));
1704 }
1705 let recorded = self.kernel.portfolio.borrow().recorded_realized_pnls();
1706 let analyzer = PortfolioAnalyzer::from_accounts(&accounts, &positions, &snapshots, recorded);
1707 log_portfolio_performance(&analyzer);
1708 }
1709
1710 fn total_positions_with_snapshots(cache: &Cache, cached_positions_count: usize) -> usize {
1711 cached_positions_count + cache.position_snapshots(None, None).len()
1712 }
1713
1714 pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
1716 if self
1717 .kernel
1718 .data_engine
1719 .borrow()
1720 .registered_clients()
1721 .contains(&client_id)
1722 {
1723 return;
1724 }
1725
1726 let venue = Venue::from(client_id.as_str());
1727 let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
1728 let data_client_adapter = DataClientAdapter::new(
1729 backtest_client.client_id,
1730 None,
1731 false,
1732 false,
1733 Box::new(backtest_client),
1734 );
1735
1736 self.kernel
1737 .data_engine
1738 .borrow_mut()
1739 .register_client(data_client_adapter, None);
1740 }
1741
1742 pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
1744 let client_id = ClientId::from(venue.as_str());
1745
1746 if !self
1747 .kernel
1748 .data_engine
1749 .borrow()
1750 .registered_clients()
1751 .contains(&client_id)
1752 {
1753 let backtest_client =
1754 BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
1755 let data_client_adapter = DataClientAdapter::new(
1756 client_id,
1757 Some(venue),
1758 false,
1759 false,
1760 Box::new(backtest_client),
1761 );
1762 self.kernel
1763 .data_engine
1764 .borrow_mut()
1765 .register_client(data_client_adapter, Some(venue));
1766 }
1767 }
1768}
1769
1770fn format_optional_nanos(nanos: Option<UnixNanos>) -> String {
1771 nanos.map_or("None".to_string(), unix_nanos_to_iso8601)
1772}
1773
1774fn format_optional_uuid(uuid: Option<&UUID4>) -> String {
1775 uuid.map_or("None".to_string(), ToString::to_string)
1776}
1777
1778fn event_count_as_usize(event_count: u64) -> usize {
1779 usize::try_from(event_count).expect("execution event count fits usize")
1780}
1781
1782fn format_optional_duration(start: Option<UnixNanos>, end: Option<UnixNanos>) -> String {
1783 match (start, end) {
1784 (Some(s), Some(e)) => {
1785 let delta = e.to_datetime_utc() - s.to_datetime_utc();
1786 let days = delta.num_days().abs();
1787 let hours = delta.num_hours().abs() % 24;
1788 let minutes = delta.num_minutes().abs() % 60;
1789 let seconds = delta.num_seconds().abs() % 60;
1790 let micros = delta.subsec_nanos().unsigned_abs() / 1_000;
1791 format!("{days} days {hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
1792 }
1793 _ => "None".to_string(),
1794 }
1795}
1796
1797#[rustfmt::skip]
1798fn log_portfolio_performance(analyzer: &PortfolioAnalyzer) {
1799 log_info!("=================================================================", color = LogColor::Cyan);
1800 log_info!(" PORTFOLIO PERFORMANCE", color = LogColor::Cyan);
1801 log_info!("=================================================================", color = LogColor::Cyan);
1802
1803 for currency in analyzer.currencies() {
1804 log::info!(" PnL Statistics ({})", currency.code);
1805 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1806
1807 if let Ok(pnl_lines) = analyzer.get_stats_pnls_formatted(Some(currency), None) {
1808 for line in &pnl_lines {
1809 log::info!("{line}");
1810 }
1811 }
1812
1813 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1814 }
1815
1816 log::info!(" Returns Statistics");
1817 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1818
1819 for line in &analyzer.get_stats_returns_formatted() {
1820 log::info!("{line}");
1821 }
1822 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1823
1824 log::info!(" General Statistics");
1825 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1826
1827 for line in &analyzer.get_stats_general_formatted() {
1828 log::info!("{line}");
1829 }
1830 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1831}
1832
1833#[cfg(test)]
1834mod tests {
1835 use nautilus_common::{
1836 actor::DataActor,
1837 enums::Environment,
1838 messages::{
1839 data::{DataCommand, UnsubscribeCommand},
1840 execution::{SubmitOrder, TradingCommand},
1841 },
1842 msgbus::{
1843 self, MessagingSwitchboard,
1844 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1845 },
1846 };
1847 use nautilus_execution::engine::SnapshotAnchorer;
1848 use nautilus_model::{
1849 data::{Data, InstrumentStatus},
1850 enums::{
1851 AccountType, BookType, MarketStatus, MarketStatusAction, OmsType, OrderSide,
1852 OrderStatus, OrderType, TriggerType,
1853 },
1854 identifiers::{ClientId, PositionId, StrategyId, Venue},
1855 instruments::{
1856 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
1857 },
1858 orders::{Order, OrderAny, OrderTestBuilder},
1859 types::{Money, Price, Quantity},
1860 };
1861 use nautilus_system::{KernelEventStore, RegisteredComponents};
1862 use nautilus_trading::{
1863 nautilus_strategy,
1864 strategy::{config::StrategyConfig, core::StrategyCore},
1865 };
1866 use rstest::*;
1867 use ustr::Ustr;
1868
1869 use super::*;
1870
1871 #[derive(Debug)]
1872 struct BacktestReplayKernelEventStore {
1873 fail_restore: bool,
1874 }
1875
1876 impl KernelEventStore for BacktestReplayKernelEventStore {
1877 fn restore_parent_cache(
1878 &mut self,
1879 _instance_id: UUID4,
1880 _cache: &mut Cache,
1881 ) -> anyhow::Result<()> {
1882 if self.fail_restore {
1883 anyhow::bail!("replay restore failed");
1884 }
1885
1886 Ok(())
1887 }
1888
1889 fn open(
1890 &mut self,
1891 _instance_id: UUID4,
1892 _components: &RegisteredComponents,
1893 _environment: Environment,
1894 ) -> anyhow::Result<()> {
1895 Ok(())
1896 }
1897
1898 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1899 None
1900 }
1901
1902 fn seal(&mut self, _ts_init: UnixNanos) {}
1903
1904 fn run_id(&self) -> Option<&str> {
1905 Some("replay-child")
1906 }
1907
1908 fn parent_run_id(&self) -> Option<&str> {
1909 Some("seed-run")
1910 }
1911
1912 fn is_event_store_replay_configured(&self) -> bool {
1913 true
1914 }
1915
1916 fn is_halted(&self) -> bool {
1917 false
1918 }
1919 }
1920
1921 #[derive(Debug)]
1922 struct TestStrategy {
1923 core: StrategyCore,
1924 }
1925
1926 impl TestStrategy {
1927 fn new(config: StrategyConfig) -> Self {
1928 Self {
1929 core: StrategyCore::new(config),
1930 }
1931 }
1932 }
1933
1934 impl DataActor for TestStrategy {}
1935
1936 nautilus_strategy!(TestStrategy);
1937
1938 fn create_engine() -> BacktestEngine {
1939 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
1940 let venue_config = SimulatedVenueConfig::builder()
1941 .venue(Venue::from("BINANCE"))
1942 .oms_type(OmsType::Netting)
1943 .account_type(AccountType::Margin)
1944 .book_type(BookType::L1_MBP)
1945 .starting_balances(vec![Money::from("1_000_000 USDT")])
1946 .build()
1947 .unwrap();
1948 engine.add_venue(venue_config).unwrap();
1949 engine
1950 }
1951
1952 #[rstest]
1953 fn test_add_strategy_registers_configured_hedging_oms_type() {
1954 let mut engine = create_engine();
1955 let instrument = crypto_perpetual_ethusdt();
1956 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
1957
1958 engine
1959 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
1960 .unwrap();
1961 engine
1962 .add_strategy(TestStrategy::new(StrategyConfig {
1963 strategy_id: Some(strategy_id),
1964 oms_type: Some(OmsType::Hedging),
1965 ..Default::default()
1966 }))
1967 .unwrap();
1968
1969 let order = OrderTestBuilder::new(OrderType::Market)
1970 .trader_id(engine.trader_id())
1971 .strategy_id(strategy_id)
1972 .instrument_id(instrument.id())
1973 .quantity(Quantity::from("1.000"))
1974 .build();
1975 let position_id = PositionId::new("CUSTOM-POSITION-001");
1976
1977 engine
1978 .kernel
1979 .exec_engine
1980 .borrow()
1981 .cache()
1982 .borrow_mut()
1983 .add_order(
1984 order.clone(),
1985 Some(position_id),
1986 Some(ClientId::from("BINANCE")),
1987 true,
1988 )
1989 .unwrap();
1990
1991 let submit_order = SubmitOrder::new(
1992 order.trader_id(),
1993 Some(ClientId::from("BINANCE")),
1994 strategy_id,
1995 instrument.id(),
1996 order.client_order_id(),
1997 order.init_event().clone(),
1998 order.exec_algorithm_id(),
1999 Some(position_id),
2000 None,
2001 UUID4::new(),
2002 UnixNanos::default(),
2003 None,
2004 );
2005
2006 engine
2007 .kernel
2008 .exec_engine
2009 .borrow()
2010 .execute(TradingCommand::SubmitOrder(submit_order));
2011
2012 let exec_engine = engine.kernel.exec_engine.borrow();
2013 let cache = exec_engine.cache().borrow();
2014 let cached_order = cache
2015 .order(&order.client_order_id())
2016 .expect("Order should be cached");
2017
2018 assert_eq!(cached_order.status(), OrderStatus::Initialized);
2019 }
2020
2021 fn create_engine_with_replay_store(fail_restore: bool) -> BacktestEngine {
2022 let config = BacktestEngineConfig {
2023 load_state: true,
2024 run_analysis: false,
2025 ..Default::default()
2026 };
2027 let mut engine = BacktestEngine::new(config.clone()).unwrap();
2028 let event_store_factory = move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
2029 Ok::<_, anyhow::Error>(Box::new(BacktestReplayKernelEventStore { fail_restore })
2030 as Box<dyn KernelEventStore>)
2031 };
2032
2033 engine.kernel = NautilusKernel::new_with(
2034 "BacktestEngine".to_string(),
2035 config,
2036 None,
2037 Some(Box::new(event_store_factory)),
2038 )
2039 .unwrap();
2040 engine.instance_id = engine.kernel.instance_id;
2041 engine
2042 }
2043
2044 fn create_stop_market_order(instrument: &CryptoPerpetual) -> OrderAny {
2045 OrderTestBuilder::new(OrderType::StopMarket)
2046 .instrument_id(instrument.id())
2047 .side(OrderSide::Buy)
2048 .trigger_price(Price::from("5100.00"))
2049 .quantity(Quantity::from(1))
2050 .emulation_trigger(TriggerType::BidAsk)
2051 .build()
2052 }
2053
2054 fn create_submit_order_command(order: &OrderAny) -> SubmitOrder {
2055 SubmitOrder::new(
2056 order.trader_id(),
2057 None,
2058 order.strategy_id(),
2059 order.instrument_id(),
2060 order.client_order_id(),
2061 order.init_event().clone(),
2062 order.exec_algorithm_id(),
2063 None,
2064 None,
2065 UUID4::new(),
2066 0.into(),
2067 None, )
2069 }
2070
2071 fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
2072 let (handler, saving_handler) =
2073 get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
2074 msgbus::register_data_command_endpoint(
2075 MessagingSwitchboard::data_engine_queue_execute(),
2076 handler,
2077 );
2078 saving_handler
2079 }
2080
2081 #[rstest]
2082 fn test_run_impl_event_store_replay_skips_trader_start() {
2083 let mut engine = create_engine_with_replay_store(false);
2084
2085 engine
2086 .run_impl(
2087 Some(UnixNanos::from(0)),
2088 Some(UnixNanos::from(1)),
2089 None,
2090 true,
2091 )
2092 .unwrap();
2093
2094 assert!(engine.kernel.is_event_store_replay_configured());
2095 assert!(engine.kernel.is_event_store_replay());
2096 assert!(!engine.kernel.trader.borrow().is_running());
2097 }
2098
2099 #[rstest]
2100 fn test_run_impl_event_store_replay_config_failure_errors() {
2101 let mut engine = create_engine_with_replay_store(true);
2102
2103 let error = engine
2104 .run_impl(
2105 Some(UnixNanos::from(0)),
2106 Some(UnixNanos::from(1)),
2107 None,
2108 true,
2109 )
2110 .unwrap_err();
2111
2112 assert_eq!(error.to_string(), "event-store replay did not start");
2113 assert!(engine.kernel.is_event_store_replay_configured());
2114 assert!(!engine.kernel.is_event_store_replay());
2115 assert!(!engine.kernel.trader.borrow().is_running());
2116 }
2117
2118 #[rstest]
2119 #[case(None)]
2120 #[case(Some(true))]
2121 #[case(Some(false))]
2122 fn test_new_forces_drop_instruments_on_reset_false(
2123 crypto_perpetual_ethusdt: CryptoPerpetual,
2124 #[case] user_value: Option<bool>,
2125 ) {
2126 use nautilus_common::cache::CacheConfig;
2127
2128 let config = match user_value {
2129 None => BacktestEngineConfig::builder().build(),
2130 Some(value) => BacktestEngineConfig::builder()
2131 .cache(
2132 CacheConfig::builder()
2133 .drop_instruments_on_reset(value)
2134 .build()
2135 .unwrap(),
2136 )
2137 .build(),
2138 };
2139 let mut engine = BacktestEngine::new(config).unwrap();
2140
2141 let venue_config = SimulatedVenueConfig::builder()
2142 .venue(Venue::from("BINANCE"))
2143 .oms_type(OmsType::Netting)
2144 .account_type(AccountType::Margin)
2145 .book_type(BookType::L1_MBP)
2146 .starting_balances(vec![Money::from("1_000_000 USDT")])
2147 .build()
2148 .unwrap();
2149 engine.add_venue(venue_config).unwrap();
2150
2151 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2152 let instrument_id = instrument.id();
2153 engine.add_instrument(&instrument).unwrap();
2154
2155 engine.reset();
2156
2157 assert!(
2158 engine
2159 .kernel()
2160 .cache
2161 .borrow()
2162 .instrument(&instrument_id)
2163 .is_some(),
2164 "instrument must survive engine.reset(); user-supplied \
2165 drop_instruments_on_reset={user_value:?} must not leak through",
2166 );
2167 }
2168
2169 #[rstest]
2170 fn test_reset_resets_order_emulator_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
2171 let mut engine = create_engine();
2172 let data_commands =
2173 register_data_command_handler("DataEngine.queue_execute.backtest_reset");
2174 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt.clone());
2175 let instrument_id = instrument.id();
2176 engine.add_instrument(&instrument).unwrap();
2177 let order = create_stop_market_order(&crypto_perpetual_ethusdt);
2178 let command = create_submit_order_command(&order);
2179 engine
2180 .kernel
2181 .cache
2182 .borrow_mut()
2183 .add_order(order, None, None, false)
2184 .unwrap();
2185 let order_emulator = engine.kernel.order_emulator.emulator();
2186 let mut order_emulator = order_emulator.borrow_mut();
2187 order_emulator.cache_submit_order_command(command.clone());
2188 order_emulator.handle_submit_order(&command);
2189 drop(order_emulator);
2190 data_commands.clear();
2191
2192 engine.reset();
2193
2194 let commands = data_commands.get_messages();
2195 let emulator = engine.kernel.order_emulator.get_emulator();
2196 assert!(emulator.subscribed_quotes().is_empty());
2197 assert!(emulator.subscribed_trades().is_empty());
2198 assert!(emulator.get_matching_core(&instrument_id).is_none());
2199 assert!(commands.iter().any(|command| matches!(
2200 command,
2201 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2202 if command.instrument_id == instrument_id
2203 )));
2204 }
2205
2206 #[rstest]
2207 fn test_route_data_to_exchange_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
2208 let mut engine = create_engine();
2209 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2210 let instrument_id = instrument.id();
2211 engine.add_instrument(&instrument).unwrap();
2212
2213 let status = InstrumentStatus::new(
2214 instrument_id,
2215 MarketStatusAction::Close,
2216 UnixNanos::from(1),
2217 UnixNanos::from(1),
2218 None,
2219 None,
2220 None,
2221 None,
2222 None,
2223 );
2224
2225 engine.route_data_to_exchange(&Data::InstrumentStatus(status));
2226
2227 let exchange = engine.venues.get(&instrument_id.venue).unwrap().borrow();
2228 let market_status = exchange
2229 .get_matching_engine(&instrument_id)
2230 .unwrap()
2231 .market_status;
2232 assert_eq!(market_status, MarketStatus::Closed);
2233 }
2234}