Skip to main content

nautilus_backtest/
exchange.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//! Provides a `SimulatedExchange` venue for backtesting on historical data.
17
18use std::{
19    cell::{Cell, RefCell},
20    collections::{BTreeMap, BTreeSet, BinaryHeap, VecDeque},
21    fmt::Debug,
22    rc::Rc,
23};
24
25use ahash::AHashMap;
26use indexmap::IndexMap;
27use nautilus_common::{
28    cache::Cache,
29    clients::ExecutionClient,
30    clock::{Clock, TestClock},
31    messages::execution::{ModifyOrder, TradingCommand},
32    msgbus::{self, MessagingSwitchboard, TypedHandler, switchboard},
33};
34use nautilus_core::{
35    DurationNanos, UUID4, UnixNanos,
36    correctness::{CorrectnessResultExt, FAILED, check_equal},
37};
38use nautilus_execution::{
39    matching_core::RestingOrder,
40    matching_engine::{
41        OrderMatchingEngine, config::OrderMatchingEngineConfig, inflight::InflightOrders,
42    },
43    models::{
44        fee::FeeModelHandle,
45        fill::FillModelHandle,
46        latency::{LatencyModel, LatencyModelHandle},
47    },
48};
49use nautilus_model::{
50    accounts::{Account, AccountAny, margin_model::MarginModelHandle},
51    data::{
52        Bar, Data, FundingRateUpdate, InstrumentClose, InstrumentStatus, OrderBookDelta,
53        OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick,
54    },
55    enums::{AccountType, AggressorSide, BookType, OmsType, OrderStatus, PositionAdjustmentType},
56    events::{FundingSettlement, OrderEventAny, OrderUpdated, PositionAdjusted, PositionEvent},
57    identifiers::{AccountId, InstrumentId, Venue},
58    instruments::{Instrument, InstrumentAny},
59    orderbook::OrderBook,
60    orders::{Order, OrderAny},
61    position::Position,
62    types::{AccountBalance, Currency, Money, Price, Quantity},
63};
64use rust_decimal::Decimal;
65use ustr::Ustr;
66
67use crate::{
68    config::SimulatedVenueConfig,
69    modules::{
70        AccountAdjustmentError, AccountAdjustmentOutcome, ExchangeContext, SimulationModule,
71        SimulationModuleHandle, SimulationModuleResult,
72    },
73};
74
75/// Represents commands with simulated network latency in a min-heap priority queue.
76/// The commands are ordered by timestamp for FIFO processing, with the
77/// earliest timestamp having the highest priority in the queue.
78#[derive(Debug, Eq, PartialEq)]
79struct InflightCommand {
80    timestamp: UnixNanos,
81    counter: u32,
82    command: TradingCommand,
83}
84
85impl InflightCommand {
86    const fn new(timestamp: UnixNanos, counter: u32, command: TradingCommand) -> Self {
87        Self {
88            timestamp,
89            counter,
90            command,
91        }
92    }
93
94    fn matches_scope(&self, ts_now: UnixNanos, scope: SettlementScope) -> bool {
95        match scope {
96            SettlementScope::All => true,
97            SettlementScope::Data(instrument_id) => {
98                self.command.ts_init() == ts_now
99                    || instrument_id.is_some_and(|id| self.command.instrument_id() == id)
100            }
101        }
102    }
103}
104
105impl Ord for InflightCommand {
106    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
107        // Reverse ordering for min-heap (earliest timestamp first then lowest counter)
108        other
109            .timestamp
110            .cmp(&self.timestamp)
111            .then_with(|| other.counter.cmp(&self.counter))
112    }
113}
114
115impl PartialOrd for InflightCommand {
116    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
117        Some(self.cmp(other))
118    }
119}
120
121/// Simulated exchange venue for realistic trading execution during backtesting.
122///
123/// The `SimulatedExchange` provides a simulation of a trading venue,
124/// including order matching engines, account management, and realistic execution
125/// models. It maintains order books, processes market data, and executes trades
126/// with configurable latency and fill models to accurately simulate real market
127/// conditions during backtesting.
128///
129/// Key features:
130/// - Multi-instrument order matching with realistic execution
131/// - Configurable fee, fill, and latency models
132/// - Support for various order types and execution options
133/// - Account balance and position management
134/// - Market data processing and order book maintenance
135/// - Simulation modules for custom venue behaviors
136#[expect(
137    clippy::struct_excessive_bools,
138    reason = "exchange state mirrors the existing venue configuration flags"
139)]
140pub struct SimulatedExchange {
141    /// The venue identifier.
142    pub id: Venue,
143    /// The order management system type.
144    pub oms_type: OmsType,
145    /// The account type for the venue.
146    pub account_type: AccountType,
147    /// The optional base currency for single-currency accounts.
148    pub base_currency: Option<Currency>,
149    starting_balances: Vec<Money>,
150    book_type: BookType,
151    default_leverage: Decimal,
152    exec_client: Option<Rc<dyn ExecutionClient>>,
153    event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
154    /// Set only while a trading command is being processed synchronously, which is the
155    /// window in which the execution engine holds a borrow and a re-entrant event would
156    /// panic. Outside it (market data, iteration, expiration, liquidation, open-order
157    /// loading) events dispatch directly, so immediate mode keeps its synchronous timing.
158    deferring_events: Rc<Cell<bool>>,
159    fee_model: FeeModelHandle,
160    fill_model: FillModelHandle,
161    latency_model: Option<LatencyModelHandle>,
162    instruments: AHashMap<InstrumentId, InstrumentAny>,
163    matching_engines: IndexMap<InstrumentId, OrderMatchingEngine>,
164    last_raw_id: u32,
165    pending_funding_rates: BTreeMap<(UnixNanos, InstrumentId), FundingRateUpdate>,
166    funding_settlements: BTreeSet<(UnixNanos, InstrumentId)>,
167    leverages: AHashMap<InstrumentId, Decimal>,
168    margin_model: Option<MarginModelHandle>,
169    modules: Vec<SimulationModuleHandle>,
170    module_error: Option<String>,
171    clock: Rc<RefCell<dyn Clock>>,
172    cache: Rc<RefCell<Cache>>,
173    message_queue: VecDeque<TradingCommand>,
174    inflight_queue: BinaryHeap<InflightCommand>,
175    inflight_orders: InflightOrders,
176    inflight_counter: AHashMap<UnixNanos, u32>,
177    bar_execution: bool,
178    bar_adaptive_high_low_ordering: bool,
179    trade_execution: bool,
180    liquidity_consumption: bool,
181    reject_stop_orders: bool,
182    support_gtd_orders: bool,
183    support_contingent_orders: bool,
184    use_position_ids: bool,
185    use_random_ids: bool,
186    use_reduce_only: bool,
187    use_message_queue: bool,
188    use_market_order_acks: bool,
189    allow_cash_borrowing: bool,
190    frozen_account: bool,
191    queue_position: bool,
192    oto_full_trigger: bool,
193    defer_option_settlement: bool,
194    price_protection_points: u32,
195    liquidation_enabled: bool,
196    liquidation_trigger_ratio: f64,
197    liquidation_cancel_open_orders: bool,
198}
199
200impl Debug for SimulatedExchange {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.debug_struct(stringify!(SimulatedExchange))
203            .field("id", &self.id)
204            .field("account_type", &self.account_type)
205            .finish_non_exhaustive()
206    }
207}
208
209impl SimulatedExchange {
210    /// Creates a new [`SimulatedExchange`] instance from a venue configuration.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if:
215    /// - `starting_balances` is empty.
216    /// - `base_currency` is `Some` but `starting_balances` contains multiple currencies.
217    pub fn new(
218        config: SimulatedVenueConfig,
219        cache: Rc<RefCell<Cache>>,
220        clock: Rc<RefCell<dyn Clock>>,
221    ) -> anyhow::Result<Self> {
222        if config.starting_balances.is_empty() {
223            anyhow::bail!("Starting balances must be provided")
224        }
225
226        if config.base_currency.is_some() && config.starting_balances.len() > 1 {
227            anyhow::bail!("single-currency account has multiple starting currencies")
228        }
229
230        let default_leverage = config.default_leverage.unwrap_or_else(|| {
231            if config.account_type == AccountType::Margin {
232                Decimal::from(10)
233            } else {
234                Decimal::from(1)
235            }
236        });
237
238        Ok(Self {
239            id: config.venue,
240            oms_type: config.oms_type,
241            account_type: config.account_type,
242            base_currency: config.base_currency,
243            starting_balances: config.starting_balances,
244            book_type: config.book_type,
245            default_leverage,
246            exec_client: None,
247            event_handler: None,
248            deferring_events: Rc::new(Cell::new(false)),
249            fee_model: config.fee_model,
250            fill_model: config.fill_model,
251            latency_model: config.latency_model,
252            instruments: AHashMap::new(),
253            matching_engines: IndexMap::new(),
254            last_raw_id: 0,
255            pending_funding_rates: BTreeMap::new(),
256            funding_settlements: BTreeSet::new(),
257            leverages: config.leverages,
258            margin_model: config.margin_model,
259            modules: config.modules,
260            module_error: None,
261            clock,
262            cache,
263            message_queue: VecDeque::new(),
264            inflight_queue: BinaryHeap::new(),
265            inflight_orders: InflightOrders::default(),
266            inflight_counter: AHashMap::new(),
267            bar_execution: config.bar_execution,
268            bar_adaptive_high_low_ordering: config.bar_adaptive_high_low_ordering,
269            trade_execution: config.trade_execution,
270            liquidity_consumption: config.liquidity_consumption,
271            reject_stop_orders: config.reject_stop_orders,
272            support_gtd_orders: config.support_gtd_orders,
273            support_contingent_orders: config.support_contingent_orders,
274            use_position_ids: config.use_position_ids,
275            use_random_ids: config.use_random_ids,
276            use_reduce_only: config.use_reduce_only,
277            use_message_queue: config.use_message_queue,
278            use_market_order_acks: config.use_market_order_acks,
279            allow_cash_borrowing: config.allow_cash_borrowing,
280            frozen_account: config.frozen_account,
281            queue_position: config.queue_position,
282            oto_full_trigger: config.oto_full_trigger,
283            defer_option_settlement: config.defer_option_settlement,
284            price_protection_points: config.price_protection_points,
285            liquidation_enabled: config.liquidation_enabled,
286            liquidation_trigger_ratio: config.liquidation_trigger_ratio,
287            liquidation_cancel_open_orders: config.liquidation_cancel_open_orders,
288        })
289    }
290
291    /// Registers the execution client for the exchange.
292    pub fn register_client(&mut self, client: Rc<dyn ExecutionClient>) {
293        self.exec_client = Some(client);
294    }
295
296    /// Registers the spread quote endpoint used by the data engine.
297    pub fn register_spread_quote_endpoint(exchange: &Rc<RefCell<Self>>) {
298        let venue = exchange.borrow().id;
299        let endpoint = format!("SimulatedExchange.process_new_quote.{venue}");
300        let handler_id = endpoint.clone();
301        let exchange = Rc::clone(exchange);
302        let handler = TypedHandler::from_with_id(handler_id, move |quote: &QuoteTick| {
303            if let Err(e) = exchange.borrow_mut().process_quote_tick(quote) {
304                log::error!("{e:#}");
305            }
306        });
307
308        msgbus::register_quote_endpoint(endpoint.into(), handler);
309    }
310
311    /// Sets the fill model for the exchange.
312    pub fn set_fill_model(&mut self, fill_model: FillModelHandle) {
313        for matching_engine in self.matching_engines.values_mut() {
314            matching_engine.set_fill_model(fill_model.clone());
315            log::info!("Setting fill model for {}", matching_engine.venue);
316        }
317        self.fill_model = fill_model;
318    }
319
320    /// Sets the latency model for the exchange.
321    pub fn set_latency_model(&mut self, latency_model: LatencyModelHandle) {
322        self.latency_model = Some(latency_model);
323    }
324
325    #[must_use]
326    pub(crate) const fn has_modules(&self) -> bool {
327        !self.modules.is_empty()
328    }
329
330    #[must_use]
331    pub(crate) const fn liquidation_enabled(&self) -> bool {
332        self.liquidation_enabled
333    }
334
335    pub(crate) fn check_module_error(&self) -> anyhow::Result<()> {
336        if let Some(error) = &self.module_error {
337            anyhow::bail!("Simulation module failure requires exchange reset: {error}");
338        }
339        Ok(())
340    }
341
342    #[must_use]
343    pub(crate) const fn has_module_error(&self) -> bool {
344        self.module_error.is_some()
345    }
346
347    fn store_module_error(
348        &mut self,
349        module_index: usize,
350        method: &str,
351        error: &anyhow::Error,
352    ) -> anyhow::Error {
353        let error = format!("Simulation module {module_index} {method} failed: {error:#}");
354        self.module_error = Some(error.clone());
355        anyhow::anyhow!(error)
356    }
357
358    fn pre_process_modules(&mut self, data: &Data) -> anyhow::Result<()> {
359        self.check_module_error()?;
360
361        for module_index in 0..self.modules.len() {
362            if let Err(e) = self.modules[module_index].pre_process(data) {
363                return Err(self.store_module_error(module_index, "pre_process", &e));
364            }
365        }
366        Ok(())
367    }
368
369    /// Returns the configured book type for this venue.
370    #[must_use]
371    pub const fn book_type(&self) -> BookType {
372        self.book_type
373    }
374
375    /// Returns an iterator over the instrument IDs registered with this exchange.
376    pub fn instrument_ids(&self) -> impl Iterator<Item = &InstrumentId> {
377        self.instruments.keys()
378    }
379
380    /// Returns the expiration timestamp for the given instrument, if present.
381    #[must_use]
382    pub fn instrument_expiration(&self, instrument_id: InstrumentId) -> Option<UnixNanos> {
383        self.matching_engines
384            .get(&instrument_id)
385            .and_then(|matching_engine| matching_engine.instrument.expiration_ns())
386    }
387
388    /// Returns whether an unprocessed instrument remains for the given expiration.
389    #[must_use]
390    pub fn has_unprocessed_instrument_expiration(&self, expiration_ns: UnixNanos) -> bool {
391        self.matching_engines.values().any(|matching_engine| {
392            !matching_engine.is_expiration_processed()
393                && matching_engine.instrument.expiration_ns() == Some(expiration_ns)
394        })
395    }
396
397    pub fn initialize_account(&mut self) {
398        self.generate_fresh_account_state();
399    }
400
401    /// Loads non-emulated open orders from the cache into matching engines.
402    pub fn load_open_orders(&mut self) {
403        let mut open_orders: Vec<(OrderAny, AccountId)> = {
404            let cache = self.cache.as_ref().borrow();
405            cache
406                .orders_open(Some(&self.id), None, None, None, None)
407                .into_iter()
408                .filter(|order| !order.is_emulated())
409                .filter_map(|order| {
410                    order
411                        .account_id()
412                        .map(|account_id| (order.clone(), account_id))
413                })
414                .collect()
415        };
416
417        // Sort for deterministic insertion order
418        open_orders.sort_by(|(a, _), (b, _)| {
419            a.ts_init()
420                .cmp(&b.ts_init())
421                .then_with(|| a.client_order_id().cmp(&b.client_order_id()))
422        });
423
424        for (mut order, account_id) in open_orders {
425            let instrument_id = order.instrument_id();
426            if let Some(matching_engine) = self.matching_engines.get_mut(&instrument_id) {
427                matching_engine.process_order(&mut order, account_id);
428            } else {
429                log::error!(
430                    "No matching engine for {instrument_id} to load open order {}",
431                    order.client_order_id()
432                );
433            }
434        }
435    }
436
437    // panics-doc-ok (transitive via expect_display on venue mismatch)
438    /// Adds an instrument to the simulated exchange and initializes its matching engine.
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if:
443    /// - The exchange account type is `Cash` and the instrument is a `CryptoPerpetual`,
444    ///   `CryptoFuture`, `FuturesContract`, or `PerpetualContract`.
445    /// - The matching engine raw ID is exhausted.
446    ///
447    /// # Panics
448    ///
449    /// Panics if the instrument cannot be added to the exchange.
450    pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
451        check_equal(
452            &instrument.id().venue,
453            &self.id,
454            "Venue of instrument id",
455            "Venue of simulated exchange",
456        )
457        .expect_display(FAILED);
458
459        if self.account_type == AccountType::Cash
460            && (matches!(instrument, InstrumentAny::CryptoPerpetual(_))
461                || matches!(instrument, InstrumentAny::CryptoFuture(_))
462                || matches!(instrument, InstrumentAny::FuturesContract(_))
463                || matches!(instrument, InstrumentAny::PerpetualContract(_)))
464        {
465            anyhow::bail!("Cash account cannot trade futures or perpetuals")
466        }
467
468        let price_protection = if self.price_protection_points == 0 {
469            None
470        } else {
471            Some(self.price_protection_points)
472        };
473
474        let matching_engine_config = OrderMatchingEngineConfig::builder()
475            .bar_execution(self.bar_execution)
476            .bar_adaptive_high_low_ordering(self.bar_adaptive_high_low_ordering)
477            .trade_execution(self.trade_execution)
478            .liquidity_consumption(self.liquidity_consumption)
479            .reject_stop_orders(self.reject_stop_orders)
480            .support_gtd_orders(self.support_gtd_orders)
481            .support_contingent_orders(self.support_contingent_orders)
482            .use_position_ids(self.use_position_ids)
483            .use_random_ids(self.use_random_ids)
484            .use_reduce_only(self.use_reduce_only)
485            .use_market_order_acks(self.use_market_order_acks)
486            .queue_position(self.queue_position)
487            .oto_full_trigger(self.oto_full_trigger)
488            .defer_option_settlement(self.defer_option_settlement)
489            .maybe_price_protection_points(price_protection)
490            .build();
491        let instrument_id = instrument.id();
492        let raw_id = self
493            .last_raw_id
494            .checked_add(1)
495            .ok_or_else(|| anyhow::anyhow!("matching engine raw ID exhausted at u32::MAX"))?;
496        self.last_raw_id = raw_id;
497        let mut matching_engine = OrderMatchingEngine::new(
498            instrument.clone(),
499            raw_id,
500            self.fill_model.clone(),
501            self.fee_model.clone(),
502            self.book_type,
503            self.oms_type,
504            self.account_type,
505            self.clock.clone(),
506            Rc::clone(&self.cache),
507            matching_engine_config,
508        );
509
510        if let Some(handler) = &self.event_handler {
511            matching_engine.set_event_handler(Rc::clone(handler));
512        }
513        self.instruments.insert(instrument_id, instrument);
514        matching_engine.set_inflight_orders(self.inflight_orders.clone());
515        self.matching_engines.insert(instrument_id, matching_engine);
516
517        log::info!("Added instrument {instrument_id} and created matching engine");
518        Ok(())
519    }
520
521    /// Sets the deferred event handler used while a trading command is processed
522    /// synchronously.
523    ///
524    /// The supplied handler is wrapped so it applies only inside that window; outside it
525    /// events go straight to the execution engine as before.
526    pub(crate) fn set_event_handler(&mut self, handler: Rc<dyn Fn(OrderEventAny)>) {
527        let deferring = Rc::clone(&self.deferring_events);
528        let gated: Rc<dyn Fn(OrderEventAny)> = Rc::new(move |event| {
529            if deferring.get() {
530                handler(event);
531            } else {
532                msgbus::send_order_event(MessagingSwitchboard::exec_engine_process(), event);
533            }
534        });
535
536        for matching_engine in self.matching_engines.values_mut() {
537            matching_engine.set_event_handler(Rc::clone(&gated));
538        }
539        self.event_handler = Some(gated);
540    }
541
542    /// Returns the best bid price for the given instrument, if available.
543    #[must_use]
544    pub fn best_bid_price(&self, instrument_id: InstrumentId) -> Option<Price> {
545        self.matching_engines
546            .get(&instrument_id)
547            .and_then(OrderMatchingEngine::best_bid_price)
548    }
549
550    /// Returns the best ask price for the given instrument, if available.
551    #[must_use]
552    pub fn best_ask_price(&self, instrument_id: InstrumentId) -> Option<Price> {
553        self.matching_engines
554            .get(&instrument_id)
555            .and_then(OrderMatchingEngine::best_ask_price)
556    }
557
558    /// Returns a reference to the order book for the given instrument, if available.
559    pub fn get_book(&self, instrument_id: InstrumentId) -> Option<&OrderBook> {
560        self.matching_engines
561            .get(&instrument_id)
562            .map(OrderMatchingEngine::get_book)
563    }
564
565    /// Returns a reference to the matching engine for the given instrument, if available.
566    #[must_use]
567    pub fn get_matching_engine(
568        &self,
569        instrument_id: &InstrumentId,
570    ) -> Option<&OrderMatchingEngine> {
571        self.matching_engines.get(instrument_id)
572    }
573
574    /// Returns a reference to all matching engines keyed by instrument ID.
575    #[must_use]
576    pub const fn get_matching_engines(&self) -> &IndexMap<InstrumentId, OrderMatchingEngine> {
577        &self.matching_engines
578    }
579
580    /// Returns all order books keyed by instrument ID.
581    #[must_use]
582    pub fn get_books(&self) -> AHashMap<InstrumentId, OrderBook> {
583        let mut books = AHashMap::new();
584        for (instrument_id, matching_engine) in &self.matching_engines {
585            books.insert(*instrument_id, matching_engine.get_book().clone());
586        }
587        books
588    }
589
590    /// Returns all open orders, optionally filtered by instrument ID.
591    ///
592    /// An instrument ID with no matching engine returns no orders.
593    #[must_use]
594    pub fn get_open_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<RestingOrder> {
595        match instrument_id {
596            Some(id) => self
597                .matching_engines
598                .get(&id)
599                .map_or_else(Vec::new, OrderMatchingEngine::get_open_orders),
600            None => self
601                .matching_engines
602                .values()
603                .flat_map(OrderMatchingEngine::get_open_orders)
604                .collect(),
605        }
606    }
607
608    /// Returns all open bid orders, optionally filtered by instrument ID.
609    ///
610    /// An instrument ID with no matching engine returns no orders.
611    #[must_use]
612    pub fn get_open_bid_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<RestingOrder> {
613        match instrument_id {
614            Some(id) => self
615                .matching_engines
616                .get(&id)
617                .map_or_else(Vec::new, OrderMatchingEngine::get_open_bid_orders),
618            None => self
619                .matching_engines
620                .values()
621                .flat_map(OrderMatchingEngine::get_open_bid_orders)
622                .collect(),
623        }
624    }
625
626    /// Returns all open ask orders, optionally filtered by instrument ID.
627    ///
628    /// An instrument ID with no matching engine returns no orders.
629    #[must_use]
630    pub fn get_open_ask_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<RestingOrder> {
631        match instrument_id {
632            Some(id) => self
633                .matching_engines
634                .get(&id)
635                .map_or_else(Vec::new, OrderMatchingEngine::get_open_ask_orders),
636            None => self
637                .matching_engines
638                .values()
639                .flat_map(OrderMatchingEngine::get_open_ask_orders)
640                .collect(),
641        }
642    }
643
644    /// Returns the account for this exchange, if an execution client is registered.
645    #[must_use]
646    pub fn get_account(&self) -> Option<AccountAny> {
647        self.exec_client
648            .as_ref()
649            .and_then(|client| client.get_account())
650    }
651
652    /// Returns a reference to the cache.
653    #[must_use]
654    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
655        &self.cache
656    }
657
658    /// Adjusts the account balance by the given amount.
659    ///
660    /// Returns whether the adjustment was applied successfully.
661    pub fn adjust_account(&mut self, adjustment: Money) -> bool {
662        if self.frozen_account {
663            // Nothing to adjust
664            return true;
665        }
666
667        if let Some(exec_client) = &self.exec_client {
668            log::debug!("Adjusting account for venue {}", exec_client.venue());
669        }
670
671        match self.try_adjust_account(adjustment) {
672            Ok(()) => true,
673            Err(e) => {
674                log::error!("{e}");
675                false
676            }
677        }
678    }
679
680    /// Tries to adjust the account balance by the given amount without logging failures.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if the account or currency balance is unavailable, the
685    /// resulting balance exceeds [`Money`] bounds, or account state generation fails.
686    pub fn try_adjust_account(&mut self, adjustment: Money) -> Result<(), AccountAdjustmentError> {
687        if self.frozen_account {
688            // Nothing to adjust
689            return Ok(());
690        }
691
692        if let Some(exec_client) = &self.exec_client {
693            let venue = exec_client.venue();
694            let account_state = {
695                let cache = self.cache.borrow();
696                if let Some(account) = cache.account_for_venue(&venue) {
697                    if let Some(balance) = account.balance(Some(adjustment.currency)) {
698                        let mut current_balance = *balance;
699                        let Some(total) = current_balance.total.checked_add(adjustment) else {
700                            return Err(AccountAdjustmentError::TotalOverflow(adjustment.currency));
701                        };
702                        let Some(free) = current_balance.free.checked_add(adjustment) else {
703                            return Err(AccountAdjustmentError::FreeBalanceOverflow(
704                                adjustment.currency,
705                            ));
706                        };
707                        current_balance.total = total;
708                        current_balance.free = free;
709
710                        let margins = match &*account {
711                            AccountAny::Margin(margin_account) => margin_account.margins.clone(),
712                            _ => IndexMap::new(),
713                        };
714
715                        Some((
716                            vec![current_balance],
717                            margins.values().copied().collect(),
718                            self.clock.borrow().timestamp_ns(),
719                        ))
720                    } else {
721                        return Err(AccountAdjustmentError::MissingBalance(adjustment.currency));
722                    }
723                } else {
724                    return Err(AccountAdjustmentError::MissingAccount(venue));
725                }
726            };
727
728            if let Some((balances, margins, ts_event)) = account_state {
729                exec_client
730                    .generate_account_state(balances, margins, true, ts_event, None)
731                    .map_err(|e| AccountAdjustmentError::AccountStateGeneration(e.to_string()))?;
732            }
733        }
734        Ok(())
735    }
736
737    /// Returns whether there are pending commands at or before `ts_now`.
738    #[must_use]
739    pub fn has_pending_commands(&self, ts_now: UnixNanos) -> bool {
740        if !self.message_queue.is_empty() {
741            return true;
742        }
743        self.inflight_queue
744            .peek()
745            .is_some_and(|inflight| inflight.timestamp <= ts_now)
746    }
747
748    pub(crate) fn has_pending_commands_for_scope(
749        &self,
750        ts_now: UnixNanos,
751        scope: SettlementScope,
752    ) -> bool {
753        if matches!(scope, SettlementScope::All) {
754            return self.has_pending_commands(ts_now);
755        }
756
757        if !self.message_queue.is_empty() {
758            return true;
759        }
760
761        self.inflight_queue
762            .iter()
763            .any(|inflight| inflight.timestamp <= ts_now && inflight.matches_scope(ts_now, scope))
764    }
765
766    /// Returns the latest arrival timestamp across all latency-deferred
767    /// inflight commands, or `None` when the inflight queue is empty.
768    ///
769    /// Used at shutdown to advance the clock past the configured `LatencyModel`
770    /// delay so trailing commands (those emitted on the final data tick or
771    /// in `on_stop`) settle before the engines stop.
772    #[must_use]
773    pub fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
774        self.inflight_queue.iter().map(|c| c.timestamp).max()
775    }
776
777    /// Iterates all matching engines so newly submitted orders can match
778    /// against the current market state.
779    pub fn iterate_matching_engines(&mut self, ts_now: UnixNanos) {
780        for matching_engine in self.matching_engines.values_mut() {
781            matching_engine.iterate(ts_now, AggressorSide::NoAggressor);
782        }
783    }
784
785    /// Processes instrument expirations due at the given timestamp.
786    pub fn process_instrument_expirations(&mut self, ts_now: UnixNanos) {
787        for matching_engine in self.matching_engines.values_mut() {
788            if matching_engine
789                .instrument
790                .expiration_ns()
791                .is_some_and(|expiration_ns| ts_now >= expiration_ns)
792            {
793                matching_engine.process_instrument_expiration(ts_now);
794            }
795        }
796    }
797
798    /// Returns unprocessed instrument expirations for timer scheduling.
799    #[must_use]
800    pub fn instrument_expirations(&self) -> Vec<(InstrumentId, UnixNanos)> {
801        self.matching_engines
802            .values()
803            .filter(|matching_engine| !matching_engine.is_expiration_processed())
804            .filter_map(|matching_engine| {
805                matching_engine
806                    .instrument
807                    .expiration_ns()
808                    .filter(|expiration_ns| *expiration_ns > UnixNanos::default())
809                    .map(|expiration_ns| (matching_engine.instrument.id(), expiration_ns))
810            })
811            .collect()
812    }
813
814    /// Advances the exchange clock to the given timestamp so that any event
815    /// generators (modules, account state) see the correct time even when
816    /// no commands are pending.
817    ///
818    /// # Panics
819    ///
820    /// Panics if the clock is not a [`TestClock`].
821    pub fn set_clock_time(&self, ts_now: UnixNanos) {
822        let mut clock_ref = self.clock.borrow_mut();
823        let test_clock = clock_ref
824            .as_any_mut()
825            .downcast_mut::<TestClock>()
826            .expect("SimulatedExchange requires TestClock");
827        test_clock.set_time(ts_now);
828    }
829
830    /// Sends a trading command to the exchange for processing.
831    pub fn send(&mut self, command: TradingCommand) {
832        if matches!(
833            &command,
834            TradingCommand::QueryOrder(_) | TradingCommand::QueryAccount(_)
835        ) {
836            log::warn!("Simulated exchange does not support queries: {command}");
837            return;
838        }
839
840        if self.use_message_queue {
841            self.inflight_orders.insert(&command);
842        }
843
844        if !self.use_message_queue {
845            let _guard = DeferEventsGuard::new(Rc::clone(&self.deferring_events));
846            self.process_trading_command(command);
847        } else if self.latency_model.is_none() {
848            self.message_queue.push_back(command);
849        } else {
850            let (timestamp, counter) = self.generate_inflight_command(&command);
851            self.inflight_queue
852                .push(InflightCommand::new(timestamp, counter, command));
853        }
854    }
855
856    fn generate_inflight_command(&mut self, command: &TradingCommand) -> (UnixNanos, u32) {
857        if let Some(latency_model) = &self.latency_model {
858            let ts = match command {
859                TradingCommand::SubmitOrder(_) | TradingCommand::SubmitOrderList(_) => {
860                    command.ts_init() + latency_model.get_insert_latency()
861                }
862                TradingCommand::ModifyOrder(_) | TradingCommand::ModifyOrders(_) => {
863                    command.ts_init() + latency_model.get_update_latency()
864                }
865                TradingCommand::CancelOrder(_)
866                | TradingCommand::CancelOrders(_)
867                | TradingCommand::CancelAllOrders(_) => {
868                    command.ts_init() + latency_model.get_delete_latency()
869                }
870                _ => panic!("Cannot handle command: {command:?}"),
871            };
872
873            let counter = self
874                .inflight_counter
875                .entry(ts)
876                .and_modify(|e| *e += 1)
877                .or_insert(1);
878
879            (ts, *counter)
880        } else {
881            panic!("Latency model should be initialized");
882        }
883    }
884
885    /// Processes a single order book delta.
886    ///
887    /// # Errors
888    ///
889    /// Returns an error if module pre-processing or matching engine processing fails.
890    pub fn process_order_book_delta(&mut self, delta: OrderBookDelta) -> anyhow::Result<()> {
891        self.pre_process_modules(&Data::BookDelta(delta))?;
892
893        if !self.matching_engines.contains_key(&delta.instrument_id) {
894            let instrument = {
895                let cache = self.cache.as_ref().borrow();
896                cache.instrument(&delta.instrument_id).cloned()
897            };
898
899            if let Some(instrument) = instrument {
900                self.add_instrument(instrument)?;
901            } else {
902                anyhow::bail!(
903                    "No matching engine found for instrument {}",
904                    delta.instrument_id
905                );
906            }
907        }
908
909        if let Some(matching_engine) = self.matching_engines.get_mut(&delta.instrument_id) {
910            matching_engine.process_order_book_delta(&delta)?;
911        } else {
912            anyhow::bail!("Matching engine should be initialized");
913        }
914        Ok(())
915    }
916
917    /// Processes a batch of order book deltas.
918    ///
919    /// # Errors
920    ///
921    /// Returns an error if module pre-processing or matching engine processing fails.
922    pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
923        self.pre_process_modules(&Data::BookDeltas(Box::new(deltas.clone())))?;
924
925        if !self.matching_engines.contains_key(&deltas.instrument_id) {
926            let instrument = {
927                let cache = self.cache.as_ref().borrow();
928                cache.instrument(&deltas.instrument_id).cloned()
929            };
930
931            if let Some(instrument) = instrument {
932                self.add_instrument(instrument)?;
933            } else {
934                anyhow::bail!(
935                    "No matching engine found for instrument {}",
936                    deltas.instrument_id
937                );
938            }
939        }
940
941        if let Some(matching_engine) = self.matching_engines.get_mut(&deltas.instrument_id) {
942            matching_engine.process_order_book_deltas(deltas)?;
943        } else {
944            anyhow::bail!("Matching engine should be initialized");
945        }
946        Ok(())
947    }
948
949    /// Processes an L2 order book depth snapshot.
950    ///
951    /// # Errors
952    ///
953    /// Returns an error if module pre-processing or matching engine processing fails.
954    pub fn process_order_book_depth10(&mut self, depth: &OrderBookDepth10) -> anyhow::Result<()> {
955        self.pre_process_modules(&Data::BookDepth10(Box::new(*depth)))?;
956
957        if !self.matching_engines.contains_key(&depth.instrument_id) {
958            let instrument = {
959                let cache = self.cache.as_ref().borrow();
960                cache.instrument(&depth.instrument_id).cloned()
961            };
962
963            if let Some(instrument) = instrument {
964                self.add_instrument(instrument)?;
965            } else {
966                anyhow::bail!(
967                    "No matching engine found for instrument {}",
968                    depth.instrument_id
969                );
970            }
971        }
972
973        if let Some(matching_engine) = self.matching_engines.get_mut(&depth.instrument_id) {
974            matching_engine.process_order_book_depth10(depth)?;
975        } else {
976            anyhow::bail!("Matching engine should be initialized");
977        }
978        Ok(())
979    }
980
981    /// Processes a quote tick and updates the matching engine.
982    ///
983    /// # Errors
984    ///
985    /// Returns an error if module pre-processing or matching engine processing fails.
986    pub fn process_quote_tick(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
987        self.pre_process_modules(&Data::Quote(*quote))?;
988
989        if !self.matching_engines.contains_key(&quote.instrument_id) {
990            let instrument = {
991                let cache = self.cache.as_ref().borrow();
992                cache.instrument(&quote.instrument_id).cloned()
993            };
994
995            if let Some(instrument) = instrument {
996                self.add_instrument(instrument)?;
997            } else {
998                anyhow::bail!(
999                    "No matching engine found for instrument {}",
1000                    quote.instrument_id
1001                );
1002            }
1003        }
1004
1005        if let Some(matching_engine) = self.matching_engines.get_mut(&quote.instrument_id) {
1006            matching_engine.process_quote_tick(quote);
1007        } else {
1008            anyhow::bail!("Matching engine should be initialized");
1009        }
1010        Ok(())
1011    }
1012
1013    /// Processes a trade tick and updates the matching engine.
1014    ///
1015    /// # Errors
1016    ///
1017    /// Returns an error if module pre-processing or matching engine processing fails.
1018    pub fn process_trade_tick(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
1019        self.pre_process_modules(&Data::Trade(*trade))?;
1020
1021        if !self.matching_engines.contains_key(&trade.instrument_id) {
1022            let instrument = {
1023                let cache = self.cache.as_ref().borrow();
1024                cache.instrument(&trade.instrument_id).cloned()
1025            };
1026
1027            if let Some(instrument) = instrument {
1028                self.add_instrument(instrument)?;
1029            } else {
1030                anyhow::bail!(
1031                    "No matching engine found for instrument {}",
1032                    trade.instrument_id
1033                );
1034            }
1035        }
1036
1037        if let Some(matching_engine) = self.matching_engines.get_mut(&trade.instrument_id) {
1038            matching_engine.process_trade_tick(trade);
1039        } else {
1040            anyhow::bail!("Matching engine should be initialized");
1041        }
1042        Ok(())
1043    }
1044
1045    /// Processes a bar and updates the matching engine.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns an error if module pre-processing or matching engine processing fails.
1050    pub fn process_bar(&mut self, bar: Bar) -> anyhow::Result<()> {
1051        self.pre_process_modules(&Data::Bar(bar))?;
1052
1053        if !self.matching_engines.contains_key(&bar.instrument_id()) {
1054            let instrument = {
1055                let cache = self.cache.as_ref().borrow();
1056                cache.instrument(&bar.instrument_id()).cloned()
1057            };
1058
1059            if let Some(instrument) = instrument {
1060                self.add_instrument(instrument)?;
1061            } else {
1062                anyhow::bail!(
1063                    "No matching engine found for instrument {}",
1064                    bar.instrument_id()
1065                );
1066            }
1067        }
1068
1069        if let Some(matching_engine) = self.matching_engines.get_mut(&bar.instrument_id()) {
1070            matching_engine.process_bar(&bar);
1071        } else {
1072            anyhow::bail!("Matching engine should be initialized");
1073        }
1074        Ok(())
1075    }
1076
1077    /// Processes an instrument status update.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an error if module pre-processing or matching engine processing fails.
1082    pub fn process_instrument_status(&mut self, status: InstrumentStatus) -> anyhow::Result<()> {
1083        self.pre_process_modules(&Data::InstrumentStatus(status))?;
1084
1085        if !self.matching_engines.contains_key(&status.instrument_id) {
1086            let instrument = {
1087                let cache = self.cache.as_ref().borrow();
1088                cache.instrument(&status.instrument_id).cloned()
1089            };
1090
1091            if let Some(instrument) = instrument {
1092                self.add_instrument(instrument)?;
1093            } else {
1094                anyhow::bail!(
1095                    "No matching engine found for instrument {}",
1096                    status.instrument_id
1097                );
1098            }
1099        }
1100
1101        if let Some(matching_engine) = self.matching_engines.get_mut(&status.instrument_id) {
1102            matching_engine.process_status(status.action);
1103        } else {
1104            anyhow::bail!("Matching engine should be initialized");
1105        }
1106        Ok(())
1107    }
1108
1109    /// Processes an instrument close event.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns an error if module pre-processing or matching engine processing fails.
1114    pub fn process_instrument_close(&mut self, close: InstrumentClose) -> anyhow::Result<()> {
1115        self.pre_process_modules(&Data::InstrumentClose(close))?;
1116
1117        if !self.matching_engines.contains_key(&close.instrument_id) {
1118            let instrument = {
1119                let cache = self.cache.as_ref().borrow();
1120                cache.instrument(&close.instrument_id).cloned()
1121            };
1122
1123            if let Some(instrument) = instrument {
1124                self.add_instrument(instrument)?;
1125            } else {
1126                anyhow::bail!(
1127                    "No matching engine found for instrument {}",
1128                    close.instrument_id
1129                );
1130            }
1131        }
1132
1133        if let Some(matching_engine) = self.matching_engines.get_mut(&close.instrument_id) {
1134            matching_engine.process_instrument_close(close);
1135        } else {
1136            anyhow::bail!("Matching engine should be initialized");
1137        }
1138        Ok(())
1139    }
1140
1141    /// Processes a funding rate update.
1142    ///
1143    /// Returns the funding boundary timestamp when the engine should schedule a settlement.
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns an error if module pre-processing or funding settlement fails.
1148    pub fn process_funding_rate(
1149        &mut self,
1150        funding_rate: FundingRateUpdate,
1151    ) -> anyhow::Result<Option<UnixNanos>> {
1152        let replay_ts = self.clock.borrow().timestamp_ns();
1153        let instrument_id = funding_rate.instrument_id;
1154        let boundary = Self::funding_boundary(&funding_rate);
1155        let next_boundary = self.queue_funding_rate(funding_rate)?;
1156
1157        if let Some(boundary) = boundary
1158            && boundary <= replay_ts
1159        {
1160            self.process_funding_settlement(instrument_id, boundary)?;
1161            return Ok(None);
1162        }
1163
1164        Ok(next_boundary)
1165    }
1166
1167    pub(crate) fn process_funding_rate_deferred(
1168        &mut self,
1169        funding_rate: FundingRateUpdate,
1170        replay_ts: UnixNanos,
1171    ) -> anyhow::Result<Option<UnixNanos>> {
1172        self.queue_funding_rate(funding_rate)?;
1173        Ok(self
1174            .next_funding_boundary()
1175            .filter(|boundary| *boundary > replay_ts))
1176    }
1177
1178    fn queue_funding_rate(
1179        &mut self,
1180        funding_rate: FundingRateUpdate,
1181    ) -> anyhow::Result<Option<UnixNanos>> {
1182        self.pre_process_modules(&Data::FundingRate(funding_rate))?;
1183
1184        let Some(boundary) = Self::funding_boundary(&funding_rate) else {
1185            log::debug!(
1186                "Funding rate update for {} does not define a settlement boundary",
1187                funding_rate.instrument_id
1188            );
1189            return Ok(None);
1190        };
1191
1192        let key = (boundary, funding_rate.instrument_id);
1193        if !self.funding_settlements.contains(&key) {
1194            self.pending_funding_rates.insert(key, funding_rate);
1195        }
1196        Ok(Some(boundary))
1197    }
1198
1199    /// Processes a scheduled funding settlement for the instrument.
1200    ///
1201    /// # Errors
1202    ///
1203    /// Returns an error if a prior simulation module failure requires reset.
1204    pub fn process_funding_settlement(
1205        &mut self,
1206        instrument_id: InstrumentId,
1207        ts_event: UnixNanos,
1208    ) -> anyhow::Result<()> {
1209        self.check_module_error()?;
1210        let key = (ts_event, instrument_id);
1211        let Some(funding_rate) = self.pending_funding_rates.remove(&key) else {
1212            return Ok(());
1213        };
1214
1215        if !self.settle_funding_rate(&funding_rate, ts_event) {
1216            self.pending_funding_rates.insert(key, funding_rate);
1217        }
1218        Ok(())
1219    }
1220
1221    #[must_use]
1222    pub(crate) fn funding_boundaries_due(
1223        &self,
1224        replay_ts: UnixNanos,
1225    ) -> Vec<(UnixNanos, InstrumentId)> {
1226        self.pending_funding_rates
1227            .keys()
1228            .copied()
1229            .take_while(|(boundary, _)| *boundary <= replay_ts)
1230            .collect()
1231    }
1232
1233    pub(crate) fn settle_funding_boundary(
1234        &mut self,
1235        boundary: UnixNanos,
1236        instrument_id: InstrumentId,
1237    ) -> bool {
1238        let key = (boundary, instrument_id);
1239        let Some(funding_rate) = self.pending_funding_rates.remove(&key) else {
1240            return true;
1241        };
1242
1243        if self.settle_funding_rate(&funding_rate, boundary) {
1244            true
1245        } else {
1246            self.pending_funding_rates.insert(key, funding_rate);
1247            false
1248        }
1249    }
1250
1251    #[must_use]
1252    pub(crate) fn next_funding_boundary(&self) -> Option<UnixNanos> {
1253        self.pending_funding_rates
1254            .first_key_value()
1255            .map(|((boundary, _), _)| *boundary)
1256    }
1257
1258    fn settle_funding_rate(
1259        &mut self,
1260        funding_rate: &FundingRateUpdate,
1261        ts_event: UnixNanos,
1262    ) -> bool {
1263        let settlement_key = (ts_event, funding_rate.instrument_id);
1264        if self.funding_settlements.contains(&settlement_key) {
1265            return true;
1266        }
1267
1268        let Some(exec_client) = &self.exec_client else {
1269            log::warn!(
1270                "Cannot settle funding for {}: execution client is not registered",
1271                funding_rate.instrument_id
1272            );
1273            return false;
1274        };
1275        let account_id = exec_client.account_id();
1276        let account_venue = exec_client.venue();
1277
1278        if !self
1279            .matching_engines
1280            .contains_key(&funding_rate.instrument_id)
1281        {
1282            let instrument = {
1283                let cache = self.cache.as_ref().borrow();
1284                cache.instrument(&funding_rate.instrument_id).cloned()
1285            };
1286
1287            if let Some(instrument) = instrument {
1288                if let Err(e) = self.add_instrument(instrument) {
1289                    log::error!(
1290                        "Cannot settle funding for {}: failed to add instrument: {e}",
1291                        funding_rate.instrument_id
1292                    );
1293                    return false;
1294                }
1295            } else {
1296                log::warn!(
1297                    "Cannot settle funding for {}: no matching engine or instrument",
1298                    funding_rate.instrument_id
1299                );
1300                return false;
1301            }
1302        }
1303
1304        let open_positions: Vec<Position> = {
1305            let cache = self.cache.borrow();
1306            cache
1307                .positions_open(
1308                    Some(&self.id),
1309                    Some(&funding_rate.instrument_id),
1310                    None,
1311                    Some(&account_id),
1312                    None,
1313                )
1314                .into_iter()
1315                .map(|position| position.cloned())
1316                .collect()
1317        };
1318
1319        if open_positions.is_empty() {
1320            self.funding_settlements.insert(settlement_key);
1321            return true;
1322        }
1323
1324        let Some(settlement_price) = self.funding_settlement_price(funding_rate.instrument_id)
1325        else {
1326            log::warn!(
1327                "Cannot settle funding for {}: no mark price or top-of-book price",
1328                funding_rate.instrument_id
1329            );
1330            return false;
1331        };
1332
1333        let settlement_currency = open_positions[0].settlement_currency;
1334        let mut valued_positions = Vec::with_capacity(open_positions.len());
1335        let mut account_adjustments: AHashMap<Currency, Money> = AHashMap::new();
1336
1337        for position in open_positions {
1338            if position.settlement_currency != settlement_currency {
1339                log::error!(
1340                    "Cannot settle funding for {}: position settlement currencies differ",
1341                    funding_rate.instrument_id
1342                );
1343                return false;
1344            }
1345
1346            let notional = match position.try_notional_value(settlement_price) {
1347                Ok(notional) => notional,
1348                Err(e) => {
1349                    log::error!(
1350                        "Cannot settle funding for position {}: invalid notional value: {e}",
1351                        position.id
1352                    );
1353                    return false;
1354                }
1355            };
1356            let side = if position.signed_qty > 0.0 {
1357                -Decimal::ONE
1358            } else {
1359                Decimal::ONE
1360            };
1361            let Some(amount) = notional
1362                .as_decimal()
1363                .checked_mul(funding_rate.rate)
1364                .and_then(|value| value.checked_mul(side))
1365            else {
1366                log::error!(
1367                    "Cannot settle funding for position {}: funding amount overflow",
1368                    position.id
1369                );
1370                return false;
1371            };
1372            let pnl_change = match Money::from_decimal(amount, notional.currency) {
1373                Ok(money) => money,
1374                Err(e) => {
1375                    log::error!(
1376                        "Cannot settle funding for position {}: invalid funding amount: {e}",
1377                        position.id
1378                    );
1379                    return false;
1380                }
1381            };
1382
1383            if pnl_change.currency != settlement_currency {
1384                log::error!(
1385                    "Cannot settle funding for position {}: settlement currency {} differs from funding currency {}",
1386                    position.id,
1387                    settlement_currency,
1388                    pnl_change.currency
1389                );
1390                return false;
1391            }
1392
1393            if let Some(realized) = position.realized_pnl {
1394                if realized.currency != pnl_change.currency {
1395                    log::error!(
1396                        "Cannot settle funding for position {}: realized PnL currency {} differs from funding currency {}",
1397                        position.id,
1398                        realized.currency,
1399                        pnl_change.currency
1400                    );
1401                    return false;
1402                }
1403
1404                if realized.checked_add(pnl_change).is_none() {
1405                    log::error!(
1406                        "Cannot settle funding for position {}: realized PnL overflow",
1407                        position.id
1408                    );
1409                    return false;
1410                }
1411            }
1412            let total_adjustment =
1413                if let Some(current) = account_adjustments.get(&pnl_change.currency).copied() {
1414                    let Some(total) = current.checked_add(pnl_change) else {
1415                        log::error!(
1416                            "Cannot settle funding for {}: aggregate account adjustment overflow",
1417                            funding_rate.instrument_id
1418                        );
1419                        return false;
1420                    };
1421                    total
1422                } else {
1423                    pnl_change
1424                };
1425            account_adjustments.insert(pnl_change.currency, total_adjustment);
1426            valued_positions.push((position, pnl_change));
1427        }
1428
1429        let mut account_adjustments = account_adjustments.into_values().collect::<Vec<_>>();
1430        account_adjustments.sort_unstable_by_key(|adjustment| adjustment.currency.code);
1431
1432        if !self.frozen_account {
1433            let cache = self.cache.borrow();
1434            let Some(account) = cache.account_for_venue(&account_venue) else {
1435                log::error!("Cannot settle funding: no account for venue {account_venue}");
1436                return false;
1437            };
1438
1439            for adjustment in &account_adjustments {
1440                let Some(balance) = account.balance(Some(adjustment.currency)) else {
1441                    log::error!(
1442                        "Cannot settle funding: no account balance for currency {}",
1443                        adjustment.currency
1444                    );
1445                    return false;
1446                };
1447
1448                if balance.total.checked_add(*adjustment).is_none()
1449                    || balance.free.checked_add(*adjustment).is_none()
1450                {
1451                    log::error!(
1452                        "Cannot settle funding: {} account adjustment exceeds Money bounds",
1453                        adjustment.currency
1454                    );
1455                    return false;
1456                }
1457            }
1458        }
1459
1460        let ts_init = self.clock.borrow().timestamp_ns();
1461        let settlement = FundingSettlement::new(
1462            msgbus::get_message_bus().borrow().trader_id,
1463            funding_rate.instrument_id,
1464            account_id,
1465            funding_rate.rate,
1466            settlement_price,
1467            settlement_currency,
1468            UUID4::new(),
1469            ts_event,
1470            ts_init,
1471        );
1472        let mut adjusted_positions = Vec::with_capacity(valued_positions.len());
1473        for (original, pnl_change) in valued_positions {
1474            let mut adjusted = original.clone();
1475            let adjustment = PositionAdjusted::new(
1476                settlement.trader_id,
1477                adjusted.strategy_id,
1478                adjusted.instrument_id,
1479                adjusted.id,
1480                adjusted.account_id,
1481                PositionAdjustmentType::Funding,
1482                None,
1483                Some(pnl_change),
1484                Some(Ustr::from(&format!(
1485                    "funding_settlement:{}",
1486                    settlement.event_id
1487                ))),
1488                UUID4::new(),
1489                settlement.ts_event,
1490                settlement.ts_init,
1491            );
1492            adjusted.apply_adjustment(adjustment);
1493            adjusted_positions.push((original, adjusted, adjustment));
1494        }
1495
1496        {
1497            let mut cache = self.cache.borrow_mut();
1498
1499            for (index, (_, adjusted, _)) in adjusted_positions.iter().enumerate() {
1500                if let Err(e) = cache.update_position(adjusted) {
1501                    log::error!(
1502                        "Cannot update position {} after funding settlement: {e}",
1503                        adjusted.id
1504                    );
1505
1506                    // Inclusive of `index`: the failed update commits the adjusted position
1507                    // to the cache before attempting to persist it, so the position whose
1508                    // update returned the error also needs restoring.
1509                    for (original, _, _) in adjusted_positions[..=index].iter().rev() {
1510                        if let Err(rollback_error) = cache.update_position(original) {
1511                            log::error!(
1512                                "Cannot roll back position {} after failed funding settlement: {rollback_error}",
1513                                original.id
1514                            );
1515                        }
1516                    }
1517                    return false;
1518                }
1519            }
1520        }
1521
1522        for adjustment in &account_adjustments {
1523            if !self.adjust_account(*adjustment) {
1524                let mut cache = self.cache.borrow_mut();
1525                for (original, _, _) in adjusted_positions.iter().rev() {
1526                    if let Err(e) = cache.update_position(original) {
1527                        log::error!(
1528                            "Cannot roll back position {} after failed account adjustment: {e}",
1529                            original.id
1530                        );
1531                    }
1532                }
1533                return false;
1534            }
1535        }
1536
1537        self.funding_settlements.insert(settlement_key);
1538        let settlement_topic = switchboard::get_funding_settlement_topic(settlement.instrument_id);
1539        msgbus::publish_any(settlement_topic, &settlement);
1540
1541        for (_, _, adjustment) in adjusted_positions {
1542            let event = PositionEvent::PositionAdjusted(adjustment);
1543            let PositionEvent::PositionAdjusted(adjustment) = &event else {
1544                continue;
1545            };
1546            let topic = switchboard::get_event_position_topic(adjustment.strategy_id);
1547            msgbus::publish_position_event(topic, &event);
1548        }
1549
1550        true
1551    }
1552
1553    fn funding_settlement_price(&self, instrument_id: InstrumentId) -> Option<Price> {
1554        if let Some(mark_price) = self.cache.borrow().mark_price(&instrument_id) {
1555            return Some(mark_price.value);
1556        }
1557
1558        let bid = self.best_bid_price(instrument_id)?;
1559        let ask = self.best_ask_price(instrument_id)?;
1560        let midpoint = (bid.as_decimal() + ask.as_decimal()) / Decimal::from(2);
1561        Price::from_decimal_dp(midpoint, bid.precision.max(ask.precision)).ok()
1562    }
1563
1564    fn is_interval_funding_boundary(funding_rate: &FundingRateUpdate) -> bool {
1565        let Some(interval_mins) = funding_rate.interval else {
1566            return false;
1567        };
1568        let Ok(interval) = DurationNanos::try_from_mins(u64::from(interval_mins)) else {
1569            return false;
1570        };
1571
1572        !interval.is_zero() && funding_rate.ts_event.floor(interval) == funding_rate.ts_event
1573    }
1574
1575    fn funding_boundary(funding_rate: &FundingRateUpdate) -> Option<UnixNanos> {
1576        funding_rate.next_funding_ns.or_else(|| {
1577            Self::is_interval_funding_boundary(funding_rate).then_some(funding_rate.ts_event)
1578        })
1579    }
1580
1581    /// Advances the exchange clock and processes all pending inflight and queued trading commands
1582    /// up to `ts_now`.
1583    ///
1584    /// # Panics
1585    ///
1586    /// Panics if the exchange clock is not a [`TestClock`] or popping an inflight command fails
1587    /// during processing.
1588    pub fn process(&mut self, ts_now: UnixNanos) {
1589        self.process_commands(ts_now, SettlementScope::All);
1590    }
1591
1592    pub(crate) fn process_for_scope(&mut self, ts_now: UnixNanos, scope: SettlementScope) {
1593        self.process_commands(ts_now, scope);
1594    }
1595
1596    fn process_commands(&mut self, ts_now: UnixNanos, scope: SettlementScope) {
1597        self.set_clock_time(ts_now);
1598
1599        let mut deferred = Vec::new();
1600        let mut processed_timestamps = BTreeSet::new();
1601
1602        while let Some(inflight) = self.inflight_queue.peek() {
1603            if inflight.timestamp > ts_now {
1604                break;
1605            }
1606            let inflight = self.inflight_queue.pop().unwrap();
1607
1608            if !inflight.matches_scope(ts_now, scope) {
1609                deferred.push(inflight);
1610                continue;
1611            }
1612
1613            processed_timestamps.insert(inflight.timestamp);
1614            self.message_queue.push_back(inflight.command);
1615        }
1616
1617        let deferred_timestamps: BTreeSet<_> =
1618            deferred.iter().map(|inflight| inflight.timestamp).collect();
1619        self.inflight_queue.extend(deferred);
1620
1621        for timestamp in processed_timestamps.difference(&deferred_timestamps) {
1622            self.inflight_counter.remove(timestamp);
1623        }
1624
1625        while let Some(command) = self.message_queue.pop_front() {
1626            self.process_trading_command(command);
1627        }
1628    }
1629
1630    /// Runs all simulation modules for the given timestamp.
1631    ///
1632    /// Must be called once per time step after all command queues have fully
1633    /// settled, not inside the settle loop.
1634    ///
1635    /// # Errors
1636    ///
1637    /// Returns an error if a simulation module fails. The exchange retains the failure and
1638    /// rejects further processing until reset.
1639    pub fn process_modules(&mut self, ts_now: UnixNanos) -> anyhow::Result<()> {
1640        self.check_module_error()?;
1641
1642        if self.frozen_account || self.exec_client.is_none() {
1643            return Ok(());
1644        }
1645
1646        let results = {
1647            let cache = self.cache.borrow();
1648            let ctx = ExchangeContext {
1649                venue: self.id,
1650                base_currency: self.base_currency,
1651                instruments: &self.instruments,
1652                matching_engines: &self.matching_engines,
1653                cache: &cache,
1654            };
1655            self.modules
1656                .iter()
1657                .enumerate()
1658                .map(|(module_index, module)| {
1659                    module
1660                        .process(ts_now, &ctx)
1661                        .map(|result| (module_index, result))
1662                        .map_err(|e| (module_index, e))
1663                })
1664                .collect::<Result<Vec<_>, _>>()
1665        };
1666        let results = match results {
1667            Ok(results) => results,
1668            Err((module_index, error)) => {
1669                return Err(self.store_module_error(module_index, "process", &error));
1670            }
1671        };
1672
1673        for (module_index, result) in results {
1674            if let SimulationModuleResult::Completed(adjustments) = result {
1675                let outcomes = adjustments
1676                    .into_iter()
1677                    .map(|adjustment| match self.try_adjust_account(adjustment) {
1678                        Ok(()) => AccountAdjustmentOutcome::Applied,
1679                        Err(e) => AccountAdjustmentOutcome::Failed(e),
1680                    })
1681                    .collect::<Vec<_>>();
1682
1683                if let Err(e) = self.modules[module_index].acknowledge(&outcomes) {
1684                    return Err(self.store_module_error(module_index, "acknowledge", &e));
1685                }
1686            }
1687        }
1688        Ok(())
1689    }
1690
1691    /// Resets the exchange to its initial state.
1692    ///
1693    /// # Errors
1694    ///
1695    /// Returns an error if a simulation module cannot reset.
1696    pub fn reset(&mut self) -> anyhow::Result<()> {
1697        if !self.account_at_starting_balances() {
1698            self.generate_fresh_account_state();
1699        }
1700
1701        let mut module_error = None;
1702
1703        for (module_index, module) in self.modules.iter().enumerate() {
1704            if let Err(e) = module.reset()
1705                && module_error.is_none()
1706            {
1707                module_error = Some(format!(
1708                    "Simulation module {module_index} reset failed: {e:#}"
1709                ));
1710            }
1711        }
1712
1713        for matching_engine in self.matching_engines.values_mut() {
1714            matching_engine.reset();
1715        }
1716
1717        self.pending_funding_rates.clear();
1718        self.funding_settlements.clear();
1719        self.message_queue.clear();
1720        self.inflight_queue.clear();
1721        self.inflight_orders.clear();
1722        self.inflight_counter.clear();
1723
1724        log::info!("Resetting exchange state");
1725        self.module_error = module_error;
1726        self.check_module_error()
1727    }
1728
1729    /// Logs diagnostic information from all simulation modules.
1730    ///
1731    /// # Errors
1732    ///
1733    /// Returns an error if a simulation module cannot produce its diagnostics.
1734    pub fn log_diagnostics(&self) -> anyhow::Result<()> {
1735        for (module_index, module) in self.modules.iter().enumerate() {
1736            module.log_diagnostics().map_err(|e| {
1737                anyhow::anyhow!("Simulation module {module_index} log_diagnostics failed: {e:#}")
1738            })?;
1739        }
1740        Ok(())
1741    }
1742
1743    /// Checks if any margin accounts have breached maintenance margin and liquidates open
1744    /// positions when the trigger threshold is met.
1745    ///
1746    /// Liquidation is scoped to the breached settlement currency: only positions whose
1747    /// instrument settles in the same currency as the breached margin account are closed.
1748    /// Positions settled in other currencies remain open, isolating the liquidation to
1749    /// the currency whose equity fell below the maintenance threshold.
1750    ///
1751    /// > **Note**: A future `cross_margin_mode` venue configuration could extend this to
1752    /// > liquidate all positions across all settlement currencies simultaneously.
1753    pub fn process_liquidations(&mut self, ts_now: UnixNanos) {
1754        if !self.liquidation_enabled {
1755            return;
1756        }
1757
1758        if self.frozen_account {
1759            return;
1760        }
1761
1762        let account = {
1763            let cache = self.cache.borrow();
1764            cache.account_for_venue_owned(&self.id)
1765        };
1766        let Some(account) = account else { return };
1767        let AccountAny::Margin(margin_account) = &account else {
1768            return;
1769        };
1770        let account_id = margin_account.id();
1771
1772        let currencies: Vec<Currency> = margin_account.currencies();
1773
1774        let open_positions: Vec<Position> = {
1775            let cache = self.cache.borrow();
1776            cache
1777                .positions_open(Some(&self.id), None, None, None, None)
1778                .into_iter()
1779                .map(|p| p.cloned())
1780                .collect()
1781        };
1782
1783        // Pre-bucket position indices by settlement currency to avoid repeated full scans.
1784        let mut positions_by_currency: AHashMap<Currency, Vec<usize>> = AHashMap::new();
1785        for (i, p) in open_positions.iter().enumerate() {
1786            positions_by_currency
1787                .entry(p.settlement_currency)
1788                .or_default()
1789                .push(i);
1790        }
1791
1792        for currency in currencies {
1793            let Some(balance) = margin_account.balance(Some(currency)) else {
1794                continue;
1795            };
1796            let balance_f64 = balance.total.as_f64();
1797
1798            let Some(indices) = positions_by_currency.get(&currency) else {
1799                continue;
1800            };
1801
1802            let (upnl_f64, all_priced) = {
1803                let cache = self.cache.borrow();
1804                let mut upnl = 0.0_f64;
1805                let mut all_priced = true;
1806
1807                for &i in indices {
1808                    let p = &open_positions[i];
1809                    if let Some(pnl) = cache.calculate_unrealized_pnl(p) {
1810                        upnl += pnl.as_f64();
1811                    } else {
1812                        all_priced = false;
1813                        break;
1814                    }
1815                }
1816                (upnl, all_priced)
1817            };
1818
1819            if !all_priced {
1820                continue; // defer until all positions are priced
1821            }
1822
1823            let equity = balance_f64 + upnl_f64;
1824            let maintenance = margin_account.total_maintenance_margin(currency).as_f64();
1825
1826            if maintenance == 0.0 {
1827                continue;
1828            }
1829
1830            let threshold = maintenance * self.liquidation_trigger_ratio;
1831
1832            if equity > threshold {
1833                continue;
1834            }
1835
1836            log::warn!(
1837                "LIQUIDATION triggered for account {} currency {}: equity={:.4} <= threshold={:.4} (maintenance={:.4} x ratio={})",
1838                account_id,
1839                currency,
1840                equity,
1841                threshold,
1842                maintenance,
1843                self.liquidation_trigger_ratio
1844            );
1845
1846            for matching_engine in self.matching_engines.values_mut() {
1847                matching_engine.liquidate_open_positions(
1848                    ts_now,
1849                    self.liquidation_cancel_open_orders,
1850                    currency,
1851                );
1852            }
1853        }
1854    }
1855
1856    fn process_trading_command(&mut self, command: TradingCommand) {
1857        self.inflight_orders.remove(&command);
1858        let instrument_id = command.instrument_id();
1859        assert!(
1860            self.matching_engines.contains_key(&instrument_id),
1861            "Matching engine not found for instrument {instrument_id}",
1862        );
1863
1864        let command = match command {
1865            TradingCommand::ModifyOrder(ref command)
1866                if self.process_modify_submitted_order(command) =>
1867            {
1868                return;
1869            }
1870            TradingCommand::ModifyOrders(mut command) => {
1871                command
1872                    .modifies
1873                    .retain(|modify| !self.process_modify_submitted_order(modify));
1874
1875                if command.modifies.is_empty() {
1876                    return;
1877                }
1878                TradingCommand::ModifyOrders(command)
1879            }
1880            command => command,
1881        };
1882
1883        let account_id = if let Some(exec_client) = &self.exec_client {
1884            exec_client.account_id()
1885        } else {
1886            panic!("Execution client should be initialized");
1887        };
1888
1889        if let TradingCommand::SubmitOrderList(ref command) = command {
1890            let mut orders: Vec<OrderAny> = self
1891                .cache
1892                .borrow()
1893                .orders_for_ids(&command.order_list.client_order_ids, command);
1894
1895            for order in &mut orders {
1896                let order_instrument_id = order.instrument_id();
1897                if let Some(matching_engine) = self.matching_engines.get_mut(&order_instrument_id) {
1898                    matching_engine.process_order(order, account_id);
1899                } else {
1900                    panic!("Matching engine not found for instrument {order_instrument_id}");
1901                }
1902            }
1903
1904            return;
1905        }
1906
1907        if let Some(matching_engine) = self.matching_engines.get_mut(&instrument_id) {
1908            match command {
1909                TradingCommand::SubmitOrder(command) => {
1910                    let mut order = self
1911                        .cache
1912                        .borrow()
1913                        .order(&command.client_order_id)
1914                        .map(|o| o.clone())
1915                        .expect("Order must exist in cache");
1916                    matching_engine.process_order(&mut order, account_id);
1917                }
1918                TradingCommand::ModifyOrder(ref command) => {
1919                    matching_engine.process_modify(command, account_id);
1920                }
1921                TradingCommand::ModifyOrders(ref command) => {
1922                    matching_engine.process_batch_modify(command, account_id);
1923                }
1924                TradingCommand::CancelOrder(ref command) => {
1925                    matching_engine.process_cancel(command, account_id);
1926                }
1927                TradingCommand::CancelOrders(ref command) => {
1928                    matching_engine.process_batch_cancel(command, account_id);
1929                }
1930                TradingCommand::CancelAllOrders(ref command) => {
1931                    matching_engine.process_cancel_all(command, account_id);
1932                }
1933                _ => {}
1934            }
1935        } else {
1936            panic!("Matching engine not found for instrument {instrument_id}");
1937        }
1938    }
1939
1940    fn process_modify_submitted_order(&self, command: &ModifyOrder) -> bool {
1941        let Some(order) = self
1942            .cache
1943            .borrow()
1944            .order(&command.client_order_id)
1945            .map(|o| o.clone())
1946        else {
1947            return false;
1948        };
1949
1950        let modifies_submitted_order = matches!(order.status(), OrderStatus::Submitted)
1951            || (matches!(order.status(), OrderStatus::PendingUpdate)
1952                && order
1953                    .previous_status()
1954                    .is_some_and(|status| matches!(status, OrderStatus::Submitted)));
1955
1956        if !modifies_submitted_order {
1957            return false;
1958        }
1959
1960        self.generate_order_updated(
1961            &order,
1962            command.quantity.unwrap_or_else(|| order.quantity()),
1963            command.price.or_else(|| order.price()),
1964            command.trigger_price.or_else(|| order.trigger_price()),
1965        );
1966        true
1967    }
1968
1969    fn generate_order_updated(
1970        &self,
1971        order: &OrderAny,
1972        quantity: Quantity,
1973        price: Option<Price>,
1974        trigger_price: Option<Price>,
1975    ) {
1976        let ts_now = self.clock.borrow().timestamp_ns();
1977        let event = OrderEventAny::Updated(OrderUpdated::new(
1978            order.trader_id(),
1979            order.strategy_id(),
1980            order.instrument_id(),
1981            order.client_order_id(),
1982            quantity,
1983            UUID4::new(),
1984            ts_now,
1985            ts_now,
1986            false,
1987            order.venue_order_id(),
1988            order.account_id(),
1989            price,
1990            trigger_price,
1991            None,
1992            order.is_quote_quantity(),
1993        ));
1994        self.dispatch_order_event(event);
1995    }
1996
1997    fn dispatch_order_event(&self, event: OrderEventAny) {
1998        if let Some(handler) = &self.event_handler {
1999            handler(event);
2000        } else {
2001            msgbus::send_order_event(MessagingSwitchboard::exec_engine_process(), event);
2002        }
2003    }
2004
2005    fn account_at_starting_balances(&self) -> bool {
2006        let Some(account) = self.get_account() else {
2007            return false;
2008        };
2009
2010        let balances = account.balances();
2011
2012        for starting in &self.starting_balances {
2013            let Some(balance) = balances.get(&starting.currency) else {
2014                return false;
2015            };
2016
2017            if balance.total != *starting || balance.free != *starting {
2018                return false;
2019            }
2020        }
2021
2022        true
2023    }
2024
2025    fn generate_fresh_account_state(&self) {
2026        let balances: Vec<AccountBalance> = self
2027            .starting_balances
2028            .iter()
2029            .map(|money| AccountBalance::new(*money, Money::zero(money.currency), *money))
2030            .collect();
2031
2032        if let Some(exec_client) = &self.exec_client {
2033            let ts_event = self.clock.borrow().timestamp_ns();
2034            exec_client
2035                .generate_account_state(balances, vec![], true, ts_event, None)
2036                .unwrap();
2037        }
2038
2039        let calculate_account_state = !self.frozen_account;
2040
2041        if let Some(mut account) = self.get_account() {
2042            account.set_calculate_account_state(calculate_account_state);
2043
2044            match &mut account {
2045                AccountAny::Margin(margin_account) => {
2046                    margin_account.set_default_leverage(self.default_leverage);
2047                    for (instrument_id, leverage) in &self.leverages {
2048                        margin_account.set_leverage(*instrument_id, *leverage);
2049                    }
2050
2051                    if let Some(model) = &self.margin_model {
2052                        margin_account.set_margin_model(model.clone());
2053                    }
2054                }
2055                AccountAny::Cash(cash_account) => {
2056                    cash_account.allow_borrowing = self.allow_cash_borrowing;
2057                }
2058                AccountAny::Betting(_) | AccountAny::Wallet(_) => {}
2059            }
2060
2061            self.cache.borrow_mut().update_account(&account).unwrap();
2062        }
2063    }
2064}
2065
2066#[derive(Clone, Copy)]
2067pub(crate) enum SettlementScope {
2068    All,
2069    Data(Option<InstrumentId>),
2070}
2071
2072/// Marks the window in which order events are routed to the deferred handler, and clears
2073/// it on drop so an unwind cannot leave the exchange deferring every later event.
2074#[derive(Debug)]
2075struct DeferEventsGuard {
2076    deferring: Rc<Cell<bool>>,
2077}
2078
2079impl DeferEventsGuard {
2080    fn new(deferring: Rc<Cell<bool>>) -> Self {
2081        deferring.set(true);
2082        Self { deferring }
2083    }
2084}
2085
2086impl Drop for DeferEventsGuard {
2087    fn drop(&mut self) {
2088        self.deferring.set(false);
2089    }
2090}
2091
2092#[cfg(test)]
2093mod tests {
2094    use nautilus_common::messages::execution::{QueryAccount, QueryOrder, SubmitOrder};
2095    use nautilus_core::DurationNanos;
2096    use nautilus_execution::models::latency::{LatencyModelHandle, StaticLatencyModel};
2097    use nautilus_model::{
2098        accounts::MarginAccount,
2099        enums::{AccountType, BookType, OrderSide, OrderType},
2100        events::AccountState,
2101        identifiers::{ClientOrderId, StrategyId, TraderId},
2102        instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
2103        orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
2104        stubs::TestDefault,
2105        types::AccountBalance,
2106    };
2107    use rstest::rstest;
2108
2109    use super::*;
2110
2111    /// The three `send` dispatch modes a query must be intercepted ahead of.
2112    #[derive(Clone, Copy)]
2113    enum Dispatch {
2114        /// `use_message_queue = true` with a latency model: `inflight_queue`.
2115        Latency,
2116        /// `use_message_queue = true`, no latency: `message_queue`.
2117        Queued,
2118        /// `use_message_queue = false`: synchronous `process_trading_command`.
2119        Immediate,
2120    }
2121
2122    fn setup_exchange(dispatch: Dispatch) -> SimulatedExchange {
2123        let cache = Rc::new(RefCell::new(Cache::default()));
2124        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2125        let mut config = SimulatedVenueConfig::builder()
2126            .venue(Venue::new("SIM"))
2127            .oms_type(OmsType::Netting)
2128            .account_type(AccountType::Margin)
2129            .book_type(BookType::L2_MBP)
2130            .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
2131            .build()
2132            .unwrap();
2133
2134        match dispatch {
2135            Dispatch::Latency => {
2136                config.latency_model = Some(LatencyModelHandle::new(StaticLatencyModel::new(
2137                    DurationNanos::default(),
2138                    DurationNanos::default(),
2139                    DurationNanos::default(),
2140                    DurationNanos::default(),
2141                )));
2142            }
2143            Dispatch::Queued => {} // Defaults: use_message_queue = true, no latency
2144            Dispatch::Immediate => config.use_message_queue = false,
2145        }
2146
2147        SimulatedExchange::new(config, cache, clock).unwrap()
2148    }
2149
2150    #[rstest]
2151    #[case(false)]
2152    #[case(true)]
2153    fn test_liquidation_enabled(#[case] expected: bool) {
2154        let cache = Rc::new(RefCell::new(Cache::default()));
2155        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2156        let config = SimulatedVenueConfig::builder()
2157            .venue(Venue::new("SIM"))
2158            .oms_type(OmsType::Netting)
2159            .account_type(AccountType::Margin)
2160            .book_type(BookType::L1_MBP)
2161            .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
2162            .liquidation_enabled(expected)
2163            .build()
2164            .unwrap();
2165        let exchange = SimulatedExchange::new(config, cache, clock).unwrap();
2166
2167        assert_eq!(exchange.liquidation_enabled(), expected);
2168    }
2169
2170    #[rstest]
2171    #[case(AccountType::Margin, Decimal::from(10))]
2172    #[case(AccountType::Cash, Decimal::ONE)]
2173    fn test_default_leverage_uses_account_type(
2174        #[case] account_type: AccountType,
2175        #[case] expected: Decimal,
2176    ) {
2177        let config = SimulatedVenueConfig::builder()
2178            .venue(Venue::new("SIM"))
2179            .oms_type(OmsType::Netting)
2180            .account_type(account_type)
2181            .book_type(BookType::L1_MBP)
2182            .starting_balances(vec![Money::from("1_000 USD")])
2183            .build()
2184            .unwrap();
2185        let cache = Rc::new(RefCell::new(Cache::default()));
2186        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2187
2188        let exchange = SimulatedExchange::new(config, cache, clock).unwrap();
2189
2190        assert_eq!(exchange.default_leverage, expected);
2191    }
2192
2193    fn query_order() -> TradingCommand {
2194        TradingCommand::QueryOrder(QueryOrder::new(
2195            TraderId::test_default(),
2196            None,
2197            StrategyId::test_default(),
2198            InstrumentId::from("AUD/USD.SIM"),
2199            ClientOrderId::from("O-001"),
2200            None,
2201            UUID4::new(),
2202            UnixNanos::default(),
2203            None,
2204            None,
2205        ))
2206    }
2207
2208    fn query_account() -> TradingCommand {
2209        TradingCommand::QueryAccount(QueryAccount::new(
2210            TraderId::test_default(),
2211            None,
2212            AccountId::test_default(),
2213            UUID4::new(),
2214            UnixNanos::default(),
2215            None,
2216            None,
2217        ))
2218    }
2219
2220    #[rstest]
2221    fn test_inflight_command_matches_settlement_scope() {
2222        let inflight = InflightCommand::new(UnixNanos::from(1), 0, query_order());
2223        let instrument_id = InstrumentId::from("AUD/USD.SIM");
2224        let other_id = InstrumentId::from("GBP/USD.SIM");
2225
2226        assert!(inflight.matches_scope(UnixNanos::from(1), SettlementScope::All));
2227        assert!(inflight.matches_scope(
2228            UnixNanos::from(1),
2229            SettlementScope::Data(Some(instrument_id)),
2230        ));
2231        assert!(
2232            !inflight.matches_scope(UnixNanos::from(1), SettlementScope::Data(Some(other_id)),)
2233        );
2234        assert!(!inflight.matches_scope(UnixNanos::from(1), SettlementScope::Data(None)));
2235        assert!(inflight.matches_scope(UnixNanos::default(), SettlementScope::Data(None)));
2236    }
2237
2238    #[rstest]
2239    #[case(Dispatch::Latency)]
2240    #[case(Dispatch::Queued)]
2241    #[case(Dispatch::Immediate)]
2242    fn test_send_query_order_is_no_op(#[case] dispatch: Dispatch) {
2243        let mut exchange = setup_exchange(dispatch);
2244
2245        exchange.send(query_order());
2246
2247        assert!(!exchange.has_pending_commands(UnixNanos::from(u64::MAX)));
2248        assert_eq!(exchange.max_inflight_command_ts(), None);
2249    }
2250
2251    #[rstest]
2252    #[case(Dispatch::Latency)]
2253    #[case(Dispatch::Queued)]
2254    #[case(Dispatch::Immediate)]
2255    fn test_send_query_account_is_no_op(#[case] dispatch: Dispatch) {
2256        let mut exchange = setup_exchange(dispatch);
2257
2258        exchange.send(query_account());
2259
2260        assert!(!exchange.has_pending_commands(UnixNanos::from(u64::MAX)));
2261        assert_eq!(exchange.max_inflight_command_ts(), None);
2262    }
2263
2264    #[rstest]
2265    fn test_add_instrument_raw_id_overflow_does_not_mutate_maps(audusd_sim: CurrencyPair) {
2266        let mut exchange = setup_exchange(Dispatch::Immediate);
2267        exchange.last_raw_id = u32::MAX;
2268
2269        let result = exchange.add_instrument(InstrumentAny::CurrencyPair(audusd_sim));
2270
2271        assert!(result.is_err());
2272        assert!(exchange.instruments.is_empty());
2273        assert!(exchange.matching_engines.is_empty());
2274        assert_eq!(exchange.last_raw_id, u32::MAX);
2275    }
2276
2277    #[rstest]
2278    fn test_reset_clears_inflight_counter() {
2279        let mut exchange = setup_exchange(Dispatch::Latency);
2280        let account = MarginAccount::new(
2281            AccountState::new(
2282                AccountId::test_default(),
2283                AccountType::Margin,
2284                vec![AccountBalance::new(
2285                    Money::from("1000 USD"),
2286                    Money::from("0 USD"),
2287                    Money::from("1000 USD"),
2288                )],
2289                vec![],
2290                false,
2291                UUID4::default(),
2292                UnixNanos::default(),
2293                UnixNanos::default(),
2294                None,
2295            ),
2296            false,
2297        );
2298        exchange
2299            .cache
2300            .borrow_mut()
2301            .add_account(AccountAny::Margin(account))
2302            .unwrap();
2303
2304        let order = OrderTestBuilder::new(OrderType::Limit)
2305            .instrument_id(InstrumentId::from("AUD/USD.SIM"))
2306            .client_order_id(ClientOrderId::from("O-RESET"))
2307            .side(OrderSide::Buy)
2308            .quantity(Quantity::from("1"))
2309            .price(Price::from("1.00000"))
2310            .build();
2311        exchange
2312            .cache
2313            .borrow_mut()
2314            .add_order(order.clone(), None, None, false)
2315            .unwrap();
2316        exchange
2317            .cache
2318            .borrow_mut()
2319            .update_order(&TestOrderEventStubs::submitted(
2320                &order,
2321                AccountId::test_default(),
2322            ))
2323            .unwrap();
2324        exchange.send(TradingCommand::SubmitOrder(SubmitOrder::new(
2325            TraderId::test_default(),
2326            None,
2327            StrategyId::test_default(),
2328            order.instrument_id(),
2329            order.client_order_id(),
2330            order.init_event().clone(),
2331            None,
2332            None,
2333            None,
2334            UUID4::default(),
2335            UnixNanos::from(100),
2336            None,
2337        )));
2338
2339        assert_eq!(exchange.inflight_counter.len(), 1);
2340
2341        exchange.reset().unwrap();
2342
2343        assert!(exchange.inflight_counter.is_empty());
2344    }
2345}