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