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