1use std::{cell::RefCell, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
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,
35 },
36 },
37 msgbus::{
38 self, MStr, MessagingSwitchboard, Pattern, TypedHandler,
39 typed_handler::ShareableMessageHandler,
40 },
41 timer::{TimeEvent, TimeEventCallback},
42};
43use nautilus_core::{Params, UnixNanos, WeakCell, datetime::NANOSECONDS_IN_SECOND};
44use nautilus_execution::{
45 client::core::ExecutionClientCore,
46 matching_engine::OrderMatchingEngine,
47 models::{fee::FeeModelHandle, fill::FillModelHandle},
48};
49use nautilus_model::{
50 accounts::AccountAny,
51 data::{Bar, InstrumentClose, InstrumentStatus, OrderBookDeltas, QuoteTick, TradeTick},
52 enums::OmsType,
53 events::{OrderEventAny, PositionEvent},
54 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, Venue},
55 instruments::{Instrument, InstrumentAny},
56 orders::{Order, OrderAny},
57 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
58 types::{AccountBalance, MarginBalance, Money},
59};
60
61use crate::config::SandboxExecutionClientConfig;
62
63const EXPIRED_ENGINE_SWEEP_INTERVAL_NS: u64 = 60 * NANOSECONDS_IN_SECOND;
68
69struct SandboxInner {
73 clock: Rc<RefCell<dyn Clock>>,
75 cache: Rc<RefCell<Cache>>,
77 config: SandboxExecutionClientConfig,
79 fill_model: FillModelHandle,
81 matching_engines: AHashMap<InstrumentId, OrderMatchingEngine>,
83 next_engine_raw_id: u32,
85 balances: AHashMap<String, Money>,
87 event_handler: Option<Rc<dyn Fn(OrderEventAny)>>,
88}
89
90fn check_quote_or_drop(context: &str, quote: &QuoteTick, instrument: &InstrumentAny) -> bool {
91 if quote_matches_instrument_precision(quote, instrument) {
92 return true;
93 }
94
95 log::warn!(
96 "Dropping {context} for {} due to precision mismatch \
97 (bid_px={}, ask_px={}, bid_sz={}, ask_sz={}, expected_price={}, expected_size={})",
98 instrument.id(),
99 quote.bid_price.precision,
100 quote.ask_price.precision,
101 quote.bid_size.precision,
102 quote.ask_size.precision,
103 instrument.price_precision(),
104 instrument.size_precision(),
105 );
106 false
107}
108
109fn check_trade_or_drop(context: &str, trade: &TradeTick, instrument: &InstrumentAny) -> bool {
110 if trade_matches_instrument_precision(trade, instrument) {
111 return true;
112 }
113
114 log::warn!(
115 "Dropping {context} for {} due to precision mismatch \
116 (px={}, sz={}, expected_price={}, expected_size={})",
117 instrument.id(),
118 trade.price.precision,
119 trade.size.precision,
120 instrument.price_precision(),
121 instrument.size_precision(),
122 );
123 false
124}
125
126fn check_bar_or_drop(context: &str, bar: &Bar, instrument: &InstrumentAny) -> bool {
127 if bar_matches_instrument_precision(bar, instrument) {
128 return true;
129 }
130
131 log::warn!(
132 "Dropping {context} for {} due to precision mismatch \
133 (open={}, high={}, low={}, close={}, volume={}, expected_price={}, expected_size={})",
134 instrument.id(),
135 bar.open.precision,
136 bar.high.precision,
137 bar.low.precision,
138 bar.close.precision,
139 bar.volume.precision,
140 instrument.price_precision(),
141 instrument.size_precision(),
142 );
143 false
144}
145
146fn quote_matches_instrument_precision(quote: &QuoteTick, instrument: &InstrumentAny) -> bool {
147 let price_precision = instrument.price_precision();
148 let size_precision = instrument.size_precision();
149
150 quote.bid_price.precision == price_precision
151 && quote.ask_price.precision == price_precision
152 && quote.bid_size.precision == size_precision
153 && quote.ask_size.precision == size_precision
154}
155
156fn trade_matches_instrument_precision(trade: &TradeTick, instrument: &InstrumentAny) -> bool {
157 let price_precision = instrument.price_precision();
158 let size_precision = instrument.size_precision();
159
160 trade.price.precision == price_precision && trade.size.precision == size_precision
161}
162
163fn bar_matches_instrument_precision(bar: &Bar, instrument: &InstrumentAny) -> bool {
164 let price_precision = instrument.price_precision();
165 let size_precision = instrument.size_precision();
166
167 bar.open.precision == price_precision
168 && bar.high.precision == price_precision
169 && bar.low.precision == price_precision
170 && bar.close.precision == price_precision
171 && bar.volume.precision == size_precision
172}
173
174impl SandboxInner {
175 fn ensure_matching_engine(&mut self, instrument: &InstrumentAny) {
177 let instrument_id = instrument.id();
178
179 if !self.matching_engines.contains_key(&instrument_id) {
180 let engine_config = self.config.to_matching_engine_config();
181 let fill_model = self.fill_model.clone();
182 let fee_model = self
183 .config
184 .fee_model
185 .clone()
186 .map(FeeModelHandle::from)
187 .unwrap_or_default();
188 let raw_id = self.next_engine_raw_id;
189 self.next_engine_raw_id = self.next_engine_raw_id.wrapping_add(1);
190
191 let mut engine = OrderMatchingEngine::new(
192 instrument.clone(),
193 raw_id,
194 fill_model,
195 fee_model,
196 self.config.book_type,
197 self.config.oms_type,
198 self.config.account_type,
199 self.clock.clone(),
200 self.cache.clone(),
201 engine_config,
202 );
203
204 if let Some(handler) = &self.event_handler {
205 engine.set_event_handler(handler.clone());
206 }
207
208 self.matching_engines.insert(instrument_id, engine);
209 }
210 }
211
212 fn process_quote_tick(&mut self, quote: &QuoteTick) {
214 let instrument_id = quote.instrument_id;
215
216 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
218 if let Some(instrument) = instrument {
219 if !check_quote_or_drop("quote tick", quote, &instrument) {
220 return;
221 }
222
223 self.ensure_matching_engine(&instrument);
224
225 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
226 engine.process_quote_tick(quote);
227 }
228 }
229 }
230
231 fn process_trade_tick(&mut self, trade: &TradeTick) {
233 if !self.config.trade_execution {
234 return;
235 }
236
237 let instrument_id = trade.instrument_id;
238
239 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
240 if let Some(instrument) = instrument {
241 if !check_trade_or_drop("trade tick", trade, &instrument) {
242 return;
243 }
244
245 self.ensure_matching_engine(&instrument);
246
247 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
248 engine.process_trade_tick(trade);
249 }
250 }
251 }
252
253 fn process_bar(&mut self, bar: &Bar) {
255 if !self.config.bar_execution {
256 return;
257 }
258
259 let instrument_id = bar.bar_type.instrument_id();
260
261 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
262 if let Some(instrument) = instrument {
263 if !check_bar_or_drop("bar", bar, &instrument) {
264 return;
265 }
266
267 self.ensure_matching_engine(&instrument);
268
269 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
270 engine.process_bar(bar);
271 }
272 }
273 }
274
275 fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) {
276 let instrument_id = deltas.instrument_id;
277
278 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
279 if let Some(instrument) = instrument {
280 self.ensure_matching_engine(&instrument);
281
282 if let Some(engine) = self.matching_engines.get_mut(&instrument_id)
283 && let Err(e) = engine.process_order_book_deltas(deltas)
284 {
285 log::error!("Error processing order book deltas: {e}");
286 }
287 }
288 }
289
290 fn process_instrument_status(&mut self, status: &InstrumentStatus) {
291 let instrument_id = status.instrument_id;
292
293 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
294 engine.process_status(status.action);
295 return;
296 }
297
298 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
299 if let Some(instrument) = instrument {
300 self.ensure_matching_engine(&instrument);
301
302 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
303 engine.process_status(status.action);
304 }
305 } else {
306 log::warn!(
307 "Ignoring instrument status for {instrument_id}: instrument missing from cache",
308 );
309 }
310 }
311
312 fn process_instrument_close(&mut self, close: &InstrumentClose) {
313 let instrument_id = close.instrument_id;
314
315 if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
320 engine.process_instrument_close(*close);
321 self.sync_expired_cleanup(instrument_id);
322 } else {
323 log::warn!(
324 "Ignoring instrument close for {instrument_id}: no existing matching engine",
325 );
326 }
327 }
328
329 fn is_expired_now(&self, instrument_id: InstrumentId) -> bool {
330 let Some(engine) = self.matching_engines.get(&instrument_id) else {
331 return false;
332 };
333
334 let now_ns = self.clock.borrow().timestamp_ns();
335 engine
336 .instrument
337 .expiration_ns()
338 .is_some_and(|ns| now_ns >= ns)
339 }
340
341 fn has_open_orders(&self, instrument_id: InstrumentId) -> bool {
342 self.cache.borrow().has_orders_open(
343 Some(&self.config.venue),
344 Some(&instrument_id),
345 None,
346 None,
347 None,
348 )
349 }
350
351 fn sync_expired_cleanup(&mut self, instrument_id: InstrumentId) {
352 if !self.is_expired_now(instrument_id) {
353 return;
354 }
355
356 let has_open_positions = self.cache.borrow().has_positions_open(
357 Some(&self.config.venue),
358 Some(&instrument_id),
359 None,
360 None,
361 None,
362 );
363
364 if has_open_positions {
365 return;
366 }
367
368 self.matching_engines.remove(&instrument_id);
369 self.cache
370 .borrow_mut()
371 .purge_instrument_skip_order_guard(instrument_id);
372 }
373
374 fn sync_expired_cleanup_many(&mut self, instrument_ids: &[InstrumentId]) {
375 for &instrument_id in instrument_ids {
376 self.sync_expired_cleanup(instrument_id);
377 }
378 }
379
380 fn sweep_expired_engines(&mut self) {
392 let expired_ids: Vec<InstrumentId> = self
393 .matching_engines
394 .keys()
395 .copied()
396 .filter(|instrument_id| {
397 self.is_expired_now(*instrument_id) && !self.has_open_orders(*instrument_id)
398 })
399 .collect();
400
401 self.sync_expired_cleanup_many(&expired_ids);
402 }
403}
404
405struct RegisteredHandlers {
407 deltas_pattern: MStr<Pattern>,
408 deltas_handler: TypedHandler<OrderBookDeltas>,
409 quote_pattern: MStr<Pattern>,
410 quote_handler: TypedHandler<QuoteTick>,
411 trade_pattern: MStr<Pattern>,
412 trade_handler: TypedHandler<TradeTick>,
413 bar_pattern: MStr<Pattern>,
414 bar_handler: TypedHandler<Bar>,
415 status_pattern: MStr<Pattern>,
416 status_handler: ShareableMessageHandler,
417 close_pattern: MStr<Pattern>,
418 close_handler: ShareableMessageHandler,
419 position_pattern: MStr<Pattern>,
420 position_handler: TypedHandler<PositionEvent>,
421}
422
423pub struct SandboxExecutionClient {
429 core: RefCell<ExecutionClientCore>,
431 factory: OrderEventFactory,
433 config: SandboxExecutionClientConfig,
435 inner: Rc<RefCell<SandboxInner>>,
437 handlers: RefCell<Option<RegisteredHandlers>>,
439 clock: Rc<RefCell<dyn Clock>>,
441 cache: Rc<RefCell<Cache>>,
443}
444
445impl Debug for SandboxExecutionClient {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 f.debug_struct(stringify!(SandboxExecutionClient))
448 .field("venue", &self.config.venue)
449 .field("account_id", &self.core.borrow().account_id)
450 .field("connected", &self.core.borrow().is_connected())
451 .field(
452 "matching_engines",
453 &self.inner.borrow().matching_engines.len(),
454 )
455 .finish()
456 }
457}
458
459impl SandboxExecutionClient {
460 #[must_use]
462 pub fn new(
463 core: ExecutionClientCore,
464 config: SandboxExecutionClientConfig,
465 clock: Rc<RefCell<dyn Clock>>,
466 cache: Rc<RefCell<Cache>>,
467 ) -> Self {
468 let mut balances = AHashMap::new();
469 for money in &config.starting_balances {
470 balances.insert(money.currency.code.to_string(), *money);
471 }
472
473 let fill_model = config
474 .fill_model
475 .clone()
476 .map(FillModelHandle::from)
477 .unwrap_or_default();
478 let inner = Rc::new(RefCell::new(SandboxInner {
479 clock: clock.clone(),
480 cache: cache.clone(),
481 config: config.clone(),
482 fill_model,
483 matching_engines: AHashMap::new(),
484 next_engine_raw_id: 0,
485 balances,
486 event_handler: None,
487 }));
488
489 let factory = OrderEventFactory::new(
490 core.trader_id,
491 core.account_id,
492 core.account_type,
493 core.base_currency,
494 );
495
496 Self {
497 core: RefCell::new(core),
498 factory,
499 config,
500 inner,
501 handlers: RefCell::new(None),
502 clock,
503 cache,
504 }
505 }
506
507 #[must_use]
509 pub const fn config(&self) -> &SandboxExecutionClientConfig {
510 &self.config
511 }
512
513 #[must_use]
515 pub fn matching_engine_count(&self) -> usize {
516 self.inner.borrow().matching_engines.len()
517 }
518
519 fn dispatch_order_event(&self, event: OrderEventAny) {
520 if let Some(handler) = &self.inner.borrow().event_handler {
521 handler(event);
522 } else {
523 let endpoint = MessagingSwitchboard::exec_engine_process();
524 msgbus::send_order_event(endpoint, event);
525 }
526 }
527
528 fn register_message_handlers(&self) {
533 if self.handlers.borrow().is_some() {
534 log::warn!("Sandbox message handlers already registered");
535 return;
536 }
537
538 let inner_weak = WeakCell::from(Rc::downgrade(&self.inner));
539 let venue = self.config.venue;
540 let account_id = self.core.borrow().account_id;
541
542 let deltas_handler = {
544 let inner = inner_weak.clone();
545 TypedHandler::from(move |deltas: &OrderBookDeltas| {
546 if deltas.instrument_id.venue == venue
547 && let Some(inner_rc) = inner.upgrade()
548 {
549 inner_rc.borrow_mut().process_order_book_deltas(deltas);
550 }
551 })
552 };
553
554 let quote_handler = {
556 let inner = inner_weak.clone();
557 TypedHandler::from(move |quote: &QuoteTick| {
558 if quote.instrument_id.venue == venue
559 && let Some(inner_rc) = inner.upgrade()
560 {
561 inner_rc.borrow_mut().process_quote_tick(quote);
562 }
563 })
564 };
565
566 let trade_handler = {
568 let inner = inner_weak.clone();
569 TypedHandler::from(move |trade: &TradeTick| {
570 if trade.instrument_id.venue == venue
571 && let Some(inner_rc) = inner.upgrade()
572 {
573 inner_rc.borrow_mut().process_trade_tick(trade);
574 }
575 })
576 };
577
578 let bar_handler = {
580 let inner = inner_weak.clone();
581 TypedHandler::from(move |bar: &Bar| {
582 if bar.bar_type.instrument_id().venue == venue
583 && let Some(inner_rc) = inner.upgrade()
584 {
585 inner_rc.borrow_mut().process_bar(bar);
586 }
587 })
588 };
589
590 let status_handler = {
591 let inner = inner_weak.clone();
592 ShareableMessageHandler::from_typed(move |status: &InstrumentStatus| {
593 if status.instrument_id.venue == venue
594 && let Some(inner_rc) = inner.upgrade()
595 {
596 inner_rc.borrow_mut().process_instrument_status(status);
597 }
598 })
599 };
600
601 let close_handler = {
602 let inner = inner_weak.clone();
603 ShareableMessageHandler::from_typed(move |close: &InstrumentClose| {
604 if close.instrument_id.venue == venue
605 && let Some(inner_rc) = inner.upgrade()
606 {
607 inner_rc.borrow_mut().process_instrument_close(close);
608 }
609 })
610 };
611
612 let position_handler = {
613 TypedHandler::from(move |event: &PositionEvent| {
614 let PositionEvent::PositionClosed(position_closed) = event else {
615 return;
616 };
617
618 if position_closed.instrument_id.venue == venue
619 && position_closed.account_id == account_id
620 && let Some(inner_rc) = inner_weak.upgrade()
621 {
622 if let Ok(mut inner) = inner_rc.try_borrow_mut() {
625 inner.sync_expired_cleanup(position_closed.instrument_id);
626 } else {
627 log::debug!(
628 "Skipping immediate expired cleanup retry for {} due to active sandbox borrow",
629 position_closed.instrument_id,
630 );
631 }
632 }
633 })
634 };
635
636 let deltas_pattern: MStr<Pattern> = format!("data.book.deltas.{venue}.*").into();
638 let quote_pattern: MStr<Pattern> = format!("data.quotes.{venue}.*").into();
639 let trade_pattern: MStr<Pattern> = format!("data.trades.{venue}.*").into();
640 let bar_pattern: MStr<Pattern> = "data.bars.*".into();
641 let status_pattern: MStr<Pattern> = format!("data.status.{venue}.*").into();
642 let close_pattern: MStr<Pattern> = format!("data.close.{venue}.*").into();
643 let position_pattern: MStr<Pattern> = "events.position.*".into();
644
645 msgbus::subscribe_book_deltas(deltas_pattern, deltas_handler.clone(), Some(10));
646 msgbus::subscribe_quotes(quote_pattern, quote_handler.clone(), Some(10));
647 msgbus::subscribe_trades(trade_pattern, trade_handler.clone(), Some(10));
648 msgbus::subscribe_bars(bar_pattern, bar_handler.clone(), Some(10));
649 msgbus::subscribe_any(status_pattern, status_handler.clone(), Some(10));
650 msgbus::subscribe_instrument_close(close_pattern, close_handler.clone(), Some(10));
651 msgbus::subscribe_position_events(position_pattern, position_handler.clone(), Some(10));
652
653 *self.handlers.borrow_mut() = Some(RegisteredHandlers {
655 deltas_pattern,
656 deltas_handler,
657 quote_pattern,
658 quote_handler,
659 trade_pattern,
660 trade_handler,
661 bar_pattern,
662 bar_handler,
663 status_pattern,
664 status_handler,
665 close_pattern,
666 close_handler,
667 position_pattern,
668 position_handler,
669 });
670
671 log::debug!(
672 "Sandbox registered message handlers for venue={}",
673 self.config.venue
674 );
675 }
676
677 fn deregister_message_handlers(&self) {
679 if let Some(handlers) = self.handlers.borrow_mut().take() {
680 msgbus::unsubscribe_book_deltas(handlers.deltas_pattern, &handlers.deltas_handler);
681 msgbus::unsubscribe_quotes(handlers.quote_pattern, &handlers.quote_handler);
682 msgbus::unsubscribe_trades(handlers.trade_pattern, &handlers.trade_handler);
683 msgbus::unsubscribe_bars(handlers.bar_pattern, &handlers.bar_handler);
684 msgbus::unsubscribe_any(handlers.status_pattern, &handlers.status_handler);
685 msgbus::unsubscribe_instrument_close(handlers.close_pattern, &handlers.close_handler);
686 msgbus::unsubscribe_position_events(
687 handlers.position_pattern,
688 &handlers.position_handler,
689 );
690
691 log::debug!(
692 "Sandbox deregistered message handlers for venue={}",
693 self.config.venue
694 );
695 }
696 }
697
698 fn expiry_sweep_timer_name(&self) -> String {
699 format!("{}-sandbox-expiry-sweep", self.core.borrow().client_id)
700 }
701
702 fn register_expiry_sweep_timer(&self) {
704 let inner_weak = WeakCell::from(Rc::downgrade(&self.inner));
705 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event: TimeEvent| {
706 let Some(inner_rc) = inner_weak.upgrade() else {
707 return;
708 };
709
710 if let Ok(mut inner) = inner_rc.try_borrow_mut() {
713 inner.sweep_expired_engines();
714 } else {
715 log::debug!("Skipping sandbox expiry sweep due to active borrow");
716 }
717 });
718
719 let name = self.expiry_sweep_timer_name();
720
721 if let Err(e) = self.clock.borrow_mut().set_timer_ns(
722 &name,
723 EXPIRED_ENGINE_SWEEP_INTERVAL_NS,
724 None,
725 None,
726 Some(TimeEventCallback::from(callback)),
727 None,
728 None,
729 ) {
730 log::error!("Failed to register sandbox expiry sweep timer: {e}");
731 }
732 }
733
734 fn cancel_expiry_sweep_timer(&self) {
736 self.clock
737 .borrow_mut()
738 .cancel_timer(&self.expiry_sweep_timer_name());
739 }
740
741 fn get_current_account_balances(&self) -> Vec<AccountBalance> {
743 let account_id = self.core.borrow().account_id;
744 let cache = self.cache.borrow();
745
746 if let Some(account) = cache.account(&account_id) {
748 return account.balances().into_values().collect();
749 }
750
751 self.get_account_balances()
753 }
754
755 fn sync_cached_account_config(&self) -> anyhow::Result<()> {
756 let Some(mut account) = self.get_account() else {
757 return Ok(());
758 };
759
760 account.set_calculate_account_state(!self.config.frozen_account);
761
762 if let AccountAny::Margin(margin_account) = &mut account {
763 margin_account.set_default_leverage(self.config.default_leverage);
764 for (instrument_id, leverage) in &self.config.leverages {
765 margin_account.set_leverage(*instrument_id, *leverage);
766 }
767 }
768
769 self.cache.borrow_mut().update_account(&account)
770 }
771
772 pub fn process_quote_tick(&self, quote: &QuoteTick) -> anyhow::Result<()> {
778 let instrument_id = quote.instrument_id;
779 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
780
781 if !check_quote_or_drop("quote tick", quote, &instrument) {
782 return Ok(());
783 }
784
785 let mut inner = self.inner.borrow_mut();
786 inner.ensure_matching_engine(&instrument);
787 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
788 engine.process_quote_tick(quote);
789 }
790 Ok(())
791 }
792
793 pub fn process_trade_tick(&self, trade: &TradeTick) -> anyhow::Result<()> {
799 if !self.config.trade_execution {
800 return Ok(());
801 }
802
803 let instrument_id = trade.instrument_id;
804 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
805
806 if !check_trade_or_drop("trade tick", trade, &instrument) {
807 return Ok(());
808 }
809
810 let mut inner = self.inner.borrow_mut();
811 inner.ensure_matching_engine(&instrument);
812 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
813 engine.process_trade_tick(trade);
814 }
815 Ok(())
816 }
817
818 pub fn process_bar(&self, bar: &Bar) -> anyhow::Result<()> {
824 if !self.config.bar_execution {
825 return Ok(());
826 }
827
828 let instrument_id = bar.bar_type.instrument_id();
829 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
830
831 if !check_bar_or_drop("bar", bar, &instrument) {
832 return Ok(());
833 }
834
835 let mut inner = self.inner.borrow_mut();
836 inner.ensure_matching_engine(&instrument);
837 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
838 engine.process_bar(bar);
839 }
840 Ok(())
841 }
842
843 pub fn process_order_book_deltas(&self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
849 let instrument_id = deltas.instrument_id;
850 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
851
852 let mut inner = self.inner.borrow_mut();
853 inner.ensure_matching_engine(&instrument);
854 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
855 engine.process_order_book_deltas(deltas)?;
856 }
857 Ok(())
858 }
859
860 pub fn reset(&self) {
862 let mut inner = self.inner.borrow_mut();
863 for engine in inner.matching_engines.values_mut() {
864 engine.reset();
865 }
866
867 inner.balances.clear();
868 for money in &self.config.starting_balances {
869 inner
870 .balances
871 .insert(money.currency.code.to_string(), *money);
872 }
873
874 log::info!(
875 "Sandbox execution client reset: venue={}",
876 self.config.venue
877 );
878 }
879
880 fn get_account_balances(&self) -> Vec<AccountBalance> {
882 self.inner
883 .borrow()
884 .balances
885 .values()
886 .map(|money| AccountBalance::new(*money, Money::zero(money.currency), *money))
887 .collect()
888 }
889
890 fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
891 Ok(self.cache.borrow().try_order_owned(client_order_id)?)
892 }
893}
894
895#[async_trait(?Send)]
896impl ExecutionClient for SandboxExecutionClient {
897 fn is_connected(&self) -> bool {
898 self.core.borrow().is_connected()
899 }
900
901 fn client_id(&self) -> ClientId {
902 self.core.borrow().client_id
903 }
904
905 fn account_id(&self) -> AccountId {
906 self.core.borrow().account_id
907 }
908
909 fn venue(&self) -> Venue {
910 self.core.borrow().venue
911 }
912
913 fn oms_type(&self) -> OmsType {
914 self.config.oms_type
915 }
916
917 fn on_instrument(&mut self, instrument: InstrumentAny) {
918 let instrument_id = instrument.id();
919 let mut inner = self.inner.borrow_mut();
920 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id)
921 && let Err(e) = engine.update_instrument(instrument)
922 {
923 log::error!("Failed to update instrument {instrument_id} in sandbox engine: {e}");
924 }
925 }
926
927 fn get_account(&self) -> Option<AccountAny> {
928 let account_id = self.core.borrow().account_id;
929 self.cache.borrow().account_owned(&account_id)
930 }
931
932 fn generate_account_state(
933 &self,
934 balances: Vec<AccountBalance>,
935 margins: Vec<MarginBalance>,
936 reported: bool,
937 ts_event: UnixNanos,
938 info: Option<Params>,
939 ) -> anyhow::Result<()> {
940 let ts_init = self.clock.borrow().timestamp_ns();
941 let state = self
942 .factory
943 .generate_account_state(balances, margins, reported, ts_event, ts_init, info);
944 let endpoint = MessagingSwitchboard::portfolio_update_account();
945 msgbus::send_account_state(endpoint, &state);
946 self.sync_cached_account_config()?;
947 Ok(())
948 }
949
950 fn start(&mut self) -> anyhow::Result<()> {
951 if self.core.borrow().is_started() {
952 return Ok(());
953 }
954
955 if let Some(sender) = try_get_exec_event_sender() {
956 let handler: Rc<dyn Fn(OrderEventAny)> = Rc::new(move |event: OrderEventAny| {
957 if let Err(e) = sender.send(ExecutionEvent::Order(event)) {
958 log::warn!("Failed to send order event: {e}");
959 }
960 });
961 let mut inner = self.inner.borrow_mut();
962 inner.event_handler = Some(handler.clone());
963 for engine in inner.matching_engines.values_mut() {
964 engine.set_event_handler(handler.clone());
965 }
966 }
967
968 self.register_message_handlers();
970 self.register_expiry_sweep_timer();
971
972 self.core.borrow().set_started();
973 let core = self.core.borrow();
974 log::info!(
975 "Sandbox execution client started: venue={}, account_id={}, oms_type={:?}, account_type={:?}",
976 self.config.venue,
977 core.account_id,
978 self.config.oms_type,
979 self.config.account_type,
980 );
981 Ok(())
982 }
983
984 fn stop(&mut self) -> anyhow::Result<()> {
985 if self.core.borrow().is_stopped() {
986 return Ok(());
987 }
988
989 self.deregister_message_handlers();
991 self.cancel_expiry_sweep_timer();
992
993 self.core.borrow().set_stopped();
994 self.core.borrow().set_disconnected();
995 log::info!(
996 "Sandbox execution client stopped: venue={}",
997 self.config.venue
998 );
999 Ok(())
1000 }
1001
1002 async fn connect(&mut self) -> anyhow::Result<()> {
1003 if self.core.borrow().is_connected() {
1004 return Ok(());
1005 }
1006
1007 let balances = self.get_account_balances();
1008 let ts_event = self.clock.borrow().timestamp_ns();
1009 self.generate_account_state(balances, vec![], false, ts_event, None)?;
1010
1011 self.core.borrow().set_connected();
1012 log::info!(
1013 "Sandbox execution client connected: venue={}",
1014 self.config.venue
1015 );
1016 Ok(())
1017 }
1018
1019 async fn disconnect(&mut self) -> anyhow::Result<()> {
1020 if self.core.borrow().is_disconnected() {
1021 return Ok(());
1022 }
1023
1024 self.core.borrow().set_disconnected();
1025 log::info!(
1026 "Sandbox execution client disconnected: venue={}",
1027 self.config.venue
1028 );
1029 Ok(())
1030 }
1031
1032 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
1033 let mut order = self.get_order(&cmd.client_order_id)?;
1034
1035 if order.is_closed() {
1036 log::warn!("Cannot submit closed order {}", order.client_order_id());
1037 return Ok(());
1038 }
1039
1040 let ts_init = self.clock.borrow().timestamp_ns();
1041 let event = self.factory.generate_order_submitted(&order, ts_init);
1042 self.dispatch_order_event(event);
1043
1044 let instrument_id = order.instrument_id();
1045 let instrument = self.cache.borrow().try_instrument(&instrument_id)?.clone();
1046
1047 let mut inner = self.inner.borrow_mut();
1048 inner.ensure_matching_engine(&instrument);
1049
1050 let cache = self.cache.borrow();
1052
1053 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1054 if let Some(quote) = cache.quote(&instrument_id)
1055 && check_quote_or_drop("cached quote tick", quote, &instrument)
1056 {
1057 engine.process_quote_tick(quote);
1058 }
1059
1060 if self.config.trade_execution
1061 && let Some(trade) = cache.trade(&instrument_id)
1062 && check_trade_or_drop("cached trade tick", trade, &instrument)
1063 {
1064 engine.process_trade_tick(trade);
1065 }
1066 }
1067 drop(cache);
1068
1069 let account_id = self.core.borrow().account_id;
1070
1071 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1072 engine.process_order(&mut order, account_id);
1073 inner.sync_expired_cleanup(instrument_id);
1074 }
1075
1076 Ok(())
1077 }
1078
1079 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1080 let ts_init = self.clock.borrow().timestamp_ns();
1081 let mut cleanup_instrument_ids = Vec::new();
1082
1083 let orders: Vec<OrderAny> = self
1084 .cache
1085 .borrow()
1086 .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
1087
1088 for order in &orders {
1089 if order.is_closed() {
1090 log::warn!("Cannot submit closed order {}", order.client_order_id());
1091 continue;
1092 }
1093
1094 let event = self.factory.generate_order_submitted(order, ts_init);
1095 self.dispatch_order_event(event);
1096 }
1097
1098 let account_id = self.core.borrow().account_id;
1099
1100 for order in &orders {
1101 if order.is_closed() {
1102 continue;
1103 }
1104
1105 let instrument_id = order.instrument_id();
1106 if !cleanup_instrument_ids.contains(&instrument_id) {
1107 cleanup_instrument_ids.push(instrument_id);
1108 }
1109 let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
1110
1111 if let Some(instrument) = instrument {
1112 let mut inner = self.inner.borrow_mut();
1113 inner.ensure_matching_engine(&instrument);
1114
1115 let cache = self.cache.borrow();
1117
1118 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1119 if let Some(quote) = cache.quote(&instrument_id)
1120 && check_quote_or_drop("cached quote tick", quote, &instrument)
1121 {
1122 engine.process_quote_tick(quote);
1123 }
1124
1125 if self.config.trade_execution
1126 && let Some(trade) = cache.trade(&instrument_id)
1127 && check_trade_or_drop("cached trade tick", trade, &instrument)
1128 {
1129 engine.process_trade_tick(trade);
1130 }
1131 }
1132 drop(cache);
1133
1134 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1135 let mut order_clone = order.clone();
1136 engine.process_order(&mut order_clone, account_id);
1137 }
1138 }
1139 }
1140
1141 if !cleanup_instrument_ids.is_empty() {
1142 self.inner
1143 .borrow_mut()
1144 .sync_expired_cleanup_many(&cleanup_instrument_ids);
1145 }
1146
1147 Ok(())
1148 }
1149
1150 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1151 let instrument_id = cmd.instrument_id;
1152 let account_id = self.core.borrow().account_id;
1153
1154 let mut inner = self.inner.borrow_mut();
1155 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1156 engine.process_modify(&cmd, account_id);
1157 }
1158 Ok(())
1159 }
1160
1161 fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
1162 let instrument_id = cmd.instrument_id;
1163 let account_id = self.core.borrow().account_id;
1164
1165 let mut inner = self.inner.borrow_mut();
1166 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1167 engine.process_batch_modify(&cmd, account_id);
1168 }
1169 Ok(())
1170 }
1171
1172 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1173 let instrument_id = cmd.instrument_id;
1174 let account_id = self.core.borrow().account_id;
1175
1176 let mut inner = self.inner.borrow_mut();
1177 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1178 engine.process_cancel(&cmd, account_id);
1179 }
1180 Ok(())
1181 }
1182
1183 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1184 let instrument_id = cmd.instrument_id;
1185 let account_id = self.core.borrow().account_id;
1186
1187 let mut inner = self.inner.borrow_mut();
1188 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1189 engine.process_cancel_all(&cmd, account_id);
1190 }
1191 Ok(())
1192 }
1193
1194 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1195 let instrument_id = cmd.instrument_id;
1196 let account_id = self.core.borrow().account_id;
1197
1198 let mut inner = self.inner.borrow_mut();
1199 if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
1200 engine.process_batch_cancel(&cmd, account_id);
1201 }
1202 Ok(())
1203 }
1204
1205 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
1206 let balances = self.get_current_account_balances();
1207 let ts_event = self.clock.borrow().timestamp_ns();
1208 self.generate_account_state(balances, vec![], false, ts_event, None)?;
1209 Ok(())
1210 }
1211
1212 fn query_order(&self, _cmd: QueryOrder) -> anyhow::Result<()> {
1213 Ok(())
1215 }
1216
1217 async fn generate_order_status_report(
1218 &self,
1219 _cmd: &GenerateOrderStatusReport,
1220 ) -> anyhow::Result<Option<OrderStatusReport>> {
1221 Ok(None)
1223 }
1224
1225 async fn generate_order_status_reports(
1226 &self,
1227 _cmd: &GenerateOrderStatusReports,
1228 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1229 Ok(Vec::new())
1231 }
1232
1233 async fn generate_fill_reports(
1234 &self,
1235 _cmd: GenerateFillReports,
1236 ) -> anyhow::Result<Vec<FillReport>> {
1237 Ok(Vec::new())
1239 }
1240
1241 async fn generate_position_status_reports(
1242 &self,
1243 _cmd: &GeneratePositionStatusReports,
1244 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1245 Ok(Vec::new())
1247 }
1248
1249 async fn generate_mass_status(
1250 &self,
1251 _lookback_mins: Option<u64>,
1252 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1253 let core = self.core.borrow();
1254 let ts_init = self.clock.borrow().timestamp_ns();
1255 Ok(Some(ExecutionMassStatus::new(
1256 core.client_id,
1257 core.account_id,
1258 core.venue,
1259 ts_init,
1260 None,
1261 )))
1262 }
1263}