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