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