Skip to main content

nautilus_sandbox/
execution.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Sandbox execution client implementation.
17
18use 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    messages::execution::{
28        BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
29        GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
30        ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
31    },
32    msgbus::{self, MStr, MessagingSwitchboard, Pattern, TypedHandler},
33};
34use nautilus_core::{UnixNanos, WeakCell};
35use nautilus_execution::{
36    client::core::ExecutionClientCore,
37    matching_engine::adapter::OrderEngineAdapter,
38    models::{
39        fee::{FeeModelAny, MakerTakerFeeModel},
40        fill::FillModelAny,
41    },
42};
43use nautilus_model::{
44    accounts::AccountAny,
45    data::{Bar, OrderBookDeltas, QuoteTick, TradeTick},
46    enums::OmsType,
47    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, Venue},
48    instruments::{Instrument, InstrumentAny},
49    orders::{Order, OrderAny},
50    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
51    types::{AccountBalance, MarginBalance, Money},
52};
53
54use crate::config::SandboxExecutionClientConfig;
55
56/// Inner state for the sandbox execution client.
57///
58/// This is wrapped in `Rc<RefCell<>>` so message handlers can hold weak references.
59struct SandboxInner {
60    /// Dynamic clock for matching engines.
61    clock: Rc<RefCell<dyn Clock>>,
62    /// Reference to the cache.
63    cache: Rc<RefCell<Cache>>,
64    /// The sandbox configuration.
65    config: SandboxExecutionClientConfig,
66    /// Matching engines per instrument.
67    matching_engines: AHashMap<InstrumentId, OrderEngineAdapter>,
68    /// Next raw ID assigned to a matching engine.
69    next_engine_raw_id: u32,
70    /// Current account balances.
71    balances: AHashMap<String, Money>,
72}
73
74impl SandboxInner {
75    /// Ensures a matching engine exists for the given instrument.
76    fn ensure_matching_engine(&mut self, instrument: &InstrumentAny) {
77        let instrument_id = instrument.id();
78
79        if !self.matching_engines.contains_key(&instrument_id) {
80            let engine_config = self.config.to_matching_engine_config();
81            let fill_model = FillModelAny::default();
82            let fee_model = FeeModelAny::MakerTaker(MakerTakerFeeModel);
83            let raw_id = self.next_engine_raw_id;
84            self.next_engine_raw_id = self.next_engine_raw_id.wrapping_add(1);
85
86            let engine = OrderEngineAdapter::new(
87                instrument.clone(),
88                raw_id,
89                fill_model,
90                fee_model,
91                self.config.book_type,
92                self.config.oms_type,
93                self.config.account_type,
94                self.clock.clone(),
95                self.cache.clone(),
96                engine_config,
97            );
98
99            self.matching_engines.insert(instrument_id, engine);
100        }
101    }
102
103    /// Processes a quote tick through the matching engine.
104    fn process_quote_tick(&mut self, quote: &QuoteTick) {
105        let instrument_id = quote.instrument_id;
106
107        // Try to get instrument from cache, create engine if found
108        let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
109        if let Some(instrument) = instrument {
110            self.ensure_matching_engine(&instrument);
111
112            if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
113                engine.get_engine_mut().process_quote_tick(quote);
114            }
115        }
116    }
117
118    /// Processes a trade tick through the matching engine.
119    fn process_trade_tick(&mut self, trade: &TradeTick) {
120        if !self.config.trade_execution {
121            return;
122        }
123
124        let instrument_id = trade.instrument_id;
125
126        let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
127        if let Some(instrument) = instrument {
128            self.ensure_matching_engine(&instrument);
129
130            if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
131                engine.get_engine_mut().process_trade_tick(trade);
132            }
133        }
134    }
135
136    /// Processes a bar through the matching engine.
137    fn process_bar(&mut self, bar: &Bar) {
138        if !self.config.bar_execution {
139            return;
140        }
141
142        let instrument_id = bar.bar_type.instrument_id();
143
144        let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
145        if let Some(instrument) = instrument {
146            self.ensure_matching_engine(&instrument);
147
148            if let Some(engine) = self.matching_engines.get_mut(&instrument_id) {
149                engine.get_engine_mut().process_bar(bar);
150            }
151        }
152    }
153
154    fn process_order_book_deltas(&mut self, deltas: &OrderBookDeltas) {
155        let instrument_id = deltas.instrument_id;
156
157        let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
158        if let Some(instrument) = instrument {
159            self.ensure_matching_engine(&instrument);
160
161            if let Some(engine) = self.matching_engines.get_mut(&instrument_id)
162                && let Err(e) = engine.get_engine_mut().process_order_book_deltas(deltas)
163            {
164                log::error!("Error processing order book deltas: {e}");
165            }
166        }
167    }
168}
169
170/// Registered message handlers for later deregistration.
171struct RegisteredHandlers {
172    deltas_pattern: MStr<Pattern>,
173    deltas_handler: TypedHandler<OrderBookDeltas>,
174    quote_pattern: MStr<Pattern>,
175    quote_handler: TypedHandler<QuoteTick>,
176    trade_pattern: MStr<Pattern>,
177    trade_handler: TypedHandler<TradeTick>,
178    bar_pattern: MStr<Pattern>,
179    bar_handler: TypedHandler<Bar>,
180}
181
182/// A sandbox execution client for paper trading against live market data.
183///
184/// The `SandboxExecutionClient` simulates order execution using the `OrderMatchingEngine`
185/// to match orders against market data. This enables strategy testing in real-time
186/// without actual order execution on exchanges.
187pub struct SandboxExecutionClient {
188    /// The core execution client functionality.
189    core: RefCell<ExecutionClientCore>,
190    /// Factory for generating order events.
191    factory: OrderEventFactory,
192    /// The sandbox configuration.
193    config: SandboxExecutionClientConfig,
194    /// Inner state wrapped for handler access.
195    inner: Rc<RefCell<SandboxInner>>,
196    /// Registered message handlers for cleanup.
197    handlers: RefCell<Option<RegisteredHandlers>>,
198    /// Reference to the clock.
199    clock: Rc<RefCell<dyn Clock>>,
200    /// Reference to the cache.
201    cache: Rc<RefCell<Cache>>,
202}
203
204impl Debug for SandboxExecutionClient {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct(stringify!(SandboxExecutionClient))
207            .field("venue", &self.config.venue)
208            .field("account_id", &self.core.borrow().account_id)
209            .field("connected", &self.core.borrow().is_connected())
210            .field(
211                "matching_engines",
212                &self.inner.borrow().matching_engines.len(),
213            )
214            .finish()
215    }
216}
217
218impl SandboxExecutionClient {
219    /// Creates a new [`SandboxExecutionClient`] instance.
220    #[must_use]
221    pub fn new(
222        core: ExecutionClientCore,
223        config: SandboxExecutionClientConfig,
224        clock: Rc<RefCell<dyn Clock>>,
225        cache: Rc<RefCell<Cache>>,
226    ) -> Self {
227        let mut balances = AHashMap::new();
228        for money in &config.starting_balances {
229            balances.insert(money.currency.code.to_string(), *money);
230        }
231
232        let inner = Rc::new(RefCell::new(SandboxInner {
233            clock: clock.clone(),
234            cache: cache.clone(),
235            config: config.clone(),
236            matching_engines: AHashMap::new(),
237            next_engine_raw_id: 0,
238            balances,
239        }));
240
241        let factory = OrderEventFactory::new(
242            core.trader_id,
243            core.account_id,
244            core.account_type,
245            core.base_currency,
246        );
247
248        Self {
249            core: RefCell::new(core),
250            factory,
251            config,
252            inner,
253            handlers: RefCell::new(None),
254            clock,
255            cache,
256        }
257    }
258
259    /// Returns a reference to the configuration.
260    #[must_use]
261    pub const fn config(&self) -> &SandboxExecutionClientConfig {
262        &self.config
263    }
264
265    /// Returns the number of active matching engines.
266    #[must_use]
267    pub fn matching_engine_count(&self) -> usize {
268        self.inner.borrow().matching_engines.len()
269    }
270
271    /// Registers message handlers for market data subscriptions.
272    ///
273    /// This subscribes to order book deltas, quotes, trades, and bars for the
274    /// configured venue, routing all received data to the matching engines.
275    fn register_message_handlers(&self) {
276        if self.handlers.borrow().is_some() {
277            log::warn!("Sandbox message handlers already registered");
278            return;
279        }
280
281        let inner_weak = WeakCell::from(Rc::downgrade(&self.inner));
282        let venue = self.config.venue;
283
284        // Order book deltas handler
285        let deltas_handler = {
286            let inner = inner_weak.clone();
287            TypedHandler::from(move |deltas: &OrderBookDeltas| {
288                if deltas.instrument_id.venue == venue
289                    && let Some(inner_rc) = inner.upgrade()
290                {
291                    inner_rc.borrow_mut().process_order_book_deltas(deltas);
292                }
293            })
294        };
295
296        // Quote tick handler
297        let quote_handler = {
298            let inner = inner_weak.clone();
299            TypedHandler::from(move |quote: &QuoteTick| {
300                if quote.instrument_id.venue == venue
301                    && let Some(inner_rc) = inner.upgrade()
302                {
303                    inner_rc.borrow_mut().process_quote_tick(quote);
304                }
305            })
306        };
307
308        // Trade tick handler
309        let trade_handler = {
310            let inner = inner_weak.clone();
311            TypedHandler::from(move |trade: &TradeTick| {
312                if trade.instrument_id.venue == venue
313                    && let Some(inner_rc) = inner.upgrade()
314                {
315                    inner_rc.borrow_mut().process_trade_tick(trade);
316                }
317            })
318        };
319
320        // Bar handler (topic is data.bars.{bar_type}, filter by venue in handler)
321        let bar_handler = {
322            let inner = inner_weak;
323            TypedHandler::from(move |bar: &Bar| {
324                if bar.bar_type.instrument_id().venue == venue
325                    && let Some(inner_rc) = inner.upgrade()
326                {
327                    inner_rc.borrow_mut().process_bar(bar);
328                }
329            })
330        };
331
332        // Subscribe patterns
333        let deltas_pattern: MStr<Pattern> = format!("data.book.deltas.{venue}.*").into();
334        let quote_pattern: MStr<Pattern> = format!("data.quotes.{venue}.*").into();
335        let trade_pattern: MStr<Pattern> = format!("data.trades.{venue}.*").into();
336        let bar_pattern: MStr<Pattern> = "data.bars.*".into();
337
338        msgbus::subscribe_book_deltas(deltas_pattern, deltas_handler.clone(), Some(10));
339        msgbus::subscribe_quotes(quote_pattern, quote_handler.clone(), Some(10));
340        msgbus::subscribe_trades(trade_pattern, trade_handler.clone(), Some(10));
341        msgbus::subscribe_bars(bar_pattern, bar_handler.clone(), Some(10));
342
343        // Store handlers for later deregistration
344        *self.handlers.borrow_mut() = Some(RegisteredHandlers {
345            deltas_pattern,
346            deltas_handler,
347            quote_pattern,
348            quote_handler,
349            trade_pattern,
350            trade_handler,
351            bar_pattern,
352            bar_handler,
353        });
354
355        log::info!(
356            "Sandbox registered message handlers for venue={}",
357            self.config.venue
358        );
359    }
360
361    /// Deregisters message handlers to stop receiving market data.
362    fn deregister_message_handlers(&self) {
363        if let Some(handlers) = self.handlers.borrow_mut().take() {
364            msgbus::unsubscribe_book_deltas(handlers.deltas_pattern, &handlers.deltas_handler);
365            msgbus::unsubscribe_quotes(handlers.quote_pattern, &handlers.quote_handler);
366            msgbus::unsubscribe_trades(handlers.trade_pattern, &handlers.trade_handler);
367            msgbus::unsubscribe_bars(handlers.bar_pattern, &handlers.bar_handler);
368
369            log::info!(
370                "Sandbox deregistered message handlers for venue={}",
371                self.config.venue
372            );
373        }
374    }
375
376    /// Returns current account balances, preferring cache state over starting balances.
377    fn get_current_account_balances(&self) -> Vec<AccountBalance> {
378        let account_id = self.core.borrow().account_id;
379        let cache = self.cache.borrow();
380
381        // Use account from cache if available (updated by fill events)
382        if let Some(account) = cache.account(&account_id) {
383            return account.balances().into_values().collect();
384        }
385
386        // Fall back to starting balances
387        self.get_account_balances()
388    }
389
390    /// Processes a quote tick through the matching engine.
391    ///
392    /// # Errors
393    ///
394    /// Returns an error if the instrument is not found in the cache.
395    pub fn process_quote_tick(&self, quote: &QuoteTick) -> anyhow::Result<()> {
396        let instrument_id = quote.instrument_id;
397        let instrument = self
398            .cache
399            .borrow()
400            .instrument(&instrument_id)
401            .cloned()
402            .ok_or_else(|| anyhow::anyhow!("Instrument not found: {instrument_id}"))?;
403
404        let mut inner = self.inner.borrow_mut();
405        inner.ensure_matching_engine(&instrument);
406        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
407            engine.get_engine_mut().process_quote_tick(quote);
408        }
409        Ok(())
410    }
411
412    /// Processes a trade tick through the matching engine.
413    ///
414    /// # Errors
415    ///
416    /// Returns an error if the instrument is not found in the cache.
417    pub fn process_trade_tick(&self, trade: &TradeTick) -> anyhow::Result<()> {
418        if !self.config.trade_execution {
419            return Ok(());
420        }
421
422        let instrument_id = trade.instrument_id;
423        let instrument = self
424            .cache
425            .borrow()
426            .instrument(&instrument_id)
427            .cloned()
428            .ok_or_else(|| anyhow::anyhow!("Instrument not found: {instrument_id}"))?;
429
430        let mut inner = self.inner.borrow_mut();
431        inner.ensure_matching_engine(&instrument);
432        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
433            engine.get_engine_mut().process_trade_tick(trade);
434        }
435        Ok(())
436    }
437
438    /// Processes a bar through the matching engine.
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if the instrument is not found in the cache.
443    pub fn process_bar(&self, bar: &Bar) -> anyhow::Result<()> {
444        if !self.config.bar_execution {
445            return Ok(());
446        }
447
448        let instrument_id = bar.bar_type.instrument_id();
449        let instrument = self
450            .cache
451            .borrow()
452            .instrument(&instrument_id)
453            .cloned()
454            .ok_or_else(|| anyhow::anyhow!("Instrument not found: {instrument_id}"))?;
455
456        let mut inner = self.inner.borrow_mut();
457        inner.ensure_matching_engine(&instrument);
458        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
459            engine.get_engine_mut().process_bar(bar);
460        }
461        Ok(())
462    }
463
464    /// Processes order book deltas through the matching engine.
465    ///
466    /// # Errors
467    ///
468    /// Returns an error if the instrument is not found in the cache.
469    pub fn process_order_book_deltas(&self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
470        let instrument_id = deltas.instrument_id;
471        let instrument = self
472            .cache
473            .borrow()
474            .instrument(&instrument_id)
475            .cloned()
476            .ok_or_else(|| anyhow::anyhow!("Instrument not found: {instrument_id}"))?;
477
478        let mut inner = self.inner.borrow_mut();
479        inner.ensure_matching_engine(&instrument);
480        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
481            engine.get_engine_mut().process_order_book_deltas(deltas)?;
482        }
483        Ok(())
484    }
485
486    /// Resets the sandbox to its initial state.
487    pub fn reset(&self) {
488        let mut inner = self.inner.borrow_mut();
489        for engine in inner.matching_engines.values_mut() {
490            engine.get_engine_mut().reset();
491        }
492
493        inner.balances.clear();
494        for money in &self.config.starting_balances {
495            inner
496                .balances
497                .insert(money.currency.code.to_string(), *money);
498        }
499
500        log::info!(
501            "Sandbox execution client reset: venue={}",
502            self.config.venue
503        );
504    }
505
506    /// Generates account balance entries from current balances.
507    fn get_account_balances(&self) -> Vec<AccountBalance> {
508        self.inner
509            .borrow()
510            .balances
511            .values()
512            .map(|money| AccountBalance::new(*money, Money::new(0.0, money.currency), *money))
513            .collect()
514    }
515
516    fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
517        self.cache
518            .borrow()
519            .order(client_order_id)
520            .cloned()
521            .ok_or_else(|| anyhow::anyhow!("Order not found in cache for {client_order_id}"))
522    }
523}
524
525#[async_trait(?Send)]
526impl ExecutionClient for SandboxExecutionClient {
527    fn is_connected(&self) -> bool {
528        self.core.borrow().is_connected()
529    }
530
531    fn client_id(&self) -> ClientId {
532        self.core.borrow().client_id
533    }
534
535    fn account_id(&self) -> AccountId {
536        self.core.borrow().account_id
537    }
538
539    fn venue(&self) -> Venue {
540        self.core.borrow().venue
541    }
542
543    fn oms_type(&self) -> OmsType {
544        self.config.oms_type
545    }
546
547    fn get_account(&self) -> Option<AccountAny> {
548        let account_id = self.core.borrow().account_id;
549        self.cache.borrow().account(&account_id).cloned()
550    }
551
552    fn generate_account_state(
553        &self,
554        balances: Vec<AccountBalance>,
555        margins: Vec<MarginBalance>,
556        reported: bool,
557        ts_event: UnixNanos,
558    ) -> anyhow::Result<()> {
559        let ts_init = self.clock.borrow().timestamp_ns();
560        let state = self
561            .factory
562            .generate_account_state(balances, margins, reported, ts_event, ts_init);
563        let endpoint = MessagingSwitchboard::portfolio_update_account();
564        msgbus::send_account_state(endpoint, &state);
565        Ok(())
566    }
567
568    fn start(&mut self) -> anyhow::Result<()> {
569        if self.core.borrow().is_started() {
570            return Ok(());
571        }
572
573        // Register message handlers to receive market data
574        self.register_message_handlers();
575
576        self.core.borrow().set_started();
577        let core = self.core.borrow();
578        log::info!(
579            "Sandbox execution client started: venue={}, account_id={}, oms_type={:?}, account_type={:?}",
580            self.config.venue,
581            core.account_id,
582            self.config.oms_type,
583            self.config.account_type,
584        );
585        Ok(())
586    }
587
588    fn stop(&mut self) -> anyhow::Result<()> {
589        if self.core.borrow().is_stopped() {
590            return Ok(());
591        }
592
593        // Deregister message handlers to stop receiving data
594        self.deregister_message_handlers();
595
596        self.core.borrow().set_stopped();
597        self.core.borrow().set_disconnected();
598        log::info!(
599            "Sandbox execution client stopped: venue={}",
600            self.config.venue
601        );
602        Ok(())
603    }
604
605    async fn connect(&mut self) -> anyhow::Result<()> {
606        if self.core.borrow().is_connected() {
607            return Ok(());
608        }
609
610        let balances = self.get_account_balances();
611        let ts_event = self.clock.borrow().timestamp_ns();
612        self.generate_account_state(balances, vec![], false, ts_event)?;
613
614        self.core.borrow().set_connected();
615        log::info!(
616            "Sandbox execution client connected: venue={}",
617            self.config.venue
618        );
619        Ok(())
620    }
621
622    async fn disconnect(&mut self) -> anyhow::Result<()> {
623        if self.core.borrow().is_disconnected() {
624            return Ok(());
625        }
626
627        self.core.borrow().set_disconnected();
628        log::info!(
629            "Sandbox execution client disconnected: venue={}",
630            self.config.venue
631        );
632        Ok(())
633    }
634
635    fn submit_order(&self, cmd: &SubmitOrder) -> anyhow::Result<()> {
636        let mut order = self.get_order(&cmd.client_order_id)?;
637
638        if order.is_closed() {
639            log::warn!("Cannot submit closed order {}", order.client_order_id());
640            return Ok(());
641        }
642
643        let ts_init = self.clock.borrow().timestamp_ns();
644        let event = self.factory.generate_order_submitted(&order, ts_init);
645        let endpoint = MessagingSwitchboard::exec_engine_process();
646        msgbus::send_order_event(endpoint, event);
647
648        let instrument_id = order.instrument_id();
649        let instrument = self
650            .cache
651            .borrow()
652            .instrument(&instrument_id)
653            .cloned()
654            .ok_or_else(|| anyhow::anyhow!("Instrument not found: {instrument_id}"))?;
655
656        let mut inner = self.inner.borrow_mut();
657        inner.ensure_matching_engine(&instrument);
658
659        // Update matching engine with latest market data from cache
660        let cache = self.cache.borrow();
661
662        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
663            if let Some(quote) = cache.quote(&instrument_id) {
664                engine.get_engine_mut().process_quote_tick(quote);
665            }
666
667            if self.config.trade_execution
668                && let Some(trade) = cache.trade(&instrument_id)
669            {
670                engine.get_engine_mut().process_trade_tick(trade);
671            }
672        }
673        drop(cache);
674
675        let account_id = self.core.borrow().account_id;
676
677        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
678            engine
679                .get_engine_mut()
680                .process_order(&mut order, account_id);
681        }
682
683        Ok(())
684    }
685
686    fn submit_order_list(&self, cmd: &SubmitOrderList) -> anyhow::Result<()> {
687        let ts_init = self.clock.borrow().timestamp_ns();
688        let endpoint = MessagingSwitchboard::exec_engine_process();
689
690        let orders: Vec<OrderAny> = self
691            .cache
692            .borrow()
693            .orders_for_ids(&cmd.order_list.client_order_ids, cmd);
694
695        for order in &orders {
696            if order.is_closed() {
697                log::warn!("Cannot submit closed order {}", order.client_order_id());
698                continue;
699            }
700
701            let event = self.factory.generate_order_submitted(order, ts_init);
702            msgbus::send_order_event(endpoint, event);
703        }
704
705        let account_id = self.core.borrow().account_id;
706        for order in &orders {
707            if order.is_closed() {
708                continue;
709            }
710
711            let instrument_id = order.instrument_id();
712            let instrument = self.cache.borrow().instrument(&instrument_id).cloned();
713
714            if let Some(instrument) = instrument {
715                let mut inner = self.inner.borrow_mut();
716                inner.ensure_matching_engine(&instrument);
717
718                // Update with latest market data
719                let cache = self.cache.borrow();
720
721                if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
722                    if let Some(quote) = cache.quote(&instrument_id) {
723                        engine.get_engine_mut().process_quote_tick(quote);
724                    }
725
726                    if self.config.trade_execution
727                        && let Some(trade) = cache.trade(&instrument_id)
728                    {
729                        engine.get_engine_mut().process_trade_tick(trade);
730                    }
731                }
732                drop(cache);
733
734                if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
735                    let mut order_clone = order.clone();
736                    engine
737                        .get_engine_mut()
738                        .process_order(&mut order_clone, account_id);
739                }
740            }
741        }
742
743        Ok(())
744    }
745
746    fn modify_order(&self, cmd: &ModifyOrder) -> anyhow::Result<()> {
747        let instrument_id = cmd.instrument_id;
748        let account_id = self.core.borrow().account_id;
749
750        let mut inner = self.inner.borrow_mut();
751        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
752            engine.get_engine_mut().process_modify(cmd, account_id);
753        }
754        Ok(())
755    }
756
757    fn cancel_order(&self, cmd: &CancelOrder) -> anyhow::Result<()> {
758        let instrument_id = cmd.instrument_id;
759        let account_id = self.core.borrow().account_id;
760
761        let mut inner = self.inner.borrow_mut();
762        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
763            engine.get_engine_mut().process_cancel(cmd, account_id);
764        }
765        Ok(())
766    }
767
768    fn cancel_all_orders(&self, cmd: &CancelAllOrders) -> anyhow::Result<()> {
769        let instrument_id = cmd.instrument_id;
770        let account_id = self.core.borrow().account_id;
771
772        let mut inner = self.inner.borrow_mut();
773        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
774            engine.get_engine_mut().process_cancel_all(cmd, account_id);
775        }
776        Ok(())
777    }
778
779    fn batch_cancel_orders(&self, cmd: &BatchCancelOrders) -> anyhow::Result<()> {
780        let instrument_id = cmd.instrument_id;
781        let account_id = self.core.borrow().account_id;
782
783        let mut inner = self.inner.borrow_mut();
784        if let Some(engine) = inner.matching_engines.get_mut(&instrument_id) {
785            engine
786                .get_engine_mut()
787                .process_batch_cancel(cmd, account_id);
788        }
789        Ok(())
790    }
791
792    fn query_account(&self, _cmd: &QueryAccount) -> anyhow::Result<()> {
793        let balances = self.get_current_account_balances();
794        let ts_event = self.clock.borrow().timestamp_ns();
795        self.generate_account_state(balances, vec![], false, ts_event)?;
796        Ok(())
797    }
798
799    fn query_order(&self, _cmd: &QueryOrder) -> anyhow::Result<()> {
800        // Orders are tracked in the cache, no external query needed for sandbox
801        Ok(())
802    }
803
804    async fn generate_order_status_report(
805        &self,
806        _cmd: &GenerateOrderStatusReport,
807    ) -> anyhow::Result<Option<OrderStatusReport>> {
808        // Sandbox orders are tracked internally
809        Ok(None)
810    }
811
812    async fn generate_order_status_reports(
813        &self,
814        _cmd: &GenerateOrderStatusReports,
815    ) -> anyhow::Result<Vec<OrderStatusReport>> {
816        // Sandbox orders are tracked internally
817        Ok(Vec::new())
818    }
819
820    async fn generate_fill_reports(
821        &self,
822        _cmd: GenerateFillReports,
823    ) -> anyhow::Result<Vec<FillReport>> {
824        // Sandbox fills are tracked internally
825        Ok(Vec::new())
826    }
827
828    async fn generate_position_status_reports(
829        &self,
830        _cmd: &GeneratePositionStatusReports,
831    ) -> anyhow::Result<Vec<PositionStatusReport>> {
832        // Sandbox positions are tracked internally
833        Ok(Vec::new())
834    }
835
836    async fn generate_mass_status(
837        &self,
838        _lookback_mins: Option<u64>,
839    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
840        // Sandbox doesn't need reconciliation
841        Ok(None)
842    }
843}