1use std::{cell::RefCell, collections::BinaryHeap, fmt::Debug, rc::Rc};
19
20use ahash::{AHashMap, AHashSet};
21use async_trait::async_trait;
22use nautilus_common::{
23 cache::Cache,
24 clients::ExecutionClient,
25 clock::Clock,
26 factories::OrderEventFactory,
27 live::try_get_exec_event_sender,
28 messages::{
29 ExecutionEvent,
30 execution::{
31 BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder,
32 GenerateFillReports, GenerateOrderStatusReport, GenerateOrderStatusReports,
33 GeneratePositionStatusReports, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
34 SubmitOrderList, TradingCommand,
35 },
36 },
37 msgbus::{
38 self, MStr, MessagingSwitchboard, Pattern, TypedHandler,
39 typed_handler::ShareableMessageHandler,
40 },
41 timer::{TimeEvent, TimeEventCallback},
42};
43use nautilus_core::{DurationNanos, Params, UUID4, UnixNanos, WeakCell};
44use nautilus_execution::{
45 client::core::ExecutionClientCore,
46 matching_engine::{OrderMatchingEngine, inflight::InflightOrders},
47 models::{fee::FeeModelHandle, fill::FillModelHandle, latency::LatencyModel},
48};
49use nautilus_model::{
50 accounts::AccountAny,
51 data::{Bar, InstrumentClose, InstrumentStatus, OrderBookDeltas, QuoteTick, TradeTick},
52 enums::OmsType,
53 events::{
54 OrderCancelRejected, OrderEventAny, OrderModifyRejected, OrderRejected, PositionEvent,
55 },
56 identifiers::{
57 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, TraderId, Venue, VenueOrderId,
58 },
59 instruments::{Instrument, InstrumentAny},
60 orders::{Order, OrderAny},
61 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
62 types::{AccountBalance, MarginBalance, Money},
63};
64use ustr::Ustr;
65
66use crate::config::SandboxExecutionClientConfig;
67
68const EXPIRED_ENGINE_SWEEP_INTERVAL: DurationNanos = DurationNanos::from_mins(1);
70
71pub struct SandboxExecutionClient {
77 core: RefCell<ExecutionClientCore>,
78 factory: OrderEventFactory,
79 config: SandboxExecutionClientConfig,
80 inner: Rc<RefCell<SandboxInner>>,
81 handlers: RefCell<Option<RegisteredHandlers>>,
82 clock: Rc<RefCell<dyn Clock>>,
83 cache: Rc<RefCell<Cache>>,
84}
85
86struct RegisteredHandlers {
87 deltas_pattern: MStr<Pattern>,
88 deltas_handler: TypedHandler<OrderBookDeltas>,
89 quote_pattern: MStr<Pattern>,
90 quote_handler: TypedHandler<QuoteTick>,
91 trade_pattern: MStr<Pattern>,
92 trade_handler: TypedHandler<TradeTick>,
93 bar_pattern: MStr<Pattern>,
94 bar_handler: TypedHandler<Bar>,
95 status_pattern: MStr<Pattern>,
96 status_handler: ShareableMessageHandler,
97 close_pattern: MStr<Pattern>,
98 close_handler: ShareableMessageHandler,
99 position_pattern: MStr<Pattern>,
100 position_handler: TypedHandler<PositionEvent>,
101}
102
103impl Debug for SandboxExecutionClient {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct(stringify!(SandboxExecutionClient))
106 .field("venue", &self.config.venue)
107 .field("account_id", &self.core.borrow().account_id)
108 .field("connected", &self.core.borrow().is_connected())
109 .field(
110 "matching_engines",
111 &self.inner.borrow().matching_engines.len(),
112 )
113 .finish()
114 }
115}
116
117impl SandboxExecutionClient {
118 pub fn new(
129 core: ExecutionClientCore,
130 config: SandboxExecutionClientConfig,
131 clock: Rc<RefCell<dyn Clock>>,
132 cache: Rc<RefCell<Cache>>,
133 ) -> anyhow::Result<Self> {
134 let mut balances = AHashMap::new();
135 for money in &config.starting_balances {
136 balances.insert(money.currency.code.to_string(), *money);
137 }
138
139 let fill_model = config
140 .fill_model
141 .clone()
142 .map(FillModelHandle::from)
143 .unwrap_or_default();
144
145 let fee_model = config.fee_model.clone().map(FeeModelHandle::from).ok_or_else(|| {
146 anyhow::anyhow!(
147 "SandboxExecutionClientConfig requires an explicit fee_model, including an explicit zero-fee model"
148 )
149 })?;
150
151 let inner = Rc::new_cyclic(|weak: &std::rc::Weak<RefCell<SandboxInner>>| {
152 RefCell::new(SandboxInner {
153 clock: clock.clone(),
154 cache: cache.clone(),
155 config: config.clone(),
156 fill_model,
157 fee_model,
158 matching_engines: AHashMap::new(),
159 next_engine_raw_id: 0,
160 balances,
161 event_handler: None,
162 inbound_queue: BinaryHeap::new(),
163 inflight_orders: InflightOrders::default(),
164 inbound_seq: 0,
165 client_id: core.client_id,
166 account_id: core.account_id,
167 self_weak: WeakCell::from(weak.clone()),
168 })
169 });
170
171 let factory = OrderEventFactory::new(
172 core.trader_id,
173 core.account_id,
174 core.account_type,
175 core.base_currency,
176 );
177
178 Ok(Self {
179 core: RefCell::new(core),
180 factory,
181 config,
182 inner,
183 handlers: RefCell::new(None),
184 clock,
185 cache,
186 })
187 }
188
189 #[must_use]
191 pub const fn config(&self) -> &SandboxExecutionClientConfig {
192 &self.config
193 }
194
195 #[must_use]
197 pub fn matching_engine_count(&self) -> usize {
198 self.inner.borrow().matching_engines.len()
199 }
200
201 fn dispatch_order_event(&self, event: OrderEventAny) {
202 self.inner.borrow().dispatch_order_event(event);
203 }
204
205 fn register_message_handlers(&self) {
206 if self.handlers.borrow().is_some() {
207 log::warn!("Sandbox message handlers already registered");
208 return;
209 }
210
211 let inner_weak = WeakCell::from(Rc::downgrade(&self.inner));
212 let venue = self.config.venue;
213 let account_id = self.core.borrow().account_id;
214
215 let deltas_handler = {
216 let inner = inner_weak.clone();
217 TypedHandler::from(move |deltas: &OrderBookDeltas| {
218 if deltas.instrument_id.venue == venue
219 && let Some(inner_rc) = inner.upgrade()
220 {
221 SandboxInner::on_order_book_deltas(&inner_rc, deltas);
222 }
223 })
224 };
225
226 let quote_handler = {
227 let inner = inner_weak.clone();
228 TypedHandler::from(move |quote: &QuoteTick| {
229 if quote.instrument_id.venue == venue
230 && let Some(inner_rc) = inner.upgrade()
231 {
232 SandboxInner::on_quote_tick(&inner_rc, quote);
233 }
234 })
235 };
236
237 let trade_handler = {
238 let inner = inner_weak.clone();
239 TypedHandler::from(move |trade: &TradeTick| {
240 if trade.instrument_id.venue == venue
241 && let Some(inner_rc) = inner.upgrade()
242 {
243 SandboxInner::on_trade_tick(&inner_rc, trade);
244 }
245 })
246 };
247
248 let bar_handler = {
250 let inner = inner_weak.clone();
251 TypedHandler::from(move |bar: &Bar| {
252 if bar.bar_type.instrument_id().venue == venue
253 && let Some(inner_rc) = inner.upgrade()
254 {
255 SandboxInner::on_bar(&inner_rc, bar);
256 }
257 })
258 };
259
260 let status_handler = {
261 let inner = inner_weak.clone();
262 ShareableMessageHandler::from_typed(move |status: &InstrumentStatus| {
263 if status.instrument_id.venue == venue
264 && let Some(inner_rc) = inner.upgrade()
265 {
266 SandboxInner::on_instrument_status(&inner_rc, status);
267 }
268 })
269 };
270
271 let close_handler = {
272 let inner = inner_weak.clone();
273 ShareableMessageHandler::from_typed(move |close: &InstrumentClose| {
274 if close.instrument_id.venue == venue
275 && let Some(inner_rc) = inner.upgrade()
276 {
277 SandboxInner::on_instrument_close(&inner_rc, close);
278 }
279 })
280 };
281
282 let position_handler = {
283 TypedHandler::from(move |event: &PositionEvent| {
284 let PositionEvent::PositionClosed(position_closed) = event else {
285 return;
286 };
287
288 if position_closed.instrument_id.venue == venue
289 && position_closed.account_id == account_id
290 && let Some(inner_rc) = inner_weak.upgrade()
291 {
292 if let Ok(mut inner) = inner_rc.try_borrow_mut() {
295 inner.sync_expired_cleanup(position_closed.instrument_id);
296 } else {
297 log::debug!(
298 "Skipping immediate expired cleanup retry for {} due to active sandbox borrow",
299 position_closed.instrument_id,
300 );
301 }
302 }
303 })
304 };
305
306 let deltas_pattern: MStr<Pattern> = format!("data.book.deltas.{venue}.*").into();
307 let quote_pattern: MStr<Pattern> = format!("data.quotes.{venue}.*").into();
308 let trade_pattern: MStr<Pattern> = format!("data.trades.{venue}.*").into();
309 let bar_pattern: MStr<Pattern> = "data.bars.*".into();
310 let status_pattern: MStr<Pattern> = format!("data.status.{venue}.*").into();
311 let close_pattern: MStr<Pattern> = format!("data.close.{venue}.*").into();
312 let position_pattern: MStr<Pattern> = "events.position.*".into();
313
314 msgbus::subscribe_book_deltas(deltas_pattern, deltas_handler.clone(), Some(10));
315 msgbus::subscribe_quotes(quote_pattern, quote_handler.clone(), Some(10));
316 msgbus::subscribe_trades(trade_pattern, trade_handler.clone(), Some(10));
317 msgbus::subscribe_bars(bar_pattern, bar_handler.clone(), Some(10));
318 msgbus::subscribe_any(status_pattern, status_handler.clone(), Some(10));
319 msgbus::subscribe_instrument_close(close_pattern, close_handler.clone(), Some(10));
320 msgbus::subscribe_position_events(position_pattern, position_handler.clone(), Some(10));
321
322 *self.handlers.borrow_mut() = Some(RegisteredHandlers {
323 deltas_pattern,
324 deltas_handler,
325 quote_pattern,
326 quote_handler,
327 trade_pattern,
328 trade_handler,
329 bar_pattern,
330 bar_handler,
331 status_pattern,
332 status_handler,
333 close_pattern,
334 close_handler,
335 position_pattern,
336 position_handler,
337 });
338
339 log::debug!(
340 "Sandbox registered message handlers for venue={}",
341 self.config.venue
342 );
343 }
344
345 fn deregister_message_handlers(&self) {
346 if let Some(handlers) = self.handlers.borrow_mut().take() {
347 msgbus::unsubscribe_book_deltas(handlers.deltas_pattern, &handlers.deltas_handler);
348 msgbus::unsubscribe_quotes(handlers.quote_pattern, &handlers.quote_handler);
349 msgbus::unsubscribe_trades(handlers.trade_pattern, &handlers.trade_handler);
350 msgbus::unsubscribe_bars(handlers.bar_pattern, &handlers.bar_handler);
351 msgbus::unsubscribe_any(handlers.status_pattern, &handlers.status_handler);
352 msgbus::unsubscribe_instrument_close(handlers.close_pattern, &handlers.close_handler);
353 msgbus::unsubscribe_position_events(
354 handlers.position_pattern,
355 &handlers.position_handler,
356 );
357
358 log::debug!(
359 "Sandbox deregistered message handlers for venue={}",
360 self.config.venue
361 );
362 }
363 }
364
365 fn expiry_sweep_timer_name(&self) -> String {
366 format!("{}-sandbox-expiry-sweep", self.core.borrow().client_id)
367 }
368
369 fn register_expiry_sweep_timer(&self) {
370 let inner_weak = WeakCell::from(Rc::downgrade(&self.inner));
371 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event: TimeEvent| {
372 let Some(inner_rc) = inner_weak.upgrade() else {
373 return;
374 };
375
376 if let Ok(mut inner) = inner_rc.try_borrow_mut() {
379 inner.sweep_expired_engines();
380 } else {
381 log::debug!("Skipping sandbox expiry sweep due to active borrow");
382 }
383 });
384
385 let name = self.expiry_sweep_timer_name();
386
387 if let Err(e) = self.clock.borrow_mut().set_timer_ns(
388 &name,
389 EXPIRED_ENGINE_SWEEP_INTERVAL,
390 None,
391 None,
392 Some(TimeEventCallback::from(callback)),
393 None,
394 None,
395 ) {
396 log::error!("Failed to register sandbox expiry sweep timer: {e}");
397 }
398 }
399
400 fn cancel_expiry_sweep_timer(&self) {
401 self.clock
402 .borrow_mut()
403 .cancel_timer(&self.expiry_sweep_timer_name());
404 }
405
406 fn cancel_inbound_alert(&self) {
407 let client_id = self.core.borrow().client_id;
408 self.clock
409 .borrow_mut()
410 .cancel_timer(&inbound_alert_name(client_id));
411 }
412
413 fn get_current_account_balances(&self) -> Vec<AccountBalance> {
414 let account_id = self.core.borrow().account_id;
415 let cache = self.cache.borrow();
416
417 if let Some(account) = cache.account(&account_id) {
418 return account.balances().into_values().collect();
419 }
420
421 self.get_account_balances()
422 }
423
424 fn sync_cached_account_config(&self) -> anyhow::Result<()> {
425 let Some(mut account) = self.get_account() else {
426 return Ok(());
427 };
428
429 account.set_calculate_account_state(!self.config.frozen_account);
430
431 if let AccountAny::Margin(margin_account) = &mut account {
432 margin_account.set_default_leverage(self.config.default_leverage);
433 for (instrument_id, leverage) in &self.config.leverages {
434 margin_account.set_leverage(*instrument_id, *leverage);
435 }
436 }
437
438 self.cache.borrow_mut().update_account(&account)
439 }
440
441 pub fn process_quote_tick(&self, quote: &QuoteTick) -> anyhow::Result<()> {
447 SandboxInner::drain_inbound(&self.inner);
448
449 let instrument_id = quote.instrument_id;
450 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
451
452 if !check_quote_or_drop("quote tick", quote, &instrument) {
453 return Ok(());
454 }
455
456 let mut inner = self.inner.borrow_mut();
457 inner.ensure_matching_engine(&instrument);
458 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
459 engine.process_quote_tick(quote);
460 }
461 Ok(())
462 }
463
464 pub fn process_trade_tick(&self, trade: &TradeTick) -> anyhow::Result<()> {
470 SandboxInner::drain_inbound(&self.inner);
471
472 if !self.config.trade_execution {
473 return Ok(());
474 }
475
476 let instrument_id = trade.instrument_id;
477 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
478
479 if !check_trade_or_drop("trade tick", trade, &instrument) {
480 return Ok(());
481 }
482
483 let mut inner = self.inner.borrow_mut();
484 inner.ensure_matching_engine(&instrument);
485 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
486 engine.process_trade_tick(trade);
487 }
488 Ok(())
489 }
490
491 pub fn process_bar(&self, bar: &Bar) -> anyhow::Result<()> {
497 SandboxInner::drain_inbound(&self.inner);
498
499 if !self.config.bar_execution {
500 return Ok(());
501 }
502
503 let instrument_id = bar.bar_type.instrument_id();
504 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
505
506 if !check_bar_or_drop("bar", bar, &instrument) {
507 return Ok(());
508 }
509
510 let mut inner = self.inner.borrow_mut();
511 inner.ensure_matching_engine(&instrument);
512 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
513 engine.process_bar(bar);
514 }
515 Ok(())
516 }
517
518 pub fn process_order_book_deltas(&self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
524 SandboxInner::drain_inbound(&self.inner);
525
526 let instrument_id = deltas.instrument_id;
527 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
528
529 let mut inner = self.inner.borrow_mut();
530 inner.ensure_matching_engine(&instrument);
531 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
532 engine.process_order_book_deltas(deltas)?;
533 }
534 Ok(())
535 }
536
537 pub fn reset(&self) {
539 let mut inner = self.inner.borrow_mut();
540 for engine in inner.matching_engines.values_mut() {
541 engine.reset();
542 }
543
544 inner.balances.clear();
545 for money in &self.config.starting_balances {
546 inner
547 .balances
548 .insert(money.currency.code.to_string(), *money);
549 }
550
551 inner.clear_inbound_queue();
552 self.cancel_inbound_alert();
553
554 log::info!(
555 "Sandbox execution client reset: venue={}",
556 self.config.venue
557 );
558 }
559
560 fn get_account_balances(&self) -> Vec<AccountBalance> {
561 self.inner
562 .borrow()
563 .balances
564 .values()
565 .map(|money| AccountBalance::new(*money, Money::zero(money.currency), *money))
566 .collect()
567 }
568
569 fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
570 Ok(self.cache.borrow().try_order_owned(client_order_id)?)
571 }
572}
573
574#[async_trait(?Send)]
575impl ExecutionClient for SandboxExecutionClient {
576 fn is_connected(&self) -> bool {
577 self.core.borrow().is_connected()
578 }
579
580 fn client_id(&self) -> ClientId {
581 self.core.borrow().client_id
582 }
583
584 fn account_id(&self) -> AccountId {
585 self.core.borrow().account_id
586 }
587
588 fn venue(&self) -> Venue {
589 self.core.borrow().venue
590 }
591
592 fn oms_type(&self) -> OmsType {
593 self.config.oms_type
594 }
595
596 fn settles_contract_expirations(&self) -> bool {
597 true
598 }
599
600 fn on_instrument(&mut self, instrument: InstrumentAny) {
601 let instrument_id = instrument.id();
602 let mut inner = self.inner.borrow_mut();
603 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id)
604 && let Err(e) = engine.update_instrument(instrument)
605 {
606 log::error!("Failed to update instrument {instrument_id} in sandbox engine: {e}");
607 }
608 }
609
610 fn get_account(&self) -> Option<AccountAny> {
611 let account_id = self.core.borrow().account_id;
612 self.cache.borrow().account_owned(&account_id)
613 }
614
615 fn reset(&mut self) -> anyhow::Result<()> {
616 Self::reset(self);
617 Ok(())
618 }
619
620 fn generate_account_state(
621 &self,
622 balances: Vec<AccountBalance>,
623 margins: Vec<MarginBalance>,
624 reported: bool,
625 ts_event: UnixNanos,
626 info: Option<Params>,
627 ) -> anyhow::Result<()> {
628 let ts_init = self.clock.borrow().timestamp_ns();
629 let state = self
630 .factory
631 .generate_account_state(balances, margins, reported, ts_event, ts_init, info);
632 let endpoint = MessagingSwitchboard::portfolio_update_account();
633 msgbus::send_account_state(endpoint, &state);
634 self.sync_cached_account_config()?;
635 Ok(())
636 }
637
638 fn start(&mut self) -> anyhow::Result<()> {
639 if self.core.borrow().is_started() {
640 return Ok(());
641 }
642
643 if let Some(sender) = try_get_exec_event_sender() {
644 let handler: Rc<dyn Fn(OrderEventAny)> = Rc::new(move |event: OrderEventAny| {
645 if let Err(e) = sender.send(ExecutionEvent::Order(event)) {
646 log::warn!("Failed to send order event: {e}");
647 }
648 });
649 let mut inner = self.inner.borrow_mut();
650 inner.event_handler = Some(handler.clone());
651 for engine in inner.matching_engines.values_mut() {
652 engine.set_event_handler(handler.clone());
653 }
654 }
655
656 self.inner.borrow_mut().load_open_orders();
657
658 self.register_message_handlers();
659 self.register_expiry_sweep_timer();
660
661 self.core.borrow().set_started();
662 let core = self.core.borrow();
663 log::info!(
664 "Sandbox execution client started: venue={}, account_id={}, oms_type={:?}, account_type={:?}",
665 self.config.venue,
666 core.account_id,
667 self.config.oms_type,
668 self.config.account_type,
669 );
670 Ok(())
671 }
672
673 fn stop(&mut self) -> anyhow::Result<()> {
674 if self.core.borrow().is_stopped() {
675 return Ok(());
676 }
677
678 self.deregister_message_handlers();
679 self.cancel_expiry_sweep_timer();
680 self.cancel_inbound_alert();
681
682 {
687 let mut inner = self.inner.borrow_mut();
688 let discarded = inner.take_inbound_queue();
689 inner.reject_discarded(discarded);
690 }
691
692 self.core.borrow().set_stopped();
693 self.core.borrow().set_disconnected();
694 log::info!(
695 "Sandbox execution client stopped: venue={}",
696 self.config.venue
697 );
698 Ok(())
699 }
700
701 async fn connect(&mut self) -> anyhow::Result<()> {
702 if self.core.borrow().is_connected() {
703 return Ok(());
704 }
705
706 let balances = self.get_account_balances();
707 let ts_event = self.clock.borrow().timestamp_ns();
708 self.generate_account_state(balances, vec![], false, ts_event, None)?;
709
710 self.core.borrow().set_connected();
711 log::info!(
712 "Sandbox execution client connected: venue={}",
713 self.config.venue
714 );
715 Ok(())
716 }
717
718 async fn disconnect(&mut self) -> anyhow::Result<()> {
719 if self.core.borrow().is_disconnected() {
720 return Ok(());
721 }
722
723 self.core.borrow().set_disconnected();
724 log::info!(
725 "Sandbox execution client disconnected: venue={}",
726 self.config.venue
727 );
728 Ok(())
729 }
730
731 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
732 let order = self.get_order(&cmd.client_order_id)?;
733
734 if order.is_closed() {
735 log::warn!("Cannot submit closed order {}", order.client_order_id());
736 return Ok(());
737 }
738
739 let ts_init = self.clock.borrow().timestamp_ns();
740 let event = self.factory.generate_order_submitted(&order, ts_init);
741 self.dispatch_order_event(event);
742
743 let mut inner = self.inner.borrow_mut();
744 if inner.config.latency_model.is_none() {
745 inner.apply_submit_order(&cmd)?;
746 } else {
747 inner.defer_or_apply(TradingCommand::SubmitOrder(cmd));
748 }
749 Ok(())
750 }
751
752 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
753 let ts_init = self.clock.borrow().timestamp_ns();
754
755 let orders: Vec<OrderAny> = self
756 .cache
757 .borrow()
758 .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
759
760 for order in &orders {
761 if order.is_closed() {
762 log::warn!("Cannot submit closed order {}", order.client_order_id());
763 continue;
764 }
765
766 let event = self.factory.generate_order_submitted(order, ts_init);
767 self.dispatch_order_event(event);
768 }
769
770 let mut inner = self.inner.borrow_mut();
771 if inner.config.latency_model.is_none() {
772 let _ = inner.apply_submit_order_list(&cmd);
773 } else {
774 inner.defer_or_apply(TradingCommand::SubmitOrderList(cmd));
775 }
776 Ok(())
777 }
778
779 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
780 let mut inner = self.inner.borrow_mut();
781 if inner.config.latency_model.is_none() {
782 inner.apply_modify_order(&cmd);
783 } else {
784 inner.defer_or_apply(TradingCommand::ModifyOrder(cmd));
785 }
786 Ok(())
787 }
788
789 fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
790 let mut inner = self.inner.borrow_mut();
791 if inner.config.latency_model.is_none() {
792 inner.apply_batch_modify_orders(&cmd);
793 } else {
794 inner.defer_or_apply(TradingCommand::ModifyOrders(cmd));
795 }
796 Ok(())
797 }
798
799 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
800 let mut inner = self.inner.borrow_mut();
801 if inner.config.latency_model.is_none() {
802 inner.apply_cancel_order(&cmd);
803 } else {
804 inner.defer_or_apply(TradingCommand::CancelOrder(cmd));
805 }
806 Ok(())
807 }
808
809 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
810 let mut inner = self.inner.borrow_mut();
811 if inner.config.latency_model.is_none() {
812 inner.apply_cancel_all_orders(&cmd);
813 } else {
814 inner.defer_or_apply(TradingCommand::CancelAllOrders(cmd));
815 }
816 Ok(())
817 }
818
819 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
820 let mut inner = self.inner.borrow_mut();
821 if inner.config.latency_model.is_none() {
822 inner.apply_batch_cancel_orders(&cmd);
823 } else {
824 inner.defer_or_apply(TradingCommand::CancelOrders(cmd));
825 }
826 Ok(())
827 }
828
829 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
830 let balances = self.get_current_account_balances();
831 let ts_event = self.clock.borrow().timestamp_ns();
832 self.generate_account_state(balances, vec![], false, ts_event, None)?;
833 Ok(())
834 }
835
836 fn query_order(&self, _cmd: QueryOrder) -> anyhow::Result<()> {
837 Ok(())
839 }
840
841 async fn generate_order_status_report(
842 &self,
843 _cmd: &GenerateOrderStatusReport,
844 ) -> anyhow::Result<Option<OrderStatusReport>> {
845 Ok(None)
847 }
848
849 async fn generate_order_status_reports(
850 &self,
851 _cmd: &GenerateOrderStatusReports,
852 ) -> anyhow::Result<Vec<OrderStatusReport>> {
853 Ok(Vec::new())
855 }
856
857 async fn generate_fill_reports(
858 &self,
859 _cmd: GenerateFillReports,
860 ) -> anyhow::Result<Vec<FillReport>> {
861 Ok(Vec::new())
863 }
864
865 async fn generate_position_status_reports(
866 &self,
867 _cmd: &GeneratePositionStatusReports,
868 ) -> anyhow::Result<Vec<PositionStatusReport>> {
869 Ok(Vec::new())
871 }
872
873 async fn generate_mass_status(
874 &self,
875 _lookback_mins: Option<u64>,
876 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
877 let core = self.core.borrow();
878 let ts_init = self.clock.borrow().timestamp_ns();
879 Ok(Some(ExecutionMassStatus::new(
880 core.client_id,
881 core.account_id,
882 core.venue,
883 ts_init,
884 None,
885 )))
886 }
887}
888
889struct SandboxInner {
891 clock: Rc<RefCell<dyn Clock>>,
892 cache: Rc<RefCell<Cache>>,
893 config: SandboxExecutionClientConfig,
894 fill_model: FillModelHandle,
895 fee_model: FeeModelHandle,
896 matching_engines: AHashMap<InstrumentId, OrderMatchingEngine>,
897 next_engine_raw_id: u32,
898 balances: AHashMap<String, Money>,
899 event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
902 inbound_queue: BinaryHeap<DelayedCommand>,
904 inflight_orders: InflightOrders,
905 inbound_seq: u64,
908 client_id: ClientId,
909 account_id: AccountId,
910 self_weak: WeakCell<Self>,
911}
912
913#[derive(Debug)]
916struct DelayedCommand {
917 due_ns: UnixNanos,
918 seq: u64,
919 command: TradingCommand,
920}
921
922impl Ord for DelayedCommand {
923 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
924 other
925 .due_ns
926 .cmp(&self.due_ns)
927 .then_with(|| other.seq.cmp(&self.seq))
928 }
929}
930
931impl PartialOrd for DelayedCommand {
932 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
933 Some(self.cmp(other))
934 }
935}
936
937impl PartialEq for DelayedCommand {
938 fn eq(&self, other: &Self) -> bool {
939 self.cmp(other) == std::cmp::Ordering::Equal
940 }
941}
942
943impl Eq for DelayedCommand {}
944
945impl SandboxInner {
946 fn load_open_orders(&mut self) {
947 let venue = self.config.venue;
948 let account_id = self.account_id;
949 let mut open_orders: Vec<(OrderAny, AccountId)> = {
950 let cache = self.cache.borrow();
951 cache
952 .orders_open(Some(&venue), None, None, Some(&account_id), None)
953 .into_iter()
954 .filter(|order| !order.is_emulated())
955 .filter_map(|order| {
956 order
957 .account_id()
958 .map(|account_id| (order.clone(), account_id))
959 })
960 .collect()
961 };
962
963 open_orders.sort_by(|(a, _), (b, _)| {
964 a.ts_init()
965 .cmp(&b.ts_init())
966 .then_with(|| a.client_order_id().cmp(&b.client_order_id()))
967 });
968
969 for (order, account_id) in open_orders {
970 let instrument_id = order.instrument_id();
971 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
972 match instrument {
973 Some(instrument) => {
974 self.ensure_matching_engine(&instrument);
975
976 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
977 engine.restore_open_order(&order, account_id);
978 }
979 }
980 None => {
981 log::warn!(
982 "No instrument for {instrument_id} to restore open order {}",
983 order.client_order_id()
984 );
985 }
986 }
987 }
988 }
989
990 fn ensure_matching_engine(&mut self, instrument: &InstrumentAny) {
991 let instrument_id = instrument.id();
992
993 if !self.matching_engines.contains_key(&instrument_id) {
994 let engine_config = self.config.to_matching_engine_config();
995 let fill_model = self.fill_model.clone();
996 let fee_model = self.fee_model.clone();
997 let raw_id = self.next_engine_raw_id;
998 self.next_engine_raw_id = self.next_engine_raw_id.wrapping_add(1);
999
1000 let mut engine = OrderMatchingEngine::new(
1001 instrument.clone(),
1002 raw_id,
1003 fill_model,
1004 fee_model,
1005 self.config.book_type,
1006 self.config.oms_type,
1007 self.config.account_type,
1008 self.clock.clone(),
1009 self.cache.clone(),
1010 engine_config,
1011 );
1012
1013 if let Some(handler) = &self.event_handler {
1014 engine.set_event_handler(handler.clone());
1015 }
1016
1017 engine.set_inflight_orders(self.inflight_orders.clone());
1018 self.matching_engines.insert(instrument_id, engine);
1019 }
1020 }
1021
1022 fn process_quote_tick(&mut self, quote: &QuoteTick) {
1023 let instrument_id = quote.instrument_id;
1024
1025 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1026 if let Some(instrument) = instrument {
1027 if !check_quote_or_drop("quote tick", quote, &instrument) {
1028 return;
1029 }
1030
1031 self.ensure_matching_engine(&instrument);
1032
1033 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1034 engine.process_quote_tick(quote);
1035 }
1036 }
1037 }
1038
1039 fn process_trade_tick(&mut self, trade: &TradeTick) {
1040 if !self.config.trade_execution {
1041 return;
1042 }
1043
1044 let instrument_id = trade.instrument_id;
1045
1046 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1047 if let Some(instrument) = instrument {
1048 if !check_trade_or_drop("trade tick", trade, &instrument) {
1049 return;
1050 }
1051
1052 self.ensure_matching_engine(&instrument);
1053
1054 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1055 engine.process_trade_tick(trade);
1056 }
1057 }
1058 }
1059
1060 fn process_bar(&mut self, bar: &Bar) {
1061 if !self.config.bar_execution {
1062 return;
1063 }
1064
1065 let instrument_id = bar.bar_type.instrument_id();
1066
1067 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1068 if let Some(instrument) = instrument {
1069 if !check_bar_or_drop("bar", bar, &instrument) {
1070 return;
1071 }
1072
1073 self.ensure_matching_engine(&instrument);
1074
1075 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1076 engine.process_bar(bar);
1077 }
1078 }
1079 }
1080
1081 fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) {
1082 let instrument_id = deltas.instrument_id;
1083
1084 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1085 if let Some(instrument) = instrument {
1086 self.ensure_matching_engine(&instrument);
1087
1088 if let Some(engine) = self.matching_engines.get_mut(&instrument_id)
1089 && let Err(e) = engine.process_order_book_deltas(deltas)
1090 {
1091 log::error!("Error processing order book deltas: {e}");
1092 }
1093 }
1094 }
1095
1096 fn process_instrument_status(&mut self, status: &InstrumentStatus) {
1097 let instrument_id = status.instrument_id;
1098
1099 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1100 engine.process_status(status.action);
1101 return;
1102 }
1103
1104 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1105 if let Some(instrument) = instrument {
1106 self.ensure_matching_engine(&instrument);
1107
1108 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1109 engine.process_status(status.action);
1110 }
1111 } else {
1112 log::warn!(
1113 "Ignoring instrument status for {instrument_id}: instrument missing from cache",
1114 );
1115 }
1116 }
1117
1118 fn process_instrument_close(&mut self, close: &InstrumentClose) {
1119 let instrument_id = close.instrument_id;
1120
1121 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1126 engine.process_instrument_close(*close);
1127 self.sync_expired_cleanup(instrument_id);
1128 } else {
1129 log::warn!(
1130 "Ignoring instrument close for {instrument_id}: no existing matching engine",
1131 );
1132 }
1133 }
1134
1135 fn is_expired_now(&self, instrument_id: InstrumentId) -> bool {
1136 let Some(engine) = self.matching_engines.get(&instrument_id) else {
1137 return false;
1138 };
1139
1140 let now_ns = self.clock.borrow().timestamp_ns();
1141 engine
1142 .instrument
1143 .expiration_ns()
1144 .is_some_and(|ns| now_ns >= ns)
1145 }
1146
1147 fn has_open_orders(&self, instrument_id: InstrumentId) -> bool {
1148 self.cache.borrow().has_orders_open(
1149 Some(&self.config.venue),
1150 Some(&instrument_id),
1151 None,
1152 None,
1153 None,
1154 )
1155 }
1156
1157 fn sync_expired_cleanup(&mut self, instrument_id: InstrumentId) {
1158 if !self.is_expired_now(instrument_id) {
1159 return;
1160 }
1161
1162 let has_open_positions = self.cache.borrow().has_positions_open(
1163 Some(&self.config.venue),
1164 Some(&instrument_id),
1165 None,
1166 None,
1167 None,
1168 );
1169
1170 if has_open_positions {
1171 return;
1172 }
1173
1174 self.matching_engines.remove(&instrument_id);
1175 self.cache
1176 .borrow_mut()
1177 .purge_instrument_skip_order_guard(instrument_id);
1178 }
1179
1180 fn sync_expired_cleanup_many(&mut self, instrument_ids: &[InstrumentId]) {
1181 for &instrument_id in instrument_ids {
1182 self.sync_expired_cleanup(instrument_id);
1183 }
1184 }
1185
1186 fn sweep_expired_engines(&mut self) {
1190 let expired_ids: Vec<InstrumentId> = self
1191 .matching_engines
1192 .keys()
1193 .copied()
1194 .filter(|instrument_id| {
1195 self.is_expired_now(*instrument_id) && !self.has_open_orders(*instrument_id)
1196 })
1197 .collect();
1198
1199 self.sync_expired_cleanup_many(&expired_ids);
1200 }
1201
1202 fn apply_trading_command(&mut self, cmd: &TradingCommand) -> anyhow::Result<()> {
1204 self.inflight_orders.remove(cmd);
1205
1206 if matches!(
1209 cmd,
1210 TradingCommand::ModifyOrder(_)
1211 | TradingCommand::ModifyOrders(_)
1212 | TradingCommand::CancelOrder(_)
1213 | TradingCommand::CancelOrders(_)
1214 ) && !self.ensure_engine_for(cmd.instrument_id())
1215 {
1216 self.reject_command(cmd, "No matching engine for instrument");
1217 return Ok(());
1218 }
1219
1220 match cmd {
1221 TradingCommand::SubmitOrder(cmd) => self.apply_submit_order(cmd)?,
1222 TradingCommand::SubmitOrderList(cmd) => {
1223 for order in self.apply_submit_order_list(cmd) {
1225 self.reject_submit_leg(cmd, &order, "No instrument for order");
1226 }
1227 }
1228 TradingCommand::ModifyOrder(cmd) => self.apply_modify_order(cmd),
1229 TradingCommand::ModifyOrders(cmd) => self.apply_batch_modify_orders(cmd),
1230 TradingCommand::CancelOrder(cmd) => self.apply_cancel_order(cmd),
1231 TradingCommand::CancelOrders(cmd) => self.apply_batch_cancel_orders(cmd),
1232 TradingCommand::CancelAllOrders(cmd) => self.apply_cancel_all_orders(cmd),
1233 TradingCommand::QueryOrder(_) | TradingCommand::QueryAccount(_) => {}
1234 }
1235 Ok(())
1236 }
1237
1238 fn reject_command(&self, command: &TradingCommand, reason: &str) {
1239 self.reject_command_deduped(command, reason, &mut AHashSet::new());
1240 }
1241
1242 fn reject_command_deduped(
1245 &self,
1246 command: &TradingCommand,
1247 reason: &str,
1248 pending_rejected: &mut AHashSet<ClientOrderId>,
1249 ) {
1250 let ts_now = self.clock.borrow().timestamp_ns();
1251 let account_id = self.account_id;
1252 let reason = Ustr::from(reason);
1253
1254 let reject_submit = |trader_id, strategy_id, instrument_id, client_order_id| {
1255 self.dispatch_order_event(OrderEventAny::Rejected(OrderRejected::new(
1256 trader_id,
1257 strategy_id,
1258 instrument_id,
1259 client_order_id,
1260 account_id,
1261 reason,
1262 UUID4::new(),
1263 ts_now,
1264 ts_now,
1265 false,
1266 false,
1267 )));
1268 };
1269
1270 match command {
1271 TradingCommand::SubmitOrder(cmd) => reject_submit(
1272 cmd.trader_id,
1273 cmd.strategy_id,
1274 cmd.instrument_id,
1275 cmd.client_order_id,
1276 ),
1277 TradingCommand::SubmitOrderList(cmd) => {
1278 let in_flight: Vec<(ClientOrderId, InstrumentId)> = self
1280 .cache
1281 .borrow()
1282 .orders_for_ids(&cmd.order_list.client_order_ids, cmd)
1283 .iter()
1284 .filter(|order| !order.is_closed())
1285 .map(|order| (order.client_order_id(), order.instrument_id()))
1286 .collect();
1287
1288 for (client_order_id, instrument_id) in in_flight {
1289 reject_submit(
1290 cmd.trader_id,
1291 cmd.strategy_id,
1292 instrument_id,
1293 client_order_id,
1294 );
1295 }
1296 }
1297 TradingCommand::ModifyOrder(cmd) => {
1298 if self.needs_rejection(cmd.client_order_id, pending_rejected) {
1299 self.reject_modify(cmd, reason, ts_now);
1300 }
1301 }
1302 TradingCommand::ModifyOrders(cmd) => {
1303 for modify in &cmd.modifies {
1304 if self.needs_rejection(modify.client_order_id, pending_rejected) {
1305 self.reject_modify(modify, reason, ts_now);
1306 }
1307 }
1308 }
1309 TradingCommand::CancelOrder(cmd) => {
1310 if self.needs_rejection(cmd.client_order_id, pending_rejected) {
1311 self.reject_cancel(
1312 cmd.trader_id,
1313 cmd.strategy_id,
1314 cmd.instrument_id,
1315 cmd.client_order_id,
1316 cmd.venue_order_id,
1317 reason,
1318 ts_now,
1319 );
1320 }
1321 }
1322 TradingCommand::CancelOrders(cmd) => {
1323 for cancel in &cmd.cancels {
1324 if self.needs_rejection(cancel.client_order_id, pending_rejected) {
1325 self.reject_cancel(
1326 cancel.trader_id,
1327 cancel.strategy_id,
1328 cancel.instrument_id,
1329 cancel.client_order_id,
1330 cancel.venue_order_id,
1331 reason,
1332 ts_now,
1333 );
1334 }
1335 }
1336 }
1337 TradingCommand::CancelAllOrders(_) => {}
1340 TradingCommand::QueryOrder(_) | TradingCommand::QueryAccount(_) => {}
1341 }
1342 }
1343
1344 fn needs_rejection(
1351 &self,
1352 client_order_id: ClientOrderId,
1353 pending_rejected: &mut AHashSet<ClientOrderId>,
1354 ) -> bool {
1355 let is_closed = self
1356 .cache
1357 .borrow()
1358 .order(&client_order_id)
1359 .is_some_and(|order| order.is_closed());
1360
1361 !is_closed && pending_rejected.insert(client_order_id)
1362 }
1363
1364 fn reject_modify(&self, cmd: &ModifyOrder, reason: Ustr, ts_now: UnixNanos) {
1365 self.dispatch_order_event(OrderEventAny::ModifyRejected(OrderModifyRejected::new(
1366 cmd.trader_id,
1367 cmd.strategy_id,
1368 cmd.instrument_id,
1369 cmd.client_order_id,
1370 reason,
1371 UUID4::new(),
1372 ts_now,
1373 ts_now,
1374 false,
1375 cmd.venue_order_id,
1376 Some(self.account_id),
1377 )));
1378 }
1379
1380 #[expect(clippy::too_many_arguments, reason = "mirrors the event's own fields")]
1381 fn reject_cancel(
1382 &self,
1383 trader_id: TraderId,
1384 strategy_id: StrategyId,
1385 instrument_id: InstrumentId,
1386 client_order_id: ClientOrderId,
1387 venue_order_id: Option<VenueOrderId>,
1388 reason: Ustr,
1389 ts_now: UnixNanos,
1390 ) {
1391 self.dispatch_order_event(OrderEventAny::CancelRejected(OrderCancelRejected::new(
1392 trader_id,
1393 strategy_id,
1394 instrument_id,
1395 client_order_id,
1396 reason,
1397 UUID4::new(),
1398 ts_now,
1399 ts_now,
1400 false,
1401 venue_order_id,
1402 Some(self.account_id),
1403 )));
1404 }
1405
1406 fn dispatch_order_event(&self, event: OrderEventAny) {
1407 if let Some(handler) = &self.event_handler {
1408 handler(event);
1409 } else {
1410 msgbus::send_order_event(MessagingSwitchboard::exec_engine_process(), event);
1411 }
1412 }
1413
1414 fn ensure_engine_for(&mut self, instrument_id: InstrumentId) -> bool {
1417 if !self.matching_engines.contains_key(&instrument_id) {
1418 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1419 let Some(instrument) = instrument else {
1420 log::warn!(
1421 "Cannot process command for {instrument_id}: instrument missing from cache",
1422 );
1423 return false;
1424 };
1425 self.ensure_matching_engine(&instrument);
1426 }
1427
1428 self.matching_engines.contains_key(&instrument_id)
1429 }
1430
1431 fn apply_submit_order(&mut self, cmd: &SubmitOrder) -> anyhow::Result<()> {
1432 let mut order = self.cache.borrow().try_order_owned(&cmd.client_order_id)?;
1433
1434 let instrument_id = order.instrument_id();
1435 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
1436
1437 self.ensure_matching_engine(&instrument);
1438
1439 self.seed_cached_market_data(instrument_id);
1440
1441 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1442 engine.process_order(&mut order, self.account_id);
1443 self.sync_expired_cleanup(instrument_id);
1444 }
1445
1446 Ok(())
1447 }
1448
1449 fn apply_submit_order_list(&mut self, cmd: &SubmitOrderList) -> Vec<OrderAny> {
1453 let orders: Vec<OrderAny> = self
1454 .cache
1455 .borrow()
1456 .orders_for_ids(&cmd.order_list.client_order_ids, cmd);
1457
1458 let mut cleanup_instrument_ids = Vec::new();
1459 let mut unresolved: Vec<OrderAny> = Vec::new();
1460
1461 for order in &orders {
1462 if order.is_closed() {
1463 continue;
1464 }
1465
1466 let instrument_id = order.instrument_id();
1467 if !cleanup_instrument_ids.contains(&instrument_id) {
1468 cleanup_instrument_ids.push(instrument_id);
1469 }
1470 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1471
1472 let Some(instrument) = instrument else {
1473 unresolved.push(order.clone());
1476 continue;
1477 };
1478
1479 self.ensure_matching_engine(&instrument);
1480
1481 self.seed_cached_market_data(instrument_id);
1482
1483 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1484 let mut order_clone = order.clone();
1485 engine.process_order(&mut order_clone, self.account_id);
1486 }
1487 }
1488
1489 if !cleanup_instrument_ids.is_empty() {
1490 self.sync_expired_cleanup_many(&cleanup_instrument_ids);
1491 }
1492
1493 unresolved
1494 }
1495
1496 fn reject_submit_leg(&self, cmd: &SubmitOrderList, order: &OrderAny, reason: &str) {
1498 let ts_now = self.clock.borrow().timestamp_ns();
1499 self.dispatch_order_event(OrderEventAny::Rejected(OrderRejected::new(
1500 cmd.trader_id,
1501 cmd.strategy_id,
1502 order.instrument_id(),
1503 order.client_order_id(),
1504 self.account_id,
1505 Ustr::from(reason),
1506 UUID4::new(),
1507 ts_now,
1508 ts_now,
1509 false,
1510 false,
1511 )));
1512 }
1513
1514 fn seed_cached_market_data(&mut self, instrument_id: InstrumentId) {
1515 let Some(engine) = self.matching_engines.get_mut(&instrument_id) else {
1516 return;
1517 };
1518
1519 if self.config.liquidity_consumption
1521 && (engine.get_book().update_count != 0 || engine.get_core().last.is_some())
1522 {
1523 return;
1524 }
1525
1526 let (quote, trade) = {
1527 let cache = self.cache.borrow();
1528 (
1529 cache.quote(&instrument_id).copied(),
1530 cache.trade(&instrument_id).copied(),
1531 )
1532 };
1533
1534 if let Some(quote) = quote
1535 && check_quote_or_drop("cached quote tick", "e, &engine.instrument)
1536 {
1537 engine.process_quote_tick("e);
1538 }
1539
1540 if self.config.trade_execution
1541 && let Some(trade) = trade
1542 && check_trade_or_drop("cached trade tick", &trade, &engine.instrument)
1543 {
1544 engine.process_trade_tick(&trade);
1545 }
1546 }
1547
1548 fn apply_modify_order(&mut self, cmd: &ModifyOrder) {
1549 let account_id = self.account_id;
1550 if let Some(engine) = self.matching_engines.get_mut(&cmd.instrument_id) {
1551 engine.process_modify(cmd, account_id);
1552 }
1553 }
1554
1555 fn apply_batch_modify_orders(&mut self, cmd: &BatchModifyOrders) {
1556 let account_id = self.account_id;
1557 if let Some(engine) = self.matching_engines.get_mut(&cmd.instrument_id) {
1558 engine.process_batch_modify(cmd, account_id);
1559 }
1560 }
1561
1562 fn apply_cancel_order(&mut self, cmd: &CancelOrder) {
1563 let account_id = self.account_id;
1564 if let Some(engine) = self.matching_engines.get_mut(&cmd.instrument_id) {
1565 engine.process_cancel(cmd, account_id);
1566 }
1567 }
1568
1569 fn apply_cancel_all_orders(&mut self, cmd: &CancelAllOrders) {
1570 let instrument_id = cmd.instrument_id;
1571 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
1572 engine.process_cancel_all(cmd, self.account_id);
1573 } else {
1574 log::debug!("No open orders to cancel for {instrument_id}: no matching engine");
1575 }
1576 }
1577
1578 fn apply_batch_cancel_orders(&mut self, cmd: &BatchCancelOrders) {
1579 let account_id = self.account_id;
1580 if let Some(engine) = self.matching_engines.get_mut(&cmd.instrument_id) {
1581 engine.process_batch_cancel(cmd, account_id);
1582 }
1583 }
1584
1585 fn enqueue(&mut self, command: TradingCommand, now_ns: UnixNanos) {
1588 let leg_latency = self.command_leg_latency(&command);
1589 let due_ns = now_ns + leg_latency;
1590
1591 let seq = self.inbound_seq;
1592 self.inbound_seq += 1;
1593
1594 self.inflight_orders.insert(&command);
1595 self.inbound_queue.push(DelayedCommand {
1596 due_ns,
1597 seq,
1598 command,
1599 });
1600
1601 self.arm_inbound_alert(now_ns);
1602 }
1603
1604 fn defer_or_apply(&mut self, command: TradingCommand) {
1609 let now_ns = self.clock.borrow().timestamp_ns();
1610 let head_is_due = self
1611 .inbound_queue
1612 .peek()
1613 .is_some_and(|delayed| delayed.due_ns <= now_ns);
1614
1615 if head_is_due || self.command_leg_latency(&command) > DurationNanos::ZERO {
1616 self.enqueue(command, now_ns);
1617 return;
1618 }
1619
1620 if let Err(e) = self.apply_trading_command(&command) {
1621 log::error!("Error applying command: {e}");
1622 self.reject_command(&command, "Command could not be applied at the venue");
1623 }
1624 }
1625
1626 fn command_leg_latency(&self, command: &TradingCommand) -> DurationNanos {
1628 let Some(latency_model) = self.config.latency_model.as_ref() else {
1629 return DurationNanos::ZERO;
1630 };
1631
1632 match command {
1633 TradingCommand::SubmitOrder(_) | TradingCommand::SubmitOrderList(_) => {
1634 latency_model.get_insert_latency()
1635 }
1636 TradingCommand::ModifyOrder(_) | TradingCommand::ModifyOrders(_) => {
1637 latency_model.get_update_latency()
1638 }
1639 TradingCommand::CancelOrder(_)
1640 | TradingCommand::CancelOrders(_)
1641 | TradingCommand::CancelAllOrders(_) => latency_model.get_delete_latency(),
1642 TradingCommand::QueryOrder(_) | TradingCommand::QueryAccount(_) => DurationNanos::ZERO,
1643 }
1644 }
1645
1646 fn pop_due(&mut self, now_ns: UnixNanos) -> Option<DelayedCommand> {
1647 self.inbound_queue
1648 .peek()
1649 .is_some_and(|delayed| delayed.due_ns <= now_ns)
1650 .then(|| self.inbound_queue.pop().expect("peek returned Some"))
1651 }
1652
1653 fn on_quote_tick(inner: &Rc<RefCell<Self>>, quote: &QuoteTick) {
1654 Self::drain_inbound(inner);
1655 inner.borrow_mut().process_quote_tick(quote);
1656 }
1657
1658 fn on_trade_tick(inner: &Rc<RefCell<Self>>, trade: &TradeTick) {
1659 Self::drain_inbound(inner);
1660 inner.borrow_mut().process_trade_tick(trade);
1661 }
1662
1663 fn on_bar(inner: &Rc<RefCell<Self>>, bar: &Bar) {
1664 Self::drain_inbound(inner);
1665 inner.borrow_mut().process_bar(bar);
1666 }
1667
1668 fn on_order_book_deltas(inner: &Rc<RefCell<Self>>, deltas: &OrderBookDeltas) {
1669 Self::drain_inbound(inner);
1670 inner.borrow_mut().process_order_book_deltas(deltas);
1671 }
1672
1673 fn on_instrument_status(inner: &Rc<RefCell<Self>>, status: &InstrumentStatus) {
1674 Self::drain_inbound(inner);
1675 inner.borrow_mut().process_instrument_status(status);
1676 }
1677
1678 fn on_instrument_close(inner: &Rc<RefCell<Self>>, close: &InstrumentClose) {
1679 Self::drain_inbound(inner);
1680 inner.borrow_mut().process_instrument_close(close);
1681 }
1682
1683 fn drain_inbound(inner: &Rc<RefCell<Self>>) {
1690 let Ok(mut this) = inner.try_borrow_mut() else {
1693 log::debug!("Skipping sandbox inbound drain due to active borrow");
1694 return;
1695 };
1696
1697 if this.config.latency_model.is_none() {
1698 return;
1699 }
1700
1701 let now_ns = this.clock.borrow().timestamp_ns();
1702
1703 while let Some(delayed) = this.pop_due(now_ns) {
1704 if let Err(e) = this.apply_trading_command(&delayed.command) {
1705 log::error!("Error applying deferred command: {e}");
1706 this.reject_command(
1707 &delayed.command,
1708 "Command could not be applied at the venue",
1709 );
1710 }
1711 }
1712
1713 this.arm_inbound_alert(now_ns);
1714 }
1715
1716 fn take_inbound_queue(&mut self) -> Vec<DelayedCommand> {
1719 self.inflight_orders.clear();
1720
1721 if self.inbound_queue.is_empty() {
1722 return Vec::new();
1723 }
1724
1725 log::warn!(
1726 "Discarding {} command(s) still in flight at stop",
1727 self.inbound_queue.len(),
1728 );
1729
1730 let mut discarded = std::mem::take(&mut self.inbound_queue).into_vec();
1733 discarded.sort_unstable_by_key(|delayed| std::cmp::Reverse(delayed.seq));
1734 discarded
1735 }
1736
1737 fn reject_discarded(&self, discarded: Vec<DelayedCommand>) {
1739 let mut pending_rejected = AHashSet::new();
1742
1743 for delayed in discarded {
1744 self.reject_command_deduped(
1745 &delayed.command,
1746 "Client stopped before the command was sent",
1747 &mut pending_rejected,
1748 );
1749 }
1750 }
1751
1752 fn clear_inbound_queue(&mut self) {
1755 self.inbound_queue.clear();
1756 self.inflight_orders.clear();
1757 }
1758
1759 fn arm_inbound_alert(&self, now_ns: UnixNanos) {
1766 let Some(earliest_due) = self.inbound_queue.peek().map(|delayed| delayed.due_ns) else {
1767 return;
1768 };
1769
1770 if earliest_due <= now_ns {
1771 return;
1772 }
1773
1774 let name = inbound_alert_name(self.client_id);
1775 let armed_ns = self.clock.borrow().next_time_ns(&name);
1776
1777 match armed_ns {
1778 Some(armed_ns) if armed_ns <= earliest_due => return,
1780 Some(_) => self.clock.borrow_mut().cancel_timer(&name),
1782 None => {}
1783 }
1784
1785 let inner_weak = self.self_weak.clone();
1786 let alert_name = name.clone();
1787
1788 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event: TimeEvent| {
1789 let Some(inner_rc) = inner_weak.upgrade() else {
1790 return;
1791 };
1792
1793 if let Ok(this) = inner_rc.try_borrow() {
1795 this.clock.borrow_mut().cancel_timer(&alert_name);
1796 }
1797
1798 Self::drain_inbound(&inner_rc);
1800 });
1801
1802 if let Err(e) = self.clock.borrow_mut().set_time_alert_ns(
1803 &name,
1804 earliest_due,
1805 Some(TimeEventCallback::from(callback)),
1806 Some(true),
1807 ) {
1808 log::error!("Failed to arm sandbox inbound alert '{name}': {e}");
1809 }
1810 }
1811}
1812
1813fn inbound_alert_name(client_id: ClientId) -> String {
1814 format!("{client_id}-sandbox-inbound-alert")
1815}
1816
1817fn check_quote_or_drop(context: &str, quote: &QuoteTick, instrument: &InstrumentAny) -> bool {
1818 if quote_matches_instrument_precision(quote, instrument) {
1819 return true;
1820 }
1821
1822 log::warn!(
1823 "Dropping {context} for {} due to precision mismatch \
1824 (bid_px={}, ask_px={}, bid_sz={}, ask_sz={}, expected_price={}, expected_size={})",
1825 instrument.id(),
1826 quote.bid_price.precision,
1827 quote.ask_price.precision,
1828 quote.bid_size.precision,
1829 quote.ask_size.precision,
1830 instrument.price_precision(),
1831 instrument.size_precision(),
1832 );
1833 false
1834}
1835
1836fn check_trade_or_drop(context: &str, trade: &TradeTick, instrument: &InstrumentAny) -> bool {
1837 if trade_matches_instrument_precision(trade, instrument) {
1838 return true;
1839 }
1840
1841 log::warn!(
1842 "Dropping {context} for {} due to precision mismatch \
1843 (px={}, sz={}, expected_price={}, expected_size={})",
1844 instrument.id(),
1845 trade.price.precision,
1846 trade.size.precision,
1847 instrument.price_precision(),
1848 instrument.size_precision(),
1849 );
1850 false
1851}
1852
1853fn check_bar_or_drop(context: &str, bar: &Bar, instrument: &InstrumentAny) -> bool {
1854 if bar_matches_instrument_precision(bar, instrument) {
1855 return true;
1856 }
1857
1858 log::warn!(
1859 "Dropping {context} for {} due to precision mismatch \
1860 (open={}, high={}, low={}, close={}, volume={}, expected_price={}, expected_size={})",
1861 instrument.id(),
1862 bar.open.precision,
1863 bar.high.precision,
1864 bar.low.precision,
1865 bar.close.precision,
1866 bar.volume.precision,
1867 instrument.price_precision(),
1868 instrument.size_precision(),
1869 );
1870 false
1871}
1872
1873fn quote_matches_instrument_precision(quote: &QuoteTick, instrument: &InstrumentAny) -> bool {
1874 let price_precision = instrument.price_precision();
1875 let size_precision = instrument.size_precision();
1876
1877 quote.bid_price.precision == price_precision
1878 && quote.ask_price.precision == price_precision
1879 && quote.bid_size.precision == size_precision
1880 && quote.ask_size.precision == size_precision
1881}
1882
1883fn trade_matches_instrument_precision(trade: &TradeTick, instrument: &InstrumentAny) -> bool {
1884 let price_precision = instrument.price_precision();
1885 let size_precision = instrument.size_precision();
1886
1887 trade.price.precision == price_precision && trade.size.precision == size_precision
1888}
1889
1890fn bar_matches_instrument_precision(bar: &Bar, instrument: &InstrumentAny) -> bool {
1891 let price_precision = instrument.price_precision();
1892 let size_precision = instrument.size_precision();
1893
1894 bar.open.precision == price_precision
1895 && bar.high.precision == price_precision
1896 && bar.low.precision == price_precision
1897 && bar.close.precision == price_precision
1898 && bar.volume.precision == size_precision
1899}