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