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