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};
62use nautilus_portfolio::portfolio::is_snapshot_timer;
63#[cfg(feature = "python")]
64use nautilus_system::trader::Trader;
65use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
66use nautilus_trading::{
67 ExecutionAlgorithm, ExecutionAlgorithmNative,
68 strategy::{Strategy, StrategyNative},
69};
70
71use crate::{
72 accumulator::TimeEventAccumulator,
73 config::{BacktestEngineConfig, SimulatedVenueConfig},
74 data_client::BacktestDataClient,
75 data_iterator::BacktestDataIterator,
76 exchange::{SettlementScope, SimulatedExchange},
77 execution_client::BacktestExecutionClient,
78 result::{
79 BacktestResult, CanonicalBacktestResult, CanonicalBacktestState, CanonicalDiagnostic,
80 CanonicalDiagnosticCode, CanonicalRunOutcome,
81 },
82};
83
84const CALLBACK_DRAIN_BUDGET: usize = 1024;
85
86pub struct BacktestEngine {
99 kernel: NautilusKernel,
100 instance_id: UUID4,
101 config: BacktestEngineConfig,
102 accumulator: TimeEventAccumulator,
103 run_config_id: Option<String>,
104 run_id: Option<UUID4>,
105 venues: IndexMap<Venue, Rc<RefCell<SimulatedExchange>>>,
106 exec_clients: Vec<BacktestExecutionClient>,
107 has_data: AHashSet<InstrumentId>,
108 has_book_data: AHashSet<InstrumentId>,
109 has_book_processed: AHashSet<InstrumentId>,
110 data_iterator: BacktestDataIterator,
111 data_len: usize,
112 data_stream_counter: usize,
113 ts_first: Option<UnixNanos>,
114 ts_last_data: Option<UnixNanos>,
115 sorted: bool,
116 iteration: usize,
117 force_stop: bool,
118 last_ns: UnixNanos,
119 last_module_ns: Option<UnixNanos>,
120 last_liquidation_ns: Option<UnixNanos>,
121 end_ns: UnixNanos,
122 run_started: Option<UnixNanos>,
123 run_finished: Option<UnixNanos>,
124 backtest_start: Option<UnixNanos>,
125 backtest_end: Option<UnixNanos>,
126 funding_error: Option<String>,
127}
128
129impl Debug for BacktestEngine {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.debug_struct(stringify!(BacktestEngine))
132 .field("instance_id", &self.instance_id)
133 .field("run_config_id", &self.run_config_id)
134 .field("run_id", &self.run_id)
135 .finish_non_exhaustive()
136 }
137}
138
139impl BacktestEngine {
140 pub fn new(mut config: BacktestEngineConfig) -> anyhow::Result<Self> {
146 let mut cache_config = config.cache.unwrap_or_default();
149 cache_config.drop_instruments_on_reset = false;
150 config.cache = Some(cache_config);
151
152 let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
153 let instance_id = kernel.instance_id;
154
155 #[cfg(feature = "python")]
156 if let Some(controller) = config.controller.as_ref() {
157 Trader::add_controller_from_importable_config(&kernel.trader, controller)?;
158 }
159 #[cfg(not(feature = "python"))]
160 if let Some(controller) = config.controller.as_ref() {
161 anyhow::bail!(
162 "BacktestEngineConfig.controller for importable controller '{}' requires the python feature",
163 controller.controller_path
164 );
165 }
166
167 Ok(Self {
168 kernel,
169 instance_id,
170 config,
171 accumulator: TimeEventAccumulator::new(),
172 run_config_id: None,
173 run_id: None,
174 venues: IndexMap::new(),
175 exec_clients: Vec::new(),
176 has_data: AHashSet::new(),
177 has_book_data: AHashSet::new(),
178 has_book_processed: AHashSet::new(),
179 data_iterator: BacktestDataIterator::new(),
180 data_len: 0,
181 data_stream_counter: 0,
182 ts_first: None,
183 ts_last_data: None,
184 sorted: true,
185 iteration: 0,
186 force_stop: false,
187 last_ns: UnixNanos::default(),
188 last_module_ns: None,
189 last_liquidation_ns: None,
190 end_ns: UnixNanos::default(),
191 run_started: None,
192 run_finished: None,
193 backtest_start: None,
194 backtest_end: None,
195 funding_error: None,
196 })
197 }
198
199 #[must_use]
201 pub const fn kernel(&self) -> &NautilusKernel {
202 &self.kernel
203 }
204
205 pub fn kernel_mut(&mut self) -> &mut NautilusKernel {
207 &mut self.kernel
208 }
209
210 #[must_use]
212 pub fn trader_id(&self) -> TraderId {
213 self.kernel.trader_id()
214 }
215
216 #[must_use]
218 pub fn machine_id(&self) -> &str {
219 self.kernel.machine_id()
220 }
221
222 #[must_use]
224 pub fn instance_id(&self) -> UUID4 {
225 self.instance_id
226 }
227
228 #[must_use]
230 pub fn iteration(&self) -> usize {
231 self.iteration
232 }
233
234 #[must_use]
236 pub fn run_config_id(&self) -> Option<&str> {
237 self.run_config_id.as_deref()
238 }
239
240 #[must_use]
242 pub const fn run_id(&self) -> Option<UUID4> {
243 self.run_id
244 }
245
246 #[must_use]
248 pub const fn run_started(&self) -> Option<UnixNanos> {
249 self.run_started
250 }
251
252 #[must_use]
254 pub const fn run_finished(&self) -> Option<UnixNanos> {
255 self.run_finished
256 }
257
258 #[must_use]
260 pub const fn backtest_start(&self) -> Option<UnixNanos> {
261 self.backtest_start
262 }
263
264 #[must_use]
266 pub const fn backtest_end(&self) -> Option<UnixNanos> {
267 self.backtest_end
268 }
269
270 #[must_use]
272 pub fn list_venues(&self) -> Vec<Venue> {
273 self.venues.keys().copied().collect()
274 }
275
276 pub fn add_venue(&mut self, config: SimulatedVenueConfig) -> anyhow::Result<()> {
281 let venue = config.venue;
284 if self.venues.contains_key(&venue) {
285 anyhow::bail!("Venue {venue} is already registered");
286 }
287
288 let routing = Some(config.routing);
289 let frozen_account = Some(config.frozen_account);
290 let use_message_queue = config.use_message_queue;
291
292 let exchange =
293 SimulatedExchange::new(config, self.kernel.cache.clone(), self.kernel.clock.clone())?;
294 let exchange = Rc::new(RefCell::new(exchange));
295
296 let account_id = AccountId::from(format!("{venue}-001").as_str());
297
298 let exec_client = BacktestExecutionClient::new(
299 self.config.trader_id(),
300 account_id,
301 &exchange,
302 self.kernel.cache.clone(),
303 self.kernel.clock.clone(),
304 routing,
305 frozen_account,
306 );
307
308 if !use_message_queue {
309 exchange
310 .borrow_mut()
311 .set_event_handler(exec_client.order_event_handler());
312 }
313
314 exchange
315 .borrow_mut()
316 .register_client(Rc::new(exec_client.clone()));
317
318 {
319 let mut exec_engine = self.kernel.exec_engine.borrow_mut();
320 let client_id = exec_client.client_id();
321 exec_engine.register_client(Box::new(exec_client.clone()))?;
322 if let Err(e) = exec_engine.register_venue_routing(client_id, venue) {
323 exec_engine.deregister_client(client_id)?;
324 return Err(e);
325 }
326 }
327
328 SimulatedExchange::register_spread_quote_endpoint(&exchange);
329 self.venues.insert(venue, exchange);
330 self.exec_clients.push(exec_client);
331
332 log::info!("Adding exchange {venue} to engine");
333
334 Ok(())
335 }
336
337 pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModelHandle) {
339 if let Some(exchange) = self.venues.get_mut(&venue) {
340 exchange.borrow_mut().set_fill_model(fill_model);
341 } else {
342 log::warn!(
343 "BacktestEngine::change_fill_model called for unknown venue {venue}, ignoring"
344 );
345 }
346 }
347
348 pub fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
356 let instrument_id = instrument.id();
357 if let Some(exchange) = self.venues.get(&instrument.id().venue) {
358 let previous_expiration_ns = exchange.borrow().instrument_expiration(instrument_id);
359
360 if matches!(
361 instrument,
362 InstrumentAny::CurrencyPair(_) | InstrumentAny::TokenizedAsset(_)
363 ) && exchange.borrow().account_type != AccountType::Margin
364 && exchange.borrow().base_currency.is_some()
365 {
366 anyhow::bail!(
367 "Cannot add a multi-currency spot instrument {instrument_id} for a venue with a single-currency CASH account"
368 )
369 }
370 exchange.borrow_mut().add_instrument(instrument.clone())?;
371 if let Some(expiration_ns) = instrument.expiration_ns() {
372 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
373 }
374
375 if let Some(previous_expiration_ns) = previous_expiration_ns
376 && instrument.expiration_ns() != Some(previous_expiration_ns)
377 && !exchange
378 .borrow()
379 .has_unprocessed_instrument_expiration(previous_expiration_ns)
380 {
381 let timer_name = Self::instrument_expiration_timer_name(
382 instrument_id.venue,
383 previous_expiration_ns,
384 );
385 self.kernel.clock.borrow_mut().cancel_timer(&timer_name);
386 }
387 } else {
388 anyhow::bail!(
389 "Cannot add an `Instrument` object without first adding its associated venue {}",
390 instrument.id().venue
391 )
392 }
393
394 self.add_market_data_client_if_not_exists(instrument.id().venue);
395
396 self.kernel
397 .data_engine
398 .borrow_mut()
399 .process(instrument as &dyn Any);
400 log::info!(
401 "Added instrument {} to exchange {}",
402 instrument_id,
403 instrument_id.venue
404 );
405 Ok(())
406 }
407
408 pub fn add_data(
421 &mut self,
422 mut data: Vec<Data>,
423 client_id: Option<ClientId>,
424 validate: bool,
425 sort: bool,
426 ) -> anyhow::Result<()> {
427 if sort {
428 data.sort_by_key(HasTsInit::ts_init);
429 }
430
431 let stream_name =
432 self.register_added_data(data.iter().map(DataRef::from), client_id, validate)?;
433 self.data_iterator.add_data(&stream_name, data, true);
434 self.sorted = sort;
435
436 Ok(())
437 }
438
439 pub fn add_data_batch(
449 &mut self,
450 data: DataBatch,
451 client_id: Option<ClientId>,
452 validate: bool,
453 sort: bool,
454 ) -> anyhow::Result<()> {
455 let stream_name = self.register_added_data(
456 (0..data.len()).filter_map(|index| data.get(index)),
457 client_id,
458 validate,
459 )?;
460 self.data_iterator.add_data_batch(&stream_name, data, true);
461 self.sorted = sort;
462
463 Ok(())
464 }
465
466 fn register_added_data<'a>(
467 &mut self,
468 items: impl Iterator<Item = DataRef<'a>> + Clone,
469 client_id: Option<ClientId>,
470 validate: bool,
471 ) -> anyhow::Result<String> {
472 #[cfg(not(feature = "defi"))]
473 let _ = client_id;
474
475 let Some(first) = items.clone().next() else {
476 anyhow::bail!("data was empty");
477 };
478
479 #[cfg(not(feature = "defi"))]
480 anyhow::ensure!(
481 !items.clone().any(is_defi),
482 "DeFi data requires the nautilus-backtest `defi` feature"
483 );
484
485 if validate {
486 if !is_defi(first) && !matches!(first, DataRef::Custom(_)) {
489 let first_instrument_id = first.instrument_id();
490 anyhow::ensure!(
491 self.kernel
492 .cache
493 .borrow()
494 .instrument(&first_instrument_id)
495 .is_some(),
496 "Instrument {first_instrument_id} for the given data not found in the cache. \
497 Add the instrument through `add_instrument()` prior to adding related data."
498 );
499
500 if let DataRef::Bar(bar) = first {
501 anyhow::ensure!(
502 bar.bar_type.aggregation_source() == AggregationSource::External,
503 "bar_type.aggregation_source must be External, was {:?}",
504 bar.bar_type.aggregation_source(),
505 );
506 }
507 }
508 }
509
510 let mut count = 0;
516 let mut batch_min_ts: Option<UnixNanos> = None;
517 let mut batch_max_ts: Option<UnixNanos> = None;
518
519 #[cfg(feature = "defi")]
520 if items.clone().any(is_defi) {
521 self.add_defi_data_client_if_not_exists(client_id);
522 }
523
524 for item in items {
525 count += 1;
526 let ts = item.ts_init();
527 batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
528 batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
529
530 if is_defi(item) {
531 continue;
532 }
533
534 if matches!(item, DataRef::Custom(_)) {
535 continue;
537 }
538
539 let instr_id = item.instrument_id();
540 self.has_data.insert(instr_id);
541
542 if item.is_order_book_data() {
543 self.has_book_data.insert(instr_id);
544 }
545
546 self.add_market_data_client_if_not_exists(instr_id.venue);
547 }
548
549 if let Some(ts) = batch_min_ts
550 && self.ts_first.is_none_or(|t| ts < t)
551 {
552 self.ts_first = Some(ts);
553 }
554
555 if let Some(ts) = batch_max_ts
556 && self.ts_last_data.is_none_or(|t| ts > t)
557 {
558 self.ts_last_data = Some(ts);
559 }
560
561 self.data_len += count;
562 let stream_name = format!("backtest_data_{}", self.data_stream_counter);
563 self.data_stream_counter += 1;
564
565 log::info!(
566 "Added {count} data element{} to BacktestEngine ({} total)",
567 if count == 1 { "" } else { "s" },
568 self.data_len,
569 );
570
571 Ok(stream_name)
572 }
573
574 pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
581 where
582 T: DataActor + DataActorNative + Component + Debug + 'static,
583 {
584 self.kernel.trader.borrow_mut().add_actor(actor)
585 }
586
587 pub fn add_actors<T>(&mut self, actors: Vec<T>) -> anyhow::Result<()>
593 where
594 T: DataActor + DataActorNative + Component + Debug + 'static,
595 {
596 for actor in actors {
597 self.add_actor(actor)?;
598 }
599 Ok(())
600 }
601
602 pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
609 where
610 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
611 {
612 let strategy_id = self
613 .kernel
614 .trader
615 .borrow()
616 .prepare_strategy_for_registration(&mut strategy)?;
617 let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
618
619 self.kernel.trader.borrow_mut().add_strategy(strategy)?;
620
621 if let Some(oms_type) = oms_type {
622 self.kernel
623 .exec_engine
624 .borrow_mut()
625 .register_oms_type(strategy_id, oms_type);
626 }
627
628 Ok(())
629 }
630
631 pub fn add_strategies<T>(&mut self, strategies: Vec<T>) -> anyhow::Result<()>
637 where
638 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
639 {
640 for strategy in strategies {
641 self.add_strategy(strategy)?;
642 }
643 Ok(())
644 }
645
646 pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
652 where
653 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
654 {
655 self.kernel
656 .trader
657 .borrow_mut()
658 .add_exec_algorithm(exec_algorithm)
659 }
660
661 pub fn add_exec_algorithms<T>(&mut self, exec_algorithms: Vec<T>) -> anyhow::Result<()>
668 where
669 T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
670 {
671 for exec_algorithm in exec_algorithms {
672 self.add_exec_algorithm(exec_algorithm)?;
673 }
674 Ok(())
675 }
676
677 pub fn run(
702 &mut self,
703 start: Option<UnixNanos>,
704 end: Option<UnixNanos>,
705 run_config_id: Option<String>,
706 streaming: bool,
707 ) -> anyhow::Result<()> {
708 if let Some(error) = actor::callback_failure() {
709 self.abort_run();
710 return Err(error.into());
711 }
712
713 if let Some(error) = &self.funding_error {
714 anyhow::bail!("{error}");
715 }
716 self.check_module_errors()?;
717 self.check_balance_error()?;
718
719 if let Err(e) = self.run_impl(start, end, run_config_id, streaming) {
720 let callback_error = actor::callback_failure();
721 if callback_error.is_some()
722 || e.is::<CallbackDispatchError>()
723 || self.funding_error.is_some()
724 || self.kernel.portfolio.borrow().balance_error().is_some()
725 || self
726 .venues
727 .values()
728 .any(|exchange| exchange.borrow().has_module_error())
729 {
730 self.abort_run();
731 }
732 return Err(match callback_error {
733 Some(callback_error) if !e.is::<CallbackDispatchError>() => {
734 let message = format!("Callback dispatch failed: {callback_error}; {e:#}");
735 e.context(message)
736 }
737 _ => e,
738 });
739 }
740
741 if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
748 self.end()?;
749 }
750
751 Ok(())
752 }
753
754 fn run_impl(
755 &mut self,
756 start: Option<UnixNanos>,
757 end: Option<UnixNanos>,
758 run_config_id: Option<String>,
759 streaming: bool,
760 ) -> anyhow::Result<()> {
761 anyhow::ensure!(
762 self.sorted,
763 "Data has been added but not sorted, call `engine.sort_data()` or use \
764 `engine.add_data(..., sort=true)` before running"
765 );
766
767 for exchange in self.venues.values() {
768 let exchange = exchange.borrow();
769 let book_type_has_depth = exchange.book_type() as u8 > BookType::L1_MBP as u8;
770 if !book_type_has_depth {
771 continue;
772 }
773
774 for instrument_id in exchange.instrument_ids() {
775 let has_data = self.has_data.contains(instrument_id);
776 let missing_book_data = !self.has_book_data.contains(instrument_id)
777 && !self.has_book_processed.contains(instrument_id);
778
779 if has_data && missing_book_data {
780 anyhow::bail!(
781 "No order book data found for instrument '{instrument_id}' when `book_type` \
782 is '{:?}'. Set the venue `book_type` to 'L1_MBP' (for top-of-book data \
783 like quotes, trades, and bars) or provide order book data for this \
784 instrument.",
785 exchange.book_type()
786 );
787 }
788 }
789 }
790
791 let start_ns = start.unwrap_or_else(|| self.ts_first.unwrap_or_default());
793 let end_ns = end.unwrap_or_else(|| self.ts_last_data.unwrap_or(start_ns));
794 anyhow::ensure!(start_ns <= end_ns, "start was > end");
795 self.end_ns = end_ns;
796 self.last_ns = start_ns;
797 self.last_module_ns = None;
798
799 let clocks = self.collect_all_clocks();
801 Self::set_all_clocks_time(&clocks, start_ns);
802
803 if self.iteration == 0 {
805 self.set_instrument_expiration_timers()?;
806
807 self.run_config_id = run_config_id;
808 self.run_id = Some(UUID4::new());
809 self.run_started = Some(UnixNanos::from(nanos_since_unix_epoch()));
810 self.backtest_start = Some(start_ns);
811
812 for exchange in self.venues.values() {
813 let mut ex = exchange.borrow_mut();
814 ex.initialize_account();
815 ex.load_open_orders();
816 }
817
818 Self::set_all_clocks_time(&clocks, start_ns);
820
821 self.force_stop = false;
823 self.kernel.reset_shutdown_flag();
824
825 Self::init_command_senders();
827
828 logging_clock_set_static_mode();
830 logging_clock_set_static_time(start_ns.as_u64());
831
832 self.kernel.start();
834 if self.kernel.is_event_store_replay() {
835 self.log_pre_run();
836 return Ok(());
837 }
838
839 if self.kernel.is_event_store_replay_configured() {
840 anyhow::bail!("event-store replay did not start");
841 }
842
843 if let Err(e) = self.kernel.start_trader() {
844 if actor::callback_failure().is_none() && !e.is::<CallbackDispatchError>() {
846 self.abort_run();
847 }
848 return Err(e);
849 }
850
851 loop {
854 drain_data_cmd_queue();
855 let callbacks_pending = actor::drain_callbacks(CALLBACK_DRAIN_BUDGET)?;
856 if data_cmd_queue_is_empty() && !callbacks_pending {
857 break;
858 }
859 }
860
861 self.log_pre_run();
862 }
863
864 self.log_run();
865
866 while let Some(d) = self.data_iterator.peek() {
868 if d.ts_init() >= start_ns {
869 break;
870 }
871 self.data_iterator.advance();
872 }
873
874 if let Some(d) = self.data_iterator.peek() {
876 let ts = d.ts_init();
877 self.last_ns = ts.saturating_sub(DurationNanos::new(1));
878 } else {
879 self.last_ns = start_ns;
880 }
881
882 loop {
883 if self.kernel.is_shutdown_requested() {
884 log::info!("Shutdown requested via ShutdownSystem, ending backtest");
885 self.force_stop = true;
886 }
887
888 if self.force_stop {
889 log::info!("Force stop triggered, ending backtest");
890 break;
891 }
892
893 let Some(data) = self.data_iterator.peek() else {
894 if streaming {
895 break;
899 }
900 let done = self.process_next_timer(&clocks)?;
901 if self.data_iterator.peek().is_none() && done {
902 break;
903 }
904 continue;
905 };
906
907 let ts_init = data.ts_init();
908
909 if ts_init > end_ns {
910 break;
911 }
912
913 if ts_init > self.last_ns {
914 self.advance_time_impl(ts_init, &clocks)?;
915 }
916
917 if self.kernel.is_shutdown_requested() {
920 self.force_stop = true;
921 break;
922 }
923
924 let settlement_scope = {
925 let Some(data) = self.data_iterator.peek() else {
926 continue;
927 };
928 let settlement_scope = Self::settlement_scope(data);
929 Self::route_data_to_exchange(
930 &self.venues,
931 &mut self.has_book_processed,
932 &self.kernel.clock,
933 data,
934 )?;
935 self.kernel.data_engine.borrow_mut().process_data_ref(data);
936 settlement_scope
937 };
938 self.data_iterator.advance();
939
940 self.drain_command_queues()?;
942 self.settle_venues(ts_init, settlement_scope)?;
943
944 let prev_last_ns = self.last_ns;
945 if self
947 .data_iterator
948 .peek()
949 .is_none_or(|next| next.ts_init() > prev_last_ns)
950 {
951 self.flush_accumulator_events(&clocks, prev_last_ns)?;
952 self.finalize_timestamp(&clocks, prev_last_ns, settlement_scope)?;
953 }
954
955 self.iteration += 1;
956 }
957
958 if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
959 let ts_now = self.kernel.clock.borrow().timestamp_ns();
960 self.finalize_timestamp(&clocks, ts_now, SettlementScope::All)?;
961 }
962
963 let flush_ts = if streaming || self.force_stop || self.kernel.is_shutdown_requested() {
966 self.last_ns
967 } else {
968 end_ns
969 };
970 self.flush_accumulator_events(&clocks, flush_ts)?;
971
972 Ok(())
973 }
974
975 fn settlement_scope(data: DataRef<'_>) -> SettlementScope {
976 match data {
977 DataRef::BookDelta(_)
978 | DataRef::BookDeltas(_)
979 | DataRef::BookDepth(_)
980 | DataRef::Quote(_)
981 | DataRef::Trade(_)
982 | DataRef::Bar(_) => SettlementScope::Data(Some(data.instrument_id())),
983 DataRef::MarkPrice(_) | DataRef::IndexPrice(_) => SettlementScope::Data(None),
984 DataRef::FundingRate(_) => SettlementScope::Data(Some(data.instrument_id())),
985 DataRef::OptionGreeks(_) => SettlementScope::Data(None),
986 DataRef::InstrumentStatus(_) | DataRef::InstrumentClose(_) => {
987 SettlementScope::Data(Some(data.instrument_id()))
988 }
989 DataRef::Instrument(_) | DataRef::Custom(_) => SettlementScope::Data(None),
990 #[cfg(feature = "defi")]
991 DataRef::Defi(_) => SettlementScope::Data(None),
992 #[cfg(not(feature = "defi"))]
993 #[allow(
994 unreachable_patterns,
995 reason = "DeFi variants can exist without this crate's defi feature"
996 )]
997 _ => SettlementScope::Data(None),
998 }
999 }
1000
1001 fn abort_run(&mut self) {
1002 self.force_stop = true;
1003 self.accumulator.clear();
1004 self.kernel.stop_trader();
1005 self.kernel.data_engine.borrow_mut().stop();
1006 self.kernel.risk_engine.borrow_mut().stop();
1007 self.kernel.exec_engine.borrow_mut().stop();
1008 clear_command_queues();
1009
1010 if let Err(e) = actor::clear_callbacks() {
1011 log::error!("Failed to clear callback dispatch while aborting backtest: {e}");
1012 }
1013 self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
1014 self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
1015 logging_clock_set_realtime_mode();
1016 }
1017
1018 pub fn end(&mut self) -> anyhow::Result<()> {
1027 let result = self.end_impl();
1028 if let Err(e) = &result
1029 && (e.is::<CallbackDispatchError>()
1030 || self.kernel.portfolio.borrow().balance_error().is_some())
1031 {
1032 self.abort_run();
1033 }
1034 result
1035 }
1036
1037 fn end_impl(&mut self) -> anyhow::Result<()> {
1038 if let Some(error) = actor::callback_failure() {
1039 return Err(error.into());
1040 }
1041
1042 if let Some(error) = &self.funding_error {
1043 anyhow::bail!("{error}");
1044 }
1045 self.check_balance_error()?;
1046
1047 if self.end_ns.as_u64() > 0 {
1054 let clocks = self.collect_all_clocks();
1055 let flush_ts = if self.force_stop || self.kernel.is_shutdown_requested() {
1056 self.last_ns
1057 } else {
1058 self.end_ns
1059 };
1060
1061 if let Err(e) = self.flush_accumulator_events(&clocks, flush_ts) {
1062 if self.funding_error.is_some()
1063 || self
1064 .venues
1065 .values()
1066 .any(|exchange| exchange.borrow().has_module_error())
1067 {
1068 self.abort_run();
1069 }
1070 return Err(e);
1071 }
1072 }
1073
1074 let mut ts_now = self.kernel.clock.borrow().timestamp_ns();
1077 self.settle_venues(ts_now, SettlementScope::All)?;
1078
1079 self.kernel.stop_trader();
1080
1081 self.drain_command_queues()?;
1086
1087 if let Some(max_inflight_ts) = self.max_inflight_command_ts()
1090 && max_inflight_ts > ts_now
1091 {
1092 ts_now = max_inflight_ts;
1093 let clocks = self.collect_all_clocks();
1094 Self::set_all_clocks_time(&clocks, ts_now);
1095 }
1096
1097 self.settle_venues(ts_now, SettlementScope::All)?;
1098
1099 for strategy_id in self.running_strategy_ids() {
1100 log::error!(
1101 "Strategy {strategy_id} is still RUNNING after the backtest end sequence; its stop did not complete",
1102 );
1103 }
1104
1105 let save_result = self.kernel.save_trader_state();
1106 let callback_result = self.drain_command_queues();
1107 let diagnostics_result = self
1108 .venues
1109 .values()
1110 .try_for_each(|exchange| exchange.borrow().log_diagnostics());
1111 self.kernel.portfolio.borrow_mut().finalize_equity_curve();
1112
1113 self.kernel.data_engine.borrow_mut().stop();
1115 self.kernel.risk_engine.borrow_mut().stop();
1116 self.kernel.exec_engine.borrow_mut().stop();
1117
1118 let streaming_result = self.kernel.close_streaming_writer();
1119
1120 self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
1121 self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
1122
1123 logging_clock_set_realtime_mode();
1125
1126 self.log_post_run();
1127 callback_result?;
1128 actor::clear_callbacks()?;
1129 save_result?;
1130 diagnostics_result?;
1131 streaming_result
1132 }
1133
1134 fn running_strategy_ids(&self) -> Vec<StrategyId> {
1141 self.kernel
1142 .trader
1143 .borrow()
1144 .strategy_ids()
1145 .into_iter()
1146 .filter(|strategy_id| match component_state(&strategy_id.inner()) {
1147 Ok(state) => matches!(state, ComponentState::Running),
1148 Err(e) => {
1149 log::warn!("Cannot resolve stop state for strategy {strategy_id}: {e}");
1150 false
1151 }
1152 })
1153 .collect()
1154 }
1155
1156 pub fn reset(&mut self) -> anyhow::Result<()> {
1166 log::debug!("Resetting");
1167
1168 let mut reset_error = None;
1169
1170 if self.kernel.trader.borrow().is_running()
1171 && let Err(e) = self.end()
1172 {
1173 reset_error = Some(e);
1174 }
1175
1176 self.kernel.data_engine.borrow_mut().stop();
1178 self.kernel.data_engine.borrow_mut().reset();
1179
1180 self.kernel.exec_engine.borrow_mut().stop();
1181
1182 for exchange in self.venues.values() {
1185 if let Err(e) = exchange.borrow_mut().reset()
1186 && reset_error.is_none()
1187 {
1188 reset_error = Some(e);
1189 }
1190 }
1191 self.kernel.exec_engine.borrow_mut().reset();
1192
1193 self.kernel.risk_engine.borrow_mut().stop();
1194 self.kernel.risk_engine.borrow_mut().reset();
1195
1196 self.kernel.order_emulator.reset();
1197
1198 if let Err(e) = self.kernel.trader.borrow_mut().reset() {
1200 log::error!("Error resetting trader: {e:?}");
1201 }
1202
1203 self.kernel.portfolio.borrow_mut().reset();
1204
1205 self.run_config_id = None;
1207 self.run_id = None;
1208 self.run_started = None;
1209 self.run_finished = None;
1210 self.backtest_start = None;
1211 self.backtest_end = None;
1212 self.funding_error = None;
1213 self.iteration = 0;
1214 self.force_stop = false;
1215 self.last_ns = UnixNanos::default();
1216 self.last_module_ns = None;
1217 self.last_liquidation_ns = None;
1218 self.end_ns = UnixNanos::default();
1219 self.has_book_processed.clear();
1220
1221 self.accumulator.clear();
1222 self.cancel_funding_settlement_timers();
1223
1224 self.data_iterator.reset_all_cursors();
1226
1227 if let Err(e) = self.kernel.reopen_streaming_writer()
1229 && reset_error.is_none()
1230 {
1231 reset_error = Some(e);
1232 }
1233
1234 clear_command_queues();
1235
1236 if let Err(e) = actor::clear_callbacks()
1237 && reset_error.is_none()
1238 {
1239 reset_error = Some(e.into());
1240 }
1241
1242 log::info!("Reset");
1243
1244 if let Some(e) = reset_error {
1245 return Err(e);
1246 }
1247 Ok(())
1248 }
1249
1250 pub fn sort_data(&mut self) {
1255 self.sorted = true;
1259 log::info!("Data sort requested (iterator merges streams by replay timestamp)");
1260 }
1261
1262 pub fn clear_data(&mut self) {
1264 self.has_data.clear();
1265 self.has_book_data.clear();
1266 self.data_iterator = BacktestDataIterator::new();
1267 self.data_len = 0;
1268 self.data_stream_counter = 0;
1269 self.ts_first = None;
1270 self.ts_last_data = None;
1271 self.sorted = true;
1272 }
1273
1274 pub fn clear_actors(&mut self) -> anyhow::Result<()> {
1280 self.kernel.trader.borrow_mut().clear_actors()
1281 }
1282
1283 pub fn clear_strategies(&mut self) -> anyhow::Result<()> {
1289 self.kernel.trader.borrow_mut().clear_strategies()
1290 }
1291
1292 pub fn clear_exec_algorithms(&mut self) -> anyhow::Result<()> {
1298 self.kernel.trader.borrow_mut().clear_exec_algorithms()
1299 }
1300
1301 pub fn dispose(&mut self) {
1305 self.clear_data();
1306 self.accumulator.clear();
1307 self.kernel.dispose();
1308 clear_command_queues();
1309
1310 if let Err(e) = actor::clear_callbacks() {
1311 log::error!("Failed to clear callback dispatch during disposal: {e}");
1312 }
1313 }
1314
1315 #[must_use]
1317 pub fn get_result(&self) -> BacktestResult {
1318 let elapsed_time_secs = match (self.backtest_start, self.backtest_end) {
1319 (Some(start), Some(end)) => end.saturating_duration_since(start).as_secs_f64(),
1320 _ => 0.0,
1321 };
1322
1323 let cache = self.kernel.cache.borrow();
1324 let orders = cache.orders(None, None, None, None, None);
1325 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1326 let total_orders = orders.len();
1327 let positions: Vec<Position> = cache
1328 .positions(None, None, None, None, None)
1329 .into_iter()
1330 .map(|p| p.cloned())
1331 .collect();
1332 let cached_positions_count = positions.len();
1333 let snapshot_positions = cache.position_snapshots(None, None).len();
1334 let total_positions = Self::total_positions_with_snapshots(&cache, cached_positions_count);
1335 let summary = self.build_result_summary(
1336 &cache,
1337 total_events,
1338 total_orders,
1339 cached_positions_count,
1340 snapshot_positions,
1341 );
1342
1343 let stats = self.kernel.portfolio.borrow().statistics();
1344 let stats_pnls = stats.pnls;
1345 let stats_returns = stats.returns;
1346 let stats_general = stats.general;
1347 let returns_series = stats.returns_series;
1348
1349 BacktestResult {
1350 trader_id: self.config.trader_id().to_string(),
1351 machine_id: self.kernel.machine_id.clone(),
1352 instance_id: self.instance_id,
1353 run_config_id: self.run_config_id.clone(),
1354 run_id: self.run_id,
1355 run_started: self.run_started,
1356 run_finished: self.run_finished,
1357 backtest_start: self.backtest_start,
1358 backtest_end: self.backtest_end,
1359 elapsed_time_secs,
1360 iterations: self.iteration,
1361 total_events,
1362 total_orders,
1363 total_positions,
1364 summary,
1365 stats_pnls,
1366 stats_returns,
1367 stats_general,
1368 returns_series,
1369 }
1370 }
1371
1372 pub fn get_canonical_result(&self) -> anyhow::Result<CanonicalBacktestResult> {
1382 let result = self.get_result();
1383 let cache = self.kernel.cache.borrow();
1384 let orders = cache
1385 .orders(None, None, None, None, None)
1386 .into_iter()
1387 .map(|order| order.cloned())
1388 .collect();
1389 let positions = cache
1390 .positions(None, None, None, None, None)
1391 .into_iter()
1392 .map(|position| position.cloned())
1393 .collect();
1394 let position_snapshots = cache.position_snapshots(None, None);
1395 let accounts = cache.accounts_all_owned();
1396 drop(cache);
1397
1398 let portfolio = self.kernel.portfolio.borrow();
1399 let mut portfolio_snapshots = Vec::new();
1400 for account in &accounts {
1401 portfolio_snapshots.extend(portfolio.snapshots(&account.id()));
1402 }
1403 drop(portfolio);
1404
1405 let trader = self.kernel.trader.borrow();
1406 let trader_state = trader.state().to_string();
1407 let actor_ids = trader
1408 .actor_ids()
1409 .into_iter()
1410 .map(|id| id.to_string())
1411 .collect();
1412 let strategy_ids = trader
1413 .strategy_ids()
1414 .into_iter()
1415 .map(|id| id.to_string())
1416 .collect();
1417 let exec_algorithm_ids = trader
1418 .exec_algorithm_ids()
1419 .into_iter()
1420 .map(|id| id.to_string())
1421 .collect();
1422 drop(trader);
1423
1424 let balance_error = self.kernel.portfolio.borrow().balance_error().is_some();
1425 let outcome = if self.funding_error.is_some() || balance_error {
1426 CanonicalRunOutcome::Failed
1427 } else if self.run_finished.is_none() {
1428 CanonicalRunOutcome::Incomplete
1429 } else if self.force_stop || self.kernel.is_shutdown_requested() {
1430 CanonicalRunOutcome::Stopped
1431 } else {
1432 CanonicalRunOutcome::Completed
1433 };
1434 let diagnostics = [
1435 self.funding_error
1436 .as_ref()
1437 .map(|_| CanonicalDiagnosticCode::FundingSettlementFailed),
1438 balance_error.then_some(CanonicalDiagnosticCode::AccountBalanceRejected),
1439 ]
1440 .into_iter()
1441 .flatten()
1442 .map(|code| CanonicalDiagnostic { code })
1443 .collect();
1444 let statistics = nautilus_analysis::PortfolioStatistics {
1445 pnls: result.stats_pnls,
1446 returns: result.stats_returns,
1447 general: result.stats_general,
1448 returns_series: result.returns_series,
1449 };
1450
1451 CanonicalBacktestResult::from_state(CanonicalBacktestState {
1452 trader_id: result.trader_id,
1453 run_config_id: result.run_config_id,
1454 backtest_start: result.backtest_start,
1455 backtest_end: result.backtest_end,
1456 iterations: result.iterations,
1457 total_events: result.total_events,
1458 total_orders: result.total_orders,
1459 total_positions: result.total_positions,
1460 outcome,
1461 diagnostics,
1462 trader_state,
1463 actor_ids,
1464 strategy_ids,
1465 exec_algorithm_ids,
1466 summary: result.summary.into_iter().collect(),
1467 orders,
1468 positions,
1469 position_snapshots,
1470 accounts,
1471 portfolio_snapshots,
1472 statistics,
1473 })
1474 }
1475
1476 fn build_result_summary(
1477 &self,
1478 cache: &Cache,
1479 total_events: usize,
1480 total_orders: usize,
1481 cached_positions_count: usize,
1482 snapshot_positions: usize,
1483 ) -> AHashMap<String, String> {
1484 let mut summary = AHashMap::new();
1485 summary.insert("iterations".to_string(), self.iteration.to_string());
1486 summary.insert("total_events".to_string(), total_events.to_string());
1487 summary.insert("orders.total".to_string(), total_orders.to_string());
1488 summary.insert(
1489 "orders.open".to_string(),
1490 cache
1491 .orders_open_count(None, None, None, None, None)
1492 .to_string(),
1493 );
1494 summary.insert(
1495 "orders.closed".to_string(),
1496 cache
1497 .orders_closed_count(None, None, None, None, None)
1498 .to_string(),
1499 );
1500 summary.insert(
1501 "orders.emulated".to_string(),
1502 cache
1503 .orders_emulated_count(None, None, None, None, None)
1504 .to_string(),
1505 );
1506 summary.insert(
1507 "orders.inflight".to_string(),
1508 cache
1509 .orders_inflight_count(None, None, None, None, None)
1510 .to_string(),
1511 );
1512 summary.insert(
1513 "positions.total".to_string(),
1514 cached_positions_count.to_string(),
1515 );
1516 summary.insert(
1517 "positions.open".to_string(),
1518 cache
1519 .positions_open_count(None, None, None, None, None)
1520 .to_string(),
1521 );
1522 summary.insert(
1523 "positions.closed".to_string(),
1524 cache
1525 .positions_closed_count(None, None, None, None, None)
1526 .to_string(),
1527 );
1528 summary.insert(
1529 "positions.snapshots".to_string(),
1530 snapshot_positions.to_string(),
1531 );
1532 summary.insert(
1533 "positions.total_with_snapshots".to_string(),
1534 (cached_positions_count + snapshot_positions).to_string(),
1535 );
1536
1537 let mut venues: Vec<Venue> = self.venues.keys().copied().collect();
1538 venues.sort_by_key(ToString::to_string);
1539 summary.insert("venues.total".to_string(), venues.len().to_string());
1540
1541 for venue in venues {
1542 let Some(account) = cache.account_for_venue(&venue) else {
1543 continue;
1544 };
1545
1546 let venue_key = venue.to_string();
1547 let account_key = format!("account.{venue_key}");
1548 summary.insert(format!("{account_key}.id"), account.id().to_string());
1549 summary.insert(
1550 format!("{account_key}.type"),
1551 account.account_type().to_string(),
1552 );
1553 summary.insert(
1554 format!("{account_key}.base_currency"),
1555 account
1556 .base_currency()
1557 .map_or_else(|| "None".to_string(), |currency| currency.code.to_string()),
1558 );
1559 summary.insert(
1560 format!("{account_key}.event_count"),
1561 account.event_count().to_string(),
1562 );
1563
1564 let mut balances: Vec<_> = account.balances().into_iter().collect();
1565 balances.sort_by_key(|(currency, _)| currency.code.to_string());
1566
1567 for (currency, balance) in balances {
1568 let balance_key = format!("{account_key}.balance.{}", currency.code);
1569 summary.insert(format!("{balance_key}.total"), balance.total.to_string());
1570 summary.insert(format!("{balance_key}.free"), balance.free.to_string());
1571 summary.insert(format!("{balance_key}.locked"), balance.locked.to_string());
1572 }
1573 }
1574
1575 summary
1576 }
1577
1578 fn route_data_to_exchange(
1579 venues: &IndexMap<Venue, Rc<RefCell<SimulatedExchange>>>,
1580 has_book_processed: &mut AHashSet<InstrumentId>,
1581 clock: &Rc<RefCell<dyn Clock>>,
1582 data: DataRef<'_>,
1583 ) -> anyhow::Result<()> {
1584 if matches!(
1585 data,
1586 DataRef::Instrument(_)
1587 | DataRef::MarkPrice(_)
1588 | DataRef::IndexPrice(_)
1589 | DataRef::OptionGreeks(_)
1590 | DataRef::Custom(_)
1591 ) {
1592 return Ok(());
1593 }
1594
1595 if is_defi(data) {
1596 return Ok(());
1597 }
1598
1599 let venue = data.instrument_id().venue;
1600 if let Some(exchange) = venues.get(&venue) {
1601 let mut exchange_ref = exchange.borrow_mut();
1602 let mut processed_book_data = false;
1603
1604 match data {
1605 DataRef::BookDelta(delta) => {
1606 exchange_ref.process_order_book_delta(*delta)?;
1607 processed_book_data = true;
1608 }
1609 DataRef::BookDeltas(deltas) => {
1610 exchange_ref.process_order_book_deltas(deltas)?;
1611 processed_book_data = true;
1612 }
1613 DataRef::BookDepth(depth) => {
1614 exchange_ref.process_order_book_depth(depth)?;
1615 processed_book_data = true;
1616 }
1617 DataRef::Quote(quote) => exchange_ref.process_quote_tick(quote)?,
1618 DataRef::Trade(trade) => exchange_ref.process_trade_tick(trade)?,
1619 DataRef::Bar(bar) => exchange_ref.process_bar(*bar)?,
1620 DataRef::MarkPrice(_) | DataRef::IndexPrice(_) => {
1621 unreachable!("filtered before exchange routing")
1622 }
1623 DataRef::FundingRate(funding) => {
1624 let settlement_ns =
1625 exchange_ref.process_funding_rate_deferred(*funding, data.ts_init())?;
1626 Self::schedule_funding_settlement_if_required(clock, venue, settlement_ns);
1627 }
1628 DataRef::OptionGreeks(_) => unreachable!("filtered before exchange routing"),
1629 DataRef::InstrumentStatus(status) => {
1630 exchange_ref.process_instrument_status(*status)?;
1631 }
1632 DataRef::InstrumentClose(close) => {
1633 exchange_ref.process_instrument_close(*close)?;
1634 }
1635 DataRef::Instrument(_) | DataRef::Custom(_) => {
1636 unreachable!("filtered before exchange routing")
1637 }
1638 #[cfg(feature = "defi")]
1639 DataRef::Defi(_) => unreachable!("filtered before exchange routing"),
1640 #[cfg(not(feature = "defi"))]
1641 #[allow(
1642 unreachable_patterns,
1643 reason = "DeFi variants can exist without this crate's defi feature"
1644 )]
1645 _ => unreachable!("filtered before exchange routing"),
1646 }
1647
1648 drop(exchange_ref);
1649
1650 if processed_book_data {
1651 has_book_processed.insert(data.instrument_id());
1652 }
1653 } else {
1654 log::warn!("No exchange found for venue {venue}, data not routed");
1655 }
1656 Ok(())
1657 }
1658
1659 fn check_module_errors(&self) -> anyhow::Result<()> {
1660 for exchange in self.venues.values() {
1661 exchange.borrow().check_module_error()?;
1662 }
1663 Ok(())
1664 }
1665
1666 fn check_balance_error(&self) -> anyhow::Result<()> {
1667 if let Some(error) = self.kernel.portfolio.borrow().balance_error() {
1668 anyhow::bail!("{error}");
1669 }
1670 Ok(())
1671 }
1672
1673 fn advance_time_impl(
1674 &mut self,
1675 ts_now: UnixNanos,
1676 clocks: &[Rc<RefCell<dyn Clock>>],
1677 ) -> anyhow::Result<()> {
1678 for clock in clocks {
1679 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1680 }
1681
1682 let ts_before = ts_now.saturating_sub(DurationNanos::new(1));
1684
1685 let mut shutdown_at: Option<UnixNanos> = None;
1686
1687 while let Some(ts_event) = self
1688 .accumulator
1689 .peek_next_time()
1690 .filter(|ts_event| *ts_event <= ts_before)
1691 {
1692 let settlement_scope = self.run_timer_handlers_at(clocks, ts_event, ts_now)?;
1693
1694 if self.kernel.is_shutdown_requested() {
1695 self.accumulator.clear();
1696 shutdown_at = Some(ts_event);
1697 break;
1698 }
1699
1700 self.finalize_timestamp(clocks, ts_event, settlement_scope)?;
1701
1702 if self.kernel.is_shutdown_requested() {
1703 self.accumulator.clear();
1704 shutdown_at = Some(ts_event);
1705 break;
1706 }
1707
1708 for clock in clocks {
1709 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1710 }
1711 }
1712
1713 if let Some(ts_event) = shutdown_at {
1716 self.last_ns = ts_event;
1717 } else {
1718 self.last_ns = ts_now;
1719 Self::set_all_clocks_time(clocks, ts_now);
1720 logging_clock_set_static_time(ts_now.as_u64());
1721 }
1722
1723 Ok(())
1724 }
1725
1726 fn flush_accumulator_events(
1727 &mut self,
1728 clocks: &[Rc<RefCell<dyn Clock>>],
1729 ts_now: UnixNanos,
1730 ) -> anyhow::Result<()> {
1731 if self.kernel.is_shutdown_requested() {
1733 self.accumulator.clear();
1734 return Ok(());
1735 }
1736
1737 let last_ns = self.last_ns;
1738
1739 for clock in clocks {
1740 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1741 }
1742
1743 while let Some(ts_event) = self
1744 .accumulator
1745 .peek_next_time()
1746 .filter(|ts_event| *ts_event <= ts_now)
1747 {
1748 let settlement_scope = self.run_timer_handlers_at(clocks, ts_event, ts_now)?;
1749
1750 if self.kernel.is_shutdown_requested() {
1751 self.accumulator.clear();
1752 break;
1753 }
1754
1755 self.finalize_timestamp(clocks, ts_event, settlement_scope)?;
1756
1757 if self.kernel.is_shutdown_requested() {
1758 self.accumulator.clear();
1759 break;
1760 }
1761
1762 for clock in clocks {
1763 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1764 }
1765 }
1766
1767 if !self.kernel.is_shutdown_requested() {
1768 self.last_ns = last_ns;
1769 }
1770
1771 Ok(())
1772 }
1773
1774 fn process_next_timer(&mut self, clocks: &[Rc<RefCell<dyn Clock>>]) -> anyhow::Result<bool> {
1775 self.flush_accumulator_events(clocks, self.last_ns)?;
1776
1777 let mut min_next_time: Option<UnixNanos> = None;
1779
1780 for clock in clocks {
1781 let clock_ref = clock.borrow();
1782 for name in clock_ref.timer_names() {
1783 if let Some(next_time) = clock_ref.next_time_ns(name)
1784 && next_time > self.last_ns
1785 {
1786 min_next_time = Some(match min_next_time {
1787 Some(current_min) => next_time.min(current_min),
1788 None => next_time,
1789 });
1790 }
1791 }
1792 }
1793
1794 match min_next_time {
1795 None => Ok(true),
1796 Some(t) if t > self.end_ns => Ok(true),
1797 Some(t) => {
1798 self.last_ns = t;
1799 self.flush_accumulator_events(clocks, t)?;
1800 Ok(false)
1801 }
1802 }
1803 }
1804
1805 fn run_timer_handlers_at(
1806 &mut self,
1807 clocks: &[Rc<RefCell<dyn Clock>>],
1808 ts_event: UnixNanos,
1809 advance_to: UnixNanos,
1810 ) -> anyhow::Result<SettlementScope> {
1811 self.last_ns = ts_event;
1812 let mut settlement_scope = SettlementScope::Data(None);
1813
1814 while self.accumulator.peek_next_time() == Some(ts_event) {
1815 let handler = self
1816 .accumulator
1817 .pop_next_at_or_before(ts_event)
1818 .expect("timer exists at timestamp");
1819
1820 if !is_snapshot_timer(handler.event.name.as_str()) {
1821 settlement_scope = SettlementScope::All;
1822 }
1823
1824 Self::set_all_clocks_time(clocks, ts_event);
1825 logging_clock_set_static_time(ts_event.as_u64());
1826 handler.run();
1827 self.drain_command_queues()?;
1828
1829 if self.kernel.is_shutdown_requested() {
1830 return Ok(settlement_scope);
1831 }
1832
1833 for clock in clocks {
1834 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, advance_to, false);
1835 }
1836 }
1837
1838 Ok(settlement_scope)
1839 }
1840
1841 fn finalize_timestamp(
1842 &mut self,
1843 clocks: &[Rc<RefCell<dyn Clock>>],
1844 ts_now: UnixNanos,
1845 mut settlement_scope: SettlementScope,
1846 ) -> anyhow::Result<()> {
1847 loop {
1848 self.settle_venues(ts_now, settlement_scope)?;
1849
1850 if self.kernel.is_shutdown_requested() {
1851 self.accumulator.clear();
1852 break;
1853 }
1854
1855 for clock in clocks {
1856 Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1857 }
1858
1859 if self.accumulator.peek_next_time() == Some(ts_now) {
1860 if matches!(
1861 self.run_timer_handlers_at(clocks, ts_now, ts_now)?,
1862 SettlementScope::All
1863 ) {
1864 settlement_scope = SettlementScope::All;
1865 }
1866
1867 continue;
1868 }
1869
1870 if !self.settle_funding_rates(ts_now)? {
1871 break;
1872 }
1873 settlement_scope = SettlementScope::All;
1874 }
1875
1876 self.run_venue_modules(ts_now, settlement_scope)?;
1877 self.run_venue_liquidations(ts_now, settlement_scope)?;
1878 Ok(())
1879 }
1880
1881 fn settle_funding_rates(&mut self, ts_now: UnixNanos) -> anyhow::Result<bool> {
1882 let mut due = self
1883 .venues
1884 .iter()
1885 .flat_map(|(venue, exchange)| {
1886 exchange
1887 .borrow()
1888 .funding_boundaries_due(ts_now)
1889 .into_iter()
1890 .map(|(boundary, instrument_id)| (boundary, *venue, instrument_id))
1891 .collect::<Vec<_>>()
1892 })
1893 .collect::<Vec<_>>();
1894 due.sort_unstable();
1895
1896 if let Some((boundary, venue, instrument_id)) = due
1897 .iter()
1898 .copied()
1899 .find(|(boundary, _, _)| *boundary < ts_now)
1900 {
1901 return self.fail_funding(format!(
1902 "Late funding boundary for {instrument_id} on {venue}: {boundary} < replay timestamp {ts_now}"
1903 ));
1904 }
1905
1906 if due.is_empty() {
1907 return Ok(false);
1908 }
1909
1910 for (boundary, venue, instrument_id) in due {
1911 if !self.venues[&venue]
1912 .borrow_mut()
1913 .settle_funding_boundary(boundary, instrument_id)
1914 {
1915 return self.fail_funding(format!(
1916 "Funding settlement failed for {instrument_id} on {venue} at {boundary}"
1917 ));
1918 }
1919 }
1920
1921 let next_boundaries = self
1922 .venues
1923 .iter()
1924 .filter_map(|(venue, exchange)| {
1925 exchange
1926 .borrow()
1927 .next_funding_boundary()
1928 .map(|boundary| (*venue, boundary))
1929 })
1930 .collect::<Vec<_>>();
1931
1932 for (venue, boundary) in next_boundaries {
1933 Self::schedule_funding_settlement_if_required(
1934 &self.kernel.clock,
1935 venue,
1936 Some(boundary),
1937 );
1938 }
1939
1940 Ok(true)
1941 }
1942
1943 fn fail_funding<T>(&mut self, error: String) -> anyhow::Result<T> {
1944 if self.funding_error.is_none() {
1945 self.funding_error = Some(error.clone());
1946 }
1947 Err(anyhow::anyhow!(error))
1948 }
1949
1950 fn set_instrument_expiration_timers(&self) -> anyhow::Result<()> {
1951 for exchange in self.venues.values() {
1952 let expirations = exchange.borrow().instrument_expirations();
1953 for (instrument_id, expiration_ns) in expirations {
1954 self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
1955 }
1956 }
1957
1958 Ok(())
1959 }
1960
1961 fn set_instrument_expiration_timer(
1962 &self,
1963 exchange: &Rc<RefCell<SimulatedExchange>>,
1964 instrument_id: InstrumentId,
1965 expiration_ns: UnixNanos,
1966 ) -> anyhow::Result<()> {
1967 if expiration_ns == UnixNanos::default() {
1968 return Ok(());
1969 }
1970
1971 let timer_name = Self::instrument_expiration_timer_name(instrument_id.venue, expiration_ns);
1972 let timer_key = ustr::Ustr::from(timer_name.as_str());
1973 if self.kernel.clock.borrow().timer_exists(&timer_key) {
1974 return Ok(());
1975 }
1976
1977 let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1978 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1979 if let Some(exchange) = exchange.upgrade() {
1980 exchange
1981 .borrow_mut()
1982 .process_instrument_expirations(event.ts_event);
1983 }
1984 });
1985 let mut clock = self.kernel.clock.borrow_mut();
1986
1987 clock.set_time_alert_ns(
1988 &timer_name,
1989 expiration_ns,
1990 Some(TimeEventCallback::from(callback)),
1991 None,
1992 )?;
1993
1994 Ok(())
1995 }
1996
1997 fn instrument_expiration_timer_name(venue: Venue, expiration_ns: UnixNanos) -> String {
1998 format!("INSTRUMENT-EXPIRATION:{venue}:{expiration_ns}")
1999 }
2000
2001 fn schedule_funding_settlement_if_required(
2002 clock: &Rc<RefCell<dyn Clock>>,
2003 venue: Venue,
2004 settlement_ns: Option<UnixNanos>,
2005 ) {
2006 let Some(settlement_ns) = settlement_ns else {
2007 return;
2008 };
2009
2010 if let Err(e) = Self::set_funding_settlement_timer(clock, venue, settlement_ns) {
2011 log::error!("Cannot schedule funding settlement for {venue}: {e}");
2012 }
2013 }
2014
2015 fn set_funding_settlement_timer(
2016 clock: &Rc<RefCell<dyn Clock>>,
2017 venue: Venue,
2018 settlement_ns: UnixNanos,
2019 ) -> anyhow::Result<()> {
2020 let timer_name = Self::funding_settlement_timer_name(venue);
2021 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(|_| {});
2022 let mut clock = clock.borrow_mut();
2023
2024 clock.set_time_alert_ns(
2025 &timer_name,
2026 settlement_ns,
2027 Some(TimeEventCallback::from(callback)),
2028 None,
2029 )?;
2030
2031 Ok(())
2032 }
2033
2034 fn funding_settlement_timer_name(venue: Venue) -> String {
2035 format!("FUNDING-SETTLEMENT:{venue}")
2036 }
2037
2038 fn cancel_funding_settlement_timers(&self) {
2039 let mut clock = self.kernel.clock.borrow_mut();
2040 for venue in self.venues.keys() {
2041 clock.cancel_timer(&Self::funding_settlement_timer_name(*venue));
2042 }
2043 }
2044
2045 fn collect_all_clocks(&self) -> Vec<Rc<RefCell<dyn Clock>>> {
2046 let mut clocks = vec![self.kernel.clock.clone()];
2047 clocks.extend(self.kernel.trader.borrow().get_component_clocks());
2048 clocks
2049 }
2050
2051 fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
2052 self.venues
2053 .values()
2054 .filter_map(|v| v.borrow().max_inflight_command_ts())
2055 .max()
2056 }
2057
2058 fn settle_venues(
2059 &self,
2060 ts_now: UnixNanos,
2061 settlement_scope: SettlementScope,
2062 ) -> anyhow::Result<()> {
2063 for exchange in self.venues.values() {
2066 exchange.borrow().set_clock_time(ts_now);
2067 }
2068
2069 loop {
2075 self.drain_command_queues()?;
2078
2079 let active_venues: Vec<Venue> = self
2080 .venues
2081 .iter()
2082 .filter(|(_, ex)| {
2083 ex.borrow()
2084 .has_pending_commands_for_scope(ts_now, settlement_scope)
2085 })
2086 .map(|(id, _)| *id)
2087 .collect();
2088
2089 if active_venues.is_empty() {
2090 break;
2091 }
2092
2093 for venue_id in &active_venues {
2094 let mut exchange = self.venues[venue_id].borrow_mut();
2095 exchange.process_for_scope(ts_now, settlement_scope);
2096 }
2097 self.drain_command_queues()?;
2098
2099 for venue_id in &active_venues {
2100 self.venues[venue_id]
2101 .borrow_mut()
2102 .iterate_matching_engines(ts_now);
2103 }
2104
2105 self.drain_command_queues()?;
2108 }
2109 Ok(())
2110 }
2111
2112 fn run_venue_modules(
2113 &mut self,
2114 ts_now: UnixNanos,
2115 settlement_scope: SettlementScope,
2116 ) -> anyhow::Result<()> {
2117 if self.last_module_ns == Some(ts_now) {
2118 return Ok(());
2119 }
2120 self.last_module_ns = Some(ts_now);
2121
2122 if self
2123 .venues
2124 .values()
2125 .all(|exchange| !exchange.borrow().has_modules())
2126 {
2127 return Ok(());
2128 }
2129
2130 self.drain_command_queues()?;
2132 self.settle_venues(ts_now, settlement_scope)?;
2133
2134 for exchange in self.venues.values() {
2135 exchange.borrow_mut().process_modules(ts_now)?;
2136 }
2137
2138 self.drain_command_queues()?;
2140 self.settle_venues(ts_now, settlement_scope)?;
2141 Ok(())
2142 }
2143
2144 fn run_venue_liquidations(
2145 &mut self,
2146 ts_now: UnixNanos,
2147 settlement_scope: SettlementScope,
2148 ) -> anyhow::Result<()> {
2149 if self.last_liquidation_ns == Some(ts_now) {
2150 return Ok(());
2151 }
2152 self.last_liquidation_ns = Some(ts_now);
2153
2154 if self
2155 .venues
2156 .values()
2157 .all(|exchange| !exchange.borrow().liquidation_enabled())
2158 {
2159 return Ok(());
2160 }
2161
2162 for exchange in self.venues.values() {
2163 exchange.borrow_mut().process_liquidations(ts_now);
2164 }
2165
2166 self.drain_command_queues()?;
2167 self.settle_venues(ts_now, settlement_scope)?;
2168 Ok(())
2169 }
2170
2171 fn drain_exec_client_events(&self) {
2172 for client in &self.exec_clients {
2173 client.drain_queued_events();
2174 }
2175 }
2176
2177 fn drain_command_queues(&self) -> anyhow::Result<()> {
2178 if let Some(error) = actor::callback_failure() {
2179 return Err(error.into());
2180 }
2181
2182 loop {
2186 drain_trading_cmd_queue();
2187 drain_data_cmd_queue();
2188 self.drain_exec_client_events();
2189 self.check_balance_error()?;
2190
2191 let callbacks_pending = actor::drain_callbacks(CALLBACK_DRAIN_BUDGET)?;
2192
2193 if trading_cmd_queue_is_empty() && data_cmd_queue_is_empty() && !callbacks_pending {
2194 break;
2195 }
2196 }
2197 Ok(())
2198 }
2199
2200 fn init_command_senders() {
2201 replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
2202 replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
2203 }
2204
2205 fn advance_clock_on_accumulator(
2206 accumulator: &mut TimeEventAccumulator,
2207 clock: &Rc<RefCell<dyn Clock>>,
2208 to_time_ns: UnixNanos,
2209 set_time: bool,
2210 ) {
2211 let mut clock_ref = clock.borrow_mut();
2212 let test_clock = clock_ref
2213 .as_any_mut()
2214 .downcast_mut::<VirtualClock>()
2215 .expect("BacktestEngine requires VirtualClock");
2216 accumulator.advance_clock(test_clock, to_time_ns, set_time);
2217 }
2218
2219 fn set_all_clocks_time(clocks: &[Rc<RefCell<dyn Clock>>], time_ns: UnixNanos) {
2220 for clock in clocks {
2221 let mut clock_ref = clock.borrow_mut();
2222 let test_clock = clock_ref
2223 .as_any_mut()
2224 .downcast_mut::<VirtualClock>()
2225 .expect("BacktestEngine requires VirtualClock");
2226 test_clock.set_time(time_ns);
2227 }
2228 }
2229
2230 #[rustfmt::skip]
2231 fn log_pre_run(&self) {
2232 log_info!("=================================================================", color = LogColor::Cyan);
2233 log_info!(" BACKTEST PRE-RUN", color = LogColor::Cyan);
2234 log_info!("=================================================================", color = LogColor::Cyan);
2235
2236 let cache = self.kernel.cache.borrow();
2237 for exchange in self.venues.values() {
2238 let ex = exchange.borrow();
2239 log_info!("=================================================================", color = LogColor::Cyan);
2240 log::info!(" SimulatedVenue {} ({})", ex.id, ex.account_type);
2241 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2242
2243 if let Some(account) = cache.account_for_venue(&ex.id) {
2244 log::info!("Balances starting:");
2245 let account_ref: &dyn Account = match &*account {
2246 AccountAny::Margin(margin) => margin,
2247 AccountAny::Cash(cash) => cash,
2248 AccountAny::Betting(betting) => betting,
2249 AccountAny::Wallet(wallet) => wallet,
2250 };
2251
2252 for balance in account_ref.starting_balances().values() {
2253 log::info!(" {balance}");
2254 }
2255 }
2256 }
2257
2258 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2259 }
2260
2261 #[rustfmt::skip]
2262 fn log_run(&self) {
2263 let config_id = self.run_config_id.as_deref().unwrap_or("None");
2264 let id = format_optional_uuid(self.run_id.as_ref());
2265 let start = format_optional_nanos(self.backtest_start);
2266
2267 log_info!("=================================================================", color = LogColor::Cyan);
2268 log_info!(" BACKTEST RUN", color = LogColor::Cyan);
2269 log_info!("=================================================================", color = LogColor::Cyan);
2270 log::info!("Run config ID: {config_id}");
2271 log::info!("Run ID: {id}");
2272 log::info!("Backtest start: {start}");
2273 log::info!("Data elements: {}", self.data_len);
2274 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2275 }
2276
2277 #[rustfmt::skip]
2278 fn log_post_run(&self) {
2279 let cache = self.kernel.cache.borrow();
2280 let orders = cache.orders(None, None, None, None, None);
2281 let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
2282 let total_orders = orders.len();
2283 let positions: Vec<Position> = cache
2284 .positions(None, None, None, None, None)
2285 .into_iter()
2286 .map(|p| p.cloned())
2287 .collect();
2288 let total_positions = Self::total_positions_with_snapshots(&cache, positions.len());
2289
2290 let config_id = self.run_config_id.as_deref().unwrap_or("None");
2291 let id = format_optional_uuid(self.run_id.as_ref());
2292 let started = format_optional_nanos(self.run_started);
2293 let finished = format_optional_nanos(self.run_finished);
2294 let elapsed = format_optional_duration(self.run_started, self.run_finished);
2295 let bt_start = format_optional_nanos(self.backtest_start);
2296 let bt_end = format_optional_nanos(self.backtest_end);
2297 let bt_range = format_optional_duration(self.backtest_start, self.backtest_end);
2298 let iterations = self.iteration.separate_with_underscores();
2299 let events = total_events.separate_with_underscores();
2300 let num_orders = total_orders.separate_with_underscores();
2301 let num_positions = total_positions.separate_with_underscores();
2302
2303 log_info!("=================================================================", color = LogColor::Cyan);
2304 log_info!(" BACKTEST POST-RUN", color = LogColor::Cyan);
2305 log_info!("=================================================================", color = LogColor::Cyan);
2306 log::info!("Run config ID: {config_id}");
2307 log::info!("Run ID: {id}");
2308 log::info!("Run started: {started}");
2309 log::info!("Run finished: {finished}");
2310 log::info!("Elapsed time: {elapsed}");
2311 log::info!("Backtest start: {bt_start}");
2312 log::info!("Backtest end: {bt_end}");
2313 log::info!("Backtest range: {bt_range}");
2314 log::info!("Iterations: {iterations}");
2315 log::info!("Total events: {events}");
2316 log::info!("Total orders: {num_orders}");
2317 log::info!("Total positions: {num_positions}");
2318
2319 if !self.config.run_analysis {
2320 return;
2321 }
2322
2323 log_portfolio_performance(&self.kernel.portfolio.borrow().analyzer());
2324 }
2325
2326 fn total_positions_with_snapshots(cache: &Cache, cached_positions_count: usize) -> usize {
2327 cached_positions_count + cache.position_snapshots(None, None).len()
2328 }
2329
2330 pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
2332 if self
2333 .kernel
2334 .data_engine
2335 .borrow()
2336 .registered_clients()
2337 .contains(&client_id)
2338 {
2339 return;
2340 }
2341
2342 let venue = Venue::from(client_id.as_str());
2343 let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2344 let data_client_adapter = DataClientAdapter::new(
2345 backtest_client.client_id,
2346 None,
2347 false,
2348 false,
2349 Box::new(backtest_client),
2350 );
2351
2352 self.kernel
2353 .data_engine
2354 .borrow_mut()
2355 .register_client(data_client_adapter, None);
2356 }
2357
2358 pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
2360 let client_id = ClientId::from(venue.as_str());
2361
2362 if !self
2363 .kernel
2364 .data_engine
2365 .borrow()
2366 .registered_clients()
2367 .contains(&client_id)
2368 {
2369 let backtest_client =
2370 BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2371 let data_client_adapter = DataClientAdapter::new(
2372 client_id,
2373 Some(venue),
2374 false,
2375 false,
2376 Box::new(backtest_client),
2377 );
2378 self.kernel
2379 .data_engine
2380 .borrow_mut()
2381 .register_client(data_client_adapter, Some(venue));
2382 }
2383 }
2384}
2385
2386fn format_optional_nanos(nanos: Option<UnixNanos>) -> String {
2387 nanos.map_or("None".to_string(), unix_nanos_to_iso8601)
2388}
2389
2390fn format_optional_uuid(uuid: Option<&UUID4>) -> String {
2391 uuid.map_or("None".to_string(), ToString::to_string)
2392}
2393
2394fn event_count_as_usize(event_count: u64) -> usize {
2395 usize::try_from(event_count).expect("execution event count fits usize")
2396}
2397
2398fn format_optional_duration(start: Option<UnixNanos>, end: Option<UnixNanos>) -> String {
2399 match (start, end) {
2400 (Some(s), Some(e)) => {
2401 let delta = s.to_datetime_utc().duration_until(e.to_datetime_utc());
2402 let days = delta.as_hours().abs() / 24;
2403 let hours = delta.as_hours().abs() % 24;
2404 let minutes = delta.as_mins().abs() % 60;
2405 let seconds = delta.as_secs().abs() % 60;
2406 let micros = delta.subsec_nanos().unsigned_abs() / 1_000;
2407 format!("{days} days {hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
2408 }
2409 _ => "None".to_string(),
2410 }
2411}
2412
2413#[rustfmt::skip]
2414fn log_portfolio_performance(analyzer: &PortfolioAnalyzer) {
2415 log_info!("=================================================================", color = LogColor::Cyan);
2416 log_info!(" PORTFOLIO PERFORMANCE", color = LogColor::Cyan);
2417 log_info!("=================================================================", color = LogColor::Cyan);
2418
2419 for currency in analyzer.currencies() {
2420 log::info!(" PnL Statistics ({})", currency.code);
2421 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2422
2423 if let Ok(pnl_lines) = analyzer.get_stats_pnls_formatted(Some(currency), None) {
2424 for line in &pnl_lines {
2425 log::info!("{line}");
2426 }
2427 }
2428
2429 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2430 }
2431
2432 log::info!(" Returns Statistics");
2433 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2434
2435 for line in &analyzer.get_stats_returns_formatted() {
2436 log::info!("{line}");
2437 }
2438 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2439
2440 log::info!(" General Statistics");
2441 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2442
2443 for line in &analyzer.get_stats_general_formatted() {
2444 log::info!("{line}");
2445 }
2446 log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2447}
2448
2449fn is_defi(data: DataRef<'_>) -> bool {
2450 match data {
2451 DataRef::Instrument(_)
2452 | DataRef::BookDelta(_)
2453 | DataRef::BookDeltas(_)
2454 | DataRef::BookDepth(_)
2455 | DataRef::Quote(_)
2456 | DataRef::Trade(_)
2457 | DataRef::Bar(_)
2458 | DataRef::MarkPrice(_)
2459 | DataRef::IndexPrice(_)
2460 | DataRef::FundingRate(_)
2461 | DataRef::OptionGreeks(_)
2462 | DataRef::InstrumentStatus(_)
2463 | DataRef::InstrumentClose(_)
2464 | DataRef::Custom(_) => false,
2465 #[cfg(feature = "defi")]
2466 DataRef::Defi(_) => true,
2467 #[cfg(not(feature = "defi"))]
2468 #[allow(
2469 unreachable_patterns,
2470 reason = "DeFi variants can exist without this crate's defi feature"
2471 )]
2472 _ => true,
2473 }
2474}
2475
2476#[cfg(test)]
2477mod tests {
2478 use std::{cell::Cell, rc::Rc};
2479
2480 use indexmap::IndexMap;
2481 use nautilus_common::{
2482 actor::DataActor,
2483 enums::Environment,
2484 messages::{
2485 data::{DataCommand, UnsubscribeCommand},
2486 execution::{BatchModifyOrders, ModifyOrder, SubmitOrder, TradingCommand},
2487 },
2488 msgbus::{
2489 self, MessagingSwitchboard, TypedHandler,
2490 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
2491 },
2492 };
2493 use nautilus_execution::{
2494 engine::{SnapshotAnchorer, stubs::StubExecutionClient},
2495 models::fee::{FeeModelAny, MakerTakerFeeModel},
2496 };
2497 use nautilus_model::{
2498 data::{Data, InstrumentStatus, QuoteTick},
2499 enums::{
2500 AccountType, BookType, ContingencyType, LiquiditySide, MarketStatus,
2501 MarketStatusAction, OmsType, OrderSide, OrderStatus, OrderType, PositionSide,
2502 TriggerType,
2503 },
2504 events::OrderEventAny,
2505 identifiers::{AccountId, ActorId, ClientId, ClientOrderId, PositionId, StrategyId, Venue},
2506 instruments::{
2507 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
2508 },
2509 orders::{
2510 Order, OrderAny, OrderTestBuilder,
2511 stubs::{OrderFilledTestBuilder, TestOrderEventStubs},
2512 },
2513 types::{Money, Price, Quantity},
2514 };
2515 use nautilus_system::{KernelEventStore, RegisteredComponents};
2516 use nautilus_testkit::{
2517 cache::TestCacheDatabaseControl,
2518 components::{StateActor, StateStrategy},
2519 };
2520 use nautilus_trading::{
2521 nautilus_strategy,
2522 strategy::{config::StrategyConfig, core::StrategyCore},
2523 };
2524 use rstest::*;
2525 use ustr::Ustr;
2526
2527 use super::*;
2528 use crate::modules::{
2529 AccountAdjustmentOutcome, ExchangeContext, SimulationModule, SimulationModuleHandle,
2530 SimulationModuleResult,
2531 };
2532
2533 #[derive(Debug)]
2534 struct BacktestReplayKernelEventStore {
2535 fail_restore: bool,
2536 }
2537
2538 impl KernelEventStore for BacktestReplayKernelEventStore {
2539 fn restore_parent_cache(
2540 &mut self,
2541 _instance_id: UUID4,
2542 _cache: &mut Cache,
2543 ) -> anyhow::Result<()> {
2544 if self.fail_restore {
2545 anyhow::bail!("replay restore failed");
2546 }
2547
2548 Ok(())
2549 }
2550
2551 fn open(
2552 &mut self,
2553 _instance_id: UUID4,
2554 _components: &RegisteredComponents,
2555 _environment: Environment,
2556 ) -> anyhow::Result<()> {
2557 Ok(())
2558 }
2559
2560 fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
2561 None
2562 }
2563
2564 fn seal(&mut self, _ts_init: UnixNanos) {}
2565
2566 fn run_id(&self) -> Option<&str> {
2567 Some("replay-child")
2568 }
2569
2570 fn parent_run_id(&self) -> Option<&str> {
2571 Some("seed-run")
2572 }
2573
2574 fn is_event_store_replay_configured(&self) -> bool {
2575 true
2576 }
2577
2578 fn is_halted(&self) -> bool {
2579 false
2580 }
2581 }
2582
2583 #[derive(Debug)]
2584 struct TestStrategy {
2585 core: StrategyCore,
2586 }
2587
2588 impl TestStrategy {
2589 fn new(config: StrategyConfig) -> Self {
2590 Self {
2591 core: StrategyCore::new(config),
2592 }
2593 }
2594 }
2595
2596 impl DataActor for TestStrategy {}
2597
2598 nautilus_strategy!(TestStrategy);
2599
2600 #[derive(Clone, Copy, Debug)]
2602 enum DeferredAcceptance {
2603 Initialized,
2605 Released,
2607 OcoSibling,
2609 BracketChildren,
2611 }
2612
2613 #[derive(Debug)]
2615 struct DeferredAcceptanceCancelStrategy {
2616 core: StrategyCore,
2617 instrument_id: InstrumentId,
2618 scenario: DeferredAcceptance,
2619 cancel_all: bool,
2620 quote_count: usize,
2621 target: Option<ClientOrderId>,
2622 }
2623
2624 impl DeferredAcceptanceCancelStrategy {
2625 const STRATEGY_ID: &str = "DEFERRED-CANCEL-001";
2626
2627 fn new(
2628 instrument_id: InstrumentId,
2629 scenario: DeferredAcceptance,
2630 cancel_all: bool,
2631 ) -> Self {
2632 Self {
2633 core: StrategyCore::new(StrategyConfig {
2634 strategy_id: Some(StrategyId::from(Self::STRATEGY_ID)),
2635 ..Default::default()
2636 }),
2637 instrument_id,
2638 scenario,
2639 cancel_all,
2640 quote_count: 0,
2641 target: None,
2642 }
2643 }
2644
2645 fn oco_leg(
2646 &self,
2647 client_order_id: &str,
2648 sibling_id: &str,
2649 side: OrderSide,
2650 price: &str,
2651 ) -> OrderAny {
2652 OrderTestBuilder::new(OrderType::Limit)
2653 .trader_id(self.trader_id().unwrap())
2654 .strategy_id(StrategyId::from(Self::STRATEGY_ID))
2655 .instrument_id(self.instrument_id)
2656 .client_order_id(ClientOrderId::from(client_order_id))
2657 .side(side)
2658 .quantity(Quantity::from("1.000"))
2659 .price(Price::from(price))
2660 .contingency_type(ContingencyType::Oco)
2661 .linked_order_ids(vec![ClientOrderId::from(sibling_id)])
2662 .build()
2663 }
2664
2665 fn submit(&mut self) -> anyhow::Result<()> {
2666 let instrument_id = self.instrument_id;
2667 let quantity = Quantity::from("1.000");
2668 let price = Price::from("900.00");
2669
2670 match self.scenario {
2671 DeferredAcceptance::Initialized => {
2672 let order = self.order().limit(
2673 instrument_id,
2674 OrderSide::Buy,
2675 quantity,
2676 price,
2677 None,
2678 None,
2679 None,
2680 None,
2681 None,
2682 None,
2683 None,
2684 None,
2685 None,
2686 None,
2687 None,
2688 None,
2689 );
2690 self.target = Some(order.client_order_id());
2691 self.submit_order(order, None, None, None)
2692 }
2693 DeferredAcceptance::Released => {
2694 let order = self.order().stop_limit(
2695 instrument_id,
2696 OrderSide::Buy,
2697 quantity,
2698 price,
2699 Price::from("1002.00"),
2700 None,
2701 None,
2702 None,
2703 None,
2704 None,
2705 None,
2706 None,
2707 Some(TriggerType::Default),
2708 None,
2709 None,
2710 None,
2711 None,
2712 None,
2713 );
2714 self.target = Some(order.client_order_id());
2715 self.submit_order(order, None, None, None)
2716 }
2717 DeferredAcceptance::OcoSibling => {
2718 let leg = self.oco_leg("O-OCO-A", "O-OCO-B", OrderSide::Buy, "900.00");
2719 let sibling = self.oco_leg("O-OCO-B", "O-OCO-A", OrderSide::Sell, "1100.00");
2720 self.target = Some(leg.client_order_id());
2721 self.submit_order_list(vec![leg, sibling], None, None, None)
2722 }
2723 DeferredAcceptance::BracketChildren => {
2724 let orders = self
2725 .order()
2726 .bracket()
2727 .instrument_id(instrument_id)
2728 .order_side(OrderSide::Buy)
2729 .quantity(quantity)
2730 .entry_order_type(OrderType::Limit)
2731 .entry_price(price)
2732 .tp_price(Price::from("1100.00"))
2733 .sl_trigger_price(Price::from("800.00"))
2734 .call();
2735 self.target = Some(orders[0].client_order_id());
2736 self.submit_order_list(orders, None, None, None)
2737 }
2738 }
2739 }
2740 }
2741
2742 impl DataActor for DeferredAcceptanceCancelStrategy {
2743 fn on_start(&mut self) -> anyhow::Result<()> {
2744 self.subscribe_quotes(self.instrument_id, None, None);
2745 Ok(())
2746 }
2747
2748 fn on_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
2749 self.quote_count += 1;
2750
2751 if self.quote_count == 1 {
2752 self.submit()?;
2753 }
2754
2755 let cancel_quote = match self.scenario {
2757 DeferredAcceptance::Released => 2,
2758 DeferredAcceptance::Initialized
2759 | DeferredAcceptance::OcoSibling
2760 | DeferredAcceptance::BracketChildren => 1,
2761 };
2762
2763 if self.quote_count != cancel_quote {
2764 return Ok(());
2765 }
2766
2767 if self.cancel_all {
2768 self.cancel_all_orders(self.instrument_id, None, None, true, None)
2769 } else {
2770 self.cancel_order(self.target.unwrap(), None, None)
2771 }
2772 }
2773 }
2774
2775 nautilus_strategy!(DeferredAcceptanceCancelStrategy);
2776
2777 struct TestSimulationModule {
2778 process_count: Rc<Cell<u32>>,
2779 }
2780
2781 impl SimulationModule for TestSimulationModule {
2782 fn pre_process(&self, _data: &Data) -> anyhow::Result<()> {
2783 Ok(())
2784 }
2785
2786 fn process(
2787 &self,
2788 _ts_now: UnixNanos,
2789 _ctx: &ExchangeContext,
2790 ) -> anyhow::Result<SimulationModuleResult> {
2791 self.process_count.set(self.process_count.get() + 1);
2792 Ok(SimulationModuleResult::NotReady)
2793 }
2794
2795 fn acknowledge(&self, _outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()> {
2796 Ok(())
2797 }
2798
2799 fn log_diagnostics(&self) -> anyhow::Result<()> {
2800 Ok(())
2801 }
2802
2803 fn reset(&self) -> anyhow::Result<()> {
2804 Ok(())
2805 }
2806 }
2807
2808 fn create_engine() -> BacktestEngine {
2809 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2810 let venue_config = SimulatedVenueConfig::builder()
2811 .venue(Venue::from("BINANCE"))
2812 .oms_type(OmsType::Netting)
2813 .account_type(AccountType::Margin)
2814 .book_type(BookType::L1_MBP)
2815 .starting_balances(vec![Money::from("1_000_000 USDT")])
2816 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
2817 .build()
2818 .unwrap();
2819 engine.add_venue(venue_config).unwrap();
2820 engine
2821 }
2822
2823 fn create_immediate_engine(instrument: &CryptoPerpetual) -> BacktestEngine {
2824 create_engine_with_message_queue(instrument, false)
2825 }
2826
2827 fn create_engine_with_message_queue(
2828 instrument: &CryptoPerpetual,
2829 use_message_queue: bool,
2830 ) -> BacktestEngine {
2831 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2832 let venue_config = SimulatedVenueConfig::builder()
2833 .venue(instrument.id().venue)
2834 .oms_type(OmsType::Netting)
2835 .account_type(AccountType::Margin)
2836 .book_type(BookType::L1_MBP)
2837 .starting_balances(vec![Money::from("1_000_000 USDT")])
2838 .use_message_queue(use_message_queue)
2839 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
2840 .build()
2841 .unwrap();
2842 engine.add_venue(venue_config).unwrap();
2843 engine
2844 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
2845 .unwrap();
2846 engine
2847 .venues
2848 .get(&instrument.id().venue)
2849 .unwrap()
2850 .borrow_mut()
2851 .initialize_account();
2852 engine
2853 }
2854
2855 fn create_engine_with_strategy(manage_stop: bool) -> (BacktestEngine, StrategyId) {
2856 let mut engine = create_engine();
2857 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
2858 let strategy_id = StrategyId::from(if manage_stop {
2859 "MANAGED-STOP-001"
2860 } else {
2861 "IMMEDIATE-STOP-001"
2862 });
2863 engine.add_instrument(&instrument).unwrap();
2864 engine
2865 .add_strategy(TestStrategy::new(StrategyConfig {
2866 strategy_id: Some(strategy_id),
2867 manage_stop,
2868 ..Default::default()
2869 }))
2870 .unwrap();
2871
2872 if manage_stop {
2873 let order = OrderTestBuilder::new(OrderType::Market)
2874 .trader_id(engine.trader_id())
2875 .strategy_id(strategy_id)
2876 .instrument_id(instrument.id())
2877 .side(OrderSide::Buy)
2878 .quantity(Quantity::from("1.000"))
2879 .build();
2880 let fill = OrderFilledTestBuilder::new(&order, &instrument).build();
2881 let OrderEventAny::Filled(fill) = fill else {
2882 unreachable!();
2883 };
2884 let position = Position::new(&instrument, fill);
2885 engine
2886 .kernel
2887 .cache
2888 .borrow_mut()
2889 .add_position_without_order(&position, OmsType::Netting)
2890 .unwrap();
2891 }
2892
2893 (engine, strategy_id)
2894 }
2895
2896 fn send_execution_command(command: TradingCommand) {
2897 msgbus::send_trading_command(MessagingSwitchboard::exec_engine_execute(), command);
2898 }
2899
2900 #[rstest]
2901 #[case(false, false, 0)]
2902 #[case(false, true, 1)]
2903 #[case(true, true, 1)]
2904 fn test_run_venue_modules_settles_only_when_enabled(
2905 #[case] first_enabled: bool,
2906 #[case] second_enabled: bool,
2907 #[case] expected_ns: u64,
2908 ) {
2909 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2910 let process_count = Rc::new(Cell::new(0));
2911
2912 for (venue, enabled) in [
2913 (Venue::from("BINANCE"), first_enabled),
2914 (Venue::from("SIM"), second_enabled),
2915 ] {
2916 let modules = enabled
2917 .then(|| {
2918 SimulationModuleHandle::new(TestSimulationModule {
2919 process_count: Rc::clone(&process_count),
2920 })
2921 })
2922 .into_iter()
2923 .collect();
2924 let venue_config = SimulatedVenueConfig::builder()
2925 .venue(venue)
2926 .oms_type(OmsType::Netting)
2927 .account_type(AccountType::Margin)
2928 .book_type(BookType::L1_MBP)
2929 .starting_balances(vec![Money::from("1_000_000 USDT")])
2930 .modules(modules)
2931 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
2932 .build()
2933 .unwrap();
2934 engine.add_venue(venue_config).unwrap();
2935 }
2936
2937 engine
2938 .run_venue_modules(UnixNanos::from(1), SettlementScope::All)
2939 .unwrap();
2940
2941 assert_eq!(
2942 engine.kernel.clock.borrow().timestamp_ns(),
2943 UnixNanos::from(expected_ns)
2944 );
2945 assert_eq!(
2946 process_count.get(),
2947 u32::from(first_enabled) + u32::from(second_enabled)
2948 );
2949 }
2950
2951 #[rstest]
2952 #[case(false, false, 0)]
2953 #[case(false, true, 1)]
2954 #[case(true, true, 1)]
2955 fn test_run_venue_liquidations_settles_only_when_enabled(
2956 #[case] first_enabled: bool,
2957 #[case] second_enabled: bool,
2958 #[case] expected_ns: u64,
2959 ) {
2960 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2961
2962 for (venue, enabled) in [
2963 (Venue::from("BINANCE"), first_enabled),
2964 (Venue::from("SIM"), second_enabled),
2965 ] {
2966 let venue_config = SimulatedVenueConfig::builder()
2967 .venue(venue)
2968 .oms_type(OmsType::Netting)
2969 .account_type(AccountType::Margin)
2970 .book_type(BookType::L1_MBP)
2971 .starting_balances(vec![Money::from("1_000_000 USDT")])
2972 .liquidation_enabled(enabled)
2973 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
2974 .build()
2975 .unwrap();
2976 engine.add_venue(venue_config).unwrap();
2977 }
2978
2979 engine
2980 .run_venue_liquidations(UnixNanos::from(1), SettlementScope::All)
2981 .unwrap();
2982
2983 assert_eq!(
2984 engine.kernel.clock.borrow().timestamp_ns(),
2985 UnixNanos::from(expected_ns)
2986 );
2987 }
2988
2989 #[rstest]
2990 fn test_immediate_submit_defers_order_events(crypto_perpetual_ethusdt: CryptoPerpetual) {
2991 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2992 let order = OrderTestBuilder::new(OrderType::Limit)
2993 .trader_id(engine.trader_id())
2994 .instrument_id(crypto_perpetual_ethusdt.id)
2995 .client_order_id(ClientOrderId::from("O-IMMEDIATE-SUBMIT"))
2996 .side(OrderSide::Buy)
2997 .quantity(Quantity::from("1.000"))
2998 .price(Price::from("1000.00"))
2999 .build();
3000 engine
3001 .kernel
3002 .cache
3003 .borrow_mut()
3004 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
3005 .unwrap();
3006
3007 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
3008 order.trader_id(),
3009 Some(ClientId::from("BINANCE")),
3010 order.strategy_id(),
3011 order.instrument_id(),
3012 order.client_order_id(),
3013 order.init_event().clone(),
3014 order.exec_algorithm_id(),
3015 None,
3016 None,
3017 UUID4::new(),
3018 UnixNanos::default(),
3019 None,
3020 )));
3021
3022 {
3023 let cache = engine.kernel.cache.borrow();
3024 let cached_order = cache.order(&order.client_order_id()).unwrap();
3025 assert_eq!(cached_order.status(), OrderStatus::Initialized);
3026 assert_eq!(cached_order.event_count(), 1);
3027 }
3028
3029 engine.drain_command_queues().unwrap();
3030
3031 let cache = engine.kernel.cache.borrow();
3032 let cached_order = cache.order(&order.client_order_id()).unwrap();
3033 let events = cached_order.events();
3034 assert!(matches!(events[1], OrderEventAny::Submitted(_)));
3035 assert!(matches!(events[2], OrderEventAny::Accepted(_)));
3036 }
3037
3038 #[rstest]
3039 fn test_immediate_modify_submitted_order_defers_updated_event(
3040 crypto_perpetual_ethusdt: CryptoPerpetual,
3041 ) {
3042 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
3043 let order = OrderTestBuilder::new(OrderType::Limit)
3044 .trader_id(engine.trader_id())
3045 .instrument_id(crypto_perpetual_ethusdt.id)
3046 .client_order_id(ClientOrderId::from("O-IMMEDIATE-MODIFY"))
3047 .side(OrderSide::Buy)
3048 .quantity(Quantity::from("1.000"))
3049 .price(Price::from("1000.00"))
3050 .build();
3051 let account_id = AccountId::from("BINANCE-001");
3052 engine
3053 .kernel
3054 .cache
3055 .borrow_mut()
3056 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
3057 .unwrap();
3058 engine
3059 .kernel
3060 .cache
3061 .borrow_mut()
3062 .update_order(&TestOrderEventStubs::submitted(&order, account_id))
3063 .unwrap();
3064
3065 send_execution_command(TradingCommand::ModifyOrder(ModifyOrder::new(
3066 order.trader_id(),
3067 Some(ClientId::from("BINANCE")),
3068 order.strategy_id(),
3069 order.instrument_id(),
3070 order.client_order_id(),
3071 None,
3072 Some(Quantity::from("2.000")),
3073 None,
3074 None,
3075 UUID4::new(),
3076 UnixNanos::from(1),
3077 None,
3078 None,
3079 )));
3080
3081 {
3082 let cache = engine.kernel.cache.borrow();
3083 let cached_order = cache.order(&order.client_order_id()).unwrap();
3084 assert_eq!(cached_order.quantity(), Quantity::from("1.000"));
3085 assert!(matches!(
3086 cached_order.events().last(),
3087 Some(OrderEventAny::Submitted(_))
3088 ));
3089 }
3090
3091 engine.drain_command_queues().unwrap();
3092
3093 let cache = engine.kernel.cache.borrow();
3094 let order = cache.order(&order.client_order_id()).unwrap();
3095 assert_eq!(order.quantity(), Quantity::from("2.000"));
3096 assert!(matches!(
3097 order.events().last(),
3098 Some(OrderEventAny::Updated(_))
3099 ));
3100 }
3101
3102 #[rstest]
3103 fn test_immediate_modifies_preserve_pending_quantity_and_matching_price(
3104 crypto_perpetual_ethusdt: CryptoPerpetual,
3105 ) {
3106 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
3107 let order = OrderTestBuilder::new(OrderType::Limit)
3108 .trader_id(engine.trader_id())
3109 .instrument_id(crypto_perpetual_ethusdt.id)
3110 .client_order_id(ClientOrderId::from("O-IMMEDIATE-MODIFY-FILL"))
3111 .side(OrderSide::Buy)
3112 .quantity(Quantity::from("1.000"))
3113 .price(Price::from("1000.00"))
3114 .build();
3115 engine
3116 .kernel
3117 .cache
3118 .borrow_mut()
3119 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
3120 .unwrap();
3121 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
3122 order.trader_id(),
3123 Some(ClientId::from("BINANCE")),
3124 order.strategy_id(),
3125 order.instrument_id(),
3126 order.client_order_id(),
3127 order.init_event().clone(),
3128 order.exec_algorithm_id(),
3129 None,
3130 None,
3131 UUID4::new(),
3132 UnixNanos::default(),
3133 None,
3134 )));
3135 engine.drain_command_queues().unwrap();
3136
3137 for (quantity, price) in [
3138 (Some(Quantity::from("2.000")), None),
3139 (None, Some(Price::from("1005.00"))),
3140 ] {
3141 send_execution_command(TradingCommand::ModifyOrder(ModifyOrder::new(
3142 order.trader_id(),
3143 Some(ClientId::from("BINANCE")),
3144 order.strategy_id(),
3145 order.instrument_id(),
3146 order.client_order_id(),
3147 None,
3148 quantity,
3149 price,
3150 None,
3151 UUID4::new(),
3152 UnixNanos::from(1),
3153 None,
3154 None,
3155 )));
3156 }
3157 {
3158 let cache = engine.kernel.cache.borrow();
3159 let cached = cache.order(&order.client_order_id()).unwrap();
3160 assert_eq!(cached.quantity(), Quantity::from("1.000"));
3161 assert_eq!(cached.price(), Some(Price::from("1000.00")));
3162 assert_eq!(cached.event_count(), 3);
3163 }
3164 engine.drain_command_queues().unwrap();
3165 {
3166 let cache = engine.kernel.cache.borrow();
3167 let cached = cache.order(&order.client_order_id()).unwrap();
3168 assert_eq!(cached.quantity(), Quantity::from("2.000"));
3169 assert_eq!(cached.price(), Some(Price::from("1005.00")));
3170 assert_eq!(cached.event_count(), 5);
3171 }
3172
3173 let quote = QuoteTick::new(
3174 order.instrument_id(),
3175 Price::from("1003.00"),
3176 Price::from("1004.00"),
3177 Quantity::from("3.000"),
3178 Quantity::from("4.000"),
3179 UnixNanos::from(2),
3180 UnixNanos::from(2),
3181 );
3182 msgbus::send_quote(
3183 format!(
3184 "SimulatedExchange.process_new_quote.{}",
3185 order.instrument_id().venue
3186 )
3187 .into(),
3188 "e,
3189 );
3190 let cache = engine.kernel.cache.borrow();
3191 let cached = cache.order(&order.client_order_id()).unwrap();
3192 assert_eq!(cached.status(), OrderStatus::Filled);
3193 assert_eq!(cached.quantity(), Quantity::from("2.000"));
3194 assert_eq!(cached.filled_qty(), Quantity::from("2.000"));
3195 assert_eq!(cached.leaves_qty(), Quantity::from("0.000"));
3196 assert_eq!(cached.event_count(), 6);
3197 let OrderEventAny::Filled(fill) = cached.last_event() else {
3198 panic!("Expected final fill");
3199 };
3200 assert_eq!(fill.last_px, Price::from("1005.00"));
3201 assert_eq!(fill.last_qty, Quantity::from("2.000"));
3202 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
3203 }
3204
3205 #[rstest]
3206 #[case::immediate(false)]
3207 #[case::queued(true)]
3208 fn test_batch_reduce_only_modifies_share_position_quantity(
3209 crypto_perpetual_ethusdt: CryptoPerpetual,
3210 #[case] use_message_queue: bool,
3211 #[values(false, true)] first_reduce_only: bool,
3212 ) {
3213 let instrument = crypto_perpetual_ethusdt;
3214 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
3215 let venue_config = SimulatedVenueConfig::builder()
3216 .venue(instrument.id().venue)
3217 .oms_type(OmsType::Netting)
3218 .account_type(AccountType::Margin)
3219 .book_type(BookType::L1_MBP)
3220 .starting_balances(vec![Money::from("1_000_000 USDT")])
3221 .use_message_queue(use_message_queue)
3222 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
3223 .build()
3224 .unwrap();
3225 engine.add_venue(venue_config).unwrap();
3226 engine
3227 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
3228 .unwrap();
3229 let exchange = engine.venues.get(&instrument.id().venue).unwrap().clone();
3230 exchange.borrow_mut().initialize_account();
3231 msgbus::send_quote(
3232 format!(
3233 "SimulatedExchange.process_new_quote.{}",
3234 instrument.id().venue
3235 )
3236 .into(),
3237 &QuoteTick::new(
3238 instrument.id(),
3239 Price::from("1000.00"),
3240 Price::from("1001.00"),
3241 Quantity::from("1.000"),
3242 Quantity::from("1.000"),
3243 UnixNanos::default(),
3244 UnixNanos::default(),
3245 ),
3246 );
3247 let opening = OrderTestBuilder::new(OrderType::Market)
3248 .trader_id(engine.trader_id())
3249 .instrument_id(instrument.id())
3250 .client_order_id(ClientOrderId::from("O-OPEN-SHORT"))
3251 .side(OrderSide::Sell)
3252 .quantity(Quantity::from("0.500"))
3253 .build();
3254 let closing =
3255 [("O-CLOSE-FIRST", "0.400"), ("O-CLOSE-SECOND", "0.300")].map(|(id, quantity)| {
3256 OrderTestBuilder::new(OrderType::Limit)
3257 .trader_id(engine.trader_id())
3258 .instrument_id(instrument.id())
3259 .client_order_id(ClientOrderId::from(id))
3260 .side(OrderSide::Buy)
3261 .quantity(Quantity::from(quantity))
3262 .price(Price::from("999.00"))
3263 .reduce_only(id != "O-CLOSE-FIRST" || first_reduce_only)
3264 .build()
3265 });
3266
3267 for order in [&opening, &closing[0], &closing[1]] {
3268 engine
3269 .kernel
3270 .cache
3271 .borrow_mut()
3272 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
3273 .unwrap();
3274 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
3275 order.trader_id(),
3276 Some(ClientId::from("BINANCE")),
3277 order.strategy_id(),
3278 order.instrument_id(),
3279 order.client_order_id(),
3280 order.init_event().clone(),
3281 order.exec_algorithm_id(),
3282 None,
3283 None,
3284 UUID4::new(),
3285 UnixNanos::default(),
3286 None,
3287 )));
3288 engine.drain_command_queues().unwrap();
3289 exchange.borrow_mut().process(UnixNanos::default());
3290 engine.drain_command_queues().unwrap();
3291 }
3292 {
3293 let cache = engine.kernel.cache.borrow();
3294 let position = cache
3295 .position_for_order(&opening.client_order_id())
3296 .unwrap();
3297 assert!(position.is_short());
3298 assert_eq!(position.quantity, Quantity::from("0.500"));
3299
3300 for order in &closing {
3301 let cached = cache.order(&order.client_order_id()).unwrap();
3302 assert_eq!(cached.status(), OrderStatus::Accepted);
3303 assert_eq!(cached.quantity(), order.quantity());
3304 assert_eq!(cached.filled_qty(), Quantity::from("0.000"));
3305 }
3306 }
3307 let modifies = closing
3308 .iter()
3309 .map(|order| {
3310 ModifyOrder::new(
3311 order.trader_id(),
3312 Some(ClientId::from("BINANCE")),
3313 order.strategy_id(),
3314 order.instrument_id(),
3315 order.client_order_id(),
3316 None,
3317 None,
3318 Some(Price::from("1002.00")),
3319 None,
3320 UUID4::new(),
3321 UnixNanos::from(1),
3322 None,
3323 None,
3324 )
3325 })
3326 .collect();
3327 exchange.borrow_mut().process(UnixNanos::from(1));
3328 send_execution_command(TradingCommand::ModifyOrders(BatchModifyOrders::new(
3329 opening.trader_id(),
3330 Some(ClientId::from("BINANCE")),
3331 opening.strategy_id(),
3332 opening.instrument_id(),
3333 modifies,
3334 UUID4::new(),
3335 UnixNanos::from(1),
3336 None,
3337 None,
3338 )));
3339 engine.drain_command_queues().unwrap();
3340 exchange.borrow_mut().process(UnixNanos::from(1));
3341 engine.drain_command_queues().unwrap();
3342
3343 let cache = engine.kernel.cache.borrow();
3344 let filled = closing
3345 .each_ref()
3346 .map(|order| cache.order(&order.client_order_id()).unwrap().filled_qty());
3347 let position = cache
3348 .position_for_order(&opening.client_order_id())
3349 .unwrap();
3350 assert_eq!(
3351 (filled, position.side, position.quantity),
3352 (
3353 [Quantity::from("0.400"), Quantity::from("0.100")],
3354 PositionSide::Flat,
3355 Quantity::from("0.000"),
3356 ),
3357 );
3358 }
3359
3360 #[rstest]
3361 fn test_immediate_market_data_dispatches_fill_synchronously(
3362 crypto_perpetual_ethusdt: CryptoPerpetual,
3363 ) {
3364 let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
3365 let order = OrderTestBuilder::new(OrderType::Limit)
3366 .trader_id(engine.trader_id())
3367 .instrument_id(crypto_perpetual_ethusdt.id)
3368 .client_order_id(ClientOrderId::from("O-IMMEDIATE-QUOTE-FILL"))
3369 .side(OrderSide::Buy)
3370 .quantity(Quantity::from("1.000"))
3371 .price(Price::from("1000.00"))
3372 .build();
3373 engine
3374 .kernel
3375 .cache
3376 .borrow_mut()
3377 .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
3378 .unwrap();
3379
3380 send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
3381 order.trader_id(),
3382 Some(ClientId::from("BINANCE")),
3383 order.strategy_id(),
3384 order.instrument_id(),
3385 order.client_order_id(),
3386 order.init_event().clone(),
3387 order.exec_algorithm_id(),
3388 None,
3389 None,
3390 UUID4::new(),
3391 UnixNanos::default(),
3392 None,
3393 )));
3394 engine.drain_command_queues().unwrap();
3395
3396 let quote = QuoteTick::new(
3397 order.instrument_id(),
3398 Price::from("999.00"),
3399 Price::from("1000.00"),
3400 Quantity::from("1.000"),
3401 Quantity::from("1.000"),
3402 UnixNanos::from(1),
3403 UnixNanos::from(1),
3404 );
3405 msgbus::send_quote(
3406 format!(
3407 "SimulatedExchange.process_new_quote.{}",
3408 order.instrument_id().venue
3409 )
3410 .into(),
3411 "e,
3412 );
3413
3414 let cache = engine.kernel.cache.borrow();
3415 let cached_order = cache.order(&order.client_order_id()).unwrap();
3416 assert_eq!(cached_order.status(), OrderStatus::Filled);
3417 assert!(matches!(
3418 cached_order.events().last(),
3419 Some(OrderEventAny::Filled(_))
3420 ));
3421 }
3422
3423 #[rstest]
3424 fn test_immediate_cancel_of_order_with_deferred_acceptance(
3425 #[values(
3426 DeferredAcceptance::Initialized,
3427 DeferredAcceptance::Released,
3428 DeferredAcceptance::OcoSibling,
3429 DeferredAcceptance::BracketChildren
3430 )]
3431 scenario: DeferredAcceptance,
3432 #[values(true, false)] cancel_all: bool,
3433 crypto_perpetual_ethusdt: CryptoPerpetual,
3434 ) {
3435 let mut engine = create_immediate_engine(&crypto_perpetual_ethusdt);
3436 let instrument_id = crypto_perpetual_ethusdt.id;
3437 engine
3438 .add_strategy(DeferredAcceptanceCancelStrategy::new(
3439 instrument_id,
3440 scenario,
3441 cancel_all,
3442 ))
3443 .unwrap();
3444 let quote = |bid: &str, ask: &str, ts: u64| {
3445 Data::Quote(QuoteTick::new(
3446 instrument_id,
3447 Price::from(bid),
3448 Price::from(ask),
3449 Quantity::from("1.000"),
3450 Quantity::from("1.000"),
3451 UnixNanos::from(ts),
3452 UnixNanos::from(ts),
3453 ))
3454 };
3455 engine
3456 .add_data(
3457 vec![
3458 quote("1000.00", "1001.00", 1),
3459 quote("1002.00", "1003.00", 2),
3460 ],
3461 None,
3462 true,
3463 true,
3464 )
3465 .unwrap();
3466
3467 engine.run(None, None, None, false).unwrap();
3468
3469 assert!(
3470 engine.venues[&instrument_id.venue]
3471 .borrow()
3472 .get_open_orders(Some(instrument_id))
3473 .is_empty()
3474 );
3475 let cache = engine.kernel.cache.borrow();
3476 let orders = cache.orders(None, Some(&instrument_id), None, None, None);
3477 let expected_orders = match scenario {
3478 DeferredAcceptance::BracketChildren => 3,
3479 DeferredAcceptance::OcoSibling => 2,
3480 DeferredAcceptance::Initialized | DeferredAcceptance::Released => 1,
3481 };
3482 assert_eq!(orders.len(), expected_orders);
3483
3484 for order in &orders {
3485 assert_eq!(order.status(), OrderStatus::Canceled);
3486 let events = order.events();
3487
3488 let matched = if order.parent_order_id().is_some() {
3490 matches!(
3491 events.as_slice(),
3492 [
3493 OrderEventAny::Initialized(_),
3494 OrderEventAny::Submitted(_),
3495 OrderEventAny::Canceled(_),
3496 ]
3497 )
3498 } else {
3499 matches!(
3500 events.as_slice(),
3501 [
3502 ..,
3503 OrderEventAny::Submitted(_),
3504 OrderEventAny::Accepted(_),
3505 OrderEventAny::Canceled(_),
3506 ]
3507 )
3508 };
3509 assert!(matched, "unexpected events {events:?}");
3510 }
3511 }
3512
3513 #[derive(Debug)]
3515 struct CancelParkedChildStrategy {
3516 core: StrategyCore,
3517 instrument_id: InstrumentId,
3518 same_handler: bool,
3519 quote_count: usize,
3520 take_profit: Option<ClientOrderId>,
3521 }
3522
3523 impl CancelParkedChildStrategy {
3524 fn new(instrument_id: InstrumentId, same_handler: bool) -> Self {
3525 Self {
3526 core: StrategyCore::new(StrategyConfig {
3527 strategy_id: Some(StrategyId::from("PARKED-CANCEL-001")),
3528 ..Default::default()
3529 }),
3530 instrument_id,
3531 same_handler,
3532 quote_count: 0,
3533 take_profit: None,
3534 }
3535 }
3536 }
3537
3538 impl DataActor for CancelParkedChildStrategy {
3539 fn on_start(&mut self) -> anyhow::Result<()> {
3540 self.subscribe_quotes(self.instrument_id, None, None);
3541 Ok(())
3542 }
3543
3544 fn on_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
3545 self.quote_count += 1;
3546
3547 if self.quote_count == 1 {
3548 let instrument_id = self.instrument_id;
3549 let orders = self
3550 .order()
3551 .bracket()
3552 .instrument_id(instrument_id)
3553 .order_side(OrderSide::Buy)
3554 .quantity(Quantity::from("1.000"))
3555 .entry_order_type(OrderType::Limit)
3556 .entry_price(Price::from("1000.00"))
3557 .tp_price(Price::from("1100.00"))
3558 .sl_trigger_price(Price::from("800.00"))
3559 .call();
3560 self.take_profit = orders
3561 .iter()
3562 .find(|order| {
3563 order.order_type() == OrderType::Limit && order.parent_order_id().is_some()
3564 })
3565 .map(Order::client_order_id);
3566 self.submit_order_list(orders, None, None, None)?;
3567 }
3568
3569 let cancel_quote = if self.same_handler { 1 } else { 2 };
3570
3571 if self.quote_count == cancel_quote {
3572 self.cancel_order(self.take_profit.unwrap(), None, None)?;
3573 }
3574 Ok(())
3575 }
3576 }
3577
3578 nautilus_strategy!(CancelParkedChildStrategy);
3579
3580 #[rstest]
3581 fn test_cancel_parked_oto_child_stays_canceled_after_entry_fills(
3582 #[values(false, true)] use_message_queue: bool,
3583 #[values(false, true)] same_handler: bool,
3584 crypto_perpetual_ethusdt: CryptoPerpetual,
3585 ) {
3586 let mut engine =
3587 create_engine_with_message_queue(&crypto_perpetual_ethusdt, use_message_queue);
3588 let instrument_id = crypto_perpetual_ethusdt.id;
3589 engine
3590 .add_strategy(CancelParkedChildStrategy::new(instrument_id, same_handler))
3591 .unwrap();
3592 let quote = |bid: &str, ask: &str, ts: u64| {
3593 Data::Quote(QuoteTick::new(
3594 instrument_id,
3595 Price::from(bid),
3596 Price::from(ask),
3597 Quantity::from("1.000"),
3598 Quantity::from("1.000"),
3599 UnixNanos::from(ts),
3600 UnixNanos::from(ts),
3601 ))
3602 };
3603 engine
3604 .add_data(
3605 vec![
3606 quote("999.00", "1001.00", 1),
3607 quote("999.00", "1001.00", 2),
3608 quote("999.00", "1000.00", 3),
3609 ],
3610 None,
3611 true,
3612 true,
3613 )
3614 .unwrap();
3615
3616 engine.run(None, None, None, false).unwrap();
3617
3618 assert!(
3619 engine.venues[&instrument_id.venue]
3620 .borrow()
3621 .get_open_orders(Some(instrument_id))
3622 .is_empty()
3623 );
3624 let cache = engine.kernel.cache.borrow();
3625 let orders = cache.orders(None, Some(&instrument_id), None, None, None);
3626 assert_eq!(orders.len(), 3);
3627
3628 for order in &orders {
3629 let expected = if order.parent_order_id().is_some() {
3631 OrderStatus::Canceled
3632 } else {
3633 OrderStatus::Filled
3634 };
3635 assert_eq!(
3636 order.status(),
3637 expected,
3638 "unexpected events {:?}",
3639 order.events()
3640 );
3641 }
3642 }
3643
3644 #[derive(Debug)]
3646 struct RejectedEntryExitStrategy {
3647 core: StrategyCore,
3648 instrument_id: InstrumentId,
3649 quote_count: usize,
3650 exits: Rc<Cell<usize>>,
3651 }
3652
3653 impl DataActor for RejectedEntryExitStrategy {
3654 fn on_start(&mut self) -> anyhow::Result<()> {
3655 self.subscribe_quotes(self.instrument_id, None, None);
3656 Ok(())
3657 }
3658
3659 fn on_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
3660 self.quote_count += 1;
3661
3662 match self.quote_count {
3663 1 => {
3664 let instrument_id = self.instrument_id;
3665 let orders = self
3666 .order()
3667 .bracket()
3668 .instrument_id(instrument_id)
3669 .order_side(OrderSide::Buy)
3670 .quantity(Quantity::from("1.000"))
3671 .entry_order_type(OrderType::Limit)
3672 .entry_price(Price::from("1001.00"))
3673 .entry_post_only(true)
3674 .tp_price(Price::from("1100.00"))
3675 .sl_trigger_price(Price::from("800.00"))
3676 .call();
3677 self.submit_order_list(orders, None, None, None)
3678 }
3679 2 => self.market_exit(),
3680 _ => Ok(()),
3681 }
3682 }
3683 }
3684
3685 nautilus_strategy!(RejectedEntryExitStrategy, {
3686 fn post_market_exit(&mut self) {
3687 self.exits.set(self.exits.get() + 1);
3688 }
3689 });
3690
3691 #[rstest]
3692 fn test_rejected_oto_parent_rejects_children_and_market_exit_completes(
3693 #[values(false, true)] use_message_queue: bool,
3694 crypto_perpetual_ethusdt: CryptoPerpetual,
3695 ) {
3696 let mut engine =
3697 create_engine_with_message_queue(&crypto_perpetual_ethusdt, use_message_queue);
3698 let instrument_id = crypto_perpetual_ethusdt.id;
3699 let exits = Rc::new(Cell::new(0));
3700 engine
3701 .add_strategy(RejectedEntryExitStrategy {
3702 core: StrategyCore::new(StrategyConfig {
3703 strategy_id: Some(StrategyId::from("REJECTED-ENTRY-001")),
3704 ..Default::default()
3705 }),
3706 instrument_id,
3707 quote_count: 0,
3708 exits: Rc::clone(&exits),
3709 })
3710 .unwrap();
3711 let quotes = [1, 2, 3]
3712 .into_iter()
3713 .map(|secs: u64| {
3714 let ts = UnixNanos::from(secs * 1_000_000_000);
3715 Data::Quote(QuoteTick::new(
3716 instrument_id,
3717 Price::from("1000.00"),
3718 Price::from("1001.00"),
3719 Quantity::from("1.000"),
3720 Quantity::from("1.000"),
3721 ts,
3722 ts,
3723 ))
3724 })
3725 .collect();
3726 engine.add_data(quotes, None, true, true).unwrap();
3727
3728 engine.run(None, None, None, false).unwrap();
3729
3730 let cache = engine.kernel.cache.borrow();
3731 let orders = cache.orders(None, Some(&instrument_id), None, None, None);
3732 assert_eq!(orders.len(), 3);
3733
3734 for order in &orders {
3735 assert_eq!(
3736 order.status(),
3737 OrderStatus::Rejected,
3738 "unexpected events {:?}",
3739 order.events()
3740 );
3741 }
3742 assert_eq!(exits.get(), 1);
3743 }
3744
3745 #[derive(Debug)]
3747 struct MultiStepEntryStrategy {
3748 core: StrategyCore,
3749 instrument_id: InstrumentId,
3750 submitted: bool,
3751 }
3752
3753 impl DataActor for MultiStepEntryStrategy {
3754 fn on_start(&mut self) -> anyhow::Result<()> {
3755 self.subscribe_quotes(self.instrument_id, None, None);
3756 Ok(())
3757 }
3758
3759 fn on_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
3760 if self.submitted {
3761 return Ok(());
3762 }
3763 self.submitted = true;
3764 let orders = self
3765 .order()
3766 .bracket()
3767 .instrument_id(self.instrument_id)
3768 .order_side(OrderSide::Buy)
3769 .quantity(Quantity::from("2.000"))
3770 .tp_price(Price::from("1100.00"))
3771 .sl_trigger_price(Price::from("800.00"))
3772 .call();
3773 self.submit_order_list(orders, None, None, None)
3774 }
3775 }
3776
3777 nautilus_strategy!(MultiStepEntryStrategy);
3778
3779 #[rstest]
3780 fn test_full_trigger_market_entry_filling_in_steps_releases_children(
3781 crypto_perpetual_ethusdt: CryptoPerpetual,
3782 ) {
3783 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
3784 let instrument_id = crypto_perpetual_ethusdt.id;
3785 let venue_config = SimulatedVenueConfig::builder()
3786 .venue(instrument_id.venue)
3787 .oms_type(OmsType::Netting)
3788 .account_type(AccountType::Margin)
3789 .book_type(BookType::L1_MBP)
3790 .starting_balances(vec![Money::from("1_000_000 USDT")])
3791 .oto_full_trigger(true)
3792 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
3793 .build()
3794 .unwrap();
3795 engine.add_venue(venue_config).unwrap();
3796 engine
3797 .add_instrument(&InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt))
3798 .unwrap();
3799 engine
3800 .add_strategy(MultiStepEntryStrategy {
3801 core: StrategyCore::new(StrategyConfig {
3802 strategy_id: Some(StrategyId::from("MULTI-STEP-001")),
3803 ..Default::default()
3804 }),
3805 instrument_id,
3806 submitted: false,
3807 })
3808 .unwrap();
3809 let quotes = [1, 2]
3810 .into_iter()
3811 .map(|secs: u64| {
3812 let ts = UnixNanos::from(secs * 1_000_000_000);
3813 Data::Quote(QuoteTick::new(
3814 instrument_id,
3815 Price::from("1000.00"),
3816 Price::from("1001.00"),
3817 Quantity::from("1.000"),
3818 Quantity::from("1.000"),
3819 ts,
3820 ts,
3821 ))
3822 })
3823 .collect();
3824 engine.add_data(quotes, None, true, true).unwrap();
3825
3826 engine.run(None, None, None, false).unwrap();
3827
3828 let cache = engine.kernel.cache.borrow();
3829 let orders = cache.orders(None, Some(&instrument_id), None, None, None);
3830 assert_eq!(orders.len(), 3);
3831
3832 for order in &orders {
3833 let fill_count = order
3834 .events()
3835 .iter()
3836 .filter(|event| matches!(event, OrderEventAny::Filled(_)))
3837 .count();
3838 let expected = if order.parent_order_id().is_none() {
3839 assert!(fill_count > 1, "entry filled in {fill_count} step(s)");
3840 OrderStatus::Filled
3841 } else {
3842 OrderStatus::Accepted
3843 };
3844 assert_eq!(
3845 order.status(),
3846 expected,
3847 "unexpected events {:?}",
3848 order.events()
3849 );
3850 }
3851 }
3852
3853 #[rstest]
3854 #[case::strategy(&["ROLL"], true)]
3855 #[case::snapshots(&["portfolio_equity_curve.SIM-001", "portfolio_snapshot.SIM-001"], false)]
3856 #[case::strategy_first(&["a_strategy", "portfolio_equity_curve.SIM-001"], true)]
3857 #[case::strategy_last(&["portfolio_equity_curve.SIM-001", "z_strategy"], true)]
3858 fn test_timer_handler_sets_last_ns_to_fire_time(
3859 #[case] timer_names: &[&str],
3860 #[case] settle_all: bool,
3861 ) {
3862 let mut engine = create_engine();
3863 engine.last_ns = UnixNanos::from(30);
3864 let fired = Rc::new(Cell::new(0));
3865 let fired_clone = Rc::clone(&fired);
3866 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
3867 fired_clone.set(fired_clone.get() + 1);
3868 }));
3869
3870 for name in timer_names {
3871 engine
3872 .kernel
3873 .clock
3874 .borrow_mut()
3875 .set_timer_ns(
3876 name,
3877 DurationNanos::new(1),
3878 Some(UnixNanos::from(20)),
3879 None,
3880 Some(callback.clone()),
3881 Some(true),
3882 Some(true),
3883 )
3884 .unwrap();
3885 }
3886
3887 let clocks = engine.collect_all_clocks();
3888
3889 for clock in &clocks {
3890 BacktestEngine::advance_clock_on_accumulator(
3891 &mut engine.accumulator,
3892 clock,
3893 UnixNanos::from(30),
3894 false,
3895 );
3896 }
3897
3898 let settlement_scope = engine
3899 .run_timer_handlers_at(&clocks, UnixNanos::from(20), UnixNanos::from(30))
3900 .unwrap();
3901
3902 assert_eq!(fired.get(), timer_names.len());
3903 assert_eq!(engine.last_ns, UnixNanos::from(20));
3904 assert_eq!(matches!(settlement_scope, SettlementScope::All), settle_all);
3905 assert_eq!(
3906 matches!(settlement_scope, SettlementScope::Data(None)),
3907 !settle_all,
3908 );
3909 }
3910
3911 #[rstest]
3912 #[case::complete(false, 25)]
3913 #[case::shutdown(true, 20)]
3914 fn test_flush_accumulator_events_sets_last_ns_for_completion(
3915 #[case] shutdown: bool,
3916 #[case] expected_last_ns: u64,
3917 ) {
3918 let mut engine = create_engine();
3919 let last_ns = UnixNanos::from(25);
3920 let ts_now = UnixNanos::from(30);
3921 engine.last_ns = last_ns;
3922 let clocks = engine.collect_all_clocks();
3923 BacktestEngine::set_all_clocks_time(&clocks, last_ns);
3924 let fired = Rc::new(Cell::new(false));
3925 let fired_clone = Rc::clone(&fired);
3926 let observed_ns = Rc::new(Cell::new(UnixNanos::default()));
3927 let observed_ns_clone = Rc::clone(&observed_ns);
3928 let clock = Rc::clone(&engine.kernel.clock);
3929 let shutdown_requested = engine.kernel.shutdown_flag();
3930 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
3931 fired_clone.set(true);
3932 observed_ns_clone.set(clock.borrow().timestamp_ns());
3933 shutdown_requested.set(shutdown);
3934 }));
3935 engine
3936 .kernel
3937 .clock
3938 .borrow_mut()
3939 .set_timer_ns(
3940 "ROLL",
3941 DurationNanos::new(100),
3942 Some(UnixNanos::from(20)),
3943 None,
3944 Some(callback),
3945 Some(true),
3946 Some(true),
3947 )
3948 .unwrap();
3949
3950 engine.flush_accumulator_events(&clocks, ts_now).unwrap();
3951
3952 assert!(fired.get());
3953 assert_eq!(observed_ns.get(), UnixNanos::from(20));
3954 assert_eq!(engine.kernel.is_shutdown_requested(), shutdown);
3955 assert_eq!(engine.last_ns, UnixNanos::from(expected_last_ns));
3956 }
3957
3958 #[rstest]
3959 fn test_add_duplicate_venue_preserves_original_exchange(
3960 crypto_perpetual_ethusdt: CryptoPerpetual,
3961 ) {
3962 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
3963 let venue = Venue::from("BINANCE");
3964 let venue_config = SimulatedVenueConfig::builder()
3965 .venue(venue)
3966 .oms_type(OmsType::Netting)
3967 .account_type(AccountType::Margin)
3968 .book_type(BookType::L1_MBP)
3969 .starting_balances(vec![Money::from("1_000_000 USDT")])
3970 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
3971 .build()
3972 .unwrap();
3973 engine.add_venue(venue_config).unwrap();
3974
3975 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
3976 let instrument_id = instrument.id();
3977 engine.add_instrument(&instrument).unwrap();
3978
3979 let initial_quote = QuoteTick::new(
3980 instrument_id,
3981 Price::from("1000.00"),
3982 Price::from("1001.00"),
3983 Quantity::from("1.000"),
3984 Quantity::from("1.000"),
3985 UnixNanos::from(1),
3986 UnixNanos::from(1),
3987 );
3988 msgbus::send_quote(
3989 format!("SimulatedExchange.process_new_quote.{venue}").into(),
3990 &initial_quote,
3991 );
3992
3993 let best_bid_before = engine
3994 .venues
3995 .get(&venue)
3996 .unwrap()
3997 .borrow()
3998 .best_bid_price(instrument_id);
3999 let best_ask_before = engine
4000 .venues
4001 .get(&venue)
4002 .unwrap()
4003 .borrow()
4004 .best_ask_price(instrument_id);
4005 let original_exchange = Rc::downgrade(engine.venues.get(&venue).unwrap());
4006 let venues_before = engine.list_venues();
4007 let exec_clients_len_before = engine.exec_clients.len();
4008 let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
4009 let duplicate_config = SimulatedVenueConfig::builder()
4010 .venue(venue)
4011 .oms_type(OmsType::Netting)
4012 .account_type(AccountType::Margin)
4013 .book_type(BookType::L1_MBP)
4014 .starting_balances(vec![Money::from("1_000_000 USDT")])
4015 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
4016 .build()
4017 .unwrap();
4018 assert!(engine.add_venue(duplicate_config).is_err());
4019
4020 let original_exchange = original_exchange
4021 .upgrade()
4022 .expect("the original exchange must remain alive");
4023 assert!(Rc::ptr_eq(
4024 &original_exchange,
4025 engine.venues.get(&venue).unwrap()
4026 ));
4027 assert_eq!(engine.list_venues(), venues_before);
4028 assert_eq!(engine.exec_clients.len(), exec_clients_len_before);
4029 assert_eq!(
4030 engine.kernel.exec_engine.borrow().client_ids(),
4031 client_ids_before
4032 );
4033
4034 let distinct_quote = QuoteTick::new(
4035 instrument_id,
4036 Price::from("2000.00"),
4037 Price::from("2001.00"),
4038 Quantity::from("2.000"),
4039 Quantity::from("2.000"),
4040 UnixNanos::from(2),
4041 UnixNanos::from(2),
4042 );
4043 msgbus::send_quote(
4044 format!("SimulatedExchange.process_new_quote.{venue}").into(),
4045 &distinct_quote,
4046 );
4047
4048 let original_exchange = original_exchange.borrow();
4049 let best_bid_after = original_exchange.best_bid_price(instrument_id);
4050 let best_ask_after = original_exchange.best_ask_price(instrument_id);
4051 assert_ne!(best_bid_after, best_bid_before);
4052 assert_ne!(best_ask_after, best_ask_before);
4053 assert_eq!(best_bid_after, Some(Price::from("2000.00")));
4054 assert_eq!(best_ask_after, Some(Price::from("2001.00")));
4055 }
4056
4057 #[rstest]
4058 #[case::duplicate_client(false)]
4059 #[case::occupied_route(true)]
4060 fn test_add_venue_execution_registration_failure_publishes_nothing(
4061 #[case] occupied_route: bool,
4062 ) {
4063 let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
4064 let venue = Venue::from("SIM");
4065 let client_id = ClientId::from(if occupied_route { "OTHER" } else { "SIM" });
4066 engine
4067 .kernel
4068 .exec_engine
4069 .borrow_mut()
4070 .register_client(Box::new(StubExecutionClient::new(
4071 client_id,
4072 AccountId::from("SIM-001"),
4073 venue,
4074 OmsType::Netting,
4075 None,
4076 )))
4077 .unwrap();
4078 engine
4079 .kernel
4080 .exec_engine
4081 .borrow_mut()
4082 .register_venue_routing(client_id, venue)
4083 .unwrap();
4084 let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
4085
4086 let endpoint = format!("SimulatedExchange.process_new_quote.{venue}");
4087 let received_quotes = Rc::new(RefCell::new(Vec::new()));
4088 let received_quotes_handler = Rc::clone(&received_quotes);
4089 let sentinel = TypedHandler::from_with_id("venue-setup-sentinel", move |quote| {
4090 received_quotes_handler.borrow_mut().push(*quote);
4091 });
4092 msgbus::register_quote_endpoint(endpoint.as_str().into(), sentinel);
4093
4094 let venue_config = SimulatedVenueConfig::builder()
4095 .venue(venue)
4096 .oms_type(OmsType::Netting)
4097 .account_type(AccountType::Margin)
4098 .book_type(BookType::L1_MBP)
4099 .starting_balances(vec![Money::from("1_000_000 USD")])
4100 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
4101 .build()
4102 .unwrap();
4103 assert!(engine.add_venue(venue_config).is_err());
4104
4105 assert!(!engine.venues.contains_key(&venue));
4106 assert!(engine.exec_clients.is_empty());
4107 assert_eq!(
4108 engine.kernel.exec_engine.borrow().client_ids(),
4109 client_ids_before
4110 );
4111
4112 let quote = QuoteTick::new(
4113 InstrumentId::from("TEST.SIM"),
4114 Price::from("100.00"),
4115 Price::from("101.00"),
4116 Quantity::from("1"),
4117 Quantity::from("1"),
4118 UnixNanos::from(1),
4119 UnixNanos::from(1),
4120 );
4121 msgbus::send_quote(endpoint.as_str().into(), "e);
4122 assert_eq!(received_quotes.borrow().as_slice(), &[quote]);
4123 }
4124
4125 #[rstest]
4126 fn test_add_strategy_registers_configured_hedging_oms_type() {
4127 let mut engine = create_engine();
4128 let instrument = crypto_perpetual_ethusdt();
4129 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
4130
4131 engine
4132 .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
4133 .unwrap();
4134 engine
4135 .add_strategy(TestStrategy::new(StrategyConfig {
4136 strategy_id: Some(strategy_id),
4137 oms_type: Some(OmsType::Hedging),
4138 ..Default::default()
4139 }))
4140 .unwrap();
4141
4142 let order = OrderTestBuilder::new(OrderType::Market)
4143 .trader_id(engine.trader_id())
4144 .strategy_id(strategy_id)
4145 .instrument_id(instrument.id())
4146 .quantity(Quantity::from("1.000"))
4147 .build();
4148 let position_id = PositionId::new("CUSTOM-POSITION-001");
4149
4150 engine
4151 .kernel
4152 .exec_engine
4153 .borrow()
4154 .cache()
4155 .borrow_mut()
4156 .add_order(
4157 order.clone(),
4158 Some(position_id),
4159 Some(ClientId::from("BINANCE")),
4160 true,
4161 )
4162 .unwrap();
4163
4164 let submit_order = SubmitOrder::new(
4165 order.trader_id(),
4166 Some(ClientId::from("BINANCE")),
4167 strategy_id,
4168 instrument.id(),
4169 order.client_order_id(),
4170 order.init_event().clone(),
4171 order.exec_algorithm_id(),
4172 Some(position_id),
4173 None,
4174 UUID4::new(),
4175 UnixNanos::default(),
4176 None,
4177 );
4178
4179 engine
4180 .kernel
4181 .exec_engine
4182 .borrow()
4183 .execute(TradingCommand::SubmitOrder(submit_order));
4184
4185 let exec_engine = engine.kernel.exec_engine.borrow();
4186 let cache = exec_engine.cache().borrow();
4187 let cached_order = cache
4188 .order(&order.client_order_id())
4189 .expect("Order should be cached");
4190
4191 assert_eq!(cached_order.status(), OrderStatus::Initialized);
4192 }
4193
4194 fn create_engine_with_replay_store(fail_restore: bool) -> BacktestEngine {
4195 let config = BacktestEngineConfig {
4196 load_state: true,
4197 run_analysis: false,
4198 ..Default::default()
4199 };
4200 let mut engine = BacktestEngine::new(config.clone()).unwrap();
4201 let event_store_factory = move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
4202 Ok::<_, anyhow::Error>(Box::new(BacktestReplayKernelEventStore { fail_restore })
4203 as Box<dyn KernelEventStore>)
4204 };
4205
4206 engine.kernel = NautilusKernel::new_with(
4207 "BacktestEngine".to_string(),
4208 config,
4209 None,
4210 Some(Box::new(event_store_factory)),
4211 )
4212 .unwrap();
4213 engine.instance_id = engine.kernel.instance_id;
4214 engine
4215 }
4216
4217 fn create_stop_market_order(instrument: &CryptoPerpetual) -> OrderAny {
4218 OrderTestBuilder::new(OrderType::StopMarket)
4219 .instrument_id(instrument.id())
4220 .side(OrderSide::Buy)
4221 .trigger_price(Price::from("5100.00"))
4222 .quantity(Quantity::from(1))
4223 .emulation_trigger(TriggerType::BidAsk)
4224 .build()
4225 }
4226
4227 fn create_submit_order_command(order: &OrderAny) -> SubmitOrder {
4228 SubmitOrder::new(
4229 order.trader_id(),
4230 None,
4231 order.strategy_id(),
4232 order.instrument_id(),
4233 order.client_order_id(),
4234 order.init_event().clone(),
4235 order.exec_algorithm_id(),
4236 None,
4237 None,
4238 UUID4::new(),
4239 0.into(),
4240 None, )
4242 }
4243
4244 fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
4245 let (handler, saving_handler) =
4246 get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
4247 msgbus::register_data_command_endpoint(
4248 MessagingSwitchboard::data_engine_queue_execute(),
4249 handler,
4250 );
4251 saving_handler
4252 }
4253
4254 #[rstest]
4255 fn test_run_impl_event_store_replay_skips_trader_start() {
4256 let mut engine = create_engine_with_replay_store(false);
4257
4258 engine
4259 .run_impl(
4260 Some(UnixNanos::from(0)),
4261 Some(UnixNanos::from(1)),
4262 None,
4263 true,
4264 )
4265 .unwrap();
4266
4267 assert!(engine.kernel.is_event_store_replay_configured());
4268 assert!(engine.kernel.is_event_store_replay());
4269 assert!(!engine.kernel.trader.borrow().is_running());
4270 }
4271
4272 #[rstest]
4273 fn test_end_reports_strategy_stranded_by_managed_stop() {
4274 let (mut engine, strategy_id) = create_engine_with_strategy(true);
4275
4276 let result = engine.run(
4277 Some(UnixNanos::from(0)),
4278 Some(UnixNanos::from(1)),
4279 None,
4280 false,
4281 );
4282
4283 assert!(result.is_ok());
4284 assert_eq!(
4285 component_state(&strategy_id.inner()).unwrap(),
4286 ComponentState::Running
4287 );
4288 assert_eq!(engine.running_strategy_ids(), vec![strategy_id]);
4289 }
4290
4291 #[rstest]
4292 fn test_end_reports_no_cleanly_stopped_strategies() {
4293 let mut empty_engine = create_engine();
4294 let empty_result = empty_engine.run(
4295 Some(UnixNanos::from(0)),
4296 Some(UnixNanos::from(1)),
4297 None,
4298 false,
4299 );
4300 assert!(empty_result.is_ok());
4301 assert!(empty_engine.running_strategy_ids().is_empty());
4302
4303 let (mut engine, strategy_id) = create_engine_with_strategy(false);
4304 let result = engine.run(
4305 Some(UnixNanos::from(0)),
4306 Some(UnixNanos::from(1)),
4307 None,
4308 false,
4309 );
4310
4311 assert!(result.is_ok());
4312 assert_ne!(
4313 component_state(&strategy_id.inner()).unwrap(),
4314 ComponentState::Running
4315 );
4316 assert!(engine.running_strategy_ids().is_empty());
4317 }
4318
4319 #[rstest]
4320 fn test_run_impl_event_store_replay_config_failure_errors() {
4321 let mut engine = create_engine_with_replay_store(true);
4322
4323 let error = engine
4324 .run_impl(
4325 Some(UnixNanos::from(0)),
4326 Some(UnixNanos::from(1)),
4327 None,
4328 true,
4329 )
4330 .unwrap_err();
4331
4332 assert_eq!(error.to_string(), "event-store replay did not start");
4333 assert!(engine.kernel.is_event_store_replay_configured());
4334 assert!(!engine.kernel.is_event_store_replay());
4335 assert!(!engine.kernel.trader.borrow().is_running());
4336 }
4337
4338 #[rstest]
4339 fn test_backtest_state_persistence_loads_before_start_and_saves_after_settle() {
4340 let actor_id = ActorId::from("BACKTEST-STATE-ACTOR");
4341 let strategy_id = StrategyId::from("BACKTEST-STATE-STRATEGY-001");
4342 let actor_load = IndexMap::from([("actor-load".to_string(), b"actor-loaded".to_vec())]);
4343 let strategy_load =
4344 IndexMap::from([("strategy-load".to_string(), b"strategy-loaded".to_vec())]);
4345 let actor_save = IndexMap::from([("actor-save".to_string(), b"actor-saved".to_vec())]);
4346 let strategy_save =
4347 IndexMap::from([("strategy-save".to_string(), b"strategy-saved".to_vec())]);
4348 let (database, control) = TestCacheDatabaseControl::create();
4349 control.set_actor_state(actor_id, &actor_load);
4350 control.set_strategy_state(strategy_id, &strategy_load);
4351 let config = BacktestEngineConfig {
4352 load_state: true,
4353 save_state: true,
4354 run_analysis: false,
4355 ..Default::default()
4356 };
4357 let mut engine = BacktestEngine::new(config).unwrap();
4358 engine
4359 .kernel
4360 .cache
4361 .borrow_mut()
4362 .set_database(Box::new(database));
4363 engine
4364 .add_actor(StateActor::new(
4365 actor_id,
4366 control.clone(),
4367 actor_save.clone(),
4368 ))
4369 .unwrap();
4370 engine
4371 .add_strategy(StateStrategy::new(
4372 strategy_id,
4373 control.clone(),
4374 strategy_save.clone(),
4375 ))
4376 .unwrap();
4377
4378 engine
4379 .run(
4380 Some(UnixNanos::from(0)),
4381 Some(UnixNanos::from(1)),
4382 None,
4383 false,
4384 )
4385 .unwrap();
4386 engine.dispose();
4387
4388 assert_eq!(
4389 control.events(),
4390 vec![
4391 "actor.load:BACKTEST-STATE-ACTOR",
4392 "actor.on_load",
4393 "strategy.load:BACKTEST-STATE-STRATEGY-001",
4394 "strategy.on_load",
4395 "actor.on_start",
4396 "strategy.on_start",
4397 "actor.on_stop",
4398 "strategy.on_stop",
4399 "actor.on_save",
4400 "actor.update:BACKTEST-STATE-ACTOR",
4401 "strategy.on_save",
4402 "strategy.update:BACKTEST-STATE-STRATEGY-001",
4403 "database.close",
4404 ]
4405 );
4406 assert_eq!(control.actor_state(&actor_id), Some(actor_save));
4407 assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
4408 assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
4409 }
4410
4411 #[rstest]
4412 fn test_backtest_state_persistence_reports_callback_errors_after_shutdown() {
4413 let actor_id = ActorId::from("BACKTEST-FAIL-SAVE-ACTOR");
4414 let strategy_id = StrategyId::from("BACKTEST-FAIL-SAVE-STRATEGY-001");
4415 let (database, control) = TestCacheDatabaseControl::create();
4416 let config = BacktestEngineConfig {
4417 save_state: true,
4418 run_analysis: false,
4419 ..Default::default()
4420 };
4421 let mut engine = BacktestEngine::new(config).unwrap();
4422 engine
4423 .kernel
4424 .cache
4425 .borrow_mut()
4426 .set_database(Box::new(database));
4427 engine
4428 .add_actor(StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save())
4429 .unwrap();
4430 engine
4431 .add_strategy(
4432 StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save(),
4433 )
4434 .unwrap();
4435
4436 let error = engine
4437 .run(
4438 Some(UnixNanos::from(0)),
4439 Some(UnixNanos::from(1)),
4440 None,
4441 false,
4442 )
4443 .unwrap_err();
4444 engine.dispose();
4445
4446 assert_eq!(
4447 error.to_string(),
4448 "Failed to save component state: actor BACKTEST-FAIL-SAVE-ACTOR callback: test actor \
4449 on_save failure; strategy BACKTEST-FAIL-SAVE-STRATEGY-001 callback: test strategy \
4450 on_save failure"
4451 );
4452 assert_eq!(
4453 control.events(),
4454 vec![
4455 "actor.on_start",
4456 "strategy.on_start",
4457 "actor.on_stop",
4458 "strategy.on_stop",
4459 "actor.on_save",
4460 "strategy.on_save",
4461 "database.close",
4462 ]
4463 );
4464 assert!(!engine.kernel.trader.borrow().is_running());
4465 assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
4466 }
4467
4468 #[rstest]
4469 #[case(None)]
4470 #[case(Some(true))]
4471 #[case(Some(false))]
4472 fn test_new_forces_drop_instruments_on_reset_false(
4473 crypto_perpetual_ethusdt: CryptoPerpetual,
4474 #[case] user_value: Option<bool>,
4475 ) {
4476 use nautilus_common::cache::CacheConfig;
4477
4478 let config = match user_value {
4479 None => BacktestEngineConfig::builder().build(),
4480 Some(value) => BacktestEngineConfig::builder()
4481 .cache(
4482 CacheConfig::builder()
4483 .drop_instruments_on_reset(value)
4484 .build()
4485 .unwrap(),
4486 )
4487 .build(),
4488 };
4489 let mut engine = BacktestEngine::new(config).unwrap();
4490
4491 let venue_config = SimulatedVenueConfig::builder()
4492 .venue(Venue::from("BINANCE"))
4493 .oms_type(OmsType::Netting)
4494 .account_type(AccountType::Margin)
4495 .book_type(BookType::L1_MBP)
4496 .starting_balances(vec![Money::from("1_000_000 USDT")])
4497 .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
4498 .build()
4499 .unwrap();
4500 engine.add_venue(venue_config).unwrap();
4501
4502 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
4503 let instrument_id = instrument.id();
4504 engine.add_instrument(&instrument).unwrap();
4505
4506 engine.reset().unwrap();
4507
4508 assert!(
4509 engine
4510 .kernel()
4511 .cache
4512 .borrow()
4513 .instrument(&instrument_id)
4514 .is_some(),
4515 "instrument must survive engine.reset(); user-supplied \
4516 drop_instruments_on_reset={user_value:?} must not leak through",
4517 );
4518 }
4519
4520 #[rstest]
4521 fn test_reset_resets_order_emulator_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
4522 let mut engine = create_engine();
4523 let data_commands =
4524 register_data_command_handler("DataEngine.queue_execute.backtest_reset");
4525 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt.clone());
4526 let instrument_id = instrument.id();
4527 engine.add_instrument(&instrument).unwrap();
4528 let order = create_stop_market_order(&crypto_perpetual_ethusdt);
4529 let command = create_submit_order_command(&order);
4530 engine
4531 .kernel
4532 .cache
4533 .borrow_mut()
4534 .add_order(order, None, None, false)
4535 .unwrap();
4536 let order_emulator = engine.kernel.order_emulator.emulator();
4537 let mut order_emulator = order_emulator.borrow_mut();
4538 order_emulator.cache_submit_order_command(command.clone());
4539 order_emulator.handle_submit_order(&command);
4540 drop(order_emulator);
4541 data_commands.clear();
4542
4543 engine.reset().unwrap();
4544
4545 let commands = data_commands.get_messages();
4546 let emulator = engine.kernel.order_emulator.get_emulator();
4547 assert!(emulator.subscribed_quotes().is_empty());
4548 assert!(emulator.subscribed_trades().is_empty());
4549 assert!(emulator.get_matching_core(&instrument_id).is_none());
4550 assert!(commands.iter().any(|command| matches!(
4551 command,
4552 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
4553 if command.instrument_id == instrument_id
4554 )));
4555 }
4556
4557 #[rstest]
4558 fn test_route_data_to_exchange_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
4559 let mut engine = create_engine();
4560 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
4561 let instrument_id = instrument.id();
4562 engine.add_instrument(&instrument).unwrap();
4563
4564 let status = InstrumentStatus::new(
4565 instrument_id,
4566 MarketStatusAction::Close,
4567 UnixNanos::from(1),
4568 UnixNanos::from(1),
4569 None,
4570 None,
4571 None,
4572 None,
4573 None,
4574 );
4575
4576 BacktestEngine::route_data_to_exchange(
4577 &engine.venues,
4578 &mut engine.has_book_processed,
4579 &engine.kernel.clock,
4580 DataRef::InstrumentStatus(&status),
4581 )
4582 .unwrap();
4583
4584 let exchange = engine.venues.get(&instrument_id.venue).unwrap().borrow();
4585 let market_status = exchange
4586 .get_matching_engine(&instrument_id)
4587 .unwrap()
4588 .market_status;
4589 assert_eq!(market_status, MarketStatus::Closed);
4590 }
4591}