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