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