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, 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
62/// Inner state for the sandbox execution client.
63///
64/// This is wrapped in `Rc<RefCell<>>` so message handlers can hold weak references.
65struct SandboxInner {
66    /// Dynamic clock for matching engines.
67    clock: Rc<RefCell<dyn Clock>>,
68    /// Reference to the cache.
69    cache: Rc<RefCell<Cache>>,
70    /// The sandbox configuration.
71    config: SandboxExecutionClientConfig,
72    /// Matching engines per instrument.
73    matching_engines: AHashMap<InstrumentId, OrderEngineAdapter>,
74    /// Next raw ID assigned to a matching engine.
75    next_engine_raw_id: u32,
76    /// Current account balances.
77    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    /// Ensures a matching engine exists for the given instrument.
167    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    /// Processes a quote tick through the matching engine.
204    fn process_quote_tick(&mut self, quote: &QuoteTick) {
205        let instrument_id = quote.instrument_id;
206
207        // Try to get instrument from cache, create engine if found
208        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    /// Processes a trade tick through the matching engine.
223    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    /// Processes a bar through the matching engine.
245    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        // A delayed close belongs to an existing exposure lifecycle. Unlike an
307        // instrument status update, it must not recreate execution state from
308        // cache after rotation/unsubscribe; pending-settlement ownership stays
309        // with the already-initialized matching engine.
310        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
363/// Registered message handlers for later deregistration.
364struct 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
381/// A sandbox execution client for paper trading against live market data.
382///
383/// The `SandboxExecutionClient` simulates order execution using the `OrderMatchingEngine`
384/// to match orders against market data. This enables strategy testing in real-time
385/// without actual order execution on exchanges.
386pub struct SandboxExecutionClient {
387    /// The core execution client functionality.
388    core: RefCell<ExecutionClientCore>,
389    /// Factory for generating order events.
390    factory: OrderEventFactory,
391    /// The sandbox configuration.
392    config: SandboxExecutionClientConfig,
393    /// Inner state wrapped for handler access.
394    inner: Rc<RefCell<SandboxInner>>,
395    /// Registered message handlers for cleanup.
396    handlers: RefCell<Option<RegisteredHandlers>>,
397    /// Reference to the clock.
398    clock: Rc<RefCell<dyn Clock>>,
399    /// Reference to the cache.
400    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    /// Creates a new [`SandboxExecutionClient`] instance.
419    #[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    /// Returns a reference to the configuration.
460    #[must_use]
461    pub const fn config(&self) -> &SandboxExecutionClientConfig {
462        &self.config
463    }
464
465    /// Returns the number of active matching engines.
466    #[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    /// Registers message handlers for market data subscriptions.
481    ///
482    /// This subscribes to order book deltas, quotes, trades, and bars for the
483    /// configured venue, routing all received data to the matching engines.
484    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        // Order book deltas handler
495        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        // Quote tick handler
507        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        // Trade tick handler
519        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        // Bar handler (topic is data.bars.{bar_type}, filter by venue in handler)
531        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                    // ExecutionEngine updates the cached position state before publishing
575                    // PositionClosed, so this retry observes the post-settlement cache view.
576                    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        // Subscribe patterns
589        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        // Store handlers for later deregistration
606        *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    /// Deregisters message handlers to stop receiving market data.
630    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    /// Returns current account balances, preferring cache state over starting balances.
651    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        // Use account from cache if available (updated by fill events)
656        if let Some(account) = cache.account(&account_id) {
657            return account.balances().into_values().collect();
658        }
659
660        // Fall back to starting balances
661        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    /// Processes a quote tick through the matching engine.
682    ///
683    /// # Errors
684    ///
685    /// Returns an error if the instrument is not found in the cache.
686    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    /// Processes a trade tick through the matching engine.
703    ///
704    /// # Errors
705    ///
706    /// Returns an error if the instrument is not found in the cache.
707    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    /// Processes a bar through the matching engine.
728    ///
729    /// # Errors
730    ///
731    /// Returns an error if the instrument is not found in the cache.
732    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    /// Processes order book deltas through the matching engine.
753    ///
754    /// # Errors
755    ///
756    /// Returns an error if the instrument is not found in the cache.
757    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    /// Resets the sandbox to its initial state.
770    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    /// Generates account balance entries from current balances.
790    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        // Register message handlers to receive market data
877        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        // Deregister message handlers to stop receiving data
897        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        // Update matching engine with latest market data from cache
957        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                // Update with latest market data
1024                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        // Orders are tracked in the cache, no external query needed for sandbox
1128        Ok(())
1129    }
1130
1131    async fn generate_order_status_report(
1132        &self,
1133        _cmd: &GenerateOrderStatusReport,
1134    ) -> anyhow::Result<Option<OrderStatusReport>> {
1135        // Sandbox orders are tracked internally
1136        Ok(None)
1137    }
1138
1139    async fn generate_order_status_reports(
1140        &self,
1141        _cmd: &GenerateOrderStatusReports,
1142    ) -> anyhow::Result<Vec<OrderStatusReport>> {
1143        // Sandbox orders are tracked internally
1144        Ok(Vec::new())
1145    }
1146
1147    async fn generate_fill_reports(
1148        &self,
1149        _cmd: GenerateFillReports,
1150    ) -> anyhow::Result<Vec<FillReport>> {
1151        // Sandbox fills are tracked internally
1152        Ok(Vec::new())
1153    }
1154
1155    async fn generate_position_status_reports(
1156        &self,
1157        _cmd: &GeneratePositionStatusReports,
1158    ) -> anyhow::Result<Vec<PositionStatusReport>> {
1159        // Sandbox positions are tracked internally
1160        Ok(Vec::new())
1161    }
1162
1163    async fn generate_mass_status(
1164        &self,
1165        _lookback_mins: Option<u64>,
1166    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1167        // Sandbox doesn't need reconciliation
1168        Ok(None)
1169    }
1170}