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