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