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