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 indexmap::IndexMap;
28use nautilus_analysis::analyzer::PortfolioAnalyzer;
29use nautilus_common::{
30 actor::{DataActor, DataActorNative},
31 cache::Cache,
32 clock::{Clock, TestClock},
33 component::{Component, component_state},
34 enums::{ComponentState, LogColor},
35 log_info,
36 logging::{
37 logging_clock_set_realtime_mode, logging_clock_set_static_mode,
38 logging_clock_set_static_time,
39 },
40 runner::{
41 SyncDataCommandSender, SyncTradingCommandSender, data_cmd_queue_is_empty,
42 drain_data_cmd_queue, drain_trading_cmd_queue, replace_data_cmd_sender,
43 replace_exec_cmd_sender, trading_cmd_queue_is_empty,
44 },
45 timer::{TimeEvent, TimeEventCallback},
46};
47use nautilus_core::{
48 UUID4, UnixNanos, datetime::unix_nanos_to_iso8601, string::formatting::Separable,
49 time::nanos_since_unix_epoch,
50};
51use nautilus_data::client::DataClientAdapter;
52use nautilus_execution::models::fill::FillModelHandle;
53use nautilus_model::{
54 accounts::{Account, AccountAny},
55 data::{Data, HasTsInit},
56 enums::{AccountType, AggregationSource, BookType},
57 identifiers::{AccountId, ClientId, InstrumentId, StrategyId, TraderId, Venue},
58 instruments::{Instrument, InstrumentAny},
59 position::Position,
60};
61#[cfg(feature = "python")]
62use nautilus_system::trader::Trader;
63use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
64use nautilus_trading::{
65 ExecutionAlgorithm, ExecutionAlgorithmNative,
66 strategy::{Strategy, StrategyNative},
67};
68
69use crate::{
70 accumulator::TimeEventAccumulator,
71 config::{BacktestEngineConfig, SimulatedVenueConfig},
72 data_client::BacktestDataClient,
73 data_iterator::BacktestDataIterator,
74 exchange::{SettlementScope, SimulatedExchange},
75 execution_client::BacktestExecutionClient,
76 result::{
77 BacktestResult, CanonicalBacktestResult, CanonicalBacktestState, CanonicalDiagnostic,
78 CanonicalDiagnosticCode, CanonicalRunOutcome,
79 },
80};
81
82pub struct BacktestEngine {
95 kernel: NautilusKernel,
96 instance_id: UUID4,
97 config: BacktestEngineConfig,
98 accumulator: TimeEventAccumulator,
99 run_config_id: Option<String>,
100 run_id: Option<UUID4>,
101 venues: IndexMap<Venue, Rc<RefCell<SimulatedExchange>>>,
102 exec_clients: Vec<BacktestExecutionClient>,
103 has_data: AHashSet<InstrumentId>,
104 has_book_data: AHashSet<InstrumentId>,
105 has_book_processed: AHashSet<InstrumentId>,
106 data_iterator: BacktestDataIterator,
107 data_len: usize,
108 data_stream_counter: usize,
109 ts_first: Option<UnixNanos>,
110 ts_last_data: Option<UnixNanos>,
111 sorted: bool,
112 iteration: usize,
113 force_stop: bool,
114 last_ns: UnixNanos,
115 last_module_ns: Option<UnixNanos>,
116 last_liquidation_ns: Option<UnixNanos>,
117 end_ns: UnixNanos,
118 run_started: Option<UnixNanos>,
119 run_finished: Option<UnixNanos>,
120 backtest_start: Option<UnixNanos>,
121 backtest_end: Option<UnixNanos>,
122 funding_error: Option<String>,
123}
124
125impl Debug for BacktestEngine {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct(stringify!(BacktestEngine))
128 .field("instance_id", &self.instance_id)
129 .field("run_config_id", &self.run_config_id)
130 .field("run_id", &self.run_id)
131 .finish_non_exhaustive()
132 }
133}
134
135impl BacktestEngine {
136 pub fn new(mut config: BacktestEngineConfig) -> anyhow::Result<Self> {
142 let mut cache_config = config.cache.unwrap_or_default();
145 cache_config.drop_instruments_on_reset = false;
146 config.cache = Some(cache_config);
147 let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
148 let instance_id = kernel.instance_id;
149 #[cfg(feature = "python")]
150 if let Some(controller) = config.controller.as_ref() {
151 Trader::add_controller_from_importable_config(&kernel.trader, controller)?;
152 }
153 #[cfg(not(feature = "python"))]
154 if let Some(controller) = config.controller.as_ref() {
155 anyhow::bail!(
156 "BacktestEngineConfig.controller for importable controller '{}' requires the python feature",
157 controller.controller_path
158 );
159 }
160
161 Ok(Self {
162 kernel,
163 instance_id,
164 config,
165 accumulator: TimeEventAccumulator::new(),
166 run_config_id: None,
167 run_id: None,
168 venues: IndexMap::new(),
169 exec_clients: Vec::new(),
170 has_data: AHashSet::new(),
171 has_book_data: AHashSet::new(),
172 has_book_processed: AHashSet::new(),
173 data_iterator: BacktestDataIterator::new(),
174 data_len: 0,
175 data_stream_counter: 0,
176 ts_first: None,
177 ts_last_data: None,
178 sorted: true,
179 iteration: 0,
180 force_stop: false,
181 last_ns: UnixNanos::default(),
182 last_module_ns: None,
183 last_liquidation_ns: None,
184 end_ns: UnixNanos::default(),
185 run_started: None,
186 run_finished: None,
187 backtest_start: None,
188 backtest_end: None,
189 funding_error: None,
190 })
191 }
192
193 #[must_use]
195 pub const fn kernel(&self) -> &NautilusKernel {
196 &self.kernel
197 }
198
199 pub fn kernel_mut(&mut self) -> &mut NautilusKernel {
201 &mut self.kernel
202 }
203
204 #[must_use]
206 pub fn trader_id(&self) -> TraderId {
207 self.kernel.trader_id()
208 }
209
210 #[must_use]
212 pub fn machine_id(&self) -> &str {
213 self.kernel.machine_id()
214 }
215
216 #[must_use]
218 pub fn instance_id(&self) -> UUID4 {
219 self.instance_id
220 }
221
222 #[must_use]
224 pub fn iteration(&self) -> usize {
225 self.iteration
226 }
227
228 #[must_use]
230 pub fn run_config_id(&self) -> Option<&str> {
231 self.run_config_id.as_deref()
232 }
233
234 #[must_use]
236 pub const fn run_id(&self) -> Option<UUID4> {
237 self.run_id
238 }
239
240 #[must_use]
242 pub const fn run_started(&self) -> Option<UnixNanos> {
243 self.run_started
244 }
245
246 #[must_use]
248 pub const fn run_finished(&self) -> Option<UnixNanos> {
249 self.run_finished
250 }
251
252 #[must_use]
254 pub const fn backtest_start(&self) -> Option<UnixNanos> {
255 self.backtest_start
256 }
257
258 #[must_use]
260 pub const fn backtest_end(&self) -> Option<UnixNanos> {
261 self.backtest_end
262 }
263
264 #[must_use]
266 pub fn list_venues(&self) -> Vec<Venue> {
267 self.venues.keys().copied().collect()
268 }
269
270 pub fn add_venue(&mut self, config: SimulatedVenueConfig) -> anyhow::Result<()> {
275 let venue = config.venue;
278 if self.venues.contains_key(&venue) {
279 anyhow::bail!("Venue {venue} is already registered");
280 }
281
282 let routing = Some(config.routing);
283 let frozen_account = Some(config.frozen_account);
284 let use_message_queue = config.use_message_queue;
285
286 let exchange =
287 SimulatedExchange::new(config, self.kernel.cache.clone(), self.kernel.clock.clone())?;
288 let exchange = Rc::new(RefCell::new(exchange));
289
290 let account_id = AccountId::from(format!("{venue}-001").as_str());
291
292 let exec_client = BacktestExecutionClient::new(
293 self.config.trader_id(),
294 account_id,
295 &exchange,
296 self.kernel.cache.clone(),
297 self.kernel.clock.clone(),
298 routing,
299 frozen_account,
300 );
301
302 if !use_message_queue {
303 exchange
304 .borrow_mut()
305 .set_event_handler(exec_client.order_event_handler());
306 }
307
308 exchange
309 .borrow_mut()
310 .register_client(Rc::new(exec_client.clone()));
311
312 self.kernel
313 .exec_engine
314 .borrow_mut()
315 .register_client(Box::new(exec_client.clone()))?;
316
317 SimulatedExchange::register_spread_quote_endpoint(&exchange);
318 self.venues.insert(venue, exchange);
319 self.exec_clients.push(exec_client);
320
321 log::info!("Adding exchange {venue} to engine");
322
323 Ok(())
324 }
325
326 pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModelHandle) {
328 if let Some(exchange) = self.venues.get_mut(&venue) {
329 exchange.borrow_mut().set_fill_model(fill_model);
330 } else {
331 log::warn!(
332 "BacktestEngine::change_fill_model called for unknown venue {venue}, ignoring"
333 );
334 }
335 }
336
337 pub fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
345 let instrument_id = instrument.id();
346 if let Some(exchange) = self.venues.get(&instrument.id().venue) {
347 let previous_expiration_ns = exchange.borrow().instrument_expiration(instrument_id);
348
349 if matches!(
350 instrument,
351 InstrumentAny::CurrencyPair(_) | InstrumentAny::TokenizedAsset(_)
352 ) && exchange.borrow().account_type != AccountType::Margin
353 && exchange.borrow().base_currency.is_some()
354 {
355 anyhow::bail!(
356 "Cannot add a multi-currency spot instrument {instrument_id} for a venue with a single-currency CASH account"
357 )
358 }
359 exchange.borrow_mut().add_instrument(instrument.clone())?;
360 if let Some(expiration_ns) = instrument.expiration_ns() {
361 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
362 }
363
364 if let Some(previous_expiration_ns) = previous_expiration_ns
365 && instrument.expiration_ns() != Some(previous_expiration_ns)
366 && !exchange
367 .borrow()
368 .has_unprocessed_instrument_expiration(previous_expiration_ns)
369 {
370 let timer_name = Self::instrument_expiration_timer_name(
371 instrument_id.venue,
372 previous_expiration_ns,
373 );
374 self.kernel.clock.borrow_mut().cancel_timer(&timer_name);
375 }
376 } else {
377 anyhow::bail!(
378 "Cannot add an `Instrument` object without first adding its associated venue {}",
379 instrument.id().venue
380 )
381 }
382
383 self.add_market_data_client_if_not_exists(instrument.id().venue);
384
385 self.kernel
386 .data_engine
387 .borrow_mut()
388 .process(instrument as &dyn Any);
389 log::info!(
390 "Added instrument {} to exchange {}",
391 instrument_id,
392 instrument_id.venue
393 );
394 Ok(())
395 }
396
397 pub fn add_data(
409 &mut self,
410 data: Vec<Data>,
411 client_id: Option<ClientId>,
412 validate: bool,
413 sort: bool,
414 ) -> anyhow::Result<()> {
415 #[cfg(not(feature = "defi"))]
416 let _ = client_id;
417
418 anyhow::ensure!(!data.is_empty(), "data was empty");
419
420 let count = data.len();
421 let mut to_add = data;
422
423 if sort {
424 to_add.sort_by_key(HasTsInit::ts_init);
425 }
426
427 if validate {
428 let first = &to_add[0];
431 #[cfg(feature = "defi")]
432 let first_is_defi = matches!(first, Data::Defi(_));
433 #[cfg(not(feature = "defi"))]
434 let first_is_defi = false;
435
436 if !first_is_defi && !matches!(first, Data::Custom(_)) {
437 let first_instrument_id = first.instrument_id();
438 anyhow::ensure!(
439 self.kernel
440 .cache
441 .borrow()
442 .instrument(&first_instrument_id)
443 .is_some(),
444 "Instrument {first_instrument_id} for the given data not found in the cache. \
445 Add the instrument through `add_instrument()` prior to adding related data."
446 );
447
448 if let Data::Bar(bar) = first {
449 anyhow::ensure!(
450 bar.bar_type.aggregation_source() == AggregationSource::External,
451 "bar_type.aggregation_source must be External, was {:?}",
452 bar.bar_type.aggregation_source(),
453 );
454 }
455 }
456 }
457
458 let mut batch_min_ts: Option<UnixNanos> = None;
464 let mut batch_max_ts: Option<UnixNanos> = None;
465
466 #[cfg(feature = "defi")]
467 if to_add.iter().any(|item| matches!(item, Data::Defi(_))) {
468 self.add_defi_data_client_if_not_exists(client_id);
469 }
470
471 for item in &to_add {
472 let ts = item.ts_init();
473 batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
474 batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
475
476 #[cfg(feature = "defi")]
477 if matches!(item, Data::Defi(_)) {
478 continue;
479 }
480
481 if matches!(item, Data::Custom(_)) {
482 continue;
484 }
485
486 let instr_id = item.instrument_id();
487 self.has_data.insert(instr_id);
488
489 if item.is_order_book_data() {
490 self.has_book_data.insert(instr_id);
491 }
492
493 self.add_market_data_client_if_not_exists(instr_id.venue);
494 }
495
496 if let Some(ts) = batch_min_ts
497 && self.ts_first.is_none_or(|t| ts < t)
498 {
499 self.ts_first = Some(ts);
500 }
501
502 if let Some(ts) = batch_max_ts
503 && self.ts_last_data.is_none_or(|t| ts > t)
504 {
505 self.ts_last_data = Some(ts);
506 }
507
508 self.data_len += count;
509 let stream_name = format!("backtest_data_{}", self.data_stream_counter);
510 self.data_stream_counter += 1;
511 self.data_iterator.add_data(&stream_name, to_add, true);
512
513 self.sorted = sort;
514
515 log::info!(
516 "Added {count} data element{} to BacktestEngine ({} total)",
517 if count == 1 { "" } else { "s" },
518 self.data_len,
519 );
520
521 Ok(())
522 }
523
524 pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
531 where
532 T: DataActor + DataActorNative + Component + Debug + 'static,
533 {
534 self.kernel.trader.borrow_mut().add_actor(actor)
535 }
536
537 pub fn add_actors<T>(&mut self, actors: Vec<T>) -> anyhow::Result<()>
543 where
544 T: DataActor + DataActorNative + Component + Debug + 'static,
545 {
546 for actor in actors {
547 self.add_actor(actor)?;
548 }
549 Ok(())
550 }
551
552 pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
559 where
560 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
561 {
562 let strategy_id = self
563 .kernel
564 .trader
565 .borrow()
566 .prepare_strategy_for_registration(&mut strategy)?;
567 let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
568
569 self.kernel.trader.borrow_mut().add_strategy(strategy)?;
570
571 if let Some(oms_type) = oms_type {
572 self.kernel
573 .exec_engine
574 .borrow_mut()
575 .register_oms_type(strategy_id, oms_type);
576 }
577
578 Ok(())
579 }
580
581 pub fn add_strategies<T>(&mut self, strategies: Vec<T>) -> anyhow::Result<()>
587 where
588 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
589 {
590 for strategy in strategies {
591 self.add_strategy(strategy)?;
592 }
593 Ok(())
594 }
595
596 pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
602 where
603 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
604 {
605 self.kernel
606 .trader
607 .borrow_mut()
608 .add_exec_algorithm(exec_algorithm)
609 }
610
611 pub fn add_exec_algorithms<T>(&mut self, exec_algorithms: Vec<T>) -> anyhow::Result<()>
618 where
619 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
620 {
621 for exec_algorithm in exec_algorithms {
622 self.add_exec_algorithm(exec_algorithm)?;
623 }
624 Ok(())
625 }
626
627 pub fn run(
649 &mut self,
650 start: Option<UnixNanos>,
651 end: Option<UnixNanos>,
652 run_config_id: Option<String>,
653 streaming: bool,
654 ) -> anyhow::Result<()> {
655 if let Some(error) = &self.funding_error {
656 anyhow::bail!("{error}");
657 }
658 self.check_module_errors()?;
659
660 if let Err(e) = self.run_impl(start, end, run_config_id, streaming) {
661 if self.funding_error.is_some()
662 || self
663 .venues
664 .values()
665 .any(|exchange| exchange.borrow().has_module_error())
666 {
667 self.abort_run();
668 }
669 return Err(e);
670 }
671
672 if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
679 self.end()?;
680 }
681
682 Ok(())
683 }
684
685 fn run_impl(
686 &mut self,
687 start: Option<UnixNanos>,
688 end: Option<UnixNanos>,
689 run_config_id: Option<String>,
690 streaming: bool,
691 ) -> anyhow::Result<()> {
692 anyhow::ensure!(
693 self.sorted,
694 "Data has been added but not sorted, call `engine.sort_data()` or use \
695 `engine.add_data(..., sort=true)` before running"
696 );
697
698 for exchange in self.venues.values() {
699 let exchange = exchange.borrow();
700 let book_type_has_depth = exchange.book_type() as u8 > BookType::L1_MBP as u8;
701 if !book_type_has_depth {
702 continue;
703 }
704
705 for instrument_id in exchange.instrument_ids() {
706 let has_data = self.has_data.contains(instrument_id);
707 let missing_book_data = !self.has_book_data.contains(instrument_id)
708 && !self.has_book_processed.contains(instrument_id);
709
710 if has_data && missing_book_data {
711 anyhow::bail!(
712 "No order book data found for instrument '{instrument_id}' when `book_type` \
713 is '{:?}'. Set the venue `book_type` to 'L1_MBP' (for top-of-book data \
714 like quotes, trades, and bars) or provide order book data for this \
715 instrument.",
716 exchange.book_type()
717 );
718 }
719 }
720 }
721
722 let start_ns = start.unwrap_or_else(|| self.ts_first.unwrap_or_default());
724 let end_ns = end.unwrap_or_else(|| self.ts_last_data.unwrap_or(start_ns));
725 anyhow::ensure!(start_ns <= end_ns, "start was > end");
726 self.end_ns = end_ns;
727 self.last_ns = start_ns;
728 self.last_module_ns = None;
729
730 let clocks = self.collect_all_clocks();
732 Self::set_all_clocks_time(&clocks, start_ns);
733
734 if self.iteration == 0 {
736 self.set_instrument_expiration_timers()?;
737
738 self.run_config_id = run_config_id;
739 self.run_id = Some(UUID4::new());
740 self.run_started = Some(UnixNanos::from(nanos_since_unix_epoch()));
741 self.backtest_start = Some(start_ns);
742
743 for exchange in self.venues.values() {
744 let mut ex = exchange.borrow_mut();
745 ex.initialize_account();
746 ex.load_open_orders();
747 }
748
749 Self::set_all_clocks_time(&clocks, start_ns);
751
752 self.force_stop = false;
754 self.kernel.reset_shutdown_flag();
755
756 Self::init_command_senders();
758
759 logging_clock_set_static_mode();
761 logging_clock_set_static_time(start_ns.as_u64());
762
763 self.kernel.start();
765 if self.kernel.is_event_store_replay() {
766 self.log_pre_run();
767 return Ok(());
768 }
769
770 if self.kernel.is_event_store_replay_configured() {
771 anyhow::bail!("event-store replay did not start");
772 }
773 self.kernel.start_trader()?;
774
775 while !data_cmd_queue_is_empty() {
778 drain_data_cmd_queue();
779 }
780
781 self.log_pre_run();
782 }
783
784 self.log_run();
785
786 while let Some(d) = self.data_iterator.peek() {
788 if d.ts_init() >= start_ns {
789 break;
790 }
791 self.data_iterator.next_item();
792 }
793
794 if let Some(d) = self.data_iterator.peek() {
796 let ts = d.ts_init();
797 self.last_ns = if ts.as_u64() > 0 {
798 UnixNanos::from(ts.as_u64() - 1)
799 } else {
800 UnixNanos::default()
801 };
802 } else {
803 self.last_ns = start_ns;
804 }
805
806 loop {
807 if self.kernel.is_shutdown_requested() {
808 log::info!("Shutdown requested via ShutdownSystem, ending backtest");
809 self.force_stop = true;
810 }
811
812 if self.force_stop {
813 log::info!("Force stop triggered, ending backtest");
814 break;
815 }
816
817 let Some(d) = self.data_iterator.peek() else {
818 if streaming {
819 break;
823 }
824 let done = self.process_next_timer(&clocks)?;
825 if self.data_iterator.peek().is_none() && done {
826 break;
827 }
828 continue;
829 };
830
831 let ts_init = d.ts_init();
832
833 if ts_init > end_ns {
834 break;
835 }
836
837 let d = self.data_iterator.next_item().unwrap();
838 let settlement_scope = Self::settlement_scope(&d);
839
840 if ts_init > self.last_ns {
841 self.advance_time_impl(ts_init, &clocks)?;
842 }
843
844 if self.kernel.is_shutdown_requested() {
847 self.force_stop = true;
848 break;
849 }
850
851 self.route_data_to_exchange(&d)?;
852 self.kernel.data_engine.borrow_mut().process_data(d);
853
854 self.drain_command_queues();
856 self.settle_venues(ts_init, settlement_scope);
857
858 let prev_last_ns = self.last_ns;
859 if self
861 .data_iterator
862 .peek()
863 .is_none_or(|next| next.ts_init() > prev_last_ns)
864 {
865 self.flush_accumulator_events(&clocks, prev_last_ns)?;
866 self.finalize_timestamp(&clocks, prev_last_ns, settlement_scope)?;
867 }
868
869 self.iteration += 1;
870 }
871
872 if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
873 let ts_now = self.kernel.clock.borrow().timestamp_ns();
874 self.finalize_timestamp(&clocks, ts_now, SettlementScope::All)?;
875 }
876
877 let flush_ts = if streaming || self.force_stop || self.kernel.is_shutdown_requested() {
880 self.last_ns
881 } else {
882 end_ns
883 };
884 self.flush_accumulator_events(&clocks, flush_ts)?;
885
886 Ok(())
887 }
888
889 fn settlement_scope(data: &Data) -> SettlementScope {
890 if matches!(
891 data,
892 Data::MarkPrice(_) | Data::IndexPrice(_) | Data::OptionGreeks(_) | Data::Custom(_)
893 ) {
894 return SettlementScope::Data(None);
895 }
896 #[cfg(feature = "defi")]
897 if matches!(data, Data::Defi(_)) {
898 return SettlementScope::Data(None);
899 }
900
901 SettlementScope::Data(Some(data.instrument_id()))
902 }
903
904 fn abort_run(&mut self) {
905 self.force_stop = true;
906 self.accumulator.clear();
907 self.kernel.stop_trader();
908 self.kernel.data_engine.borrow_mut().stop();
909 self.kernel.risk_engine.borrow_mut().stop();
910 self.kernel.exec_engine.borrow_mut().stop();
911 self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
912 self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
913 logging_clock_set_realtime_mode();
914 }
915
916 pub fn end(&mut self) -> anyhow::Result<()> {
923 if let Some(error) = &self.funding_error {
924 anyhow::bail!("{error}");
925 }
926
927 if self.end_ns.as_u64() > 0 {
934 let clocks = self.collect_all_clocks();
935 let flush_ts = if self.force_stop || self.kernel.is_shutdown_requested() {
936 self.last_ns
937 } else {
938 self.end_ns
939 };
940
941 if let Err(e) = self.flush_accumulator_events(&clocks, flush_ts) {
942 if self.funding_error.is_some()
943 || self
944 .venues
945 .values()
946 .any(|exchange| exchange.borrow().has_module_error())
947 {
948 self.abort_run();
949 }
950 return Err(e);
951 }
952 }
953
954 let mut ts_now = self.kernel.clock.borrow().timestamp_ns();
957 self.settle_venues(ts_now, SettlementScope::All);
958
959 self.kernel.stop_trader();
960
961 self.drain_command_queues();
966
967 if let Some(max_inflight_ts) = self.max_inflight_command_ts()
970 && max_inflight_ts > ts_now
971 {
972 ts_now = max_inflight_ts;
973 let clocks = self.collect_all_clocks();
974 Self::set_all_clocks_time(&clocks, ts_now);
975 }
976
977 self.settle_venues(ts_now, SettlementScope::All);
978
979 for strategy_id in self.running_strategy_ids() {
980 log::error!(
981 "Strategy {strategy_id} is still RUNNING after the backtest end sequence; its stop did not complete",
982 );
983 }
984
985 let save_result = self.kernel.save_trader_state();
986 let diagnostics_result = self
987 .venues
988 .values()
989 .try_for_each(|exchange| exchange.borrow().log_diagnostics());
990 self.kernel.portfolio.borrow_mut().finalize_equity_curve();
991
992 self.kernel.data_engine.borrow_mut().stop();
994 self.kernel.risk_engine.borrow_mut().stop();
995 self.kernel.exec_engine.borrow_mut().stop();
996
997 let streaming_result = self.kernel.flush_streaming();
998
999 self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
1000 self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
1001
1002 logging_clock_set_realtime_mode();
1004
1005 self.log_post_run();
1006 save_result?;
1007 diagnostics_result?;
1008 streaming_result
1009 }
1010
1011 fn running_strategy_ids(&self) -> Vec<StrategyId> {
1018 self.kernel
1019 .trader
1020 .borrow()
1021 .strategy_ids()
1022 .into_iter()
1023 .filter(|strategy_id| match component_state(&strategy_id.inner()) {
1024 Ok(state) => matches!(state, ComponentState::Running),
1025 Err(e) => {
1026 log::warn!("Cannot resolve stop state for strategy {strategy_id}: {e}");
1027 false
1028 }
1029 })
1030 .collect()
1031 }
1032
1033 pub fn reset(&mut self) -> anyhow::Result<()> {
1042 log::debug!("Resetting");
1043
1044 let mut reset_error = None;
1045
1046 if self.kernel.trader.borrow().is_running()
1047 && let Err(e) = self.end()
1048 {
1049 reset_error = Some(e);
1050 }
1051
1052 self.kernel.data_engine.borrow_mut().stop();
1054 self.kernel.data_engine.borrow_mut().reset();
1055
1056 self.kernel.exec_engine.borrow_mut().stop();
1057
1058 for exchange in self.venues.values() {
1061 if let Err(e) = exchange.borrow_mut().reset()
1062 && reset_error.is_none()
1063 {
1064 reset_error = Some(e);
1065 }
1066 }
1067 self.kernel.exec_engine.borrow_mut().reset();
1068
1069 self.kernel.risk_engine.borrow_mut().stop();
1070 self.kernel.risk_engine.borrow_mut().reset();
1071
1072 self.kernel.order_emulator.reset();
1073
1074 if let Err(e) = self.kernel.trader.borrow_mut().reset() {
1076 log::error!("Error resetting trader: {e:?}");
1077 }
1078
1079 self.kernel.portfolio.borrow_mut().reset();
1080
1081 self.run_config_id = None;
1083 self.run_id = None;
1084 self.run_started = None;
1085 self.run_finished = None;
1086 self.backtest_start = None;
1087 self.backtest_end = None;
1088 self.funding_error = None;
1089 self.iteration = 0;
1090 self.force_stop = false;
1091 self.last_ns = UnixNanos::default();
1092 self.last_module_ns = None;
1093 self.last_liquidation_ns = None;
1094 self.end_ns = UnixNanos::default();
1095 self.has_book_processed.clear();
1096
1097 self.accumulator.clear();
1098 self.cancel_funding_settlement_timers();
1099
1100 self.data_iterator.reset_all_cursors();
1102
1103 log::info!("Reset");
1104
1105 if let Some(e) = reset_error {
1106 return Err(e);
1107 }
1108 Ok(())
1109 }
1110
1111 pub fn sort_data(&mut self) {
1116 self.sorted = true;
1120 log::info!("Data sort requested (iterator merges streams by replay timestamp)");
1121 }
1122
1123 pub fn clear_data(&mut self) {
1125 self.has_data.clear();
1126 self.has_book_data.clear();
1127 self.data_iterator = BacktestDataIterator::new();
1128 self.data_len = 0;
1129 self.data_stream_counter = 0;
1130 self.ts_first = None;
1131 self.ts_last_data = None;
1132 self.sorted = true;
1133 }
1134
1135 pub fn clear_actors(&mut self) -> anyhow::Result<()> {
1141 self.kernel.trader.borrow_mut().clear_actors()
1142 }
1143
1144 pub fn clear_strategies(&mut self) -> anyhow::Result<()> {
1150 self.kernel.trader.borrow_mut().clear_strategies()
1151 }
1152
1153 pub fn clear_exec_algorithms(&mut self) -> anyhow::Result<()> {
1159 self.kernel.trader.borrow_mut().clear_exec_algorithms()
1160 }
1161
1162 pub fn dispose(&mut self) {
1164 self.clear_data();
1165 self.accumulator.clear();
1166 self.kernel.dispose();
1167 }
1168
1169 #[must_use]
1171 pub fn get_result(&self) -> BacktestResult {
1172 let elapsed_time_secs = match (self.backtest_start, self.backtest_end) {
1173 (Some(start), Some(end)) => (end.as_f64() - start.as_f64()) / 1_000_000_000.0,
1174 _ => 0.0,
1175 };
1176
1177 let cache = self.kernel.cache.borrow();
1178 let orders = cache.orders(None, None, None, None, None);
1179 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1180 let total_orders = orders.len();
1181 let positions: Vec<Position> = cache
1182 .positions(None, None, None, None, None)
1183 .into_iter()
1184 .map(|p| p.cloned())
1185 .collect();
1186 let cached_positions_count = positions.len();
1187 let snapshot_positions = cache.position_snapshots(None, None).len();
1188 let total_positions = Self::total_positions_with_snapshots(&cache, cached_positions_count);
1189 let summary = self.build_result_summary(
1190 &cache,
1191 total_events,
1192 total_orders,
1193 cached_positions_count,
1194 snapshot_positions,
1195 );
1196
1197 let stats = self.kernel.portfolio.borrow().statistics();
1198 let stats_pnls = stats.pnls;
1199 let stats_returns = stats.returns;
1200 let stats_general = stats.general;
1201 let returns_series = stats.returns_series;
1202
1203 BacktestResult {
1204 trader_id: self.config.trader_id().to_string(),
1205 machine_id: self.kernel.machine_id.clone(),
1206 instance_id: self.instance_id,
1207 run_config_id: self.run_config_id.clone(),
1208 run_id: self.run_id,
1209 run_started: self.run_started,
1210 run_finished: self.run_finished,
1211 backtest_start: self.backtest_start,
1212 backtest_end: self.backtest_end,
1213 elapsed_time_secs,
1214 iterations: self.iteration,
1215 total_events,
1216 total_orders,
1217 total_positions,
1218 summary,
1219 stats_pnls,
1220 stats_returns,
1221 stats_general,
1222 returns_series,
1223 }
1224 }
1225
1226 pub fn get_canonical_result(&self) -> anyhow::Result<CanonicalBacktestResult> {
1236 let result = self.get_result();
1237 let cache = self.kernel.cache.borrow();
1238 let orders = cache
1239 .orders(None, None, None, None, None)
1240 .into_iter()
1241 .map(|order| order.cloned())
1242 .collect();
1243 let positions = cache
1244 .positions(None, None, None, None, None)
1245 .into_iter()
1246 .map(|position| position.cloned())
1247 .collect();
1248 let position_snapshots = cache.position_snapshots(None, None);
1249 let accounts = cache.accounts_all_owned();
1250 drop(cache);
1251
1252 let portfolio = self.kernel.portfolio.borrow();
1253 let mut portfolio_snapshots = Vec::new();
1254 for account in &accounts {
1255 portfolio_snapshots.extend(portfolio.snapshots(&account.id()));
1256 }
1257 drop(portfolio);
1258
1259 let trader = self.kernel.trader.borrow();
1260 let trader_state = trader.state().to_string();
1261 let actor_ids = trader
1262 .actor_ids()
1263 .into_iter()
1264 .map(|id| id.to_string())
1265 .collect();
1266 let strategy_ids = trader
1267 .strategy_ids()
1268 .into_iter()
1269 .map(|id| id.to_string())
1270 .collect();
1271 let exec_algorithm_ids = trader
1272 .exec_algorithm_ids()
1273 .into_iter()
1274 .map(|id| id.to_string())
1275 .collect();
1276 drop(trader);
1277
1278 let outcome = if self.funding_error.is_some() {
1279 CanonicalRunOutcome::Failed
1280 } else if self.run_finished.is_none() {
1281 CanonicalRunOutcome::Incomplete
1282 } else if self.force_stop || self.kernel.is_shutdown_requested() {
1283 CanonicalRunOutcome::Stopped
1284 } else {
1285 CanonicalRunOutcome::Completed
1286 };
1287 let diagnostics = self
1288 .funding_error
1289 .as_ref()
1290 .map(|_| CanonicalDiagnostic {
1291 code: CanonicalDiagnosticCode::FundingSettlementFailed,
1292 })
1293 .into_iter()
1294 .collect();
1295 let statistics = nautilus_analysis::PortfolioStatistics {
1296 pnls: result.stats_pnls,
1297 returns: result.stats_returns,
1298 general: result.stats_general,
1299 returns_series: result.returns_series,
1300 };
1301
1302 CanonicalBacktestResult::from_state(CanonicalBacktestState {
1303 trader_id: result.trader_id,
1304 run_config_id: result.run_config_id,
1305 backtest_start: result.backtest_start,
1306 backtest_end: result.backtest_end,
1307 iterations: result.iterations,
1308 total_events: result.total_events,
1309 total_orders: result.total_orders,
1310 total_positions: result.total_positions,
1311 outcome,
1312 diagnostics,
1313 trader_state,
1314 actor_ids,
1315 strategy_ids,
1316 exec_algorithm_ids,
1317 summary: result.summary.into_iter().collect(),
1318 orders,
1319 positions,
1320 position_snapshots,
1321 accounts,
1322 portfolio_snapshots,
1323 statistics,
1324 })
1325 }
1326
1327 fn build_result_summary(
1328 &self,
1329 cache: &Cache,
1330 total_events: usize,
1331 total_orders: usize,
1332 cached_positions_count: usize,
1333 snapshot_positions: usize,
1334 ) -> AHashMap<String, String> {
1335 let mut summary = AHashMap::new();
1336 summary.insert("iterations".to_string(), self.iteration.to_string());
1337 summary.insert("total_events".to_string(), total_events.to_string());
1338 summary.insert("orders.total".to_string(), total_orders.to_string());
1339 summary.insert(
1340 "orders.open".to_string(),
1341 cache
1342 .orders_open_count(None, None, None, None, None)
1343 .to_string(),
1344 );
1345 summary.insert(
1346 "orders.closed".to_string(),
1347 cache
1348 .orders_closed_count(None, None, None, None, None)
1349 .to_string(),
1350 );
1351 summary.insert(
1352 "orders.emulated".to_string(),
1353 cache
1354 .orders_emulated_count(None, None, None, None, None)
1355 .to_string(),
1356 );
1357 summary.insert(
1358 "orders.inflight".to_string(),
1359 cache
1360 .orders_inflight_count(None, None, None, None, None)
1361 .to_string(),
1362 );
1363 summary.insert(
1364 "positions.total".to_string(),
1365 cached_positions_count.to_string(),
1366 );
1367 summary.insert(
1368 "positions.open".to_string(),
1369 cache
1370 .positions_open_count(None, None, None, None, None)
1371 .to_string(),
1372 );
1373 summary.insert(
1374 "positions.closed".to_string(),
1375 cache
1376 .positions_closed_count(None, None, None, None, None)
1377 .to_string(),
1378 );
1379 summary.insert(
1380 "positions.snapshots".to_string(),
1381 snapshot_positions.to_string(),
1382 );
1383 summary.insert(
1384 "positions.total_with_snapshots".to_string(),
1385 (cached_positions_count + snapshot_positions).to_string(),
1386 );
1387
1388 let mut venues: Vec<Venue> = self.venues.keys().copied().collect();
1389 venues.sort_by_key(ToString::to_string);
1390 summary.insert("venues.total".to_string(), venues.len().to_string());
1391
1392 for venue in venues {
1393 let Some(account) = cache.account_for_venue(&venue) else {
1394 continue;
1395 };
1396
1397 let venue_key = venue.to_string();
1398 let account_key = format!("account.{venue_key}");
1399 summary.insert(format!("{account_key}.id"), account.id().to_string());
1400 summary.insert(
1401 format!("{account_key}.type"),
1402 account.account_type().to_string(),
1403 );
1404 summary.insert(
1405 format!("{account_key}.base_currency"),
1406 account
1407 .base_currency()
1408 .map_or_else(|| "None".to_string(), |currency| currency.code.to_string()),
1409 );
1410 summary.insert(
1411 format!("{account_key}.event_count"),
1412 account.event_count().to_string(),
1413 );
1414
1415 let mut balances: Vec<_> = account.balances().into_iter().collect();
1416 balances.sort_by_key(|(currency, _)| currency.code.to_string());
1417
1418 for (currency, balance) in balances {
1419 let balance_key = format!("{account_key}.balance.{}", currency.code);
1420 summary.insert(format!("{balance_key}.total"), balance.total.to_string());
1421 summary.insert(format!("{balance_key}.free"), balance.free.to_string());
1422 summary.insert(format!("{balance_key}.locked"), balance.locked.to_string());
1423 }
1424 }
1425
1426 summary
1427 }
1428
1429 fn route_data_to_exchange(&mut self, data: &Data) -> anyhow::Result<()> {
1430 if matches!(
1431 data,
1432 Data::MarkPrice(_) | Data::IndexPrice(_) | Data::OptionGreeks(_) | Data::Custom(_)
1433 ) {
1434 return Ok(());
1435 }
1436 #[cfg(feature = "defi")]
1437 if matches!(data, Data::Defi(_)) {
1438 return Ok(());
1439 }
1440
1441 let venue = data.instrument_id().venue;
1442 if let Some(exchange) = self.venues.get(&venue) {
1443 let mut exchange_ref = exchange.borrow_mut();
1444 let mut processed_book_data = false;
1445
1446 match data {
1447 Data::Delta(delta) => {
1448 exchange_ref.process_order_book_delta(*delta)?;
1449 processed_book_data = true;
1450 }
1451 Data::Deltas(deltas) => {
1452 exchange_ref.process_order_book_deltas(deltas)?;
1453 processed_book_data = true;
1454 }
1455 Data::Depth10(depth) => {
1456 exchange_ref.process_order_book_depth10(depth)?;
1457 processed_book_data = true;
1458 }
1459 Data::Quote(quote) => exchange_ref.process_quote_tick(quote)?,
1460 Data::Trade(trade) => exchange_ref.process_trade_tick(trade)?,
1461 Data::Bar(bar) => exchange_ref.process_bar(*bar)?,
1462 Data::InstrumentStatus(status) => {
1463 exchange_ref.process_instrument_status(*status)?;
1464 }
1465 Data::InstrumentClose(close) => exchange_ref.process_instrument_close(*close)?,
1466 Data::FundingRate(funding) => {
1467 let settlement_ns =
1468 exchange_ref.process_funding_rate_deferred(*funding, data.ts_init())?;
1469 self.schedule_funding_settlement_if_required(venue, settlement_ns);
1470 }
1471 _ => {}
1472 }
1473
1474 drop(exchange_ref);
1475
1476 if processed_book_data {
1477 self.has_book_processed.insert(data.instrument_id());
1478 }
1479 } else {
1480 log::warn!("No exchange found for venue {venue}, data not routed");
1481 }
1482 Ok(())
1483 }
1484
1485 fn check_module_errors(&self) -> anyhow::Result<()> {
1486 for exchange in self.venues.values() {
1487 exchange.borrow().check_module_error()?;
1488 }
1489 Ok(())
1490 }
1491
1492 fn advance_time_impl(
1493 &mut self,
1494 ts_now: UnixNanos,
1495 clocks: &[Rc<RefCell<dyn Clock>>],
1496 ) -> anyhow::Result<()> {
1497 for clock in clocks {
1498 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1499 }
1500
1501 let ts_before = if ts_now.as_u64() > 0 {
1503 UnixNanos::from(ts_now.as_u64() - 1)
1504 } else {
1505 UnixNanos::default()
1506 };
1507
1508 let mut shutdown_at: Option<UnixNanos> = None;
1509
1510 while let Some(ts_event) = self
1511 .accumulator
1512 .peek_next_time()
1513 .filter(|ts_event| *ts_event <= ts_before)
1514 {
1515 self.run_timer_handlers_at(clocks, ts_event, ts_now);
1516
1517 if self.kernel.is_shutdown_requested() {
1518 self.accumulator.clear();
1519 shutdown_at = Some(ts_event);
1520 break;
1521 }
1522 self.finalize_timestamp(clocks, ts_event, SettlementScope::All)?;
1523
1524 if self.kernel.is_shutdown_requested() {
1525 self.accumulator.clear();
1526 shutdown_at = Some(ts_event);
1527 break;
1528 }
1529
1530 for clock in clocks {
1531 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1532 }
1533 }
1534
1535 if let Some(ts_event) = shutdown_at {
1538 self.last_ns = ts_event;
1539 } else {
1540 self.last_ns = ts_now;
1541 Self::set_all_clocks_time(clocks, ts_now);
1542 logging_clock_set_static_time(ts_now.as_u64());
1543 }
1544
1545 Ok(())
1546 }
1547
1548 fn flush_accumulator_events(
1549 &mut self,
1550 clocks: &[Rc<RefCell<dyn Clock>>],
1551 ts_now: UnixNanos,
1552 ) -> anyhow::Result<()> {
1553 if self.kernel.is_shutdown_requested() {
1555 self.accumulator.clear();
1556 return Ok(());
1557 }
1558
1559 let last_ns = self.last_ns;
1560
1561 for clock in clocks {
1562 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1563 }
1564
1565 while let Some(ts_event) = self
1566 .accumulator
1567 .peek_next_time()
1568 .filter(|ts_event| *ts_event <= ts_now)
1569 {
1570 self.run_timer_handlers_at(clocks, ts_event, ts_now);
1571
1572 if self.kernel.is_shutdown_requested() {
1573 self.accumulator.clear();
1574 break;
1575 }
1576 self.finalize_timestamp(clocks, ts_event, SettlementScope::All)?;
1577
1578 if self.kernel.is_shutdown_requested() {
1579 self.accumulator.clear();
1580 break;
1581 }
1582
1583 for clock in clocks {
1584 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1585 }
1586 }
1587
1588 if !self.kernel.is_shutdown_requested() {
1589 self.last_ns = last_ns;
1590 }
1591
1592 Ok(())
1593 }
1594
1595 fn process_next_timer(&mut self, clocks: &[Rc<RefCell<dyn Clock>>]) -> anyhow::Result<bool> {
1596 self.flush_accumulator_events(clocks, self.last_ns)?;
1597
1598 let mut min_next_time: Option<UnixNanos> = None;
1600
1601 for clock in clocks {
1602 let clock_ref = clock.borrow();
1603 for name in clock_ref.timer_names() {
1604 if let Some(next_time) = clock_ref.next_time_ns(name)
1605 && next_time > self.last_ns
1606 {
1607 min_next_time = Some(match min_next_time {
1608 Some(current_min) => next_time.min(current_min),
1609 None => next_time,
1610 });
1611 }
1612 }
1613 }
1614
1615 match min_next_time {
1616 None => Ok(true),
1617 Some(t) if t > self.end_ns => Ok(true),
1618 Some(t) => {
1619 self.last_ns = t;
1620 self.flush_accumulator_events(clocks, t)?;
1621 Ok(false)
1622 }
1623 }
1624 }
1625
1626 fn run_timer_handlers_at(
1627 &mut self,
1628 clocks: &[Rc<RefCell<dyn Clock>>],
1629 ts_event: UnixNanos,
1630 advance_to: UnixNanos,
1631 ) {
1632 self.last_ns = ts_event;
1633 while self.accumulator.peek_next_time() == Some(ts_event) {
1634 let handler = self
1635 .accumulator
1636 .pop_next_at_or_before(ts_event)
1637 .expect("timer exists at timestamp");
1638 Self::set_all_clocks_time(clocks, ts_event);
1639 logging_clock_set_static_time(ts_event.as_u64());
1640 handler.run();
1641 self.drain_command_queues();
1642
1643 if self.kernel.is_shutdown_requested() {
1644 return;
1645 }
1646
1647 for clock in clocks {
1648 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, advance_to, false);
1649 }
1650 }
1651 }
1652
1653 fn finalize_timestamp(
1654 &mut self,
1655 clocks: &[Rc<RefCell<dyn Clock>>],
1656 ts_now: UnixNanos,
1657 mut settlement_scope: SettlementScope,
1658 ) -> anyhow::Result<()> {
1659 loop {
1660 self.settle_venues(ts_now, settlement_scope);
1661
1662 if self.kernel.is_shutdown_requested() {
1663 self.accumulator.clear();
1664 break;
1665 }
1666
1667 for clock in clocks {
1668 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1669 }
1670
1671 if self.accumulator.peek_next_time() == Some(ts_now) {
1672 self.run_timer_handlers_at(clocks, ts_now, ts_now);
1673 settlement_scope = SettlementScope::All;
1674 continue;
1675 }
1676
1677 if !self.settle_funding_rates(ts_now)? {
1678 break;
1679 }
1680 settlement_scope = SettlementScope::All;
1681 }
1682
1683 self.run_venue_modules(ts_now, settlement_scope)?;
1684 self.run_venue_liquidations(ts_now, settlement_scope);
1685 Ok(())
1686 }
1687
1688 fn settle_funding_rates(&mut self, ts_now: UnixNanos) -> anyhow::Result<bool> {
1689 let mut due = self
1690 .venues
1691 .iter()
1692 .flat_map(|(venue, exchange)| {
1693 exchange
1694 .borrow()
1695 .funding_boundaries_due(ts_now)
1696 .into_iter()
1697 .map(|(boundary, instrument_id)| (boundary, *venue, instrument_id))
1698 .collect::<Vec<_>>()
1699 })
1700 .collect::<Vec<_>>();
1701 due.sort_unstable();
1702
1703 if let Some((boundary, venue, instrument_id)) = due
1704 .iter()
1705 .copied()
1706 .find(|(boundary, _, _)| *boundary < ts_now)
1707 {
1708 return self.fail_funding(format!(
1709 "Late funding boundary for {instrument_id} on {venue}: {boundary} < replay timestamp {ts_now}"
1710 ));
1711 }
1712
1713 if due.is_empty() {
1714 return Ok(false);
1715 }
1716
1717 for (boundary, venue, instrument_id) in due {
1718 if !self.venues[&venue]
1719 .borrow_mut()
1720 .settle_funding_boundary(boundary, instrument_id)
1721 {
1722 return self.fail_funding(format!(
1723 "Funding settlement failed for {instrument_id} on {venue} at {boundary}"
1724 ));
1725 }
1726 }
1727
1728 let next_boundaries = self
1729 .venues
1730 .iter()
1731 .filter_map(|(venue, exchange)| {
1732 exchange
1733 .borrow()
1734 .next_funding_boundary()
1735 .map(|boundary| (*venue, boundary))
1736 })
1737 .collect::<Vec<_>>();
1738
1739 for (venue, boundary) in next_boundaries {
1740 self.schedule_funding_settlement_if_required(venue, Some(boundary));
1741 }
1742
1743 Ok(true)
1744 }
1745
1746 fn fail_funding<T>(&mut self, error: String) -> anyhow::Result<T> {
1747 if self.funding_error.is_none() {
1748 self.funding_error = Some(error.clone());
1749 }
1750 Err(anyhow::anyhow!(error))
1751 }
1752
1753 fn set_instrument_expiration_timers(&self) -> anyhow::Result<()> {
1754 for exchange in self.venues.values() {
1755 let expirations = exchange.borrow().instrument_expirations();
1756 for (instrument_id, expiration_ns) in expirations {
1757 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
1758 }
1759 }
1760
1761 Ok(())
1762 }
1763
1764 fn set_instrument_expiration_timer(
1765 &self,
1766 exchange: &Rc<RefCell<SimulatedExchange>>,
1767 instrument_id: InstrumentId,
1768 expiration_ns: UnixNanos,
1769 ) -> anyhow::Result<()> {
1770 if expiration_ns == UnixNanos::default() {
1771 return Ok(());
1772 }
1773
1774 let timer_name = Self::instrument_expiration_timer_name(instrument_id.venue, expiration_ns);
1775 let timer_key = ustr::Ustr::from(timer_name.as_str());
1776 if self.kernel.clock.borrow().timer_exists(&timer_key) {
1777 return Ok(());
1778 }
1779
1780 let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1781 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1782 if let Some(exchange) = exchange.upgrade() {
1783 exchange
1784 .borrow_mut()
1785 .process_instrument_expirations(event.ts_event);
1786 }
1787 });
1788 let mut clock = self.kernel.clock.borrow_mut();
1789
1790 clock.set_time_alert_ns(
1791 &timer_name,
1792 expiration_ns,
1793 Some(TimeEventCallback::from(callback)),
1794 None,
1795 )?;
1796
1797 Ok(())
1798 }
1799
1800 fn instrument_expiration_timer_name(venue: Venue, expiration_ns: UnixNanos) -> String {
1801 format!("INSTRUMENT-EXPIRATION:{venue}:{expiration_ns}")
1802 }
1803
1804 fn schedule_funding_settlement_if_required(
1805 &self,
1806 venue: Venue,
1807 settlement_ns: Option<UnixNanos>,
1808 ) {
1809 let Some(settlement_ns) = settlement_ns else {
1810 return;
1811 };
1812
1813 if let Err(e) = self.set_funding_settlement_timer(venue, settlement_ns) {
1814 log::error!("Cannot schedule funding settlement for {venue}: {e}");
1815 }
1816 }
1817
1818 fn set_funding_settlement_timer(
1819 &self,
1820 venue: Venue,
1821 settlement_ns: UnixNanos,
1822 ) -> anyhow::Result<()> {
1823 let timer_name = Self::funding_settlement_timer_name(venue);
1824 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(|_| {});
1825 let mut clock = self.kernel.clock.borrow_mut();
1826
1827 clock.set_time_alert_ns(
1828 &timer_name,
1829 settlement_ns,
1830 Some(TimeEventCallback::from(callback)),
1831 None,
1832 )?;
1833
1834 Ok(())
1835 }
1836
1837 fn funding_settlement_timer_name(venue: Venue) -> String {
1838 format!("FUNDING-SETTLEMENT:{venue}")
1839 }
1840
1841 fn cancel_funding_settlement_timers(&self) {
1842 let mut clock = self.kernel.clock.borrow_mut();
1843 for venue in self.venues.keys() {
1844 clock.cancel_timer(&Self::funding_settlement_timer_name(*venue));
1845 }
1846 }
1847
1848 fn collect_all_clocks(&self) -> Vec<Rc<RefCell<dyn Clock>>> {
1849 let mut clocks = vec![self.kernel.clock.clone()];
1850 clocks.extend(self.kernel.trader.borrow().get_component_clocks());
1851 clocks
1852 }
1853
1854 fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
1855 self.venues
1856 .values()
1857 .filter_map(|v| v.borrow().max_inflight_command_ts())
1858 .max()
1859 }
1860
1861 fn settle_venues(&self, ts_now: UnixNanos, settlement_scope: SettlementScope) {
1862 for exchange in self.venues.values() {
1865 exchange.borrow().set_clock_time(ts_now);
1866 }
1867
1868 loop {
1874 self.drain_command_queues();
1877
1878 let active_venues: Vec<Venue> = self
1879 .venues
1880 .iter()
1881 .filter(|(_, ex)| {
1882 ex.borrow()
1883 .has_pending_commands_for_scope(ts_now, settlement_scope)
1884 })
1885 .map(|(id, _)| *id)
1886 .collect();
1887
1888 if active_venues.is_empty() {
1889 break;
1890 }
1891
1892 for venue_id in &active_venues {
1893 let mut exchange = self.venues[venue_id].borrow_mut();
1894 exchange.process_for_scope(ts_now, settlement_scope);
1895 }
1896 self.drain_command_queues();
1897
1898 for venue_id in &active_venues {
1899 self.venues[venue_id]
1900 .borrow_mut()
1901 .iterate_matching_engines(ts_now);
1902 }
1903
1904 self.drain_command_queues();
1907 }
1908 }
1909
1910 fn run_venue_modules(
1911 &mut self,
1912 ts_now: UnixNanos,
1913 settlement_scope: SettlementScope,
1914 ) -> anyhow::Result<()> {
1915 if self.last_module_ns == Some(ts_now) {
1916 return Ok(());
1917 }
1918 self.last_module_ns = Some(ts_now);
1919
1920 if self
1921 .venues
1922 .values()
1923 .all(|exchange| !exchange.borrow().has_modules())
1924 {
1925 return Ok(());
1926 }
1927
1928 self.drain_command_queues();
1930 self.settle_venues(ts_now, settlement_scope);
1931
1932 for exchange in self.venues.values() {
1933 exchange.borrow_mut().process_modules(ts_now)?;
1934 }
1935
1936 self.drain_command_queues();
1938 self.settle_venues(ts_now, settlement_scope);
1939 Ok(())
1940 }
1941
1942 fn run_venue_liquidations(&mut self, ts_now: UnixNanos, settlement_scope: SettlementScope) {
1943 if self.last_liquidation_ns == Some(ts_now) {
1944 return;
1945 }
1946 self.last_liquidation_ns = Some(ts_now);
1947
1948 if self
1949 .venues
1950 .values()
1951 .all(|exchange| !exchange.borrow().liquidation_enabled())
1952 {
1953 return;
1954 }
1955
1956 for exchange in self.venues.values() {
1957 exchange.borrow_mut().process_liquidations(ts_now);
1958 }
1959
1960 self.drain_command_queues();
1961 self.settle_venues(ts_now, settlement_scope);
1962 }
1963
1964 fn drain_exec_client_events(&self) {
1965 for client in &self.exec_clients {
1966 client.drain_queued_events();
1967 }
1968 }
1969
1970 fn drain_command_queues(&self) {
1971 loop {
1975 drain_trading_cmd_queue();
1976 drain_data_cmd_queue();
1977 self.drain_exec_client_events();
1978
1979 if trading_cmd_queue_is_empty() && data_cmd_queue_is_empty() {
1980 break;
1981 }
1982 }
1983 }
1984
1985 fn init_command_senders() {
1986 replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1987 replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1988 }
1989
1990 fn advance_clock_on_accumulator(
1991 accumulator: &mut TimeEventAccumulator,
1992 clock: &Rc<RefCell<dyn Clock>>,
1993 to_time_ns: UnixNanos,
1994 set_time: bool,
1995 ) {
1996 let mut clock_ref = clock.borrow_mut();
1997 let test_clock = clock_ref
1998 .as_any_mut()
1999 .downcast_mut::<TestClock>()
2000 .expect("BacktestEngine requires TestClock");
2001 accumulator.advance_clock(test_clock, to_time_ns, set_time);
2002 }
2003
2004 fn set_all_clocks_time(clocks: &[Rc<RefCell<dyn Clock>>], ts: UnixNanos) {
2005 for clock in clocks {
2006 let mut clock_ref = clock.borrow_mut();
2007 let test_clock = clock_ref
2008 .as_any_mut()
2009 .downcast_mut::<TestClock>()
2010 .expect("BacktestEngine requires TestClock");
2011 test_clock.set_time(ts);
2012 }
2013 }
2014
2015 #[rustfmt::skip]
2016 fn log_pre_run(&self) {
2017 log_info!("=================================================================", color = LogColor::Cyan);
2018 log_info!(" BACKTEST PRE-RUN", color = LogColor::Cyan);
2019 log_info!("=================================================================", color = LogColor::Cyan);
2020
2021 let cache = self.kernel.cache.borrow();
2022 for exchange in self.venues.values() {
2023 let ex = exchange.borrow();
2024 log_info!("=================================================================", color = LogColor::Cyan);
2025 log::info!(" SimulatedVenue {} ({})", ex.id, ex.account_type);
2026 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2027
2028 if let Some(account) = cache.account_for_venue(&ex.id) {
2029 log::info!("Balances starting:");
2030 let account_ref: &dyn Account = match &*account {
2031 AccountAny::Margin(margin) => margin,
2032 AccountAny::Cash(cash) => cash,
2033 AccountAny::Betting(betting) => betting,
2034 AccountAny::Wallet(wallet) => wallet,
2035 };
2036
2037 for balance in account_ref.starting_balances().values() {
2038 log::info!(" {balance}");
2039 }
2040 }
2041 }
2042
2043 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2044 }
2045
2046 #[rustfmt::skip]
2047 fn log_run(&self) {
2048 let config_id = self.run_config_id.as_deref().unwrap_or("None");
2049 let id = format_optional_uuid(self.run_id.as_ref());
2050 let start = format_optional_nanos(self.backtest_start);
2051
2052 log_info!("=================================================================", color = LogColor::Cyan);
2053 log_info!(" BACKTEST RUN", color = LogColor::Cyan);
2054 log_info!("=================================================================", color = LogColor::Cyan);
2055 log::info!("Run config ID: {config_id}");
2056 log::info!("Run ID: {id}");
2057 log::info!("Backtest start: {start}");
2058 log::info!("Data elements: {}", self.data_len);
2059 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2060 }
2061
2062 #[rustfmt::skip]
2063 fn log_post_run(&self) {
2064 let cache = self.kernel.cache.borrow();
2065 let orders = cache.orders(None, None, None, None, None);
2066 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
2067 let total_orders = orders.len();
2068 let positions: Vec<Position> = cache
2069 .positions(None, None, None, None, None)
2070 .into_iter()
2071 .map(|p| p.cloned())
2072 .collect();
2073 let total_positions = Self::total_positions_with_snapshots(&cache, positions.len());
2074
2075 let config_id = self.run_config_id.as_deref().unwrap_or("None");
2076 let id = format_optional_uuid(self.run_id.as_ref());
2077 let started = format_optional_nanos(self.run_started);
2078 let finished = format_optional_nanos(self.run_finished);
2079 let elapsed = format_optional_duration(self.run_started, self.run_finished);
2080 let bt_start = format_optional_nanos(self.backtest_start);
2081 let bt_end = format_optional_nanos(self.backtest_end);
2082 let bt_range = format_optional_duration(self.backtest_start, self.backtest_end);
2083 let iterations = self.iteration.separate_with_underscores();
2084 let events = total_events.separate_with_underscores();
2085 let num_orders = total_orders.separate_with_underscores();
2086 let num_positions = total_positions.separate_with_underscores();
2087
2088 log_info!("=================================================================", color = LogColor::Cyan);
2089 log_info!(" BACKTEST POST-RUN", color = LogColor::Cyan);
2090 log_info!("=================================================================", color = LogColor::Cyan);
2091 log::info!("Run config ID: {config_id}");
2092 log::info!("Run ID: {id}");
2093 log::info!("Run started: {started}");
2094 log::info!("Run finished: {finished}");
2095 log::info!("Elapsed time: {elapsed}");
2096 log::info!("Backtest start: {bt_start}");
2097 log::info!("Backtest end: {bt_end}");
2098 log::info!("Backtest range: {bt_range}");
2099 log::info!("Iterations: {iterations}");
2100 log::info!("Total events: {events}");
2101 log::info!("Total orders: {num_orders}");
2102 log::info!("Total positions: {num_positions}");
2103
2104 if !self.config.run_analysis {
2105 return;
2106 }
2107
2108 let accounts = cache.accounts_all_owned();
2109 let mut snapshots = Vec::new();
2110 for position in &positions {
2111 snapshots.extend(cache.position_snapshots(Some(&position.id), None));
2112 }
2113 let recorded = self.kernel.portfolio.borrow().recorded_realized_pnls();
2114 let portfolio = self.kernel.portfolio.borrow();
2115 let portfolio_snapshots = accounts
2116 .iter()
2117 .flat_map(|account| portfolio.snapshots(&account.id()))
2118 .collect::<Vec<_>>();
2119 let analyzer = PortfolioAnalyzer::from_accounts_with_snapshots(
2120 &accounts,
2121 &positions,
2122 &snapshots,
2123 &portfolio_snapshots,
2124 recorded,
2125 );
2126 log_portfolio_performance(&analyzer);
2127 }
2128
2129 fn total_positions_with_snapshots(cache: &Cache, cached_positions_count: usize) -> usize {
2130 cached_positions_count + cache.position_snapshots(None, None).len()
2131 }
2132
2133 pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
2135 if self
2136 .kernel
2137 .data_engine
2138 .borrow()
2139 .registered_clients()
2140 .contains(&client_id)
2141 {
2142 return;
2143 }
2144
2145 let venue = Venue::from(client_id.as_str());
2146 let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2147 let data_client_adapter = DataClientAdapter::new(
2148 backtest_client.client_id,
2149 None,
2150 false,
2151 false,
2152 Box::new(backtest_client),
2153 );
2154
2155 self.kernel
2156 .data_engine
2157 .borrow_mut()
2158 .register_client(data_client_adapter, None);
2159 }
2160
2161 pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
2163 let client_id = ClientId::from(venue.as_str());
2164
2165 if !self
2166 .kernel
2167 .data_engine
2168 .borrow()
2169 .registered_clients()
2170 .contains(&client_id)
2171 {
2172 let backtest_client =
2173 BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2174 let data_client_adapter = DataClientAdapter::new(
2175 client_id,
2176 Some(venue),
2177 false,
2178 false,
2179 Box::new(backtest_client),
2180 );
2181 self.kernel
2182 .data_engine
2183 .borrow_mut()
2184 .register_client(data_client_adapter, Some(venue));
2185 }
2186 }
2187}
2188
2189fn format_optional_nanos(nanos: Option<UnixNanos>) -> String {
2190 nanos.map_or("None".to_string(), unix_nanos_to_iso8601)
2191}
2192
2193fn format_optional_uuid(uuid: Option<&UUID4>) -> String {
2194 uuid.map_or("None".to_string(), ToString::to_string)
2195}
2196
2197fn event_count_as_usize(event_count: u64) -> usize {
2198 usize::try_from(event_count).expect("execution event count fits usize")
2199}
2200
2201fn format_optional_duration(start: Option<UnixNanos>, end: Option<UnixNanos>) -> String {
2202 match (start, end) {
2203 (Some(s), Some(e)) => {
2204 let delta = s.to_datetime_utc().duration_until(e.to_datetime_utc());
2205 let days = delta.as_hours().abs() / 24;
2206 let hours = delta.as_hours().abs() % 24;
2207 let minutes = delta.as_mins().abs() % 60;
2208 let seconds = delta.as_secs().abs() % 60;
2209 let micros = delta.subsec_nanos().unsigned_abs() / 1_000;
2210 format!("{days} days {hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
2211 }
2212 _ => "None".to_string(),
2213 }
2214}
2215
2216#[rustfmt::skip]
2217fn log_portfolio_performance(analyzer: &PortfolioAnalyzer) {
2218 log_info!("=================================================================", color = LogColor::Cyan);
2219 log_info!(" PORTFOLIO PERFORMANCE", color = LogColor::Cyan);
2220 log_info!("=================================================================", color = LogColor::Cyan);
2221
2222 for currency in analyzer.currencies() {
2223 log::info!(" PnL Statistics ({})", currency.code);
2224 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2225
2226 if let Ok(pnl_lines) = analyzer.get_stats_pnls_formatted(Some(currency), None) {
2227 for line in &pnl_lines {
2228 log::info!("{line}");
2229 }
2230 }
2231
2232 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2233 }
2234
2235 log::info!(" Returns Statistics");
2236 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2237
2238 for line in &analyzer.get_stats_returns_formatted() {
2239 log::info!("{line}");
2240 }
2241 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2242
2243 log::info!(" General Statistics");
2244 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2245
2246 for line in &analyzer.get_stats_general_formatted() {
2247 log::info!("{line}");
2248 }
2249 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254 use std::{cell::Cell, rc::Rc};
2255
2256 use indexmap::IndexMap;
2257 use nautilus_common::{
2258 actor::DataActor,
2259 enums::Environment,
2260 messages::{
2261 data::{DataCommand, UnsubscribeCommand},
2262 execution::{ModifyOrder, SubmitOrder, TradingCommand},
2263 },
2264 msgbus::{
2265 self, MessagingSwitchboard, TypedHandler,
2266 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
2267 },
2268 };
2269 use nautilus_execution::engine::{SnapshotAnchorer, stubs::StubExecutionClient};
2270 use nautilus_model::{
2271 data::{Data, InstrumentStatus, QuoteTick},
2272 enums::{
2273 AccountType, BookType, MarketStatus, MarketStatusAction, OmsType, OrderSide,
2274 OrderStatus, OrderType, TriggerType,
2275 },
2276 events::OrderEventAny,
2277 identifiers::{AccountId, ActorId, ClientId, ClientOrderId, PositionId, StrategyId, Venue},
2278 instruments::{
2279 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
2280 },
2281 orders::{
2282 Order, OrderAny, OrderTestBuilder,
2283 stubs::{OrderFilledTestBuilder, TestOrderEventStubs},
2284 },
2285 types::{Money, Price, Quantity},
2286 };
2287 use nautilus_system::{KernelEventStore, RegisteredComponents};
2288 use nautilus_testkit::{
2289 cache::TestCacheDatabaseControl,
2290 components::{StateActor, StateStrategy},
2291 };
2292 use nautilus_trading::{
2293 nautilus_strategy,
2294 strategy::{config::StrategyConfig, core::StrategyCore},
2295 };
2296 use rstest::*;
2297 use ustr::Ustr;
2298
2299 use super::*;
2300 use crate::modules::{
2301 AccountAdjustmentOutcome, ExchangeContext, SimulationModule, SimulationModuleHandle,
2302 SimulationModuleResult,
2303 };
2304
2305 #[derive(Debug)]
2306 struct BacktestReplayKernelEventStore {
2307 fail_restore: bool,
2308 }
2309
2310 impl KernelEventStore for BacktestReplayKernelEventStore {
2311 fn restore_parent_cache(
2312 &mut self,
2313 _instance_id: UUID4,
2314 _cache: &mut Cache,
2315 ) -> anyhow::Result<()> {
2316 if self.fail_restore {
2317 anyhow::bail!("replay restore failed");
2318 }
2319
2320 Ok(())
2321 }
2322
2323 fn open(
2324 &mut self,
2325 _instance_id: UUID4,
2326 _components: &RegisteredComponents,
2327 _environment: Environment,
2328 ) -> anyhow::Result<()> {
2329 Ok(())
2330 }
2331
2332 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
2333 None
2334 }
2335
2336 fn seal(&mut self, _ts_init: UnixNanos) {}
2337
2338 fn run_id(&self) -> Option<&str> {
2339 Some("replay-child")
2340 }
2341
2342 fn parent_run_id(&self) -> Option<&str> {
2343 Some("seed-run")
2344 }
2345
2346 fn is_event_store_replay_configured(&self) -> bool {
2347 true
2348 }
2349
2350 fn is_halted(&self) -> bool {
2351 false
2352 }
2353 }
2354
2355 #[derive(Debug)]
2356 struct TestStrategy {
2357 core: StrategyCore,
2358 }
2359
2360 impl TestStrategy {
2361 fn new(config: StrategyConfig) -> Self {
2362 Self {
2363 core: StrategyCore::new(config),
2364 }
2365 }
2366 }
2367
2368 impl DataActor for TestStrategy {}
2369
2370 nautilus_strategy!(TestStrategy);
2371
2372 struct TestSimulationModule {
2373 process_count: Rc<Cell<u32>>,
2374 }
2375
2376 impl SimulationModule for TestSimulationModule {
2377 fn pre_process(&self, _data: &Data) -> anyhow::Result<()> {
2378 Ok(())
2379 }
2380
2381 fn process(
2382 &self,
2383 _ts_now: UnixNanos,
2384 _ctx: &ExchangeContext,
2385 ) -> anyhow::Result<SimulationModuleResult> {
2386 self.process_count.set(self.process_count.get() + 1);
2387 Ok(SimulationModuleResult::NotReady)
2388 }
2389
2390 fn acknowledge(&self, _outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()> {
2391 Ok(())
2392 }
2393
2394 fn log_diagnostics(&self) -> anyhow::Result<()> {
2395 Ok(())
2396 }
2397
2398 fn reset(&self) -> anyhow::Result<()> {
2399 Ok(())
2400 }
2401 }
2402
2403 fn create_engine() -> BacktestEngine {
2404 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2405 let venue_config = SimulatedVenueConfig::builder()
2406 .venue(Venue::from("BINANCE"))
2407 .oms_type(OmsType::Netting)
2408 .account_type(AccountType::Margin)
2409 .book_type(BookType::L1_MBP)
2410 .starting_balances(vec![Money::from("1_000_000 USDT")])
2411 .build()
2412 .unwrap();
2413 engine.add_venue(venue_config).unwrap();
2414 engine
2415 }
2416
2417 fn create_immediate_engine(instrument: &CryptoPerpetual) -> BacktestEngine {
2418 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2419 let venue_config = SimulatedVenueConfig::builder()
2420 .venue(instrument.id().venue)
2421 .oms_type(OmsType::Netting)
2422 .account_type(AccountType::Margin)
2423 .book_type(BookType::L1_MBP)
2424 .starting_balances(vec![Money::from("1_000_000 USDT")])
2425 .use_message_queue(false)
2426 .build()
2427 .unwrap();
2428 engine.add_venue(venue_config).unwrap();
2429 engine
2430 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
2431 .unwrap();
2432 engine
2433 .venues
2434 .get(&instrument.id().venue)
2435 .unwrap()
2436 .borrow_mut()
2437 .initialize_account();
2438 engine
2439 }
2440
2441 fn create_engine_with_strategy(manage_stop: bool) -> (BacktestEngine, StrategyId) {
2442 let mut engine = create_engine();
2443 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
2444 let strategy_id = StrategyId::from(if manage_stop {
2445 "MANAGED-STOP-001"
2446 } else {
2447 "IMMEDIATE-STOP-001"
2448 });
2449 engine.add_instrument(&instrument).unwrap();
2450 engine
2451 .add_strategy(TestStrategy::new(StrategyConfig {
2452 strategy_id: Some(strategy_id),
2453 manage_stop,
2454 ..Default::default()
2455 }))
2456 .unwrap();
2457
2458 if manage_stop {
2459 let order = OrderTestBuilder::new(OrderType::Market)
2460 .trader_id(engine.trader_id())
2461 .strategy_id(strategy_id)
2462 .instrument_id(instrument.id())
2463 .side(OrderSide::Buy)
2464 .quantity(Quantity::from("1.000"))
2465 .build();
2466 let fill = OrderFilledTestBuilder::new(&order, &instrument).build();
2467 let OrderEventAny::Filled(fill) = fill else {
2468 unreachable!();
2469 };
2470 let position = Position::new(&instrument, fill);
2471 engine
2472 .kernel
2473 .cache
2474 .borrow_mut()
2475 .add_position_without_order(&position, OmsType::Netting)
2476 .unwrap();
2477 }
2478
2479 (engine, strategy_id)
2480 }
2481
2482 fn send_execution_command(command: TradingCommand) {
2483 msgbus::send_trading_command(MessagingSwitchboard::exec_engine_execute(), command);
2484 }
2485
2486 #[rstest]
2487 #[case(false, false, 0)]
2488 #[case(false, true, 1)]
2489 #[case(true, true, 1)]
2490 fn test_run_venue_modules_settles_only_when_enabled(
2491 #[case] first_enabled: bool,
2492 #[case] second_enabled: bool,
2493 #[case] expected_ns: u64,
2494 ) {
2495 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2496 let process_count = Rc::new(Cell::new(0));
2497
2498 for (venue, enabled) in [
2499 (Venue::from("BINANCE"), first_enabled),
2500 (Venue::from("SIM"), second_enabled),
2501 ] {
2502 let modules = enabled
2503 .then(|| {
2504 SimulationModuleHandle::new(TestSimulationModule {
2505 process_count: Rc::clone(&process_count),
2506 })
2507 })
2508 .into_iter()
2509 .collect();
2510 let venue_config = SimulatedVenueConfig::builder()
2511 .venue(venue)
2512 .oms_type(OmsType::Netting)
2513 .account_type(AccountType::Margin)
2514 .book_type(BookType::L1_MBP)
2515 .starting_balances(vec![Money::from("1_000_000 USDT")])
2516 .modules(modules)
2517 .build()
2518 .unwrap();
2519 engine.add_venue(venue_config).unwrap();
2520 }
2521
2522 engine
2523 .run_venue_modules(UnixNanos::from(1), SettlementScope::All)
2524 .unwrap();
2525
2526 assert_eq!(
2527 engine.kernel.clock.borrow().timestamp_ns(),
2528 UnixNanos::from(expected_ns)
2529 );
2530 assert_eq!(
2531 process_count.get(),
2532 u32::from(first_enabled) + u32::from(second_enabled)
2533 );
2534 }
2535
2536 #[rstest]
2537 #[case(false, false, 0)]
2538 #[case(false, true, 1)]
2539 #[case(true, true, 1)]
2540 fn test_run_venue_liquidations_settles_only_when_enabled(
2541 #[case] first_enabled: bool,
2542 #[case] second_enabled: bool,
2543 #[case] expected_ns: u64,
2544 ) {
2545 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2546
2547 for (venue, enabled) in [
2548 (Venue::from("BINANCE"), first_enabled),
2549 (Venue::from("SIM"), second_enabled),
2550 ] {
2551 let venue_config = SimulatedVenueConfig::builder()
2552 .venue(venue)
2553 .oms_type(OmsType::Netting)
2554 .account_type(AccountType::Margin)
2555 .book_type(BookType::L1_MBP)
2556 .starting_balances(vec![Money::from("1_000_000 USDT")])
2557 .liquidation_enabled(enabled)
2558 .build()
2559 .unwrap();
2560 engine.add_venue(venue_config).unwrap();
2561 }
2562
2563 engine.run_venue_liquidations(UnixNanos::from(1), SettlementScope::All);
2564
2565 assert_eq!(
2566 engine.kernel.clock.borrow().timestamp_ns(),
2567 UnixNanos::from(expected_ns)
2568 );
2569 }
2570
2571 #[rstest]
2572 fn test_immediate_submit_defers_order_events(crypto_perpetual_ethusdt: CryptoPerpetual) {
2573 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2574 let order = OrderTestBuilder::new(OrderType::Limit)
2575 .trader_id(engine.trader_id())
2576 .instrument_id(crypto_perpetual_ethusdt.id)
2577 .client_order_id(ClientOrderId::from("O-IMMEDIATE-SUBMIT"))
2578 .side(OrderSide::Buy)
2579 .quantity(Quantity::from("1.000"))
2580 .price(Price::from("1000.00"))
2581 .build();
2582 engine
2583 .kernel
2584 .cache
2585 .borrow_mut()
2586 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2587 .unwrap();
2588
2589 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
2590 order.trader_id(),
2591 Some(ClientId::from("BINANCE")),
2592 order.strategy_id(),
2593 order.instrument_id(),
2594 order.client_order_id(),
2595 order.init_event().clone(),
2596 order.exec_algorithm_id(),
2597 None,
2598 None,
2599 UUID4::new(),
2600 UnixNanos::default(),
2601 None,
2602 )));
2603
2604 {
2605 let cache = engine.kernel.cache.borrow();
2606 let cached_order = cache.order(&order.client_order_id()).unwrap();
2607 assert_eq!(cached_order.status(), OrderStatus::Initialized);
2608 assert_eq!(cached_order.event_count(), 1);
2609 }
2610
2611 engine.drain_command_queues();
2612
2613 let cache = engine.kernel.cache.borrow();
2614 let cached_order = cache.order(&order.client_order_id()).unwrap();
2615 let events = cached_order.events();
2616 assert!(matches!(events[1], OrderEventAny::Submitted(_)));
2617 assert!(matches!(events[2], OrderEventAny::Accepted(_)));
2618 }
2619
2620 #[rstest]
2621 fn test_immediate_modify_submitted_order_defers_updated_event(
2622 crypto_perpetual_ethusdt: CryptoPerpetual,
2623 ) {
2624 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2625 let order = OrderTestBuilder::new(OrderType::Limit)
2626 .trader_id(engine.trader_id())
2627 .instrument_id(crypto_perpetual_ethusdt.id)
2628 .client_order_id(ClientOrderId::from("O-IMMEDIATE-MODIFY"))
2629 .side(OrderSide::Buy)
2630 .quantity(Quantity::from("1.000"))
2631 .price(Price::from("1000.00"))
2632 .build();
2633 let account_id = AccountId::from("BINANCE-001");
2634 engine
2635 .kernel
2636 .cache
2637 .borrow_mut()
2638 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2639 .unwrap();
2640 engine
2641 .kernel
2642 .cache
2643 .borrow_mut()
2644 .update_order(&TestOrderEventStubs::submitted(&order, account_id))
2645 .unwrap();
2646
2647 send_execution_command(TradingCommand::ModifyOrder(ModifyOrder::new(
2648 order.trader_id(),
2649 Some(ClientId::from("BINANCE")),
2650 order.strategy_id(),
2651 order.instrument_id(),
2652 order.client_order_id(),
2653 None,
2654 Some(Quantity::from("2.000")),
2655 None,
2656 None,
2657 UUID4::new(),
2658 UnixNanos::from(1),
2659 None,
2660 None,
2661 )));
2662
2663 {
2664 let cache = engine.kernel.cache.borrow();
2665 let cached_order = cache.order(&order.client_order_id()).unwrap();
2666 assert_eq!(cached_order.quantity(), Quantity::from("1.000"));
2667 assert!(matches!(
2668 cached_order.events().last(),
2669 Some(OrderEventAny::Submitted(_))
2670 ));
2671 }
2672
2673 engine.drain_command_queues();
2674
2675 let cache = engine.kernel.cache.borrow();
2676 let order = cache.order(&order.client_order_id()).unwrap();
2677 assert_eq!(order.quantity(), Quantity::from("2.000"));
2678 assert!(matches!(
2679 order.events().last(),
2680 Some(OrderEventAny::Updated(_))
2681 ));
2682 }
2683
2684 #[rstest]
2685 fn test_immediate_market_data_dispatches_fill_synchronously(
2686 crypto_perpetual_ethusdt: CryptoPerpetual,
2687 ) {
2688 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2689 let order = OrderTestBuilder::new(OrderType::Limit)
2690 .trader_id(engine.trader_id())
2691 .instrument_id(crypto_perpetual_ethusdt.id)
2692 .client_order_id(ClientOrderId::from("O-IMMEDIATE-QUOTE-FILL"))
2693 .side(OrderSide::Buy)
2694 .quantity(Quantity::from("1.000"))
2695 .price(Price::from("1000.00"))
2696 .build();
2697 engine
2698 .kernel
2699 .cache
2700 .borrow_mut()
2701 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2702 .unwrap();
2703
2704 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
2705 order.trader_id(),
2706 Some(ClientId::from("BINANCE")),
2707 order.strategy_id(),
2708 order.instrument_id(),
2709 order.client_order_id(),
2710 order.init_event().clone(),
2711 order.exec_algorithm_id(),
2712 None,
2713 None,
2714 UUID4::new(),
2715 UnixNanos::default(),
2716 None,
2717 )));
2718 engine.drain_command_queues();
2719
2720 let quote = QuoteTick::new(
2721 order.instrument_id(),
2722 Price::from("999.00"),
2723 Price::from("1000.00"),
2724 Quantity::from("1.000"),
2725 Quantity::from("1.000"),
2726 UnixNanos::from(1),
2727 UnixNanos::from(1),
2728 );
2729 msgbus::send_quote(
2730 format!(
2731 "SimulatedExchange.process_new_quote.{}",
2732 order.instrument_id().venue
2733 )
2734 .into(),
2735 "e,
2736 );
2737
2738 let cache = engine.kernel.cache.borrow();
2739 let cached_order = cache.order(&order.client_order_id()).unwrap();
2740 assert_eq!(cached_order.status(), OrderStatus::Filled);
2741 assert!(matches!(
2742 cached_order.events().last(),
2743 Some(OrderEventAny::Filled(_))
2744 ));
2745 }
2746
2747 #[rstest]
2748 fn test_timer_handler_sets_last_ns_to_fire_time() {
2749 let mut engine = create_engine();
2750 engine.last_ns = UnixNanos::from(30);
2751 let fired = Rc::new(Cell::new(false));
2752 let fired_clone = Rc::clone(&fired);
2753 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
2754 fired_clone.set(true);
2755 }));
2756 engine
2757 .kernel
2758 .clock
2759 .borrow_mut()
2760 .set_timer_ns(
2761 "ROLL",
2762 1,
2763 Some(UnixNanos::from(20)),
2764 None,
2765 Some(callback),
2766 Some(true),
2767 Some(true),
2768 )
2769 .unwrap();
2770 let clocks = engine.collect_all_clocks();
2771
2772 for clock in &clocks {
2773 BacktestEngine::advance_clock_on_accumulator(
2774 &mut engine.accumulator,
2775 clock,
2776 UnixNanos::from(30),
2777 false,
2778 );
2779 }
2780 engine.run_timer_handlers_at(&clocks, UnixNanos::from(20), UnixNanos::from(30));
2781
2782 assert!(fired.get());
2783 assert_eq!(engine.last_ns, UnixNanos::from(20));
2784 }
2785
2786 #[rstest]
2787 #[case::complete(false, 25)]
2788 #[case::shutdown(true, 20)]
2789 fn test_flush_accumulator_events_sets_last_ns_for_completion(
2790 #[case] shutdown: bool,
2791 #[case] expected_last_ns: u64,
2792 ) {
2793 let mut engine = create_engine();
2794 let last_ns = UnixNanos::from(25);
2795 let ts_now = UnixNanos::from(30);
2796 engine.last_ns = last_ns;
2797 let clocks = engine.collect_all_clocks();
2798 BacktestEngine::set_all_clocks_time(&clocks, last_ns);
2799 let fired = Rc::new(Cell::new(false));
2800 let fired_clone = Rc::clone(&fired);
2801 let observed_ns = Rc::new(Cell::new(UnixNanos::default()));
2802 let observed_ns_clone = Rc::clone(&observed_ns);
2803 let clock = Rc::clone(&engine.kernel.clock);
2804 let shutdown_requested = engine.kernel.shutdown_flag();
2805 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
2806 fired_clone.set(true);
2807 observed_ns_clone.set(clock.borrow().timestamp_ns());
2808 shutdown_requested.set(shutdown);
2809 }));
2810 engine
2811 .kernel
2812 .clock
2813 .borrow_mut()
2814 .set_timer_ns(
2815 "ROLL",
2816 100,
2817 Some(UnixNanos::from(20)),
2818 None,
2819 Some(callback),
2820 Some(true),
2821 Some(true),
2822 )
2823 .unwrap();
2824
2825 engine.flush_accumulator_events(&clocks, ts_now).unwrap();
2826
2827 assert!(fired.get());
2828 assert_eq!(observed_ns.get(), UnixNanos::from(20));
2829 assert_eq!(engine.kernel.is_shutdown_requested(), shutdown);
2830 assert_eq!(engine.last_ns, UnixNanos::from(expected_last_ns));
2831 }
2832
2833 #[rstest]
2834 fn test_add_duplicate_venue_preserves_original_exchange(
2835 crypto_perpetual_ethusdt: CryptoPerpetual,
2836 ) {
2837 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2838 let venue = Venue::from("BINANCE");
2839 let venue_config = SimulatedVenueConfig::builder()
2840 .venue(venue)
2841 .oms_type(OmsType::Netting)
2842 .account_type(AccountType::Margin)
2843 .book_type(BookType::L1_MBP)
2844 .starting_balances(vec![Money::from("1_000_000 USDT")])
2845 .build()
2846 .unwrap();
2847 engine.add_venue(venue_config).unwrap();
2848
2849 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2850 let instrument_id = instrument.id();
2851 engine.add_instrument(&instrument).unwrap();
2852
2853 let initial_quote = QuoteTick::new(
2854 instrument_id,
2855 Price::from("1000.00"),
2856 Price::from("1001.00"),
2857 Quantity::from("1.000"),
2858 Quantity::from("1.000"),
2859 UnixNanos::from(1),
2860 UnixNanos::from(1),
2861 );
2862 msgbus::send_quote(
2863 format!("SimulatedExchange.process_new_quote.{venue}").into(),
2864 &initial_quote,
2865 );
2866
2867 let best_bid_before = engine
2868 .venues
2869 .get(&venue)
2870 .unwrap()
2871 .borrow()
2872 .best_bid_price(instrument_id);
2873 let best_ask_before = engine
2874 .venues
2875 .get(&venue)
2876 .unwrap()
2877 .borrow()
2878 .best_ask_price(instrument_id);
2879 let original_exchange = Rc::downgrade(engine.venues.get(&venue).unwrap());
2880 let venues_before = engine.list_venues();
2881 let exec_clients_len_before = engine.exec_clients.len();
2882 let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
2883 let duplicate_config = SimulatedVenueConfig::builder()
2884 .venue(venue)
2885 .oms_type(OmsType::Netting)
2886 .account_type(AccountType::Margin)
2887 .book_type(BookType::L1_MBP)
2888 .starting_balances(vec![Money::from("1_000_000 USDT")])
2889 .build()
2890 .unwrap();
2891 assert!(engine.add_venue(duplicate_config).is_err());
2892
2893 let original_exchange = original_exchange
2894 .upgrade()
2895 .expect("the original exchange must remain alive");
2896 assert!(Rc::ptr_eq(
2897 &original_exchange,
2898 engine.venues.get(&venue).unwrap()
2899 ));
2900 assert_eq!(engine.list_venues(), venues_before);
2901 assert_eq!(engine.exec_clients.len(), exec_clients_len_before);
2902 assert_eq!(
2903 engine.kernel.exec_engine.borrow().client_ids(),
2904 client_ids_before
2905 );
2906
2907 let distinct_quote = QuoteTick::new(
2908 instrument_id,
2909 Price::from("2000.00"),
2910 Price::from("2001.00"),
2911 Quantity::from("2.000"),
2912 Quantity::from("2.000"),
2913 UnixNanos::from(2),
2914 UnixNanos::from(2),
2915 );
2916 msgbus::send_quote(
2917 format!("SimulatedExchange.process_new_quote.{venue}").into(),
2918 &distinct_quote,
2919 );
2920
2921 let original_exchange = original_exchange.borrow();
2922 let best_bid_after = original_exchange.best_bid_price(instrument_id);
2923 let best_ask_after = original_exchange.best_ask_price(instrument_id);
2924 assert_ne!(best_bid_after, best_bid_before);
2925 assert_ne!(best_ask_after, best_ask_before);
2926 assert_eq!(best_bid_after, Some(Price::from("2000.00")));
2927 assert_eq!(best_ask_after, Some(Price::from("2001.00")));
2928 }
2929
2930 #[rstest]
2931 fn test_add_venue_execution_registration_failure_publishes_nothing() {
2932 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2933 let venue = Venue::from("SIM");
2934 engine
2935 .kernel
2936 .exec_engine
2937 .borrow_mut()
2938 .register_client(Box::new(StubExecutionClient::new(
2939 ClientId::from(venue.as_str()),
2940 AccountId::from("SIM-001"),
2941 venue,
2942 OmsType::Netting,
2943 None,
2944 )))
2945 .unwrap();
2946 let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
2947
2948 let endpoint = format!("SimulatedExchange.process_new_quote.{venue}");
2949 let received_quotes = Rc::new(RefCell::new(Vec::new()));
2950 let received_quotes_handler = Rc::clone(&received_quotes);
2951 let sentinel = TypedHandler::from_with_id("venue-setup-sentinel", move |quote| {
2952 received_quotes_handler.borrow_mut().push(*quote);
2953 });
2954 msgbus::register_quote_endpoint(endpoint.as_str().into(), sentinel);
2955
2956 let venue_config = SimulatedVenueConfig::builder()
2957 .venue(venue)
2958 .oms_type(OmsType::Netting)
2959 .account_type(AccountType::Margin)
2960 .book_type(BookType::L1_MBP)
2961 .starting_balances(vec![Money::from("1_000_000 USD")])
2962 .build()
2963 .unwrap();
2964 assert!(engine.add_venue(venue_config).is_err());
2965
2966 assert!(!engine.venues.contains_key(&venue));
2967 assert!(engine.exec_clients.is_empty());
2968 assert_eq!(
2969 engine.kernel.exec_engine.borrow().client_ids(),
2970 client_ids_before
2971 );
2972
2973 let quote = QuoteTick::new(
2974 InstrumentId::from("TEST.SIM"),
2975 Price::from("100.00"),
2976 Price::from("101.00"),
2977 Quantity::from("1"),
2978 Quantity::from("1"),
2979 UnixNanos::from(1),
2980 UnixNanos::from(1),
2981 );
2982 msgbus::send_quote(endpoint.as_str().into(), "e);
2983 assert_eq!(received_quotes.borrow().as_slice(), &[quote]);
2984 }
2985
2986 #[rstest]
2987 fn test_add_strategy_registers_configured_hedging_oms_type() {
2988 let mut engine = create_engine();
2989 let instrument = crypto_perpetual_ethusdt();
2990 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
2991
2992 engine
2993 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
2994 .unwrap();
2995 engine
2996 .add_strategy(TestStrategy::new(StrategyConfig {
2997 strategy_id: Some(strategy_id),
2998 oms_type: Some(OmsType::Hedging),
2999 ..Default::default()
3000 }))
3001 .unwrap();
3002
3003 let order = OrderTestBuilder::new(OrderType::Market)
3004 .trader_id(engine.trader_id())
3005 .strategy_id(strategy_id)
3006 .instrument_id(instrument.id())
3007 .quantity(Quantity::from("1.000"))
3008 .build();
3009 let position_id = PositionId::new("CUSTOM-POSITION-001");
3010
3011 engine
3012 .kernel
3013 .exec_engine
3014 .borrow()
3015 .cache()
3016 .borrow_mut()
3017 .add_order(
3018 order.clone(),
3019 Some(position_id),
3020 Some(ClientId::from("BINANCE")),
3021 true,
3022 )
3023 .unwrap();
3024
3025 let submit_order = SubmitOrder::new(
3026 order.trader_id(),
3027 Some(ClientId::from("BINANCE")),
3028 strategy_id,
3029 instrument.id(),
3030 order.client_order_id(),
3031 order.init_event().clone(),
3032 order.exec_algorithm_id(),
3033 Some(position_id),
3034 None,
3035 UUID4::new(),
3036 UnixNanos::default(),
3037 None,
3038 );
3039
3040 engine
3041 .kernel
3042 .exec_engine
3043 .borrow()
3044 .execute(TradingCommand::SubmitOrder(submit_order));
3045
3046 let exec_engine = engine.kernel.exec_engine.borrow();
3047 let cache = exec_engine.cache().borrow();
3048 let cached_order = cache
3049 .order(&order.client_order_id())
3050 .expect("Order should be cached");
3051
3052 assert_eq!(cached_order.status(), OrderStatus::Initialized);
3053 }
3054
3055 fn create_engine_with_replay_store(fail_restore: bool) -> BacktestEngine {
3056 let config = BacktestEngineConfig {
3057 load_state: true,
3058 run_analysis: false,
3059 ..Default::default()
3060 };
3061 let mut engine = BacktestEngine::new(config.clone()).unwrap();
3062 let event_store_factory = move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
3063 Ok::<_, anyhow::Error>(Box::new(BacktestReplayKernelEventStore { fail_restore })
3064 as Box<dyn KernelEventStore>)
3065 };
3066
3067 engine.kernel = NautilusKernel::new_with(
3068 "BacktestEngine".to_string(),
3069 config,
3070 None,
3071 Some(Box::new(event_store_factory)),
3072 )
3073 .unwrap();
3074 engine.instance_id = engine.kernel.instance_id;
3075 engine
3076 }
3077
3078 fn create_stop_market_order(instrument: &CryptoPerpetual) -> OrderAny {
3079 OrderTestBuilder::new(OrderType::StopMarket)
3080 .instrument_id(instrument.id())
3081 .side(OrderSide::Buy)
3082 .trigger_price(Price::from("5100.00"))
3083 .quantity(Quantity::from(1))
3084 .emulation_trigger(TriggerType::BidAsk)
3085 .build()
3086 }
3087
3088 fn create_submit_order_command(order: &OrderAny) -> SubmitOrder {
3089 SubmitOrder::new(
3090 order.trader_id(),
3091 None,
3092 order.strategy_id(),
3093 order.instrument_id(),
3094 order.client_order_id(),
3095 order.init_event().clone(),
3096 order.exec_algorithm_id(),
3097 None,
3098 None,
3099 UUID4::new(),
3100 0.into(),
3101 None, )
3103 }
3104
3105 fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
3106 let (handler, saving_handler) =
3107 get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
3108 msgbus::register_data_command_endpoint(
3109 MessagingSwitchboard::data_engine_queue_execute(),
3110 handler,
3111 );
3112 saving_handler
3113 }
3114
3115 #[rstest]
3116 fn test_run_impl_event_store_replay_skips_trader_start() {
3117 let mut engine = create_engine_with_replay_store(false);
3118
3119 engine
3120 .run_impl(
3121 Some(UnixNanos::from(0)),
3122 Some(UnixNanos::from(1)),
3123 None,
3124 true,
3125 )
3126 .unwrap();
3127
3128 assert!(engine.kernel.is_event_store_replay_configured());
3129 assert!(engine.kernel.is_event_store_replay());
3130 assert!(!engine.kernel.trader.borrow().is_running());
3131 }
3132
3133 #[rstest]
3134 fn test_end_reports_strategy_stranded_by_managed_stop() {
3135 let (mut engine, strategy_id) = create_engine_with_strategy(true);
3136
3137 let result = engine.run(
3138 Some(UnixNanos::from(0)),
3139 Some(UnixNanos::from(1)),
3140 None,
3141 false,
3142 );
3143
3144 assert!(result.is_ok());
3145 assert_eq!(
3146 component_state(&strategy_id.inner()).unwrap(),
3147 ComponentState::Running
3148 );
3149 assert_eq!(engine.running_strategy_ids(), vec![strategy_id]);
3150 }
3151
3152 #[rstest]
3153 fn test_end_reports_no_cleanly_stopped_strategies() {
3154 let mut empty_engine = create_engine();
3155 let empty_result = empty_engine.run(
3156 Some(UnixNanos::from(0)),
3157 Some(UnixNanos::from(1)),
3158 None,
3159 false,
3160 );
3161 assert!(empty_result.is_ok());
3162 assert!(empty_engine.running_strategy_ids().is_empty());
3163
3164 let (mut engine, strategy_id) = create_engine_with_strategy(false);
3165 let result = engine.run(
3166 Some(UnixNanos::from(0)),
3167 Some(UnixNanos::from(1)),
3168 None,
3169 false,
3170 );
3171
3172 assert!(result.is_ok());
3173 assert_ne!(
3174 component_state(&strategy_id.inner()).unwrap(),
3175 ComponentState::Running
3176 );
3177 assert!(engine.running_strategy_ids().is_empty());
3178 }
3179
3180 #[rstest]
3181 fn test_run_impl_event_store_replay_config_failure_errors() {
3182 let mut engine = create_engine_with_replay_store(true);
3183
3184 let error = engine
3185 .run_impl(
3186 Some(UnixNanos::from(0)),
3187 Some(UnixNanos::from(1)),
3188 None,
3189 true,
3190 )
3191 .unwrap_err();
3192
3193 assert_eq!(error.to_string(), "event-store replay did not start");
3194 assert!(engine.kernel.is_event_store_replay_configured());
3195 assert!(!engine.kernel.is_event_store_replay());
3196 assert!(!engine.kernel.trader.borrow().is_running());
3197 }
3198
3199 #[rstest]
3200 fn test_backtest_state_persistence_loads_before_start_and_saves_after_settle() {
3201 let actor_id = ActorId::from("BACKTEST-STATE-ACTOR");
3202 let strategy_id = StrategyId::from("BACKTEST-STATE-STRATEGY-001");
3203 let actor_load = IndexMap::from([("actor-load".to_string(), b"actor-loaded".to_vec())]);
3204 let strategy_load =
3205 IndexMap::from([("strategy-load".to_string(), b"strategy-loaded".to_vec())]);
3206 let actor_save = IndexMap::from([("actor-save".to_string(), b"actor-saved".to_vec())]);
3207 let strategy_save =
3208 IndexMap::from([("strategy-save".to_string(), b"strategy-saved".to_vec())]);
3209 let (database, control) = TestCacheDatabaseControl::create();
3210 control.set_actor_state(actor_id, &actor_load);
3211 control.set_strategy_state(strategy_id, &strategy_load);
3212 let config = BacktestEngineConfig {
3213 load_state: true,
3214 save_state: true,
3215 run_analysis: false,
3216 ..Default::default()
3217 };
3218 let mut engine = BacktestEngine::new(config).unwrap();
3219 engine
3220 .kernel
3221 .cache
3222 .borrow_mut()
3223 .set_database(Box::new(database));
3224 engine
3225 .add_actor(StateActor::new(
3226 actor_id,
3227 control.clone(),
3228 actor_save.clone(),
3229 ))
3230 .unwrap();
3231 engine
3232 .add_strategy(StateStrategy::new(
3233 strategy_id,
3234 control.clone(),
3235 strategy_save.clone(),
3236 ))
3237 .unwrap();
3238
3239 engine
3240 .run(
3241 Some(UnixNanos::from(0)),
3242 Some(UnixNanos::from(1)),
3243 None,
3244 false,
3245 )
3246 .unwrap();
3247 engine.dispose();
3248
3249 assert_eq!(
3250 control.events(),
3251 vec![
3252 "actor.load:BACKTEST-STATE-ACTOR",
3253 "actor.on_load",
3254 "strategy.load:BACKTEST-STATE-STRATEGY-001",
3255 "strategy.on_load",
3256 "actor.on_start",
3257 "strategy.on_start",
3258 "actor.on_stop",
3259 "strategy.on_stop",
3260 "actor.on_save",
3261 "actor.update:BACKTEST-STATE-ACTOR",
3262 "strategy.on_save",
3263 "strategy.update:BACKTEST-STATE-STRATEGY-001",
3264 "database.close",
3265 ]
3266 );
3267 assert_eq!(control.actor_state(&actor_id), Some(actor_save));
3268 assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
3269 assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
3270 }
3271
3272 #[rstest]
3273 fn test_backtest_state_persistence_reports_callback_errors_after_shutdown() {
3274 let actor_id = ActorId::from("BACKTEST-FAIL-SAVE-ACTOR");
3275 let strategy_id = StrategyId::from("BACKTEST-FAIL-SAVE-STRATEGY-001");
3276 let (database, control) = TestCacheDatabaseControl::create();
3277 let config = BacktestEngineConfig {
3278 save_state: true,
3279 run_analysis: false,
3280 ..Default::default()
3281 };
3282 let mut engine = BacktestEngine::new(config).unwrap();
3283 engine
3284 .kernel
3285 .cache
3286 .borrow_mut()
3287 .set_database(Box::new(database));
3288 engine
3289 .add_actor(StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save())
3290 .unwrap();
3291 engine
3292 .add_strategy(
3293 StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save(),
3294 )
3295 .unwrap();
3296
3297 let error = engine
3298 .run(
3299 Some(UnixNanos::from(0)),
3300 Some(UnixNanos::from(1)),
3301 None,
3302 false,
3303 )
3304 .unwrap_err();
3305 engine.dispose();
3306
3307 assert_eq!(
3308 error.to_string(),
3309 "Failed to save component state: actor BACKTEST-FAIL-SAVE-ACTOR callback: test actor \
3310 on_save failure; strategy BACKTEST-FAIL-SAVE-STRATEGY-001 callback: test strategy \
3311 on_save failure"
3312 );
3313 assert_eq!(
3314 control.events(),
3315 vec![
3316 "actor.on_start",
3317 "strategy.on_start",
3318 "actor.on_stop",
3319 "strategy.on_stop",
3320 "actor.on_save",
3321 "strategy.on_save",
3322 "database.close",
3323 ]
3324 );
3325 assert!(!engine.kernel.trader.borrow().is_running());
3326 assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
3327 }
3328
3329 #[rstest]
3330 #[case(None)]
3331 #[case(Some(true))]
3332 #[case(Some(false))]
3333 fn test_new_forces_drop_instruments_on_reset_false(
3334 crypto_perpetual_ethusdt: CryptoPerpetual,
3335 #[case] user_value: Option<bool>,
3336 ) {
3337 use nautilus_common::cache::CacheConfig;
3338
3339 let config = match user_value {
3340 None => BacktestEngineConfig::builder().build(),
3341 Some(value) => BacktestEngineConfig::builder()
3342 .cache(
3343 CacheConfig::builder()
3344 .drop_instruments_on_reset(value)
3345 .build()
3346 .unwrap(),
3347 )
3348 .build(),
3349 };
3350 let mut engine = BacktestEngine::new(config).unwrap();
3351
3352 let venue_config = SimulatedVenueConfig::builder()
3353 .venue(Venue::from("BINANCE"))
3354 .oms_type(OmsType::Netting)
3355 .account_type(AccountType::Margin)
3356 .book_type(BookType::L1_MBP)
3357 .starting_balances(vec![Money::from("1_000_000 USDT")])
3358 .build()
3359 .unwrap();
3360 engine.add_venue(venue_config).unwrap();
3361
3362 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
3363 let instrument_id = instrument.id();
3364 engine.add_instrument(&instrument).unwrap();
3365
3366 engine.reset().unwrap();
3367
3368 assert!(
3369 engine
3370 .kernel()
3371 .cache
3372 .borrow()
3373 .instrument(&instrument_id)
3374 .is_some(),
3375 "instrument must survive engine.reset(); user-supplied \
3376 drop_instruments_on_reset={user_value:?} must not leak through",
3377 );
3378 }
3379
3380 #[rstest]
3381 fn test_reset_resets_order_emulator_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
3382 let mut engine = create_engine();
3383 let data_commands =
3384 register_data_command_handler("DataEngine.queue_execute.backtest_reset");
3385 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt.clone());
3386 let instrument_id = instrument.id();
3387 engine.add_instrument(&instrument).unwrap();
3388 let order = create_stop_market_order(&crypto_perpetual_ethusdt);
3389 let command = create_submit_order_command(&order);
3390 engine
3391 .kernel
3392 .cache
3393 .borrow_mut()
3394 .add_order(order, None, None, false)
3395 .unwrap();
3396 let order_emulator = engine.kernel.order_emulator.emulator();
3397 let mut order_emulator = order_emulator.borrow_mut();
3398 order_emulator.cache_submit_order_command(command.clone());
3399 order_emulator.handle_submit_order(&command);
3400 drop(order_emulator);
3401 data_commands.clear();
3402
3403 engine.reset().unwrap();
3404
3405 let commands = data_commands.get_messages();
3406 let emulator = engine.kernel.order_emulator.get_emulator();
3407 assert!(emulator.subscribed_quotes().is_empty());
3408 assert!(emulator.subscribed_trades().is_empty());
3409 assert!(emulator.get_matching_core(&instrument_id).is_none());
3410 assert!(commands.iter().any(|command| matches!(
3411 command,
3412 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
3413 if command.instrument_id == instrument_id
3414 )));
3415 }
3416
3417 #[rstest]
3418 fn test_route_data_to_exchange_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
3419 let mut engine = create_engine();
3420 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
3421 let instrument_id = instrument.id();
3422 engine.add_instrument(&instrument).unwrap();
3423
3424 let status = InstrumentStatus::new(
3425 instrument_id,
3426 MarketStatusAction::Close,
3427 UnixNanos::from(1),
3428 UnixNanos::from(1),
3429 None,
3430 None,
3431 None,
3432 None,
3433 None,
3434 );
3435
3436 engine
3437 .route_data_to_exchange(&Data::InstrumentStatus(status))
3438 .unwrap();
3439
3440 let exchange = engine.venues.get(&instrument_id.venue).unwrap().borrow();
3441 let market_status = exchange
3442 .get_matching_engine(&instrument_id)
3443 .unwrap()
3444 .market_status;
3445 assert_eq!(market_status, MarketStatus::Closed);
3446 }
3447}