Skip to main content

nautilus_backtest/
exchange.rs

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