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