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