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