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 indexmap::IndexMap;
28use nautilus_analysis::analyzer::PortfolioAnalyzer;
29use nautilus_common::{
30    actor::{DataActor, DataActorNative},
31    cache::Cache,
32    clock::{Clock, TestClock},
33    component::Component,
34    enums::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    UUID4, UnixNanos, datetime::unix_nanos_to_iso8601, string::formatting::Separable,
49    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, HasTsInit},
56    enums::{AccountType, AggregationSource, BookType},
57    identifiers::{AccountId, ClientId, InstrumentId, TraderId, Venue},
58    instruments::{Instrument, InstrumentAny},
59    position::Position,
60    types::Price,
61};
62#[cfg(feature = "python")]
63use nautilus_system::trader::Trader;
64use nautilus_system::{config::NautilusKernelConfig, kernel::NautilusKernel};
65use nautilus_trading::{
66    ExecutionAlgorithm, ExecutionAlgorithmNative,
67    strategy::{Strategy, StrategyNative},
68};
69
70use crate::{
71    accumulator::TimeEventAccumulator,
72    config::{BacktestEngineConfig, SimulatedVenueConfig},
73    data_client::BacktestDataClient,
74    data_iterator::BacktestDataIterator,
75    exchange::SimulatedExchange,
76    execution_client::BacktestExecutionClient,
77    result::{
78        BacktestResult, CanonicalBacktestResult, CanonicalBacktestState, CanonicalDiagnostic,
79        CanonicalDiagnosticCode, CanonicalRunOutcome,
80    },
81};
82
83/// Core backtesting engine for running event-driven strategy backtests on historical data.
84///
85/// The `BacktestEngine` provides a high-fidelity simulation environment that processes
86/// historical market data chronologically through an event-driven architecture. It maintains
87/// simulated exchanges with realistic order matching and execution, allowing strategies
88/// to be tested exactly as they would run in live trading:
89///
90/// - Event-driven data replay with configurable latency models.
91/// - Multi-venue and multi-asset support.
92/// - Realistic order matching and execution simulation.
93/// - Strategy and portfolio performance analysis.
94/// - Transition from backtesting to live trading.
95pub struct BacktestEngine {
96    kernel: NautilusKernel,
97    instance_id: UUID4,
98    config: BacktestEngineConfig,
99    accumulator: TimeEventAccumulator,
100    run_config_id: Option<String>,
101    run_id: Option<UUID4>,
102    venues: IndexMap<Venue, Rc<RefCell<SimulatedExchange>>>,
103    exec_clients: Vec<BacktestExecutionClient>,
104    has_data: AHashSet<InstrumentId>,
105    has_book_data: AHashSet<InstrumentId>,
106    has_book_processed: AHashSet<InstrumentId>,
107    data_iterator: BacktestDataIterator,
108    data_len: usize,
109    data_stream_counter: usize,
110    ts_first: Option<UnixNanos>,
111    ts_last_data: Option<UnixNanos>,
112    sorted: bool,
113    iteration: usize,
114    force_stop: bool,
115    last_ns: UnixNanos,
116    last_module_ns: Option<UnixNanos>,
117    last_liquidation_ns: Option<UnixNanos>,
118    end_ns: UnixNanos,
119    run_started: Option<UnixNanos>,
120    run_finished: Option<UnixNanos>,
121    backtest_start: Option<UnixNanos>,
122    backtest_end: Option<UnixNanos>,
123    funding_error: Option<String>,
124}
125
126impl Debug for BacktestEngine {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct(stringify!(BacktestEngine))
129            .field("instance_id", &self.instance_id)
130            .field("run_config_id", &self.run_config_id)
131            .field("run_id", &self.run_id)
132            .finish_non_exhaustive()
133    }
134}
135
136impl BacktestEngine {
137    /// Create a new [`BacktestEngine`] instance.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the core `NautilusKernel` fails to initialize.
142    pub fn new(mut config: BacktestEngineConfig) -> anyhow::Result<Self> {
143        // The engine does not replay `add_instrument` on reset, so reruns rely
144        // on the cache retaining instruments regardless of the caller's config.
145        let mut cache_config = config.cache.unwrap_or_default();
146        cache_config.drop_instruments_on_reset = false;
147        config.cache = Some(cache_config);
148        let kernel = NautilusKernel::new("BacktestEngine".to_string(), config.clone())?;
149        let instance_id = kernel.instance_id;
150        #[cfg(feature = "python")]
151        if let Some(controller) = config.controller.as_ref() {
152            Trader::add_controller_from_importable_config(&kernel.trader, controller)?;
153        }
154        #[cfg(not(feature = "python"))]
155        if let Some(controller) = config.controller.as_ref() {
156            anyhow::bail!(
157                "BacktestEngineConfig.controller for importable controller '{}' requires the python feature",
158                controller.controller_path
159            );
160        }
161
162        Ok(Self {
163            kernel,
164            instance_id,
165            config,
166            accumulator: TimeEventAccumulator::new(),
167            run_config_id: None,
168            run_id: None,
169            venues: IndexMap::new(),
170            exec_clients: Vec::new(),
171            has_data: AHashSet::new(),
172            has_book_data: AHashSet::new(),
173            has_book_processed: AHashSet::new(),
174            data_iterator: BacktestDataIterator::new(),
175            data_len: 0,
176            data_stream_counter: 0,
177            ts_first: None,
178            ts_last_data: None,
179            sorted: true,
180            iteration: 0,
181            force_stop: false,
182            last_ns: UnixNanos::default(),
183            last_module_ns: None,
184            last_liquidation_ns: None,
185            end_ns: UnixNanos::default(),
186            run_started: None,
187            run_finished: None,
188            backtest_start: None,
189            backtest_end: None,
190            funding_error: None,
191        })
192    }
193
194    /// Returns a reference to the underlying kernel.
195    #[must_use]
196    pub const fn kernel(&self) -> &NautilusKernel {
197        &self.kernel
198    }
199
200    /// Returns a mutable reference to the underlying kernel.
201    pub fn kernel_mut(&mut self) -> &mut NautilusKernel {
202        &mut self.kernel
203    }
204
205    /// Returns the trader ID for this engine.
206    #[must_use]
207    pub fn trader_id(&self) -> TraderId {
208        self.kernel.trader_id()
209    }
210
211    /// Returns the machine ID for this engine.
212    #[must_use]
213    pub fn machine_id(&self) -> &str {
214        self.kernel.machine_id()
215    }
216
217    /// Returns the unique instance ID for this engine.
218    #[must_use]
219    pub fn instance_id(&self) -> UUID4 {
220        self.instance_id
221    }
222
223    /// Returns the current iteration count.
224    #[must_use]
225    pub fn iteration(&self) -> usize {
226        self.iteration
227    }
228
229    /// Returns the last run config ID, if any.
230    #[must_use]
231    pub fn run_config_id(&self) -> Option<&str> {
232        self.run_config_id.as_deref()
233    }
234
235    /// Returns the last run ID, if any.
236    #[must_use]
237    pub const fn run_id(&self) -> Option<UUID4> {
238        self.run_id
239    }
240
241    /// Returns when the last run started, if any.
242    #[must_use]
243    pub const fn run_started(&self) -> Option<UnixNanos> {
244        self.run_started
245    }
246
247    /// Returns when the last run finished, if any.
248    #[must_use]
249    pub const fn run_finished(&self) -> Option<UnixNanos> {
250        self.run_finished
251    }
252
253    /// Returns the last backtest range start, if any.
254    #[must_use]
255    pub const fn backtest_start(&self) -> Option<UnixNanos> {
256        self.backtest_start
257    }
258
259    /// Returns the last backtest range end, if any.
260    #[must_use]
261    pub const fn backtest_end(&self) -> Option<UnixNanos> {
262        self.backtest_end
263    }
264
265    /// Returns the list of registered venue identifiers.
266    #[must_use]
267    pub fn list_venues(&self) -> Vec<Venue> {
268        self.venues.keys().copied().collect()
269    }
270
271    /// # Errors
272    ///
273    /// Returns an error if the venue is already registered, initializing the simulated exchange
274    /// fails, or registering its execution client fails.
275    pub fn add_venue(&mut self, config: SimulatedVenueConfig) -> anyhow::Result<()> {
276        // `routing` and `frozen_account` flow to the exec client, so capture
277        // them before the config is consumed by the exchange constructor.
278        let venue = config.venue;
279        if self.venues.contains_key(&venue) {
280            anyhow::bail!("Venue {venue} is already registered");
281        }
282
283        let routing = Some(config.routing);
284        let frozen_account = Some(config.frozen_account);
285        let use_message_queue = config.use_message_queue;
286
287        let exchange =
288            SimulatedExchange::new(config, self.kernel.cache.clone(), self.kernel.clock.clone())?;
289        let exchange = Rc::new(RefCell::new(exchange));
290
291        let account_id = AccountId::from(format!("{venue}-001").as_str());
292
293        let exec_client = BacktestExecutionClient::new(
294            self.config.trader_id(),
295            account_id,
296            &exchange,
297            self.kernel.cache.clone(),
298            self.kernel.clock.clone(),
299            routing,
300            frozen_account,
301        );
302
303        if !use_message_queue {
304            exchange
305                .borrow_mut()
306                .set_event_handler(exec_client.order_event_handler());
307        }
308
309        exchange
310            .borrow_mut()
311            .register_client(Rc::new(exec_client.clone()));
312
313        self.kernel
314            .exec_engine
315            .borrow_mut()
316            .register_client(Box::new(exec_client.clone()))?;
317
318        SimulatedExchange::register_spread_quote_endpoint(&exchange);
319        self.venues.insert(venue, exchange);
320        self.exec_clients.push(exec_client);
321
322        log::info!("Adding exchange {venue} to engine");
323
324        Ok(())
325    }
326
327    /// Sets the settlement price for the specified venue instrument.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if the venue has not been added to the engine.
332    pub fn set_settlement_price(
333        &mut self,
334        venue: Venue,
335        instrument_id: InstrumentId,
336        price: Price,
337    ) -> anyhow::Result<()> {
338        let exchange = self
339            .venues
340            .get_mut(&venue)
341            .ok_or_else(|| anyhow::anyhow!("Unknown venue {venue}"))?;
342        exchange
343            .borrow_mut()
344            .set_settlement_price(instrument_id, price);
345        Ok(())
346    }
347
348    /// Changes the fill model for the specified venue.
349    pub fn change_fill_model(&mut self, venue: Venue, fill_model: FillModelHandle) {
350        if let Some(exchange) = self.venues.get_mut(&venue) {
351            exchange.borrow_mut().set_fill_model(fill_model);
352        } else {
353            log::warn!(
354                "BacktestEngine::change_fill_model called for unknown venue {venue}, ignoring"
355            );
356        }
357    }
358
359    /// Adds an instrument to the backtest engine for the specified venue.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if:
364    /// - The instrument's associated venue has not been added via `add_venue`.
365    /// - Attempting to add a `CurrencyPair` instrument for a single-currency CASH account.
366    ///
367    pub fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
368        let instrument_id = instrument.id();
369        if let Some(exchange) = self.venues.get(&instrument.id().venue) {
370            let previous_expiration_ns = exchange.borrow().instrument_expiration(instrument_id);
371
372            if matches!(
373                instrument,
374                InstrumentAny::CurrencyPair(_) | InstrumentAny::TokenizedAsset(_)
375            ) && exchange.borrow().account_type != AccountType::Margin
376                && exchange.borrow().base_currency.is_some()
377            {
378                anyhow::bail!(
379                    "Cannot add a multi-currency spot instrument {instrument_id} for a venue with a single-currency CASH account"
380                )
381            }
382            exchange.borrow_mut().add_instrument(instrument.clone())?;
383            if let Some(expiration_ns) = instrument.expiration_ns() {
384                self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
385            }
386
387            if let Some(previous_expiration_ns) = previous_expiration_ns
388                && instrument.expiration_ns() != Some(previous_expiration_ns)
389                && !exchange
390                    .borrow()
391                    .has_unprocessed_instrument_expiration(previous_expiration_ns)
392            {
393                let timer_name = Self::instrument_expiration_timer_name(
394                    instrument_id.venue,
395                    previous_expiration_ns,
396                );
397                self.kernel.clock.borrow_mut().cancel_timer(&timer_name);
398            }
399        } else {
400            anyhow::bail!(
401                "Cannot add an `Instrument` object without first adding its associated venue {}",
402                instrument.id().venue
403            )
404        }
405
406        self.add_market_data_client_if_not_exists(instrument.id().venue);
407
408        self.kernel
409            .data_engine
410            .borrow_mut()
411            .process(instrument as &dyn Any);
412        log::info!(
413            "Added instrument {} to exchange {}",
414            instrument_id,
415            instrument_id.venue
416        );
417        Ok(())
418    }
419
420    /// Adds data to the engine for replay during the backtest run.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if:
425    /// - `data` is empty.
426    /// - `validate` is `true`, the first element is built-in market data (excluding
427    ///   custom and DeFi data), and its instrument has not been added to the cache via
428    ///   [`add_instrument`](Self::add_instrument).
429    /// - `validate` is `true` and the first element is a [`Data::Bar`] whose
430    ///   `aggregation_source` is not [`AggregationSource::External`].
431    pub fn add_data(
432        &mut self,
433        data: Vec<Data>,
434        client_id: Option<ClientId>,
435        validate: bool,
436        sort: bool,
437    ) -> anyhow::Result<()> {
438        #[cfg(not(feature = "defi"))]
439        let _ = client_id;
440
441        anyhow::ensure!(!data.is_empty(), "data was empty");
442
443        let count = data.len();
444        let mut to_add = data;
445
446        if sort {
447            to_add.sort_by_key(HasTsInit::ts_init);
448        }
449
450        if validate {
451            // Validate against the first element only and assume the batch is
452            // homogeneous (documented contract on add_data).
453            let first = &to_add[0];
454            #[cfg(feature = "defi")]
455            let first_is_defi = matches!(first, Data::Defi(_));
456            #[cfg(not(feature = "defi"))]
457            let first_is_defi = false;
458
459            if !first_is_defi && !matches!(first, Data::Custom(_)) {
460                let first_instrument_id = first.instrument_id();
461                anyhow::ensure!(
462                    self.kernel
463                        .cache
464                        .borrow()
465                        .instrument(&first_instrument_id)
466                        .is_some(),
467                    "Instrument {first_instrument_id} for the given data not found in the cache. \
468                     Add the instrument through `add_instrument()` prior to adding related data."
469                );
470
471                if let Data::Bar(bar) = first {
472                    anyhow::ensure!(
473                        bar.bar_type.aggregation_source() == AggregationSource::External,
474                        "bar_type.aggregation_source must be External, was {:?}",
475                        bar.bar_type.aggregation_source(),
476                    );
477                }
478            }
479        }
480
481        // Track has_data / has_book_data unconditionally so the depth-vs-data
482        // run-time check still fires for callers that pass validate=false
483        // (e.g. node.rs run_oneshot loading from a catalog). Time bounds are
484        // also tracked here so start/end defaults are correct even when the
485        // batch was added with sort=false.
486        let mut batch_min_ts: Option<UnixNanos> = None;
487        let mut batch_max_ts: Option<UnixNanos> = None;
488
489        #[cfg(feature = "defi")]
490        if to_add.iter().any(|item| matches!(item, Data::Defi(_))) {
491            self.add_defi_data_client_if_not_exists(client_id);
492        }
493
494        for item in &to_add {
495            let ts = item.ts_init();
496            batch_min_ts = Some(batch_min_ts.map_or(ts, |cur| cur.min(ts)));
497            batch_max_ts = Some(batch_max_ts.map_or(ts, |cur| cur.max(ts)));
498
499            #[cfg(feature = "defi")]
500            if matches!(item, Data::Defi(_)) {
501                continue;
502            }
503
504            if matches!(item, Data::Custom(_)) {
505                // Custom data routes by DataType and is independent of market venue bookkeeping.
506                continue;
507            }
508
509            let instr_id = item.instrument_id();
510            self.has_data.insert(instr_id);
511
512            if item.is_order_book_data() {
513                self.has_book_data.insert(instr_id);
514            }
515
516            self.add_market_data_client_if_not_exists(instr_id.venue);
517        }
518
519        if let Some(ts) = batch_min_ts
520            && self.ts_first.is_none_or(|t| ts < t)
521        {
522            self.ts_first = Some(ts);
523        }
524
525        if let Some(ts) = batch_max_ts
526            && self.ts_last_data.is_none_or(|t| ts > t)
527        {
528            self.ts_last_data = Some(ts);
529        }
530
531        self.data_len += count;
532        let stream_name = format!("backtest_data_{}", self.data_stream_counter);
533        self.data_stream_counter += 1;
534        self.data_iterator.add_data(&stream_name, to_add, true);
535
536        self.sorted = sort;
537
538        log::info!(
539            "Added {count} data element{} to BacktestEngine ({} total)",
540            if count == 1 { "" } else { "s" },
541            self.data_len,
542        );
543
544        Ok(())
545    }
546
547    /// Adds an actor to the backtest engine.
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if the actor is already registered or the trader is in an invalid
552    /// state for actor registration.
553    pub fn add_actor<T>(&mut self, actor: T) -> anyhow::Result<()>
554    where
555        T: DataActor + DataActorNative + Component + Debug + 'static,
556    {
557        self.kernel.trader.borrow_mut().add_actor(actor)
558    }
559
560    /// Adds the given actors to the backtest engine. Stops at the first error.
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if any actor fails to register; preceding actors remain registered.
565    pub fn add_actors<T>(&mut self, actors: Vec<T>) -> anyhow::Result<()>
566    where
567        T: DataActor + DataActorNative + Component + Debug + 'static,
568    {
569        for actor in actors {
570            self.add_actor(actor)?;
571        }
572        Ok(())
573    }
574
575    /// Adds a strategy to the backtest engine.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if the strategy is already registered or the trader is in an invalid
580    /// state for strategy registration.
581    pub fn add_strategy<T>(&mut self, mut strategy: T) -> anyhow::Result<()>
582    where
583        T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
584    {
585        let strategy_id = self
586            .kernel
587            .trader
588            .borrow()
589            .prepare_strategy_for_registration(&mut strategy)?;
590        let oms_type = StrategyNative::strategy_core(&strategy).config.oms_type;
591
592        self.kernel.trader.borrow_mut().add_strategy(strategy)?;
593
594        if let Some(oms_type) = oms_type {
595            self.kernel
596                .exec_engine
597                .borrow_mut()
598                .register_oms_type(strategy_id, oms_type);
599        }
600
601        Ok(())
602    }
603
604    /// Adds the given strategies to the backtest engine. Stops at the first error.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error if any strategy fails to register; preceding strategies remain registered.
609    pub fn add_strategies<T>(&mut self, strategies: Vec<T>) -> anyhow::Result<()>
610    where
611        T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
612    {
613        for strategy in strategies {
614            self.add_strategy(strategy)?;
615        }
616        Ok(())
617    }
618
619    /// Adds an execution algorithm to the backtest engine.
620    ///
621    /// # Errors
622    ///
623    /// Returns an error if the algorithm is already registered or the trader is running.
624    pub fn add_exec_algorithm<T>(&mut self, exec_algorithm: T) -> anyhow::Result<()>
625    where
626        T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
627    {
628        self.kernel
629            .trader
630            .borrow_mut()
631            .add_exec_algorithm(exec_algorithm)
632    }
633
634    /// Adds the given execution algorithms to the backtest engine. Stops at the first error.
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if any execution algorithm fails to register; preceding algorithms remain
639    /// registered.
640    pub fn add_exec_algorithms<T>(&mut self, exec_algorithms: Vec<T>) -> anyhow::Result<()>
641    where
642        T: ExecutionAlgorithm + ExecutionAlgorithmNative + Component + Debug + 'static,
643    {
644        for exec_algorithm in exec_algorithms {
645            self.add_exec_algorithm(exec_algorithm)?;
646        }
647        Ok(())
648    }
649
650    /// Run a backtest.
651    ///
652    /// Processes all data chronologically. When `streaming` is false (default),
653    /// finalizes the run via [`end`](Self::end). When `streaming` is true, the
654    /// run pauses without finalizing so additional data batches can be loaded.
655    /// Timer advancement stops at data exhaustion to avoid producing synthetic
656    /// events (e.g. zero-volume bars) past the current batch.
657    ///
658    /// Each streaming batch must include every data item with its final `ts_init`;
659    /// splitting one replay timestamp across calls can finalize timers and venue
660    /// modules before later items at that timestamp. [`BacktestNode`](crate::node::BacktestNode)
661    /// aligns its chunks to this boundary.
662    ///
663    /// Streaming workflow:
664    /// 1. Add initial data and strategies
665    /// 2. Loop: call `run(streaming=true)`, `clear_data()`, `add_data(next_batch)`
666    /// 3. After all batches: call `end()` to finalize
667    ///
668    /// # Errors
669    ///
670    /// Returns an error if the backtest encounters an unrecoverable state.
671    pub fn run(
672        &mut self,
673        start: Option<UnixNanos>,
674        end: Option<UnixNanos>,
675        run_config_id: Option<String>,
676        streaming: bool,
677    ) -> anyhow::Result<()> {
678        if let Some(error) = &self.funding_error {
679            anyhow::bail!("{error}");
680        }
681
682        if let Err(e) = self.run_impl(start, end, run_config_id, streaming) {
683            if self.funding_error.is_some() {
684                self.abort_run();
685            }
686            return Err(e);
687        }
688
689        // Finalize on non-streaming runs, or when a shutdown was triggered
690        // at any point during the run (including the trailing settle, module,
691        // and flush callbacks that execute after the main data loop) so the
692        // trader and engines actually stop.
693        if !streaming || self.force_stop || self.kernel.is_shutdown_requested() {
694            self.end_with_result()?;
695        }
696
697        Ok(())
698    }
699
700    fn run_impl(
701        &mut self,
702        start: Option<UnixNanos>,
703        end: Option<UnixNanos>,
704        run_config_id: Option<String>,
705        streaming: bool,
706    ) -> anyhow::Result<()> {
707        anyhow::ensure!(
708            self.sorted,
709            "Data has been added but not sorted, call `engine.sort_data()` or use \
710             `engine.add_data(..., sort=true)` before running"
711        );
712
713        for exchange in self.venues.values() {
714            let exchange = exchange.borrow();
715            let book_type_has_depth = exchange.book_type() as u8 > BookType::L1_MBP as u8;
716            if !book_type_has_depth {
717                continue;
718            }
719
720            for instrument_id in exchange.instrument_ids() {
721                let has_data = self.has_data.contains(instrument_id);
722                let missing_book_data = !self.has_book_data.contains(instrument_id)
723                    && !self.has_book_processed.contains(instrument_id);
724
725                if has_data && missing_book_data {
726                    anyhow::bail!(
727                        "No order book data found for instrument '{instrument_id}' when `book_type` \
728                         is '{:?}'. Set the venue `book_type` to 'L1_MBP' (for top-of-book data \
729                         like quotes, trades, and bars) or provide order book data for this \
730                         instrument.",
731                        exchange.book_type()
732                    );
733                }
734            }
735        }
736
737        // Determine time boundaries
738        let start_ns = start.unwrap_or_else(|| self.ts_first.unwrap_or_default());
739        let end_ns = end.unwrap_or_else(|| self.ts_last_data.unwrap_or(start_ns));
740        anyhow::ensure!(start_ns <= end_ns, "start was > end");
741        self.end_ns = end_ns;
742        self.last_ns = start_ns;
743        self.last_module_ns = None;
744
745        // Set all component clocks to start
746        let clocks = self.collect_all_clocks();
747        Self::set_all_clocks_time(&clocks, start_ns);
748
749        // First-iteration initialization
750        if self.iteration == 0 {
751            self.set_instrument_expiration_timers()?;
752
753            self.run_config_id = run_config_id;
754            self.run_id = Some(UUID4::new());
755            self.run_started = Some(UnixNanos::from(nanos_since_unix_epoch()));
756            self.backtest_start = Some(start_ns);
757
758            for exchange in self.venues.values() {
759                let mut ex = exchange.borrow_mut();
760                ex.initialize_account();
761                ex.load_open_orders();
762            }
763
764            // Re-set clocks after account init
765            Self::set_all_clocks_time(&clocks, start_ns);
766
767            // Reset force stop flag
768            self.force_stop = false;
769            self.kernel.reset_shutdown_flag();
770
771            // Initialize sync command senders (once per thread)
772            Self::init_command_senders();
773
774            // Set logging to static clock mode for deterministic timestamps
775            logging_clock_set_static_mode();
776            logging_clock_set_static_time(start_ns.as_u64());
777
778            // Start kernel, then stop before trader startup for event-store replay
779            self.kernel.start();
780            if self.kernel.is_event_store_replay() {
781                self.log_pre_run();
782                return Ok(());
783            }
784
785            if self.kernel.is_event_store_replay_configured() {
786                anyhow::bail!("event-store replay did not start");
787            }
788            self.kernel.start_trader()?;
789
790            // Drain on_start data subscriptions so aggregators subscribe before the first data
791            // point, else internal aggregation drops the first tick. Trading/exec stay queued
792            while !data_cmd_queue_is_empty() {
793                drain_data_cmd_queue();
794            }
795
796            self.log_pre_run();
797        }
798
799        self.log_run();
800
801        // Skip data before start_ns
802        while let Some(d) = self.data_iterator.peek() {
803            if d.ts_init() >= start_ns {
804                break;
805            }
806            self.data_iterator.next_item();
807        }
808
809        // Initialize last_ns before first data point
810        if let Some(d) = self.data_iterator.peek() {
811            let ts = d.ts_init();
812            self.last_ns = if ts.as_u64() > 0 {
813                UnixNanos::from(ts.as_u64() - 1)
814            } else {
815                UnixNanos::default()
816            };
817        } else {
818            self.last_ns = start_ns;
819        }
820
821        loop {
822            if self.kernel.is_shutdown_requested() {
823                log::info!("Shutdown requested via ShutdownSystem, ending backtest");
824                self.force_stop = true;
825            }
826
827            if self.force_stop {
828                log::info!("Force stop triggered, ending backtest");
829                break;
830            }
831
832            let Some(d) = self.data_iterator.peek() else {
833                if streaming {
834                    // In streaming mode, don't advance timers past the
835                    // current batch. The next batch will provide more data
836                    // and timers will fire naturally as time advances.
837                    break;
838                }
839                let done = self.process_next_timer(&clocks)?;
840                if self.data_iterator.peek().is_none() && done {
841                    break;
842                }
843                continue;
844            };
845
846            let ts_init = d.ts_init();
847
848            if ts_init > end_ns {
849                break;
850            }
851
852            let d = self.data_iterator.next_item().unwrap();
853
854            if ts_init > self.last_ns {
855                self.advance_time_impl(ts_init, &clocks)?;
856            }
857
858            // A timer fired during clock advance may have requested shutdown,
859            // skip delivering this data point in that case
860            if self.kernel.is_shutdown_requested() {
861                self.force_stop = true;
862                break;
863            }
864
865            self.route_data_to_exchange(&d);
866            self.kernel.data_engine.borrow_mut().process_data(d);
867
868            // Drain deferred commands, then process exchange queues
869            self.drain_command_queues();
870            self.settle_venues(ts_init);
871
872            let prev_last_ns = self.last_ns;
873            // If timestamp changed (or exhausted), flush timers then run modules
874            if self
875                .data_iterator
876                .peek()
877                .is_none_or(|next| next.ts_init() > prev_last_ns)
878            {
879                self.flush_accumulator_events(&clocks, prev_last_ns)?;
880                self.finalize_timestamp(&clocks, prev_last_ns)?;
881            }
882
883            self.iteration += 1;
884        }
885
886        // Process remaining exchange messages
887        let ts_now = self.kernel.clock.borrow().timestamp_ns();
888        self.finalize_timestamp(&clocks, ts_now)?;
889
890        // Cap at last_ns when streaming or after shutdown to avoid firing
891        // timers past the current batch or the graceful stop
892        let flush_ts = if streaming || self.force_stop || self.kernel.is_shutdown_requested() {
893            self.last_ns
894        } else {
895            end_ns
896        };
897        self.flush_accumulator_events(&clocks, flush_ts)?;
898
899        Ok(())
900    }
901
902    fn abort_run(&mut self) {
903        self.force_stop = true;
904        self.accumulator.clear();
905        self.kernel.stop_trader();
906        self.kernel.data_engine.borrow_mut().stop();
907        self.kernel.risk_engine.borrow_mut().stop();
908        self.kernel.exec_engine.borrow_mut().stop();
909        self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
910        self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
911        logging_clock_set_realtime_mode();
912    }
913
914    /// Manually end the backtest.
915    pub fn end(&mut self) {
916        if let Err(e) = self.end_with_result() {
917            log::error!("Error ending backtest: {e}");
918        }
919    }
920
921    /// Ends the backtest and reports lifecycle persistence failures.
922    ///
923    /// # Errors
924    ///
925    /// Returns an error if actor or strategy state cannot be saved.
926    pub(crate) fn end_with_result(&mut self) -> anyhow::Result<()> {
927        if let Some(error) = &self.funding_error {
928            anyhow::bail!("{error}");
929        }
930
931        // Flush remaining timer events to the backtest end boundary so that
932        // tail alerts/expiries scheduled after the last data point still fire.
933        // Must run before stopping engines since DataEngine::stop() cancels
934        // bar aggregator timers. When a shutdown was requested, cap the flush
935        // at the last processed timestamp so timers scheduled past the stop
936        // point do not fire extra callbacks after the graceful stop request.
937        if self.end_ns.as_u64() > 0 {
938            let clocks = self.collect_all_clocks();
939            let flush_ts = if self.force_stop || self.kernel.is_shutdown_requested() {
940                self.last_ns
941            } else {
942                self.end_ns
943            };
944
945            if let Err(e) = self.flush_accumulator_events(&clocks, flush_ts) {
946                if self.funding_error.is_some() {
947                    self.abort_run();
948                }
949                return Err(e);
950            }
951        }
952
953        self.kernel.stop_trader();
954
955        // Settle residual on_stop commands before stopping engines. Venue modules are
956        // not re-run; process_modules is once per timestamp.
957        let mut ts_now = self.kernel.clock.borrow().timestamp_ns();
958
959        // Drain first so latency-deferred commands reach venue inflight queues
960        self.drain_command_queues();
961
962        // Advance the clock to the latest inflight arrival; otherwise commands deferred
963        // by a LatencyModel sit past ts_now and never settle.
964        if let Some(max_inflight_ts) = self.max_inflight_command_ts()
965            && max_inflight_ts > ts_now
966        {
967            ts_now = max_inflight_ts;
968            let clocks = self.collect_all_clocks();
969            Self::set_all_clocks_time(&clocks, ts_now);
970        }
971
972        self.settle_venues(ts_now);
973
974        let save_result = self.kernel.save_trader_state();
975        self.kernel.portfolio.borrow_mut().finalize_equity_curve();
976
977        // Stop engines
978        self.kernel.data_engine.borrow_mut().stop();
979        self.kernel.risk_engine.borrow_mut().stop();
980        self.kernel.exec_engine.borrow_mut().stop();
981
982        self.run_finished = Some(UnixNanos::from(nanos_since_unix_epoch()));
983        self.backtest_end = Some(self.kernel.clock.borrow().timestamp_ns());
984
985        // Switch logging back to realtime mode
986        logging_clock_set_realtime_mode();
987
988        self.log_post_run();
989        save_result
990    }
991
992    /// Reset the backtest engine.
993    ///
994    /// All stateful fields are reset to their initial value. Data and instruments
995    /// persist across resets to enable repeated runs with different strategies.
996    pub fn reset(&mut self) {
997        log::debug!("Resetting");
998
999        if self.kernel.trader.borrow().is_running() {
1000            self.end();
1001        }
1002
1003        // Stop and reset engines
1004        self.kernel.data_engine.borrow_mut().stop();
1005        self.kernel.data_engine.borrow_mut().reset();
1006
1007        self.kernel.exec_engine.borrow_mut().stop();
1008
1009        // Reset exchanges before the exec engine wipes the cache so
1010        // exchange.reset() can see the prior run's account.
1011        for exchange in self.venues.values() {
1012            exchange.borrow_mut().reset();
1013        }
1014        self.kernel.exec_engine.borrow_mut().reset();
1015
1016        self.kernel.risk_engine.borrow_mut().stop();
1017        self.kernel.risk_engine.borrow_mut().reset();
1018
1019        self.kernel.order_emulator.reset();
1020
1021        // Reset trader
1022        if let Err(e) = self.kernel.trader.borrow_mut().reset() {
1023            log::error!("Error resetting trader: {e:?}");
1024        }
1025
1026        self.kernel.portfolio.borrow_mut().reset();
1027
1028        // Clear run state
1029        self.run_config_id = None;
1030        self.run_id = None;
1031        self.run_started = None;
1032        self.run_finished = None;
1033        self.backtest_start = None;
1034        self.backtest_end = None;
1035        self.funding_error = None;
1036        self.iteration = 0;
1037        self.force_stop = false;
1038        self.last_ns = UnixNanos::default();
1039        self.last_module_ns = None;
1040        self.last_liquidation_ns = None;
1041        self.end_ns = UnixNanos::default();
1042        self.has_book_processed.clear();
1043
1044        self.accumulator.clear();
1045        self.cancel_funding_settlement_timers();
1046
1047        // Reset all iterator cursors to beginning (data persists)
1048        self.data_iterator.reset_all_cursors();
1049
1050        log::info!("Reset");
1051    }
1052
1053    /// Sort the engine's internal data stream by timestamp.
1054    ///
1055    /// Useful when data has been added with `sort=false` for batch performance,
1056    /// then sorted once before running.
1057    pub fn sort_data(&mut self) {
1058        // Each add call creates its own stream; the iterator merges streams by
1059        // replay timestamp across streams. Mark the engine as sorted so `run`
1060        // no longer rejects it.
1061        self.sorted = true;
1062        log::info!("Data sort requested (iterator merges streams by replay timestamp)");
1063    }
1064
1065    /// Clear the engine's internal data stream. Does not clear instruments.
1066    pub fn clear_data(&mut self) {
1067        self.has_data.clear();
1068        self.has_book_data.clear();
1069        self.data_iterator = BacktestDataIterator::new();
1070        self.data_len = 0;
1071        self.data_stream_counter = 0;
1072        self.ts_first = None;
1073        self.ts_last_data = None;
1074        self.sorted = true;
1075    }
1076
1077    /// Clear all actors from the engine's internal trader.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an error if any actor fails to dispose.
1082    pub fn clear_actors(&mut self) -> anyhow::Result<()> {
1083        self.kernel.trader.borrow_mut().clear_actors()
1084    }
1085
1086    /// Clear all trading strategies from the engine's internal trader.
1087    ///
1088    /// # Errors
1089    ///
1090    /// Returns an error if any strategy fails to dispose.
1091    pub fn clear_strategies(&mut self) -> anyhow::Result<()> {
1092        self.kernel.trader.borrow_mut().clear_strategies()
1093    }
1094
1095    /// Clear all execution algorithms from the engine's internal trader.
1096    ///
1097    /// # Errors
1098    ///
1099    /// Returns an error if any execution algorithm fails to dispose.
1100    pub fn clear_exec_algorithms(&mut self) -> anyhow::Result<()> {
1101        self.kernel.trader.borrow_mut().clear_exec_algorithms()
1102    }
1103
1104    /// Dispose of the backtest engine, releasing all resources.
1105    pub fn dispose(&mut self) {
1106        self.clear_data();
1107        self.accumulator.clear();
1108        self.kernel.dispose();
1109    }
1110
1111    /// Return the backtest result from the last run.
1112    #[must_use]
1113    pub fn get_result(&self) -> BacktestResult {
1114        let elapsed_time_secs = match (self.backtest_start, self.backtest_end) {
1115            (Some(start), Some(end)) => (end.as_f64() - start.as_f64()) / 1_000_000_000.0,
1116            _ => 0.0,
1117        };
1118
1119        let cache = self.kernel.cache.borrow();
1120        let orders = cache.orders(None, None, None, None, None);
1121        let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1122        let total_orders = orders.len();
1123        let positions: Vec<Position> = cache
1124            .positions(None, None, None, None, None)
1125            .into_iter()
1126            .map(|p| p.cloned())
1127            .collect();
1128        let cached_positions_count = positions.len();
1129        let snapshot_positions = cache.position_snapshots(None, None).len();
1130        let total_positions = Self::total_positions_with_snapshots(&cache, cached_positions_count);
1131        let summary = self.build_result_summary(
1132            &cache,
1133            total_events,
1134            total_orders,
1135            cached_positions_count,
1136            snapshot_positions,
1137        );
1138
1139        let stats = self.kernel.portfolio.borrow().statistics();
1140        let stats_pnls = stats.pnls;
1141        let stats_returns = stats.returns;
1142        let stats_general = stats.general;
1143        let returns_series = stats.returns_series;
1144
1145        BacktestResult {
1146            trader_id: self.config.trader_id().to_string(),
1147            machine_id: self.kernel.machine_id.clone(),
1148            instance_id: self.instance_id,
1149            run_config_id: self.run_config_id.clone(),
1150            run_id: self.run_id,
1151            run_started: self.run_started,
1152            run_finished: self.run_finished,
1153            backtest_start: self.backtest_start,
1154            backtest_end: self.backtest_end,
1155            elapsed_time_secs,
1156            iterations: self.iteration,
1157            total_events,
1158            total_orders,
1159            total_positions,
1160            summary,
1161            stats_pnls,
1162            stats_returns,
1163            stats_general,
1164            returns_series,
1165        }
1166    }
1167
1168    /// Returns the versioned deterministic projection of observable state from the last run.
1169    ///
1170    /// This projection excludes host, process, random identity, wall-clock, and elapsed-time noise.
1171    /// It retains deterministic references between domain events and includes the observable cache,
1172    /// account, portfolio, component, outcome, and diagnostic state available after the run ends.
1173    ///
1174    /// # Errors
1175    ///
1176    /// Returns an error if observable state cannot be projected into the canonical schema.
1177    pub fn get_canonical_result(&self) -> anyhow::Result<CanonicalBacktestResult> {
1178        let result = self.get_result();
1179        let cache = self.kernel.cache.borrow();
1180        let orders = cache
1181            .orders(None, None, None, None, None)
1182            .into_iter()
1183            .map(|order| order.cloned())
1184            .collect();
1185        let positions = cache
1186            .positions(None, None, None, None, None)
1187            .into_iter()
1188            .map(|position| position.cloned())
1189            .collect();
1190        let position_snapshots = cache.position_snapshots(None, None);
1191        let accounts = cache.accounts_all_owned();
1192        drop(cache);
1193
1194        let portfolio = self.kernel.portfolio.borrow();
1195        let mut portfolio_snapshots = Vec::new();
1196        for account in &accounts {
1197            portfolio_snapshots.extend(portfolio.snapshots(&account.id()));
1198        }
1199        drop(portfolio);
1200
1201        let trader = self.kernel.trader.borrow();
1202        let trader_state = trader.state().to_string();
1203        let actor_ids = trader
1204            .actor_ids()
1205            .into_iter()
1206            .map(|id| id.to_string())
1207            .collect();
1208        let strategy_ids = trader
1209            .strategy_ids()
1210            .into_iter()
1211            .map(|id| id.to_string())
1212            .collect();
1213        let exec_algorithm_ids = trader
1214            .exec_algorithm_ids()
1215            .into_iter()
1216            .map(|id| id.to_string())
1217            .collect();
1218        drop(trader);
1219
1220        let outcome = if self.funding_error.is_some() {
1221            CanonicalRunOutcome::Failed
1222        } else if self.run_finished.is_none() {
1223            CanonicalRunOutcome::Incomplete
1224        } else if self.force_stop || self.kernel.is_shutdown_requested() {
1225            CanonicalRunOutcome::Stopped
1226        } else {
1227            CanonicalRunOutcome::Completed
1228        };
1229        let diagnostics = self
1230            .funding_error
1231            .as_ref()
1232            .map(|_| CanonicalDiagnostic {
1233                code: CanonicalDiagnosticCode::FundingSettlementFailed,
1234            })
1235            .into_iter()
1236            .collect();
1237        let statistics = nautilus_analysis::PortfolioStatistics {
1238            pnls: result.stats_pnls,
1239            returns: result.stats_returns,
1240            general: result.stats_general,
1241            returns_series: result.returns_series,
1242        };
1243
1244        CanonicalBacktestResult::from_state(CanonicalBacktestState {
1245            trader_id: result.trader_id,
1246            run_config_id: result.run_config_id,
1247            backtest_start: result.backtest_start,
1248            backtest_end: result.backtest_end,
1249            iterations: result.iterations,
1250            total_events: result.total_events,
1251            total_orders: result.total_orders,
1252            total_positions: result.total_positions,
1253            outcome,
1254            diagnostics,
1255            trader_state,
1256            actor_ids,
1257            strategy_ids,
1258            exec_algorithm_ids,
1259            summary: result.summary.into_iter().collect(),
1260            orders,
1261            positions,
1262            position_snapshots,
1263            accounts,
1264            portfolio_snapshots,
1265            statistics,
1266        })
1267    }
1268
1269    fn build_result_summary(
1270        &self,
1271        cache: &Cache,
1272        total_events: usize,
1273        total_orders: usize,
1274        cached_positions_count: usize,
1275        snapshot_positions: usize,
1276    ) -> AHashMap<String, String> {
1277        let mut summary = AHashMap::new();
1278        summary.insert("iterations".to_string(), self.iteration.to_string());
1279        summary.insert("total_events".to_string(), total_events.to_string());
1280        summary.insert("orders.total".to_string(), total_orders.to_string());
1281        summary.insert(
1282            "orders.open".to_string(),
1283            cache
1284                .orders_open_count(None, None, None, None, None)
1285                .to_string(),
1286        );
1287        summary.insert(
1288            "orders.closed".to_string(),
1289            cache
1290                .orders_closed_count(None, None, None, None, None)
1291                .to_string(),
1292        );
1293        summary.insert(
1294            "orders.emulated".to_string(),
1295            cache
1296                .orders_emulated_count(None, None, None, None, None)
1297                .to_string(),
1298        );
1299        summary.insert(
1300            "orders.inflight".to_string(),
1301            cache
1302                .orders_inflight_count(None, None, None, None, None)
1303                .to_string(),
1304        );
1305        summary.insert(
1306            "positions.total".to_string(),
1307            cached_positions_count.to_string(),
1308        );
1309        summary.insert(
1310            "positions.open".to_string(),
1311            cache
1312                .positions_open_count(None, None, None, None, None)
1313                .to_string(),
1314        );
1315        summary.insert(
1316            "positions.closed".to_string(),
1317            cache
1318                .positions_closed_count(None, None, None, None, None)
1319                .to_string(),
1320        );
1321        summary.insert(
1322            "positions.snapshots".to_string(),
1323            snapshot_positions.to_string(),
1324        );
1325        summary.insert(
1326            "positions.total_with_snapshots".to_string(),
1327            (cached_positions_count + snapshot_positions).to_string(),
1328        );
1329
1330        let mut venues: Vec<Venue> = self.venues.keys().copied().collect();
1331        venues.sort_by_key(ToString::to_string);
1332        summary.insert("venues.total".to_string(), venues.len().to_string());
1333
1334        for venue in venues {
1335            let Some(account) = cache.account_for_venue(&venue) else {
1336                continue;
1337            };
1338
1339            let venue_key = venue.to_string();
1340            let account_key = format!("account.{venue_key}");
1341            summary.insert(format!("{account_key}.id"), account.id().to_string());
1342            summary.insert(
1343                format!("{account_key}.type"),
1344                account.account_type().to_string(),
1345            );
1346            summary.insert(
1347                format!("{account_key}.base_currency"),
1348                account
1349                    .base_currency()
1350                    .map_or_else(|| "None".to_string(), |currency| currency.code.to_string()),
1351            );
1352            summary.insert(
1353                format!("{account_key}.event_count"),
1354                account.event_count().to_string(),
1355            );
1356
1357            let mut balances: Vec<_> = account.balances().into_iter().collect();
1358            balances.sort_by_key(|(currency, _)| currency.code.to_string());
1359
1360            for (currency, balance) in balances {
1361                let balance_key = format!("{account_key}.balance.{}", currency.code);
1362                summary.insert(format!("{balance_key}.total"), balance.total.to_string());
1363                summary.insert(format!("{balance_key}.free"), balance.free.to_string());
1364                summary.insert(format!("{balance_key}.locked"), balance.locked.to_string());
1365            }
1366        }
1367
1368        summary
1369    }
1370
1371    fn route_data_to_exchange(&mut self, data: &Data) {
1372        if matches!(
1373            data,
1374            Data::MarkPrice(_) | Data::IndexPrice(_) | Data::OptionGreeks(_) | Data::Custom(_)
1375        ) {
1376            return;
1377        }
1378        #[cfg(feature = "defi")]
1379        if matches!(data, Data::Defi(_)) {
1380            return;
1381        }
1382
1383        let venue = data.instrument_id().venue;
1384        if let Some(exchange) = self.venues.get(&venue) {
1385            let mut exchange_ref = exchange.borrow_mut();
1386            let mut processed_book_data = false;
1387
1388            match data {
1389                Data::Delta(delta) => {
1390                    exchange_ref.process_order_book_delta(*delta);
1391                    processed_book_data = true;
1392                }
1393                Data::Deltas(deltas) => {
1394                    exchange_ref.process_order_book_deltas(deltas);
1395                    processed_book_data = true;
1396                }
1397                Data::Depth10(depth) => {
1398                    exchange_ref.process_order_book_depth10(depth);
1399                    processed_book_data = true;
1400                }
1401                Data::Quote(quote) => exchange_ref.process_quote_tick(quote),
1402                Data::Trade(trade) => exchange_ref.process_trade_tick(trade),
1403                Data::Bar(bar) => exchange_ref.process_bar(*bar),
1404                Data::InstrumentStatus(status) => exchange_ref.process_instrument_status(*status),
1405                Data::InstrumentClose(close) => exchange_ref.process_instrument_close(*close),
1406                Data::FundingRate(funding) => {
1407                    let settlement_ns =
1408                        exchange_ref.process_funding_rate_deferred(*funding, data.ts_init());
1409                    self.schedule_funding_settlement_if_required(venue, settlement_ns);
1410                }
1411                _ => {}
1412            }
1413
1414            drop(exchange_ref);
1415
1416            if processed_book_data {
1417                self.has_book_processed.insert(data.instrument_id());
1418            }
1419        } else {
1420            log::warn!("No exchange found for venue {venue}, data not routed");
1421        }
1422    }
1423
1424    fn advance_time_impl(
1425        &mut self,
1426        ts_now: UnixNanos,
1427        clocks: &[Rc<RefCell<dyn Clock>>],
1428    ) -> anyhow::Result<()> {
1429        for clock in clocks {
1430            Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1431        }
1432
1433        // Process events with ts_event < ts_now
1434        let ts_before = if ts_now.as_u64() > 0 {
1435            UnixNanos::from(ts_now.as_u64() - 1)
1436        } else {
1437            UnixNanos::default()
1438        };
1439
1440        let mut shutdown_at: Option<UnixNanos> = None;
1441
1442        while let Some(ts_event) = self
1443            .accumulator
1444            .peek_next_time()
1445            .filter(|ts_event| *ts_event <= ts_before)
1446        {
1447            self.run_timer_handlers_at(clocks, ts_event, ts_now);
1448
1449            if self.kernel.is_shutdown_requested() {
1450                self.accumulator.clear();
1451                shutdown_at = Some(ts_event);
1452                break;
1453            }
1454            self.finalize_timestamp(clocks, ts_event)?;
1455
1456            if self.kernel.is_shutdown_requested() {
1457                self.accumulator.clear();
1458                shutdown_at = Some(ts_event);
1459                break;
1460            }
1461
1462            for clock in clocks {
1463                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1464            }
1465        }
1466
1467        // On a mid-drain shutdown, anchor state at the firing timer's ts so
1468        // post-run settlement and backtest_end reflect the graceful stop
1469        if let Some(ts_event) = shutdown_at {
1470            self.last_ns = ts_event;
1471        } else {
1472            self.last_ns = ts_now;
1473            Self::set_all_clocks_time(clocks, ts_now);
1474            logging_clock_set_static_time(ts_now.as_u64());
1475        }
1476
1477        Ok(())
1478    }
1479
1480    fn flush_accumulator_events(
1481        &mut self,
1482        clocks: &[Rc<RefCell<dyn Clock>>],
1483        ts_now: UnixNanos,
1484    ) -> anyhow::Result<()> {
1485        // Bail after shutdown so handler-scheduled alerts do not fire post-stop
1486        if self.kernel.is_shutdown_requested() {
1487            self.accumulator.clear();
1488            return Ok(());
1489        }
1490
1491        let last_ns = self.last_ns;
1492
1493        for clock in clocks {
1494            Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1495        }
1496
1497        while let Some(ts_event) = self
1498            .accumulator
1499            .peek_next_time()
1500            .filter(|ts_event| *ts_event <= ts_now)
1501        {
1502            self.run_timer_handlers_at(clocks, ts_event, ts_now);
1503
1504            if self.kernel.is_shutdown_requested() {
1505                self.accumulator.clear();
1506                break;
1507            }
1508            self.finalize_timestamp(clocks, ts_event)?;
1509
1510            if self.kernel.is_shutdown_requested() {
1511                self.accumulator.clear();
1512                break;
1513            }
1514
1515            for clock in clocks {
1516                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1517            }
1518        }
1519
1520        if !self.kernel.is_shutdown_requested() {
1521            self.last_ns = last_ns;
1522        }
1523
1524        Ok(())
1525    }
1526
1527    fn process_next_timer(&mut self, clocks: &[Rc<RefCell<dyn Clock>>]) -> anyhow::Result<bool> {
1528        self.flush_accumulator_events(clocks, self.last_ns)?;
1529
1530        // Find minimum next timer time across all component clocks
1531        let mut min_next_time: Option<UnixNanos> = None;
1532
1533        for clock in clocks {
1534            let clock_ref = clock.borrow();
1535            for name in clock_ref.timer_names() {
1536                if let Some(next_time) = clock_ref.next_time_ns(name)
1537                    && next_time > self.last_ns
1538                {
1539                    min_next_time = Some(match min_next_time {
1540                        Some(current_min) => next_time.min(current_min),
1541                        None => next_time,
1542                    });
1543                }
1544            }
1545        }
1546
1547        match min_next_time {
1548            None => Ok(true),
1549            Some(t) if t > self.end_ns => Ok(true),
1550            Some(t) => {
1551                self.last_ns = t;
1552                self.flush_accumulator_events(clocks, t)?;
1553                Ok(false)
1554            }
1555        }
1556    }
1557
1558    fn run_timer_handlers_at(
1559        &mut self,
1560        clocks: &[Rc<RefCell<dyn Clock>>],
1561        ts_event: UnixNanos,
1562        advance_to: UnixNanos,
1563    ) {
1564        self.last_ns = ts_event;
1565        while self.accumulator.peek_next_time() == Some(ts_event) {
1566            let handler = self
1567                .accumulator
1568                .pop_next_at_or_before(ts_event)
1569                .expect("timer exists at timestamp");
1570            Self::set_all_clocks_time(clocks, ts_event);
1571            logging_clock_set_static_time(ts_event.as_u64());
1572            handler.run();
1573            self.drain_command_queues();
1574
1575            if self.kernel.is_shutdown_requested() {
1576                return;
1577            }
1578
1579            for clock in clocks {
1580                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, advance_to, false);
1581            }
1582        }
1583    }
1584
1585    fn finalize_timestamp(
1586        &mut self,
1587        clocks: &[Rc<RefCell<dyn Clock>>],
1588        ts_now: UnixNanos,
1589    ) -> anyhow::Result<()> {
1590        loop {
1591            self.settle_venues(ts_now);
1592
1593            if self.kernel.is_shutdown_requested() {
1594                self.accumulator.clear();
1595                break;
1596            }
1597
1598            for clock in clocks {
1599                Self::advance_clock_on_accumulator(&mut self.accumulator, clock, ts_now, false);
1600            }
1601
1602            if self.accumulator.peek_next_time() == Some(ts_now) {
1603                self.run_timer_handlers_at(clocks, ts_now, ts_now);
1604                continue;
1605            }
1606
1607            if !self.settle_funding_rates(ts_now)? {
1608                break;
1609            }
1610        }
1611
1612        self.run_venue_modules(ts_now);
1613        self.run_venue_liquidations(ts_now);
1614        Ok(())
1615    }
1616
1617    fn settle_funding_rates(&mut self, ts_now: UnixNanos) -> anyhow::Result<bool> {
1618        let mut due = self
1619            .venues
1620            .iter()
1621            .flat_map(|(venue, exchange)| {
1622                exchange
1623                    .borrow()
1624                    .funding_boundaries_due(ts_now)
1625                    .into_iter()
1626                    .map(|(boundary, instrument_id)| (boundary, *venue, instrument_id))
1627                    .collect::<Vec<_>>()
1628            })
1629            .collect::<Vec<_>>();
1630        due.sort_unstable();
1631
1632        if let Some((boundary, venue, instrument_id)) = due
1633            .iter()
1634            .copied()
1635            .find(|(boundary, _, _)| *boundary < ts_now)
1636        {
1637            return self.fail_funding(format!(
1638                "Late funding boundary for {instrument_id} on {venue}: {boundary} < replay timestamp {ts_now}"
1639            ));
1640        }
1641
1642        if due.is_empty() {
1643            return Ok(false);
1644        }
1645
1646        for (boundary, venue, instrument_id) in due {
1647            if !self.venues[&venue]
1648                .borrow_mut()
1649                .settle_funding_boundary(boundary, instrument_id)
1650            {
1651                return self.fail_funding(format!(
1652                    "Funding settlement failed for {instrument_id} on {venue} at {boundary}"
1653                ));
1654            }
1655        }
1656
1657        let next_boundaries = self
1658            .venues
1659            .iter()
1660            .filter_map(|(venue, exchange)| {
1661                exchange
1662                    .borrow()
1663                    .next_funding_boundary()
1664                    .map(|boundary| (*venue, boundary))
1665            })
1666            .collect::<Vec<_>>();
1667
1668        for (venue, boundary) in next_boundaries {
1669            self.schedule_funding_settlement_if_required(venue, Some(boundary));
1670        }
1671
1672        Ok(true)
1673    }
1674
1675    fn fail_funding<T>(&mut self, error: String) -> anyhow::Result<T> {
1676        if self.funding_error.is_none() {
1677            self.funding_error = Some(error.clone());
1678        }
1679        Err(anyhow::anyhow!(error))
1680    }
1681
1682    fn set_instrument_expiration_timers(&self) -> anyhow::Result<()> {
1683        for exchange in self.venues.values() {
1684            let expirations = exchange.borrow().instrument_expirations();
1685            for (instrument_id, expiration_ns) in expirations {
1686                self.set_instrument_expiration_timer(exchange, instrument_id, expiration_ns)?;
1687            }
1688        }
1689
1690        Ok(())
1691    }
1692
1693    fn set_instrument_expiration_timer(
1694        &self,
1695        exchange: &Rc<RefCell<SimulatedExchange>>,
1696        instrument_id: InstrumentId,
1697        expiration_ns: UnixNanos,
1698    ) -> anyhow::Result<()> {
1699        if expiration_ns == UnixNanos::default() {
1700            return Ok(());
1701        }
1702
1703        let timer_name = Self::instrument_expiration_timer_name(instrument_id.venue, expiration_ns);
1704        let timer_key = ustr::Ustr::from(timer_name.as_str());
1705        if self.kernel.clock.borrow().timer_exists(&timer_key) {
1706            return Ok(());
1707        }
1708
1709        let exchange: Weak<RefCell<SimulatedExchange>> = Rc::downgrade(exchange);
1710        let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event: TimeEvent| {
1711            if let Some(exchange) = exchange.upgrade() {
1712                exchange
1713                    .borrow_mut()
1714                    .process_instrument_expirations(event.ts_event);
1715            }
1716        });
1717        let mut clock = self.kernel.clock.borrow_mut();
1718
1719        clock.set_time_alert_ns(
1720            &timer_name,
1721            expiration_ns,
1722            Some(TimeEventCallback::from(callback)),
1723            None,
1724        )?;
1725
1726        Ok(())
1727    }
1728
1729    fn instrument_expiration_timer_name(venue: Venue, expiration_ns: UnixNanos) -> String {
1730        format!("INSTRUMENT-EXPIRATION:{venue}:{expiration_ns}")
1731    }
1732
1733    fn schedule_funding_settlement_if_required(
1734        &self,
1735        venue: Venue,
1736        settlement_ns: Option<UnixNanos>,
1737    ) {
1738        let Some(settlement_ns) = settlement_ns else {
1739            return;
1740        };
1741
1742        if let Err(e) = self.set_funding_settlement_timer(venue, settlement_ns) {
1743            log::error!("Cannot schedule funding settlement for {venue}: {e}");
1744        }
1745    }
1746
1747    fn set_funding_settlement_timer(
1748        &self,
1749        venue: Venue,
1750        settlement_ns: UnixNanos,
1751    ) -> anyhow::Result<()> {
1752        let timer_name = Self::funding_settlement_timer_name(venue);
1753        let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(|_| {});
1754        let mut clock = self.kernel.clock.borrow_mut();
1755
1756        clock.set_time_alert_ns(
1757            &timer_name,
1758            settlement_ns,
1759            Some(TimeEventCallback::from(callback)),
1760            None,
1761        )?;
1762
1763        Ok(())
1764    }
1765
1766    fn funding_settlement_timer_name(venue: Venue) -> String {
1767        format!("FUNDING-SETTLEMENT:{venue}")
1768    }
1769
1770    fn cancel_funding_settlement_timers(&self) {
1771        let mut clock = self.kernel.clock.borrow_mut();
1772        for venue in self.venues.keys() {
1773            clock.cancel_timer(&Self::funding_settlement_timer_name(*venue));
1774        }
1775    }
1776
1777    fn collect_all_clocks(&self) -> Vec<Rc<RefCell<dyn Clock>>> {
1778        let mut clocks = vec![self.kernel.clock.clone()];
1779        clocks.extend(self.kernel.trader.borrow().get_component_clocks());
1780        clocks
1781    }
1782
1783    fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
1784        self.venues
1785            .values()
1786            .filter_map(|v| v.borrow().max_inflight_command_ts())
1787            .max()
1788    }
1789
1790    fn settle_venues(&self, ts_now: UnixNanos) {
1791        // Advance venue clocks so modules and event generators see the
1792        // correct timestamp even when no commands are pending
1793        for exchange in self.venues.values() {
1794            exchange.borrow().set_clock_time(ts_now);
1795        }
1796
1797        // Drain commands then iterate matching engines to fill newly added
1798        // orders. Fills may enqueue further commands (e.g. hedge orders
1799        // submitted from on_order_filled), so loop until quiescent.
1800        // Only process and iterate venues that had pending commands each
1801        // pass, to avoid extra fill-model rolls on untouched venues.
1802        loop {
1803            // Drain first so commands buffered in the trading queue (e.g. from
1804            // on_stop handlers) reach the venues before we check for activity.
1805            self.drain_command_queues();
1806
1807            let active_venues: Vec<Venue> = self
1808                .venues
1809                .iter()
1810                .filter(|(_, ex)| ex.borrow().has_pending_commands(ts_now))
1811                .map(|(id, _)| *id)
1812                .collect();
1813
1814            if active_venues.is_empty() {
1815                break;
1816            }
1817
1818            for venue_id in &active_venues {
1819                self.venues[venue_id].borrow_mut().process(ts_now);
1820            }
1821            self.drain_command_queues();
1822
1823            for venue_id in &active_venues {
1824                self.venues[venue_id]
1825                    .borrow_mut()
1826                    .iterate_matching_engines(ts_now);
1827            }
1828
1829            // Drain again so fill-triggered commands (e.g. hedge orders
1830            // from on_order_filled) are visible to has_pending_commands
1831            self.drain_command_queues();
1832        }
1833    }
1834
1835    fn run_venue_modules(&mut self, ts_now: UnixNanos) {
1836        if self.last_module_ns == Some(ts_now) {
1837            return;
1838        }
1839        self.last_module_ns = Some(ts_now);
1840
1841        // Pre-settle handler-generated work so modules see final state
1842        self.drain_command_queues();
1843        self.settle_venues(ts_now);
1844
1845        for exchange in self.venues.values() {
1846            exchange.borrow_mut().process_modules(ts_now);
1847        }
1848
1849        // Post-settle any commands emitted by modules
1850        self.drain_command_queues();
1851        self.settle_venues(ts_now);
1852    }
1853
1854    fn run_venue_liquidations(&mut self, ts_now: UnixNanos) {
1855        if self.last_liquidation_ns == Some(ts_now) {
1856            return;
1857        }
1858        self.last_liquidation_ns = Some(ts_now);
1859
1860        for exchange in self.venues.values() {
1861            exchange.borrow_mut().process_liquidations(ts_now);
1862        }
1863
1864        self.drain_command_queues();
1865        self.settle_venues(ts_now);
1866    }
1867
1868    fn drain_exec_client_events(&self) {
1869        for client in &self.exec_clients {
1870            client.drain_queued_events();
1871        }
1872    }
1873
1874    fn drain_command_queues(&self) {
1875        // Drain trading commands, exec client events, and data commands
1876        // in a loop until all queues settle. Handles cascading re-entrancy
1877        // (e.g. strategy submits order from on_order_filled).
1878        loop {
1879            drain_trading_cmd_queue();
1880            drain_data_cmd_queue();
1881            self.drain_exec_client_events();
1882
1883            if trading_cmd_queue_is_empty() && data_cmd_queue_is_empty() {
1884                break;
1885            }
1886        }
1887    }
1888
1889    fn init_command_senders() {
1890        replace_data_cmd_sender(Arc::new(SyncDataCommandSender));
1891        replace_exec_cmd_sender(Arc::new(SyncTradingCommandSender));
1892    }
1893
1894    fn advance_clock_on_accumulator(
1895        accumulator: &mut TimeEventAccumulator,
1896        clock: &Rc<RefCell<dyn Clock>>,
1897        to_time_ns: UnixNanos,
1898        set_time: bool,
1899    ) {
1900        let mut clock_ref = clock.borrow_mut();
1901        let test_clock = clock_ref
1902            .as_any_mut()
1903            .downcast_mut::<TestClock>()
1904            .expect("BacktestEngine requires TestClock");
1905        accumulator.advance_clock(test_clock, to_time_ns, set_time);
1906    }
1907
1908    fn set_all_clocks_time(clocks: &[Rc<RefCell<dyn Clock>>], ts: UnixNanos) {
1909        for clock in clocks {
1910            let mut clock_ref = clock.borrow_mut();
1911            let test_clock = clock_ref
1912                .as_any_mut()
1913                .downcast_mut::<TestClock>()
1914                .expect("BacktestEngine requires TestClock");
1915            test_clock.set_time(ts);
1916        }
1917    }
1918
1919    #[rustfmt::skip]
1920    fn log_pre_run(&self) {
1921        log_info!("=================================================================", color = LogColor::Cyan);
1922        log_info!(" BACKTEST PRE-RUN", color = LogColor::Cyan);
1923        log_info!("=================================================================", color = LogColor::Cyan);
1924
1925        let cache = self.kernel.cache.borrow();
1926        for exchange in self.venues.values() {
1927            let ex = exchange.borrow();
1928            log_info!("=================================================================", color = LogColor::Cyan);
1929            log::info!(" SimulatedVenue {} ({})", ex.id, ex.account_type);
1930            log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1931
1932            if let Some(account) = cache.account_for_venue(&ex.id) {
1933                log::info!("Balances starting:");
1934                let account_ref: &dyn Account = match &*account {
1935                    AccountAny::Margin(margin) => margin,
1936                    AccountAny::Cash(cash) => cash,
1937                    AccountAny::Betting(betting) => betting,
1938                    AccountAny::Wallet(wallet) => wallet,
1939                };
1940
1941                for balance in account_ref.starting_balances().values() {
1942                    log::info!("  {balance}");
1943                }
1944            }
1945        }
1946
1947        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1948    }
1949
1950    #[rustfmt::skip]
1951    fn log_run(&self) {
1952        let config_id = self.run_config_id.as_deref().unwrap_or("None");
1953        let id = format_optional_uuid(self.run_id.as_ref());
1954        let start = format_optional_nanos(self.backtest_start);
1955
1956        log_info!("=================================================================", color = LogColor::Cyan);
1957        log_info!(" BACKTEST RUN", color = LogColor::Cyan);
1958        log_info!("=================================================================", color = LogColor::Cyan);
1959        log::info!("Run config ID:  {config_id}");
1960        log::info!("Run ID:         {id}");
1961        log::info!("Backtest start: {start}");
1962        log::info!("Data elements:  {}", self.data_len);
1963        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
1964    }
1965
1966    #[rustfmt::skip]
1967    fn log_post_run(&self) {
1968        let cache = self.kernel.cache.borrow();
1969        let orders = cache.orders(None, None, None, None, None);
1970        let total_events = event_count_as_usize(self.kernel.exec_engine.borrow().event_count());
1971        let total_orders = orders.len();
1972        let positions: Vec<Position> = cache
1973            .positions(None, None, None, None, None)
1974            .into_iter()
1975            .map(|p| p.cloned())
1976            .collect();
1977        let total_positions = Self::total_positions_with_snapshots(&cache, positions.len());
1978
1979        let config_id = self.run_config_id.as_deref().unwrap_or("None");
1980        let id = format_optional_uuid(self.run_id.as_ref());
1981        let started = format_optional_nanos(self.run_started);
1982        let finished = format_optional_nanos(self.run_finished);
1983        let elapsed = format_optional_duration(self.run_started, self.run_finished);
1984        let bt_start = format_optional_nanos(self.backtest_start);
1985        let bt_end = format_optional_nanos(self.backtest_end);
1986        let bt_range = format_optional_duration(self.backtest_start, self.backtest_end);
1987        let iterations = self.iteration.separate_with_underscores();
1988        let events = total_events.separate_with_underscores();
1989        let num_orders = total_orders.separate_with_underscores();
1990        let num_positions = total_positions.separate_with_underscores();
1991
1992        log_info!("=================================================================", color = LogColor::Cyan);
1993        log_info!(" BACKTEST POST-RUN", color = LogColor::Cyan);
1994        log_info!("=================================================================", color = LogColor::Cyan);
1995        log::info!("Run config ID:  {config_id}");
1996        log::info!("Run ID:         {id}");
1997        log::info!("Run started:    {started}");
1998        log::info!("Run finished:   {finished}");
1999        log::info!("Elapsed time:   {elapsed}");
2000        log::info!("Backtest start: {bt_start}");
2001        log::info!("Backtest end:   {bt_end}");
2002        log::info!("Backtest range: {bt_range}");
2003        log::info!("Iterations: {iterations}");
2004        log::info!("Total events: {events}");
2005        log::info!("Total orders: {num_orders}");
2006        log::info!("Total positions: {num_positions}");
2007
2008        if !self.config.run_analysis {
2009            return;
2010        }
2011
2012        let accounts = cache.accounts_all_owned();
2013        let mut snapshots = Vec::new();
2014        for position in &positions {
2015            snapshots.extend(cache.position_snapshots(Some(&position.id), None));
2016        }
2017        let recorded = self.kernel.portfolio.borrow().recorded_realized_pnls();
2018        let portfolio = self.kernel.portfolio.borrow();
2019        let portfolio_snapshots = accounts
2020            .iter()
2021            .flat_map(|account| portfolio.snapshots(&account.id()))
2022            .collect::<Vec<_>>();
2023        let analyzer = PortfolioAnalyzer::from_accounts_with_snapshots(
2024            &accounts,
2025            &positions,
2026            &snapshots,
2027            &portfolio_snapshots,
2028            recorded,
2029        );
2030        log_portfolio_performance(&analyzer);
2031    }
2032
2033    fn total_positions_with_snapshots(cache: &Cache, cached_positions_count: usize) -> usize {
2034        cached_positions_count + cache.position_snapshots(None, None).len()
2035    }
2036
2037    /// Registers a data client for the given `client_id` if one does not already exist.
2038    pub fn add_data_client_if_not_exists(&mut self, client_id: ClientId) {
2039        if self
2040            .kernel
2041            .data_engine
2042            .borrow()
2043            .registered_clients()
2044            .contains(&client_id)
2045        {
2046            return;
2047        }
2048
2049        let venue = Venue::from(client_id.as_str());
2050        let backtest_client = BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2051        let data_client_adapter = DataClientAdapter::new(
2052            backtest_client.client_id,
2053            None,
2054            false,
2055            false,
2056            Box::new(backtest_client),
2057        );
2058
2059        self.kernel
2060            .data_engine
2061            .borrow_mut()
2062            .register_client(data_client_adapter, None);
2063    }
2064
2065    /// Registers a market data client for the given `venue` if one does not already exist.
2066    pub fn add_market_data_client_if_not_exists(&mut self, venue: Venue) {
2067        let client_id = ClientId::from(venue.as_str());
2068
2069        if !self
2070            .kernel
2071            .data_engine
2072            .borrow()
2073            .registered_clients()
2074            .contains(&client_id)
2075        {
2076            let backtest_client =
2077                BacktestDataClient::new(client_id, venue, self.kernel.cache.clone());
2078            let data_client_adapter = DataClientAdapter::new(
2079                client_id,
2080                Some(venue),
2081                false,
2082                false,
2083                Box::new(backtest_client),
2084            );
2085            self.kernel
2086                .data_engine
2087                .borrow_mut()
2088                .register_client(data_client_adapter, Some(venue));
2089        }
2090    }
2091}
2092
2093fn format_optional_nanos(nanos: Option<UnixNanos>) -> String {
2094    nanos.map_or("None".to_string(), unix_nanos_to_iso8601)
2095}
2096
2097fn format_optional_uuid(uuid: Option<&UUID4>) -> String {
2098    uuid.map_or("None".to_string(), ToString::to_string)
2099}
2100
2101fn event_count_as_usize(event_count: u64) -> usize {
2102    usize::try_from(event_count).expect("execution event count fits usize")
2103}
2104
2105fn format_optional_duration(start: Option<UnixNanos>, end: Option<UnixNanos>) -> String {
2106    match (start, end) {
2107        (Some(s), Some(e)) => {
2108            let delta = s.to_datetime_utc().duration_until(e.to_datetime_utc());
2109            let days = delta.as_hours().abs() / 24;
2110            let hours = delta.as_hours().abs() % 24;
2111            let minutes = delta.as_mins().abs() % 60;
2112            let seconds = delta.as_secs().abs() % 60;
2113            let micros = delta.subsec_nanos().unsigned_abs() / 1_000;
2114            format!("{days} days {hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
2115        }
2116        _ => "None".to_string(),
2117    }
2118}
2119
2120#[rustfmt::skip]
2121fn log_portfolio_performance(analyzer: &PortfolioAnalyzer) {
2122    log_info!("=================================================================", color = LogColor::Cyan);
2123    log_info!(" PORTFOLIO PERFORMANCE", color = LogColor::Cyan);
2124    log_info!("=================================================================", color = LogColor::Cyan);
2125
2126    for currency in analyzer.currencies() {
2127        log::info!(" PnL Statistics ({})", currency.code);
2128        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2129
2130        if let Ok(pnl_lines) = analyzer.get_stats_pnls_formatted(Some(currency), None) {
2131            for line in &pnl_lines {
2132                log::info!("{line}");
2133            }
2134        }
2135
2136        log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2137    }
2138
2139    log::info!(" Returns Statistics");
2140    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2141
2142    for line in &analyzer.get_stats_returns_formatted() {
2143        log::info!("{line}");
2144    }
2145    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2146
2147    log::info!(" General Statistics");
2148    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2149
2150    for line in &analyzer.get_stats_general_formatted() {
2151        log::info!("{line}");
2152    }
2153    log_info!("-----------------------------------------------------------------", color = LogColor::Cyan);
2154}
2155
2156#[cfg(test)]
2157mod tests {
2158    use std::cell::Cell;
2159
2160    use indexmap::IndexMap;
2161    use nautilus_common::{
2162        actor::DataActor,
2163        enums::Environment,
2164        messages::{
2165            data::{DataCommand, UnsubscribeCommand},
2166            execution::{ModifyOrder, SubmitOrder, TradingCommand},
2167        },
2168        msgbus::{
2169            self, MessagingSwitchboard, TypedHandler,
2170            stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
2171        },
2172    };
2173    use nautilus_execution::engine::{SnapshotAnchorer, stubs::StubExecutionClient};
2174    use nautilus_model::{
2175        data::{Data, InstrumentStatus, QuoteTick},
2176        enums::{
2177            AccountType, BookType, MarketStatus, MarketStatusAction, OmsType, OrderSide,
2178            OrderStatus, OrderType, TriggerType,
2179        },
2180        events::OrderEventAny,
2181        identifiers::{AccountId, ActorId, ClientId, ClientOrderId, PositionId, StrategyId, Venue},
2182        instruments::{
2183            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
2184        },
2185        orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
2186        types::{Money, Price, Quantity},
2187    };
2188    use nautilus_system::{KernelEventStore, RegisteredComponents};
2189    use nautilus_testkit::{
2190        cache::TestCacheDatabaseControl,
2191        components::{StateActor, StateStrategy},
2192    };
2193    use nautilus_trading::{
2194        nautilus_strategy,
2195        strategy::{config::StrategyConfig, core::StrategyCore},
2196    };
2197    use rstest::*;
2198    use ustr::Ustr;
2199
2200    use super::*;
2201
2202    #[derive(Debug)]
2203    struct BacktestReplayKernelEventStore {
2204        fail_restore: bool,
2205    }
2206
2207    impl KernelEventStore for BacktestReplayKernelEventStore {
2208        fn restore_parent_cache(
2209            &mut self,
2210            _instance_id: UUID4,
2211            _cache: &mut Cache,
2212        ) -> anyhow::Result<()> {
2213            if self.fail_restore {
2214                anyhow::bail!("replay restore failed");
2215            }
2216
2217            Ok(())
2218        }
2219
2220        fn open(
2221            &mut self,
2222            _instance_id: UUID4,
2223            _components: &RegisteredComponents,
2224            _environment: Environment,
2225        ) -> anyhow::Result<()> {
2226            Ok(())
2227        }
2228
2229        fn snapshot_anchorer(&self) -> Option<SnapshotAnchorer> {
2230            None
2231        }
2232
2233        fn seal(&mut self, _ts_init: UnixNanos) {}
2234
2235        fn run_id(&self) -> Option<&str> {
2236            Some("replay-child")
2237        }
2238
2239        fn parent_run_id(&self) -> Option<&str> {
2240            Some("seed-run")
2241        }
2242
2243        fn is_event_store_replay_configured(&self) -> bool {
2244            true
2245        }
2246
2247        fn is_halted(&self) -> bool {
2248            false
2249        }
2250    }
2251
2252    #[derive(Debug)]
2253    struct TestStrategy {
2254        core: StrategyCore,
2255    }
2256
2257    impl TestStrategy {
2258        fn new(config: StrategyConfig) -> Self {
2259            Self {
2260                core: StrategyCore::new(config),
2261            }
2262        }
2263    }
2264
2265    impl DataActor for TestStrategy {}
2266
2267    nautilus_strategy!(TestStrategy);
2268
2269    fn create_engine() -> BacktestEngine {
2270        let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2271        let venue_config = SimulatedVenueConfig::builder()
2272            .venue(Venue::from("BINANCE"))
2273            .oms_type(OmsType::Netting)
2274            .account_type(AccountType::Margin)
2275            .book_type(BookType::L1_MBP)
2276            .starting_balances(vec![Money::from("1_000_000 USDT")])
2277            .build()
2278            .unwrap();
2279        engine.add_venue(venue_config).unwrap();
2280        engine
2281    }
2282
2283    fn create_immediate_engine(instrument: &CryptoPerpetual) -> BacktestEngine {
2284        let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2285        let venue_config = SimulatedVenueConfig::builder()
2286            .venue(instrument.id().venue)
2287            .oms_type(OmsType::Netting)
2288            .account_type(AccountType::Margin)
2289            .book_type(BookType::L1_MBP)
2290            .starting_balances(vec![Money::from("1_000_000 USDT")])
2291            .use_message_queue(false)
2292            .build()
2293            .unwrap();
2294        engine.add_venue(venue_config).unwrap();
2295        engine
2296            .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
2297            .unwrap();
2298        engine
2299            .venues
2300            .get(&instrument.id().venue)
2301            .unwrap()
2302            .borrow_mut()
2303            .initialize_account();
2304        engine
2305    }
2306
2307    fn send_execution_command(command: TradingCommand) {
2308        msgbus::send_trading_command(MessagingSwitchboard::exec_engine_execute(), command);
2309    }
2310
2311    #[rstest]
2312    fn test_immediate_submit_defers_order_events(crypto_perpetual_ethusdt: CryptoPerpetual) {
2313        let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2314        let order = OrderTestBuilder::new(OrderType::Limit)
2315            .trader_id(engine.trader_id())
2316            .instrument_id(crypto_perpetual_ethusdt.id)
2317            .client_order_id(ClientOrderId::from("O-IMMEDIATE-SUBMIT"))
2318            .side(OrderSide::Buy)
2319            .quantity(Quantity::from("1.000"))
2320            .price(Price::from("1000.00"))
2321            .build();
2322        engine
2323            .kernel
2324            .cache
2325            .borrow_mut()
2326            .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2327            .unwrap();
2328
2329        send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
2330            order.trader_id(),
2331            Some(ClientId::from("BINANCE")),
2332            order.strategy_id(),
2333            order.instrument_id(),
2334            order.client_order_id(),
2335            order.init_event().clone(),
2336            order.exec_algorithm_id(),
2337            None,
2338            None,
2339            UUID4::new(),
2340            UnixNanos::default(),
2341            None,
2342        )));
2343
2344        {
2345            let cache = engine.kernel.cache.borrow();
2346            let cached_order = cache.order(&order.client_order_id()).unwrap();
2347            assert_eq!(cached_order.status(), OrderStatus::Initialized);
2348            assert_eq!(cached_order.event_count(), 1);
2349        }
2350
2351        engine.drain_command_queues();
2352
2353        let cache = engine.kernel.cache.borrow();
2354        let cached_order = cache.order(&order.client_order_id()).unwrap();
2355        let events = cached_order.events();
2356        assert!(matches!(events[1], OrderEventAny::Submitted(_)));
2357        assert!(matches!(events[2], OrderEventAny::Accepted(_)));
2358    }
2359
2360    #[rstest]
2361    fn test_immediate_modify_submitted_order_defers_updated_event(
2362        crypto_perpetual_ethusdt: CryptoPerpetual,
2363    ) {
2364        let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2365        let order = OrderTestBuilder::new(OrderType::Limit)
2366            .trader_id(engine.trader_id())
2367            .instrument_id(crypto_perpetual_ethusdt.id)
2368            .client_order_id(ClientOrderId::from("O-IMMEDIATE-MODIFY"))
2369            .side(OrderSide::Buy)
2370            .quantity(Quantity::from("1.000"))
2371            .price(Price::from("1000.00"))
2372            .build();
2373        let account_id = AccountId::from("BINANCE-001");
2374        engine
2375            .kernel
2376            .cache
2377            .borrow_mut()
2378            .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2379            .unwrap();
2380        engine
2381            .kernel
2382            .cache
2383            .borrow_mut()
2384            .update_order(&TestOrderEventStubs::submitted(&order, account_id))
2385            .unwrap();
2386
2387        send_execution_command(TradingCommand::ModifyOrder(ModifyOrder::new(
2388            order.trader_id(),
2389            Some(ClientId::from("BINANCE")),
2390            order.strategy_id(),
2391            order.instrument_id(),
2392            order.client_order_id(),
2393            None,
2394            Some(Quantity::from("2.000")),
2395            None,
2396            None,
2397            UUID4::new(),
2398            UnixNanos::from(1),
2399            None,
2400            None,
2401        )));
2402
2403        {
2404            let cache = engine.kernel.cache.borrow();
2405            let cached_order = cache.order(&order.client_order_id()).unwrap();
2406            assert_eq!(cached_order.quantity(), Quantity::from("1.000"));
2407            assert!(matches!(
2408                cached_order.events().last(),
2409                Some(OrderEventAny::Submitted(_))
2410            ));
2411        }
2412
2413        engine.drain_command_queues();
2414
2415        let cache = engine.kernel.cache.borrow();
2416        let order = cache.order(&order.client_order_id()).unwrap();
2417        assert_eq!(order.quantity(), Quantity::from("2.000"));
2418        assert!(matches!(
2419            order.events().last(),
2420            Some(OrderEventAny::Updated(_))
2421        ));
2422    }
2423
2424    #[rstest]
2425    fn test_immediate_market_data_dispatches_fill_synchronously(
2426        crypto_perpetual_ethusdt: CryptoPerpetual,
2427    ) {
2428        let engine = create_immediate_engine(&crypto_perpetual_ethusdt);
2429        let order = OrderTestBuilder::new(OrderType::Limit)
2430            .trader_id(engine.trader_id())
2431            .instrument_id(crypto_perpetual_ethusdt.id)
2432            .client_order_id(ClientOrderId::from("O-IMMEDIATE-QUOTE-FILL"))
2433            .side(OrderSide::Buy)
2434            .quantity(Quantity::from("1.000"))
2435            .price(Price::from("1000.00"))
2436            .build();
2437        engine
2438            .kernel
2439            .cache
2440            .borrow_mut()
2441            .add_order(order.clone(), None, Some(ClientId::from("BINANCE")), false)
2442            .unwrap();
2443
2444        send_execution_command(TradingCommand::SubmitOrder(SubmitOrder::new(
2445            order.trader_id(),
2446            Some(ClientId::from("BINANCE")),
2447            order.strategy_id(),
2448            order.instrument_id(),
2449            order.client_order_id(),
2450            order.init_event().clone(),
2451            order.exec_algorithm_id(),
2452            None,
2453            None,
2454            UUID4::new(),
2455            UnixNanos::default(),
2456            None,
2457        )));
2458        engine.drain_command_queues();
2459
2460        let quote = QuoteTick::new(
2461            order.instrument_id(),
2462            Price::from("999.00"),
2463            Price::from("1000.00"),
2464            Quantity::from("1.000"),
2465            Quantity::from("1.000"),
2466            UnixNanos::from(1),
2467            UnixNanos::from(1),
2468        );
2469        msgbus::send_quote(
2470            format!(
2471                "SimulatedExchange.process_new_quote.{}",
2472                order.instrument_id().venue
2473            )
2474            .into(),
2475            &quote,
2476        );
2477
2478        let cache = engine.kernel.cache.borrow();
2479        let cached_order = cache.order(&order.client_order_id()).unwrap();
2480        assert_eq!(cached_order.status(), OrderStatus::Filled);
2481        assert!(matches!(
2482            cached_order.events().last(),
2483            Some(OrderEventAny::Filled(_))
2484        ));
2485    }
2486
2487    #[rstest]
2488    fn test_timer_handler_sets_last_ns_to_fire_time() {
2489        let mut engine = create_engine();
2490        engine.last_ns = UnixNanos::from(30);
2491        let fired = Rc::new(Cell::new(false));
2492        let fired_clone = Rc::clone(&fired);
2493        let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
2494            fired_clone.set(true);
2495        }));
2496        engine
2497            .kernel
2498            .clock
2499            .borrow_mut()
2500            .set_timer_ns(
2501                "ROLL",
2502                1,
2503                Some(UnixNanos::from(20)),
2504                None,
2505                Some(callback),
2506                Some(true),
2507                Some(true),
2508            )
2509            .unwrap();
2510        let clocks = engine.collect_all_clocks();
2511
2512        for clock in &clocks {
2513            BacktestEngine::advance_clock_on_accumulator(
2514                &mut engine.accumulator,
2515                clock,
2516                UnixNanos::from(30),
2517                false,
2518            );
2519        }
2520        engine.run_timer_handlers_at(&clocks, UnixNanos::from(20), UnixNanos::from(30));
2521
2522        assert!(fired.get());
2523        assert_eq!(engine.last_ns, UnixNanos::from(20));
2524    }
2525
2526    #[rstest]
2527    #[case::complete(false, 25)]
2528    #[case::shutdown(true, 20)]
2529    fn test_flush_accumulator_events_sets_last_ns_for_completion(
2530        #[case] shutdown: bool,
2531        #[case] expected_last_ns: u64,
2532    ) {
2533        let mut engine = create_engine();
2534        let last_ns = UnixNanos::from(25);
2535        let ts_now = UnixNanos::from(30);
2536        engine.last_ns = last_ns;
2537        let clocks = engine.collect_all_clocks();
2538        BacktestEngine::set_all_clocks_time(&clocks, last_ns);
2539        let fired = Rc::new(Cell::new(false));
2540        let fired_clone = Rc::clone(&fired);
2541        let observed_ns = Rc::new(Cell::new(UnixNanos::default()));
2542        let observed_ns_clone = Rc::clone(&observed_ns);
2543        let clock = Rc::clone(&engine.kernel.clock);
2544        let shutdown_requested = engine.kernel.shutdown_flag();
2545        let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
2546            fired_clone.set(true);
2547            observed_ns_clone.set(clock.borrow().timestamp_ns());
2548            shutdown_requested.set(shutdown);
2549        }));
2550        engine
2551            .kernel
2552            .clock
2553            .borrow_mut()
2554            .set_timer_ns(
2555                "ROLL",
2556                100,
2557                Some(UnixNanos::from(20)),
2558                None,
2559                Some(callback),
2560                Some(true),
2561                Some(true),
2562            )
2563            .unwrap();
2564
2565        engine.flush_accumulator_events(&clocks, ts_now).unwrap();
2566
2567        assert!(fired.get());
2568        assert_eq!(observed_ns.get(), UnixNanos::from(20));
2569        assert_eq!(engine.kernel.is_shutdown_requested(), shutdown);
2570        assert_eq!(engine.last_ns, UnixNanos::from(expected_last_ns));
2571    }
2572
2573    #[rstest]
2574    fn test_add_duplicate_venue_preserves_original_exchange(
2575        crypto_perpetual_ethusdt: CryptoPerpetual,
2576    ) {
2577        let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2578        let venue = Venue::from("BINANCE");
2579        let venue_config = SimulatedVenueConfig::builder()
2580            .venue(venue)
2581            .oms_type(OmsType::Netting)
2582            .account_type(AccountType::Margin)
2583            .book_type(BookType::L1_MBP)
2584            .starting_balances(vec![Money::from("1_000_000 USDT")])
2585            .build()
2586            .unwrap();
2587        engine.add_venue(venue_config).unwrap();
2588
2589        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
2590        let instrument_id = instrument.id();
2591        engine.add_instrument(&instrument).unwrap();
2592
2593        let initial_quote = QuoteTick::new(
2594            instrument_id,
2595            Price::from("1000.00"),
2596            Price::from("1001.00"),
2597            Quantity::from("1.000"),
2598            Quantity::from("1.000"),
2599            UnixNanos::from(1),
2600            UnixNanos::from(1),
2601        );
2602        msgbus::send_quote(
2603            format!("SimulatedExchange.process_new_quote.{venue}").into(),
2604            &initial_quote,
2605        );
2606
2607        let best_bid_before = engine
2608            .venues
2609            .get(&venue)
2610            .unwrap()
2611            .borrow()
2612            .best_bid_price(instrument_id);
2613        let best_ask_before = engine
2614            .venues
2615            .get(&venue)
2616            .unwrap()
2617            .borrow()
2618            .best_ask_price(instrument_id);
2619        let original_exchange = Rc::downgrade(engine.venues.get(&venue).unwrap());
2620        let venues_before = engine.list_venues();
2621        let exec_clients_len_before = engine.exec_clients.len();
2622        let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
2623        let duplicate_config = SimulatedVenueConfig::builder()
2624            .venue(venue)
2625            .oms_type(OmsType::Netting)
2626            .account_type(AccountType::Margin)
2627            .book_type(BookType::L1_MBP)
2628            .starting_balances(vec![Money::from("1_000_000 USDT")])
2629            .build()
2630            .unwrap();
2631        assert!(engine.add_venue(duplicate_config).is_err());
2632
2633        let original_exchange = original_exchange
2634            .upgrade()
2635            .expect("the original exchange must remain alive");
2636        assert!(Rc::ptr_eq(
2637            &original_exchange,
2638            engine.venues.get(&venue).unwrap()
2639        ));
2640        assert_eq!(engine.list_venues(), venues_before);
2641        assert_eq!(engine.exec_clients.len(), exec_clients_len_before);
2642        assert_eq!(
2643            engine.kernel.exec_engine.borrow().client_ids(),
2644            client_ids_before
2645        );
2646
2647        let distinct_quote = QuoteTick::new(
2648            instrument_id,
2649            Price::from("2000.00"),
2650            Price::from("2001.00"),
2651            Quantity::from("2.000"),
2652            Quantity::from("2.000"),
2653            UnixNanos::from(2),
2654            UnixNanos::from(2),
2655        );
2656        msgbus::send_quote(
2657            format!("SimulatedExchange.process_new_quote.{venue}").into(),
2658            &distinct_quote,
2659        );
2660
2661        let original_exchange = original_exchange.borrow();
2662        let best_bid_after = original_exchange.best_bid_price(instrument_id);
2663        let best_ask_after = original_exchange.best_ask_price(instrument_id);
2664        assert_ne!(best_bid_after, best_bid_before);
2665        assert_ne!(best_ask_after, best_ask_before);
2666        assert_eq!(best_bid_after, Some(Price::from("2000.00")));
2667        assert_eq!(best_ask_after, Some(Price::from("2001.00")));
2668    }
2669
2670    #[rstest]
2671    fn test_add_venue_execution_registration_failure_publishes_nothing() {
2672        let mut engine = BacktestEngine::new(BacktestEngineConfig::default()).unwrap();
2673        let venue = Venue::from("SIM");
2674        engine
2675            .kernel
2676            .exec_engine
2677            .borrow_mut()
2678            .register_client(Box::new(StubExecutionClient::new(
2679                ClientId::from(venue.as_str()),
2680                AccountId::from("SIM-001"),
2681                venue,
2682                OmsType::Netting,
2683                None,
2684            )))
2685            .unwrap();
2686        let client_ids_before = engine.kernel.exec_engine.borrow().client_ids();
2687
2688        let endpoint = format!("SimulatedExchange.process_new_quote.{venue}");
2689        let received_quotes = Rc::new(RefCell::new(Vec::new()));
2690        let received_quotes_handler = Rc::clone(&received_quotes);
2691        let sentinel = TypedHandler::from_with_id("venue-setup-sentinel", move |quote| {
2692            received_quotes_handler.borrow_mut().push(*quote);
2693        });
2694        msgbus::register_quote_endpoint(endpoint.as_str().into(), sentinel);
2695
2696        let venue_config = SimulatedVenueConfig::builder()
2697            .venue(venue)
2698            .oms_type(OmsType::Netting)
2699            .account_type(AccountType::Margin)
2700            .book_type(BookType::L1_MBP)
2701            .starting_balances(vec![Money::from("1_000_000 USD")])
2702            .build()
2703            .unwrap();
2704        assert!(engine.add_venue(venue_config).is_err());
2705
2706        assert!(!engine.venues.contains_key(&venue));
2707        assert!(engine.exec_clients.is_empty());
2708        assert_eq!(
2709            engine.kernel.exec_engine.borrow().client_ids(),
2710            client_ids_before
2711        );
2712
2713        let quote = QuoteTick::new(
2714            InstrumentId::from("TEST.SIM"),
2715            Price::from("100.00"),
2716            Price::from("101.00"),
2717            Quantity::from("1"),
2718            Quantity::from("1"),
2719            UnixNanos::from(1),
2720            UnixNanos::from(1),
2721        );
2722        msgbus::send_quote(endpoint.as_str().into(), &quote);
2723        assert_eq!(received_quotes.borrow().as_slice(), &[quote]);
2724    }
2725
2726    #[rstest]
2727    fn test_add_strategy_registers_configured_hedging_oms_type() {
2728        let mut engine = create_engine();
2729        let instrument = crypto_perpetual_ethusdt();
2730        let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-001");
2731
2732        engine
2733            .add_instrument(&InstrumentAny::CryptoPerpetual(instrument.clone()))
2734            .unwrap();
2735        engine
2736            .add_strategy(TestStrategy::new(StrategyConfig {
2737                strategy_id: Some(strategy_id),
2738                oms_type: Some(OmsType::Hedging),
2739                ..Default::default()
2740            }))
2741            .unwrap();
2742
2743        let order = OrderTestBuilder::new(OrderType::Market)
2744            .trader_id(engine.trader_id())
2745            .strategy_id(strategy_id)
2746            .instrument_id(instrument.id())
2747            .quantity(Quantity::from("1.000"))
2748            .build();
2749        let position_id = PositionId::new("CUSTOM-POSITION-001");
2750
2751        engine
2752            .kernel
2753            .exec_engine
2754            .borrow()
2755            .cache()
2756            .borrow_mut()
2757            .add_order(
2758                order.clone(),
2759                Some(position_id),
2760                Some(ClientId::from("BINANCE")),
2761                true,
2762            )
2763            .unwrap();
2764
2765        let submit_order = SubmitOrder::new(
2766            order.trader_id(),
2767            Some(ClientId::from("BINANCE")),
2768            strategy_id,
2769            instrument.id(),
2770            order.client_order_id(),
2771            order.init_event().clone(),
2772            order.exec_algorithm_id(),
2773            Some(position_id),
2774            None,
2775            UUID4::new(),
2776            UnixNanos::default(),
2777            None,
2778        );
2779
2780        engine
2781            .kernel
2782            .exec_engine
2783            .borrow()
2784            .execute(TradingCommand::SubmitOrder(submit_order));
2785
2786        let exec_engine = engine.kernel.exec_engine.borrow();
2787        let cache = exec_engine.cache().borrow();
2788        let cached_order = cache
2789            .order(&order.client_order_id())
2790            .expect("Order should be cached");
2791
2792        assert_eq!(cached_order.status(), OrderStatus::Initialized);
2793    }
2794
2795    fn create_engine_with_replay_store(fail_restore: bool) -> BacktestEngine {
2796        let config = BacktestEngineConfig {
2797            load_state: true,
2798            run_analysis: false,
2799            ..Default::default()
2800        };
2801        let mut engine = BacktestEngine::new(config.clone()).unwrap();
2802        let event_store_factory = move |_instance_id: UUID4, _clock: Rc<RefCell<dyn Clock>>| {
2803            Ok::<_, anyhow::Error>(Box::new(BacktestReplayKernelEventStore { fail_restore })
2804                as Box<dyn KernelEventStore>)
2805        };
2806
2807        engine.kernel = NautilusKernel::new_with(
2808            "BacktestEngine".to_string(),
2809            config,
2810            None,
2811            Some(Box::new(event_store_factory)),
2812        )
2813        .unwrap();
2814        engine.instance_id = engine.kernel.instance_id;
2815        engine
2816    }
2817
2818    fn create_stop_market_order(instrument: &CryptoPerpetual) -> OrderAny {
2819        OrderTestBuilder::new(OrderType::StopMarket)
2820            .instrument_id(instrument.id())
2821            .side(OrderSide::Buy)
2822            .trigger_price(Price::from("5100.00"))
2823            .quantity(Quantity::from(1))
2824            .emulation_trigger(TriggerType::BidAsk)
2825            .build()
2826    }
2827
2828    fn create_submit_order_command(order: &OrderAny) -> SubmitOrder {
2829        SubmitOrder::new(
2830            order.trader_id(),
2831            None,
2832            order.strategy_id(),
2833            order.instrument_id(),
2834            order.client_order_id(),
2835            order.init_event().clone(),
2836            order.exec_algorithm_id(),
2837            None,
2838            None,
2839            UUID4::new(),
2840            0.into(),
2841            None, // correlation_id
2842        )
2843    }
2844
2845    fn register_data_command_handler(id: &str) -> TypedIntoMessageSavingHandler<DataCommand> {
2846        let (handler, saving_handler) =
2847            get_typed_into_message_saving_handler::<DataCommand>(Some(Ustr::from(id)));
2848        msgbus::register_data_command_endpoint(
2849            MessagingSwitchboard::data_engine_queue_execute(),
2850            handler,
2851        );
2852        saving_handler
2853    }
2854
2855    #[rstest]
2856    fn test_run_impl_event_store_replay_skips_trader_start() {
2857        let mut engine = create_engine_with_replay_store(false);
2858
2859        engine
2860            .run_impl(
2861                Some(UnixNanos::from(0)),
2862                Some(UnixNanos::from(1)),
2863                None,
2864                true,
2865            )
2866            .unwrap();
2867
2868        assert!(engine.kernel.is_event_store_replay_configured());
2869        assert!(engine.kernel.is_event_store_replay());
2870        assert!(!engine.kernel.trader.borrow().is_running());
2871    }
2872
2873    #[rstest]
2874    fn test_run_impl_event_store_replay_config_failure_errors() {
2875        let mut engine = create_engine_with_replay_store(true);
2876
2877        let error = engine
2878            .run_impl(
2879                Some(UnixNanos::from(0)),
2880                Some(UnixNanos::from(1)),
2881                None,
2882                true,
2883            )
2884            .unwrap_err();
2885
2886        assert_eq!(error.to_string(), "event-store replay did not start");
2887        assert!(engine.kernel.is_event_store_replay_configured());
2888        assert!(!engine.kernel.is_event_store_replay());
2889        assert!(!engine.kernel.trader.borrow().is_running());
2890    }
2891
2892    #[rstest]
2893    fn test_backtest_state_persistence_loads_before_start_and_saves_after_settle() {
2894        let actor_id = ActorId::from("BACKTEST-STATE-ACTOR");
2895        let strategy_id = StrategyId::from("BACKTEST-STATE-STRATEGY-001");
2896        let actor_load = IndexMap::from([("actor-load".to_string(), b"actor-loaded".to_vec())]);
2897        let strategy_load =
2898            IndexMap::from([("strategy-load".to_string(), b"strategy-loaded".to_vec())]);
2899        let actor_save = IndexMap::from([("actor-save".to_string(), b"actor-saved".to_vec())]);
2900        let strategy_save =
2901            IndexMap::from([("strategy-save".to_string(), b"strategy-saved".to_vec())]);
2902        let (database, control) = TestCacheDatabaseControl::create();
2903        control.set_actor_state(actor_id, &actor_load);
2904        control.set_strategy_state(strategy_id, &strategy_load);
2905        let config = BacktestEngineConfig {
2906            load_state: true,
2907            save_state: true,
2908            run_analysis: false,
2909            ..Default::default()
2910        };
2911        let mut engine = BacktestEngine::new(config).unwrap();
2912        engine
2913            .kernel
2914            .cache
2915            .borrow_mut()
2916            .set_database(Box::new(database));
2917        engine
2918            .add_actor(StateActor::new(
2919                actor_id,
2920                control.clone(),
2921                actor_save.clone(),
2922            ))
2923            .unwrap();
2924        engine
2925            .add_strategy(StateStrategy::new(
2926                strategy_id,
2927                control.clone(),
2928                strategy_save.clone(),
2929            ))
2930            .unwrap();
2931
2932        engine
2933            .run(
2934                Some(UnixNanos::from(0)),
2935                Some(UnixNanos::from(1)),
2936                None,
2937                false,
2938            )
2939            .unwrap();
2940        engine.dispose();
2941
2942        assert_eq!(
2943            control.events(),
2944            vec![
2945                "actor.load:BACKTEST-STATE-ACTOR",
2946                "actor.on_load",
2947                "strategy.load:BACKTEST-STATE-STRATEGY-001",
2948                "strategy.on_load",
2949                "actor.on_start",
2950                "strategy.on_start",
2951                "actor.on_stop",
2952                "strategy.on_stop",
2953                "actor.on_save",
2954                "actor.update:BACKTEST-STATE-ACTOR",
2955                "strategy.on_save",
2956                "strategy.update:BACKTEST-STATE-STRATEGY-001",
2957                "database.close",
2958            ]
2959        );
2960        assert_eq!(control.actor_state(&actor_id), Some(actor_save));
2961        assert_eq!(control.strategy_state(&strategy_id), Some(strategy_save));
2962        assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
2963    }
2964
2965    #[rstest]
2966    fn test_backtest_state_persistence_reports_callback_errors_after_shutdown() {
2967        let actor_id = ActorId::from("BACKTEST-FAIL-SAVE-ACTOR");
2968        let strategy_id = StrategyId::from("BACKTEST-FAIL-SAVE-STRATEGY-001");
2969        let (database, control) = TestCacheDatabaseControl::create();
2970        let config = BacktestEngineConfig {
2971            save_state: true,
2972            run_analysis: false,
2973            ..Default::default()
2974        };
2975        let mut engine = BacktestEngine::new(config).unwrap();
2976        engine
2977            .kernel
2978            .cache
2979            .borrow_mut()
2980            .set_database(Box::new(database));
2981        engine
2982            .add_actor(StateActor::new(actor_id, control.clone(), IndexMap::new()).with_fail_save())
2983            .unwrap();
2984        engine
2985            .add_strategy(
2986                StateStrategy::new(strategy_id, control.clone(), IndexMap::new()).with_fail_save(),
2987            )
2988            .unwrap();
2989
2990        let error = engine
2991            .run(
2992                Some(UnixNanos::from(0)),
2993                Some(UnixNanos::from(1)),
2994                None,
2995                false,
2996            )
2997            .unwrap_err();
2998        engine.dispose();
2999
3000        assert_eq!(
3001            error.to_string(),
3002            "Failed to save component state: actor BACKTEST-FAIL-SAVE-ACTOR callback: test actor \
3003             on_save failure; strategy BACKTEST-FAIL-SAVE-STRATEGY-001 callback: test strategy \
3004             on_save failure"
3005        );
3006        assert_eq!(
3007            control.events(),
3008            vec![
3009                "actor.on_start",
3010                "strategy.on_start",
3011                "actor.on_stop",
3012                "strategy.on_stop",
3013                "actor.on_save",
3014                "strategy.on_save",
3015                "database.close",
3016            ]
3017        );
3018        assert!(!engine.kernel.trader.borrow().is_running());
3019        assert_eq!(engine.backtest_end, Some(UnixNanos::from(0)));
3020    }
3021
3022    #[rstest]
3023    #[case(None)]
3024    #[case(Some(true))]
3025    #[case(Some(false))]
3026    fn test_new_forces_drop_instruments_on_reset_false(
3027        crypto_perpetual_ethusdt: CryptoPerpetual,
3028        #[case] user_value: Option<bool>,
3029    ) {
3030        use nautilus_common::cache::CacheConfig;
3031
3032        let config = match user_value {
3033            None => BacktestEngineConfig::builder().build(),
3034            Some(value) => BacktestEngineConfig::builder()
3035                .cache(
3036                    CacheConfig::builder()
3037                        .drop_instruments_on_reset(value)
3038                        .build()
3039                        .unwrap(),
3040                )
3041                .build(),
3042        };
3043        let mut engine = BacktestEngine::new(config).unwrap();
3044
3045        let venue_config = SimulatedVenueConfig::builder()
3046            .venue(Venue::from("BINANCE"))
3047            .oms_type(OmsType::Netting)
3048            .account_type(AccountType::Margin)
3049            .book_type(BookType::L1_MBP)
3050            .starting_balances(vec![Money::from("1_000_000 USDT")])
3051            .build()
3052            .unwrap();
3053        engine.add_venue(venue_config).unwrap();
3054
3055        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
3056        let instrument_id = instrument.id();
3057        engine.add_instrument(&instrument).unwrap();
3058
3059        engine.reset();
3060
3061        assert!(
3062            engine
3063                .kernel()
3064                .cache
3065                .borrow()
3066                .instrument(&instrument_id)
3067                .is_some(),
3068            "instrument must survive engine.reset(); user-supplied \
3069             drop_instruments_on_reset={user_value:?} must not leak through",
3070        );
3071    }
3072
3073    #[rstest]
3074    fn test_reset_resets_order_emulator_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
3075        let mut engine = create_engine();
3076        let data_commands =
3077            register_data_command_handler("DataEngine.queue_execute.backtest_reset");
3078        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt.clone());
3079        let instrument_id = instrument.id();
3080        engine.add_instrument(&instrument).unwrap();
3081        let order = create_stop_market_order(&crypto_perpetual_ethusdt);
3082        let command = create_submit_order_command(&order);
3083        engine
3084            .kernel
3085            .cache
3086            .borrow_mut()
3087            .add_order(order, None, None, false)
3088            .unwrap();
3089        let order_emulator = engine.kernel.order_emulator.emulator();
3090        let mut order_emulator = order_emulator.borrow_mut();
3091        order_emulator.cache_submit_order_command(command.clone());
3092        order_emulator.handle_submit_order(&command);
3093        drop(order_emulator);
3094        data_commands.clear();
3095
3096        engine.reset();
3097
3098        let commands = data_commands.get_messages();
3099        let emulator = engine.kernel.order_emulator.get_emulator();
3100        assert!(emulator.subscribed_quotes().is_empty());
3101        assert!(emulator.subscribed_trades().is_empty());
3102        assert!(emulator.get_matching_core(&instrument_id).is_none());
3103        assert!(commands.iter().any(|command| matches!(
3104            command,
3105            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(command))
3106                if command.instrument_id == instrument_id
3107        )));
3108    }
3109
3110    #[rstest]
3111    fn test_route_data_to_exchange_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
3112        let mut engine = create_engine();
3113        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
3114        let instrument_id = instrument.id();
3115        engine.add_instrument(&instrument).unwrap();
3116
3117        let status = InstrumentStatus::new(
3118            instrument_id,
3119            MarketStatusAction::Close,
3120            UnixNanos::from(1),
3121            UnixNanos::from(1),
3122            None,
3123            None,
3124            None,
3125            None,
3126            None,
3127        );
3128
3129        engine.route_data_to_exchange(&Data::InstrumentStatus(status));
3130
3131        let exchange = engine.venues.get(&instrument_id.venue).unwrap().borrow();
3132        let market_status = exchange
3133            .get_matching_engine(&instrument_id)
3134            .unwrap()
3135            .market_status;
3136        assert_eq!(market_status, MarketStatus::Closed);
3137    }
3138}