Skip to main content

nautilus_data/engine/
mod.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//! Provides a high-performance `DataEngine` for all environments.
17//!
18//! The `DataEngine` is the central component of the entire data stack.
19//! The data engines primary responsibility is to orchestrate interactions between
20//! the `DataClient` instances, and the rest of the platform. This includes sending
21//! requests to, and receiving responses from, data endpoints via its registered
22//! data clients.
23//!
24//! The engine employs a simple fan-in fan-out messaging pattern to execute
25//! `DataCommand` type messages, and process `DataResponse` messages or market data
26//! objects.
27//!
28//! Alternative implementations can be written on top of the generic engine - which
29//! just need to override the `execute`, `process`, `send`, and `receive` methods.
30
31pub mod bar;
32pub mod book;
33pub mod config;
34
35#[cfg(feature = "defi")]
36pub mod pool;
37
38#[cfg(feature = "streaming")]
39mod streaming;
40
41mod commands;
42mod dispatch;
43mod futures;
44mod handlers;
45mod option_chain;
46mod registry;
47mod requests;
48mod responses;
49mod spread_quote;
50mod synthetic;
51mod time_range;
52
53use std::{
54    any::{Any, type_name},
55    cell::RefCell,
56    collections::VecDeque,
57    fmt::{Debug, Display},
58    mem,
59    num::NonZeroUsize,
60    rc::Rc,
61    str::FromStr,
62};
63
64use ::futures::future::join_all;
65use ahash::{AHashMap, AHashSet};
66use anyhow::Context;
67pub use bar::BarAggregatorSubscription;
68use bar::{BarAggregationSubscription, BarAggregatorKey, bar_aggregator_key};
69use book::{
70    BookDeltasKey, BookSnapshotInfos, BookSnapshotKey, BookSnapshotSource, BookSnapshotter,
71    BookSubscription, BookSubscriptionOwner, BookUpdater,
72};
73pub(crate) use commands::{DeferredCommand, DeferredCommandQueue};
74use config::DataEngineConfig;
75use dispatch::{log_error_on_cache_insert, process_engine_bar};
76use futures::{ContinuousFutureRoller, ContinuousFutureSubscriptionState, datetime_to_unix_nanos};
77use handlers::{
78    BAR_AGGREGATOR_PRIORITY, BarBarHandler, BarQuoteHandler, BarTradeHandler, SpreadQuoteHandler,
79};
80use indexmap::IndexMap;
81#[cfg(feature = "defi")]
82use nautilus_common::messages::defi::PoolSnapshotResponse;
83use nautilus_common::{
84    cache::Cache,
85    clock::Clock,
86    logging::{RECV, RES},
87    messages::data::{
88        BarsResponse, BookDeltasResponse, BookDepthResponse, CustomDataResponse, DataCommand,
89        DataRequestFailed, DataResponse, DataResponseEnvelope, IdentifierCountError,
90        OptionChainReferencePriceResponse, PARAMS_IS_PARENT, QuoteRouting, RequestBars,
91        RequestCommand, RequestJoin, RequestOptionChainReferencePrice, RequestQuotes,
92        RequestTrades, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth, SubscribeCommand,
93        SubscribeOptionChain, SubscribeOptionGreeks, SubscribeQuotes, SubscribeTrades,
94        UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth, UnsubscribeBookSnapshots,
95        UnsubscribeCommand, UnsubscribeInstrumentStatus, UnsubscribeOptionChain,
96        UnsubscribeOptionGreeks, UnsubscribeQuotes, UnsubscribeTrades, ensure_routable_bar_types,
97        ensure_routable_instrument_ids, is_parent_subscription, quote_routing,
98    },
99    msgbus::{
100        self, BusPayloadType, ShareableMessageHandler, TypedHandler, TypedIntoHandler,
101        switchboard::{self, MessagingSwitchboard},
102    },
103    runner::get_data_cmd_sender,
104    timer::{TimeEvent, TimeEventCallback},
105};
106use nautilus_core::{
107    DurationNanos, Params, UUID4, UnixNanos, WeakCell,
108    correctness::{FAILED, check_key_in_map, check_key_not_in_map, check_predicate_true},
109    datetime::NANOSECONDS_IN_DAY,
110};
111#[cfg(feature = "defi")]
112use nautilus_model::defi::DefiData;
113use nautilus_model::{
114    data::{
115        Bar, BarType, CustomData, Data, DataRef, DataType, FundingRateUpdate, HasTsInit,
116        IndexPriceUpdate, InstrumentClose, InstrumentStatus, MarkPriceUpdate, OrderBookDelta,
117        OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
118        option_chain::{OptionGreeks, StrikeRange},
119    },
120    enums::{
121        AggregationSource, BarAggregation, BookType, InstrumentClass, MarketStatusAction,
122        PriceType, RecordFlag,
123    },
124    identifiers::{
125        ClientId, GENERIC_SPREAD_ID_SEPARATOR, InstrumentId, OptionSeriesId, Venue,
126        parse_generic_spread_id_legs,
127    },
128    instruments::{Instrument, InstrumentAny, SyntheticInstrument},
129    orderbook::OrderBook,
130    types::{Price, Quantity},
131};
132use option_chain::{
133    OptionChainBootstrapper, OptionChainGreeksBootstrap, PendingOptionChainRequest,
134};
135use requests::{
136    ContinuousFutureRequest, ContinuousFutureRequestState, ContinuousFutureSegment,
137    ContinuousFutureSource, RequestBarAggregation, continuous_future_request_from_bars,
138    continuous_future_subscription_from_bars, has_continuous_future_params,
139    request_bar_aggregation_from_params, request_params, response_params,
140};
141use responses::{
142    log_if_empty_response, parent_request_window, rebind_response_correlation,
143    rebuild_pipeline_response, same_instrument,
144};
145use serde_json::Value;
146use spread_quote::SpreadQuoteState;
147#[cfg(feature = "streaming")]
148use streaming::CatalogMap;
149use time_range::{
150    TimeRangePipelineState, has_time_range_pipeline_params, is_time_range_pipeline_variant,
151};
152use ustr::Ustr;
153
154#[cfg(feature = "defi")]
155#[allow(unused_imports)] // Brings DeFi impl blocks into scope
156use crate::defi::engine as _;
157#[cfg(feature = "defi")]
158use crate::engine::pool::PoolUpdater;
159use crate::{
160    aggregation::{
161        BarAggregator, RenkoBarAggregator, SpreadQuoteAggregator, TickBarAggregator,
162        TickImbalanceBarAggregator, TickRunsBarAggregator, TimeBarAggregator, ValueBarAggregator,
163        ValueImbalanceBarAggregator, ValueRunsBarAggregator, VolumeBarAggregator,
164        VolumeImbalanceBarAggregator, VolumeRunsBarAggregator,
165    },
166    client::DataClientAdapter,
167    option_chains::OptionChainManager,
168    subscription::{
169        SubscriptionKey, SubscriptionRegistry, SubscriptionRelease, split_subscribe,
170        split_unsubscribe,
171    },
172};
173
174/// Provides a high-performance `DataEngine` for all environments.
175#[derive(Debug)]
176pub struct DataEngine {
177    clock: Rc<RefCell<dyn Clock>>,
178    cache: Rc<RefCell<Cache>>,
179    config: DataEngineConfig,
180    msgbus_priority: u32,
181    subscriptions_external: SubscriptionRegistry<(ClientId, SubscriptionKey), SubscribeCommand>,
182    clients: IndexMap<ClientId, DataClientAdapter>,
183    default_client_id: Option<ClientId>,
184    routing_map: IndexMap<Venue, ClientId>,
185    book_intervals: AHashMap<NonZeroUsize, BookSnapshotInfos>,
186    book_snapshot_counts: IndexMap<BookSnapshotKey, usize>,
187    book_snapshot_sources: AHashMap<InstrumentId, BookSnapshotSource>,
188    book_deltas_counts: IndexMap<BookDeltasKey, usize>,
189    book_depth_counts: IndexMap<BookDeltasKey, usize>,
190    book_updaters: AHashMap<InstrumentId, Rc<BookUpdater>>,
191    book_subscriptions: AHashMap<InstrumentId, BookSubscription>,
192    book_subscription_owners: AHashMap<InstrumentId, Vec<Rc<BookSubscriptionOwner>>>,
193    book_snapshotters: AHashMap<NonZeroUsize, Rc<BookSnapshotter>>,
194    bar_aggregators: IndexMap<BarAggregatorKey, Rc<RefCell<Box<dyn BarAggregator>>>>,
195    bar_aggregator_handlers: AHashMap<BarAggregatorKey, Vec<BarAggregatorSubscription>>,
196    subscriptions_bar_aggregation: AHashMap<BarType, BarAggregationSubscription>,
197    request_bar_aggregations: AHashMap<UUID4, RequestBarAggregation>,
198    inflight_requests: AHashMap<UUID4, RequestCommand>,
199    request_pipeline_parent_request: AHashMap<UUID4, RequestCommand>,
200    request_pipeline_n_components: AHashMap<UUID4, usize>,
201    request_pipeline_parent_request_id: AHashMap<UUID4, (UUID4, usize)>,
202    request_pipeline_responses: AHashMap<UUID4, Vec<Option<Vec<DataResponseEnvelope>>>>,
203    time_range_pipeline_requests: AHashMap<UUID4, TimeRangePipelineState>,
204    time_range_pipeline_parent_request_id: AHashMap<UUID4, UUID4>,
205    parent_join_request_id: AHashMap<UUID4, UUID4>,
206    pending_join_requests: AHashMap<UUID4, RequestJoin>,
207    continuous_future_requests: AHashMap<UUID4, ContinuousFutureRequestState>,
208    continuous_future_parent_request_id: AHashMap<UUID4, UUID4>,
209    continuous_future_subscriptions: AHashMap<BarType, ContinuousFutureSubscriptionState>,
210    continuous_future_roller: Option<Rc<ContinuousFutureRoller>>,
211    spread_quote_states: AHashMap<InstrumentId, SpreadQuoteState>,
212    option_chain_managers: AHashMap<OptionSeriesId, Rc<RefCell<OptionChainManager>>>,
213    option_chain_instrument_index: AHashMap<InstrumentId, OptionSeriesId>,
214    deferred_cmd_queue: DeferredCommandQueue,
215    option_chain_bootstrapper: Option<Rc<OptionChainBootstrapper>>,
216    pending_option_chain_requests: AHashMap<UUID4, PendingOptionChainRequest>,
217    option_chain_greeks_bootstraps: AHashMap<OptionSeriesId, OptionChainGreeksBootstrap>,
218    synthetic_quote_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
219    synthetic_trade_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
220    subscribed_synthetic_quotes: AHashMap<InstrumentId, usize>,
221    subscribed_synthetic_trades: AHashMap<InstrumentId, usize>,
222    buffered_deltas_map: AHashMap<InstrumentId, OrderBookDeltas>,
223    deltas_frame: Vec<OrderBookDelta>,
224    command_count: u64,
225    data_count: u64,
226    request_count: u64,
227    response_count: u64,
228    #[cfg(feature = "streaming")]
229    catalogs: CatalogMap,
230    #[cfg(feature = "defi")]
231    pub(crate) pool_updaters: AHashMap<InstrumentId, Rc<PoolUpdater>>,
232    #[cfg(feature = "defi")]
233    pub(crate) pool_snapshot_pending: AHashMap<InstrumentId, UUID4>,
234    #[cfg(feature = "defi")]
235    pub(crate) pool_event_buffers: AHashMap<InstrumentId, Vec<DefiData>>,
236}
237
238impl DataEngine {
239    /// Creates a new [`DataEngine`] instance.
240    #[must_use]
241    pub fn new(
242        clock: Rc<RefCell<dyn Clock>>,
243        cache: Rc<RefCell<Cache>>,
244        config: Option<DataEngineConfig>,
245    ) -> Self {
246        let config = config.unwrap_or_default();
247        if let Some(client_ids) = &config.external_clients {
248            let mut cache = cache.borrow_mut();
249            for client_id in client_ids {
250                cache.add_external_data_client(*client_id);
251            }
252        }
253
254        Self {
255            clock,
256            cache,
257            config,
258            msgbus_priority: 10, // High-priority for built-in component
259            subscriptions_external: SubscriptionRegistry::default(),
260            clients: IndexMap::new(),
261            default_client_id: None,
262            routing_map: IndexMap::new(),
263            book_intervals: AHashMap::new(),
264            book_snapshot_counts: IndexMap::new(),
265            book_snapshot_sources: AHashMap::new(),
266            book_deltas_counts: IndexMap::new(),
267            book_depth_counts: IndexMap::new(),
268            book_updaters: AHashMap::new(),
269            book_subscriptions: AHashMap::new(),
270            book_subscription_owners: AHashMap::new(),
271            book_snapshotters: AHashMap::new(),
272            bar_aggregators: IndexMap::new(),
273            bar_aggregator_handlers: AHashMap::new(),
274            subscriptions_bar_aggregation: AHashMap::new(),
275            request_bar_aggregations: AHashMap::new(),
276            inflight_requests: AHashMap::new(),
277            request_pipeline_parent_request: AHashMap::new(),
278            request_pipeline_n_components: AHashMap::new(),
279            request_pipeline_parent_request_id: AHashMap::new(),
280            request_pipeline_responses: AHashMap::new(),
281            time_range_pipeline_requests: AHashMap::new(),
282            time_range_pipeline_parent_request_id: AHashMap::new(),
283            parent_join_request_id: AHashMap::new(),
284            pending_join_requests: AHashMap::new(),
285            continuous_future_requests: AHashMap::new(),
286            continuous_future_parent_request_id: AHashMap::new(),
287            continuous_future_subscriptions: AHashMap::new(),
288            continuous_future_roller: None,
289            spread_quote_states: AHashMap::new(),
290            option_chain_managers: AHashMap::new(),
291            option_chain_instrument_index: AHashMap::new(),
292            deferred_cmd_queue: Rc::new(RefCell::new(VecDeque::new())),
293            option_chain_bootstrapper: None,
294            pending_option_chain_requests: AHashMap::new(),
295            option_chain_greeks_bootstraps: AHashMap::new(),
296            synthetic_quote_feeds: AHashMap::new(),
297            synthetic_trade_feeds: AHashMap::new(),
298            subscribed_synthetic_quotes: AHashMap::new(),
299            subscribed_synthetic_trades: AHashMap::new(),
300            buffered_deltas_map: AHashMap::new(),
301            deltas_frame: Vec::new(),
302            command_count: 0,
303            data_count: 0,
304            request_count: 0,
305            response_count: 0,
306            #[cfg(feature = "streaming")]
307            catalogs: CatalogMap::new(),
308            #[cfg(feature = "defi")]
309            pool_updaters: AHashMap::new(),
310            #[cfg(feature = "defi")]
311            pool_snapshot_pending: AHashMap::new(),
312            #[cfg(feature = "defi")]
313            pool_event_buffers: AHashMap::new(),
314        }
315    }
316
317    /// Returns a reference to the clock.
318    #[must_use]
319    pub fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
320        &self.clock
321    }
322
323    /// Returns a reference to the cache.
324    #[must_use]
325    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
326        &self.cache
327    }
328
329    /// Returns a reference to the configuration.
330    #[must_use]
331    pub const fn config(&self) -> &DataEngineConfig {
332        &self.config
333    }
334
335    #[cfg(feature = "defi")]
336    #[must_use]
337    pub(crate) const fn msgbus_priority(&self) -> u32 {
338        self.msgbus_priority
339    }
340
341    /// Registers all message bus handlers for the data engine.
342    pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
343        let weak = WeakCell::from(Rc::downgrade(engine));
344        engine.borrow_mut().continuous_future_roller =
345            Some(Rc::new(ContinuousFutureRoller::new(engine)));
346        engine.borrow_mut().option_chain_bootstrapper =
347            Some(Rc::new(OptionChainBootstrapper::new(engine)));
348
349        let weak1 = weak.clone();
350        msgbus::register_data_command_endpoint(
351            MessagingSwitchboard::data_engine_execute(),
352            TypedIntoHandler::from(move |cmd: DataCommand| {
353                if let Some(rc) = weak1.upgrade() {
354                    rc.borrow_mut().execute(cmd);
355                }
356            }),
357        );
358
359        msgbus::register_data_command_endpoint(
360            MessagingSwitchboard::data_engine_queue_execute(),
361            TypedIntoHandler::from(move |cmd: DataCommand| {
362                get_data_cmd_sender().execute(cmd);
363            }),
364        );
365
366        // Register process handler (polymorphic - uses Any)
367        let weak2 = weak.clone();
368        msgbus::register_any(
369            MessagingSwitchboard::data_engine_process(),
370            ShareableMessageHandler::from_any(move |data: &dyn Any| {
371                if let Some(rc) = weak2.upgrade() {
372                    rc.borrow_mut().process(data);
373                }
374            }),
375        );
376
377        // Register process_data handler (typed - takes ownership)
378        let weak3 = weak.clone();
379        msgbus::register_data_endpoint(
380            MessagingSwitchboard::data_engine_process_data(),
381            TypedIntoHandler::from(move |data: Data| {
382                if let Some(rc) = weak3.upgrade() {
383                    rc.borrow_mut().process_data(data);
384                }
385            }),
386        );
387
388        // Register process_defi_data handler (typed - takes ownership)
389        #[cfg(feature = "defi")]
390        {
391            let weak4 = weak.clone();
392            msgbus::register_defi_data_endpoint(
393                MessagingSwitchboard::data_engine_process_defi_data(),
394                TypedIntoHandler::from(move |data: DefiData| {
395                    if let Some(rc) = weak4.upgrade() {
396                        rc.borrow_mut().process_defi_data(data);
397                    }
398                }),
399            );
400        }
401
402        let weak5 = weak;
403        msgbus::register_data_response_endpoint(
404            MessagingSwitchboard::data_engine_response(),
405            TypedIntoHandler::from(move |resp: DataResponse| {
406                if let Some(rc) = weak5.upgrade() {
407                    rc.borrow_mut().response(resp);
408                }
409            }),
410        );
411    }
412
413    /// Returns the total count of data commands received by the engine.
414    #[must_use]
415    pub const fn command_count(&self) -> u64 {
416        self.command_count
417    }
418
419    /// Returns the total count of data stream objects received by the engine.
420    #[must_use]
421    pub const fn data_count(&self) -> u64 {
422        self.data_count
423    }
424
425    #[cfg(feature = "defi")]
426    pub(crate) const fn increment_data_count(&mut self) {
427        self.data_count += 1;
428    }
429
430    /// Returns the total count of data requests received by the engine.
431    #[must_use]
432    pub const fn request_count(&self) -> u64 {
433        self.request_count
434    }
435
436    /// Returns the total count of data responses received by the engine.
437    #[must_use]
438    pub const fn response_count(&self) -> u64 {
439        self.response_count
440    }
441
442    /// Returns the number of request pipelines awaiting leg responses.
443    #[must_use]
444    pub fn request_pipeline_count(&self) -> usize {
445        self.request_pipeline_parent_request.len()
446    }
447
448    /// Returns the number of time-range pipelines awaiting child responses.
449    #[must_use]
450    pub fn time_range_pipeline_count(&self) -> usize {
451        self.time_range_pipeline_requests.len()
452    }
453
454    /// Returns the number of `RequestJoin` originals awaiting finalization.
455    #[must_use]
456    pub fn pending_join_request_count(&self) -> usize {
457        self.pending_join_requests.len()
458    }
459
460    /// Starts all registered data clients and re-arms bar aggregator timers.
461    pub fn start(&mut self) {
462        for client in self.get_clients_mut() {
463            if client.start().is_err() {
464                log::error!("Failed to start data client {}", client.client_id());
465            }
466        }
467
468        for ((_, request_id), aggregator) in &self.bar_aggregators {
469            // Request-scoped or historical aggregators run on private clocks;
470            // re-arming them here would perturb an in-flight request's timer state
471            let is_subscription = request_id.is_none() && !aggregator.borrow().is_historical();
472            if is_subscription && aggregator.borrow().bar_type().spec().is_time_aggregated() {
473                aggregator
474                    .borrow_mut()
475                    .start_timer(Some(Rc::clone(aggregator)));
476            }
477        }
478
479        for state in self.spread_quote_states.values() {
480            state
481                .aggregator
482                .borrow_mut()
483                .start_timer(Some(Rc::clone(&state.aggregator)));
484        }
485    }
486
487    /// Stops all registered data clients and bar aggregator timers.
488    pub fn stop(&mut self) {
489        for client in self.get_clients_mut() {
490            if client.stop().is_err() {
491                log::error!("Failed to stop data client {}", client.client_id());
492            }
493        }
494
495        for aggregator in self.bar_aggregators.values() {
496            aggregator.borrow_mut().stop();
497        }
498
499        for state in self.spread_quote_states.values() {
500            state.aggregator.borrow_mut().stop_timer();
501        }
502    }
503
504    /// Resets all registered data clients and clears engine state.
505    pub fn reset(&mut self) {
506        for client in self.get_clients_mut() {
507            match client.reset() {
508                Ok(()) => client.clear_subscription_state(),
509                Err(_) => log::error!("Failed to reset data client {}", client.client_id()),
510            }
511        }
512
513        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
514        for (bar_type, request_id) in keys {
515            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
516                log::error!("Error stopping bar aggregator during reset for {bar_type}: {e}");
517            }
518        }
519        self.subscriptions_bar_aggregation.clear();
520
521        self.request_bar_aggregations.clear();
522        self.inflight_requests.clear();
523        self.request_pipeline_parent_request.clear();
524        self.request_pipeline_n_components.clear();
525        self.request_pipeline_parent_request_id.clear();
526        self.request_pipeline_responses.clear();
527        self.time_range_pipeline_requests.clear();
528        self.time_range_pipeline_parent_request_id.clear();
529        self.parent_join_request_id.clear();
530        self.pending_join_requests.clear();
531        self.continuous_future_requests.clear();
532        self.continuous_future_parent_request_id.clear();
533
534        for state in self.continuous_future_subscriptions.values_mut() {
535            if let Some(name) = state.timer_name.take() {
536                self.clock.borrow_mut().cancel_timer(&name);
537            }
538        }
539        self.continuous_future_subscriptions.clear();
540
541        let spread_ids: Vec<InstrumentId> = self.spread_quote_states.keys().copied().collect();
542        for spread_id in spread_ids {
543            self.stop_spread_quote_aggregation(spread_id);
544        }
545
546        // Tear down option chain managers to unregister their msgbus handlers
547        let managers: Vec<_> = self.option_chain_managers.drain().collect();
548        for (_, manager) in managers {
549            manager.borrow_mut().teardown(&self.clock);
550        }
551
552        self.option_chain_instrument_index.clear();
553        self.cancel_pending_option_chain_requests(None);
554        self.clear_option_chain_greeks_bootstraps();
555
556        // Unsubscribe BookUpdaters before dropping; otherwise the typed router
557        // keeps dispatching to abandoned updaters. `book_updaters` is keyed by
558        // per-underlying id, so the literal per-underlying topic is the same
559        // string the subscribe path used.
560        let book_updaters: Vec<(InstrumentId, Rc<BookUpdater>)> =
561            self.book_updaters.drain().collect();
562        for (instrument_id, updater) in book_updaters {
563            let deltas_topic = switchboard::get_book_deltas_topic(instrument_id);
564            let depth_topic = switchboard::get_book_depth_topic(instrument_id);
565            let deltas_handler: TypedHandler<OrderBookDeltas> =
566                TypedHandler::new(Rc::clone(&updater));
567            let depth_handler: TypedHandler<OrderBookDepth> = TypedHandler::new(updater);
568            msgbus::unsubscribe_book_deltas(deltas_topic.into(), &deltas_handler);
569            msgbus::unsubscribe_book_depth(depth_topic.into(), &depth_handler);
570        }
571
572        self.book_subscriptions.clear();
573        self.book_subscription_owners.clear();
574
575        self.book_deltas_counts.clear();
576        self.book_depth_counts.clear();
577        self.book_intervals.clear();
578        self.book_snapshot_counts.clear();
579        self.book_snapshot_sources.clear();
580        self.book_snapshotters.clear();
581        self.buffered_deltas_map.clear();
582        self.deltas_frame.clear();
583
584        self.synthetic_quote_feeds.clear();
585        self.synthetic_trade_feeds.clear();
586        self.subscribed_synthetic_quotes.clear();
587        self.subscribed_synthetic_trades.clear();
588        self.subscriptions_external.clear();
589
590        self.deferred_cmd_queue.borrow_mut().clear();
591
592        #[cfg(feature = "defi")]
593        self.clear_pool_updaters();
594
595        self.clock.borrow_mut().cancel_timers();
596
597        self.command_count = 0;
598        self.data_count = 0;
599        self.request_count = 0;
600        self.response_count = 0;
601    }
602
603    /// Disposes the engine, stopping all clients and canceling any timers.
604    pub fn dispose(&mut self) {
605        for client in self.get_clients_mut() {
606            if client.dispose().is_err() {
607                log::error!("Failed to dispose data client {}", client.client_id());
608            }
609        }
610
611        self.clear_option_chain_greeks_bootstraps();
612
613        // Continuous-future source handlers live outside bar_aggregator_handlers,
614        // so release them before dropping the aggregators
615        let mut cf_sources = Vec::new();
616
617        for state in self.continuous_future_subscriptions.values_mut() {
618            if let Some(name) = state.timer_name.take() {
619                self.clock.borrow_mut().cancel_timer(&name);
620            }
621
622            if let Some(subscription) = state.active_source_subscription.take() {
623                cf_sources.push((state.target_bar_type, subscription));
624            }
625        }
626
627        for (target_bar_type, subscription) in cf_sources {
628            self.unsubscribe_continuous_future_source(target_bar_type, subscription);
629        }
630        self.continuous_future_subscriptions.clear();
631
632        // Unsubscribe aggregator msgbus handlers so the typed routers don't keep
633        // entries pointing at dropped aggregators
634        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
635        for (bar_type, request_id) in keys {
636            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
637                log::error!("Error stopping bar aggregator during dispose for {bar_type}: {e}");
638            }
639        }
640
641        self.subscriptions_external.clear();
642
643        self.clock.borrow_mut().cancel_timers();
644    }
645
646    /// Connects all registered data clients concurrently.
647    ///
648    /// Connection failures are logged but do not prevent the node from running.
649    /// Logs identify the client but omit provider error text, which can contain credentials.
650    pub async fn connect(&mut self) {
651        let futures: Vec<_> = self
652            .get_clients_mut()
653            .into_iter()
654            .map(|client| async move {
655                let client_id = client.client_id();
656                (client_id, client.connect().await)
657            })
658            .collect();
659
660        let results = join_all(futures).await;
661
662        for (client_id, result) in results {
663            if result.is_err() {
664                log::error!("Failed to connect data client {client_id}: connection failed");
665            }
666        }
667    }
668
669    /// Disconnects all registered data clients concurrently.
670    ///
671    /// # Errors
672    ///
673    /// Returns an error if any client fails to disconnect.
674    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
675        let futures: Vec<_> = self
676            .get_clients_mut()
677            .into_iter()
678            .map(|client| async move {
679                let client_id = client.client_id();
680                (client_id, client.disconnect().await)
681            })
682            .collect();
683
684        let results = join_all(futures).await;
685
686        // A closed session cannot answer, so discard its pending bootstraps
687        #[cfg(feature = "defi")]
688        self.abandon_pool_snapshots();
689
690        let failed_clients: Vec<_> = results
691            .into_iter()
692            .filter_map(|(client_id, result)| result.err().map(|_| client_id.to_string()))
693            .collect();
694
695        if failed_clients.is_empty() {
696            Ok(())
697        } else {
698            anyhow::bail!(
699                "Failed to disconnect data clients: {}",
700                failed_clients.join(", ")
701            )
702        }
703    }
704
705    /// Returns `true` if all registered data clients are currently connected.
706    #[must_use]
707    pub fn check_connected(&self) -> bool {
708        self.get_clients()
709            .iter()
710            .all(|client| client.is_connected())
711    }
712
713    /// Returns `true` if all registered data clients are currently disconnected.
714    #[must_use]
715    pub fn check_disconnected(&self) -> bool {
716        self.get_clients()
717            .iter()
718            .all(|client| !client.is_connected())
719    }
720
721    /// Returns all custom data types currently subscribed across all clients.
722    #[must_use]
723    pub fn subscribed_custom_data(&self) -> Vec<DataType> {
724        self.collect_subscriptions(|client| &client.subscriptions_custom)
725    }
726
727    /// Returns all instrument IDs currently subscribed across all clients.
728    #[must_use]
729    pub fn subscribed_instruments(&self) -> Vec<InstrumentId> {
730        self.collect_subscriptions(|client| &client.subscriptions_instrument)
731    }
732
733    /// Returns all instrument IDs for which quote subscriptions exist.
734    #[must_use]
735    pub fn subscribed_quotes(&self) -> Vec<InstrumentId> {
736        self.collect_subscriptions(|client| &client.subscriptions_quotes)
737    }
738
739    /// Returns all instrument IDs for which trade subscriptions exist.
740    #[must_use]
741    pub fn subscribed_trades(&self) -> Vec<InstrumentId> {
742        self.collect_subscriptions(|client| &client.subscriptions_trades)
743    }
744
745    /// Returns all bar types currently subscribed across all clients,
746    /// including internally aggregated subscriptions (v1 parity).
747    #[must_use]
748    pub fn subscribed_bars(&self) -> Vec<BarType> {
749        let mut subscribed = self.collect_subscriptions(|client| &client.subscriptions_bars);
750        subscribed.extend(
751            self.bar_aggregators
752                .keys()
753                .filter(|(_, request_id)| request_id.is_none())
754                .map(|(bar_type, _)| *bar_type),
755        );
756        subscribed
757    }
758
759    /// Returns all instrument IDs for which mark price subscriptions exist.
760    #[must_use]
761    pub fn subscribed_mark_prices(&self) -> Vec<InstrumentId> {
762        self.collect_subscriptions(|client| &client.subscriptions_mark_prices)
763    }
764
765    /// Returns all instrument IDs for which index price subscriptions exist.
766    #[must_use]
767    pub fn subscribed_index_prices(&self) -> Vec<InstrumentId> {
768        self.collect_subscriptions(|client| &client.subscriptions_index_prices)
769    }
770
771    /// Returns all instrument IDs for which funding rate subscriptions exist.
772    #[must_use]
773    pub fn subscribed_funding_rates(&self) -> Vec<InstrumentId> {
774        self.collect_subscriptions(|client| &client.subscriptions_funding_rates)
775    }
776
777    /// Returns all instrument IDs for which status subscriptions exist.
778    #[must_use]
779    pub fn subscribed_instrument_status(&self) -> Vec<InstrumentId> {
780        self.collect_subscriptions(|client| &client.subscriptions_instrument_status)
781    }
782
783    /// Returns all instrument IDs for which instrument close subscriptions exist.
784    #[must_use]
785    pub fn subscribed_instrument_close(&self) -> Vec<InstrumentId> {
786        self.collect_subscriptions(|client| &client.subscriptions_instrument_close)
787    }
788
789    /// Executes a `DataCommand` by delegating to subscribe, unsubscribe, or request handlers.
790    ///
791    /// This is the final synchronous dispatch point for data commands. Runtime command producers
792    /// should send to `DataEngine.queue_execute`, which lets the runner sequence command execution
793    /// before this method runs. The engine also calls this method for child commands generated while
794    /// processing a parent command, where immediate in-engine ordering matters.
795    ///
796    /// Errors during execution are logged.
797    pub fn execute(&mut self, cmd: DataCommand) {
798        match &cmd {
799            DataCommand::Subscribe(_) | DataCommand::Unsubscribe(_) => self.command_count += 1,
800            DataCommand::Request(_) => self.request_count += 1,
801            #[cfg(feature = "defi")]
802            DataCommand::DefiRequest(_) => self.request_count += 1,
803            #[cfg(feature = "defi")]
804            DataCommand::DefiSubscribe(_) | DataCommand::DefiUnsubscribe(_) => {
805                self.command_count += 1;
806            }
807            _ => {}
808        }
809
810        let command_context = match &cmd {
811            DataCommand::Subscribe(c) => (
812                "Subscribe",
813                c.client_id().copied(),
814                c.venue().copied(),
815                Some(c.command_id()),
816            ),
817            DataCommand::Unsubscribe(c) => (
818                "Unsubscribe",
819                c.client_id().copied(),
820                c.venue().copied(),
821                Some(c.command_id()),
822            ),
823            DataCommand::Request(c) => (
824                "Request",
825                c.client_id().copied(),
826                c.venue().copied(),
827                Some(*c.request_id()),
828            ),
829            #[cfg(feature = "defi")]
830            DataCommand::DefiRequest(c) => (
831                "DefiRequest",
832                c.client_id().copied(),
833                c.venue().copied(),
834                Some(*c.request_id()),
835            ),
836            #[cfg(feature = "defi")]
837            DataCommand::DefiSubscribe(c) => (
838                "DefiSubscribe",
839                c.client_id().copied(),
840                c.venue().copied(),
841                Some(c.command_id()),
842            ),
843            #[cfg(feature = "defi")]
844            DataCommand::DefiUnsubscribe(c) => (
845                "DefiUnsubscribe",
846                c.client_id().copied(),
847                c.venue().copied(),
848                Some(c.command_id()),
849            ),
850            _ => ("Unknown", None, None, None),
851        };
852
853        let result = match cmd {
854            DataCommand::Subscribe(c) => self.execute_subscribe(c),
855            DataCommand::Unsubscribe(c) => self.execute_unsubscribe(&c),
856            DataCommand::Request(c) => self.execute_request(c),
857            #[cfg(feature = "defi")]
858            DataCommand::DefiRequest(c) => self.execute_defi_request(c),
859            #[cfg(feature = "defi")]
860            DataCommand::DefiSubscribe(c) => self.execute_defi_subscribe(c),
861            #[cfg(feature = "defi")]
862            DataCommand::DefiUnsubscribe(c) => self.execute_defi_unsubscribe(&c),
863            _ => {
864                log::warn!("Unhandled DataCommand variant");
865                Ok(())
866            }
867        };
868
869        if let Err(e) = result {
870            log::error!("Data command failed: {command_context:?}: {e}");
871        }
872    }
873
874    /// Handles a subscribe command, updating internal state and forwarding to the client.
875    ///
876    /// # Errors
877    ///
878    /// Returns an error if the subscription is invalid (e.g., synthetic instrument for book data),
879    /// or if the underlying client operation fails.
880    pub fn execute_subscribe(&mut self, cmd: SubscribeCommand) -> anyhow::Result<()> {
881        cmd.validate_routing(&self.cache.borrow())?;
882        if let Some(client_id) = cmd.client_id().copied()
883            && self.is_external_client(client_id)
884        {
885            self.subscribe_external(client_id, &cmd);
886            return Ok(());
887        }
888
889        self.retain_intent(&cmd)?;
890
891        let retained = cmd.clone();
892        let (groups, result) = self.prepare_subscribe_groups(cmd);
893        self.dispatch_subscribe_groups(&retained, groups);
894        result
895    }
896
897    fn retain_intent(&mut self, cmd: &SubscribeCommand) -> anyhow::Result<()> {
898        let retain_intent = match cmd {
899            SubscribeCommand::Quotes(command) => {
900                self.quote_routing(&command.instrument_ids, command.params.as_ref())?
901                    == QuoteRouting::Client
902            }
903            SubscribeCommand::Trades(command) => !command.instrument_ids[0].is_synthetic(),
904            _ => false,
905        };
906
907        if let Some(client) = self.get_command_client(cmd.client_id(), cmd.venue()) {
908            if let SubscribeCommand::Bars(command) = cmd {
909                let external: Vec<_> = command
910                    .bar_types
911                    .iter()
912                    .copied()
913                    .filter(BarType::is_externally_aggregated)
914                    .collect();
915
916                if !external.is_empty() {
917                    let intent = SubscribeCommand::Bars(command.with_bar_types(external));
918                    client.retain_intent(&intent);
919                }
920            } else if retain_intent {
921                client.retain_intent(cmd);
922            }
923        }
924
925        Ok(())
926    }
927
928    fn prepare_subscribe_groups(
929        &mut self,
930        cmd: SubscribeCommand,
931    ) -> (Vec<SubscribeCommand>, anyhow::Result<()>) {
932        let members = if matches!(
933            &cmd,
934            SubscribeCommand::Quotes(_) | SubscribeCommand::Trades(_)
935        ) {
936            vec![cmd]
937        } else {
938            split_subscribe(cmd)
939        };
940
941        let mut groups: IndexMap<Option<Value>, Vec<SubscribeCommand>> = IndexMap::new();
942        let mut result = Ok(());
943
944        for member in members {
945            let prepared = match self.prepare_subscribe_member(&member) {
946                Ok(Some(prepared)) => prepared,
947                Ok(None) => continue,
948                Err(e) => {
949                    log::error!(
950                        "Subscribe preparation failed: command={}, client={:?}, instrument_ids={:?}, bar_types={:?}: {e}",
951                        member.command_id(),
952                        member.client_id(),
953                        member.instrument_ids(),
954                        member.bar_types(),
955                    );
956
957                    if result.is_ok() {
958                        result = Err(e);
959                    }
960
961                    continue;
962                }
963            };
964
965            // Members share one parent; only catalog-inferred params can differ
966            let params = prepared.params().map(|params| {
967                Value::Object(params.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
968            });
969
970            groups.entry(params).or_default().push(prepared);
971        }
972
973        let groups = groups
974            .into_values()
975            .map(|mut members| {
976                if members.len() == 1 {
977                    return members.pop().expect("nonempty group");
978                }
979
980                let first = &members[0];
981                if first.instrument_ids().is_some() {
982                    let ids: Vec<_> = members
983                        .iter()
984                        .flat_map(|member| member.instrument_ids().expect("same parent kind"))
985                        .copied()
986                        .collect();
987                    first.with_instrument_ids(ids).expect("instrument command")
988                } else if first.bar_types().is_some() {
989                    let types: Vec<_> = members
990                        .iter()
991                        .flat_map(|member| member.bar_types().expect("same parent kind"))
992                        .copied()
993                        .collect();
994                    first.with_bar_types(types).expect("bar command")
995                } else {
996                    first.clone()
997                }
998            })
999            .collect();
1000
1001        (groups, result)
1002    }
1003
1004    fn dispatch_subscribe_groups(&mut self, cmd: &SubscribeCommand, groups: Vec<SubscribeCommand>) {
1005        for group in groups {
1006            let retained = if let Some(ids) = group.instrument_ids() {
1007                cmd.with_instrument_ids(ids).expect("instrument parent")
1008            } else if let Some(types) = group.bar_types() {
1009                cmd.with_bar_types(types).expect("bar parent")
1010            } else {
1011                cmd.clone()
1012            };
1013
1014            let retain_on_failure = !matches!(
1015                &group,
1016                SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookDepth(_)
1017            );
1018
1019            if let Some(client) = self.get_command_client(group.client_id(), group.venue()) {
1020                client.execute_subscribe_with_retained(group, retained, retain_on_failure);
1021            } else {
1022                log::error!(
1023                    "Cannot handle command: no client found for client_id={:?}, venue={:?}",
1024                    group.client_id(),
1025                    group.venue()
1026                );
1027            }
1028        }
1029    }
1030
1031    fn prepare_subscribe_member(
1032        &mut self,
1033        cmd: &SubscribeCommand,
1034    ) -> anyhow::Result<Option<SubscribeCommand>> {
1035        // Update internal engine state
1036        match cmd {
1037            SubscribeCommand::BookDeltas(book_cmd) => {
1038                if !self.subscribe_book_deltas(book_cmd)? && self.client_subscription_active(cmd) {
1039                    return Ok(None);
1040                }
1041            }
1042            SubscribeCommand::BookDepth(book_cmd) => {
1043                if !self.subscribe_book_depth(book_cmd)? && self.client_subscription_active(cmd) {
1044                    return Ok(None);
1045                }
1046            }
1047            SubscribeCommand::BookSnapshots(cmd) => {
1048                // Handles client forwarding internally (forwards as BookDeltas)
1049                self.subscribe_book_snapshots(cmd)?;
1050                return Ok(None);
1051            }
1052            SubscribeCommand::Bars(cmd) if has_continuous_future_params(cmd.params.as_ref()) => {
1053                self.subscribe_continuous_future_bars(cmd)?;
1054                return Ok(None);
1055            }
1056            SubscribeCommand::Bars(cmd) => {
1057                self.subscribe_bars(cmd)?;
1058                if cmd
1059                    .single_bar_type()
1060                    .expect("member preparation receives one bar type")
1061                    .is_internally_aggregated()
1062                {
1063                    return Ok(None);
1064                }
1065            }
1066            SubscribeCommand::OptionChain(cmd) if cmd.snapshot_interval_ms == Some(0) => {
1067                anyhow::bail!(
1068                    "Cannot subscribe option chain {} with a zero `snapshot_interval_ms`; use `None` for raw mode",
1069                    cmd.series_id,
1070                );
1071            }
1072            SubscribeCommand::OptionChain(cmd) => {
1073                self.subscribe_option_chain(cmd);
1074                return Ok(None);
1075            }
1076            SubscribeCommand::Quotes(cmd) if self.subscribe_quotes(cmd)? => {
1077                return Ok(None);
1078            }
1079            SubscribeCommand::Trades(cmd) if self.subscribe_trades(cmd) => {
1080                return Ok(None);
1081            }
1082            SubscribeCommand::Instrument(cmd)
1083                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1084            {
1085                anyhow::bail!("Cannot subscribe for synthetic instrument `Instrument` data");
1086            }
1087            SubscribeCommand::InstrumentStatus(cmd)
1088                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1089            {
1090                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentStatus` data");
1091            }
1092            SubscribeCommand::InstrumentClose(cmd)
1093                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1094            {
1095                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentClose` data");
1096            }
1097            SubscribeCommand::OptionGreeks(cmd)
1098                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1099            {
1100                anyhow::bail!("Cannot subscribe for synthetic instrument `OptionGreeks` data");
1101            }
1102            _ => {} // Do nothing else
1103        }
1104
1105        let cmd = cmd.clone();
1106        #[cfg(feature = "streaming")]
1107        let cmd = self.subscribe_command_with_prefilled_start_ns(cmd)?;
1108
1109        Ok(Some(cmd))
1110    }
1111
1112    fn client_subscription_active(&mut self, cmd: &SubscribeCommand) -> bool {
1113        self.get_command_client(cmd.client_id(), cmd.venue())
1114            .is_some_and(|client| client.has_active_subscription(cmd))
1115    }
1116
1117    /// Handles an unsubscribe command, updating internal state and forwarding to the client.
1118    ///
1119    /// # Errors
1120    ///
1121    /// Returns an error if the underlying client operation fails.
1122    pub fn execute_unsubscribe(&mut self, cmd: &UnsubscribeCommand) -> anyhow::Result<()> {
1123        if let Some(bar_types) = cmd.bar_types() {
1124            ensure_routable_bar_types(bar_types, cmd.client_id().is_some())?;
1125        }
1126
1127        if let Some(ids) = cmd.instrument_ids() {
1128            ensure_routable_instrument_ids(ids, cmd.client_id().is_some())?;
1129        }
1130
1131        if let UnsubscribeCommand::Trades(trade) = cmd
1132            && !trade
1133                .instrument_ids
1134                .iter()
1135                .all(|id| id.is_synthetic() == trade.instrument_ids[0].is_synthetic())
1136        {
1137            anyhow::bail!("trade identifier lists cannot mix client and synthetic routes");
1138        }
1139
1140        if let Some(client_id) = cmd.client_id().copied()
1141            && self.is_external_client(client_id)
1142        {
1143            self.unsubscribe_external(client_id, cmd);
1144            return Ok(());
1145        }
1146
1147        if matches!(
1148            &cmd,
1149            UnsubscribeCommand::Instrument(_)
1150                | UnsubscribeCommand::InstrumentStatus(_)
1151                | UnsubscribeCommand::InstrumentClose(_)
1152                | UnsubscribeCommand::OptionGreeks(_)
1153                | UnsubscribeCommand::BookDeltas(_)
1154                | UnsubscribeCommand::BookDepth(_)
1155                | UnsubscribeCommand::BookSnapshots(_)
1156        ) && cmd
1157            .instrument_ids()
1158            .is_some_and(|ids| ids.len() > 1 && ids.iter().any(InstrumentId::is_synthetic))
1159        {
1160            anyhow::bail!("Cannot unsubscribe synthetic instruments for this data type");
1161        }
1162
1163        let (prepared, result) = self.prepare_unsubscribe_members(cmd);
1164
1165        for command in UnsubscribeCommand::group(prepared) {
1166            if let Some(client) = self.get_command_client(command.client_id(), command.venue()) {
1167                client.execute_unsubscribe(&command);
1168            } else {
1169                log::error!(
1170                    "Cannot handle command: no client found for client_id={:?}, venue={:?}",
1171                    command.client_id(),
1172                    command.venue()
1173                );
1174            }
1175        }
1176
1177        result
1178    }
1179
1180    fn prepare_unsubscribe_members(
1181        &mut self,
1182        cmd: &UnsubscribeCommand,
1183    ) -> (Vec<UnsubscribeCommand>, anyhow::Result<()>) {
1184        let members = if matches!(
1185            cmd,
1186            UnsubscribeCommand::Quotes(_) | UnsubscribeCommand::Trades(_)
1187        ) {
1188            vec![cmd.clone()]
1189        } else {
1190            split_unsubscribe(cmd)
1191        };
1192
1193        let mut prepared = Vec::new();
1194        let mut result = Ok(());
1195
1196        for member in members {
1197            match self.prepare_unsubscribe_member(&member) {
1198                Ok(true) => prepared.push(member),
1199                Ok(false) => {}
1200                Err(e) => {
1201                    log::error!(
1202                        "Unsubscribe preparation failed: command={}, client={:?}, instrument_ids={:?}, bar_types={:?}: {e}",
1203                        member.command_id(),
1204                        member.client_id(),
1205                        member.instrument_ids(),
1206                        member.bar_types(),
1207                    );
1208
1209                    if result.is_ok() {
1210                        result = Err(e);
1211                    }
1212                }
1213            }
1214        }
1215
1216        (prepared, result)
1217    }
1218
1219    fn prepare_unsubscribe_member(&mut self, cmd: &UnsubscribeCommand) -> anyhow::Result<bool> {
1220        if matches!(
1221            cmd,
1222            UnsubscribeCommand::BookDeltas(_)
1223                | UnsubscribeCommand::BookDepth(_)
1224                | UnsubscribeCommand::BookSnapshots(_)
1225        ) && !self.release_book_subscription(cmd)
1226        {
1227            return Ok(false);
1228        }
1229
1230        match &cmd {
1231            UnsubscribeCommand::BookDeltas(cmd) if !self.unsubscribe_book_deltas(cmd) => {
1232                return Ok(false);
1233            }
1234            UnsubscribeCommand::BookDepth(cmd) if !self.unsubscribe_book_depth(cmd) => {
1235                return Ok(false);
1236            }
1237            UnsubscribeCommand::BookSnapshots(cmd) => {
1238                // Handles client forwarding internally (forwards as BookDeltas)
1239                self.unsubscribe_book_snapshots(cmd);
1240                return Ok(false);
1241            }
1242            UnsubscribeCommand::Bars(cmd)
1243                if self.continuous_future_subscriptions.contains_key(
1244                    &cmd.single_bar_type()
1245                        .expect("member preparation receives one bar type")
1246                        .standard(),
1247                ) =>
1248            {
1249                // Don't tear down the chain while other actors remain subscribed
1250                let topic = switchboard::get_bars_topic(
1251                    cmd.single_bar_type()
1252                        .expect("member preparation receives one bar type")
1253                        .standard(),
1254                );
1255
1256                if msgbus::exact_subscriber_count_bars(topic) == 0 {
1257                    self.unsubscribe_continuous_future_bars(cmd);
1258                }
1259
1260                return Ok(false);
1261            }
1262            UnsubscribeCommand::Bars(cmd) => {
1263                self.unsubscribe_bars(cmd);
1264                if cmd
1265                    .single_bar_type()
1266                    .expect("member preparation receives one bar type")
1267                    .is_internally_aggregated()
1268                {
1269                    return Ok(false);
1270                }
1271            }
1272            UnsubscribeCommand::OptionChain(cmd) => {
1273                self.unsubscribe_option_chain(cmd);
1274                return Ok(false);
1275            }
1276            UnsubscribeCommand::Quotes(cmd) if self.unsubscribe_quotes(cmd)? => {
1277                return Ok(false);
1278            }
1279            UnsubscribeCommand::Trades(cmd) if self.unsubscribe_trades(cmd) => {
1280                return Ok(false);
1281            }
1282            UnsubscribeCommand::Instrument(cmd)
1283                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1284            {
1285                anyhow::bail!("Cannot unsubscribe from synthetic instrument `Instrument` data");
1286            }
1287            UnsubscribeCommand::InstrumentStatus(cmd)
1288                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1289            {
1290                anyhow::bail!(
1291                    "Cannot unsubscribe from synthetic instrument `InstrumentStatus` data"
1292                );
1293            }
1294            UnsubscribeCommand::InstrumentClose(cmd)
1295                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1296            {
1297                anyhow::bail!(
1298                    "Cannot unsubscribe from synthetic instrument `InstrumentClose` data"
1299                );
1300            }
1301            UnsubscribeCommand::OptionGreeks(cmd)
1302                if cmd.instrument_ids.iter().any(InstrumentId::is_synthetic) =>
1303            {
1304                anyhow::bail!("Cannot unsubscribe from synthetic instrument `OptionGreeks` data");
1305            }
1306            _ => {}
1307        }
1308
1309        Ok(true)
1310    }
1311
1312    fn subscribe_external(&mut self, client_id: ClientId, cmd: &SubscribeCommand) {
1313        let dispatch = match cmd {
1314            SubscribeCommand::OptionChain(command) => {
1315                self.retain_external_option_chain(client_id, command, cmd);
1316                cmd.clone()
1317            }
1318            _ => {
1319                let mut acquired = false;
1320                let mut fresh_ids = Vec::new();
1321                let mut fresh_bar_types = Vec::new();
1322
1323                for member in split_subscribe(cmd.clone()) {
1324                    let key = SubscriptionKey::from_subscribe(&member);
1325
1326                    if !self.subscriptions_external.retain(
1327                        (client_id, key.clone()),
1328                        member.command_id(),
1329                        member,
1330                    ) {
1331                        continue;
1332                    }
1333
1334                    acquired = true;
1335
1336                    match key {
1337                        SubscriptionKey::Bars(bar_type) => fresh_bar_types.push(bar_type),
1338                        SubscriptionKey::Quotes(id)
1339                        | SubscriptionKey::Trades(id)
1340                        | SubscriptionKey::Instrument(id)
1341                        | SubscriptionKey::BookDeltas(id)
1342                        | SubscriptionKey::BookDepth(id)
1343                        | SubscriptionKey::MarkPrices(id)
1344                        | SubscriptionKey::IndexPrices(id)
1345                        | SubscriptionKey::FundingRates(id)
1346                        | SubscriptionKey::InstrumentStatus(id)
1347                        | SubscriptionKey::InstrumentClose(id)
1348                        | SubscriptionKey::OptionGreeks(id)
1349                        | SubscriptionKey::BookSnapshots(id, _) => fresh_ids.push(id),
1350                        _ => {}
1351                    }
1352                }
1353
1354                if !acquired {
1355                    return;
1356                }
1357
1358                if cmd.bar_types().is_some() {
1359                    cmd.with_bar_types(fresh_bar_types)
1360                        .expect("bar command has types")
1361                } else {
1362                    cmd.with_instrument_ids(fresh_ids)
1363                        .unwrap_or_else(|| cmd.clone())
1364                }
1365            }
1366        };
1367
1368        register_external_streaming_type(&dispatch);
1369        publish_external_data_command(client_id, &dispatch);
1370
1371        if self.config.debug {
1372            log::debug!("Skipping subscribe command for external client {client_id}: {cmd:?}");
1373        }
1374    }
1375
1376    fn unsubscribe_external(&mut self, client_id: ClientId, cmd: &UnsubscribeCommand) {
1377        let mut releases = IndexMap::new();
1378
1379        for member in split_unsubscribe(cmd) {
1380            let key = (client_id, SubscriptionKey::from_unsubscribe(&member));
1381
1382            let (acquisition_id, command) = match self.subscriptions_external.release(&key) {
1383                SubscriptionRelease::Retained => continue,
1384                SubscriptionRelease::Final(subscribe) => (
1385                    subscribe.command_id(),
1386                    subscribe.into_unsubscribe(
1387                        cmd.command_id(),
1388                        cmd.ts_init(),
1389                        cmd.correlation_id(),
1390                    ),
1391                ),
1392                SubscriptionRelease::Untracked => (cmd.command_id(), member),
1393            };
1394
1395            self.subscriptions_external.remove(&key);
1396            releases
1397                .entry(acquisition_id)
1398                .or_insert_with(Vec::new)
1399                .push(command);
1400        }
1401
1402        for command in releases.into_values().flat_map(UnsubscribeCommand::group) {
1403            publish_external_data_command(client_id, &command);
1404        }
1405
1406        if self.config.debug {
1407            log::debug!("Skipping unsubscribe command for external client {client_id}: {cmd:?}");
1408        }
1409    }
1410
1411    fn subscribe_quotes(&mut self, cmd: &SubscribeQuotes) -> anyhow::Result<bool> {
1412        match self.quote_routing(&cmd.instrument_ids, cmd.params.as_ref())? {
1413            QuoteRouting::Client => Ok(false),
1414            QuoteRouting::Synthetic => {
1415                for id in &cmd.instrument_ids {
1416                    self.subscribe_synthetic_quotes(*id);
1417                }
1418
1419                Ok(true)
1420            }
1421            QuoteRouting::Spread => {
1422                for id in &cmd.instrument_ids {
1423                    let member = cmd.with_instrument_ids(*id);
1424                    self.subscribe_spread_quotes(&member);
1425                }
1426
1427                Ok(true)
1428            }
1429        }
1430    }
1431
1432    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<bool> {
1433        match self.quote_routing(&cmd.instrument_ids, cmd.params.as_ref())? {
1434            QuoteRouting::Client => Ok(false),
1435            QuoteRouting::Synthetic => {
1436                for id in &cmd.instrument_ids {
1437                    self.unsubscribe_synthetic_quotes(*id);
1438                }
1439
1440                Ok(true)
1441            }
1442            QuoteRouting::Spread => {
1443                for id in &cmd.instrument_ids {
1444                    let member = cmd.with_instrument_ids(*id);
1445                    self.unsubscribe_spread_quotes(&member);
1446                }
1447
1448                Ok(true)
1449            }
1450        }
1451    }
1452
1453    fn subscribe_trades(&mut self, cmd: &SubscribeTrades) -> bool {
1454        if !cmd.instrument_ids[0].is_synthetic() {
1455            return false;
1456        }
1457
1458        for id in &cmd.instrument_ids {
1459            self.subscribe_synthetic_trades(*id);
1460        }
1461
1462        true
1463    }
1464
1465    fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> bool {
1466        if !cmd.instrument_ids[0].is_synthetic() {
1467            return false;
1468        }
1469
1470        for id in &cmd.instrument_ids {
1471            self.unsubscribe_synthetic_trades(*id);
1472        }
1473
1474        true
1475    }
1476
1477    fn quote_routing(
1478        &self,
1479        ids: &[InstrumentId],
1480        params: Option<&Params>,
1481    ) -> anyhow::Result<QuoteRouting> {
1482        quote_routing(ids, None, params, &self.cache.borrow())
1483    }
1484
1485    /// Sends a [`RequestCommand`] to a suitable data client implementation.
1486    ///
1487    /// Submission errors also send the requester a terminal failure envelope.
1488    ///
1489    /// # Errors
1490    ///
1491    /// Returns an error if validation fails, no client is found for the given client ID or venue,
1492    /// or the client rejects direct submission.
1493    pub fn execute_request(&mut self, req: RequestCommand) -> anyhow::Result<()> {
1494        let request_id = *req.request_id();
1495        let result = self.execute_request_inner(req);
1496        if let Err(e) = &result {
1497            self.process_response_envelope(DataResponseEnvelope::terminal(
1498                request_id,
1499                Some(e.to_string()),
1500            ));
1501        }
1502
1503        result
1504    }
1505
1506    fn execute_request_inner(&mut self, req: RequestCommand) -> anyhow::Result<()> {
1507        Self::validate_request(&req)?;
1508
1509        // Skip requests for external clients
1510        if let Some(cid) = req.client_id()
1511            && self.is_external_client(*cid)
1512        {
1513            if self.config.debug {
1514                log::debug!("Skipping data request for external client {cid}: {req:?}");
1515            }
1516            return Ok(());
1517        }
1518
1519        if let RequestCommand::Join(join) = req {
1520            return self.handle_request_join(join);
1521        }
1522
1523        if has_continuous_future_params(request_params(&req)) {
1524            return self.execute_continuous_future_request(req);
1525        }
1526
1527        let request_id = *req.request_id();
1528        self.prepare_request_bar_aggregators(&req)?;
1529
1530        if has_time_range_pipeline_params(request_params(&req))
1531            && is_time_range_pipeline_variant(&req)
1532        {
1533            let result = self.execute_time_range_pipeline_request(req);
1534            if result.is_err() {
1535                self.cleanup_request_bar_aggregators(&request_id);
1536            }
1537            return result;
1538        }
1539
1540        #[cfg(feature = "streaming")]
1541        if self.catalogs_registered() && streaming::is_date_range_variant(&req) {
1542            let result = self.dispatch_date_range_request(req);
1543            if result.is_err() {
1544                self.cleanup_request_bar_aggregators(&request_id);
1545            }
1546            return result;
1547        }
1548
1549        let result = self.dispatch_request_to_client(req);
1550
1551        if result.is_err() {
1552            self.cleanup_request_bar_aggregators(&request_id);
1553        }
1554
1555        result.map(|_| ())
1556    }
1557
1558    fn validate_request(req: &RequestCommand) -> anyhow::Result<()> {
1559        let (instrument_ids, requires_single, limit) = match req {
1560            RequestCommand::Instrument(cmd) => (&cmd.instrument_ids, false, None),
1561            RequestCommand::BookSnapshot(cmd) => (&cmd.instrument_ids, true, None),
1562            RequestCommand::BookDeltas(cmd) => (&cmd.instrument_ids, true, None),
1563            RequestCommand::BookDepth(cmd) => (&cmd.instrument_ids, true, None),
1564            RequestCommand::Quotes(cmd) => (&cmd.instrument_ids, false, cmd.limit),
1565            RequestCommand::Trades(cmd) => (&cmd.instrument_ids, false, cmd.limit),
1566            RequestCommand::FundingRates(cmd) => (&cmd.instrument_ids, true, None),
1567            RequestCommand::Bars(cmd) => {
1568                ensure_routable_bar_types(&cmd.bar_types, cmd.client_id.is_some())?;
1569                cmd.single_bar_type()?;
1570                return Ok(());
1571            }
1572            _ => return Ok(()),
1573        };
1574
1575        ensure_routable_instrument_ids(instrument_ids, req.client_id().is_some())?;
1576        anyhow::ensure!(
1577            !requires_single || instrument_ids.len() == 1,
1578            "request requires one instrument ID for this data type, received {}",
1579            instrument_ids.len(),
1580        );
1581        anyhow::ensure!(
1582            instrument_ids.len() == 1 || limit.is_none(),
1583            "A record limit is unsupported for multi-instrument requests",
1584        );
1585        Ok(())
1586    }
1587
1588    pub(super) fn dispatch_request_to_client(
1589        &mut self,
1590        req: RequestCommand,
1591    ) -> anyhow::Result<ClientId> {
1592        self.inflight_requests
1593            .insert(*req.request_id(), req.clone());
1594        let client_id = req.client_id().copied();
1595        let venue = req.venue().copied();
1596        let Some(client) = self.get_client(client_id.as_ref(), venue.as_ref()) else {
1597            anyhow::bail!("Cannot handle request: no client found for {client_id:?} {venue:?}");
1598        };
1599        let resolved_client_id = client.client_id();
1600
1601        #[rustfmt::skip]
1602        let result = match req {
1603            RequestCommand::Data(req) => client.request_data(req),
1604            RequestCommand::Instrument(req) => client.request_instrument(req),
1605            RequestCommand::Instruments(req) => client.request_instruments(req),
1606            RequestCommand::BookSnapshot(req) => client.request_book_snapshot(req),
1607            RequestCommand::BookDeltas(req) => client.request_book_deltas(req),
1608            RequestCommand::BookDepth(req) => client.request_book_depth(req),
1609            RequestCommand::Quotes(req) => client.request_quotes(req),
1610            RequestCommand::Trades(req) => client.request_trades(req),
1611            RequestCommand::FundingRates(req) => client.request_funding_rates(req),
1612            RequestCommand::OptionChainReferencePrice(req) => client.request_option_chain_reference_price(req),
1613            RequestCommand::Bars(req) => client.request_bars(req),
1614            RequestCommand::Join(_) => anyhow::bail!("RequestJoin must be handled by handle_request_join"),
1615        };
1616
1617        result.map_err(|e| {
1618            let context = format!("Data client {resolved_client_id} request failed");
1619
1620            let context = if let Some(rejection) = e.downcast_ref::<IdentifierCountError>() {
1621                format!("{context}: {rejection}")
1622            } else {
1623                context
1624            };
1625
1626            e.context(context)
1627        })?;
1628
1629        Ok(resolved_client_id)
1630    }
1631
1632    /// Processes a dynamically-typed data message.
1633    ///
1634    /// Supports instruments, response envelopes, request failures, funding rates, option greeks,
1635    /// instrument status, and custom data; unrecognized types are logged as errors.
1636    pub fn process(&mut self, data: &dyn Any) {
1637        if let Some(response) = data.downcast_ref::<DataResponseEnvelope>() {
1638            self.response_envelope(response.clone());
1639            return;
1640        }
1641
1642        if let Some(failure) = data.downcast_ref::<DataRequestFailed>() {
1643            self.request_failed(failure.clone());
1644            return;
1645        }
1646
1647        self.data_count += 1;
1648
1649        if let Some(instrument) = data.downcast_ref::<InstrumentAny>() {
1650            self.handle_instrument(instrument);
1651        } else if let Some(funding_rate) = data.downcast_ref::<FundingRateUpdate>() {
1652            self.handle_funding_rate(*funding_rate);
1653        } else if let Some(option_greeks) = data.downcast_ref::<OptionGreeks>() {
1654            self.cache.borrow_mut().add_option_greeks(*option_greeks);
1655            self.feed_option_greeks_to_pre_bootstrap_chain(option_greeks);
1656            let topic = switchboard::get_option_greeks_topic(option_greeks.instrument_id);
1657            msgbus::publish_option_greeks(topic, option_greeks);
1658            self.drain_deferred_commands();
1659        } else if let Some(status) = data.downcast_ref::<InstrumentStatus>() {
1660            self.handle_instrument_status(*status);
1661        } else if let Some(custom) = data.downcast_ref::<CustomData>() {
1662            self.handle_custom_data(custom);
1663        } else {
1664            #[cfg(feature = "defi")]
1665            if let Some(response) = data.downcast_ref::<PoolSnapshotResponse>() {
1666                self.handle_pool_snapshot_response(response);
1667                return;
1668            }
1669
1670            log::error!("Cannot process data {data:?}, type is unrecognized");
1671        }
1672    }
1673
1674    /// Processes a `Data` enum instance, dispatching to live handlers.
1675    #[allow(
1676        clippy::needless_pass_by_value,
1677        reason = "callers hand over ownership; the payload is only moved when a DeFi handler consumes it"
1678    )]
1679    pub fn process_data(&mut self, data: Data) {
1680        #[cfg(feature = "defi")]
1681        let data = match data {
1682            Data::Defi(defi) => {
1683                self.process_defi_data(*defi);
1684                return;
1685            }
1686            data => data,
1687        };
1688
1689        self.process_data_ref(DataRef::from(&data));
1690    }
1691
1692    /// Processes a borrowed `Data` enum view, dispatching to live handlers.
1693    ///
1694    /// DeFi payloads are cloned because the DeFi handler may buffer them while a pool snapshot
1695    /// is pending; no other variant clones its payload.
1696    pub fn process_data_ref(&mut self, data: DataRef<'_>) {
1697        #[cfg(feature = "defi")]
1698        let data = match data {
1699            DataRef::Defi(defi) => {
1700                self.process_defi_data(defi.clone());
1701                return;
1702            }
1703            data => data,
1704        };
1705
1706        self.data_count += 1;
1707
1708        match data {
1709            DataRef::Instrument(instrument) => self.handle_instrument(instrument),
1710            DataRef::BookDelta(delta) => self.handle_delta(*delta),
1711            DataRef::BookDeltas(deltas) => self.handle_deltas(deltas),
1712            DataRef::BookDepth(depth) => self.handle_depth(depth),
1713            DataRef::Quote(quote) => {
1714                self.handle_quote(*quote);
1715                self.drain_deferred_commands();
1716            }
1717            DataRef::Trade(trade) => self.handle_trade(*trade),
1718            DataRef::Bar(bar) => self.handle_bar(*bar),
1719            DataRef::MarkPrice(mark_price) => {
1720                self.handle_mark_price(*mark_price);
1721                self.drain_deferred_commands();
1722            }
1723            DataRef::IndexPrice(index_price) => {
1724                self.handle_index_price(*index_price);
1725                self.drain_deferred_commands();
1726            }
1727            DataRef::FundingRate(funding_rate) => {
1728                self.handle_funding_rate(*funding_rate);
1729                self.drain_deferred_commands();
1730            }
1731            DataRef::OptionGreeks(greeks) => {
1732                self.cache.borrow_mut().add_option_greeks(*greeks);
1733                self.feed_option_greeks_to_pre_bootstrap_chain(greeks);
1734                let topic = switchboard::get_option_greeks_topic(greeks.instrument_id);
1735                msgbus::publish_option_greeks(topic, greeks);
1736                self.drain_deferred_commands();
1737            }
1738            DataRef::InstrumentStatus(status) => {
1739                self.handle_instrument_status(*status);
1740                self.drain_deferred_commands();
1741            }
1742            DataRef::InstrumentClose(close) => self.handle_instrument_close(*close),
1743            DataRef::Custom(custom) => self.handle_custom_data(custom),
1744            #[cfg(feature = "defi")]
1745            DataRef::Defi(_) => unreachable!("handled before market data dispatch"),
1746            #[cfg(not(feature = "defi"))]
1747            #[allow(
1748                unreachable_patterns,
1749                reason = "DeFi variants can exist without this crate's defi feature"
1750            )]
1751            other => log_defi_data_dropped(other),
1752        }
1753    }
1754
1755    /// Processes a `Data` instance through the pipeline bus path.
1756    ///
1757    /// Pipeline mode publishes each item on the `data.pipeline.` topic family and gates cache
1758    /// writes on `disable_historical_cache`. None of the live-only side effects (synthetic
1759    /// republish, option-chain expiry, depth-derived quotes, deferred-command drains) run in this
1760    /// path.
1761    pub fn process_pipeline(&mut self, data: Data) {
1762        #[cfg(feature = "defi")]
1763        let data = match data {
1764            Data::Defi(defi) => {
1765                self.process_defi_data(*defi);
1766                return;
1767            }
1768            data => data,
1769        };
1770
1771        self.data_count += 1;
1772
1773        match data {
1774            Data::Instrument(instrument) => self.handle_instrument(&instrument),
1775            Data::BookDelta(delta) => self.handle_delta_pipeline(delta),
1776            Data::BookDeltas(deltas) => self.handle_deltas_pipeline(&deltas),
1777            Data::BookDepth(depth) => self.handle_depth_pipeline(&depth),
1778            Data::Quote(quote) => self.handle_quote_pipeline(quote),
1779            Data::Trade(trade) => self.handle_trade_pipeline(trade),
1780            Data::Bar(bar) => self.handle_bar_pipeline(bar),
1781            Data::MarkPrice(mark_price) => self.handle_mark_price_pipeline(mark_price),
1782            Data::IndexPrice(index_price) => self.handle_index_price_pipeline(index_price),
1783            Data::FundingRate(funding_rate) => {
1784                self.handle_funding_rate_pipeline(funding_rate);
1785            }
1786            Data::OptionGreeks(greeks) => self.handle_option_greeks_pipeline(greeks),
1787            Data::InstrumentStatus(status) => self.handle_instrument_status_pipeline(status),
1788            Data::InstrumentClose(close) => self.handle_instrument_close_pipeline(close),
1789            Data::Custom(custom) => self.handle_custom_data_pipeline(&custom),
1790            #[cfg(feature = "defi")]
1791            Data::Defi(_) => unreachable!("handled before market data dispatch"),
1792            #[cfg(not(feature = "defi"))]
1793            #[allow(
1794                unreachable_patterns,
1795                reason = "DeFi variants can exist without this crate's defi feature"
1796            )]
1797            other => log_defi_data_dropped(DataRef::from(&other)),
1798        }
1799    }
1800
1801    /// Processes a payload-only reply as a completed request.
1802    ///
1803    /// Payload-only replies do not certify coverage for catalog write-back. Use
1804    /// [`Self::response_envelope`] to report explicit source completion.
1805    pub fn response(&mut self, mut resp: DataResponse) {
1806        resp.trim_to_bounds();
1807        let request = self.inflight_requests.remove(resp.correlation_id());
1808        #[cfg(feature = "streaming")]
1809        if request
1810            .as_ref()
1811            .and_then(request_params)
1812            .is_some_and(|params| params.get_bool(streaming::PARAM_UPDATE_CATALOG) == Some(true))
1813        {
1814            log::warn!(
1815                "Catalog write-back requires an explicit completed response envelope, request={}",
1816                resp.correlation_id()
1817            );
1818        }
1819
1820        #[cfg(not(feature = "streaming"))]
1821        let _ = request;
1822        let envelope = DataResponseEnvelope::new(resp, true, None);
1823        self.process_response_envelope(envelope);
1824    }
1825
1826    /// Completes failed adapter work while preserving any returned rows.
1827    pub fn request_failed(&mut self, failure: DataRequestFailed) {
1828        log::error!(
1829            "Data request {} failed: {}",
1830            failure.request_id,
1831            failure.reason
1832        );
1833
1834        let envelope = if let Some(mut response) = failure.response {
1835            response.trim_to_bounds();
1836            DataResponseEnvelope::new(response, true, None)
1837                .with_correlation_id(failure.request_id)
1838                .with_failure(failure.reason)
1839        } else {
1840            DataResponseEnvelope::terminal(failure.request_id, Some(failure.reason))
1841        };
1842
1843        self.process_response_envelope(envelope);
1844    }
1845
1846    /// Processes an adapter envelope, retaining intermediate request handlers.
1847    pub fn response_envelope(&mut self, mut envelope: DataResponseEnvelope) {
1848        if let Some(response) = envelope.response_mut() {
1849            response.trim_to_bounds();
1850        }
1851
1852        self.process_response_envelope(envelope);
1853    }
1854
1855    fn process_response_envelope(&mut self, envelope: DataResponseEnvelope) {
1856        let request = self.inflight_requests.remove(envelope.correlation_id());
1857        #[cfg(feature = "streaming")]
1858        if envelope.completed()
1859            && envelope.failure_reason().is_none()
1860            && let (Some(request), Some(response)) = (&request, envelope.response())
1861        {
1862            self.update_catalog_from_response(response, request);
1863        }
1864
1865        #[cfg(not(feature = "streaming"))]
1866        let _ = request;
1867
1868        let Some(envelope) = self.handle_request_pipeline_response(envelope) else {
1869            return;
1870        };
1871
1872        if self
1873            .request_pipeline_parent_request_id
1874            .contains_key(envelope.correlation_id())
1875        {
1876            self.process_response_envelope(envelope);
1877            return;
1878        }
1879
1880        if self.handle_continuous_future_child_response(&envelope)
1881            || self.handle_time_range_pipeline_child_response(&envelope)
1882        {
1883            return;
1884        }
1885
1886        let Some(resp) = envelope.response().cloned() else {
1887            self.finish_empty_response(&envelope);
1888            return;
1889        };
1890
1891        self.route_response_payload(resp, &envelope);
1892    }
1893
1894    fn finish_empty_response(&mut self, envelope: &DataResponseEnvelope) {
1895        if let Some(original_id) = self
1896            .parent_join_request_id
1897            .remove(envelope.correlation_id())
1898        {
1899            if let Some(original) = self.pending_join_requests.remove(&original_id) {
1900                for child_id in original.request_ids {
1901                    msgbus::send_response_envelope(&DataResponseEnvelope::terminal(
1902                        child_id,
1903                        envelope.failure_reason().map(str::to_owned),
1904                    ));
1905                }
1906            }
1907
1908            self.process_response_envelope(DataResponseEnvelope::terminal(
1909                original_id,
1910                envelope.failure_reason().map(str::to_owned),
1911            ));
1912            return;
1913        }
1914
1915        self.cleanup_request_bar_aggregators(envelope.correlation_id());
1916        msgbus::send_response_envelope(envelope);
1917    }
1918
1919    fn route_response_payload(&mut self, resp: DataResponse, envelope: &DataResponseEnvelope) {
1920        let resp = rebind_response_correlation(resp, *envelope.correlation_id());
1921        if log::log_enabled!(log::Level::Debug) {
1922            let correlation_id = resp.correlation_id();
1923            match resp.record_count() {
1924                Some(count) => log::debug!(
1925                    "{RECV}{RES} {} correlation_id={correlation_id} records={count}",
1926                    resp.kind(),
1927                ),
1928                None => log::debug!(
1929                    "{RECV}{RES} {} correlation_id={correlation_id}",
1930                    resp.kind(),
1931                ),
1932            }
1933        }
1934        log::trace!("{RECV}{RES} {resp:?}");
1935
1936        self.response_count += 1;
1937
1938        if self
1939            .parent_join_request_id
1940            .contains_key(resp.correlation_id())
1941        {
1942            self.finalize_request_join(resp, envelope.failure_reason().map(str::to_owned));
1943            return;
1944        }
1945
1946        if !self.handle_response_payload(&resp) {
1947            return;
1948        }
1949
1950        self.process_request_bar_aggregation_response(&resp, envelope.completed());
1951
1952        msgbus::send_response_envelope(envelope);
1953    }
1954
1955    fn handle_response_payload(&mut self, resp: &DataResponse) -> bool {
1956        let correlation_id = *resp.correlation_id();
1957
1958        match resp {
1959            DataResponse::Instrument(r) => {
1960                self.handle_instrument_response(r.data.clone());
1961            }
1962            DataResponse::Instruments(r) => {
1963                self.handle_instruments(&r.data);
1964            }
1965            DataResponse::Quotes(r) => {
1966                if !log_if_empty_response(
1967                    &r.data,
1968                    &format_args!("{:?}", r.instrument_ids),
1969                    &correlation_id,
1970                ) {
1971                    self.handle_quotes(&r.data);
1972                }
1973            }
1974            DataResponse::Trades(r) => {
1975                if !log_if_empty_response(
1976                    &r.data,
1977                    &format_args!("{:?}", r.instrument_ids),
1978                    &correlation_id,
1979                ) {
1980                    self.handle_trades(&r.data);
1981                }
1982            }
1983            DataResponse::FundingRates(r) => {
1984                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1985                    self.handle_funding_rates(&r.data);
1986                }
1987            }
1988            DataResponse::Bars(r) => {
1989                if !log_if_empty_response(&r.data, &r.bar_type, &correlation_id) {
1990                    self.handle_bars(&r.data);
1991                }
1992            }
1993            DataResponse::Book(r) => self.handle_book_response(&r.data),
1994            DataResponse::BookDeltas(r) => {
1995                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1996                    self.handle_book_deltas_response(r);
1997                }
1998            }
1999            DataResponse::BookDepth(r) => {
2000                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2001                    self.handle_book_depth_response(r);
2002                }
2003            }
2004            DataResponse::OptionChainReferencePrice(r) => {
2005                self.process_request_bar_aggregation_response(resp, true);
2006                self.handle_option_chain_reference_price_response(&correlation_id, r);
2007                return false;
2008            }
2009            DataResponse::Data(_) => {}
2010        }
2011
2012        true
2013    }
2014
2015    /// Registers a parent request whose response will be rebuilt from `n_components` leg responses.
2016    pub fn new_request_pipeline(&mut self, parent: RequestCommand, n_components: usize) {
2017        let parent_id = *parent.request_id();
2018        self.request_pipeline_n_components
2019            .insert(parent_id, n_components);
2020        self.request_pipeline_parent_request
2021            .insert(parent_id, parent);
2022        self.request_pipeline_responses
2023            .insert(parent_id, Vec::with_capacity(n_components));
2024    }
2025
2026    /// Registers a leg `request_id` as a child of the pipeline keyed by `parent_id`.
2027    ///
2028    /// # Panics
2029    ///
2030    /// Panics if the parent pipeline is not registered.
2031    pub fn register_request_pipeline_leg(&mut self, leg_id: UUID4, parent_id: UUID4) {
2032        let slots = self
2033            .request_pipeline_responses
2034            .get_mut(&parent_id)
2035            .expect("pipeline registered before its legs");
2036        let index = slots.len();
2037        slots.push(None);
2038        self.request_pipeline_parent_request_id
2039            .insert(leg_id, (parent_id, index));
2040    }
2041
2042    fn handle_request_pipeline_response(
2043        &mut self,
2044        envelope: DataResponseEnvelope,
2045    ) -> Option<DataResponseEnvelope> {
2046        let leg_id = *envelope.correlation_id();
2047
2048        let Some(&(parent_id, index)) = self.request_pipeline_parent_request_id.get(&leg_id) else {
2049            return Some(envelope);
2050        };
2051
2052        let completed = envelope.completed();
2053        let slots = self.request_pipeline_responses.get_mut(&parent_id)?;
2054        slots[index].get_or_insert_with(Vec::new).push(envelope);
2055
2056        if !completed {
2057            return None;
2058        }
2059
2060        self.request_pipeline_parent_request_id.remove(&leg_id);
2061        let expected = *self.request_pipeline_n_components.get(&parent_id)?;
2062
2063        if slots
2064            .iter()
2065            .flatten()
2066            .filter(|slot| slot.last().is_some_and(DataResponseEnvelope::completed))
2067            .count()
2068            < expected
2069        {
2070            return None;
2071        }
2072
2073        let slots = self.request_pipeline_responses.remove(&parent_id)?;
2074        self.request_pipeline_n_components.remove(&parent_id);
2075        let parent = self.request_pipeline_parent_request.remove(&parent_id);
2076        let failures = slots
2077            .iter()
2078            .flatten()
2079            .flatten()
2080            .filter_map(|value| value.failure_reason())
2081            .collect::<Vec<_>>();
2082        let failure_reason = (!failures.is_empty()).then(|| failures.join("; "));
2083        let legs = slots
2084            .into_iter()
2085            .flatten()
2086            .flatten()
2087            .filter_map(|value| value.response().cloned())
2088            .collect::<Vec<_>>();
2089
2090        if legs.is_empty() {
2091            return Some(DataResponseEnvelope::terminal(parent_id, failure_reason));
2092        }
2093
2094        let (parent_start, parent_end) = parent_request_window(parent.as_ref());
2095        if self.emit_partitioned_book_responses(parent_id, parent.as_ref(), &legs) {
2096            return Some(DataResponseEnvelope::terminal(parent_id, failure_reason));
2097        }
2098
2099        let rebuilt = rebuild_pipeline_response(parent_id, parent.as_ref(), legs);
2100
2101        let Some(mut rebuilt) = rebuilt else {
2102            let reason = failure_reason
2103                .unwrap_or_else(|| "Request pipeline could not rebuild its response".to_owned());
2104
2105            return Some(DataResponseEnvelope::terminal(parent_id, Some(reason)));
2106        };
2107
2108        if let DataResponse::BookDeltas(r) = &mut rebuilt {
2109            self.book_deltas_snapshot_replay(r);
2110        }
2111
2112        // Client definitions can have a newer ts_init than the requested history
2113        if !matches!(parent, Some(RequestCommand::Instrument(_)))
2114            && (parent_start.is_some() || parent_end.is_some())
2115        {
2116            rebuilt.trim_to_bounds();
2117        }
2118
2119        let result = DataResponseEnvelope::new(rebuilt, true, None);
2120        Some(if let Some(reason) = failure_reason {
2121            result.with_failure(reason)
2122        } else {
2123            result
2124        })
2125    }
2126
2127    fn emit_partitioned_book_responses(
2128        &mut self,
2129        parent_id: UUID4,
2130        parent: Option<&RequestCommand>,
2131        legs: &[DataResponse],
2132    ) -> bool {
2133        if !matches!(parent, Some(RequestCommand::BookDeltas(_))) {
2134            return false;
2135        }
2136
2137        let mut ids = legs.iter().filter_map(|r| match r {
2138            DataResponse::BookDeltas(r) => Some(r.instrument_id),
2139            _ => None,
2140        });
2141
2142        let Some(first) = ids.next() else {
2143            return false;
2144        };
2145
2146        if !ids.any(|id| !same_instrument(id, first)) {
2147            return false;
2148        }
2149
2150        let (start, end) = parent_request_window(parent);
2151
2152        for response in legs {
2153            let child_id = *response.correlation_id();
2154            let mut response = rebind_response_correlation(response.clone(), parent_id);
2155            if let DataResponse::BookDeltas(r) = &mut response {
2156                r.start = start.or(r.start);
2157                r.end = end.or(r.end);
2158                self.book_deltas_snapshot_replay(r);
2159            }
2160
2161            response.trim_to_bounds();
2162            self.process_response_envelope(DataResponseEnvelope::new(
2163                response,
2164                false,
2165                Some(child_id),
2166            ));
2167        }
2168
2169        true
2170    }
2171
2172    fn handle_request_join(&mut self, req: RequestJoin) -> anyhow::Result<()> {
2173        if has_time_range_pipeline_params(req.params.as_ref()) {
2174            return self.execute_time_range_pipeline_request(RequestCommand::Join(req));
2175        }
2176
2177        let now_ns = self.clock.borrow().timestamp_ns();
2178        let now_dt = now_ns.to_datetime_utc();
2179        let zero = jiff::Timestamp::UNIX_EPOCH;
2180        let start = req.start.unwrap_or(zero).min(now_dt);
2181        let end = req.end.unwrap_or(now_dt).min(now_dt);
2182        let dated = req.with_dates(Some(start), Some(end), now_ns);
2183
2184        let original_id = req.request_id;
2185        let dated_id = dated.request_id;
2186
2187        self.pending_join_requests.insert(original_id, req);
2188        self.parent_join_request_id.insert(dated_id, original_id);
2189
2190        let leg_ids: Vec<UUID4> = dated.request_ids.clone();
2191        self.new_request_pipeline(RequestCommand::Join(dated), leg_ids.len());
2192        for leg_id in leg_ids {
2193            self.register_request_pipeline_leg(leg_id, dated_id);
2194        }
2195
2196        Ok(())
2197    }
2198
2199    fn finalize_request_join(&mut self, resp: DataResponse, failure_reason: Option<String>) {
2200        let dated_id = *resp.correlation_id();
2201        let Some(original_id) = self.parent_join_request_id.remove(&dated_id) else {
2202            log::error!("parent_join_request_id missing for dated correlation {dated_id}");
2203            return;
2204        };
2205
2206        let Some(original) = self.pending_join_requests.remove(&original_id) else {
2207            log::error!("pending_join_requests missing for original {original_id}");
2208            return;
2209        };
2210
2211        for leg_request_id in &original.request_ids {
2212            let terminal = DataResponseEnvelope::terminal(*leg_request_id, failure_reason.clone());
2213            msgbus::send_response_envelope(&terminal);
2214        }
2215
2216        // Route the final join response through the normal response path so
2217        // bounds-trim against the parent window runs and the per-variant
2218        // handlers (cache writes, request bar aggregators) fire. The pipeline
2219        // and join staging maps for this request have already been popped, so
2220        // the recursive call cannot re-enter either gate.
2221        let final_resp = rebind_response_correlation(resp, original_id);
2222        let envelope = DataResponseEnvelope::new(final_resp, true, None);
2223        self.process_response_envelope(if let Some(reason) = failure_reason {
2224            envelope.with_failure(reason)
2225        } else {
2226            envelope
2227        });
2228    }
2229}
2230
2231fn register_external_streaming_type(cmd: &SubscribeCommand) {
2232    if let Some(payload_type) = streaming_payload_type(cmd) {
2233        msgbus::get_message_bus()
2234            .borrow_mut()
2235            .add_streaming_type(payload_type);
2236    }
2237}
2238
2239fn publish_external_data_command<T>(client_id: ClientId, command: &T)
2240where
2241    T: Any,
2242{
2243    let topic = format!("commands.data.{client_id}");
2244    msgbus::publish_any(topic.into(), command);
2245}
2246
2247#[rustfmt::skip]
2248fn streaming_payload_type(cmd: &SubscribeCommand) -> Option<BusPayloadType> {
2249    match cmd {
2250        SubscribeCommand::Data(cmd) => Some(BusPayloadType::Custom(Ustr::from(
2251            cmd.data_type.type_name(),
2252        ))),
2253        SubscribeCommand::Instrument(_) | SubscribeCommand::Instruments(_) => Some(BusPayloadType::Instrument),
2254        SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookSnapshots(_) => Some(BusPayloadType::OrderBookDeltas),
2255        SubscribeCommand::BookDepth(_) => Some(BusPayloadType::OrderBookDepth),
2256        SubscribeCommand::Quotes(_) => Some(BusPayloadType::QuoteTick),
2257        SubscribeCommand::Trades(_) => Some(BusPayloadType::TradeTick),
2258        SubscribeCommand::Bars(_) => Some(BusPayloadType::Bar),
2259        SubscribeCommand::MarkPrices(_) => Some(BusPayloadType::MarkPriceUpdate),
2260        SubscribeCommand::IndexPrices(_) => Some(BusPayloadType::IndexPriceUpdate),
2261        SubscribeCommand::FundingRates(_) => Some(BusPayloadType::FundingRateUpdate),
2262        SubscribeCommand::OptionGreeks(_) => Some(BusPayloadType::OptionGreeks),
2263        SubscribeCommand::InstrumentStatus(_)
2264        | SubscribeCommand::InstrumentClose(_)
2265        | SubscribeCommand::OptionChain(_) => None,
2266    }
2267}
2268
2269#[cfg(not(feature = "defi"))]
2270fn log_defi_data_dropped(data: DataRef<'_>) {
2271    log::error!("Cannot process data {data:?}, nautilus-data built without its `defi` feature");
2272}