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