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, TestClock},
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, OrderBookDepth10, 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 if self.frozen_account {
663 return true;
665 }
666
667 if let Some(exec_client) = &self.exec_client {
668 log::debug!("Adjusting account for venue {}", exec_client.venue());
669 }
670
671 match self.try_adjust_account(adjustment) {
672 Ok(()) => true,
673 Err(e) => {
674 log::error!("{e}");
675 false
676 }
677 }
678 }
679
680 pub fn try_adjust_account(&mut self, adjustment: Money) -> Result<(), AccountAdjustmentError> {
687 if self.frozen_account {
688 return Ok(());
690 }
691
692 if let Some(exec_client) = &self.exec_client {
693 let venue = exec_client.venue();
694 let account_state = {
695 let cache = self.cache.borrow();
696 if let Some(account) = cache.account_for_venue(&venue) {
697 if let Some(balance) = account.balance(Some(adjustment.currency)) {
698 let mut current_balance = *balance;
699 let Some(total) = current_balance.total.checked_add(adjustment) else {
700 return Err(AccountAdjustmentError::TotalOverflow(adjustment.currency));
701 };
702 let Some(free) = current_balance.free.checked_add(adjustment) else {
703 return Err(AccountAdjustmentError::FreeBalanceOverflow(
704 adjustment.currency,
705 ));
706 };
707 current_balance.total = total;
708 current_balance.free = free;
709
710 let margins = match &*account {
711 AccountAny::Margin(margin_account) => margin_account.margins.clone(),
712 _ => IndexMap::new(),
713 };
714
715 Some((
716 vec![current_balance],
717 margins.values().copied().collect(),
718 self.clock.borrow().timestamp_ns(),
719 ))
720 } else {
721 return Err(AccountAdjustmentError::MissingBalance(adjustment.currency));
722 }
723 } else {
724 return Err(AccountAdjustmentError::MissingAccount(venue));
725 }
726 };
727
728 if let Some((balances, margins, ts_event)) = account_state {
729 exec_client
730 .generate_account_state(balances, margins, true, ts_event, None)
731 .map_err(|e| AccountAdjustmentError::AccountStateGeneration(e.to_string()))?;
732 }
733 }
734 Ok(())
735 }
736
737 #[must_use]
739 pub fn has_pending_commands(&self, ts_now: UnixNanos) -> bool {
740 if !self.message_queue.is_empty() {
741 return true;
742 }
743 self.inflight_queue
744 .peek()
745 .is_some_and(|inflight| inflight.timestamp <= ts_now)
746 }
747
748 pub(crate) fn has_pending_commands_for_scope(
749 &self,
750 ts_now: UnixNanos,
751 scope: SettlementScope,
752 ) -> bool {
753 if matches!(scope, SettlementScope::All) {
754 return self.has_pending_commands(ts_now);
755 }
756
757 if !self.message_queue.is_empty() {
758 return true;
759 }
760
761 self.inflight_queue
762 .iter()
763 .any(|inflight| inflight.timestamp <= ts_now && inflight.matches_scope(ts_now, scope))
764 }
765
766 #[must_use]
773 pub fn max_inflight_command_ts(&self) -> Option<UnixNanos> {
774 self.inflight_queue.iter().map(|c| c.timestamp).max()
775 }
776
777 pub fn iterate_matching_engines(&mut self, ts_now: UnixNanos) {
780 for matching_engine in self.matching_engines.values_mut() {
781 matching_engine.iterate(ts_now, AggressorSide::NoAggressor);
782 }
783 }
784
785 pub fn process_instrument_expirations(&mut self, ts_now: UnixNanos) {
787 for matching_engine in self.matching_engines.values_mut() {
788 if matching_engine
789 .instrument
790 .expiration_ns()
791 .is_some_and(|expiration_ns| ts_now >= expiration_ns)
792 {
793 matching_engine.process_instrument_expiration(ts_now);
794 }
795 }
796 }
797
798 #[must_use]
800 pub fn instrument_expirations(&self) -> Vec<(InstrumentId, UnixNanos)> {
801 self.matching_engines
802 .values()
803 .filter(|matching_engine| !matching_engine.is_expiration_processed())
804 .filter_map(|matching_engine| {
805 matching_engine
806 .instrument
807 .expiration_ns()
808 .filter(|expiration_ns| *expiration_ns > UnixNanos::default())
809 .map(|expiration_ns| (matching_engine.instrument.id(), expiration_ns))
810 })
811 .collect()
812 }
813
814 pub fn set_clock_time(&self, ts_now: UnixNanos) {
822 let mut clock_ref = self.clock.borrow_mut();
823 let test_clock = clock_ref
824 .as_any_mut()
825 .downcast_mut::<TestClock>()
826 .expect("SimulatedExchange requires TestClock");
827 test_clock.set_time(ts_now);
828 }
829
830 pub fn send(&mut self, command: TradingCommand) {
832 if matches!(
833 &command,
834 TradingCommand::QueryOrder(_) | TradingCommand::QueryAccount(_)
835 ) {
836 log::warn!("Simulated exchange does not support queries: {command}");
837 return;
838 }
839
840 if self.use_message_queue {
841 self.inflight_orders.insert(&command);
842 }
843
844 if !self.use_message_queue {
845 let _guard = DeferEventsGuard::new(Rc::clone(&self.deferring_events));
846 self.process_trading_command(command);
847 } else if self.latency_model.is_none() {
848 self.message_queue.push_back(command);
849 } else {
850 let (timestamp, counter) = self.generate_inflight_command(&command);
851 self.inflight_queue
852 .push(InflightCommand::new(timestamp, counter, command));
853 }
854 }
855
856 fn generate_inflight_command(&mut self, command: &TradingCommand) -> (UnixNanos, u32) {
857 if let Some(latency_model) = &self.latency_model {
858 let ts = match command {
859 TradingCommand::SubmitOrder(_) | TradingCommand::SubmitOrderList(_) => {
860 command.ts_init() + latency_model.get_insert_latency()
861 }
862 TradingCommand::ModifyOrder(_) | TradingCommand::ModifyOrders(_) => {
863 command.ts_init() + latency_model.get_update_latency()
864 }
865 TradingCommand::CancelOrder(_)
866 | TradingCommand::CancelOrders(_)
867 | TradingCommand::CancelAllOrders(_) => {
868 command.ts_init() + latency_model.get_delete_latency()
869 }
870 _ => panic!("Cannot handle command: {command:?}"),
871 };
872
873 let counter = self
874 .inflight_counter
875 .entry(ts)
876 .and_modify(|e| *e += 1)
877 .or_insert(1);
878
879 (ts, *counter)
880 } else {
881 panic!("Latency model should be initialized");
882 }
883 }
884
885 pub fn process_order_book_delta(&mut self, delta: OrderBookDelta) -> anyhow::Result<()> {
891 self.pre_process_modules(&Data::BookDelta(delta))?;
892
893 if !self.matching_engines.contains_key(&delta.instrument_id) {
894 let instrument = {
895 let cache = self.cache.as_ref().borrow();
896 cache.instrument(&delta.instrument_id).cloned()
897 };
898
899 if let Some(instrument) = instrument {
900 self.add_instrument(instrument)?;
901 } else {
902 anyhow::bail!(
903 "No matching engine found for instrument {}",
904 delta.instrument_id
905 );
906 }
907 }
908
909 if let Some(matching_engine) = self.matching_engines.get_mut(&delta.instrument_id) {
910 matching_engine.process_order_book_delta(&delta)?;
911 } else {
912 anyhow::bail!("Matching engine should be initialized");
913 }
914 Ok(())
915 }
916
917 pub fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
923 self.pre_process_modules(&Data::BookDeltas(Box::new(deltas.clone())))?;
924
925 if !self.matching_engines.contains_key(&deltas.instrument_id) {
926 let instrument = {
927 let cache = self.cache.as_ref().borrow();
928 cache.instrument(&deltas.instrument_id).cloned()
929 };
930
931 if let Some(instrument) = instrument {
932 self.add_instrument(instrument)?;
933 } else {
934 anyhow::bail!(
935 "No matching engine found for instrument {}",
936 deltas.instrument_id
937 );
938 }
939 }
940
941 if let Some(matching_engine) = self.matching_engines.get_mut(&deltas.instrument_id) {
942 matching_engine.process_order_book_deltas(deltas)?;
943 } else {
944 anyhow::bail!("Matching engine should be initialized");
945 }
946 Ok(())
947 }
948
949 pub fn process_order_book_depth10(&mut self, depth: &OrderBookDepth10) -> anyhow::Result<()> {
955 self.pre_process_modules(&Data::BookDepth10(Box::new(*depth)))?;
956
957 if !self.matching_engines.contains_key(&depth.instrument_id) {
958 let instrument = {
959 let cache = self.cache.as_ref().borrow();
960 cache.instrument(&depth.instrument_id).cloned()
961 };
962
963 if let Some(instrument) = instrument {
964 self.add_instrument(instrument)?;
965 } else {
966 anyhow::bail!(
967 "No matching engine found for instrument {}",
968 depth.instrument_id
969 );
970 }
971 }
972
973 if let Some(matching_engine) = self.matching_engines.get_mut(&depth.instrument_id) {
974 matching_engine.process_order_book_depth10(depth)?;
975 } else {
976 anyhow::bail!("Matching engine should be initialized");
977 }
978 Ok(())
979 }
980
981 pub fn process_quote_tick(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
987 self.pre_process_modules(&Data::Quote(*quote))?;
988
989 if !self.matching_engines.contains_key("e.instrument_id) {
990 let instrument = {
991 let cache = self.cache.as_ref().borrow();
992 cache.instrument("e.instrument_id).cloned()
993 };
994
995 if let Some(instrument) = instrument {
996 self.add_instrument(instrument)?;
997 } else {
998 anyhow::bail!(
999 "No matching engine found for instrument {}",
1000 quote.instrument_id
1001 );
1002 }
1003 }
1004
1005 if let Some(matching_engine) = self.matching_engines.get_mut("e.instrument_id) {
1006 matching_engine.process_quote_tick(quote);
1007 } else {
1008 anyhow::bail!("Matching engine should be initialized");
1009 }
1010 Ok(())
1011 }
1012
1013 pub fn process_trade_tick(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
1019 self.pre_process_modules(&Data::Trade(*trade))?;
1020
1021 if !self.matching_engines.contains_key(&trade.instrument_id) {
1022 let instrument = {
1023 let cache = self.cache.as_ref().borrow();
1024 cache.instrument(&trade.instrument_id).cloned()
1025 };
1026
1027 if let Some(instrument) = instrument {
1028 self.add_instrument(instrument)?;
1029 } else {
1030 anyhow::bail!(
1031 "No matching engine found for instrument {}",
1032 trade.instrument_id
1033 );
1034 }
1035 }
1036
1037 if let Some(matching_engine) = self.matching_engines.get_mut(&trade.instrument_id) {
1038 matching_engine.process_trade_tick(trade);
1039 } else {
1040 anyhow::bail!("Matching engine should be initialized");
1041 }
1042 Ok(())
1043 }
1044
1045 pub fn process_bar(&mut self, bar: Bar) -> anyhow::Result<()> {
1051 self.pre_process_modules(&Data::Bar(bar))?;
1052
1053 if !self.matching_engines.contains_key(&bar.instrument_id()) {
1054 let instrument = {
1055 let cache = self.cache.as_ref().borrow();
1056 cache.instrument(&bar.instrument_id()).cloned()
1057 };
1058
1059 if let Some(instrument) = instrument {
1060 self.add_instrument(instrument)?;
1061 } else {
1062 anyhow::bail!(
1063 "No matching engine found for instrument {}",
1064 bar.instrument_id()
1065 );
1066 }
1067 }
1068
1069 if let Some(matching_engine) = self.matching_engines.get_mut(&bar.instrument_id()) {
1070 matching_engine.process_bar(&bar);
1071 } else {
1072 anyhow::bail!("Matching engine should be initialized");
1073 }
1074 Ok(())
1075 }
1076
1077 pub fn process_instrument_status(&mut self, status: InstrumentStatus) -> anyhow::Result<()> {
1083 self.pre_process_modules(&Data::InstrumentStatus(status))?;
1084
1085 if !self.matching_engines.contains_key(&status.instrument_id) {
1086 let instrument = {
1087 let cache = self.cache.as_ref().borrow();
1088 cache.instrument(&status.instrument_id).cloned()
1089 };
1090
1091 if let Some(instrument) = instrument {
1092 self.add_instrument(instrument)?;
1093 } else {
1094 anyhow::bail!(
1095 "No matching engine found for instrument {}",
1096 status.instrument_id
1097 );
1098 }
1099 }
1100
1101 if let Some(matching_engine) = self.matching_engines.get_mut(&status.instrument_id) {
1102 matching_engine.process_status(status.action);
1103 } else {
1104 anyhow::bail!("Matching engine should be initialized");
1105 }
1106 Ok(())
1107 }
1108
1109 pub fn process_instrument_close(&mut self, close: InstrumentClose) -> anyhow::Result<()> {
1115 self.pre_process_modules(&Data::InstrumentClose(close))?;
1116
1117 if !self.matching_engines.contains_key(&close.instrument_id) {
1118 let instrument = {
1119 let cache = self.cache.as_ref().borrow();
1120 cache.instrument(&close.instrument_id).cloned()
1121 };
1122
1123 if let Some(instrument) = instrument {
1124 self.add_instrument(instrument)?;
1125 } else {
1126 anyhow::bail!(
1127 "No matching engine found for instrument {}",
1128 close.instrument_id
1129 );
1130 }
1131 }
1132
1133 if let Some(matching_engine) = self.matching_engines.get_mut(&close.instrument_id) {
1134 matching_engine.process_instrument_close(close);
1135 } else {
1136 anyhow::bail!("Matching engine should be initialized");
1137 }
1138 Ok(())
1139 }
1140
1141 pub fn process_funding_rate(
1149 &mut self,
1150 funding_rate: FundingRateUpdate,
1151 ) -> anyhow::Result<Option<UnixNanos>> {
1152 let replay_ts = self.clock.borrow().timestamp_ns();
1153 let instrument_id = funding_rate.instrument_id;
1154 let boundary = Self::funding_boundary(&funding_rate);
1155 let next_boundary = self.queue_funding_rate(funding_rate)?;
1156
1157 if let Some(boundary) = boundary
1158 && boundary <= replay_ts
1159 {
1160 self.process_funding_settlement(instrument_id, boundary)?;
1161 return Ok(None);
1162 }
1163
1164 Ok(next_boundary)
1165 }
1166
1167 pub(crate) fn process_funding_rate_deferred(
1168 &mut self,
1169 funding_rate: FundingRateUpdate,
1170 replay_ts: UnixNanos,
1171 ) -> anyhow::Result<Option<UnixNanos>> {
1172 self.queue_funding_rate(funding_rate)?;
1173 Ok(self
1174 .next_funding_boundary()
1175 .filter(|boundary| *boundary > replay_ts))
1176 }
1177
1178 fn queue_funding_rate(
1179 &mut self,
1180 funding_rate: FundingRateUpdate,
1181 ) -> anyhow::Result<Option<UnixNanos>> {
1182 self.pre_process_modules(&Data::FundingRate(funding_rate))?;
1183
1184 let Some(boundary) = Self::funding_boundary(&funding_rate) else {
1185 log::debug!(
1186 "Funding rate update for {} does not define a settlement boundary",
1187 funding_rate.instrument_id
1188 );
1189 return Ok(None);
1190 };
1191
1192 let key = (boundary, funding_rate.instrument_id);
1193 if !self.funding_settlements.contains(&key) {
1194 self.pending_funding_rates.insert(key, funding_rate);
1195 }
1196 Ok(Some(boundary))
1197 }
1198
1199 pub fn process_funding_settlement(
1205 &mut self,
1206 instrument_id: InstrumentId,
1207 ts_event: UnixNanos,
1208 ) -> anyhow::Result<()> {
1209 self.check_module_error()?;
1210 let key = (ts_event, instrument_id);
1211 let Some(funding_rate) = self.pending_funding_rates.remove(&key) else {
1212 return Ok(());
1213 };
1214
1215 if !self.settle_funding_rate(&funding_rate, ts_event) {
1216 self.pending_funding_rates.insert(key, funding_rate);
1217 }
1218 Ok(())
1219 }
1220
1221 #[must_use]
1222 pub(crate) fn funding_boundaries_due(
1223 &self,
1224 replay_ts: UnixNanos,
1225 ) -> Vec<(UnixNanos, InstrumentId)> {
1226 self.pending_funding_rates
1227 .keys()
1228 .copied()
1229 .take_while(|(boundary, _)| *boundary <= replay_ts)
1230 .collect()
1231 }
1232
1233 pub(crate) fn settle_funding_boundary(
1234 &mut self,
1235 boundary: UnixNanos,
1236 instrument_id: InstrumentId,
1237 ) -> bool {
1238 let key = (boundary, instrument_id);
1239 let Some(funding_rate) = self.pending_funding_rates.remove(&key) else {
1240 return true;
1241 };
1242
1243 if self.settle_funding_rate(&funding_rate, boundary) {
1244 true
1245 } else {
1246 self.pending_funding_rates.insert(key, funding_rate);
1247 false
1248 }
1249 }
1250
1251 #[must_use]
1252 pub(crate) fn next_funding_boundary(&self) -> Option<UnixNanos> {
1253 self.pending_funding_rates
1254 .first_key_value()
1255 .map(|((boundary, _), _)| *boundary)
1256 }
1257
1258 fn settle_funding_rate(
1259 &mut self,
1260 funding_rate: &FundingRateUpdate,
1261 ts_event: UnixNanos,
1262 ) -> bool {
1263 let settlement_key = (ts_event, funding_rate.instrument_id);
1264 if self.funding_settlements.contains(&settlement_key) {
1265 return true;
1266 }
1267
1268 let Some(exec_client) = &self.exec_client else {
1269 log::warn!(
1270 "Cannot settle funding for {}: execution client is not registered",
1271 funding_rate.instrument_id
1272 );
1273 return false;
1274 };
1275 let account_id = exec_client.account_id();
1276 let account_venue = exec_client.venue();
1277
1278 if !self
1279 .matching_engines
1280 .contains_key(&funding_rate.instrument_id)
1281 {
1282 let instrument = {
1283 let cache = self.cache.as_ref().borrow();
1284 cache.instrument(&funding_rate.instrument_id).cloned()
1285 };
1286
1287 if let Some(instrument) = instrument {
1288 if let Err(e) = self.add_instrument(instrument) {
1289 log::error!(
1290 "Cannot settle funding for {}: failed to add instrument: {e}",
1291 funding_rate.instrument_id
1292 );
1293 return false;
1294 }
1295 } else {
1296 log::warn!(
1297 "Cannot settle funding for {}: no matching engine or instrument",
1298 funding_rate.instrument_id
1299 );
1300 return false;
1301 }
1302 }
1303
1304 let open_positions: Vec<Position> = {
1305 let cache = self.cache.borrow();
1306 cache
1307 .positions_open(
1308 Some(&self.id),
1309 Some(&funding_rate.instrument_id),
1310 None,
1311 Some(&account_id),
1312 None,
1313 )
1314 .into_iter()
1315 .map(|position| position.cloned())
1316 .collect()
1317 };
1318
1319 if open_positions.is_empty() {
1320 self.funding_settlements.insert(settlement_key);
1321 return true;
1322 }
1323
1324 let Some(settlement_price) = self.funding_settlement_price(funding_rate.instrument_id)
1325 else {
1326 log::warn!(
1327 "Cannot settle funding for {}: no mark price or top-of-book price",
1328 funding_rate.instrument_id
1329 );
1330 return false;
1331 };
1332
1333 let settlement_currency = open_positions[0].settlement_currency;
1334 let mut valued_positions = Vec::with_capacity(open_positions.len());
1335 let mut account_adjustments: AHashMap<Currency, Money> = AHashMap::new();
1336
1337 for position in open_positions {
1338 if position.settlement_currency != settlement_currency {
1339 log::error!(
1340 "Cannot settle funding for {}: position settlement currencies differ",
1341 funding_rate.instrument_id
1342 );
1343 return false;
1344 }
1345
1346 let notional = match position.try_notional_value(settlement_price) {
1347 Ok(notional) => notional,
1348 Err(e) => {
1349 log::error!(
1350 "Cannot settle funding for position {}: invalid notional value: {e}",
1351 position.id
1352 );
1353 return false;
1354 }
1355 };
1356 let side = if position.signed_qty > 0.0 {
1357 -Decimal::ONE
1358 } else {
1359 Decimal::ONE
1360 };
1361 let Some(amount) = notional
1362 .as_decimal()
1363 .checked_mul(funding_rate.rate)
1364 .and_then(|value| value.checked_mul(side))
1365 else {
1366 log::error!(
1367 "Cannot settle funding for position {}: funding amount overflow",
1368 position.id
1369 );
1370 return false;
1371 };
1372 let pnl_change = match Money::from_decimal(amount, notional.currency) {
1373 Ok(money) => money,
1374 Err(e) => {
1375 log::error!(
1376 "Cannot settle funding for position {}: invalid funding amount: {e}",
1377 position.id
1378 );
1379 return false;
1380 }
1381 };
1382
1383 if pnl_change.currency != settlement_currency {
1384 log::error!(
1385 "Cannot settle funding for position {}: settlement currency {} differs from funding currency {}",
1386 position.id,
1387 settlement_currency,
1388 pnl_change.currency
1389 );
1390 return false;
1391 }
1392
1393 if let Some(realized) = position.realized_pnl {
1394 if realized.currency != pnl_change.currency {
1395 log::error!(
1396 "Cannot settle funding for position {}: realized PnL currency {} differs from funding currency {}",
1397 position.id,
1398 realized.currency,
1399 pnl_change.currency
1400 );
1401 return false;
1402 }
1403
1404 if realized.checked_add(pnl_change).is_none() {
1405 log::error!(
1406 "Cannot settle funding for position {}: realized PnL overflow",
1407 position.id
1408 );
1409 return false;
1410 }
1411 }
1412 let total_adjustment =
1413 if let Some(current) = account_adjustments.get(&pnl_change.currency).copied() {
1414 let Some(total) = current.checked_add(pnl_change) else {
1415 log::error!(
1416 "Cannot settle funding for {}: aggregate account adjustment overflow",
1417 funding_rate.instrument_id
1418 );
1419 return false;
1420 };
1421 total
1422 } else {
1423 pnl_change
1424 };
1425 account_adjustments.insert(pnl_change.currency, total_adjustment);
1426 valued_positions.push((position, pnl_change));
1427 }
1428
1429 let mut account_adjustments = account_adjustments.into_values().collect::<Vec<_>>();
1430 account_adjustments.sort_unstable_by_key(|adjustment| adjustment.currency.code);
1431
1432 if !self.frozen_account {
1433 let cache = self.cache.borrow();
1434 let Some(account) = cache.account_for_venue(&account_venue) else {
1435 log::error!("Cannot settle funding: no account for venue {account_venue}");
1436 return false;
1437 };
1438
1439 for adjustment in &account_adjustments {
1440 let Some(balance) = account.balance(Some(adjustment.currency)) else {
1441 log::error!(
1442 "Cannot settle funding: no account balance for currency {}",
1443 adjustment.currency
1444 );
1445 return false;
1446 };
1447
1448 if balance.total.checked_add(*adjustment).is_none()
1449 || balance.free.checked_add(*adjustment).is_none()
1450 {
1451 log::error!(
1452 "Cannot settle funding: {} account adjustment exceeds Money bounds",
1453 adjustment.currency
1454 );
1455 return false;
1456 }
1457 }
1458 }
1459
1460 let ts_init = self.clock.borrow().timestamp_ns();
1461 let settlement = FundingSettlement::new(
1462 msgbus::get_message_bus().borrow().trader_id,
1463 funding_rate.instrument_id,
1464 account_id,
1465 funding_rate.rate,
1466 settlement_price,
1467 settlement_currency,
1468 UUID4::new(),
1469 ts_event,
1470 ts_init,
1471 );
1472 let mut adjusted_positions = Vec::with_capacity(valued_positions.len());
1473 for (original, pnl_change) in valued_positions {
1474 let mut adjusted = original.clone();
1475 let adjustment = PositionAdjusted::new(
1476 settlement.trader_id,
1477 adjusted.strategy_id,
1478 adjusted.instrument_id,
1479 adjusted.id,
1480 adjusted.account_id,
1481 PositionAdjustmentType::Funding,
1482 None,
1483 Some(pnl_change),
1484 Some(Ustr::from(&format!(
1485 "funding_settlement:{}",
1486 settlement.event_id
1487 ))),
1488 UUID4::new(),
1489 settlement.ts_event,
1490 settlement.ts_init,
1491 );
1492 adjusted.apply_adjustment(adjustment);
1493 adjusted_positions.push((original, adjusted, adjustment));
1494 }
1495
1496 {
1497 let mut cache = self.cache.borrow_mut();
1498
1499 for (index, (_, adjusted, _)) in adjusted_positions.iter().enumerate() {
1500 if let Err(e) = cache.update_position(adjusted) {
1501 log::error!(
1502 "Cannot update position {} after funding settlement: {e}",
1503 adjusted.id
1504 );
1505
1506 for (original, _, _) in adjusted_positions[..=index].iter().rev() {
1510 if let Err(rollback_error) = cache.update_position(original) {
1511 log::error!(
1512 "Cannot roll back position {} after failed funding settlement: {rollback_error}",
1513 original.id
1514 );
1515 }
1516 }
1517 return false;
1518 }
1519 }
1520 }
1521
1522 for adjustment in &account_adjustments {
1523 if !self.adjust_account(*adjustment) {
1524 let mut cache = self.cache.borrow_mut();
1525 for (original, _, _) in adjusted_positions.iter().rev() {
1526 if let Err(e) = cache.update_position(original) {
1527 log::error!(
1528 "Cannot roll back position {} after failed account adjustment: {e}",
1529 original.id
1530 );
1531 }
1532 }
1533 return false;
1534 }
1535 }
1536
1537 self.funding_settlements.insert(settlement_key);
1538 let settlement_topic = switchboard::get_funding_settlement_topic(settlement.instrument_id);
1539 msgbus::publish_any(settlement_topic, &settlement);
1540
1541 for (_, _, adjustment) in adjusted_positions {
1542 let event = PositionEvent::PositionAdjusted(adjustment);
1543 let PositionEvent::PositionAdjusted(adjustment) = &event else {
1544 continue;
1545 };
1546 let topic = switchboard::get_event_position_topic(adjustment.strategy_id);
1547 msgbus::publish_position_event(topic, &event);
1548 }
1549
1550 true
1551 }
1552
1553 fn funding_settlement_price(&self, instrument_id: InstrumentId) -> Option<Price> {
1554 if let Some(mark_price) = self.cache.borrow().mark_price(&instrument_id) {
1555 return Some(mark_price.value);
1556 }
1557
1558 let bid = self.best_bid_price(instrument_id)?;
1559 let ask = self.best_ask_price(instrument_id)?;
1560 let midpoint = (bid.as_decimal() + ask.as_decimal()) / Decimal::from(2);
1561 Price::from_decimal_dp(midpoint, bid.precision.max(ask.precision)).ok()
1562 }
1563
1564 fn is_interval_funding_boundary(funding_rate: &FundingRateUpdate) -> bool {
1565 let Some(interval_mins) = funding_rate.interval else {
1566 return false;
1567 };
1568 let Ok(interval) = DurationNanos::try_from_mins(u64::from(interval_mins)) else {
1569 return false;
1570 };
1571
1572 !interval.is_zero() && funding_rate.ts_event.floor(interval) == funding_rate.ts_event
1573 }
1574
1575 fn funding_boundary(funding_rate: &FundingRateUpdate) -> Option<UnixNanos> {
1576 funding_rate.next_funding_ns.or_else(|| {
1577 Self::is_interval_funding_boundary(funding_rate).then_some(funding_rate.ts_event)
1578 })
1579 }
1580
1581 pub fn process(&mut self, ts_now: UnixNanos) {
1589 self.process_commands(ts_now, SettlementScope::All);
1590 }
1591
1592 pub(crate) fn process_for_scope(&mut self, ts_now: UnixNanos, scope: SettlementScope) {
1593 self.process_commands(ts_now, scope);
1594 }
1595
1596 fn process_commands(&mut self, ts_now: UnixNanos, scope: SettlementScope) {
1597 self.set_clock_time(ts_now);
1598
1599 let mut deferred = Vec::new();
1600 let mut processed_timestamps = BTreeSet::new();
1601
1602 while let Some(inflight) = self.inflight_queue.peek() {
1603 if inflight.timestamp > ts_now {
1604 break;
1605 }
1606 let inflight = self.inflight_queue.pop().unwrap();
1607
1608 if !inflight.matches_scope(ts_now, scope) {
1609 deferred.push(inflight);
1610 continue;
1611 }
1612
1613 processed_timestamps.insert(inflight.timestamp);
1614 self.message_queue.push_back(inflight.command);
1615 }
1616
1617 let deferred_timestamps: BTreeSet<_> =
1618 deferred.iter().map(|inflight| inflight.timestamp).collect();
1619 self.inflight_queue.extend(deferred);
1620
1621 for timestamp in processed_timestamps.difference(&deferred_timestamps) {
1622 self.inflight_counter.remove(timestamp);
1623 }
1624
1625 while let Some(command) = self.message_queue.pop_front() {
1626 self.process_trading_command(command);
1627 }
1628 }
1629
1630 pub fn process_modules(&mut self, ts_now: UnixNanos) -> anyhow::Result<()> {
1640 self.check_module_error()?;
1641
1642 if self.frozen_account || self.exec_client.is_none() {
1643 return Ok(());
1644 }
1645
1646 let results = {
1647 let cache = self.cache.borrow();
1648 let ctx = ExchangeContext {
1649 venue: self.id,
1650 base_currency: self.base_currency,
1651 instruments: &self.instruments,
1652 matching_engines: &self.matching_engines,
1653 cache: &cache,
1654 };
1655 self.modules
1656 .iter()
1657 .enumerate()
1658 .map(|(module_index, module)| {
1659 module
1660 .process(ts_now, &ctx)
1661 .map(|result| (module_index, result))
1662 .map_err(|e| (module_index, e))
1663 })
1664 .collect::<Result<Vec<_>, _>>()
1665 };
1666 let results = match results {
1667 Ok(results) => results,
1668 Err((module_index, error)) => {
1669 return Err(self.store_module_error(module_index, "process", &error));
1670 }
1671 };
1672
1673 for (module_index, result) in results {
1674 if let SimulationModuleResult::Completed(adjustments) = result {
1675 let outcomes = adjustments
1676 .into_iter()
1677 .map(|adjustment| match self.try_adjust_account(adjustment) {
1678 Ok(()) => AccountAdjustmentOutcome::Applied,
1679 Err(e) => AccountAdjustmentOutcome::Failed(e),
1680 })
1681 .collect::<Vec<_>>();
1682
1683 if let Err(e) = self.modules[module_index].acknowledge(&outcomes) {
1684 return Err(self.store_module_error(module_index, "acknowledge", &e));
1685 }
1686 }
1687 }
1688 Ok(())
1689 }
1690
1691 pub fn reset(&mut self) -> anyhow::Result<()> {
1697 if !self.account_at_starting_balances() {
1698 self.generate_fresh_account_state();
1699 }
1700
1701 let mut module_error = None;
1702
1703 for (module_index, module) in self.modules.iter().enumerate() {
1704 if let Err(e) = module.reset()
1705 && module_error.is_none()
1706 {
1707 module_error = Some(format!(
1708 "Simulation module {module_index} reset failed: {e:#}"
1709 ));
1710 }
1711 }
1712
1713 for matching_engine in self.matching_engines.values_mut() {
1714 matching_engine.reset();
1715 }
1716
1717 self.pending_funding_rates.clear();
1718 self.funding_settlements.clear();
1719 self.message_queue.clear();
1720 self.inflight_queue.clear();
1721 self.inflight_orders.clear();
1722 self.inflight_counter.clear();
1723
1724 log::info!("Resetting exchange state");
1725 self.module_error = module_error;
1726 self.check_module_error()
1727 }
1728
1729 pub fn log_diagnostics(&self) -> anyhow::Result<()> {
1735 for (module_index, module) in self.modules.iter().enumerate() {
1736 module.log_diagnostics().map_err(|e| {
1737 anyhow::anyhow!("Simulation module {module_index} log_diagnostics failed: {e:#}")
1738 })?;
1739 }
1740 Ok(())
1741 }
1742
1743 pub fn process_liquidations(&mut self, ts_now: UnixNanos) {
1754 if !self.liquidation_enabled {
1755 return;
1756 }
1757
1758 if self.frozen_account {
1759 return;
1760 }
1761
1762 let account = {
1763 let cache = self.cache.borrow();
1764 cache.account_for_venue_owned(&self.id)
1765 };
1766 let Some(account) = account else { return };
1767 let AccountAny::Margin(margin_account) = &account else {
1768 return;
1769 };
1770 let account_id = margin_account.id();
1771
1772 let currencies: Vec<Currency> = margin_account.currencies();
1773
1774 let open_positions: Vec<Position> = {
1775 let cache = self.cache.borrow();
1776 cache
1777 .positions_open(Some(&self.id), None, None, None, None)
1778 .into_iter()
1779 .map(|p| p.cloned())
1780 .collect()
1781 };
1782
1783 let mut positions_by_currency: AHashMap<Currency, Vec<usize>> = AHashMap::new();
1785 for (i, p) in open_positions.iter().enumerate() {
1786 positions_by_currency
1787 .entry(p.settlement_currency)
1788 .or_default()
1789 .push(i);
1790 }
1791
1792 for currency in currencies {
1793 let Some(balance) = margin_account.balance(Some(currency)) else {
1794 continue;
1795 };
1796 let balance_f64 = balance.total.as_f64();
1797
1798 let Some(indices) = positions_by_currency.get(¤cy) else {
1799 continue;
1800 };
1801
1802 let (upnl_f64, all_priced) = {
1803 let cache = self.cache.borrow();
1804 let mut upnl = 0.0_f64;
1805 let mut all_priced = true;
1806
1807 for &i in indices {
1808 let p = &open_positions[i];
1809 if let Some(pnl) = cache.calculate_unrealized_pnl(p) {
1810 upnl += pnl.as_f64();
1811 } else {
1812 all_priced = false;
1813 break;
1814 }
1815 }
1816 (upnl, all_priced)
1817 };
1818
1819 if !all_priced {
1820 continue; }
1822
1823 let equity = balance_f64 + upnl_f64;
1824 let maintenance = margin_account.total_maintenance_margin(currency).as_f64();
1825
1826 if maintenance == 0.0 {
1827 continue;
1828 }
1829
1830 let threshold = maintenance * self.liquidation_trigger_ratio;
1831
1832 if equity > threshold {
1833 continue;
1834 }
1835
1836 log::warn!(
1837 "LIQUIDATION triggered for account {} currency {}: equity={:.4} <= threshold={:.4} (maintenance={:.4} x ratio={})",
1838 account_id,
1839 currency,
1840 equity,
1841 threshold,
1842 maintenance,
1843 self.liquidation_trigger_ratio
1844 );
1845
1846 for matching_engine in self.matching_engines.values_mut() {
1847 matching_engine.liquidate_open_positions(
1848 ts_now,
1849 self.liquidation_cancel_open_orders,
1850 currency,
1851 );
1852 }
1853 }
1854 }
1855
1856 fn process_trading_command(&mut self, command: TradingCommand) {
1857 self.inflight_orders.remove(&command);
1858 let instrument_id = command.instrument_id();
1859 assert!(
1860 self.matching_engines.contains_key(&instrument_id),
1861 "Matching engine not found for instrument {instrument_id}",
1862 );
1863
1864 let command = match command {
1865 TradingCommand::ModifyOrder(ref command)
1866 if self.process_modify_submitted_order(command) =>
1867 {
1868 return;
1869 }
1870 TradingCommand::ModifyOrders(mut command) => {
1871 command
1872 .modifies
1873 .retain(|modify| !self.process_modify_submitted_order(modify));
1874
1875 if command.modifies.is_empty() {
1876 return;
1877 }
1878 TradingCommand::ModifyOrders(command)
1879 }
1880 command => command,
1881 };
1882
1883 let account_id = if let Some(exec_client) = &self.exec_client {
1884 exec_client.account_id()
1885 } else {
1886 panic!("Execution client should be initialized");
1887 };
1888
1889 if let TradingCommand::SubmitOrderList(ref command) = command {
1890 let mut orders: Vec<OrderAny> = self
1891 .cache
1892 .borrow()
1893 .orders_for_ids(&command.order_list.client_order_ids, command);
1894
1895 for order in &mut orders {
1896 let order_instrument_id = order.instrument_id();
1897 if let Some(matching_engine) = self.matching_engines.get_mut(&order_instrument_id) {
1898 matching_engine.process_order(order, account_id);
1899 } else {
1900 panic!("Matching engine not found for instrument {order_instrument_id}");
1901 }
1902 }
1903
1904 return;
1905 }
1906
1907 if let Some(matching_engine) = self.matching_engines.get_mut(&instrument_id) {
1908 match command {
1909 TradingCommand::SubmitOrder(command) => {
1910 let mut order = self
1911 .cache
1912 .borrow()
1913 .order(&command.client_order_id)
1914 .map(|o| o.clone())
1915 .expect("Order must exist in cache");
1916 matching_engine.process_order(&mut order, account_id);
1917 }
1918 TradingCommand::ModifyOrder(ref command) => {
1919 matching_engine.process_modify(command, account_id);
1920 }
1921 TradingCommand::ModifyOrders(ref command) => {
1922 matching_engine.process_batch_modify(command, account_id);
1923 }
1924 TradingCommand::CancelOrder(ref command) => {
1925 matching_engine.process_cancel(command, account_id);
1926 }
1927 TradingCommand::CancelOrders(ref command) => {
1928 matching_engine.process_batch_cancel(command, account_id);
1929 }
1930 TradingCommand::CancelAllOrders(ref command) => {
1931 matching_engine.process_cancel_all(command, account_id);
1932 }
1933 _ => {}
1934 }
1935 } else {
1936 panic!("Matching engine not found for instrument {instrument_id}");
1937 }
1938 }
1939
1940 fn process_modify_submitted_order(&self, command: &ModifyOrder) -> bool {
1941 let Some(order) = self
1942 .cache
1943 .borrow()
1944 .order(&command.client_order_id)
1945 .map(|o| o.clone())
1946 else {
1947 return false;
1948 };
1949
1950 let modifies_submitted_order = matches!(order.status(), OrderStatus::Submitted)
1951 || (matches!(order.status(), OrderStatus::PendingUpdate)
1952 && order
1953 .previous_status()
1954 .is_some_and(|status| matches!(status, OrderStatus::Submitted)));
1955
1956 if !modifies_submitted_order {
1957 return false;
1958 }
1959
1960 self.generate_order_updated(
1961 &order,
1962 command.quantity.unwrap_or_else(|| order.quantity()),
1963 command.price.or_else(|| order.price()),
1964 command.trigger_price.or_else(|| order.trigger_price()),
1965 );
1966 true
1967 }
1968
1969 fn generate_order_updated(
1970 &self,
1971 order: &OrderAny,
1972 quantity: Quantity,
1973 price: Option<Price>,
1974 trigger_price: Option<Price>,
1975 ) {
1976 let ts_now = self.clock.borrow().timestamp_ns();
1977 let event = OrderEventAny::Updated(OrderUpdated::new(
1978 order.trader_id(),
1979 order.strategy_id(),
1980 order.instrument_id(),
1981 order.client_order_id(),
1982 quantity,
1983 UUID4::new(),
1984 ts_now,
1985 ts_now,
1986 false,
1987 order.venue_order_id(),
1988 order.account_id(),
1989 price,
1990 trigger_price,
1991 None,
1992 order.is_quote_quantity(),
1993 ));
1994 self.dispatch_order_event(event);
1995 }
1996
1997 fn dispatch_order_event(&self, event: OrderEventAny) {
1998 if let Some(handler) = &self.event_handler {
1999 handler(event);
2000 } else {
2001 msgbus::send_order_event(MessagingSwitchboard::exec_engine_process(), event);
2002 }
2003 }
2004
2005 fn account_at_starting_balances(&self) -> bool {
2006 let Some(account) = self.get_account() else {
2007 return false;
2008 };
2009
2010 let balances = account.balances();
2011
2012 for starting in &self.starting_balances {
2013 let Some(balance) = balances.get(&starting.currency) else {
2014 return false;
2015 };
2016
2017 if balance.total != *starting || balance.free != *starting {
2018 return false;
2019 }
2020 }
2021
2022 true
2023 }
2024
2025 fn generate_fresh_account_state(&self) {
2026 let balances: Vec<AccountBalance> = self
2027 .starting_balances
2028 .iter()
2029 .map(|money| AccountBalance::new(*money, Money::zero(money.currency), *money))
2030 .collect();
2031
2032 if let Some(exec_client) = &self.exec_client {
2033 let ts_event = self.clock.borrow().timestamp_ns();
2034 exec_client
2035 .generate_account_state(balances, vec![], true, ts_event, None)
2036 .unwrap();
2037 }
2038
2039 let calculate_account_state = !self.frozen_account;
2040
2041 if let Some(mut account) = self.get_account() {
2042 account.set_calculate_account_state(calculate_account_state);
2043
2044 match &mut account {
2045 AccountAny::Margin(margin_account) => {
2046 margin_account.set_default_leverage(self.default_leverage);
2047 for (instrument_id, leverage) in &self.leverages {
2048 margin_account.set_leverage(*instrument_id, *leverage);
2049 }
2050
2051 if let Some(model) = &self.margin_model {
2052 margin_account.set_margin_model(model.clone());
2053 }
2054 }
2055 AccountAny::Cash(cash_account) => {
2056 cash_account.allow_borrowing = self.allow_cash_borrowing;
2057 }
2058 AccountAny::Betting(_) | AccountAny::Wallet(_) => {}
2059 }
2060
2061 self.cache.borrow_mut().update_account(&account).unwrap();
2062 }
2063 }
2064}
2065
2066#[derive(Clone, Copy)]
2067pub(crate) enum SettlementScope {
2068 All,
2069 Data(Option<InstrumentId>),
2070}
2071
2072#[derive(Debug)]
2075struct DeferEventsGuard {
2076 deferring: Rc<Cell<bool>>,
2077}
2078
2079impl DeferEventsGuard {
2080 fn new(deferring: Rc<Cell<bool>>) -> Self {
2081 deferring.set(true);
2082 Self { deferring }
2083 }
2084}
2085
2086impl Drop for DeferEventsGuard {
2087 fn drop(&mut self) {
2088 self.deferring.set(false);
2089 }
2090}
2091
2092#[cfg(test)]
2093mod tests {
2094 use nautilus_common::messages::execution::{QueryAccount, QueryOrder, SubmitOrder};
2095 use nautilus_core::DurationNanos;
2096 use nautilus_execution::models::latency::{LatencyModelHandle, StaticLatencyModel};
2097 use nautilus_model::{
2098 accounts::MarginAccount,
2099 enums::{AccountType, BookType, OrderSide, OrderType},
2100 events::AccountState,
2101 identifiers::{ClientOrderId, StrategyId, TraderId},
2102 instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
2103 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
2104 stubs::TestDefault,
2105 types::AccountBalance,
2106 };
2107 use rstest::rstest;
2108
2109 use super::*;
2110
2111 #[derive(Clone, Copy)]
2113 enum Dispatch {
2114 Latency,
2116 Queued,
2118 Immediate,
2120 }
2121
2122 fn setup_exchange(dispatch: Dispatch) -> SimulatedExchange {
2123 let cache = Rc::new(RefCell::new(Cache::default()));
2124 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2125 let mut config = SimulatedVenueConfig::builder()
2126 .venue(Venue::new("SIM"))
2127 .oms_type(OmsType::Netting)
2128 .account_type(AccountType::Margin)
2129 .book_type(BookType::L2_MBP)
2130 .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
2131 .build()
2132 .unwrap();
2133
2134 match dispatch {
2135 Dispatch::Latency => {
2136 config.latency_model = Some(LatencyModelHandle::new(StaticLatencyModel::new(
2137 DurationNanos::default(),
2138 DurationNanos::default(),
2139 DurationNanos::default(),
2140 DurationNanos::default(),
2141 )));
2142 }
2143 Dispatch::Queued => {} Dispatch::Immediate => config.use_message_queue = false,
2145 }
2146
2147 SimulatedExchange::new(config, cache, clock).unwrap()
2148 }
2149
2150 #[rstest]
2151 #[case(false)]
2152 #[case(true)]
2153 fn test_liquidation_enabled(#[case] expected: bool) {
2154 let cache = Rc::new(RefCell::new(Cache::default()));
2155 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2156 let config = SimulatedVenueConfig::builder()
2157 .venue(Venue::new("SIM"))
2158 .oms_type(OmsType::Netting)
2159 .account_type(AccountType::Margin)
2160 .book_type(BookType::L1_MBP)
2161 .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
2162 .liquidation_enabled(expected)
2163 .build()
2164 .unwrap();
2165 let exchange = SimulatedExchange::new(config, cache, clock).unwrap();
2166
2167 assert_eq!(exchange.liquidation_enabled(), expected);
2168 }
2169
2170 #[rstest]
2171 #[case(AccountType::Margin, Decimal::from(10))]
2172 #[case(AccountType::Cash, Decimal::ONE)]
2173 fn test_default_leverage_uses_account_type(
2174 #[case] account_type: AccountType,
2175 #[case] expected: Decimal,
2176 ) {
2177 let config = SimulatedVenueConfig::builder()
2178 .venue(Venue::new("SIM"))
2179 .oms_type(OmsType::Netting)
2180 .account_type(account_type)
2181 .book_type(BookType::L1_MBP)
2182 .starting_balances(vec![Money::from("1_000 USD")])
2183 .build()
2184 .unwrap();
2185 let cache = Rc::new(RefCell::new(Cache::default()));
2186 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
2187
2188 let exchange = SimulatedExchange::new(config, cache, clock).unwrap();
2189
2190 assert_eq!(exchange.default_leverage, expected);
2191 }
2192
2193 fn query_order() -> TradingCommand {
2194 TradingCommand::QueryOrder(QueryOrder::new(
2195 TraderId::test_default(),
2196 None,
2197 StrategyId::test_default(),
2198 InstrumentId::from("AUD/USD.SIM"),
2199 ClientOrderId::from("O-001"),
2200 None,
2201 UUID4::new(),
2202 UnixNanos::default(),
2203 None,
2204 None,
2205 ))
2206 }
2207
2208 fn query_account() -> TradingCommand {
2209 TradingCommand::QueryAccount(QueryAccount::new(
2210 TraderId::test_default(),
2211 None,
2212 AccountId::test_default(),
2213 UUID4::new(),
2214 UnixNanos::default(),
2215 None,
2216 None,
2217 ))
2218 }
2219
2220 #[rstest]
2221 fn test_inflight_command_matches_settlement_scope() {
2222 let inflight = InflightCommand::new(UnixNanos::from(1), 0, query_order());
2223 let instrument_id = InstrumentId::from("AUD/USD.SIM");
2224 let other_id = InstrumentId::from("GBP/USD.SIM");
2225
2226 assert!(inflight.matches_scope(UnixNanos::from(1), SettlementScope::All));
2227 assert!(inflight.matches_scope(
2228 UnixNanos::from(1),
2229 SettlementScope::Data(Some(instrument_id)),
2230 ));
2231 assert!(
2232 !inflight.matches_scope(UnixNanos::from(1), SettlementScope::Data(Some(other_id)),)
2233 );
2234 assert!(!inflight.matches_scope(UnixNanos::from(1), SettlementScope::Data(None)));
2235 assert!(inflight.matches_scope(UnixNanos::default(), SettlementScope::Data(None)));
2236 }
2237
2238 #[rstest]
2239 #[case(Dispatch::Latency)]
2240 #[case(Dispatch::Queued)]
2241 #[case(Dispatch::Immediate)]
2242 fn test_send_query_order_is_no_op(#[case] dispatch: Dispatch) {
2243 let mut exchange = setup_exchange(dispatch);
2244
2245 exchange.send(query_order());
2246
2247 assert!(!exchange.has_pending_commands(UnixNanos::from(u64::MAX)));
2248 assert_eq!(exchange.max_inflight_command_ts(), None);
2249 }
2250
2251 #[rstest]
2252 #[case(Dispatch::Latency)]
2253 #[case(Dispatch::Queued)]
2254 #[case(Dispatch::Immediate)]
2255 fn test_send_query_account_is_no_op(#[case] dispatch: Dispatch) {
2256 let mut exchange = setup_exchange(dispatch);
2257
2258 exchange.send(query_account());
2259
2260 assert!(!exchange.has_pending_commands(UnixNanos::from(u64::MAX)));
2261 assert_eq!(exchange.max_inflight_command_ts(), None);
2262 }
2263
2264 #[rstest]
2265 fn test_add_instrument_raw_id_overflow_does_not_mutate_maps(audusd_sim: CurrencyPair) {
2266 let mut exchange = setup_exchange(Dispatch::Immediate);
2267 exchange.last_raw_id = u32::MAX;
2268
2269 let result = exchange.add_instrument(InstrumentAny::CurrencyPair(audusd_sim));
2270
2271 assert!(result.is_err());
2272 assert!(exchange.instruments.is_empty());
2273 assert!(exchange.matching_engines.is_empty());
2274 assert_eq!(exchange.last_raw_id, u32::MAX);
2275 }
2276
2277 #[rstest]
2278 fn test_reset_clears_inflight_counter() {
2279 let mut exchange = setup_exchange(Dispatch::Latency);
2280 let account = MarginAccount::new(
2281 AccountState::new(
2282 AccountId::test_default(),
2283 AccountType::Margin,
2284 vec![AccountBalance::new(
2285 Money::from("1000 USD"),
2286 Money::from("0 USD"),
2287 Money::from("1000 USD"),
2288 )],
2289 vec![],
2290 false,
2291 UUID4::default(),
2292 UnixNanos::default(),
2293 UnixNanos::default(),
2294 None,
2295 ),
2296 false,
2297 );
2298 exchange
2299 .cache
2300 .borrow_mut()
2301 .add_account(AccountAny::Margin(account))
2302 .unwrap();
2303
2304 let order = OrderTestBuilder::new(OrderType::Limit)
2305 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
2306 .client_order_id(ClientOrderId::from("O-RESET"))
2307 .side(OrderSide::Buy)
2308 .quantity(Quantity::from("1"))
2309 .price(Price::from("1.00000"))
2310 .build();
2311 exchange
2312 .cache
2313 .borrow_mut()
2314 .add_order(order.clone(), None, None, false)
2315 .unwrap();
2316 exchange
2317 .cache
2318 .borrow_mut()
2319 .update_order(&TestOrderEventStubs::submitted(
2320 &order,
2321 AccountId::test_default(),
2322 ))
2323 .unwrap();
2324 exchange.send(TradingCommand::SubmitOrder(SubmitOrder::new(
2325 TraderId::test_default(),
2326 None,
2327 StrategyId::test_default(),
2328 order.instrument_id(),
2329 order.client_order_id(),
2330 order.init_event().clone(),
2331 None,
2332 None,
2333 None,
2334 UUID4::default(),
2335 UnixNanos::from(100),
2336 None,
2337 )));
2338
2339 assert_eq!(exchange.inflight_counter.len(), 1);
2340
2341 exchange.reset().unwrap();
2342
2343 assert!(exchange.inflight_counter.is_empty());
2344 }
2345}