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        DataResponse, FundingRatesResponse, OptionChainReferencePriceResponse, PARAMS_IS_PARENT,
90        QuotesResponse, RequestBars, RequestCommand, RequestJoin, RequestOptionChainReferencePrice,
91        RequestQuotes, RequestTrades, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth,
92        SubscribeCommand, SubscribeOptionChain, SubscribeOptionGreeks, SubscribeQuotes,
93        SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
94        UnsubscribeBookDepth, UnsubscribeBookSnapshots, UnsubscribeCommand,
95        UnsubscribeInstrumentStatus, UnsubscribeOptionChain, UnsubscribeOptionGreeks,
96        UnsubscribeQuotes, UnsubscribeTrades, is_parent_subscription,
97    },
98    msgbus::{
99        self, BusPayloadType, ShareableMessageHandler, TypedHandler, TypedIntoHandler,
100        switchboard::{self, MessagingSwitchboard},
101    },
102    runner::get_data_cmd_sender,
103    timer::{TimeEvent, TimeEventCallback},
104};
105use nautilus_core::{
106    DurationNanos, Params, UUID4, UnixNanos, WeakCell,
107    correctness::{FAILED, check_key_in_map, check_key_not_in_map, check_predicate_true},
108    datetime::NANOSECONDS_IN_DAY,
109};
110#[cfg(feature = "defi")]
111use nautilus_model::defi::DefiData;
112use nautilus_model::{
113    data::{
114        Bar, BarType, CustomData, Data, DataRef, DataType, FundingRateUpdate, HasTsInit,
115        IndexPriceUpdate, InstrumentClose, InstrumentStatus, MarkPriceUpdate, OrderBookDelta,
116        OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
117        option_chain::{OptionGreeks, StrikeRange},
118    },
119    enums::{
120        AggregationSource, BarAggregation, BookType, InstrumentClass, MarketStatusAction,
121        PriceType, RecordFlag,
122    },
123    identifiers::{
124        ClientId, GENERIC_SPREAD_ID_SEPARATOR, InstrumentId, OptionSeriesId, Venue,
125        parse_generic_spread_id_legs,
126    },
127    instruments::{Instrument, InstrumentAny, SyntheticInstrument},
128    orderbook::OrderBook,
129    types::{Price, Quantity},
130};
131use option_chain::{
132    OptionChainBootstrapper, OptionChainGreeksBootstrap, PendingOptionChainRequest,
133};
134use requests::{
135    ContinuousFutureRequest, ContinuousFutureRequestState, ContinuousFutureSegment,
136    ContinuousFutureSource, RequestBarAggregation, continuous_future_parent_request_id,
137    continuous_future_request_from_bars, continuous_future_subscription_from_bars,
138    has_continuous_future_params, request_bar_aggregation_from_params, request_params,
139    response_params,
140};
141use responses::{
142    empty_response_like, log_if_empty_response, parent_request_window, rebind_response_correlation,
143    rebuild_pipeline_response,
144};
145use spread_quote::SpreadQuoteState;
146#[cfg(feature = "streaming")]
147use streaming::CatalogMap;
148use time_range::{
149    TimeRangePipelineState, has_time_range_pipeline_params, is_time_range_pipeline_variant,
150};
151use ustr::Ustr;
152
153#[cfg(feature = "defi")]
154#[allow(unused_imports)] // Brings DeFi impl blocks into scope
155use crate::defi::engine as _;
156#[cfg(feature = "defi")]
157use crate::engine::pool::PoolUpdater;
158use crate::{
159    aggregation::{
160        BarAggregator, RenkoBarAggregator, SpreadQuoteAggregator, TickBarAggregator,
161        TickImbalanceBarAggregator, TickRunsBarAggregator, TimeBarAggregator, ValueBarAggregator,
162        ValueImbalanceBarAggregator, ValueRunsBarAggregator, VolumeBarAggregator,
163        VolumeImbalanceBarAggregator, VolumeRunsBarAggregator,
164    },
165    client::DataClientAdapter,
166    option_chains::OptionChainManager,
167    subscription::{SubscriptionKey, SubscriptionRegistry, SubscriptionRelease},
168};
169
170/// Provides a high-performance `DataEngine` for all environments.
171#[derive(Debug)]
172pub struct DataEngine {
173    clock: Rc<RefCell<dyn Clock>>,
174    cache: Rc<RefCell<Cache>>,
175    config: DataEngineConfig,
176    msgbus_priority: u32,
177    external_clients: AHashSet<ClientId>,
178    subscriptions_external: SubscriptionRegistry<(ClientId, SubscriptionKey), SubscribeCommand>,
179    clients: IndexMap<ClientId, DataClientAdapter>,
180    default_client_id: Option<ClientId>,
181    routing_map: IndexMap<Venue, ClientId>,
182    book_intervals: AHashMap<NonZeroUsize, BookSnapshotInfos>,
183    book_snapshot_counts: IndexMap<BookSnapshotKey, usize>,
184    book_snapshot_sources: AHashMap<InstrumentId, BookSnapshotSource>,
185    book_deltas_counts: IndexMap<BookDeltasKey, usize>,
186    book_depth_counts: IndexMap<BookDeltasKey, usize>,
187    book_updaters: AHashMap<InstrumentId, Rc<BookUpdater>>,
188    book_subscriptions: AHashMap<InstrumentId, BookSubscription>,
189    book_subscription_owners: AHashMap<InstrumentId, Vec<Rc<BookSubscriptionOwner>>>,
190    book_snapshotters: AHashMap<NonZeroUsize, Rc<BookSnapshotter>>,
191    bar_aggregators: IndexMap<BarAggregatorKey, Rc<RefCell<Box<dyn BarAggregator>>>>,
192    bar_aggregator_handlers: AHashMap<BarAggregatorKey, Vec<BarAggregatorSubscription>>,
193    subscriptions_bar_aggregation: AHashMap<BarType, BarAggregationSubscription>,
194    request_bar_aggregations: AHashMap<UUID4, RequestBarAggregation>,
195    request_pipeline_parent_request: AHashMap<UUID4, RequestCommand>,
196    request_pipeline_n_components: AHashMap<UUID4, usize>,
197    request_pipeline_parent_request_id: AHashMap<UUID4, UUID4>,
198    request_pipeline_responses: AHashMap<UUID4, Vec<DataResponse>>,
199    time_range_pipeline_requests: AHashMap<UUID4, TimeRangePipelineState>,
200    time_range_pipeline_parent_request_id: AHashMap<UUID4, UUID4>,
201    parent_join_request_id: AHashMap<UUID4, UUID4>,
202    pending_join_requests: AHashMap<UUID4, RequestJoin>,
203    continuous_future_requests: AHashMap<UUID4, ContinuousFutureRequestState>,
204    continuous_future_subscriptions: AHashMap<BarType, ContinuousFutureSubscriptionState>,
205    continuous_future_roller: Option<Rc<ContinuousFutureRoller>>,
206    spread_quote_states: AHashMap<InstrumentId, SpreadQuoteState>,
207    option_chain_managers: AHashMap<OptionSeriesId, Rc<RefCell<OptionChainManager>>>,
208    option_chain_instrument_index: AHashMap<InstrumentId, OptionSeriesId>,
209    deferred_cmd_queue: DeferredCommandQueue,
210    option_chain_bootstrapper: Option<Rc<OptionChainBootstrapper>>,
211    pending_option_chain_requests: AHashMap<UUID4, PendingOptionChainRequest>,
212    option_chain_greeks_bootstraps: AHashMap<OptionSeriesId, OptionChainGreeksBootstrap>,
213    synthetic_quote_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
214    synthetic_trade_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
215    subscribed_synthetic_quotes: AHashMap<InstrumentId, usize>,
216    subscribed_synthetic_trades: AHashMap<InstrumentId, usize>,
217    buffered_deltas_map: AHashMap<InstrumentId, OrderBookDeltas>,
218    deltas_frame: Vec<OrderBookDelta>,
219    command_count: u64,
220    data_count: u64,
221    request_count: u64,
222    response_count: u64,
223    #[cfg(feature = "streaming")]
224    catalogs: CatalogMap,
225    #[cfg(feature = "defi")]
226    pub(crate) pool_updaters: AHashMap<InstrumentId, Rc<PoolUpdater>>,
227    #[cfg(feature = "defi")]
228    pub(crate) pool_snapshot_pending: AHashMap<InstrumentId, UUID4>,
229    #[cfg(feature = "defi")]
230    pub(crate) pool_event_buffers: AHashMap<InstrumentId, Vec<DefiData>>,
231}
232
233impl DataEngine {
234    /// Creates a new [`DataEngine`] instance.
235    #[must_use]
236    pub fn new(
237        clock: Rc<RefCell<dyn Clock>>,
238        cache: Rc<RefCell<Cache>>,
239        config: Option<DataEngineConfig>,
240    ) -> Self {
241        let config = config.unwrap_or_default();
242        let external_clients: AHashSet<ClientId> = config
243            .external_clients
244            .clone()
245            .unwrap_or_default()
246            .into_iter()
247            .collect();
248
249        Self {
250            clock,
251            cache,
252            config,
253            msgbus_priority: 10, // High-priority for built-in component
254            external_clients,
255            subscriptions_external: SubscriptionRegistry::default(),
256            clients: IndexMap::new(),
257            default_client_id: None,
258            routing_map: IndexMap::new(),
259            book_intervals: AHashMap::new(),
260            book_snapshot_counts: IndexMap::new(),
261            book_snapshot_sources: AHashMap::new(),
262            book_deltas_counts: IndexMap::new(),
263            book_depth_counts: IndexMap::new(),
264            book_updaters: AHashMap::new(),
265            book_subscriptions: AHashMap::new(),
266            book_subscription_owners: AHashMap::new(),
267            book_snapshotters: AHashMap::new(),
268            bar_aggregators: IndexMap::new(),
269            bar_aggregator_handlers: AHashMap::new(),
270            subscriptions_bar_aggregation: AHashMap::new(),
271            request_bar_aggregations: AHashMap::new(),
272            request_pipeline_parent_request: AHashMap::new(),
273            request_pipeline_n_components: AHashMap::new(),
274            request_pipeline_parent_request_id: AHashMap::new(),
275            request_pipeline_responses: AHashMap::new(),
276            time_range_pipeline_requests: AHashMap::new(),
277            time_range_pipeline_parent_request_id: AHashMap::new(),
278            parent_join_request_id: AHashMap::new(),
279            pending_join_requests: AHashMap::new(),
280            continuous_future_requests: AHashMap::new(),
281            continuous_future_subscriptions: AHashMap::new(),
282            continuous_future_roller: None,
283            spread_quote_states: AHashMap::new(),
284            option_chain_managers: AHashMap::new(),
285            option_chain_instrument_index: AHashMap::new(),
286            deferred_cmd_queue: Rc::new(RefCell::new(VecDeque::new())),
287            option_chain_bootstrapper: None,
288            pending_option_chain_requests: AHashMap::new(),
289            option_chain_greeks_bootstraps: AHashMap::new(),
290            synthetic_quote_feeds: AHashMap::new(),
291            synthetic_trade_feeds: AHashMap::new(),
292            subscribed_synthetic_quotes: AHashMap::new(),
293            subscribed_synthetic_trades: AHashMap::new(),
294            buffered_deltas_map: AHashMap::new(),
295            deltas_frame: Vec::new(),
296            command_count: 0,
297            data_count: 0,
298            request_count: 0,
299            response_count: 0,
300            #[cfg(feature = "streaming")]
301            catalogs: CatalogMap::new(),
302            #[cfg(feature = "defi")]
303            pool_updaters: AHashMap::new(),
304            #[cfg(feature = "defi")]
305            pool_snapshot_pending: AHashMap::new(),
306            #[cfg(feature = "defi")]
307            pool_event_buffers: AHashMap::new(),
308        }
309    }
310
311    /// Returns a reference to the clock.
312    #[must_use]
313    pub fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
314        &self.clock
315    }
316
317    /// Returns a reference to the cache.
318    #[must_use]
319    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
320        &self.cache
321    }
322
323    /// Returns a reference to the configuration.
324    #[must_use]
325    pub const fn config(&self) -> &DataEngineConfig {
326        &self.config
327    }
328
329    #[cfg(feature = "defi")]
330    #[must_use]
331    pub(crate) const fn msgbus_priority(&self) -> u32 {
332        self.msgbus_priority
333    }
334
335    /// Registers all message bus handlers for the data engine.
336    pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
337        let weak = WeakCell::from(Rc::downgrade(engine));
338        engine.borrow_mut().continuous_future_roller =
339            Some(Rc::new(ContinuousFutureRoller::new(engine)));
340        engine.borrow_mut().option_chain_bootstrapper =
341            Some(Rc::new(OptionChainBootstrapper::new(engine)));
342
343        let weak1 = weak.clone();
344        msgbus::register_data_command_endpoint(
345            MessagingSwitchboard::data_engine_execute(),
346            TypedIntoHandler::from(move |cmd: DataCommand| {
347                if let Some(rc) = weak1.upgrade() {
348                    rc.borrow_mut().execute(cmd);
349                }
350            }),
351        );
352
353        msgbus::register_data_command_endpoint(
354            MessagingSwitchboard::data_engine_queue_execute(),
355            TypedIntoHandler::from(move |cmd: DataCommand| {
356                get_data_cmd_sender().execute(cmd);
357            }),
358        );
359
360        // Register process handler (polymorphic - uses Any)
361        let weak2 = weak.clone();
362        msgbus::register_any(
363            MessagingSwitchboard::data_engine_process(),
364            ShareableMessageHandler::from_any(move |data: &dyn Any| {
365                if let Some(rc) = weak2.upgrade() {
366                    rc.borrow_mut().process(data);
367                }
368            }),
369        );
370
371        // Register process_data handler (typed - takes ownership)
372        let weak3 = weak.clone();
373        msgbus::register_data_endpoint(
374            MessagingSwitchboard::data_engine_process_data(),
375            TypedIntoHandler::from(move |data: Data| {
376                if let Some(rc) = weak3.upgrade() {
377                    rc.borrow_mut().process_data(data);
378                }
379            }),
380        );
381
382        // Register process_defi_data handler (typed - takes ownership)
383        #[cfg(feature = "defi")]
384        {
385            let weak4 = weak.clone();
386            msgbus::register_defi_data_endpoint(
387                MessagingSwitchboard::data_engine_process_defi_data(),
388                TypedIntoHandler::from(move |data: DefiData| {
389                    if let Some(rc) = weak4.upgrade() {
390                        rc.borrow_mut().process_defi_data(data);
391                    }
392                }),
393            );
394        }
395
396        let weak5 = weak;
397        msgbus::register_data_response_endpoint(
398            MessagingSwitchboard::data_engine_response(),
399            TypedIntoHandler::from(move |resp: DataResponse| {
400                if let Some(rc) = weak5.upgrade() {
401                    rc.borrow_mut().response(resp);
402                }
403            }),
404        );
405    }
406
407    /// Returns the total count of data commands received by the engine.
408    #[must_use]
409    pub const fn command_count(&self) -> u64 {
410        self.command_count
411    }
412
413    /// Returns the total count of data stream objects received by the engine.
414    #[must_use]
415    pub const fn data_count(&self) -> u64 {
416        self.data_count
417    }
418
419    #[cfg(feature = "defi")]
420    pub(crate) const fn increment_data_count(&mut self) {
421        self.data_count += 1;
422    }
423
424    /// Returns the total count of data requests received by the engine.
425    #[must_use]
426    pub const fn request_count(&self) -> u64 {
427        self.request_count
428    }
429
430    /// Returns the total count of data responses received by the engine.
431    #[must_use]
432    pub const fn response_count(&self) -> u64 {
433        self.response_count
434    }
435
436    /// Returns the number of request pipelines awaiting leg responses.
437    #[must_use]
438    pub fn request_pipeline_count(&self) -> usize {
439        self.request_pipeline_parent_request.len()
440    }
441
442    /// Returns the number of time-range pipelines awaiting child responses.
443    #[must_use]
444    pub fn time_range_pipeline_count(&self) -> usize {
445        self.time_range_pipeline_requests.len()
446    }
447
448    /// Returns the number of `RequestJoin` originals awaiting finalization.
449    #[must_use]
450    pub fn pending_join_request_count(&self) -> usize {
451        self.pending_join_requests.len()
452    }
453
454    /// Starts all registered data clients and re-arms bar aggregator timers.
455    pub fn start(&mut self) {
456        for client in self.get_clients_mut() {
457            if client.start().is_err() {
458                log::error!("Failed to start data client {}", client.client_id());
459            }
460        }
461
462        for ((_, request_id), aggregator) in &self.bar_aggregators {
463            // Request-scoped or historical aggregators run on private clocks;
464            // re-arming them here would perturb an in-flight request's timer state
465            let is_subscription = request_id.is_none() && !aggregator.borrow().is_historical();
466            if is_subscription && aggregator.borrow().bar_type().spec().is_time_aggregated() {
467                aggregator
468                    .borrow_mut()
469                    .start_timer(Some(Rc::clone(aggregator)));
470            }
471        }
472
473        for state in self.spread_quote_states.values() {
474            state
475                .aggregator
476                .borrow_mut()
477                .start_timer(Some(Rc::clone(&state.aggregator)));
478        }
479    }
480
481    /// Stops all registered data clients and bar aggregator timers.
482    pub fn stop(&mut self) {
483        for client in self.get_clients_mut() {
484            if client.stop().is_err() {
485                log::error!("Failed to stop data client {}", client.client_id());
486            }
487        }
488
489        for aggregator in self.bar_aggregators.values() {
490            aggregator.borrow_mut().stop();
491        }
492
493        for state in self.spread_quote_states.values() {
494            state.aggregator.borrow_mut().stop_timer();
495        }
496    }
497
498    /// Resets all registered data clients and clears engine state.
499    pub fn reset(&mut self) {
500        for client in self.get_clients_mut() {
501            match client.reset() {
502                Ok(()) => client.clear_subscription_state(),
503                Err(_) => log::error!("Failed to reset data client {}", client.client_id()),
504            }
505        }
506
507        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
508        for (bar_type, request_id) in keys {
509            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
510                log::error!("Error stopping bar aggregator during reset for {bar_type}: {e}");
511            }
512        }
513        self.subscriptions_bar_aggregation.clear();
514
515        self.request_bar_aggregations.clear();
516        self.request_pipeline_parent_request.clear();
517        self.request_pipeline_n_components.clear();
518        self.request_pipeline_parent_request_id.clear();
519        self.request_pipeline_responses.clear();
520        self.time_range_pipeline_requests.clear();
521        self.time_range_pipeline_parent_request_id.clear();
522        self.parent_join_request_id.clear();
523        self.pending_join_requests.clear();
524        self.continuous_future_requests.clear();
525
526        for state in self.continuous_future_subscriptions.values_mut() {
527            if let Some(name) = state.timer_name.take() {
528                self.clock.borrow_mut().cancel_timer(&name);
529            }
530        }
531        self.continuous_future_subscriptions.clear();
532
533        let spread_ids: Vec<InstrumentId> = self.spread_quote_states.keys().copied().collect();
534        for spread_id in spread_ids {
535            self.stop_spread_quote_aggregation(spread_id);
536        }
537
538        // Tear down option chain managers to unregister their msgbus handlers
539        let managers: Vec<_> = self.option_chain_managers.drain().collect();
540        for (_, manager) in managers {
541            manager.borrow_mut().teardown(&self.clock);
542        }
543
544        self.option_chain_instrument_index.clear();
545        self.cancel_pending_option_chain_requests(None);
546        self.clear_option_chain_greeks_bootstraps();
547
548        // Unsubscribe BookUpdaters before dropping; otherwise the typed router
549        // keeps dispatching to abandoned updaters. `book_updaters` is keyed by
550        // per-underlying id, so the literal per-underlying topic is the same
551        // string the subscribe path used.
552        let book_updaters: Vec<(InstrumentId, Rc<BookUpdater>)> =
553            self.book_updaters.drain().collect();
554        for (instrument_id, updater) in book_updaters {
555            let deltas_topic = switchboard::get_book_deltas_topic(instrument_id);
556            let depth_topic = switchboard::get_book_depth_topic(instrument_id);
557            let deltas_handler: TypedHandler<OrderBookDeltas> =
558                TypedHandler::new(Rc::clone(&updater));
559            let depth_handler: TypedHandler<OrderBookDepth> = TypedHandler::new(updater);
560            msgbus::unsubscribe_book_deltas(deltas_topic.into(), &deltas_handler);
561            msgbus::unsubscribe_book_depth(depth_topic.into(), &depth_handler);
562        }
563
564        self.book_subscriptions.clear();
565        self.book_subscription_owners.clear();
566
567        self.book_deltas_counts.clear();
568        self.book_depth_counts.clear();
569        self.book_intervals.clear();
570        self.book_snapshot_counts.clear();
571        self.book_snapshot_sources.clear();
572        self.book_snapshotters.clear();
573        self.buffered_deltas_map.clear();
574        self.deltas_frame.clear();
575
576        self.synthetic_quote_feeds.clear();
577        self.synthetic_trade_feeds.clear();
578        self.subscribed_synthetic_quotes.clear();
579        self.subscribed_synthetic_trades.clear();
580        self.subscriptions_external.clear();
581
582        self.deferred_cmd_queue.borrow_mut().clear();
583
584        #[cfg(feature = "defi")]
585        self.clear_pool_updaters();
586
587        self.clock.borrow_mut().cancel_timers();
588
589        self.command_count = 0;
590        self.data_count = 0;
591        self.request_count = 0;
592        self.response_count = 0;
593    }
594
595    /// Disposes the engine, stopping all clients and canceling any timers.
596    pub fn dispose(&mut self) {
597        for client in self.get_clients_mut() {
598            if client.dispose().is_err() {
599                log::error!("Failed to dispose data client {}", client.client_id());
600            }
601        }
602
603        self.clear_option_chain_greeks_bootstraps();
604
605        // Continuous-future source handlers live outside bar_aggregator_handlers,
606        // so release them before dropping the aggregators
607        let mut cf_sources = Vec::new();
608
609        for state in self.continuous_future_subscriptions.values_mut() {
610            if let Some(name) = state.timer_name.take() {
611                self.clock.borrow_mut().cancel_timer(&name);
612            }
613
614            if let Some(subscription) = state.active_source_subscription.take() {
615                cf_sources.push((state.target_bar_type, subscription));
616            }
617        }
618
619        for (target_bar_type, subscription) in cf_sources {
620            self.unsubscribe_continuous_future_source(target_bar_type, subscription);
621        }
622        self.continuous_future_subscriptions.clear();
623
624        // Unsubscribe aggregator msgbus handlers so the typed routers don't keep
625        // entries pointing at dropped aggregators
626        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
627        for (bar_type, request_id) in keys {
628            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
629                log::error!("Error stopping bar aggregator during dispose for {bar_type}: {e}");
630            }
631        }
632
633        self.subscriptions_external.clear();
634
635        self.clock.borrow_mut().cancel_timers();
636    }
637
638    /// Connects all registered data clients concurrently.
639    ///
640    /// Connection failures are logged but do not prevent the node from running.
641    /// Logs identify the client but omit provider error text, which can contain credentials.
642    pub async fn connect(&mut self) {
643        let futures: Vec<_> = self
644            .get_clients_mut()
645            .into_iter()
646            .map(|client| async move {
647                let client_id = client.client_id();
648                (client_id, client.connect().await)
649            })
650            .collect();
651
652        let results = join_all(futures).await;
653
654        for (client_id, result) in results {
655            if result.is_err() {
656                log::error!("Failed to connect data client {client_id}: connection failed");
657            }
658        }
659    }
660
661    /// Disconnects all registered data clients concurrently.
662    ///
663    /// # Errors
664    ///
665    /// Returns an error if any client fails to disconnect.
666    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
667        let futures: Vec<_> = self
668            .get_clients_mut()
669            .into_iter()
670            .map(|client| async move {
671                let client_id = client.client_id();
672                (client_id, client.disconnect().await)
673            })
674            .collect();
675
676        let results = join_all(futures).await;
677
678        // A closed session cannot answer, so discard its pending bootstraps
679        #[cfg(feature = "defi")]
680        self.abandon_pool_snapshots();
681
682        let failed_clients: Vec<_> = results
683            .into_iter()
684            .filter_map(|(client_id, result)| result.err().map(|_| client_id.to_string()))
685            .collect();
686
687        if failed_clients.is_empty() {
688            Ok(())
689        } else {
690            anyhow::bail!(
691                "Failed to disconnect data clients: {}",
692                failed_clients.join(", ")
693            )
694        }
695    }
696
697    /// Returns `true` if all registered data clients are currently connected.
698    #[must_use]
699    pub fn check_connected(&self) -> bool {
700        self.get_clients()
701            .iter()
702            .all(|client| client.is_connected())
703    }
704
705    /// Returns `true` if all registered data clients are currently disconnected.
706    #[must_use]
707    pub fn check_disconnected(&self) -> bool {
708        self.get_clients()
709            .iter()
710            .all(|client| !client.is_connected())
711    }
712
713    /// Returns all custom data types currently subscribed across all clients.
714    #[must_use]
715    pub fn subscribed_custom_data(&self) -> Vec<DataType> {
716        self.collect_subscriptions(|client| &client.subscriptions_custom)
717    }
718
719    /// Returns all instrument IDs currently subscribed across all clients.
720    #[must_use]
721    pub fn subscribed_instruments(&self) -> Vec<InstrumentId> {
722        self.collect_subscriptions(|client| &client.subscriptions_instrument)
723    }
724
725    /// Returns all instrument IDs for which quote subscriptions exist.
726    #[must_use]
727    pub fn subscribed_quotes(&self) -> Vec<InstrumentId> {
728        self.collect_subscriptions(|client| &client.subscriptions_quotes)
729    }
730
731    /// Returns all instrument IDs for which trade subscriptions exist.
732    #[must_use]
733    pub fn subscribed_trades(&self) -> Vec<InstrumentId> {
734        self.collect_subscriptions(|client| &client.subscriptions_trades)
735    }
736
737    /// Returns all bar types currently subscribed across all clients,
738    /// including internally aggregated subscriptions (v1 parity).
739    #[must_use]
740    pub fn subscribed_bars(&self) -> Vec<BarType> {
741        let mut subscribed = self.collect_subscriptions(|client| &client.subscriptions_bars);
742        subscribed.extend(
743            self.bar_aggregators
744                .keys()
745                .filter(|(_, request_id)| request_id.is_none())
746                .map(|(bar_type, _)| *bar_type),
747        );
748        subscribed
749    }
750
751    /// Returns all instrument IDs for which mark price subscriptions exist.
752    #[must_use]
753    pub fn subscribed_mark_prices(&self) -> Vec<InstrumentId> {
754        self.collect_subscriptions(|client| &client.subscriptions_mark_prices)
755    }
756
757    /// Returns all instrument IDs for which index price subscriptions exist.
758    #[must_use]
759    pub fn subscribed_index_prices(&self) -> Vec<InstrumentId> {
760        self.collect_subscriptions(|client| &client.subscriptions_index_prices)
761    }
762
763    /// Returns all instrument IDs for which funding rate subscriptions exist.
764    #[must_use]
765    pub fn subscribed_funding_rates(&self) -> Vec<InstrumentId> {
766        self.collect_subscriptions(|client| &client.subscriptions_funding_rates)
767    }
768
769    /// Returns all instrument IDs for which status subscriptions exist.
770    #[must_use]
771    pub fn subscribed_instrument_status(&self) -> Vec<InstrumentId> {
772        self.collect_subscriptions(|client| &client.subscriptions_instrument_status)
773    }
774
775    /// Returns all instrument IDs for which instrument close subscriptions exist.
776    #[must_use]
777    pub fn subscribed_instrument_close(&self) -> Vec<InstrumentId> {
778        self.collect_subscriptions(|client| &client.subscriptions_instrument_close)
779    }
780
781    /// Executes a `DataCommand` by delegating to subscribe, unsubscribe, or request handlers.
782    ///
783    /// This is the final synchronous dispatch point for data commands. Runtime command producers
784    /// should send to `DataEngine.queue_execute`, which lets the runner sequence command execution
785    /// before this method runs. The engine also calls this method for child commands generated while
786    /// processing a parent command, where immediate in-engine ordering matters.
787    ///
788    /// Errors during execution are logged.
789    pub fn execute(&mut self, cmd: DataCommand) {
790        match &cmd {
791            DataCommand::Subscribe(_) | DataCommand::Unsubscribe(_) => self.command_count += 1,
792            DataCommand::Request(_) => self.request_count += 1,
793            #[cfg(feature = "defi")]
794            DataCommand::DefiRequest(_) => self.request_count += 1,
795            #[cfg(feature = "defi")]
796            DataCommand::DefiSubscribe(_) | DataCommand::DefiUnsubscribe(_) => {
797                self.command_count += 1;
798            }
799            _ => {}
800        }
801
802        let command_context = match &cmd {
803            DataCommand::Subscribe(c) => (
804                "Subscribe",
805                c.client_id().copied(),
806                c.venue().copied(),
807                Some(c.command_id()),
808            ),
809            DataCommand::Unsubscribe(c) => (
810                "Unsubscribe",
811                c.client_id().copied(),
812                c.venue().copied(),
813                Some(c.command_id()),
814            ),
815            DataCommand::Request(c) => (
816                "Request",
817                c.client_id().copied(),
818                c.venue().copied(),
819                Some(*c.request_id()),
820            ),
821            #[cfg(feature = "defi")]
822            DataCommand::DefiRequest(c) => (
823                "DefiRequest",
824                c.client_id().copied(),
825                c.venue().copied(),
826                Some(*c.request_id()),
827            ),
828            #[cfg(feature = "defi")]
829            DataCommand::DefiSubscribe(c) => (
830                "DefiSubscribe",
831                c.client_id().copied(),
832                c.venue().copied(),
833                Some(c.command_id()),
834            ),
835            #[cfg(feature = "defi")]
836            DataCommand::DefiUnsubscribe(c) => (
837                "DefiUnsubscribe",
838                c.client_id().copied(),
839                c.venue().copied(),
840                Some(c.command_id()),
841            ),
842            _ => ("Unknown", None, None, None),
843        };
844
845        let result = match cmd {
846            DataCommand::Subscribe(c) => self.execute_subscribe(c),
847            DataCommand::Unsubscribe(c) => self.execute_unsubscribe(&c),
848            DataCommand::Request(c) => self.execute_request(c),
849            #[cfg(feature = "defi")]
850            DataCommand::DefiRequest(c) => self.execute_defi_request(c),
851            #[cfg(feature = "defi")]
852            DataCommand::DefiSubscribe(c) => self.execute_defi_subscribe(c),
853            #[cfg(feature = "defi")]
854            DataCommand::DefiUnsubscribe(c) => self.execute_defi_unsubscribe(&c),
855            _ => {
856                log::warn!("Unhandled DataCommand variant");
857                Ok(())
858            }
859        };
860
861        if let Err(e) = result {
862            log::error!("Data command failed: {command_context:?}: {e}");
863        }
864    }
865
866    /// Handles a subscribe command, updating internal state and forwarding to the client.
867    ///
868    /// # Errors
869    ///
870    /// Returns an error if the subscription is invalid (e.g., synthetic instrument for book data),
871    /// or if the underlying client operation fails.
872    pub fn execute_subscribe(&mut self, cmd: SubscribeCommand) -> anyhow::Result<()> {
873        if let Some(client_id) = cmd.client_id()
874            && self.external_clients.contains(client_id)
875        {
876            if let SubscribeCommand::OptionChain(command) = &cmd {
877                self.retain_external_option_chain(*client_id, command, &cmd);
878            } else if !self.subscriptions_external.retain(
879                (*client_id, SubscriptionKey::from_subscribe(&cmd)),
880                cmd.command_id(),
881                cmd.clone(),
882            ) {
883                return Ok(());
884            }
885
886            register_external_streaming_type(&cmd);
887            publish_external_data_command(*client_id, &cmd);
888
889            if self.config.debug {
890                log::debug!("Skipping subscribe command for external client {client_id}: {cmd:?}");
891            }
892
893            return Ok(());
894        }
895
896        // Update internal engine state
897        match &cmd {
898            SubscribeCommand::BookDeltas(book_cmd) => {
899                if !self.subscribe_book_deltas(book_cmd)? && self.client_subscription_active(&cmd) {
900                    return Ok(());
901                }
902            }
903            SubscribeCommand::BookDepth(book_cmd) => {
904                if !self.subscribe_book_depth(book_cmd)? && self.client_subscription_active(&cmd) {
905                    return Ok(());
906                }
907            }
908            SubscribeCommand::BookSnapshots(cmd) => {
909                // Handles client forwarding internally (forwards as BookDeltas)
910                return self.subscribe_book_snapshots(cmd);
911            }
912            SubscribeCommand::Bars(cmd) if has_continuous_future_params(cmd.params.as_ref()) => {
913                return self.subscribe_continuous_future_bars(cmd);
914            }
915            SubscribeCommand::Bars(cmd) => {
916                self.subscribe_bars(cmd)?;
917                if cmd.bar_type.is_internally_aggregated() {
918                    return Ok(());
919                }
920            }
921            SubscribeCommand::OptionChain(cmd) if cmd.snapshot_interval_ms == Some(0) => {
922                anyhow::bail!(
923                    "Cannot subscribe option chain {} with a zero `snapshot_interval_ms`; use `None` for raw mode",
924                    cmd.series_id,
925                );
926            }
927            SubscribeCommand::OptionChain(cmd) => {
928                self.subscribe_option_chain(cmd);
929                return Ok(());
930            }
931            SubscribeCommand::Quotes(cmd) if cmd.instrument_id.is_synthetic() => {
932                self.subscribe_synthetic_quotes(cmd.instrument_id);
933                return Ok(());
934            }
935            SubscribeCommand::Quotes(cmd)
936                if self.is_spread_quote_command(cmd.instrument_id, cmd.params.as_ref()) =>
937            {
938                self.subscribe_spread_quotes(cmd);
939                return Ok(());
940            }
941            SubscribeCommand::Trades(cmd) if cmd.instrument_id.is_synthetic() => {
942                self.subscribe_synthetic_trades(cmd.instrument_id);
943                return Ok(());
944            }
945            SubscribeCommand::Instrument(cmd) if cmd.instrument_id.is_synthetic() => {
946                anyhow::bail!("Cannot subscribe for synthetic instrument `Instrument` data");
947            }
948            SubscribeCommand::InstrumentStatus(cmd) if cmd.instrument_id.is_synthetic() => {
949                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentStatus` data");
950            }
951            SubscribeCommand::InstrumentClose(cmd) if cmd.instrument_id.is_synthetic() => {
952                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentClose` data");
953            }
954            SubscribeCommand::OptionGreeks(cmd) if cmd.instrument_id.is_synthetic() => {
955                anyhow::bail!("Cannot subscribe for synthetic instrument `OptionGreeks` data");
956            }
957            _ => {} // Do nothing else
958        }
959
960        let retained = cmd.clone();
961
962        // Book ownership, including failed acquisitions, is already counted by the engine
963        let retain_on_failure = !matches!(
964            &cmd,
965            SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookDepth(_)
966        );
967
968        #[cfg(feature = "streaming")]
969        let cmd = self.subscribe_command_with_prefilled_start_ns(cmd)?;
970
971        if let Some(client) = self.get_command_client(cmd.client_id(), cmd.venue()) {
972            client.execute_subscribe_with_retained(cmd, retained, retain_on_failure);
973        } else {
974            log::error!(
975                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
976                cmd.client_id(),
977                cmd.venue(),
978            );
979        }
980
981        Ok(())
982    }
983
984    fn client_subscription_active(&mut self, cmd: &SubscribeCommand) -> bool {
985        self.get_command_client(cmd.client_id(), cmd.venue())
986            .is_some_and(|client| client.has_active_subscription(cmd))
987    }
988
989    /// Handles an unsubscribe command, updating internal state and forwarding to the client.
990    ///
991    /// # Errors
992    ///
993    /// Returns an error if the underlying client operation fails.
994    pub fn execute_unsubscribe(&mut self, cmd: &UnsubscribeCommand) -> anyhow::Result<()> {
995        if let Some(client_id) = cmd.client_id()
996            && self.external_clients.contains(client_id)
997        {
998            let key = (*client_id, SubscriptionKey::from_unsubscribe(cmd));
999            let command = match self.subscriptions_external.release(&key) {
1000                SubscriptionRelease::Retained => return Ok(()),
1001                SubscriptionRelease::Final(subscribe) => subscribe.into_unsubscribe(
1002                    cmd.command_id(),
1003                    cmd.ts_init(),
1004                    cmd.correlation_id(),
1005                ),
1006                SubscriptionRelease::Untracked => cmd.clone(),
1007            };
1008            self.subscriptions_external.remove(&key);
1009            publish_external_data_command(*client_id, &command);
1010
1011            if self.config.debug {
1012                log::debug!(
1013                    "Skipping unsubscribe command for external client {client_id}: {command:?}",
1014                );
1015            }
1016            return Ok(());
1017        }
1018
1019        if matches!(
1020            cmd,
1021            UnsubscribeCommand::BookDeltas(_)
1022                | UnsubscribeCommand::BookDepth(_)
1023                | UnsubscribeCommand::BookSnapshots(_)
1024        ) && !self.release_book_subscription(cmd)
1025        {
1026            return Ok(());
1027        }
1028
1029        match &cmd {
1030            UnsubscribeCommand::BookDeltas(cmd) if !self.unsubscribe_book_deltas(cmd) => {
1031                return Ok(());
1032            }
1033            UnsubscribeCommand::BookDepth(cmd) if !self.unsubscribe_book_depth(cmd) => {
1034                return Ok(());
1035            }
1036            UnsubscribeCommand::BookSnapshots(cmd) => {
1037                // Handles client forwarding internally (forwards as BookDeltas)
1038                self.unsubscribe_book_snapshots(cmd);
1039                return Ok(());
1040            }
1041            UnsubscribeCommand::Bars(cmd)
1042                if self
1043                    .continuous_future_subscriptions
1044                    .contains_key(&cmd.bar_type.standard()) =>
1045            {
1046                // Don't tear down the chain while other actors remain subscribed
1047                let topic = switchboard::get_bars_topic(cmd.bar_type.standard());
1048                if msgbus::exact_subscriber_count_bars(topic) == 0 {
1049                    self.unsubscribe_continuous_future_bars(cmd);
1050                }
1051                return Ok(());
1052            }
1053            UnsubscribeCommand::Bars(cmd) => {
1054                self.unsubscribe_bars(cmd);
1055                if cmd.bar_type.is_internally_aggregated() {
1056                    return Ok(());
1057                }
1058            }
1059            UnsubscribeCommand::OptionChain(cmd) => {
1060                self.unsubscribe_option_chain(cmd);
1061                return Ok(());
1062            }
1063            UnsubscribeCommand::Quotes(cmd) if cmd.instrument_id.is_synthetic() => {
1064                self.unsubscribe_synthetic_quotes(cmd.instrument_id);
1065                return Ok(());
1066            }
1067            UnsubscribeCommand::Quotes(cmd)
1068                if self.is_spread_quote_command(cmd.instrument_id, cmd.params.as_ref()) =>
1069            {
1070                self.unsubscribe_spread_quotes(cmd);
1071                return Ok(());
1072            }
1073            UnsubscribeCommand::Trades(cmd) if cmd.instrument_id.is_synthetic() => {
1074                self.unsubscribe_synthetic_trades(cmd.instrument_id);
1075                return Ok(());
1076            }
1077            UnsubscribeCommand::Instrument(cmd) if cmd.instrument_id.is_synthetic() => {
1078                anyhow::bail!("Cannot unsubscribe from synthetic instrument `Instrument` data");
1079            }
1080            UnsubscribeCommand::InstrumentStatus(cmd) if cmd.instrument_id.is_synthetic() => {
1081                anyhow::bail!(
1082                    "Cannot unsubscribe from synthetic instrument `InstrumentStatus` data"
1083                );
1084            }
1085            UnsubscribeCommand::InstrumentClose(cmd) if cmd.instrument_id.is_synthetic() => {
1086                anyhow::bail!(
1087                    "Cannot unsubscribe from synthetic instrument `InstrumentClose` data"
1088                );
1089            }
1090            UnsubscribeCommand::OptionGreeks(cmd) if cmd.instrument_id.is_synthetic() => {
1091                anyhow::bail!("Cannot unsubscribe from synthetic instrument `OptionGreeks` data");
1092            }
1093            _ => {}
1094        }
1095
1096        if let Some(client) = self.get_command_client(cmd.client_id(), cmd.venue()) {
1097            client.execute_unsubscribe(cmd);
1098        } else {
1099            log::error!(
1100                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
1101                cmd.client_id(),
1102                cmd.venue(),
1103            );
1104        }
1105
1106        Ok(())
1107    }
1108
1109    /// Sends a [`RequestCommand`] to a suitable data client implementation.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns an error if no client is found for the given client ID or venue,
1114    /// or if the client fails to process the request.
1115    pub fn execute_request(&mut self, req: RequestCommand) -> anyhow::Result<()> {
1116        // Skip requests for external clients
1117        if let Some(cid) = req.client_id()
1118            && self.external_clients.contains(cid)
1119        {
1120            if self.config.debug {
1121                log::debug!("Skipping data request for external client {cid}: {req:?}");
1122            }
1123            return Ok(());
1124        }
1125
1126        if let RequestCommand::Join(join) = req {
1127            return self.handle_request_join(join);
1128        }
1129
1130        if has_continuous_future_params(request_params(&req)) {
1131            return self.execute_continuous_future_request(req);
1132        }
1133
1134        let request_id = *req.request_id();
1135        self.prepare_request_bar_aggregators(&req)?;
1136
1137        if has_time_range_pipeline_params(request_params(&req))
1138            && is_time_range_pipeline_variant(&req)
1139        {
1140            let result = self.execute_time_range_pipeline_request(req);
1141            if result.is_err() {
1142                self.cleanup_request_bar_aggregators(&request_id);
1143            }
1144            return result;
1145        }
1146
1147        #[cfg(feature = "streaming")]
1148        if self.catalogs_registered() && streaming::is_date_range_variant(&req) {
1149            let result = self.dispatch_date_range_request(req);
1150            if result.is_err() {
1151                self.cleanup_request_bar_aggregators(&request_id);
1152            }
1153            return result;
1154        }
1155
1156        let result = self.dispatch_request_to_client(req);
1157
1158        if result.is_err() {
1159            self.cleanup_request_bar_aggregators(&request_id);
1160        }
1161
1162        result.map(|_| ())
1163    }
1164
1165    pub(super) fn dispatch_request_to_client(
1166        &mut self,
1167        req: RequestCommand,
1168    ) -> anyhow::Result<ClientId> {
1169        let client_id = req.client_id().copied();
1170        let venue = req.venue().copied();
1171        let Some(client) = self.get_client(client_id.as_ref(), venue.as_ref()) else {
1172            anyhow::bail!("Cannot handle request: no client found for {client_id:?} {venue:?}");
1173        };
1174        let resolved_client_id = client.client_id();
1175
1176        #[rustfmt::skip]
1177        let result = match req {
1178            RequestCommand::Data(req) => client.request_data(req),
1179            RequestCommand::Instrument(req) => client.request_instrument(req),
1180            RequestCommand::Instruments(req) => client.request_instruments(req),
1181            RequestCommand::BookSnapshot(req) => client.request_book_snapshot(req),
1182            RequestCommand::BookDeltas(req) => client.request_book_deltas(req),
1183            RequestCommand::BookDepth(req) => client.request_book_depth(req),
1184            RequestCommand::Quotes(req) => client.request_quotes(req),
1185            RequestCommand::Trades(req) => client.request_trades(req),
1186            RequestCommand::FundingRates(req) => client.request_funding_rates(req),
1187            RequestCommand::OptionChainReferencePrice(req) => client.request_option_chain_reference_price(req),
1188            RequestCommand::Bars(req) => client.request_bars(req),
1189            RequestCommand::Join(_) => anyhow::bail!("RequestJoin must be handled by handle_request_join"),
1190        };
1191
1192        result
1193            .map_err(|e| e.context(format!("Data client {resolved_client_id} request failed")))?;
1194
1195        Ok(resolved_client_id)
1196    }
1197
1198    /// Processes a dynamically-typed data message.
1199    ///
1200    /// Currently supports `InstrumentAny`, funding rates, option greeks, instrument status, and
1201    /// custom data; unrecognized types are logged as errors.
1202    pub fn process(&mut self, data: &dyn Any) {
1203        self.data_count += 1;
1204        // Dynamically-typed entry point: `FundingRateUpdate`, `OptionGreeks`, `InstrumentStatus`,
1205        // and custom data are also `Data` enum variants handled in `process_data`, but can arrive
1206        // here as typed data, whereas `InstrumentAny` is not a `Data` variant.
1207        if let Some(instrument) = data.downcast_ref::<InstrumentAny>() {
1208            self.handle_instrument(instrument);
1209        } else if let Some(funding_rate) = data.downcast_ref::<FundingRateUpdate>() {
1210            self.handle_funding_rate(*funding_rate);
1211        } else if let Some(option_greeks) = data.downcast_ref::<OptionGreeks>() {
1212            self.cache.borrow_mut().add_option_greeks(*option_greeks);
1213            self.feed_option_greeks_to_pre_bootstrap_chain(option_greeks);
1214            let topic = switchboard::get_option_greeks_topic(option_greeks.instrument_id);
1215            msgbus::publish_option_greeks(topic, option_greeks);
1216            self.drain_deferred_commands();
1217        } else if let Some(status) = data.downcast_ref::<InstrumentStatus>() {
1218            self.handle_instrument_status(*status);
1219        } else if let Some(custom) = data.downcast_ref::<CustomData>() {
1220            self.handle_custom_data(custom);
1221        } else {
1222            #[cfg(feature = "defi")]
1223            if let Some(response) = data.downcast_ref::<PoolSnapshotResponse>() {
1224                self.handle_pool_snapshot_response(response);
1225                return;
1226            }
1227
1228            log::error!("Cannot process data {data:?}, type is unrecognized");
1229        }
1230    }
1231
1232    /// Processes a `Data` enum instance, dispatching to live handlers.
1233    #[allow(
1234        clippy::needless_pass_by_value,
1235        reason = "callers hand over ownership; the payload is only moved when a DeFi handler consumes it"
1236    )]
1237    pub fn process_data(&mut self, data: Data) {
1238        #[cfg(feature = "defi")]
1239        let data = match data {
1240            Data::Defi(defi) => {
1241                self.process_defi_data(*defi);
1242                return;
1243            }
1244            data => data,
1245        };
1246
1247        self.process_data_ref(DataRef::from(&data));
1248    }
1249
1250    /// Processes a borrowed `Data` enum view, dispatching to live handlers.
1251    ///
1252    /// DeFi payloads are cloned because the DeFi handler may buffer them while a pool snapshot
1253    /// is pending; no other variant clones its payload.
1254    pub fn process_data_ref(&mut self, data: DataRef<'_>) {
1255        #[cfg(feature = "defi")]
1256        let data = match data {
1257            DataRef::Defi(defi) => {
1258                self.process_defi_data(defi.clone());
1259                return;
1260            }
1261            data => data,
1262        };
1263
1264        self.data_count += 1;
1265
1266        match data {
1267            DataRef::Instrument(instrument) => self.handle_instrument(instrument),
1268            DataRef::BookDelta(delta) => self.handle_delta(*delta),
1269            DataRef::BookDeltas(deltas) => self.handle_deltas(deltas),
1270            DataRef::BookDepth(depth) => self.handle_depth(depth),
1271            DataRef::Quote(quote) => {
1272                self.handle_quote(*quote);
1273                self.drain_deferred_commands();
1274            }
1275            DataRef::Trade(trade) => self.handle_trade(*trade),
1276            DataRef::Bar(bar) => self.handle_bar(*bar),
1277            DataRef::MarkPrice(mark_price) => {
1278                self.handle_mark_price(*mark_price);
1279                self.drain_deferred_commands();
1280            }
1281            DataRef::IndexPrice(index_price) => {
1282                self.handle_index_price(*index_price);
1283                self.drain_deferred_commands();
1284            }
1285            DataRef::FundingRate(funding_rate) => {
1286                self.handle_funding_rate(*funding_rate);
1287                self.drain_deferred_commands();
1288            }
1289            DataRef::OptionGreeks(greeks) => {
1290                self.cache.borrow_mut().add_option_greeks(*greeks);
1291                self.feed_option_greeks_to_pre_bootstrap_chain(greeks);
1292                let topic = switchboard::get_option_greeks_topic(greeks.instrument_id);
1293                msgbus::publish_option_greeks(topic, greeks);
1294                self.drain_deferred_commands();
1295            }
1296            DataRef::InstrumentStatus(status) => {
1297                self.handle_instrument_status(*status);
1298                self.drain_deferred_commands();
1299            }
1300            DataRef::InstrumentClose(close) => self.handle_instrument_close(*close),
1301            DataRef::Custom(custom) => self.handle_custom_data(custom),
1302            #[cfg(feature = "defi")]
1303            DataRef::Defi(_) => unreachable!("handled before market data dispatch"),
1304            #[cfg(not(feature = "defi"))]
1305            #[allow(
1306                unreachable_patterns,
1307                reason = "DeFi variants can exist without this crate's defi feature"
1308            )]
1309            other => log_defi_data_dropped(other),
1310        }
1311    }
1312
1313    /// Processes a `Data` instance through the pipeline bus path.
1314    ///
1315    /// Pipeline mode publishes each item on the `data.pipeline.` topic family and gates cache
1316    /// writes on `disable_historical_cache`. None of the live-only side effects (synthetic
1317    /// republish, option-chain expiry, depth-derived quotes, deferred-command drains) run in this
1318    /// path.
1319    pub fn process_pipeline(&mut self, data: Data) {
1320        #[cfg(feature = "defi")]
1321        let data = match data {
1322            Data::Defi(defi) => {
1323                self.process_defi_data(*defi);
1324                return;
1325            }
1326            data => data,
1327        };
1328
1329        self.data_count += 1;
1330
1331        match data {
1332            Data::Instrument(instrument) => self.handle_instrument(&instrument),
1333            Data::BookDelta(delta) => self.handle_delta_pipeline(delta),
1334            Data::BookDeltas(deltas) => self.handle_deltas_pipeline(&deltas),
1335            Data::BookDepth(depth) => self.handle_depth_pipeline(&depth),
1336            Data::Quote(quote) => self.handle_quote_pipeline(quote),
1337            Data::Trade(trade) => self.handle_trade_pipeline(trade),
1338            Data::Bar(bar) => self.handle_bar_pipeline(bar),
1339            Data::MarkPrice(mark_price) => self.handle_mark_price_pipeline(mark_price),
1340            Data::IndexPrice(index_price) => self.handle_index_price_pipeline(index_price),
1341            Data::FundingRate(funding_rate) => {
1342                self.handle_funding_rate_pipeline(funding_rate);
1343            }
1344            Data::OptionGreeks(greeks) => self.handle_option_greeks_pipeline(greeks),
1345            Data::InstrumentStatus(status) => self.handle_instrument_status_pipeline(status),
1346            Data::InstrumentClose(close) => self.handle_instrument_close_pipeline(close),
1347            Data::Custom(custom) => self.handle_custom_data_pipeline(&custom),
1348            #[cfg(feature = "defi")]
1349            Data::Defi(_) => unreachable!("handled before market data dispatch"),
1350            #[cfg(not(feature = "defi"))]
1351            #[allow(
1352                unreachable_patterns,
1353                reason = "DeFi variants can exist without this crate's defi feature"
1354            )]
1355            other => log_defi_data_dropped(DataRef::from(&other)),
1356        }
1357    }
1358
1359    /// Processes a `DataResponse`, handling and publishing the response message.
1360    pub fn response(&mut self, mut resp: DataResponse) {
1361        if log::log_enabled!(log::Level::Debug) {
1362            let correlation_id = resp.correlation_id();
1363            match resp.record_count() {
1364                Some(count) => log::debug!(
1365                    "{RECV}{RES} {} correlation_id={correlation_id} records={count}",
1366                    resp.kind(),
1367                ),
1368                None => log::debug!(
1369                    "{RECV}{RES} {} correlation_id={correlation_id}",
1370                    resp.kind(),
1371                ),
1372            }
1373        }
1374        log::trace!("{RECV}{RES} {resp:?}");
1375
1376        self.response_count += 1;
1377
1378        resp.trim_to_bounds();
1379
1380        let Some(resp) = self.handle_request_pipeline_response(resp) else {
1381            return;
1382        };
1383
1384        // Catalog legs inherit the child's params, so route only the assembled segment
1385        if let Some(parent_id) = continuous_future_parent_request_id(response_params(&resp)) {
1386            self.handle_continuous_future_child_response(parent_id, &resp);
1387            return;
1388        }
1389
1390        if let Some(parent_id) = self
1391            .time_range_pipeline_parent_request_id
1392            .remove(resp.correlation_id())
1393        {
1394            self.handle_time_range_pipeline_child_response(parent_id, &resp);
1395            return;
1396        }
1397
1398        if self
1399            .parent_join_request_id
1400            .contains_key(resp.correlation_id())
1401        {
1402            self.finalize_request_join(resp);
1403            return;
1404        }
1405
1406        let correlation_id = *resp.correlation_id();
1407
1408        match &resp {
1409            DataResponse::Instrument(r) => {
1410                self.handle_instrument_response(r.data.clone());
1411            }
1412            DataResponse::Instruments(r) => {
1413                self.handle_instruments(&r.data);
1414            }
1415            DataResponse::Quotes(r) => {
1416                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1417                    self.handle_quotes(&r.data);
1418                }
1419            }
1420            DataResponse::Trades(r) => {
1421                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1422                    self.handle_trades(&r.data);
1423                }
1424            }
1425            DataResponse::FundingRates(r) => {
1426                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1427                    self.handle_funding_rates(&r.data);
1428                }
1429            }
1430            DataResponse::Bars(r) => {
1431                if !log_if_empty_response(&r.data, &r.bar_type, &correlation_id) {
1432                    self.handle_bars(&r.data);
1433                }
1434            }
1435            DataResponse::Book(r) => self.handle_book_response(&r.data),
1436            DataResponse::BookDeltas(r) => {
1437                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1438                    self.handle_book_deltas_response(r);
1439                }
1440            }
1441            DataResponse::BookDepth(r) => {
1442                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
1443                    self.handle_book_depth_response(r);
1444                }
1445            }
1446            DataResponse::OptionChainReferencePrice(r) => {
1447                self.process_request_bar_aggregation_response(&resp);
1448                return self.handle_option_chain_reference_price_response(&correlation_id, r);
1449            }
1450            DataResponse::Data(_) => {}
1451        }
1452
1453        self.process_request_bar_aggregation_response(&resp);
1454
1455        msgbus::send_response(&correlation_id, &resp);
1456    }
1457
1458    /// Registers a parent request whose response will be rebuilt from `n_components` leg responses.
1459    pub fn new_request_pipeline(&mut self, parent: RequestCommand, n_components: usize) {
1460        let parent_id = *parent.request_id();
1461        self.request_pipeline_n_components
1462            .insert(parent_id, n_components);
1463        self.request_pipeline_parent_request
1464            .insert(parent_id, parent);
1465        self.request_pipeline_responses
1466            .insert(parent_id, Vec::with_capacity(n_components));
1467    }
1468
1469    /// Registers a leg `request_id` as a child of the pipeline keyed by `parent_id`.
1470    pub fn register_request_pipeline_leg(&mut self, leg_id: UUID4, parent_id: UUID4) {
1471        self.request_pipeline_parent_request_id
1472            .insert(leg_id, parent_id);
1473    }
1474
1475    /// Fans a leg response into its parent pipeline and emits the rebuilt response when all legs arrive.
1476    ///
1477    /// Responses whose `correlation_id` is not part of any pipeline pass through unchanged.
1478    /// While accumulating legs, returns `None` so the caller skips further response handling.
1479    fn handle_request_pipeline_response(&mut self, resp: DataResponse) -> Option<DataResponse> {
1480        let leg_id = *resp.correlation_id();
1481        let Some(parent_id) = self.request_pipeline_parent_request_id.remove(&leg_id) else {
1482            return Some(resp);
1483        };
1484
1485        let Some(buf) = self.request_pipeline_responses.get_mut(&parent_id) else {
1486            log::error!("Pipeline response buffer missing for parent {parent_id} (leg {leg_id})");
1487            return Some(resp);
1488        };
1489        buf.push(resp);
1490
1491        let expected = self.request_pipeline_n_components.get(&parent_id).copied();
1492        let received = buf.len();
1493        match expected {
1494            Some(n) if received < n => return None,
1495            Some(_) => {}
1496            None => {
1497                log::error!("Pipeline n_components missing for parent {parent_id}");
1498                return None;
1499            }
1500        }
1501
1502        let mut legs = self.request_pipeline_responses.remove(&parent_id)?;
1503        self.request_pipeline_n_components.remove(&parent_id);
1504        let parent = self.request_pipeline_parent_request.remove(&parent_id);
1505
1506        for leg in &mut legs {
1507            leg.trim_to_bounds();
1508        }
1509
1510        let (parent_start, parent_end) = parent_request_window(parent.as_ref());
1511        let rebuilt = rebuild_pipeline_response(parent_id, parent.as_ref(), legs);
1512
1513        // If the rebuild failed (mixed-variant, unsupported-variant, or mixed-instrument
1514        // `BookDeltas` legs), drop the associated `RequestJoin` so its staging maps do not
1515        // leak. Without this the
1516        // original join request stays in `pending_join_requests` and its
1517        // `parent_join_request_id` mapping stays live, neither of which will ever
1518        // resolve through normal flow.
1519        if rebuilt.is_none()
1520            && let Some(original_id) = self.parent_join_request_id.remove(&parent_id)
1521        {
1522            self.pending_join_requests.remove(&original_id);
1523            log::error!(
1524                "Dropped RequestJoin {original_id} because pipeline rebuild failed for dated parent {parent_id}"
1525            );
1526        }
1527
1528        let mut rebuilt = rebuilt?;
1529
1530        // Replay must run before `trim_to_bounds`, which would otherwise discard the pre-start
1531        // deltas the replay folds into the snapshot.
1532        if let DataResponse::BookDeltas(r) = &mut rebuilt {
1533            self.book_deltas_snapshot_replay(r);
1534        }
1535
1536        // Trim against the parent window only when the parent supplied one. With no
1537        // parent window the rebuilt response inherits the first leg's bounds; legs are
1538        // already trimmed against their own bounds at the top of `response()`, so a
1539        // second pass would discard data from later legs whose bounds the parent never
1540        // constrained.
1541        if parent_start.is_some() || parent_end.is_some() {
1542            rebuilt.trim_to_bounds();
1543        }
1544
1545        Some(rebuilt)
1546    }
1547
1548    fn handle_request_join(&mut self, req: RequestJoin) -> anyhow::Result<()> {
1549        if has_time_range_pipeline_params(req.params.as_ref()) {
1550            return self.execute_time_range_pipeline_request(RequestCommand::Join(req));
1551        }
1552
1553        let now_ns = self.clock.borrow().timestamp_ns();
1554        let now_dt = now_ns.to_datetime_utc();
1555        let zero = jiff::Timestamp::UNIX_EPOCH;
1556        let start = req.start.unwrap_or(zero).min(now_dt);
1557        let end = req.end.unwrap_or(now_dt).min(now_dt);
1558        let dated = req.with_dates(Some(start), Some(end), now_ns);
1559
1560        let original_id = req.request_id;
1561        let dated_id = dated.request_id;
1562
1563        self.pending_join_requests.insert(original_id, req);
1564        self.parent_join_request_id.insert(dated_id, original_id);
1565
1566        let leg_ids: Vec<UUID4> = dated.request_ids.clone();
1567        self.new_request_pipeline(RequestCommand::Join(dated), leg_ids.len());
1568        for leg_id in leg_ids {
1569            self.register_request_pipeline_leg(leg_id, dated_id);
1570        }
1571
1572        Ok(())
1573    }
1574
1575    fn finalize_request_join(&mut self, resp: DataResponse) {
1576        let dated_id = *resp.correlation_id();
1577        let Some(original_id) = self.parent_join_request_id.remove(&dated_id) else {
1578            log::error!("parent_join_request_id missing for dated correlation {dated_id}");
1579            return;
1580        };
1581
1582        let Some(original) = self.pending_join_requests.remove(&original_id) else {
1583            log::error!("pending_join_requests missing for original {original_id}");
1584            return;
1585        };
1586
1587        let now_ns = self.clock.borrow().timestamp_ns();
1588
1589        // Empty leg responses fire each leg's callback so caller-side request
1590        // workflows clean up. Per-leg metadata is reconstructed from the
1591        // rebuilt parent response and may not match a leg's original
1592        // instrument_id/bar_type when the join spans heterogeneous legs;
1593        // tracked as a follow-up in #5 (needs an in-flight leg-request cache).
1594        for leg_request_id in &original.request_ids {
1595            let empty = empty_response_like(&resp, *leg_request_id, now_ns);
1596            msgbus::send_response(leg_request_id, &empty);
1597        }
1598
1599        // Route the final join response through the normal response path so
1600        // bounds-trim against the parent window runs and the per-variant
1601        // handlers (cache writes, request bar aggregators) fire. The pipeline
1602        // and join staging maps for this request have already been popped, so
1603        // the recursive call cannot re-enter either gate.
1604        let final_resp = rebind_response_correlation(resp, original_id);
1605        self.response(final_resp);
1606    }
1607}
1608
1609fn register_external_streaming_type(cmd: &SubscribeCommand) {
1610    if let Some(payload_type) = streaming_payload_type(cmd) {
1611        msgbus::get_message_bus()
1612            .borrow_mut()
1613            .add_streaming_type(payload_type);
1614    }
1615}
1616
1617fn publish_external_data_command<T>(client_id: ClientId, command: &T)
1618where
1619    T: Any,
1620{
1621    let topic = format!("commands.data.{client_id}");
1622    msgbus::publish_any(topic.into(), command);
1623}
1624
1625#[rustfmt::skip]
1626fn streaming_payload_type(cmd: &SubscribeCommand) -> Option<BusPayloadType> {
1627    match cmd {
1628        SubscribeCommand::Data(cmd) => Some(BusPayloadType::Custom(Ustr::from(
1629            cmd.data_type.type_name(),
1630        ))),
1631        SubscribeCommand::Instrument(_) | SubscribeCommand::Instruments(_) => Some(BusPayloadType::Instrument),
1632        SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookSnapshots(_) => Some(BusPayloadType::OrderBookDeltas),
1633        SubscribeCommand::BookDepth(_) => Some(BusPayloadType::OrderBookDepth),
1634        SubscribeCommand::Quotes(_) => Some(BusPayloadType::QuoteTick),
1635        SubscribeCommand::Trades(_) => Some(BusPayloadType::TradeTick),
1636        SubscribeCommand::Bars(_) => Some(BusPayloadType::Bar),
1637        SubscribeCommand::MarkPrices(_) => Some(BusPayloadType::MarkPriceUpdate),
1638        SubscribeCommand::IndexPrices(_) => Some(BusPayloadType::IndexPriceUpdate),
1639        SubscribeCommand::FundingRates(_) => Some(BusPayloadType::FundingRateUpdate),
1640        SubscribeCommand::OptionGreeks(_) => Some(BusPayloadType::OptionGreeks),
1641        SubscribeCommand::InstrumentStatus(_)
1642        | SubscribeCommand::InstrumentClose(_)
1643        | SubscribeCommand::OptionChain(_) => None,
1644    }
1645}
1646
1647#[cfg(not(feature = "defi"))]
1648fn log_defi_data_dropped(data: DataRef<'_>) {
1649    log::error!("Cannot process data {data:?}, nautilus-data built without its `defi` feature");
1650}