Skip to main content

nautilus_backtest/
engine.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! The core `BacktestEngine` for backtesting on historical data.
17
18use std::{
19    any::Any,
20    cell::RefCell,
21    fmt::Debug,
22    rc::{Rc, Weak},
23    sync::Arc,
24};
25
26use ahash::{AHashMap, AHashSet};
27use nautilus_analysis::analyzer::PortfolioAnalyzer;
28use nautilus_common::{
29    actor::{DataActor, DataActorNative},
30    cache::Cache,
31    clock::{Clock, TestClock},
32    component::Component,
33    enums::LogColor,
34    log_info,
35    logging::{
36        logging_clock_set_realtime_mode, logging_clock_set_static_mode,
37        logging_clock_set_static_time,
38    },
39    runner::{
40        SyncDataCommandSender, SyncTradingCommandSender, data_cmd_queue_is_empty,
41        drain_data_cmd_queue, drain_trading_cmd_queue, replace_data_cmd_sender,
42        replace_exec_cmd_sender, trading_cmd_queue_is_empty,
43    },
44    timer::{TimeEvent, TimeEventCallback},
45};
46use nautilus_core::{
47    UUID4, UnixNanos, datetime::unix_nanos_to_iso8601, string::formatting::Separable,
48};
49use nautilus_data::client::DataClientAdapter;
50use nautilus_execution::models::fill::FillModelAny;
51use nautilus_model::{
52    accounts::{Account, AccountAny},
53    data::{Data, HasTsInit},
54    enums::{AccountType, AggregationSource, BookType},
55    identifiers::{AccountId, ClientId, InstrumentId, TraderId, Venue},
56    instruments::{Instrument, InstrumentAny},
57    position::Position,
58    types::Price,
59};
60use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
61use nautilus_trading::{
62    ExecutionAlgorithm, ExecutionAlgorithmNative,
63    strategy::{Strategy, StrategyNative},
64};
65
66use crate::{
67    accumulator::TimeEventAccumulator,
68    config::{BacktestEngineConfig, SimulatedVenueConfig},
69    data_client::BacktestDataClient,
70    data_iterator::BacktestDataIterator,
71    exchange::SimulatedExchange,
72    execution_client::BacktestExecutionClient,
73    result::BacktestResult,
74};
75
76/// Core backtesting engine for running event-driven strategy backtests on historical data.
77///
78/// The `BacktestEngine` provides a high-fidelity simulation environment that processes
79/// historical market data chronologically through an event-driven architecture. It maintains
80/// simulated exchanges with realistic order matching and execution, allowing strategies
81/// to be tested exactly as they would run in live trading:
82///
83/// - Event-driven data replay with configurable latency models.
84/// - Multi-venue and multi-asset support.
85/// - Realistic order matching and execution simulation.
86/// - Strategy and portfolio performance analysis.
87/// - Transition from backtesting to live trading.
88pub struct BacktestEngine {
89    kernel: NautilusKernel,
90    instance_id: UUID4,
91    config: BacktestEngineConfig,
92    accumulator: TimeEventAccumulator,
93    run_config_id: Option<String>,
94    run_id: Option<UUID4>,
95    venues: AHashMap<Venue, Rc<RefCell<SimulatedExchange>>>,
96    exec_clients: Vec<BacktestExecutionClient>,
97    has_data: AHashSet<InstrumentId>,
98    has_book_data: AHashSet<InstrumentId>,
99    data_iterator: BacktestDataIterator,
100    data_len: usize,
101    data_stream_counter: usize,
102    ts_first: Option<UnixNanos>,
103    ts_last_data: Option<UnixNanos>,
104    sorted: bool,
105    iteration: usize,
106    force_stop: bool,
107    last_ns: UnixNanos,
108    last_module_ns: Option<UnixNanos>,
109    last_liquidation_ns: Option<UnixNanos>,
110    end_ns: UnixNanos,
111    run_started: Option<UnixNanos>,
112    run_finished: Option<UnixNanos>,
113    backtest_start: Option<UnixNanos>,
114    backtest_end: Option<UnixNanos>,
115}
116
117impl Debug for BacktestEngine {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct(stringify!(BacktestEngine))
120            .field("instance_id", &self.instance_id)
121            .field("run_config_id", &self.run_config_id)
122            .field("run_id", &self.run_id)
123            .finish_non_exhaustive()
124    }
125}
126
127impl BacktestEngine {
128    /// Create a new [`BacktestEngine`] instance.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the core `NautilusKernel` fails to initialize.
133    pub fn new(mut config: BacktestEngineConfig) -> anyhow::Result<Self> {
134        // The engine does not replay `add_instrument` on reset, so reruns rely
135        // on the cache retaining instruments regardless of the caller's config.
136        let mut cache_config = config.cache.unwrap_or_default();
137        cache_config.drop_instruments_on_reset = false;
138        config.cache = Some(cache_config);
139        let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
140        let instance_id = kernel.instance_id;
141
142        Ok(Self {
143            kernel,
144            instance_id,
145            config,
146            accumulator: TimeEventAccumulator::new(),
147            run_config_id: None,
148            run_id: None,
149            venues: AHashMap::new(),
150            exec_clients: Vec::new(),
151            has_data: AHashSet::new(),
152            has_book_data: AHashSet::new(),
153            data_iterator: BacktestDataIterator::new(),
154            data_len: 0,
155            data_stream_counter: 0,
156            ts_first: None,
157            ts_last_data: None,
158            sorted: true,
159            iteration: 0,
160            force_stop: false,
161            last_ns: UnixNanos::default(),
162            last_module_ns: None,
163            last_liquidation_ns: None,
164            end_ns: UnixNanos::default(),
165            run_started: None,
166            run_finished: None,
167            backtest_start: None,
168            backtest_end: None,
169        })
170    }
171
172    /// Returns a reference to the underlying kernel.
173    #[must_use]
174    pub const fn kernel(&self) -> &NautilusKernel {
175        &self.kernel
176    }
177
178    /// Returns a mutable reference to the underlying kernel.
179    pub fn kernel_mut(&mut self) -> &mut NautilusKernel {
180        &mut self.kernel
181    }
182
183    /// Returns the trader ID for this engine.
184    #[must_use]
185    pub fn trader_id(&self) -> TraderId {
186        self.kernel.trader_id()
187    }
188
189    /// Returns the machine ID for this engine.
190    #[must_use]
191    pub fn machine_id(&self) -> &str {
192        self.kernel.machine_id()
193    }
194
195    /// Returns the unique instance ID for this engine.
196    #[must_use]
197    pub fn instance_id(&self) -> UUID4 {
198        self.instance_id
199    }
200
201    /// Returns the current iteration count.
202    #[must_use]
203    pub fn iteration(&self) -> usize {
204        self.iteration
205    }
206
207    /// Returns the last run config ID, if any.
208    #[must_use]
209    pub fn run_config_id(&self) -> Option<&str> {
210        self.run_config_id.as_deref()
211    }
212
213    /// Returns the last run ID, if any.
214    #[must_use]
215    pub const fn run_id(&self) -> Option<UUID4> {
216        self.run_id
217    }
218
219    /// Returns when the last run started, if any.
220    #[must_use]
221    pub const fn run_started(&self) -> Option<UnixNanos> {
222        self.run_started
223    }
224
225    /// Returns when the last run finished, if any.
226    #[must_use]
227    pub const fn run_finished(&self) -> Option<UnixNanos> {
228        self.run_finished
229    }
230
231    /// Returns the last backtest range start, if any.
232    #[must_use]
233    pub const fn backtest_start(&self) -> Option<UnixNanos> {
234        self.backtest_start
235    }
236
237    /// Returns the last backtest range end, if any.
238    #[must_use]
239    pub const fn backtest_end(&self) -> Option<UnixNanos> {
240        self.backtest_end
241    }
242
243    /// Returns the list of registered venue identifiers.
244    #[must_use]
245    pub fn list_venues(&self) -> Vec<Venue> {
246        self.venues.keys().copied().collect()
247    }
248
249    /// # Errors
250    ///
251    /// Returns an error if initializing the simulated exchange for the venue fails.
252    pub fn add_venue(&mut self, config: SimulatedVenueConfig) -> anyhow::Result<()> {
253        // `routing` and `frozen_account` flow to the exec client, so capture
254        // them before the config is consumed by the exchange constructor.
255        let venue = config.venue;
256        let routing = Some(config.routing);
257        let frozen_account = Some(config.frozen_account);
258
259        let exchange =
260            SimulatedExchange::new(config, self.kernel.cache.clone(), self.kernel.clock.clone())?;
261        let exchange = Rc::new(RefCell::new(exchange));
262        SimulatedExchange::register_spread_quote_endpoint(&exchange);
263        self.venues.insert(venue, exchange.clone());
264
265        let account_id = AccountId::from(format!("{venue}-001").as_str());
266
267        let exec_client = BacktestExecutionClient::new(
268            self.config.trader_id(),
269            account_id,
270            &exchange,
271            self.kernel.cache.clone(),
272            self.kernel.clock.clone(),
273            routing,
274            frozen_account,
275        );
276
277        exchange
278            .borrow_mut()
279            .register_client(Rc::new(exec_client.clone()));
280
281        self.exec_clients.push(exec_client.clone());
282
283        self.kernel
284            .exec_engine
285            .borrow_mut()
286            .register_client(Box::new(exec_client))?;
287
288        log::info!("Adding exchange {venue} to engine");
289
290        Ok(())
291    }
292
293    /// Sets the settlement price for the specified venue instrument.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if the venue has not been added to the engine.
298    pub fn set_settlement_price(
299        &mut self,
300        venue: Venue,
301        instrument_id: InstrumentId,
302        price: Price,
303    ) -> anyhow::Result<()> {
304        let exchange = self
305            .venues
306            .get_mut(&venue)
307            .ok_or_else(|| anyhow::anyhow!("Unknown venue {venue}"))?;
308        exchange
309            .borrow_mut()
310            .set_settlement_price(instrument_id, price);
311        Ok(())
312    }
313
314    /// Changes the fill model for the specified venue.
315    pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModelAny) {
316        if let Some(exchange) = self.venues.get_mut(&venue) {
317            exchange.borrow_mut().set_fill_model(fill_model);
318        } else {
319            log::warn!(
320                "BacktestEngine::change_fill_model called for unknown venue {venue}, ignoring"
321            );
322        }
323    }
324
325    /// Adds an instrument to the backtest engine for the specified venue.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if:
330    /// - The instrument's associated venue has not been added via `add_venue`.
331    /// - Attempting to add a `CurrencyPair` instrument for a single-currency CASH account.
332    ///
333    pub fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
334        let instrument_id = instrument.id();
335        if let Some(exchange) = self.venues.get(&instrument.id().venue) {
336            let previous_expiration_ns = exchange.borrow().instrument_expiration(instrument_id);
337
338            if matches!(
339                instrument,
340                InstrumentAny::CurrencyPair(_) | InstrumentAny::TokenizedAsset(_)
341            ) && exchange.borrow().account_type != AccountType::Margin
342                && exchange.borrow().base_currency.is_some()
343            {
344                anyhow::bail!(
345                    "Cannot add a multi-currency spot instrument {instrument_id} for a venue with a single-currency CASH account"
346                )
347            }
348            exchange.borrow_mut().add_instrument(instrument.clone())?;
349            if let Some(expiration_ns) = instrument.expiration_ns() {
350                self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
351            }
352
353            if let Some(previous_expiration_ns) = previous_expiration_ns
354                && instrument.expiration_ns() != Some(previous_expiration_ns)
355                && !exchange
356                    .borrow()
357                    .has_unprocessed_instrument_expiration(previous_expiration_ns)
358            {
359                let timer_name = Self::instrument_expiration_timer_name(
360                    instrument_id.venue,
361                    previous_expiration_ns,
362                );
363                self.kernel.clock.borrow_mut().cancel_timer(&timer_name);
364            }
365        } else {
366            anyhow::bail!(
367                "Cannot add an `Instrument` object without first adding its associated venue {}",
368                instrument.id().venue
369            )
370        }
371
372        self.add_market_data_client_if_not_exists(instrument.id().venue);
373
374        self.kernel
375            .data_engine
376            .borrow_mut()
377            .process(instrument as &dyn Any);
378        log::info!(
379            "Added instrument {} to exchange {}",
380            instrument_id,
381            instrument_id.venue
382        );
383        Ok(())
384    }
385
386    /// Adds market data to the engine for replay during the backtest run.
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if:
391    /// - `data` is empty.
392    /// - `validate` is `true` and the instrument for the first element has not been
393    ///   added to the cache via [`add_instrument`](Self::add_instrument).
394    /// - `validate` is `true` and the first element is a [`Data::Bar`] whose
395    ///   `aggregation_source` is not [`AggregationSource::External`].
396    pub fn add_data(
397        &mut self,
398        data: Vec<Data>,
399        client_id: Option<ClientId>,
400        validate: bool,
401        sort: bool,
402    ) -> anyhow::Result<()> {
403        #[cfg(not(feature = "defi"))]
404        let _ = client_id;
405
406        anyhow::ensure!(!data.is_empty(), "data was empty");
407
408        let count = data.len();
409        let mut to_add = data;
410
411        if sort {
412            to_add.sort_by_key(HasTsInit::ts_init);
413        }
414
415        if validate {
416            // Mirror Cython: validate against the first element only and assume the
417            // batch is homogeneous (documented contract on add_data).
418            let first = &to_add[0];
419            #[cfg(feature = "defi")]
420            let first_is_defi = matches!(first, Data::Defi(_));
421            #[cfg(not(feature = "defi"))]
422            let first_is_defi = false;
423
424            if !first_is_defi {
425                let first_instrument_id = first.instrument_id();
426                anyhow::ensure!(
427                    self.kernel
428                        .cache
429                        .borrow()
430                        .instrument(&first_instrument_id)
431                        .is_some(),
432                    "Instrument {first_instrument_id} for the given data not found in the cache. \
433                     Add the instrument through `add_instrument()` prior to adding related data."
434                );
435
436                if let Data::Bar(bar) = first {
437                    anyhow::ensure!(
438                        bar.bar_type.aggregation_source() == AggregationSource::External,
439                        "bar_type.aggregation_source must be External, was {:?}",
440                        bar.bar_type.aggregation_source(),
441                    );
442                }
443            }
444        }
445
446        // Track has_data / has_book_data unconditionally so the depth-vs-data
447        // run-time check still fires for callers that pass validate=false
448        // (e.g. node.rs run_oneshot loading from a catalog). Time bounds are
449        // also tracked here so start/end defaults are correct even when the
450        // batch was added with sort=false.
451        let mut batch_min_ts: Option<UnixNanos> = None;
452        let mut batch_max_ts: Option<UnixNanos> = None;
453
454        #[cfg(feature = "defi")]
455        if to_add.iter().any(|item| matches!(item, Data::Defi(_))) {
456            self.add_defi_data_client_if_not_exists(client_id);
457        }
458
459        for item in &to_add {
460            #[cfg(feature = "defi")]
461            if matches!(item, Data::Defi(_)) {
462                let ts = item.ts_init();
463                batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
464                batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
465                continue;
466            }
467
468            let instr_id = item.instrument_id();
469            self.has_data.insert(instr_id);
470
471            if item.is_order_book_data() {
472                self.has_book_data.insert(instr_id);
473            }
474
475            self.add_market_data_client_if_not_exists(instr_id.venue);
476
477            let ts = item.ts_init();
478            batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
479            batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
480        }
481
482        if let Some(ts) = batch_min_ts
483            && self.ts_first.is_none_or(|t| ts < t)
484        {
485            self.ts_first = Some(ts);
486        }
487
488        if let Some(ts) = batch_max_ts
489            && self.ts_last_data.is_none_or(|t| ts > t)
490        {
491            self.ts_last_data = Some(ts);
492        }
493
494        self.data_len += count;
495        let stream_name = format!("backtest_data_{}", self.data_stream_counter);
496        self.data_stream_counter += 1;
497        self.data_iterator.add_data(&stream_name, to_add, true);
498
499        self.sorted = sort;
500
501        log::info!(
502            "Added {count} data element{} to BacktestEngine ({} total)",
503            if count == 1 { "" } else { "s" },
504            self.data_len,
505        );
506
507        Ok(())
508    }
509
510    /// Adds an actor to the backtest engine.
511    ///
512    /// # Errors
513    ///
514    /// Returns an error if the actor is already registered or the trader is in an invalid
515    /// state for actor registration.
516    pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
517    where
518        T: DataActor + DataActorNative + Component + Debug + 'static,
519    {
520        self.kernel.trader.borrow_mut().add_actor(actor)
521    }
522
523    /// Adds the given actors to the backtest engine. Stops at the first error.
524    ///
525    /// # Errors
526    ///
527    /// Returns an error if any actor fails to register; preceding actors remain registered.
528    pub fn add_actors<T>(&mut self, actors: Vec<T>) -> anyhow::Result<()>
529    where
530        T: DataActor + DataActorNative + Component + Debug + 'static,
531    {
532        for actor in actors {
533            self.add_actor(actor)?;
534        }
535        Ok(())
536    }
537
538    /// Adds a strategy to the backtest engine.
539    ///
540    /// # Errors
541    ///
542    /// Returns an error if the strategy is already registered or the trader is in an invalid
543    /// state for strategy registration.
544    pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
545    where
546        T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
547    {
548        let strategy_id = self
549            .kernel
550            .trader
551            .borrow()
552            .prepare_strategy_for_registration(&mut strategy)?;
553        let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
554
555        self.kernel.trader.borrow_mut().add_strategy(strategy)?;
556
557        if let Some(oms_type) = oms_type {
558            self.kernel
559                .exec_engine
560                .borrow_mut()
561                .register_oms_type(strategy_id, oms_type);
562        }
563
564        Ok(())
565    }
566
567    /// Adds the given strategies to the backtest engine. Stops at the first error.
568    ///
569    /// # Errors
570    ///
571    /// Returns an error if any strategy fails to register; preceding strategies remain registered.
572    pub fn add_strategies<T>(&mut self, strategies: Vec<T>) -> anyhow::Result<()>
573    where
574        T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
575    {
576        for strategy in strategies {
577            self.add_strategy(strategy)?;
578        }
579        Ok(())
580    }
581
582    /// Adds an execution algorithm to the backtest engine.
583    ///
584    /// # Errors
585    ///
586    /// Returns an error if the algorithm is already registered or the trader is running.
587    pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
588    where
589        T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
590    {
591        self.kernel
592            .trader
593            .borrow_mut()
594            .add_exec_algorithm(exec_algorithm)
595    }
596
597    /// Adds the given execution algorithms to the backtest engine. Stops at the first error.
598    ///
599    /// # Errors
600    ///
601    /// Returns an error if any execution algorithm fails to register; preceding algorithms remain
602    /// registered.
603    pub fn add_exec_algorithms<T>(&mut self, exec_algorithms: Vec<T>) -> anyhow::Result<()>
604    where
605        T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
606    {
607        for exec_algorithm in exec_algorithms {
608            self.add_exec_algorithm(exec_algorithm)?;
609        }
610        Ok(())
611    }
612
613    /// Run a backtest.
614    ///
615    /// Processes all data chronologically. When `streaming` is false (default),
616    /// finalizes the run via [`end`](Self::end). When `streaming` is true, the
617    /// run pauses without finalizing so additional data batches can be loaded.
618    /// Timer advancement stops at data exhaustion to avoid producing synthetic
619    /// events (e.g. zero-volume bars) past the current batch.
620    ///
621    /// Streaming workflow:
622    /// 1. Add initial data and strategies
623    /// 2. Loop: call `run(streaming=true)`, `clear_data()`, `add_data(next_batch)`
624    /// 3. After all batches: call `end()` to finalize
625    ///
626    /// # Errors
627    ///
628    /// Returns an error if the backtest encounters an unrecoverable state.
629    pub fn run(
630        &mut self,
631        start: Option<UnixNanos>,
632        end: Option<UnixNanos>,
633        run_config_id: Option<String>,
634        streaming: bool,
635    ) -> anyhow::Result<()> {
636        self.run_impl(start, end, run_config_id, streaming)?;
637
638        // Finalize on non-streaming runs, or when a shutdown was triggered
639        // at any point during the run (including the trailing settle, module,
640        // and flush callbacks that execute after the main data loop) so the
641        // trader and engines actually stop.
642        if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
643            self.end();
644        }
645
646        Ok(())
647    }
648
649    fn run_impl(
650        &mut self,
651        start: Option<UnixNanos>,
652        end: Option<UnixNanos>,
653        run_config_id: Option<String>,
654        streaming: bool,
655    ) -> anyhow::Result<()> {
656        anyhow::ensure!(
657            self.sorted,
658            "Data has been added but not sorted, call `engine.sort_data()` or use \
659             `engine.add_data(..., sort=true)` before running"
660        );
661
662        for exchange in self.venues.values() {
663            let exchange = exchange.borrow();
664            let book_type_has_depth = exchange.book_type() as u8 > BookType::L1_MBP as u8;
665            if !book_type_has_depth {
666                continue;
667            }
668
669            for instrument_id in exchange.instrument_ids() {
670                let has_data = self.has_data.contains(instrument_id);
671                let missing_book_data = !self.has_book_data.contains(instrument_id);
672                if has_data && missing_book_data {
673                    anyhow::bail!(
674                        "No order book data found for instrument '{instrument_id}' when `book_type` \
675                         is '{:?}'. Set the venue `book_type` to 'L1_MBP' (for top-of-book data \
676                         like quotes, trades, and bars) or provide order book data for this \
677                         instrument.",
678                        exchange.book_type()
679                    );
680                }
681            }
682        }
683
684        // Determine time boundaries
685        let start_ns = start.unwrap_or_else(|| self.ts_first.unwrap_or_default());
686        let end_ns = end.unwrap_or_else(|| {
687            self.ts_last_data
688                .unwrap_or(UnixNanos::from(4_102_444_800_000_000_000u64))
689        });
690        anyhow::ensure!(start_ns <= end_ns, "start was > end");
691        self.end_ns = end_ns;
692        self.last_ns = start_ns;
693        self.last_module_ns = None;
694
695        // Set all component clocks to start
696        let clocks = self.collect_all_clocks();
697        Self::set_all_clocks_time(&clocks, start_ns);
698
699        // First-iteration initialization
700        if self.iteration == 0 {
701            self.set_instrument_expiration_timers()?;
702
703            self.run_config_id = run_config_id;
704            self.run_id = Some(UUID4::new());
705            self.run_started = Some(UnixNanos::from(std::time::SystemTime::now()));
706            self.backtest_start = Some(start_ns);
707
708            for exchange in self.venues.values() {
709                let mut ex = exchange.borrow_mut();
710                ex.initialize_account();
711                ex.load_open_orders();
712            }
713
714            // Re-set clocks after account init
715            Self::set_all_clocks_time(&clocks, start_ns);
716
717            // Reset force stop flag
718            self.force_stop = false;
719            self.kernel.reset_shutdown_flag();
720
721            // Initialize sync command senders (once per thread)
722            Self::init_command_senders();
723
724            // Set logging to static clock mode for deterministic timestamps
725            logging_clock_set_static_mode();
726            logging_clock_set_static_time(start_ns.as_u64());
727
728            // Start kernel, then stop before trader startup for event-store replay
729            self.kernel.start();
730            if self.kernel.is_event_store_replay() {
731                self.log_pre_run();
732                return Ok(());
733            }
734
735            if self.kernel.is_event_store_replay_configured() {
736                anyhow::bail!("event-store replay did not start");
737            }
738            self.kernel.start_trader();
739
740            self.log_pre_run();
741        }
742
743        self.log_run();
744
745        // Skip data before start_ns
746        let mut data = self.data_iterator.next_item();
747        while let Some(ref d) = data {
748            if d.ts_init() >= start_ns {
749                break;
750            }
751            data = self.data_iterator.next_item();
752        }
753
754        // Initialize last_ns before first data point
755        if let Some(ref d) = data {
756            let ts = d.ts_init();
757            self.last_ns = if ts.as_u64() > 0 {
758                UnixNanos::from(ts.as_u64() - 1)
759            } else {
760                UnixNanos::default()
761            };
762        } else {
763            self.last_ns = start_ns;
764        }
765
766        loop {
767            if self.kernel.is_shutdown_requested() {
768                log::info!("Shutdown requested via ShutdownSystem, ending backtest");
769                self.force_stop = true;
770            }
771
772            if self.force_stop {
773                log::info!("Force stop triggered, ending backtest");
774                break;
775            }
776
777            if data.is_none() {
778                if streaming {
779                    // In streaming mode, don't advance timers past the
780                    // current batch. The next batch will provide more data
781                    // and timers will fire naturally as time advances.
782                    break;
783                }
784                let done = self.process_next_timer(&clocks);
785                data = self.data_iterator.next_item();
786                if data.is_none() && done {
787                    break;
788                }
789                continue;
790            }
791
792            let d = data.as_ref().unwrap();
793            let ts_init = d.ts_init();
794
795            if ts_init > end_ns {
796                break;
797            }
798
799            if ts_init > self.last_ns {
800                self.last_ns = ts_init;
801                self.advance_time_impl(ts_init, &clocks);
802            }
803
804            // A timer fired during clock advance may have requested shutdown,
805            // skip delivering this data point in that case
806            if self.kernel.is_shutdown_requested() {
807                self.force_stop = true;
808                break;
809            }
810
811            self.route_data_to_exchange(d);
812            self.kernel.data_engine.borrow_mut().process_data(d.clone());
813
814            // Drain deferred commands, then process exchange queues
815            self.drain_command_queues();
816            self.settle_venues(ts_init);
817
818            let prev_last_ns = self.last_ns;
819            data = self.data_iterator.next_item();
820
821            // If timestamp changed (or exhausted), flush timers then run modules
822            if data.is_none() || data.as_ref().unwrap().ts_init() > prev_last_ns {
823                self.flush_accumulator_events(&clocks, prev_last_ns);
824                self.run_venue_modules(prev_last_ns);
825                self.run_venue_liquidations(prev_last_ns);
826            }
827
828            self.iteration += 1;
829        }
830
831        // Process remaining exchange messages
832        let ts_now = self.kernel.clock.borrow().timestamp_ns();
833        self.settle_venues(ts_now);
834        self.run_venue_modules(ts_now);
835        self.run_venue_liquidations(ts_now);
836
837        // Cap at last_ns when streaming or after shutdown to avoid firing
838        // timers past the current batch or the graceful stop
839        let flush_ts = if streaming || self.force_stop || self.kernel.is_shutdown_requested() {
840            self.last_ns
841        } else {
842            end_ns
843        };
844        self.flush_accumulator_events(&clocks, flush_ts);
845
846        Ok(())
847    }
848
849    /// Manually end the backtest.
850    pub fn end(&mut self) {
851        // Flush remaining timer events to the backtest end boundary so that
852        // tail alerts/expiries scheduled after the last data point still fire.
853        // Must run before stopping engines since DataEngine::stop() cancels
854        // bar aggregator timers. When a shutdown was requested, cap the flush
855        // at the last processed timestamp so timers scheduled past the stop
856        // point do not fire extra callbacks after the graceful stop request.
857        if self.end_ns.as_u64() > 0 {
858            let clocks = self.collect_all_clocks();
859            let flush_ts = if self.force_stop || self.kernel.is_shutdown_requested() {
860                self.last_ns
861            } else {
862                self.end_ns
863            };
864
865            self.flush_accumulator_events(&clocks, flush_ts);
866        }
867
868        self.kernel.stop_trader();
869
870        // Settle residual on_stop commands before stopping engines. Venue modules are
871        // not re-run; process_modules is once per timestamp.
872        let mut ts_now = self.kernel.clock.borrow().timestamp_ns();
873
874        // Drain first so latency-deferred commands reach venue inflight queues
875        self.drain_command_queues();
876
877        // Advance the clock to the latest inflight arrival; otherwise commands deferred
878        // by a LatencyModel sit past ts_now and never settle.
879        if let Some(max_inflight_ts) = self.max_inflight_command_ts()
880            && max_inflight_ts > ts_now
881        {
882            ts_now = max_inflight_ts;
883            let clocks = self.collect_all_clocks();
884            Self::set_all_clocks_time(&clocks, ts_now);
885        }
886
887        self.settle_venues(ts_now);
888
889        // Stop engines
890        self.kernel.data_engine.borrow_mut().stop();
891        self.kernel.risk_engine.borrow_mut().stop();
892        self.kernel.exec_engine.borrow_mut().stop();
893
894        self.run_finished = Some(UnixNanos::from(std::time::SystemTime::now()));
895        self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
896
897        // Switch logging back to realtime mode
898        logging_clock_set_realtime_mode();
899
900        self.log_post_run();
901    }
902
903    /// Reset the backtest engine.
904    ///
905    /// All stateful fields are reset to their initial value. Data and instruments
906    /// persist across resets to enable repeated runs with different strategies.
907    pub fn reset(&mut self) {
908        log::debug!("Resetting");
909
910        if self.kernel.trader.borrow().is_running() {
911            self.end();
912        }
913
914        // Stop and reset engines
915        self.kernel.data_engine.borrow_mut().stop();
916        self.kernel.data_engine.borrow_mut().reset();
917
918        self.kernel.exec_engine.borrow_mut().stop();
919
920        // Reset exchanges before the exec engine wipes the cache so
921        // exchange.reset() can see the prior run's account.
922        for exchange in self.venues.values() {
923            exchange.borrow_mut().reset();
924        }
925        self.kernel.exec_engine.borrow_mut().reset();
926
927        self.kernel.risk_engine.borrow_mut().stop();
928        self.kernel.risk_engine.borrow_mut().reset();
929
930        self.kernel.order_emulator.reset();
931
932        // Reset trader
933        if let Err(e) = self.kernel.trader.borrow_mut().reset() {
934            log::error!("Error resetting trader: {e:?}");
935        }
936
937        self.kernel.portfolio.borrow_mut().reset();
938
939        // Clear run state
940        self.run_config_id = None;
941        self.run_id = None;
942        self.run_started = None;
943        self.run_finished = None;
944        self.backtest_start = None;
945        self.backtest_end = None;
946        self.iteration = 0;
947        self.force_stop = false;
948        self.last_ns = UnixNanos::default();
949        self.last_module_ns = None;
950        self.last_liquidation_ns = None;
951        self.end_ns = UnixNanos::default();
952
953        self.accumulator.clear();
954
955        // Reset all iterator cursors to beginning (data persists)
956        self.data_iterator.reset_all_cursors();
957
958        log::info!("Reset");
959    }
960
961    /// Sort the engine's internal data stream by timestamp.
962    ///
963    /// Useful when data has been added with `sort=false` for batch performance,
964    /// then sorted once before running.
965    pub fn sort_data(&mut self) {
966        // Each add call creates its own stream; the iterator merges streams by
967        // replay timestamp across streams. Mark the engine as sorted so `run`
968        // no longer rejects it.
969        self.sorted = true;
970        log::info!("Data sort requested (iterator merges streams by replay timestamp)");
971    }
972
973    /// Clear the engine's internal data stream. Does not clear instruments.
974    pub fn clear_data(&mut self) {
975        self.has_data.clear();
976        self.has_book_data.clear();
977        self.data_iterator = BacktestDataIterator::new();
978        self.data_len = 0;
979        self.data_stream_counter = 0;
980        self.ts_first = None;
981        self.ts_last_data = None;
982        self.sorted = true;
983    }
984
985    /// Clear all actors from the engine's internal trader.
986    ///
987    /// # Errors
988    ///
989    /// Returns an error if any actor fails to dispose.
990    pub fn clear_actors(&mut self) -> anyhow::Result<()> {
991        self.kernel.trader.borrow_mut().clear_actors()
992    }
993
994    /// Clear all trading strategies from the engine's internal trader.
995    ///
996    /// # Errors
997    ///
998    /// Returns an error if any strategy fails to dispose.
999    pub fn clear_strategies(&mut self) -> anyhow::Result<()> {
1000        self.kernel.trader.borrow_mut().clear_strategies()
1001    }
1002
1003    /// Clear all execution algorithms from the engine's internal trader.
1004    ///
1005    /// # Errors
1006    ///
1007    /// Returns an error if any execution algorithm fails to dispose.
1008    pub fn clear_exec_algorithms(&mut self) -> anyhow::Result<()> {
1009        self.kernel.trader.borrow_mut().clear_exec_algorithms()
1010    }
1011
1012    /// Dispose of the backtest engine, releasing all resources.
1013    pub fn dispose(&mut self) {
1014        self.clear_data();
1015        self.accumulator.clear();
1016        self.kernel.dispose();
1017    }
1018
1019    /// Return the backtest result from the last run.
1020    #[must_use]
1021    pub fn get_result(&self) -> BacktestResult {
1022        let elapsed_time_secs = match (self.backtest_start, self.backtest_end) {
1023            (Some(start), Some(end)) => (end.as_f64() - start.as_f64()) / 1_000_000_000.0,
1024            _ => 0.0,
1025        };
1026
1027        let cache = self.kernel.cache.borrow();
1028        let orders = cache.orders(None, None, None, None, None);
1029        let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1030        let total_orders = orders.len();
1031        let positions: Vec<Position> = cache
1032            .positions(None, None, None, None, None)
1033            .into_iter()
1034            .map(|p| p.cloned())
1035            .collect();
1036        let cached_positions_count = positions.len();
1037        let snapshot_positions = cache.position_snapshots(None, None).len();
1038        let total_positions = Self::total_positions_with_snapshots(&cache, cached_positions_count);
1039        let summary = self.build_result_summary(
1040            &cache,
1041            total_events,
1042            total_orders,
1043            cached_positions_count,
1044            snapshot_positions,
1045        );
1046
1047        let stats = self.kernel.portfolio.borrow().statistics();
1048        let stats_pnls = stats.pnls;
1049        let stats_returns = stats.returns;
1050        let stats_general = stats.general;
1051
1052        BacktestResult {
1053            trader_id: self.config.trader_id().to_string(),
1054            machine_id: self.kernel.machine_id.clone(),
1055            instance_id: self.instance_id,
1056            run_config_id: self.run_config_id.clone(),
1057            run_id: self.run_id,
1058            run_started: self.run_started,
1059            run_finished: self.run_finished,
1060            backtest_start: self.backtest_start,
1061            backtest_end: self.backtest_end,
1062            elapsed_time_secs,
1063            iterations: self.iteration,
1064            total_events,
1065            total_orders,
1066            total_positions,
1067            summary,
1068            stats_pnls,
1069            stats_returns,
1070            stats_general,
1071        }
1072    }
1073
1074    fn build_result_summary(
1075        &self,
1076        cache: &Cache,
1077        total_events: usize,
1078        total_orders: usize,
1079        cached_positions_count: usize,
1080        snapshot_positions: usize,
1081    ) -> AHashMap<String, String> {
1082        let mut summary = AHashMap::new();
1083        summary.insert("iterations".to_string(), self.iteration.to_string());
1084        summary.insert("total_events".to_string(), total_events.to_string());
1085        summary.insert("orders.total".to_string(), total_orders.to_string());
1086        summary.insert(
1087            "orders.open".to_string(),
1088            cache
1089                .orders_open_count(None, None, None, None, None)
1090                .to_string(),
1091        );
1092        summary.insert(
1093            "orders.closed".to_string(),
1094            cache
1095                .orders_closed_count(None, None, None, None, None)
1096                .to_string(),
1097        );
1098        summary.insert(
1099            "orders.emulated".to_string(),
1100            cache
1101                .orders_emulated_count(None, None, None, None, None)
1102                .to_string(),
1103        );
1104        summary.insert(
1105            "orders.inflight".to_string(),
1106            cache
1107                .orders_inflight_count(None, None, None, None, None)
1108                .to_string(),
1109        );
1110        summary.insert(
1111            "positions.total".to_string(),
1112            cached_positions_count.to_string(),
1113        );
1114        summary.insert(
1115            "positions.open".to_string(),
1116            cache
1117                .positions_open_count(None, None, None, None, None)
1118                .to_string(),
1119        );
1120        summary.insert(
1121            "positions.closed".to_string(),
1122            cache
1123                .positions_closed_count(None, None, None, None, None)
1124                .to_string(),
1125        );
1126        summary.insert(
1127            "positions.snapshots".to_string(),
1128            snapshot_positions.to_string(),
1129        );
1130        summary.insert(
1131            "positions.total_with_snapshots".to_string(),
1132            (cached_positions_count + snapshot_positions).to_string(),
1133        );
1134
1135        let mut venues: Vec<Venue> = self.venues.keys().copied().collect();
1136        venues.sort_by_key(ToString::to_string);
1137        summary.insert("venues.total".to_string(), venues.len().to_string());
1138
1139        for venue in venues {
1140            let Some(account) = cache.account_for_venue(&venue) else {
1141                continue;
1142            };
1143
1144            let venue_key = venue.to_string();
1145            let account_key = format!("account.{venue_key}");
1146            summary.insert(format!("{account_key}.id"), account.id().to_string());
1147            summary.insert(
1148                format!("{account_key}.type"),
1149                account.account_type().to_string(),
1150            );
1151            summary.insert(
1152                format!("{account_key}.base_currency"),
1153                account
1154                    .base_currency()
1155                    .map_or_else(|| "None".to_string(), |currency| currency.code.to_string()),
1156            );
1157            summary.insert(
1158                format!("{account_key}.event_count"),
1159                account.event_count().to_string(),
1160            );
1161
1162            let mut balances: Vec<_> = account.balances().into_iter().collect();
1163            balances.sort_by_key(|(currency, _)| currency.code.to_string());
1164
1165            for (currency, balance) in balances {
1166                let balance_key = format!("{account_key}.balance.{}", currency.code);
1167                summary.insert(format!("{balance_key}.total"), balance.total.to_string());
1168                summary.insert(format!("{balance_key}.free"), balance.free.to_string());
1169                summary.insert(format!("{balance_key}.locked"), balance.locked.to_string());
1170            }
1171        }
1172
1173        summary
1174    }
1175
1176    fn route_data_to_exchange(&self, data: &Data) {
1177        if matches!(
1178            data,
1179            Data::MarkPriceUpdate(_)
1180                | Data::IndexPriceUpdate(_)
1181                | Data::OptionGreeks(_)
1182                | Data::Custom(_)
1183        ) {
1184            return;
1185        }
1186        #[cfg(feature = "defi")]
1187        if matches!(data, Data::Defi(_)) {
1188            return;
1189        }
1190
1191        let venue = data.instrument_id().venue;
1192        if let Some(exchange) = self.venues.get(&venue) {
1193            let mut exchange_ref = exchange.borrow_mut();
1194
1195            match data {
1196                Data::Delta(delta) => exchange_ref.process_order_book_delta(*delta),
1197                Data::Deltas(deltas) => exchange_ref.process_order_book_deltas(deltas),
1198                Data::Depth10(depth) => exchange_ref.process_order_book_depth10(depth),
1199                Data::Quote(quote) => exchange_ref.process_quote_tick(quote),
1200                Data::Trade(trade) => exchange_ref.process_trade_tick(trade),
1201                Data::Bar(bar) => exchange_ref.process_bar(*bar),
1202                Data::InstrumentStatus(status) => exchange_ref.process_instrument_status(*status),
1203                Data::InstrumentClose(close) => exchange_ref.process_instrument_close(*close),
1204                Data::FundingRateUpdate(funding) => {
1205                    let settlement_ns = exchange_ref.process_funding_rate(*funding);
1206                    drop(exchange_ref);
1207                    self.schedule_funding_settlement_if_required(
1208                        exchange,
1209                        funding.instrument_id,
1210                        settlement_ns,
1211                    );
1212                }
1213                _ => {}
1214            }
1215        } else {
1216            log::warn!("No exchange found for venue {venue}, data not routed");
1217        }
1218    }
1219
1220    fn advance_time_impl(&mut self, ts_now: UnixNanos, clocks: &[Rc<RefCell<dyn Clock>>]) {
1221        for clock in clocks {
1222            Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1223        }
1224
1225        // Process events with ts_event < ts_now
1226        let ts_before = if ts_now.as_u64() > 0 {
1227            UnixNanos::from(ts_now.as_u64() - 1)
1228        } else {
1229            UnixNanos::default()
1230        };
1231
1232        let mut ts_last: Option<UnixNanos> = None;
1233        let mut shutdown_at: Option<UnixNanos> = None;
1234
1235        while let Some(handler) = self.accumulator.pop_next_at_or_before(ts_before) {
1236            let ts_event = handler.event.ts_event;
1237
1238            // Settle previous timestamp batch before advancing
1239            if let Some(ts) = ts_last
1240                && ts != ts_event
1241            {
1242                self.settle_venues(ts);
1243                self.run_venue_modules(ts);
1244                self.run_venue_liquidations(ts);
1245            }
1246
1247            ts_last = Some(ts_event);
1248            Self::set_all_clocks_time(clocks, ts_event);
1249            logging_clock_set_static_time(ts_event.as_u64());
1250
1251            handler.run();
1252            self.drain_command_queues();
1253
1254            // Drop queued events on a handler-triggered shutdown so no later
1255            // timer fires after the graceful stop
1256            if self.kernel.is_shutdown_requested() {
1257                self.accumulator.clear();
1258                shutdown_at = Some(ts_event);
1259                break;
1260            }
1261
1262            // Re-advance clocks to capture chained timers
1263            for clock in clocks {
1264                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1265            }
1266        }
1267
1268        // Settle the last timestamp batch
1269        if let Some(ts) = ts_last {
1270            self.settle_venues(ts);
1271            self.run_venue_modules(ts);
1272            self.run_venue_liquidations(ts);
1273        }
1274
1275        // On a mid-drain shutdown, anchor state at the firing timer's ts so
1276        // post-run settlement and backtest_end reflect the graceful stop
1277        if let Some(ts_event) = shutdown_at {
1278            self.last_ns = ts_event;
1279        } else {
1280            Self::set_all_clocks_time(clocks, ts_now);
1281            logging_clock_set_static_time(ts_now.as_u64());
1282        }
1283    }
1284
1285    fn flush_accumulator_events(&mut self, clocks: &[Rc<RefCell<dyn Clock>>], ts_now: UnixNanos) {
1286        // Bail after shutdown so handler-scheduled alerts do not fire post-stop
1287        if self.kernel.is_shutdown_requested() {
1288            self.accumulator.clear();
1289            return;
1290        }
1291
1292        for clock in clocks {
1293            Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1294        }
1295
1296        let mut ts_last: Option<UnixNanos> = None;
1297
1298        while let Some(handler) = self.accumulator.pop_next_at_or_before(ts_now) {
1299            let ts_event = handler.event.ts_event;
1300
1301            // Settle previous timestamp batch before advancing
1302            if let Some(ts) = ts_last
1303                && ts != ts_event
1304            {
1305                self.settle_venues(ts);
1306                self.run_venue_modules(ts);
1307                self.run_venue_liquidations(ts);
1308            }
1309
1310            ts_last = Some(ts_event);
1311            Self::set_all_clocks_time(clocks, ts_event);
1312            logging_clock_set_static_time(ts_event.as_u64());
1313
1314            handler.run();
1315            self.drain_command_queues();
1316
1317            // Drop queued events on a handler-triggered shutdown so no later
1318            // timer fires after the graceful stop
1319            if self.kernel.is_shutdown_requested() {
1320                self.accumulator.clear();
1321                break;
1322            }
1323
1324            // Re-advance clocks to capture chained timers
1325            for clock in clocks {
1326                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1327            }
1328        }
1329
1330        // Settle the last timestamp batch
1331        if let Some(ts) = ts_last {
1332            self.settle_venues(ts);
1333            self.run_venue_modules(ts);
1334            self.run_venue_liquidations(ts);
1335        }
1336    }
1337
1338    fn process_next_timer(&mut self, clocks: &[Rc<RefCell<dyn Clock>>]) -> bool {
1339        self.flush_accumulator_events(clocks, self.last_ns);
1340
1341        // Find minimum next timer time across all component clocks
1342        let mut min_next_time: Option<UnixNanos> = None;
1343
1344        for clock in clocks {
1345            let clock_ref = clock.borrow();
1346            for name in clock_ref.timer_names() {
1347                if let Some(next_time) = clock_ref.next_time_ns(name)
1348                    && next_time > self.last_ns
1349                {
1350                    min_next_time = Some(match min_next_time {
1351                        Some(current_min) => next_time.min(current_min),
1352                        None => next_time,
1353                    });
1354                }
1355            }
1356        }
1357
1358        match min_next_time {
1359            None => true,
1360            Some(t) if t > self.end_ns => true,
1361            Some(t) => {
1362                self.last_ns = t;
1363                self.flush_accumulator_events(clocks, t);
1364                false
1365            }
1366        }
1367    }
1368
1369    fn set_instrument_expiration_timers(&self) -> anyhow::Result<()> {
1370        for exchange in self.venues.values() {
1371            let expirations = exchange.borrow().instrument_expirations();
1372            for (instrument_id, expiration_ns) in expirations {
1373                self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
1374            }
1375        }
1376
1377        Ok(())
1378    }
1379
1380    fn set_instrument_expiration_timer(
1381        &self,
1382        exchange: &Rc<RefCell<SimulatedExchange>>,
1383        instrument_id: InstrumentId,
1384        expiration_ns: UnixNanos,
1385    ) -> anyhow::Result<()> {
1386        if expiration_ns == UnixNanos::default() {
1387            return Ok(());
1388        }
1389
1390        let timer_name = Self::instrument_expiration_timer_name(instrument_id.venue, expiration_ns);
1391        let timer_key = ustr::Ustr::from(timer_name.as_str());
1392        if self.kernel.clock.borrow().timer_exists(&timer_key) {
1393            return Ok(());
1394        }
1395
1396        let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1397        let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1398            if let Some(exchange) = exchange.upgrade() {
1399                exchange
1400                    .borrow_mut()
1401                    .process_instrument_expirations(event.ts_event);
1402            }
1403        });
1404        let mut clock = self.kernel.clock.borrow_mut();
1405
1406        clock.set_time_alert_ns(
1407            &timer_name,
1408            expiration_ns,
1409            Some(TimeEventCallback::from(callback)),
1410            None,
1411        )?;
1412
1413        Ok(())
1414    }
1415
1416    fn instrument_expiration_timer_name(venue: Venue, expiration_ns: UnixNanos) -> String {
1417        format!("INSTRUMENT-EXPIRATION:{venue}:{expiration_ns}")
1418    }
1419
1420    fn schedule_funding_settlement_if_required(
1421        &self,
1422        exchange: &Rc<RefCell<SimulatedExchange>>,
1423        instrument_id: InstrumentId,
1424        settlement_ns: Option<UnixNanos>,
1425    ) {
1426        let Some(settlement_ns) = settlement_ns else {
1427            return;
1428        };
1429
1430        if let Err(e) = self.set_funding_settlement_timer(exchange, instrument_id, settlement_ns) {
1431            log::error!("Cannot schedule funding settlement for {instrument_id}: {e}");
1432        }
1433    }
1434
1435    fn set_funding_settlement_timer(
1436        &self,
1437        exchange: &Rc<RefCell<SimulatedExchange>>,
1438        instrument_id: InstrumentId,
1439        settlement_ns: UnixNanos,
1440    ) -> anyhow::Result<()> {
1441        let timer_name = Self::funding_settlement_timer_name(instrument_id);
1442        let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1443        let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1444            if let Some(exchange) = exchange.upgrade() {
1445                exchange
1446                    .borrow_mut()
1447                    .process_funding_settlement(instrument_id, event.ts_event);
1448            }
1449        });
1450        let mut clock = self.kernel.clock.borrow_mut();
1451
1452        clock.set_time_alert_ns(
1453            &timer_name,
1454            settlement_ns,
1455            Some(TimeEventCallback::from(callback)),
1456            None,
1457        )?;
1458
1459        Ok(())
1460    }
1461
1462    fn funding_settlement_timer_name(instrument_id: InstrumentId) -> String {
1463        format!("FUNDING-SETTLEMENT:{instrument_id}")
1464    }
1465
1466    fn collect_all_clocks(&self) -> Vec<Rc<RefCell<dyn Clock>>> {
1467        let mut clocks = vec![self.kernel.clock.clone()];
1468        clocks.extend(self.kernel.trader.borrow().get_component_clocks());
1469        clocks
1470    }
1471
1472    fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
1473        self.venues
1474            .values()
1475            .filter_map(|v| v.borrow().max_inflight_command_ts())
1476            .max()
1477    }
1478
1479    fn settle_venues(&self, ts_now: UnixNanos) {
1480        // Advance venue clocks so modules and event generators see the
1481        // correct timestamp even when no commands are pending
1482        for exchange in self.venues.values() {
1483            exchange.borrow().set_clock_time(ts_now);
1484        }
1485
1486        // Drain commands then iterate matching engines to fill newly added
1487        // orders. Fills may enqueue further commands (e.g. hedge orders
1488        // submitted from on_order_filled), so loop until quiescent.
1489        // Only process and iterate venues that had pending commands each
1490        // pass, to avoid extra fill-model rolls on untouched venues.
1491        loop {
1492            // Drain first so commands buffered in the trading queue (e.g. from
1493            // on_stop handlers) reach the venues before we check for activity.
1494            self.drain_command_queues();
1495
1496            let active_venues: Vec<Venue> = self
1497                .venues
1498                .iter()
1499                .filter(|(_, ex)| ex.borrow().has_pending_commands(ts_now))
1500                .map(|(id, _)| *id)
1501                .collect();
1502
1503            if active_venues.is_empty() {
1504                break;
1505            }
1506
1507            for venue_id in &active_venues {
1508                self.venues[venue_id].borrow_mut().process(ts_now);
1509            }
1510            self.drain_command_queues();
1511
1512            for venue_id in &active_venues {
1513                self.venues[venue_id]
1514                    .borrow_mut()
1515                    .iterate_matching_engines(ts_now);
1516            }
1517
1518            // Drain again so fill-triggered commands (e.g. hedge orders
1519            // from on_order_filled) are visible to has_pending_commands
1520            self.drain_command_queues();
1521        }
1522    }
1523
1524    fn run_venue_modules(&mut self, ts_now: UnixNanos) {
1525        if self.last_module_ns == Some(ts_now) {
1526            return;
1527        }
1528        self.last_module_ns = Some(ts_now);
1529
1530        // Pre-settle handler-generated work so modules see final state
1531        self.drain_command_queues();
1532        self.settle_venues(ts_now);
1533
1534        for exchange in self.venues.values() {
1535            exchange.borrow_mut().process_modules(ts_now);
1536        }
1537
1538        // Post-settle any commands emitted by modules
1539        self.drain_command_queues();
1540        self.settle_venues(ts_now);
1541    }
1542
1543    fn run_venue_liquidations(&mut self, ts_now: UnixNanos) {
1544        if self.last_liquidation_ns == Some(ts_now) {
1545            return;
1546        }
1547        self.last_liquidation_ns = Some(ts_now);
1548
1549        for exchange in self.venues.values() {
1550            exchange.borrow_mut().process_liquidations(ts_now);
1551        }
1552
1553        self.drain_command_queues();
1554        self.settle_venues(ts_now);
1555    }
1556
1557    fn drain_exec_client_events(&self) {
1558        for client in &self.exec_clients {
1559            client.drain_queued_events();
1560        }
1561    }
1562
1563    fn drain_command_queues(&self) {
1564        // Drain trading commands, exec client events, and data commands
1565        // in a loop until all queues settle. Handles cascading re-entrancy
1566        // (e.g. strategy submits order from on_order_filled).
1567        loop {
1568            drain_trading_cmd_queue();
1569            drain_data_cmd_queue();
1570            self.drain_exec_client_events();
1571
1572            if trading_cmd_queue_is_empty() && data_cmd_queue_is_empty() {
1573                break;
1574            }
1575        }
1576    }
1577
1578    fn init_command_senders() {
1579        replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1580        replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1581    }
1582
1583    fn advance_clock_on_accumulator(
1584        accumulator: &mut TimeEventAccumulator,
1585        clock: &Rc<RefCell<dyn Clock>>,
1586        to_time_ns: UnixNanos,
1587        set_time: bool,
1588    ) {
1589        let mut clock_ref = clock.borrow_mut();
1590        let test_clock = clock_ref
1591            .as_any_mut()
1592            .downcast_mut::<TestClock>()
1593            .expect("BacktestEngine requires TestClock");
1594        accumulator.advance_clock(test_clock, to_time_ns, set_time);
1595    }
1596
1597    fn set_all_clocks_time(clocks: &[Rc<RefCell<dyn Clock>>], ts: UnixNanos) {
1598        for clock in clocks {
1599            let mut clock_ref = clock.borrow_mut();
1600            let test_clock = clock_ref
1601                .as_any_mut()
1602                .downcast_mut::<TestClock>()
1603                .expect("BacktestEngine requires TestClock");
1604            test_clock.set_time(ts);
1605        }
1606    }
1607
1608    #[rustfmt::skip]
1609    fn log_pre_run(&self) {
1610        log_info!("=================================================================", color = LogColor::Cyan);
1611        log_info!(" BACKTEST PRE-RUN", color = LogColor::Cyan);
1612        log_info!("=================================================================", color = LogColor::Cyan);
1613
1614        let cache = self.kernel.cache.borrow();
1615        for exchange in self.venues.values() {
1616            let ex = exchange.borrow();
1617            log_info!("=================================================================", color = LogColor::Cyan);
1618            log::info!(" SimulatedVenue {} ({})", ex.id, ex.account_type);
1619            log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1620
1621            if let Some(account) = cache.account_for_venue(&ex.id) {
1622                log::info!("Balances starting:");
1623                let account_ref: &dyn Account = match &*account {
1624                    AccountAny::Margin(margin) => margin,
1625                    AccountAny::Cash(cash) => cash,
1626                    AccountAny::Betting(betting) => betting,
1627                };
1628
1629                for balance in account_ref.starting_balances().values() {
1630                    log::info!("  {balance}");
1631                }
1632            }
1633        }
1634
1635        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1636    }
1637
1638    #[rustfmt::skip]
1639    fn log_run(&self) {
1640        let config_id = self.run_config_id.as_deref().unwrap_or("None");
1641        let id = format_optional_uuid(self.run_id.as_ref());
1642        let start = format_optional_nanos(self.backtest_start);
1643
1644        log_info!("=================================================================", color = LogColor::Cyan);
1645        log_info!(" BACKTEST RUN", color = LogColor::Cyan);
1646        log_info!("=================================================================", color = LogColor::Cyan);
1647        log::info!("Run config ID:  {config_id}");
1648        log::info!("Run ID:         {id}");
1649        log::info!("Backtest start: {start}");
1650        log::info!("Data elements:  {}", self.data_len);
1651        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1652    }
1653
1654    #[rustfmt::skip]
1655    fn log_post_run(&self) {
1656        let cache = self.kernel.cache.borrow();
1657        let orders = cache.orders(None, None, None, None, None);
1658        let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1659        let total_orders = orders.len();
1660        let positions: Vec<Position> = cache
1661            .positions(None, None, None, None, None)
1662            .into_iter()
1663            .map(|p| p.cloned())
1664            .collect();
1665        let total_positions = Self::total_positions_with_snapshots(&cache, positions.len());
1666
1667        let config_id = self.run_config_id.as_deref().unwrap_or("None");
1668        let id = format_optional_uuid(self.run_id.as_ref());
1669        let started = format_optional_nanos(self.run_started);
1670        let finished = format_optional_nanos(self.run_finished);
1671        let elapsed = format_optional_duration(self.run_started, self.run_finished);
1672        let bt_start = format_optional_nanos(self.backtest_start);
1673        let bt_end = format_optional_nanos(self.backtest_end);
1674        let bt_range = format_optional_duration(self.backtest_start, self.backtest_end);
1675        let iterations = self.iteration.separate_with_underscores();
1676        let events = total_events.separate_with_underscores();
1677        let num_orders = total_orders.separate_with_underscores();
1678        let num_positions = total_positions.separate_with_underscores();
1679
1680        log_info!("=================================================================", color = LogColor::Cyan);
1681        log_info!(" BACKTEST POST-RUN", color = LogColor::Cyan);
1682        log_info!("=================================================================", color = LogColor::Cyan);
1683        log::info!("Run config ID:  {config_id}");
1684        log::info!("Run ID:         {id}");
1685        log::info!("Run started:    {started}");
1686        log::info!("Run finished:   {finished}");
1687        log::info!("Elapsed time:   {elapsed}");
1688        log::info!("Backtest start: {bt_start}");
1689        log::info!("Backtest end:   {bt_end}");
1690        log::info!("Backtest range: {bt_range}");
1691        log::info!("Iterations: {iterations}");
1692        log::info!("Total events: {events}");
1693        log::info!("Total orders: {num_orders}");
1694        log::info!("Total positions: {num_positions}");
1695
1696        if !self.config.run_analysis {
1697            return;
1698        }
1699
1700        let accounts = cache.accounts_all_owned();
1701        let mut snapshots = Vec::new();
1702        for position in &positions {
1703            snapshots.extend(cache.position_snapshots(Some(&position.id), None));
1704        }
1705        let recorded = self.kernel.portfolio.borrow().recorded_realized_pnls();
1706        let analyzer = PortfolioAnalyzer::from_accounts(&accounts, &positions, &snapshots, recorded);
1707        log_portfolio_performance(&analyzer);
1708    }
1709
1710    fn total_positions_with_snapshots(cache: &Cache, cached_positions_count: usize) -> usize {
1711        cached_positions_count + cache.position_snapshots(None, None).len()
1712    }
1713
1714    /// Registers a data client for the given `client_id` if one does not already exist.
1715    pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
1716        if self
1717            .kernel
1718            .data_engine
1719            .borrow()
1720            .registered_clients()
1721            .contains(&client_id)
1722        {
1723            return;
1724        }
1725
1726        let venue = Venue::from(client_id.as_str());
1727        let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
1728        let data_client_adapter = DataClientAdapter::new(
1729            backtest_client.client_id,
1730            None,
1731            false,
1732            false,
1733            Box::new(backtest_client),
1734        );
1735
1736        self.kernel
1737            .data_engine
1738            .borrow_mut()
1739            .register_client(data_client_adapter, None);
1740    }
1741
1742    /// Registers a market data client for the given `venue` if one does not already exist.
1743    pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
1744        let client_id = ClientId::from(venue.as_str());
1745
1746        if !self
1747            .kernel
1748            .data_engine
1749            .borrow()
1750            .registered_clients()
1751            .contains(&client_id)
1752        {
1753            let backtest_client =
1754                BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
1755            let data_client_adapter = DataClientAdapter::new(
1756                client_id,
1757                Some(venue),
1758                false,
1759                false,
1760                Box::new(backtest_client),
1761            );
1762            self.kernel
1763                .data_engine
1764                .borrow_mut()
1765                .register_client(data_client_adapter, Some(venue));
1766        }
1767    }
1768}
1769
1770fn format_optional_nanos(nanos: Option<UnixNanos>) -> String {
1771    nanos.map_or("None".to_string(), unix_nanos_to_iso8601)
1772}
1773
1774fn format_optional_uuid(uuid: Option<&UUID4>) -> String {
1775    uuid.map_or("None".to_string(), ToString::to_string)
1776}
1777
1778fn event_count_as_usize(event_count: u64) -> usize {
1779    usize::try_from(event_count).expect("execution event count fits usize")
1780}
1781
1782fn format_optional_duration(start: Option<UnixNanos>, end: Option<UnixNanos>) -> String {
1783    match (start, end) {
1784        (Some(s), Some(e)) => {
1785            let delta = e.to_datetime_utc() - s.to_datetime_utc();
1786            let days = delta.num_days().abs();
1787            let hours = delta.num_hours().abs() % 24;
1788            let minutes = delta.num_minutes().abs() % 60;
1789            let seconds = delta.num_seconds().abs() % 60;
1790            let micros = delta.subsec_nanos().unsigned_abs() / 1_000;
1791            format!("{days} days {hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
1792        }
1793        _ => "None".to_string(),
1794    }
1795}
1796
1797#[rustfmt::skip]
1798fn log_portfolio_performance(analyzer: &PortfolioAnalyzer) {
1799    log_info!("=================================================================", color = LogColor::Cyan);
1800    log_info!(" PORTFOLIO PERFORMANCE", color = LogColor::Cyan);
1801    log_info!("=================================================================", color = LogColor::Cyan);
1802
1803    for currency in analyzer.currencies() {
1804        log::info!(" PnL Statistics ({})", currency.code);
1805        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1806
1807        if let Ok(pnl_lines) = analyzer.get_stats_pnls_formatted(Some(currency), None) {
1808            for line in &pnl_lines {
1809                log::info!("{line}");
1810            }
1811        }
1812
1813        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1814    }
1815
1816    log::info!(" Returns Statistics");
1817    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1818
1819    for line in &analyzer.get_stats_returns_formatted() {
1820        log::info!("{line}");
1821    }
1822    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1823
1824    log::info!(" General Statistics");
1825    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1826
1827    for line in &analyzer.get_stats_general_formatted() {
1828        log::info!("{line}");
1829    }
1830    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1831}
1832
1833#[cfg(test)]
1834mod tests {
1835    use nautilus_common::{
1836        actor::DataActor,
1837        enums::Environment,
1838        messages::{
1839            data::{DataCommand, UnsubscribeCommand},
1840            execution::{SubmitOrder, TradingCommand},
1841        },
1842        msgbus::{
1843            self, MessagingSwitchboard,
1844            stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1845        },
1846    };
1847    use nautilus_execution::engine::SnapshotAnchorer;
1848    use nautilus_model::{
1849        data::{Data, InstrumentStatus},
1850        enums::{
1851            AccountType, BookType, MarketStatus, MarketStatusAction, OmsType, OrderSide,
1852            OrderStatus, OrderType, TriggerType,
1853        },
1854        identifiers::{ClientId, PositionId, StrategyId, Venue},
1855        instruments::{
1856            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
1857        },
1858        orders::{Order, OrderAny, OrderTestBuilder},
1859        types::{Money, Price, Quantity},
1860    };
1861    use nautilus_system::{KernelEventStore, RegisteredComponents};
1862    use nautilus_trading::{
1863        nautilus_strategy,
1864        strategy::{config::StrategyConfig, core::StrategyCore},
1865    };
1866    use rstest::*;
1867    use ustr::Ustr;
1868
1869    use super::*;
1870
1871    #[derive(Debug)]
1872    struct BacktestReplayKernelEventStore {
1873        fail_restore: bool,
1874    }
1875
1876    impl KernelEventStore for BacktestReplayKernelEventStore {
1877        fn restore_parent_cache(
1878            &mut self,
1879            _instance_id: UUID4,
1880            _cache: &mut Cache,
1881        ) -> anyhow::Result<()> {
1882            if self.fail_restore {
1883                anyhow::bail!("replay restore failed");
1884            }
1885
1886            Ok(())
1887        }
1888
1889        fn open(
1890            &mut self,
1891            _instance_id: UUID4,
1892            _components: &RegisteredComponents,
1893            _environment: Environment,
1894        ) -> anyhow::Result<()> {
1895            Ok(())
1896        }
1897
1898        fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
1899            None
1900        }
1901
1902        fn seal(&mut self, _ts_init: UnixNanos) {}
1903
1904        fn run_id(&self) -> Option<&str> {
1905            Some("replay-child")
1906        }
1907
1908        fn parent_run_id(&self) -> Option<&str> {
1909            Some("seed-run")
1910        }
1911
1912        fn is_event_store_replay_configured(&self) -> bool {
1913            true
1914        }
1915
1916        fn is_halted(&self) -> bool {
1917            false
1918        }
1919    }
1920
1921    #[derive(Debug)]
1922    struct TestStrategy {
1923        core: StrategyCore,
1924    }
1925
1926    impl TestStrategy {
1927        fn new(config: StrategyConfig) -> Self {
1928            Self {
1929                core: StrategyCore::new(config),
1930            }
1931        }
1932    }
1933
1934    impl DataActor for TestStrategy {}
1935
1936    nautilus_strategy!(TestStrategy);
1937
1938    fn create_engine() -> BacktestEngine {
1939        let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
1940        let venue_config = SimulatedVenueConfig::builder()
1941            .venue(Venue::from("BINANCE"))
1942            .oms_type(OmsType::Netting)
1943            .account_type(AccountType::Margin)
1944            .book_type(BookType::L1_MBP)
1945            .starting_balances(vec![Money::from("1_000_000 USDT")])
1946            .build()
1947            .unwrap();
1948        engine.add_venue(venue_config).unwrap();
1949        engine
1950    }
1951
1952    #[rstest]
1953    fn test_add_strategy_registers_configured_hedging_oms_type() {
1954        let mut engine = create_engine();
1955        let instrument = crypto_perpetual_ethusdt();
1956        let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
1957
1958        engine
1959            .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
1960            .unwrap();
1961        engine
1962            .add_strategy(TestStrategy::new(StrategyConfig {
1963                strategy_id: Some(strategy_id),
1964                oms_type: Some(OmsType::Hedging),
1965                ..Default::default()
1966            }))
1967            .unwrap();
1968
1969        let order = OrderTestBuilder::new(OrderType::Market)
1970            .trader_id(engine.trader_id())
1971            .strategy_id(strategy_id)
1972            .instrument_id(instrument.id())
1973            .quantity(Quantity::from("1.000"))
1974            .build();
1975        let position_id = PositionId::new("CUSTOM-POSITION-001");
1976
1977        engine
1978            .kernel
1979            .exec_engine
1980            .borrow()
1981            .cache()
1982            .borrow_mut()
1983            .add_order(
1984                order.clone(),
1985                Some(position_id),
1986                Some(ClientId::from("BINANCE")),
1987                true,
1988            )
1989            .unwrap();
1990
1991        let submit_order = SubmitOrder::new(
1992            order.trader_id(),
1993            Some(ClientId::from("BINANCE")),
1994            strategy_id,
1995            instrument.id(),
1996            order.client_order_id(),
1997            order.init_event().clone(),
1998            order.exec_algorithm_id(),
1999            Some(position_id),
2000            None,
2001            UUID4::new(),
2002            UnixNanos::default(),
2003            None,
2004        );
2005
2006        engine
2007            .kernel
2008            .exec_engine
2009            .borrow()
2010            .execute(TradingCommand::SubmitOrder(submit_order));
2011
2012        let exec_engine = engine.kernel.exec_engine.borrow();
2013        let cache = exec_engine.cache().borrow();
2014        let cached_order = cache
2015            .order(&order.client_order_id())
2016            .expect("Order should be cached");
2017
2018        assert_eq!(cached_order.status(), OrderStatus::Initialized);
2019    }
2020
2021    fn create_engine_with_replay_store(fail_restore: bool) -> BacktestEngine {
2022        let config = BacktestEngineConfig {
2023            load_state: true,
2024            run_analysis: false,
2025            ..Default::default()
2026        };
2027        let mut engine = BacktestEngine::new(config.clone()).unwrap();
2028        let event_store_factory = move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
2029            Ok::<_, anyhow::Error>(Box::new(BacktestReplayKernelEventStore { fail_restore })
2030                as Box<dyn KernelEventStore>)
2031        };
2032
2033        engine.kernel = NautilusKernel::new_with(
2034            "BacktestEngine".to_string(),
2035            config,
2036            None,
2037            Some(Box::new(event_store_factory)),
2038        )
2039        .unwrap();
2040        engine.instance_id = engine.kernel.instance_id;
2041        engine
2042    }
2043
2044    fn create_stop_market_order(instrument: &CryptoPerpetual) -> OrderAny {
2045        OrderTestBuilder::new(OrderType::StopMarket)
2046            .instrument_id(instrument.id())
2047            .side(OrderSide::Buy)
2048            .trigger_price(Price::from("5100.00"))
2049            .quantity(Quantity::from(1))
2050            .emulation_trigger(TriggerType::BidAsk)
2051            .build()
2052    }
2053
2054    fn create_submit_order_command(order: &OrderAny) -> SubmitOrder {
2055        SubmitOrder::new(
2056            order.trader_id(),
2057            None,
2058            order.strategy_id(),
2059            order.instrument_id(),
2060            order.client_order_id(),
2061            order.init_event().clone(),
2062            order.exec_algorithm_id(),
2063            None,
2064            None,
2065            UUID4::new(),
2066            0.into(),
2067            None, // correlation_id
2068        )
2069    }
2070
2071    fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
2072        let (handler, saving_handler) =
2073            get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
2074        msgbus::register_data_command_endpoint(
2075            MessagingSwitchboard::data_engine_queue_execute(),
2076            handler,
2077        );
2078        saving_handler
2079    }
2080
2081    #[rstest]
2082    fn test_run_impl_event_store_replay_skips_trader_start() {
2083        let mut engine = create_engine_with_replay_store(false);
2084
2085        engine
2086            .run_impl(
2087                Some(UnixNanos::from(0)),
2088                Some(UnixNanos::from(1)),
2089                None,
2090                true,
2091            )
2092            .unwrap();
2093
2094        assert!(engine.kernel.is_event_store_replay_configured());
2095        assert!(engine.kernel.is_event_store_replay());
2096        assert!(!engine.kernel.trader.borrow().is_running());
2097    }
2098
2099    #[rstest]
2100    fn test_run_impl_event_store_replay_config_failure_errors() {
2101        let mut engine = create_engine_with_replay_store(true);
2102
2103        let error = engine
2104            .run_impl(
2105                Some(UnixNanos::from(0)),
2106                Some(UnixNanos::from(1)),
2107                None,
2108                true,
2109            )
2110            .unwrap_err();
2111
2112        assert_eq!(error.to_string(), "event-store replay did not start");
2113        assert!(engine.kernel.is_event_store_replay_configured());
2114        assert!(!engine.kernel.is_event_store_replay());
2115        assert!(!engine.kernel.trader.borrow().is_running());
2116    }
2117
2118    #[rstest]
2119    #[case(None)]
2120    #[case(Some(true))]
2121    #[case(Some(false))]
2122    fn test_new_forces_drop_instruments_on_reset_false(
2123        crypto_perpetual_ethusdt: CryptoPerpetual,
2124        #[case] user_value: Option<bool>,
2125    ) {
2126        use nautilus_common::cache::CacheConfig;
2127
2128        let config = match user_value {
2129            None => BacktestEngineConfig::builder().build(),
2130            Some(value) => BacktestEngineConfig::builder()
2131                .cache(
2132                    CacheConfig::builder()
2133                        .drop_instruments_on_reset(value)
2134                        .build()
2135                        .unwrap(),
2136                )
2137                .build(),
2138        };
2139        let mut engine = BacktestEngine::new(config).unwrap();
2140
2141        let venue_config = SimulatedVenueConfig::builder()
2142            .venue(Venue::from("BINANCE"))
2143            .oms_type(OmsType::Netting)
2144            .account_type(AccountType::Margin)
2145            .book_type(BookType::L1_MBP)
2146            .starting_balances(vec![Money::from("1_000_000 USDT")])
2147            .build()
2148            .unwrap();
2149        engine.add_venue(venue_config).unwrap();
2150
2151        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2152        let instrument_id = instrument.id();
2153        engine.add_instrument(&instrument).unwrap();
2154
2155        engine.reset();
2156
2157        assert!(
2158            engine
2159                .kernel()
2160                .cache
2161                .borrow()
2162                .instrument(&instrument_id)
2163                .is_some(),
2164            "instrument must survive engine.reset(); user-supplied \
2165             drop_instruments_on_reset={user_value:?} must not leak through",
2166        );
2167    }
2168
2169    #[rstest]
2170    fn test_reset_resets_order_emulator_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
2171        let mut engine = create_engine();
2172        let data_commands =
2173            register_data_command_handler("DataEngine.queue_execute.backtest_reset");
2174        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt.clone());
2175        let instrument_id = instrument.id();
2176        engine.add_instrument(&instrument).unwrap();
2177        let order = create_stop_market_order(&crypto_perpetual_ethusdt);
2178        let command = create_submit_order_command(&order);
2179        engine
2180            .kernel
2181            .cache
2182            .borrow_mut()
2183            .add_order(order, None, None, false)
2184            .unwrap();
2185        let order_emulator = engine.kernel.order_emulator.emulator();
2186        let mut order_emulator = order_emulator.borrow_mut();
2187        order_emulator.cache_submit_order_command(command.clone());
2188        order_emulator.handle_submit_order(&command);
2189        drop(order_emulator);
2190        data_commands.clear();
2191
2192        engine.reset();
2193
2194        let commands = data_commands.get_messages();
2195        let emulator = engine.kernel.order_emulator.get_emulator();
2196        assert!(emulator.subscribed_quotes().is_empty());
2197        assert!(emulator.subscribed_trades().is_empty());
2198        assert!(emulator.get_matching_core(&instrument_id).is_none());
2199        assert!(commands.iter().any(|command| matches!(
2200            command,
2201            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
2202                if command.instrument_id == instrument_id
2203        )));
2204    }
2205
2206    #[rstest]
2207    fn test_route_data_to_exchange_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
2208        let mut engine = create_engine();
2209        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2210        let instrument_id = instrument.id();
2211        engine.add_instrument(&instrument).unwrap();
2212
2213        let status = InstrumentStatus::new(
2214            instrument_id,
2215            MarketStatusAction::Close,
2216            UnixNanos::from(1),
2217            UnixNanos::from(1),
2218            None,
2219            None,
2220            None,
2221            None,
2222            None,
2223        );
2224
2225        engine.route_data_to_exchange(&Data::InstrumentStatus(status));
2226
2227        let exchange = engine.venues.get(&instrument_id.venue).unwrap().borrow();
2228        let market_status = exchange
2229            .get_matching_engine(&instrument_id)
2230            .unwrap()
2231            .market_status;
2232        assert_eq!(market_status, MarketStatus::Closed);
2233    }
2234}