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