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