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