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 handlers;
43mod requests;
44mod time_range;
45
46use std::{
47    any::{Any, type_name},
48    cell::RefCell,
49    collections::VecDeque,
50    fmt::{Debug, Display},
51    mem,
52    num::NonZeroUsize,
53    rc::Rc,
54    str::FromStr,
55};
56
57use ahash::{AHashMap, AHashSet};
58use anyhow::Context;
59pub use bar::BarAggregatorSubscription;
60use bar::{BarAggregationSubscription, BarAggregatorKey, bar_aggregator_key};
61use book::{
62    BookDeltasKey, BookDeltasUnsubscribeResult, BookSnapshotInfo, BookSnapshotInfos,
63    BookSnapshotKey, BookSnapshotSource, BookSnapshotUnsubscribeResult, BookSnapshotter,
64    BookSubscription, BookSubscriptionOwner, BookUpdater,
65};
66pub(crate) use commands::{DeferredCommand, DeferredCommandQueue};
67use config::DataEngineConfig;
68use futures::future::join_all;
69use handlers::{
70    BAR_AGGREGATOR_PRIORITY, BarBarHandler, BarQuoteHandler, BarTradeHandler, SpreadQuoteHandler,
71};
72use indexmap::IndexMap;
73#[cfg(feature = "defi")]
74use nautilus_common::messages::defi::PoolSnapshotResponse;
75use nautilus_common::{
76    cache::Cache,
77    clock::Clock,
78    logging::{RECV, RES},
79    messages::data::{
80        BarsResponse, BookDeltasResponse, BookDepthResponse, CustomDataResponse, DataCommand,
81        DataResponse, FundingRatesResponse, OptionChainReferencePriceResponse, PARAMS_IS_PARENT,
82        QuotesResponse, RequestBars, RequestCommand, RequestJoin, RequestOptionChainReferencePrice,
83        RequestQuotes, RequestTrades, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth,
84        SubscribeBookSnapshots, SubscribeCommand, SubscribeOptionChain, SubscribeOptionGreeks,
85        SubscribeQuotes, SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
86        UnsubscribeBookDepth, UnsubscribeBookSnapshots, UnsubscribeCommand,
87        UnsubscribeInstrumentStatus, UnsubscribeOptionChain, UnsubscribeOptionGreeks,
88        UnsubscribeQuotes, UnsubscribeTrades, is_parent_subscription,
89    },
90    msgbus::{
91        self, BusPayloadType, ShareableMessageHandler, TypedHandler, TypedIntoHandler,
92        switchboard::{self, MessagingSwitchboard},
93    },
94    runner::get_data_cmd_sender,
95    timer::{TimeEvent, TimeEventCallback},
96};
97use nautilus_core::{
98    DurationNanos, Params, UUID4, UnixNanos, WeakCell,
99    correctness::{FAILED, check_key_in_map, check_key_not_in_map, check_predicate_true},
100    datetime::NANOSECONDS_IN_DAY,
101};
102#[cfg(feature = "defi")]
103use nautilus_model::defi::DefiData;
104use nautilus_model::{
105    data::{
106        Bar, BarType, CustomData, Data, DataRef, DataType, FundingRateUpdate, HasTsInit,
107        IndexPriceUpdate, InstrumentClose, InstrumentStatus, MarkPriceUpdate, OrderBookDelta,
108        OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
109        option_chain::{OptionGreeks, StrikeRange},
110    },
111    enums::{
112        AggregationSource, BarAggregation, BookType, InstrumentClass, MarketStatusAction,
113        PriceType, RecordFlag,
114    },
115    identifiers::{
116        ClientId, GENERIC_SPREAD_ID_SEPARATOR, InstrumentId, OptionSeriesId, Venue,
117        parse_generic_spread_id_legs,
118    },
119    instruments::{Instrument, InstrumentAny, SyntheticInstrument},
120    orderbook::OrderBook,
121    types::{Price, Quantity},
122};
123use requests::{
124    ContinuousFutureRequest, ContinuousFutureRequestState, ContinuousFutureSegment,
125    ContinuousFutureSource, RequestBarAggregation, continuous_future_parent_request_id,
126    continuous_future_request_from_bars, continuous_future_subscription_from_bars,
127    has_continuous_future_params, request_bar_aggregation_from_params, request_params,
128    response_params,
129};
130#[cfg(feature = "streaming")]
131use streaming::CatalogMap;
132use time_range::{
133    TimeRangePipelineState, has_time_range_pipeline_params, is_time_range_pipeline_variant,
134};
135use ustr::Ustr;
136
137#[cfg(feature = "defi")]
138#[allow(unused_imports)] // Brings DeFi impl blocks into scope
139use crate::defi::engine as _;
140#[cfg(feature = "defi")]
141use crate::engine::pool::PoolUpdater;
142use crate::{
143    aggregation::{
144        BarAggregator, RenkoBarAggregator, SpreadQuoteAggregator, TickBarAggregator,
145        TickImbalanceBarAggregator, TickRunsBarAggregator, TimeBarAggregator, ValueBarAggregator,
146        ValueImbalanceBarAggregator, ValueRunsBarAggregator, VolumeBarAggregator,
147        VolumeImbalanceBarAggregator, VolumeRunsBarAggregator,
148    },
149    client::DataClientAdapter,
150    option_chains::OptionChainManager,
151    subscription::{SubscriptionKey, SubscriptionRegistry, SubscriptionRelease},
152};
153
154const OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT: DurationNanos = DurationNanos::from_secs(30);
155const OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER: &str = "option-chain-reference-price-timeout";
156
157/// Provides a high-performance `DataEngine` for all environments.
158#[derive(Debug)]
159pub struct DataEngine {
160    clock: Rc<RefCell<dyn Clock>>,
161    cache: Rc<RefCell<Cache>>,
162    config: DataEngineConfig,
163    msgbus_priority: u32,
164    external_clients: AHashSet<ClientId>,
165    subscriptions_external: SubscriptionRegistry<(ClientId, SubscriptionKey), SubscribeCommand>,
166    clients: IndexMap<ClientId, DataClientAdapter>,
167    default_client_id: Option<ClientId>,
168    routing_map: IndexMap<Venue, ClientId>,
169    book_intervals: AHashMap<NonZeroUsize, BookSnapshotInfos>,
170    book_snapshot_counts: IndexMap<BookSnapshotKey, usize>,
171    book_snapshot_sources: AHashMap<InstrumentId, BookSnapshotSource>,
172    book_deltas_counts: IndexMap<BookDeltasKey, usize>,
173    book_depth_counts: IndexMap<BookDeltasKey, usize>,
174    book_updaters: AHashMap<InstrumentId, Rc<BookUpdater>>,
175    book_subscriptions: AHashMap<InstrumentId, BookSubscription>,
176    book_subscription_owners: AHashMap<InstrumentId, Vec<Rc<BookSubscriptionOwner>>>,
177    book_snapshotters: AHashMap<NonZeroUsize, Rc<BookSnapshotter>>,
178    bar_aggregators: IndexMap<BarAggregatorKey, Rc<RefCell<Box<dyn BarAggregator>>>>,
179    bar_aggregator_handlers: AHashMap<BarAggregatorKey, Vec<BarAggregatorSubscription>>,
180    subscriptions_bar_aggregation: AHashMap<BarType, BarAggregationSubscription>,
181    request_bar_aggregations: AHashMap<UUID4, RequestBarAggregation>,
182    request_pipeline_parent_request: AHashMap<UUID4, RequestCommand>,
183    request_pipeline_n_components: AHashMap<UUID4, usize>,
184    request_pipeline_parent_request_id: AHashMap<UUID4, UUID4>,
185    request_pipeline_responses: AHashMap<UUID4, Vec<DataResponse>>,
186    time_range_pipeline_requests: AHashMap<UUID4, TimeRangePipelineState>,
187    time_range_pipeline_parent_request_id: AHashMap<UUID4, UUID4>,
188    parent_join_request_id: AHashMap<UUID4, UUID4>,
189    pending_join_requests: AHashMap<UUID4, RequestJoin>,
190    continuous_future_requests: AHashMap<UUID4, ContinuousFutureRequestState>,
191    continuous_future_subscriptions: AHashMap<BarType, ContinuousFutureSubscriptionState>,
192    continuous_future_roller: Option<Rc<ContinuousFutureRoller>>,
193    spread_quote_states: AHashMap<InstrumentId, SpreadQuoteState>,
194    option_chain_managers: AHashMap<OptionSeriesId, Rc<RefCell<OptionChainManager>>>,
195    option_chain_instrument_index: AHashMap<InstrumentId, OptionSeriesId>,
196    deferred_cmd_queue: DeferredCommandQueue,
197    option_chain_bootstrapper: Option<Rc<OptionChainBootstrapper>>,
198    pending_option_chain_requests: AHashMap<UUID4, PendingOptionChainRequest>,
199    option_chain_greeks_bootstraps: AHashMap<OptionSeriesId, OptionChainGreeksBootstrap>,
200    synthetic_quote_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
201    synthetic_trade_feeds: AHashMap<InstrumentId, Vec<SyntheticInstrument>>,
202    subscribed_synthetic_quotes: AHashMap<InstrumentId, usize>,
203    subscribed_synthetic_trades: AHashMap<InstrumentId, usize>,
204    buffered_deltas_map: AHashMap<InstrumentId, OrderBookDeltas>,
205    deltas_frame: Vec<OrderBookDelta>,
206    command_count: u64,
207    data_count: u64,
208    request_count: u64,
209    response_count: u64,
210    #[cfg(feature = "streaming")]
211    catalogs: CatalogMap,
212    #[cfg(feature = "defi")]
213    pub(crate) pool_updaters: AHashMap<InstrumentId, Rc<PoolUpdater>>,
214    #[cfg(feature = "defi")]
215    pub(crate) pool_snapshot_pending: AHashMap<InstrumentId, UUID4>,
216    #[cfg(feature = "defi")]
217    pub(crate) pool_event_buffers: AHashMap<InstrumentId, Vec<DefiData>>,
218}
219
220impl DataEngine {
221    /// Creates a new [`DataEngine`] instance.
222    #[must_use]
223    pub fn new(
224        clock: Rc<RefCell<dyn Clock>>,
225        cache: Rc<RefCell<Cache>>,
226        config: Option<DataEngineConfig>,
227    ) -> Self {
228        let config = config.unwrap_or_default();
229        let external_clients: AHashSet<ClientId> = config
230            .external_clients
231            .clone()
232            .unwrap_or_default()
233            .into_iter()
234            .collect();
235
236        Self {
237            clock,
238            cache,
239            config,
240            msgbus_priority: 10, // High-priority for built-in component
241            external_clients,
242            subscriptions_external: SubscriptionRegistry::default(),
243            clients: IndexMap::new(),
244            default_client_id: None,
245            routing_map: IndexMap::new(),
246            book_intervals: AHashMap::new(),
247            book_snapshot_counts: IndexMap::new(),
248            book_snapshot_sources: AHashMap::new(),
249            book_deltas_counts: IndexMap::new(),
250            book_depth_counts: IndexMap::new(),
251            book_updaters: AHashMap::new(),
252            book_subscriptions: AHashMap::new(),
253            book_subscription_owners: AHashMap::new(),
254            book_snapshotters: AHashMap::new(),
255            bar_aggregators: IndexMap::new(),
256            bar_aggregator_handlers: AHashMap::new(),
257            subscriptions_bar_aggregation: AHashMap::new(),
258            request_bar_aggregations: AHashMap::new(),
259            request_pipeline_parent_request: AHashMap::new(),
260            request_pipeline_n_components: AHashMap::new(),
261            request_pipeline_parent_request_id: AHashMap::new(),
262            request_pipeline_responses: AHashMap::new(),
263            time_range_pipeline_requests: AHashMap::new(),
264            time_range_pipeline_parent_request_id: AHashMap::new(),
265            parent_join_request_id: AHashMap::new(),
266            pending_join_requests: AHashMap::new(),
267            continuous_future_requests: AHashMap::new(),
268            continuous_future_subscriptions: AHashMap::new(),
269            continuous_future_roller: None,
270            spread_quote_states: AHashMap::new(),
271            option_chain_managers: AHashMap::new(),
272            option_chain_instrument_index: AHashMap::new(),
273            deferred_cmd_queue: Rc::new(RefCell::new(VecDeque::new())),
274            option_chain_bootstrapper: None,
275            pending_option_chain_requests: AHashMap::new(),
276            option_chain_greeks_bootstraps: AHashMap::new(),
277            synthetic_quote_feeds: AHashMap::new(),
278            synthetic_trade_feeds: AHashMap::new(),
279            subscribed_synthetic_quotes: AHashMap::new(),
280            subscribed_synthetic_trades: AHashMap::new(),
281            buffered_deltas_map: AHashMap::new(),
282            deltas_frame: Vec::new(),
283            command_count: 0,
284            data_count: 0,
285            request_count: 0,
286            response_count: 0,
287            #[cfg(feature = "streaming")]
288            catalogs: CatalogMap::new(),
289            #[cfg(feature = "defi")]
290            pool_updaters: AHashMap::new(),
291            #[cfg(feature = "defi")]
292            pool_snapshot_pending: AHashMap::new(),
293            #[cfg(feature = "defi")]
294            pool_event_buffers: AHashMap::new(),
295        }
296    }
297
298    /// Returns a reference to the clock.
299    #[must_use]
300    pub fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
301        &self.clock
302    }
303
304    /// Returns a reference to the cache.
305    #[must_use]
306    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
307        &self.cache
308    }
309
310    /// Returns a reference to the configuration.
311    #[must_use]
312    pub const fn config(&self) -> &DataEngineConfig {
313        &self.config
314    }
315
316    #[cfg(feature = "defi")]
317    #[must_use]
318    pub(crate) const fn msgbus_priority(&self) -> u32 {
319        self.msgbus_priority
320    }
321
322    /// Registers all message bus handlers for the data engine.
323    pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
324        let weak = WeakCell::from(Rc::downgrade(engine));
325        engine.borrow_mut().continuous_future_roller =
326            Some(Rc::new(ContinuousFutureRoller::new(engine)));
327        engine.borrow_mut().option_chain_bootstrapper =
328            Some(Rc::new(OptionChainBootstrapper::new(engine)));
329
330        let weak1 = weak.clone();
331        msgbus::register_data_command_endpoint(
332            MessagingSwitchboard::data_engine_execute(),
333            TypedIntoHandler::from(move |cmd: DataCommand| {
334                if let Some(rc) = weak1.upgrade() {
335                    rc.borrow_mut().execute(cmd);
336                }
337            }),
338        );
339
340        msgbus::register_data_command_endpoint(
341            MessagingSwitchboard::data_engine_queue_execute(),
342            TypedIntoHandler::from(move |cmd: DataCommand| {
343                get_data_cmd_sender().clone().execute(cmd);
344            }),
345        );
346
347        // Register process handler (polymorphic - uses Any)
348        let weak2 = weak.clone();
349        msgbus::register_any(
350            MessagingSwitchboard::data_engine_process(),
351            ShareableMessageHandler::from_any(move |data: &dyn Any| {
352                if let Some(rc) = weak2.upgrade() {
353                    rc.borrow_mut().process(data);
354                }
355            }),
356        );
357
358        // Register process_data handler (typed - takes ownership)
359        let weak3 = weak.clone();
360        msgbus::register_data_endpoint(
361            MessagingSwitchboard::data_engine_process_data(),
362            TypedIntoHandler::from(move |data: Data| {
363                if let Some(rc) = weak3.upgrade() {
364                    rc.borrow_mut().process_data(data);
365                }
366            }),
367        );
368
369        // Register process_defi_data handler (typed - takes ownership)
370        #[cfg(feature = "defi")]
371        {
372            let weak4 = weak.clone();
373            msgbus::register_defi_data_endpoint(
374                MessagingSwitchboard::data_engine_process_defi_data(),
375                TypedIntoHandler::from(move |data: DefiData| {
376                    if let Some(rc) = weak4.upgrade() {
377                        rc.borrow_mut().process_defi_data(data);
378                    }
379                }),
380            );
381        }
382
383        let weak5 = weak;
384        msgbus::register_data_response_endpoint(
385            MessagingSwitchboard::data_engine_response(),
386            TypedIntoHandler::from(move |resp: DataResponse| {
387                if let Some(rc) = weak5.upgrade() {
388                    rc.borrow_mut().response(resp);
389                }
390            }),
391        );
392    }
393
394    #[cfg(feature = "defi")]
395    #[must_use]
396    pub(crate) fn is_external_client(&self, client_id: ClientId) -> bool {
397        self.external_clients.contains(&client_id)
398    }
399
400    /// Returns the total count of data commands received by the engine.
401    #[must_use]
402    pub const fn command_count(&self) -> u64 {
403        self.command_count
404    }
405
406    /// Returns the total count of data stream objects received by the engine.
407    #[must_use]
408    pub const fn data_count(&self) -> u64 {
409        self.data_count
410    }
411
412    #[cfg(feature = "defi")]
413    pub(crate) const fn increment_data_count(&mut self) {
414        self.data_count += 1;
415    }
416
417    /// Returns the total count of data requests received by the engine.
418    #[must_use]
419    pub const fn request_count(&self) -> u64 {
420        self.request_count
421    }
422
423    /// Returns the total count of data responses received by the engine.
424    #[must_use]
425    pub const fn response_count(&self) -> u64 {
426        self.response_count
427    }
428
429    /// Returns whether an `OptionChainManager` exists for the given series.
430    #[must_use]
431    pub fn has_option_chain_manager(&self, series_id: &OptionSeriesId) -> bool {
432        self.option_chain_managers.contains_key(series_id)
433    }
434
435    /// Returns the count of pending option-chain bootstrap requests.
436    #[must_use]
437    pub fn pending_option_chain_request_count(&self) -> usize {
438        self.pending_option_chain_requests.len()
439    }
440
441    /// Returns the number of request pipelines awaiting leg responses.
442    #[must_use]
443    pub fn request_pipeline_count(&self) -> usize {
444        self.request_pipeline_parent_request.len()
445    }
446
447    /// Returns the number of time-range pipelines awaiting child responses.
448    #[must_use]
449    pub fn time_range_pipeline_count(&self) -> usize {
450        self.time_range_pipeline_requests.len()
451    }
452
453    /// Returns the number of `RequestJoin` originals awaiting finalization.
454    #[must_use]
455    pub fn pending_join_request_count(&self) -> usize {
456        self.pending_join_requests.len()
457    }
458
459    /// Registers the `client` with the engine with an optional venue `routing`.
460    ///
461    ///
462    /// # Panics
463    ///
464    /// Panics if a client with the same client ID has already been registered.
465    pub fn register_client(&mut self, client: DataClientAdapter, routing: Option<Venue>) {
466        let client_id = client.client_id();
467
468        check_key_not_in_map(&client_id, &self.clients, "client_id", "clients").expect(FAILED);
469
470        if let Some(routing) = routing {
471            self.routing_map.insert(routing, client_id);
472            log::debug!("Set client {client_id} routing for {routing}");
473        }
474
475        if client.venue.is_none() && self.default_client_id.is_none() {
476            self.default_client_id = Some(client_id);
477            log::debug!("Registered client {client_id} for default routing");
478        }
479
480        self.clients.insert(client_id, client);
481        log::debug!("Registered client {client_id}");
482    }
483
484    /// Deregisters the client for the `client_id`.
485    ///
486    /// # Panics
487    ///
488    /// Panics if the client ID has not been registered.
489    pub fn deregister_client(&mut self, client_id: &ClientId) {
490        check_key_in_map(client_id, &self.clients, "client_id", "clients").expect(FAILED);
491
492        if self.default_client_id.as_ref() == Some(client_id) {
493            self.default_client_id = None;
494        }
495        self.clients.shift_remove(client_id);
496        log::info!("Deregistered client {client_id}");
497    }
498
499    /// Registers the data `client` with the engine as the default routing client.
500    ///
501    /// When a specific venue routing cannot be found, this client will receive messages.
502    ///
503    /// # Warnings
504    ///
505    /// Any existing default routing client will be overwritten.
506    ///
507    /// # Panics
508    ///
509    /// Panics if a default client has already been registered.
510    pub fn register_default_client(&mut self, client: DataClientAdapter) {
511        check_predicate_true(
512            self.default_client_id.is_none(),
513            "default client already registered",
514        )
515        .expect(FAILED);
516
517        let client_id = client.client_id();
518        self.clients.insert(client_id, client);
519        self.default_client_id = Some(client_id);
520        log::debug!("Registered default client {client_id}");
521    }
522
523    /// Marks an already-registered client as the default for fallback routing.
524    ///
525    /// # Errors
526    ///
527    /// Returns an error if no client is registered with the given ID, or a different
528    /// client is already the default.
529    pub fn set_default_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
530        if self.default_client_id.is_some_and(|id| id != client_id) {
531            anyhow::bail!("default client already registered");
532        }
533
534        if !self.clients.contains_key(&client_id) {
535            anyhow::bail!("No client registered with ID {client_id}");
536        }
537        self.default_client_id = Some(client_id);
538        log::debug!("Set client {client_id} as default");
539        Ok(())
540    }
541
542    /// Sets routing for a specific venue to a given client ID.
543    ///
544    /// # Errors
545    ///
546    /// Returns an error if the client ID is not registered, or the venue is already routed to a
547    /// different client.
548    pub fn register_venue_routing(
549        &mut self,
550        client_id: ClientId,
551        venue: Venue,
552    ) -> anyhow::Result<()> {
553        if !self.clients.contains_key(&client_id) {
554            anyhow::bail!("No client registered with ID {client_id}");
555        }
556
557        if let Some(existing_client_id) = self.routing_map.get(&venue)
558            && *existing_client_id != client_id
559        {
560            anyhow::bail!(
561                "Venue {venue} already routed to {existing_client_id}, \
562                 cannot re-route to {client_id}"
563            );
564        }
565
566        self.routing_map.insert(venue, client_id);
567        log::debug!("Set client {client_id} routing for {venue}");
568        Ok(())
569    }
570
571    /// Starts all registered data clients and re-arms bar aggregator timers.
572    pub fn start(&mut self) {
573        for client in self.get_clients_mut() {
574            if let Err(e) = client.start() {
575                log::error!("{e}");
576            }
577        }
578
579        for ((_, request_id), aggregator) in &self.bar_aggregators {
580            // Request-scoped or historical aggregators run on private clocks;
581            // re-arming them here would perturb an in-flight request's timer state
582            let is_subscription = request_id.is_none() && !aggregator.borrow().is_historical();
583            if is_subscription && aggregator.borrow().bar_type().spec().is_time_aggregated() {
584                aggregator
585                    .borrow_mut()
586                    .start_timer(Some(aggregator.clone()));
587            }
588        }
589
590        for state in self.spread_quote_states.values() {
591            state
592                .aggregator
593                .borrow_mut()
594                .start_timer(Some(state.aggregator.clone()));
595        }
596    }
597
598    /// Stops all registered data clients and bar aggregator timers.
599    pub fn stop(&mut self) {
600        for client in self.get_clients_mut() {
601            if let Err(e) = client.stop() {
602                log::error!("{e}");
603            }
604        }
605
606        for aggregator in self.bar_aggregators.values() {
607            aggregator.borrow_mut().stop();
608        }
609
610        for state in self.spread_quote_states.values() {
611            state.aggregator.borrow_mut().stop_timer();
612        }
613    }
614
615    /// Resets all registered data clients and clears engine state.
616    pub fn reset(&mut self) {
617        for client in self.get_clients_mut() {
618            match client.reset() {
619                Ok(()) => client.clear_subscription_state(),
620                Err(e) => log::error!("{e}"),
621            }
622        }
623
624        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
625        for (bar_type, request_id) in keys {
626            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
627                log::error!("Error stopping bar aggregator during reset for {bar_type}: {e}");
628            }
629        }
630        self.subscriptions_bar_aggregation.clear();
631
632        self.request_bar_aggregations.clear();
633        self.request_pipeline_parent_request.clear();
634        self.request_pipeline_n_components.clear();
635        self.request_pipeline_parent_request_id.clear();
636        self.request_pipeline_responses.clear();
637        self.time_range_pipeline_requests.clear();
638        self.time_range_pipeline_parent_request_id.clear();
639        self.parent_join_request_id.clear();
640        self.pending_join_requests.clear();
641        self.continuous_future_requests.clear();
642
643        for state in self.continuous_future_subscriptions.values_mut() {
644            if let Some(name) = state.timer_name.take() {
645                self.clock.borrow_mut().cancel_timer(&name);
646            }
647        }
648        self.continuous_future_subscriptions.clear();
649
650        let spread_ids: Vec<InstrumentId> = self.spread_quote_states.keys().copied().collect();
651        for spread_id in spread_ids {
652            self.stop_spread_quote_aggregation(spread_id);
653        }
654
655        // Tear down option chain managers to unregister their msgbus handlers
656        let managers: Vec<_> = self.option_chain_managers.drain().collect();
657        for (_, manager) in managers {
658            manager.borrow_mut().teardown(&self.clock);
659        }
660
661        self.option_chain_instrument_index.clear();
662        self.cancel_pending_option_chain_requests(None);
663        self.clear_option_chain_greeks_bootstraps();
664
665        // Unsubscribe BookUpdaters before dropping; otherwise the typed router
666        // keeps dispatching to abandoned updaters. `book_updaters` is keyed by
667        // per-underlying id, so the literal per-underlying topic is the same
668        // string the subscribe path used.
669        let book_updaters: Vec<(InstrumentId, Rc<BookUpdater>)> =
670            self.book_updaters.drain().collect();
671        for (instrument_id, updater) in book_updaters {
672            let deltas_topic = switchboard::get_book_deltas_topic(instrument_id);
673            let depth_topic = switchboard::get_book_depth_topic(instrument_id);
674            let deltas_handler: TypedHandler<OrderBookDeltas> = TypedHandler::new(updater.clone());
675            let depth_handler: TypedHandler<OrderBookDepth> = TypedHandler::new(updater);
676            msgbus::unsubscribe_book_deltas(deltas_topic.into(), &deltas_handler);
677            msgbus::unsubscribe_book_depth(depth_topic.into(), &depth_handler);
678        }
679
680        self.book_subscriptions.clear();
681        self.book_subscription_owners.clear();
682
683        self.book_deltas_counts.clear();
684        self.book_depth_counts.clear();
685        self.book_intervals.clear();
686        self.book_snapshot_counts.clear();
687        self.book_snapshot_sources.clear();
688        self.book_snapshotters.clear();
689        self.buffered_deltas_map.clear();
690        self.deltas_frame.clear();
691
692        self.synthetic_quote_feeds.clear();
693        self.synthetic_trade_feeds.clear();
694        self.subscribed_synthetic_quotes.clear();
695        self.subscribed_synthetic_trades.clear();
696        self.subscriptions_external.clear();
697
698        self.deferred_cmd_queue.borrow_mut().clear();
699
700        #[cfg(feature = "defi")]
701        self.clear_pool_updaters();
702
703        self.clock.borrow_mut().cancel_timers();
704
705        self.command_count = 0;
706        self.data_count = 0;
707        self.request_count = 0;
708        self.response_count = 0;
709    }
710
711    /// Disposes the engine, stopping all clients and canceling any timers.
712    pub fn dispose(&mut self) {
713        for client in self.get_clients_mut() {
714            if let Err(e) = client.dispose() {
715                log::error!("{e}");
716            }
717        }
718
719        self.clear_option_chain_greeks_bootstraps();
720
721        // Continuous-future source handlers live outside bar_aggregator_handlers,
722        // so release them before dropping the aggregators
723        let mut cf_sources = Vec::new();
724
725        for state in self.continuous_future_subscriptions.values_mut() {
726            if let Some(name) = state.timer_name.take() {
727                self.clock.borrow_mut().cancel_timer(&name);
728            }
729
730            if let Some(subscription) = state.active_source_subscription.take() {
731                cf_sources.push((state.target_bar_type, subscription));
732            }
733        }
734
735        for (target_bar_type, subscription) in cf_sources {
736            self.unsubscribe_continuous_future_source(target_bar_type, subscription);
737        }
738        self.continuous_future_subscriptions.clear();
739
740        // Unsubscribe aggregator msgbus handlers so the typed routers don't keep
741        // entries pointing at dropped aggregators
742        let keys: Vec<BarAggregatorKey> = self.bar_aggregators.keys().copied().collect();
743        for (bar_type, request_id) in keys {
744            if let Err(e) = self.stop_bar_aggregator(bar_type, request_id) {
745                log::error!("Error stopping bar aggregator during dispose for {bar_type}: {e}");
746            }
747        }
748
749        self.subscriptions_external.clear();
750
751        self.clock.borrow_mut().cancel_timers();
752    }
753
754    /// Connects all registered data clients concurrently.
755    ///
756    /// Connection failures are logged but do not prevent the node from running.
757    pub async fn connect(&mut self) {
758        let futures: Vec<_> = self
759            .get_clients_mut()
760            .into_iter()
761            .map(DataClientAdapter::connect)
762            .collect();
763
764        let results = join_all(futures).await;
765
766        for error in results.into_iter().filter_map(Result::err) {
767            log::error!("Failed to connect data client: {error}");
768        }
769    }
770
771    /// Disconnects all registered data clients concurrently.
772    ///
773    /// # Errors
774    ///
775    /// Returns an error if any client fails to disconnect.
776    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
777        let futures: Vec<_> = self
778            .get_clients_mut()
779            .into_iter()
780            .map(DataClientAdapter::disconnect)
781            .collect();
782
783        let results = join_all(futures).await;
784
785        // A closed session cannot answer, so discard its pending bootstraps
786        #[cfg(feature = "defi")]
787        self.abandon_pool_snapshots();
788
789        let errors: Vec<_> = results.into_iter().filter_map(Result::err).collect();
790
791        if errors.is_empty() {
792            Ok(())
793        } else {
794            let error_msgs: Vec<_> = errors.iter().map(ToString::to_string).collect();
795            anyhow::bail!(
796                "Failed to disconnect data clients: {}",
797                error_msgs.join("; ")
798            )
799        }
800    }
801
802    /// Returns `true` if all registered data clients are currently connected.
803    #[must_use]
804    pub fn check_connected(&self) -> bool {
805        self.get_clients()
806            .iter()
807            .all(|client| client.is_connected())
808    }
809
810    /// Returns `true` if all registered data clients are currently disconnected.
811    #[must_use]
812    pub fn check_disconnected(&self) -> bool {
813        self.get_clients()
814            .iter()
815            .all(|client| !client.is_connected())
816    }
817
818    /// Returns connection status for each registered client.
819    #[must_use]
820    pub fn client_connection_status(&self) -> Vec<(ClientId, bool)> {
821        self.get_clients()
822            .into_iter()
823            .map(|client| (client.client_id(), client.is_connected()))
824            .collect()
825    }
826
827    /// Returns a list of all registered client IDs, including the default client if set.
828    #[must_use]
829    pub fn registered_clients(&self) -> Vec<ClientId> {
830        self.get_clients()
831            .into_iter()
832            .map(|client| client.client_id())
833            .collect()
834    }
835
836    pub(crate) fn collect_subscriptions<F, T>(&self, get_subs: F) -> Vec<T>
837    where
838        F: Fn(&DataClientAdapter) -> &AHashSet<T>,
839        T: Clone,
840    {
841        self.get_clients()
842            .into_iter()
843            .flat_map(get_subs)
844            .cloned()
845            .collect()
846    }
847
848    #[must_use]
849    pub fn get_clients(&self) -> Vec<&DataClientAdapter> {
850        self.clients.values().collect()
851    }
852
853    #[must_use]
854    pub fn get_clients_mut(&mut self) -> Vec<&mut DataClientAdapter> {
855        self.clients.values_mut().collect()
856    }
857
858    pub fn get_client(
859        &mut self,
860        client_id: Option<&ClientId>,
861        venue: Option<&Venue>,
862    ) -> Option<&mut DataClientAdapter> {
863        if let Some(client_id) = client_id {
864            return self.clients.get_mut(client_id);
865        }
866
867        if let Some(v) = venue
868            && let Some(client_id) = self.routing_map.get(v)
869        {
870            return self.clients.get_mut(client_id);
871        }
872
873        self.get_default_client()
874    }
875
876    /// Resolves the client for a subscribe/unsubscribe command.
877    ///
878    /// When `BACKTEST` is registered, all commands route through it regardless of
879    /// the command's `client_id` or `venue`. Request paths skip this override.
880    fn get_command_client(
881        &mut self,
882        client_id: Option<&ClientId>,
883        venue: Option<&Venue>,
884    ) -> Option<&mut DataClientAdapter> {
885        let backtest_id = ClientId::new("BACKTEST");
886        if self.clients.contains_key(&backtest_id) {
887            return self.clients.get_mut(&backtest_id);
888        }
889        self.get_client(client_id, venue)
890    }
891
892    fn get_default_client(&mut self) -> Option<&mut DataClientAdapter> {
893        match self.default_client_id {
894            Some(id) => self.clients.get_mut(&id),
895            None => None,
896        }
897    }
898
899    /// Returns all custom data types currently subscribed across all clients.
900    #[must_use]
901    pub fn subscribed_custom_data(&self) -> Vec<DataType> {
902        self.collect_subscriptions(|client| &client.subscriptions_custom)
903    }
904
905    /// Returns all instrument IDs currently subscribed across all clients.
906    #[must_use]
907    pub fn subscribed_instruments(&self) -> Vec<InstrumentId> {
908        self.collect_subscriptions(|client| &client.subscriptions_instrument)
909    }
910
911    /// Returns all instrument IDs for which book delta subscriptions exist.
912    #[must_use]
913    pub fn subscribed_book_deltas(&self) -> Vec<InstrumentId> {
914        self.collect_subscriptions(|client| &client.subscriptions_book_deltas)
915    }
916
917    /// Returns all instrument IDs for which book depth subscriptions exist.
918    #[must_use]
919    pub fn subscribed_book_depth(&self) -> Vec<InstrumentId> {
920        self.collect_subscriptions(|client| &client.subscriptions_book_depth)
921    }
922
923    /// Returns all instrument IDs for which book snapshot subscriptions exist.
924    #[must_use]
925    pub fn subscribed_book_snapshots(&self) -> Vec<InstrumentId> {
926        self.book_snapshot_counts
927            .keys()
928            .map(|(instrument_id, _)| *instrument_id)
929            .collect()
930    }
931
932    /// Returns all instrument IDs for which quote subscriptions exist.
933    #[must_use]
934    pub fn subscribed_quotes(&self) -> Vec<InstrumentId> {
935        self.collect_subscriptions(|client| &client.subscriptions_quotes)
936    }
937
938    /// Returns all synthetic instrument IDs for which quote subscriptions exist.
939    #[must_use]
940    pub fn subscribed_synthetic_quotes(&self) -> Vec<InstrumentId> {
941        self.subscribed_synthetic_quotes.keys().copied().collect()
942    }
943
944    /// Returns all instrument IDs for which trade subscriptions exist.
945    #[must_use]
946    pub fn subscribed_trades(&self) -> Vec<InstrumentId> {
947        self.collect_subscriptions(|client| &client.subscriptions_trades)
948    }
949
950    /// Returns all synthetic instrument IDs for which trade subscriptions exist.
951    #[must_use]
952    pub fn subscribed_synthetic_trades(&self) -> Vec<InstrumentId> {
953        self.subscribed_synthetic_trades.keys().copied().collect()
954    }
955
956    /// Returns all bar types currently subscribed across all clients,
957    /// including internally aggregated subscriptions (v1 parity).
958    #[must_use]
959    pub fn subscribed_bars(&self) -> Vec<BarType> {
960        let mut subscribed = self.collect_subscriptions(|client| &client.subscriptions_bars);
961        subscribed.extend(
962            self.bar_aggregators
963                .keys()
964                .filter(|(_, request_id)| request_id.is_none())
965                .map(|(bar_type, _)| *bar_type),
966        );
967        subscribed
968    }
969
970    /// Returns all instrument IDs for which mark price subscriptions exist.
971    #[must_use]
972    pub fn subscribed_mark_prices(&self) -> Vec<InstrumentId> {
973        self.collect_subscriptions(|client| &client.subscriptions_mark_prices)
974    }
975
976    /// Returns all instrument IDs for which index price subscriptions exist.
977    #[must_use]
978    pub fn subscribed_index_prices(&self) -> Vec<InstrumentId> {
979        self.collect_subscriptions(|client| &client.subscriptions_index_prices)
980    }
981
982    /// Returns all instrument IDs for which funding rate subscriptions exist.
983    #[must_use]
984    pub fn subscribed_funding_rates(&self) -> Vec<InstrumentId> {
985        self.collect_subscriptions(|client| &client.subscriptions_funding_rates)
986    }
987
988    /// Returns all instrument IDs for which status subscriptions exist.
989    #[must_use]
990    pub fn subscribed_instrument_status(&self) -> Vec<InstrumentId> {
991        self.collect_subscriptions(|client| &client.subscriptions_instrument_status)
992    }
993
994    /// Returns all instrument IDs for which instrument close subscriptions exist.
995    #[must_use]
996    pub fn subscribed_instrument_close(&self) -> Vec<InstrumentId> {
997        self.collect_subscriptions(|client| &client.subscriptions_instrument_close)
998    }
999
1000    /// Executes a `DataCommand` by delegating to subscribe, unsubscribe, or request handlers.
1001    ///
1002    /// This is the final synchronous dispatch point for data commands. Runtime command producers
1003    /// should send to `DataEngine.queue_execute`, which lets the runner sequence command execution
1004    /// before this method runs. The engine also calls this method for child commands generated while
1005    /// processing a parent command, where immediate in-engine ordering matters.
1006    ///
1007    /// Errors during execution are logged.
1008    pub fn execute(&mut self, cmd: DataCommand) {
1009        match &cmd {
1010            DataCommand::Subscribe(_) | DataCommand::Unsubscribe(_) => self.command_count += 1,
1011            DataCommand::Request(_) => self.request_count += 1,
1012            #[cfg(feature = "defi")]
1013            DataCommand::DefiRequest(_) => self.request_count += 1,
1014            #[cfg(feature = "defi")]
1015            DataCommand::DefiSubscribe(_) | DataCommand::DefiUnsubscribe(_) => {
1016                self.command_count += 1;
1017            }
1018            _ => {}
1019        }
1020
1021        if let Err(e) = match cmd {
1022            DataCommand::Subscribe(c) => self.execute_subscribe(c),
1023            DataCommand::Unsubscribe(c) => self.execute_unsubscribe(&c),
1024            DataCommand::Request(c) => self.execute_request(c),
1025            #[cfg(feature = "defi")]
1026            DataCommand::DefiRequest(c) => self.execute_defi_request(c),
1027            #[cfg(feature = "defi")]
1028            DataCommand::DefiSubscribe(c) => self.execute_defi_subscribe(c),
1029            #[cfg(feature = "defi")]
1030            DataCommand::DefiUnsubscribe(c) => self.execute_defi_unsubscribe(&c),
1031            _ => {
1032                log::warn!("Unhandled DataCommand variant");
1033                Ok(())
1034            }
1035        } {
1036            log::error!("{e}");
1037        }
1038    }
1039
1040    /// Handles a subscribe command, updating internal state and forwarding to the client.
1041    ///
1042    /// # Errors
1043    ///
1044    /// Returns an error if the subscription is invalid (e.g., synthetic instrument for book data),
1045    /// or if the underlying client operation fails.
1046    pub fn execute_subscribe(&mut self, cmd: SubscribeCommand) -> anyhow::Result<()> {
1047        if let Some(client_id) = cmd.client_id()
1048            && self.external_clients.contains(client_id)
1049        {
1050            if let SubscribeCommand::OptionChain(command) = &cmd {
1051                self.retain_external_option_chain(*client_id, command, &cmd);
1052            } else if !self.subscriptions_external.retain(
1053                (*client_id, SubscriptionKey::from_subscribe(&cmd)),
1054                cmd.command_id(),
1055                cmd.clone(),
1056            ) {
1057                return Ok(());
1058            }
1059
1060            register_external_streaming_type(&cmd);
1061            publish_external_data_command(*client_id, &cmd);
1062
1063            if self.config.debug {
1064                log::debug!("Skipping subscribe command for external client {client_id}: {cmd:?}");
1065            }
1066
1067            return Ok(());
1068        }
1069
1070        // Update internal engine state
1071        match &cmd {
1072            SubscribeCommand::BookDeltas(book_cmd) => {
1073                if !self.subscribe_book_deltas(book_cmd)? && self.client_subscription_active(&cmd) {
1074                    return Ok(());
1075                }
1076            }
1077            SubscribeCommand::BookDepth(book_cmd) => {
1078                if !self.subscribe_book_depth(book_cmd)? && self.client_subscription_active(&cmd) {
1079                    return Ok(());
1080                }
1081            }
1082            SubscribeCommand::BookSnapshots(cmd) => {
1083                // Handles client forwarding internally (forwards as BookDeltas)
1084                return self.subscribe_book_snapshots(cmd);
1085            }
1086            SubscribeCommand::Bars(cmd) if has_continuous_future_params(cmd.params.as_ref()) => {
1087                return self.subscribe_continuous_future_bars(cmd);
1088            }
1089            SubscribeCommand::Bars(cmd) => {
1090                self.subscribe_bars(cmd)?;
1091                if cmd.bar_type.is_internally_aggregated() {
1092                    return Ok(());
1093                }
1094            }
1095            SubscribeCommand::OptionChain(cmd) if cmd.snapshot_interval_ms == Some(0) => {
1096                anyhow::bail!(
1097                    "Cannot subscribe option chain {} with a zero `snapshot_interval_ms`; use `None` for raw mode",
1098                    cmd.series_id,
1099                );
1100            }
1101            SubscribeCommand::OptionChain(cmd) => {
1102                self.subscribe_option_chain(cmd);
1103                return Ok(());
1104            }
1105            SubscribeCommand::Quotes(cmd) if cmd.instrument_id.is_synthetic() => {
1106                self.subscribe_synthetic_quotes(cmd.instrument_id);
1107                return Ok(());
1108            }
1109            SubscribeCommand::Quotes(cmd)
1110                if self.is_spread_quote_command(cmd.instrument_id, cmd.params.as_ref()) =>
1111            {
1112                self.subscribe_spread_quotes(cmd);
1113                return Ok(());
1114            }
1115            SubscribeCommand::Trades(cmd) if cmd.instrument_id.is_synthetic() => {
1116                self.subscribe_synthetic_trades(cmd.instrument_id);
1117                return Ok(());
1118            }
1119            SubscribeCommand::Instrument(cmd) if cmd.instrument_id.is_synthetic() => {
1120                anyhow::bail!("Cannot subscribe for synthetic instrument `Instrument` data");
1121            }
1122            SubscribeCommand::InstrumentStatus(cmd) if cmd.instrument_id.is_synthetic() => {
1123                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentStatus` data");
1124            }
1125            SubscribeCommand::InstrumentClose(cmd) if cmd.instrument_id.is_synthetic() => {
1126                anyhow::bail!("Cannot subscribe for synthetic instrument `InstrumentClose` data");
1127            }
1128            SubscribeCommand::OptionGreeks(cmd) if cmd.instrument_id.is_synthetic() => {
1129                anyhow::bail!("Cannot subscribe for synthetic instrument `OptionGreeks` data");
1130            }
1131            _ => {} // Do nothing else
1132        }
1133
1134        let retained = cmd.clone();
1135
1136        // Book ownership, including failed acquisitions, is already counted by the engine
1137        let retain_on_failure = !matches!(
1138            &cmd,
1139            SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookDepth(_)
1140        );
1141
1142        #[cfg(feature = "streaming")]
1143        let cmd = self.subscribe_command_with_prefilled_start_ns(cmd)?;
1144
1145        if let Some(client) = self.get_command_client(cmd.client_id(), cmd.venue()) {
1146            client.execute_subscribe_with_retained(cmd, retained, retain_on_failure);
1147        } else {
1148            log::error!(
1149                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
1150                cmd.client_id(),
1151                cmd.venue(),
1152            );
1153        }
1154
1155        Ok(())
1156    }
1157
1158    fn client_subscription_active(&mut self, cmd: &SubscribeCommand) -> bool {
1159        self.get_command_client(cmd.client_id(), cmd.venue())
1160            .is_some_and(|client| client.has_active_subscription(cmd))
1161    }
1162
1163    fn retain_external_option_chain(
1164        &mut self,
1165        client_id: ClientId,
1166        command: &SubscribeOptionChain,
1167        subscribe: &SubscribeCommand,
1168    ) {
1169        let owner_id = command.correlation_id.unwrap_or(command.command_id);
1170        let current_key = (client_id, SubscriptionKey::OptionChain(command.series_id));
1171        let previous_key = self
1172            .subscriptions_external
1173            .iter()
1174            .find_map(|(key, active)| {
1175                (matches!(
1176                    &key.1,
1177                    SubscriptionKey::OptionChain(series_id) if *series_id == command.series_id
1178                ) && active.acquisitions.contains(&owner_id))
1179                .then(|| key.clone())
1180            });
1181
1182        if let Some(previous_key) = previous_key
1183            && previous_key != current_key
1184        {
1185            let release = {
1186                let active = self
1187                    .subscriptions_external
1188                    .get_mut(&previous_key)
1189                    .expect("external option chain owner was present");
1190                active.acquisitions.remove(&owner_id);
1191                active.owners = active.owners.saturating_sub(1);
1192                active.owners == 0
1193            };
1194
1195            if release {
1196                let active = self
1197                    .subscriptions_external
1198                    .remove(&previous_key)
1199                    .expect("external option chain was present for final release");
1200                let unsubscribe =
1201                    active
1202                        .command
1203                        .into_unsubscribe(UUID4::new(), command.ts_init, Some(owner_id));
1204                publish_external_data_command(previous_key.0, &unsubscribe);
1205            }
1206        }
1207
1208        self.subscriptions_external
1209            .retain(current_key.clone(), owner_id, subscribe.clone());
1210        self.subscriptions_external
1211            .get_mut(&current_key)
1212            .expect("external option chain was retained")
1213            .command = subscribe.clone();
1214    }
1215
1216    /// Handles an unsubscribe command, updating internal state and forwarding to the client.
1217    ///
1218    /// # Errors
1219    ///
1220    /// Returns an error if the underlying client operation fails.
1221    pub fn execute_unsubscribe(&mut self, cmd: &UnsubscribeCommand) -> anyhow::Result<()> {
1222        if let Some(client_id) = cmd.client_id()
1223            && self.external_clients.contains(client_id)
1224        {
1225            let key = (*client_id, SubscriptionKey::from_unsubscribe(cmd));
1226            let command = match self.subscriptions_external.release(&key) {
1227                SubscriptionRelease::Retained => return Ok(()),
1228                SubscriptionRelease::Final(subscribe) => subscribe.into_unsubscribe(
1229                    cmd.command_id(),
1230                    cmd.ts_init(),
1231                    cmd.correlation_id(),
1232                ),
1233                SubscriptionRelease::Untracked => cmd.clone(),
1234            };
1235            self.subscriptions_external.remove(&key);
1236            publish_external_data_command(*client_id, &command);
1237
1238            if self.config.debug {
1239                log::debug!(
1240                    "Skipping unsubscribe command for external client {client_id}: {command:?}",
1241                );
1242            }
1243            return Ok(());
1244        }
1245
1246        if matches!(
1247            cmd,
1248            UnsubscribeCommand::BookDeltas(_)
1249                | UnsubscribeCommand::BookDepth(_)
1250                | UnsubscribeCommand::BookSnapshots(_)
1251        ) && !self.release_book_subscription(cmd)
1252        {
1253            return Ok(());
1254        }
1255
1256        match &cmd {
1257            UnsubscribeCommand::BookDeltas(cmd) if !self.unsubscribe_book_deltas(cmd) => {
1258                return Ok(());
1259            }
1260            UnsubscribeCommand::BookDepth(cmd) if !self.unsubscribe_book_depth(cmd) => {
1261                return Ok(());
1262            }
1263            UnsubscribeCommand::BookSnapshots(cmd) => {
1264                // Handles client forwarding internally (forwards as BookDeltas)
1265                self.unsubscribe_book_snapshots(cmd);
1266                return Ok(());
1267            }
1268            UnsubscribeCommand::Bars(cmd)
1269                if self
1270                    .continuous_future_subscriptions
1271                    .contains_key(&cmd.bar_type.standard()) =>
1272            {
1273                // Don't tear down the chain while other actors remain subscribed
1274                let topic = switchboard::get_bars_topic(cmd.bar_type.standard());
1275                if msgbus::exact_subscriber_count_bars(topic) == 0 {
1276                    self.unsubscribe_continuous_future_bars(cmd);
1277                }
1278                return Ok(());
1279            }
1280            UnsubscribeCommand::Bars(cmd) => {
1281                self.unsubscribe_bars(cmd);
1282                if cmd.bar_type.is_internally_aggregated() {
1283                    return Ok(());
1284                }
1285            }
1286            UnsubscribeCommand::OptionChain(cmd) => {
1287                self.unsubscribe_option_chain(cmd);
1288                return Ok(());
1289            }
1290            UnsubscribeCommand::Quotes(cmd) if cmd.instrument_id.is_synthetic() => {
1291                self.unsubscribe_synthetic_quotes(cmd.instrument_id);
1292                return Ok(());
1293            }
1294            UnsubscribeCommand::Quotes(cmd)
1295                if self.is_spread_quote_command(cmd.instrument_id, cmd.params.as_ref()) =>
1296            {
1297                self.unsubscribe_spread_quotes(cmd);
1298                return Ok(());
1299            }
1300            UnsubscribeCommand::Trades(cmd) if cmd.instrument_id.is_synthetic() => {
1301                self.unsubscribe_synthetic_trades(cmd.instrument_id);
1302                return Ok(());
1303            }
1304            UnsubscribeCommand::Instrument(cmd) if cmd.instrument_id.is_synthetic() => {
1305                anyhow::bail!("Cannot unsubscribe from synthetic instrument `Instrument` data");
1306            }
1307            UnsubscribeCommand::InstrumentStatus(cmd) if cmd.instrument_id.is_synthetic() => {
1308                anyhow::bail!(
1309                    "Cannot unsubscribe from synthetic instrument `InstrumentStatus` data"
1310                );
1311            }
1312            UnsubscribeCommand::InstrumentClose(cmd) if cmd.instrument_id.is_synthetic() => {
1313                anyhow::bail!(
1314                    "Cannot unsubscribe from synthetic instrument `InstrumentClose` data"
1315                );
1316            }
1317            UnsubscribeCommand::OptionGreeks(cmd) if cmd.instrument_id.is_synthetic() => {
1318                anyhow::bail!("Cannot unsubscribe from synthetic instrument `OptionGreeks` data");
1319            }
1320            _ => {}
1321        }
1322
1323        if let Some(client) = self.get_command_client(cmd.client_id(), cmd.venue()) {
1324            client.execute_unsubscribe(cmd);
1325        } else {
1326            log::error!(
1327                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
1328                cmd.client_id(),
1329                cmd.venue(),
1330            );
1331        }
1332
1333        Ok(())
1334    }
1335
1336    /// Sends a [`RequestCommand`] to a suitable data client implementation.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns an error if no client is found for the given client ID or venue,
1341    /// or if the client fails to process the request.
1342    pub fn execute_request(&mut self, req: RequestCommand) -> anyhow::Result<()> {
1343        // Skip requests for external clients
1344        if let Some(cid) = req.client_id()
1345            && self.external_clients.contains(cid)
1346        {
1347            if self.config.debug {
1348                log::debug!("Skipping data request for external client {cid}: {req:?}");
1349            }
1350            return Ok(());
1351        }
1352
1353        if let RequestCommand::Join(join) = req {
1354            return self.handle_request_join(join);
1355        }
1356
1357        if has_continuous_future_params(request_params(&req)) {
1358            return self.execute_continuous_future_request(req);
1359        }
1360
1361        let request_id = *req.request_id();
1362        self.prepare_request_bar_aggregators(&req)?;
1363
1364        if has_time_range_pipeline_params(request_params(&req))
1365            && is_time_range_pipeline_variant(&req)
1366        {
1367            let result = self.execute_time_range_pipeline_request(req);
1368            if result.is_err() {
1369                self.cleanup_request_bar_aggregators(&request_id);
1370            }
1371            return result;
1372        }
1373
1374        #[cfg(feature = "streaming")]
1375        if self.catalogs_registered() && streaming::is_date_range_variant(&req) {
1376            let result = self.dispatch_date_range_request(req);
1377            if result.is_err() {
1378                self.cleanup_request_bar_aggregators(&request_id);
1379            }
1380            return result;
1381        }
1382
1383        let result = self.dispatch_request_to_client(req);
1384
1385        if result.is_err() {
1386            self.cleanup_request_bar_aggregators(&request_id);
1387        }
1388
1389        result.map(|_| ())
1390    }
1391
1392    pub(super) fn dispatch_request_to_client(
1393        &mut self,
1394        req: RequestCommand,
1395    ) -> anyhow::Result<ClientId> {
1396        let client_id = req.client_id().copied();
1397        let venue = req.venue().copied();
1398        let Some(client) = self.get_client(client_id.as_ref(), venue.as_ref()) else {
1399            anyhow::bail!("Cannot handle request: no client found for {client_id:?} {venue:?}");
1400        };
1401        let resolved_client_id = client.client_id();
1402
1403        #[rustfmt::skip]
1404        match req {
1405            RequestCommand::Data(req) => client.request_data(req),
1406            RequestCommand::Instrument(req) => client.request_instrument(req),
1407            RequestCommand::Instruments(req) => client.request_instruments(req),
1408            RequestCommand::BookSnapshot(req) => client.request_book_snapshot(req),
1409            RequestCommand::BookDeltas(req) => client.request_book_deltas(req),
1410            RequestCommand::BookDepth(req) => client.request_book_depth(req),
1411            RequestCommand::Quotes(req) => client.request_quotes(req),
1412            RequestCommand::Trades(req) => client.request_trades(req),
1413            RequestCommand::FundingRates(req) => client.request_funding_rates(req),
1414            RequestCommand::OptionChainReferencePrice(req) => client.request_option_chain_reference_price(req),
1415            RequestCommand::Bars(req) => client.request_bars(req),
1416            RequestCommand::Join(_) => anyhow::bail!("RequestJoin must be handled by handle_request_join"),
1417        }?;
1418
1419        Ok(resolved_client_id)
1420    }
1421
1422    fn execute_continuous_future_request(&mut self, req: RequestCommand) -> anyhow::Result<()> {
1423        let RequestCommand::Bars(parent) = req else {
1424            anyhow::bail!("Continuous future requests require `RequestBars`");
1425        };
1426        let request_id = parent.request_id;
1427        let Some(continuous_request) = continuous_future_request_from_bars(&parent)? else {
1428            return Ok(());
1429        };
1430
1431        self.ensure_continuous_future_target_instrument(&continuous_request);
1432        self.prepare_request_bar_aggregators_from_state(
1433            request_id,
1434            &continuous_request.request_bar_aggregation,
1435        )?;
1436
1437        let response_client_id = match self.resolve_request_client_id(
1438            parent.client_id.as_ref(),
1439            Some(&continuous_request.primary_bar_type.instrument_id().venue),
1440        ) {
1441            Ok(client_id) => client_id,
1442            Err(e) => {
1443                self.cleanup_request_bar_aggregators(&request_id);
1444                return Err(e);
1445            }
1446        };
1447        let (cursor_ns, end_ns) = match self.bound_continuous_future_dates(&parent) {
1448            Ok(bounds) => bounds,
1449            Err(e) => {
1450                self.cleanup_request_bar_aggregators(&request_id);
1451                return Err(e);
1452            }
1453        };
1454
1455        self.continuous_future_requests.insert(
1456            request_id,
1457            ContinuousFutureRequestState {
1458                parent,
1459                request: continuous_request,
1460                start_ns: cursor_ns,
1461                cursor_ns,
1462                end_ns,
1463                response_client_id,
1464                data_count: 0,
1465            },
1466        );
1467
1468        if let Err(e) = self.dispatch_next_continuous_future_segment(request_id) {
1469            self.continuous_future_requests.remove(&request_id);
1470            self.cleanup_request_bar_aggregators(&request_id);
1471            return Err(e);
1472        }
1473
1474        Ok(())
1475    }
1476
1477    fn resolve_request_client_id(
1478        &mut self,
1479        client_id: Option<&ClientId>,
1480        venue: Option<&Venue>,
1481    ) -> anyhow::Result<ClientId> {
1482        self.get_client(client_id, venue)
1483            .map(|client| client.client_id())
1484            .ok_or_else(|| {
1485                anyhow::anyhow!(
1486                    "Cannot handle request: no client found for {client_id:?} {venue:?}"
1487                )
1488            })
1489    }
1490
1491    fn bound_continuous_future_dates(
1492        &self,
1493        request: &RequestBars,
1494    ) -> anyhow::Result<(UnixNanos, UnixNanos)> {
1495        let now = self.clock.borrow().timestamp_ns();
1496        let start = request
1497            .start
1498            .map(datetime_to_unix_nanos)
1499            .transpose()?
1500            .unwrap_or_default();
1501        let end = request
1502            .end
1503            .map(datetime_to_unix_nanos)
1504            .transpose()?
1505            .unwrap_or(now);
1506
1507        Ok((start.min(now), end.min(now)))
1508    }
1509
1510    fn ensure_continuous_future_target_instrument(&self, request: &ContinuousFutureRequest) {
1511        let target_id = request.primary_bar_type.instrument_id();
1512        if self.cache.borrow().instrument(&target_id).is_some() {
1513            return;
1514        }
1515
1516        let segment_id = request.first_segment_instrument_id();
1517        let segment_instrument = self.cache.borrow().instrument(&segment_id).cloned();
1518        let Some(segment_instrument) = segment_instrument else {
1519            log::warn!(
1520                "Cannot synthesize continuous future instrument {target_id}: first segment {segment_id} not in cache"
1521            );
1522            return;
1523        };
1524
1525        let InstrumentAny::FuturesContract(mut target) = segment_instrument else {
1526            log::warn!(
1527                "Cannot synthesize continuous future instrument {target_id}: segment {segment_id} is not a FuturesContract",
1528            );
1529            return;
1530        };
1531
1532        target.id = target_id;
1533        target.raw_symbol = target_id.symbol;
1534        target.activation_ns = UnixNanos::default();
1535        target.expiration_ns = UnixNanos::default();
1536
1537        if let Err(e) = self
1538            .cache
1539            .borrow_mut()
1540            .add_instrument(InstrumentAny::FuturesContract(target))
1541        {
1542            log_error_on_cache_insert(&e);
1543        }
1544    }
1545
1546    fn prepare_request_bar_aggregators_from_state(
1547        &mut self,
1548        request_id: UUID4,
1549        state: &RequestBarAggregation,
1550    ) -> anyhow::Result<()> {
1551        if !self.can_start_request_bar_aggregators(request_id, state) {
1552            anyhow::bail!(
1553                "Cannot request aggregated bars: one of the aggregators in `bar_types` is already running"
1554            );
1555        }
1556
1557        self.request_bar_aggregations
1558            .insert(request_id, state.clone());
1559
1560        if let Err(e) = self.init_request_bar_aggregators(request_id, state) {
1561            self.cleanup_request_bar_aggregators(&request_id);
1562            return Err(e);
1563        }
1564
1565        Ok(())
1566    }
1567
1568    fn dispatch_next_continuous_future_segment(&mut self, request_id: UUID4) -> anyhow::Result<()> {
1569        let Some(state) = self.continuous_future_requests.get(&request_id).cloned() else {
1570            anyhow::bail!("No active continuous future request for {request_id}");
1571        };
1572
1573        let Some(segment) = state
1574            .request
1575            .next_segment(state.cursor_ns.as_u64(), state.end_ns.as_u64())
1576        else {
1577            self.emit_empty_continuous_future_response(request_id);
1578            return Ok(());
1579        };
1580
1581        self.apply_continuous_future_adjustment(request_id, &state.request, segment.index)?;
1582        let child = self.build_continuous_future_child_request(request_id, &state, segment);
1583        if let Some(active) = self.continuous_future_requests.get_mut(&request_id) {
1584            active.cursor_ns = UnixNanos::from(segment.end_ns.saturating_add(1));
1585        }
1586
1587        self.execute_request(child)
1588    }
1589
1590    fn apply_continuous_future_adjustment(
1591        &self,
1592        request_id: UUID4,
1593        request: &ContinuousFutureRequest,
1594        segment_index: usize,
1595    ) -> anyhow::Result<()> {
1596        let adjustment = request.adjustment_for_segment(segment_index);
1597        let key = bar_aggregator_key(request.primary_bar_type, Some(request_id));
1598        let aggregator = self.bar_aggregators.get(&key).ok_or_else(|| {
1599            anyhow::anyhow!("No aggregator for continuous future request {request_id}")
1600        })?;
1601        aggregator
1602            .borrow_mut()
1603            .set_adjustment(adjustment, request.adjustment_mode);
1604
1605        Ok(())
1606    }
1607
1608    fn build_continuous_future_child_request(
1609        &self,
1610        request_id: UUID4,
1611        state: &ContinuousFutureRequestState,
1612        segment: ContinuousFutureSegment,
1613    ) -> RequestCommand {
1614        let source = state.request.source_for_segment(segment.instrument_id);
1615        let start = Some(UnixNanos::from(segment.start_ns).to_datetime_utc());
1616        let end = Some(UnixNanos::from(segment.end_ns).to_datetime_utc());
1617        let child_params = Some(
1618            state
1619                .request
1620                .child_params(state.parent.params.as_ref(), request_id),
1621        );
1622        let child_request_id = UUID4::new();
1623        let ts_init = self.clock.borrow().timestamp_ns();
1624
1625        match source {
1626            ContinuousFutureSource::Bars(bar_type) => RequestCommand::Bars(RequestBars::new(
1627                bar_type,
1628                start,
1629                end,
1630                state.parent.limit,
1631                state.parent.client_id,
1632                child_request_id,
1633                ts_init,
1634                child_params,
1635            )),
1636            ContinuousFutureSource::Trades => RequestCommand::Trades(RequestTrades::new(
1637                segment.instrument_id,
1638                start,
1639                end,
1640                state.parent.limit,
1641                state.parent.client_id,
1642                child_request_id,
1643                ts_init,
1644                child_params,
1645            )),
1646            ContinuousFutureSource::Quotes => RequestCommand::Quotes(RequestQuotes::new(
1647                segment.instrument_id,
1648                start,
1649                end,
1650                state.parent.limit,
1651                state.parent.client_id,
1652                child_request_id,
1653                ts_init,
1654                child_params,
1655            )),
1656        }
1657    }
1658
1659    fn emit_empty_continuous_future_response(&mut self, request_id: UUID4) {
1660        let Some(state) = self.continuous_future_requests.remove(&request_id) else {
1661            return;
1662        };
1663
1664        let mut params = state.parent.params.unwrap_or_default();
1665        if state.data_count != 0 {
1666            params.insert(
1667                "data_count".to_string(),
1668                serde_json::json!(state.data_count),
1669            );
1670        }
1671
1672        let response = DataResponse::Bars(BarsResponse::new(
1673            request_id,
1674            state.response_client_id,
1675            state.parent.bar_type,
1676            Vec::new(),
1677            Some(state.start_ns),
1678            Some(state.end_ns),
1679            self.clock.borrow().timestamp_ns(),
1680            Some(params),
1681        ));
1682        self.response(response);
1683    }
1684
1685    fn prepare_request_bar_aggregators(&mut self, req: &RequestCommand) -> anyhow::Result<()> {
1686        let request_id = *req.request_id();
1687        let Some(state) = request_bar_aggregation_from_params(request_params(req))? else {
1688            return Ok(());
1689        };
1690
1691        self.prepare_request_bar_aggregators_from_state(request_id, &state)
1692    }
1693
1694    fn can_start_request_bar_aggregators(
1695        &self,
1696        request_id: UUID4,
1697        state: &RequestBarAggregation,
1698    ) -> bool {
1699        let aggregator_request_id = state.aggregator_request_id(request_id);
1700        state.bar_types.iter().all(|bar_type| {
1701            let key = bar_aggregator_key(*bar_type, aggregator_request_id);
1702            self.bar_aggregators
1703                .get(&key)
1704                .is_none_or(|aggregator| !aggregator.borrow().is_running())
1705        })
1706    }
1707
1708    fn init_request_bar_aggregators(
1709        &mut self,
1710        request_id: UUID4,
1711        state: &RequestBarAggregation,
1712    ) -> anyhow::Result<()> {
1713        let aggregator_request_id = state.aggregator_request_id(request_id);
1714
1715        for bar_type in &state.bar_types {
1716            self.create_bar_aggregator_for_key(
1717                *bar_type,
1718                aggregator_request_id,
1719                state.skip_first_non_full_bar,
1720            )?;
1721            self.setup_bar_aggregator(*bar_type, true, aggregator_request_id)?;
1722
1723            let key = bar_aggregator_key(*bar_type, aggregator_request_id);
1724            if let Some(aggregator) = self.bar_aggregators.get(&key) {
1725                if state.disable_build_with_no_updates {
1726                    aggregator.borrow_mut().set_build_with_no_updates(false);
1727                }
1728                aggregator.borrow_mut().set_is_running(true);
1729            }
1730        }
1731
1732        self.set_request_bar_aggregator_chain_handlers(request_id, state);
1733
1734        Ok(())
1735    }
1736
1737    fn set_request_bar_aggregator_chain_handlers(
1738        &self,
1739        request_id: UUID4,
1740        state: &RequestBarAggregation,
1741    ) {
1742        let aggregator_request_id = state.aggregator_request_id(request_id);
1743
1744        for bar_type in &state.bar_types {
1745            let key = bar_aggregator_key(*bar_type, aggregator_request_id);
1746            let Some(aggregator) = self.bar_aggregators.get(&key).cloned() else {
1747                continue;
1748            };
1749
1750            let downstream: Vec<_> = state
1751                .bar_types
1752                .iter()
1753                .filter(|candidate| {
1754                    candidate.is_composite()
1755                        && candidate.composite().standard() == bar_type.standard()
1756                })
1757                .filter_map(|candidate| {
1758                    let key = bar_aggregator_key(*candidate, aggregator_request_id);
1759                    self.bar_aggregators.get(&key).cloned()
1760                })
1761                .collect();
1762            let cache = self.cache.clone();
1763            let handler: Box<dyn FnMut(Bar)> = Box::new(move |bar: Bar| {
1764                // Request-generated bars are delivered only through the cache.
1765                if let Err(e) = cache.as_ref().borrow_mut().add_bar_historical(bar) {
1766                    log_error_on_cache_insert(&e);
1767                }
1768
1769                for aggregator in &downstream {
1770                    aggregator.borrow_mut().handle_bar(bar);
1771                }
1772            });
1773
1774            aggregator.borrow_mut().set_historical_mode(true, handler);
1775        }
1776    }
1777
1778    fn cleanup_request_bar_aggregators(&mut self, request_id: &UUID4) -> bool {
1779        let Some(state) = self.request_bar_aggregations.remove(request_id) else {
1780            return false;
1781        };
1782        let aggregator_request_id = state.aggregator_request_id(*request_id);
1783
1784        for bar_type in state.bar_types {
1785            let key = bar_aggregator_key(bar_type, aggregator_request_id);
1786            let has_live_handlers =
1787                state.update_subscriptions && self.bar_aggregator_handlers.contains_key(&key);
1788            let keep_running = if has_live_handlers {
1789                match self.setup_bar_aggregator(bar_type, false, aggregator_request_id) {
1790                    Ok(()) => true,
1791                    Err(e) => {
1792                        log::error!(
1793                            "Error starting live request bar aggregator for {bar_type}: {e}"
1794                        );
1795                        false
1796                    }
1797                }
1798            } else {
1799                false
1800            };
1801
1802            if let Some(aggregator) = self.bar_aggregators.get(&key) {
1803                aggregator.borrow_mut().set_is_running(keep_running);
1804            }
1805
1806            if !state.update_subscriptions
1807                && let Err(e) = self.stop_bar_aggregator(bar_type, aggregator_request_id)
1808            {
1809                log::error!("Error stopping request bar aggregator for {bar_type}: {e}");
1810            }
1811        }
1812
1813        true
1814    }
1815
1816    /// Processes a dynamically-typed data message.
1817    ///
1818    /// Currently supports `InstrumentAny`, funding rates, option greeks, instrument status, and
1819    /// custom data; unrecognized types are logged as errors.
1820    pub fn process(&mut self, data: &dyn Any) {
1821        self.data_count += 1;
1822        // Dynamically-typed entry point: `FundingRateUpdate`, `OptionGreeks`, `InstrumentStatus`,
1823        // and custom data are also `Data` enum variants handled in `process_data`, but can arrive
1824        // here as typed data, whereas `InstrumentAny` is not a `Data` variant.
1825        if let Some(instrument) = data.downcast_ref::<InstrumentAny>() {
1826            self.handle_instrument(instrument);
1827        } else if let Some(funding_rate) = data.downcast_ref::<FundingRateUpdate>() {
1828            self.handle_funding_rate(*funding_rate);
1829        } else if let Some(option_greeks) = data.downcast_ref::<OptionGreeks>() {
1830            self.cache.borrow_mut().add_option_greeks(*option_greeks);
1831            self.feed_option_greeks_to_pre_bootstrap_chain(option_greeks);
1832            let topic = switchboard::get_option_greeks_topic(option_greeks.instrument_id);
1833            msgbus::publish_option_greeks(topic, option_greeks);
1834            self.drain_deferred_commands();
1835        } else if let Some(status) = data.downcast_ref::<InstrumentStatus>() {
1836            self.handle_instrument_status(*status);
1837        } else if let Some(custom) = data.downcast_ref::<CustomData>() {
1838            self.handle_custom_data(custom);
1839        } else {
1840            #[cfg(feature = "defi")]
1841            if let Some(response) = data.downcast_ref::<PoolSnapshotResponse>() {
1842                self.handle_pool_snapshot_response(response);
1843                return;
1844            }
1845
1846            log::error!("Cannot process data {data:?}, type is unrecognized");
1847        }
1848    }
1849
1850    /// Processes a `Data` enum instance, dispatching to live handlers.
1851    #[allow(
1852        clippy::needless_pass_by_value,
1853        reason = "callers hand over ownership; the payload is only moved when a DeFi handler consumes it"
1854    )]
1855    pub fn process_data(&mut self, data: Data) {
1856        #[cfg(feature = "defi")]
1857        let data = match data {
1858            Data::Defi(defi) => {
1859                self.process_defi_data(*defi);
1860                return;
1861            }
1862            data => data,
1863        };
1864
1865        self.process_data_ref(DataRef::from(&data));
1866    }
1867
1868    /// Processes a borrowed `Data` enum view, dispatching to live handlers.
1869    ///
1870    /// DeFi payloads are cloned because the DeFi handler may buffer them while a pool snapshot
1871    /// is pending; no other variant clones its payload.
1872    pub fn process_data_ref(&mut self, data: DataRef<'_>) {
1873        #[cfg(feature = "defi")]
1874        let data = match data {
1875            DataRef::Defi(defi) => {
1876                self.process_defi_data(defi.clone());
1877                return;
1878            }
1879            data => data,
1880        };
1881
1882        self.data_count += 1;
1883
1884        match data {
1885            DataRef::Instrument(instrument) => self.handle_instrument(instrument),
1886            DataRef::BookDelta(delta) => self.handle_delta(*delta),
1887            DataRef::BookDeltas(deltas) => self.handle_deltas(deltas),
1888            DataRef::BookDepth(depth) => self.handle_depth(depth),
1889            DataRef::Quote(quote) => {
1890                self.handle_quote(*quote);
1891                self.drain_deferred_commands();
1892            }
1893            DataRef::Trade(trade) => self.handle_trade(*trade),
1894            DataRef::Bar(bar) => self.handle_bar(*bar),
1895            DataRef::MarkPrice(mark_price) => {
1896                self.handle_mark_price(*mark_price);
1897                self.drain_deferred_commands();
1898            }
1899            DataRef::IndexPrice(index_price) => {
1900                self.handle_index_price(*index_price);
1901                self.drain_deferred_commands();
1902            }
1903            DataRef::FundingRate(funding_rate) => {
1904                self.handle_funding_rate(*funding_rate);
1905                self.drain_deferred_commands();
1906            }
1907            DataRef::OptionGreeks(greeks) => {
1908                self.cache.borrow_mut().add_option_greeks(*greeks);
1909                self.feed_option_greeks_to_pre_bootstrap_chain(greeks);
1910                let topic = switchboard::get_option_greeks_topic(greeks.instrument_id);
1911                msgbus::publish_option_greeks(topic, greeks);
1912                self.drain_deferred_commands();
1913            }
1914            DataRef::InstrumentStatus(status) => {
1915                self.handle_instrument_status(*status);
1916                self.drain_deferred_commands();
1917            }
1918            DataRef::InstrumentClose(close) => self.handle_instrument_close(*close),
1919            DataRef::Custom(custom) => self.handle_custom_data(custom),
1920            #[cfg(feature = "defi")]
1921            DataRef::Defi(_) => unreachable!("handled before market data dispatch"),
1922            #[cfg(not(feature = "defi"))]
1923            #[allow(
1924                unreachable_patterns,
1925                reason = "DeFi variants can exist without this crate's defi feature"
1926            )]
1927            other => log_defi_data_dropped(other),
1928        }
1929    }
1930
1931    fn feed_option_greeks_to_pre_bootstrap_chain(&mut self, greeks: &OptionGreeks) {
1932        let Some(series_id) = self
1933            .option_chain_instrument_index
1934            .get(&greeks.instrument_id)
1935            .copied()
1936        else {
1937            return;
1938        };
1939
1940        let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
1941            return;
1942        };
1943
1944        if manager_rc.borrow().is_bootstrapped() {
1945            return;
1946        }
1947
1948        manager_rc.borrow_mut().handle_greeks(greeks);
1949
1950        if manager_rc.borrow().is_bootstrapped() {
1951            // Apply the chain's own subscribes before releasing the bootstrap owner, so a sample
1952            // inside the active window keeps its feed without a physical unsubscribe.
1953            self.drain_deferred_commands();
1954            self.stop_option_chain_greeks_bootstrap(series_id);
1955        }
1956    }
1957
1958    /// Processes a `Data` instance through the pipeline bus path.
1959    ///
1960    /// Pipeline mode publishes each item on the `data.pipeline.` topic family and gates cache
1961    /// writes on `disable_historical_cache`. None of the live-only side effects (synthetic
1962    /// republish, option-chain expiry, depth-derived quotes, deferred-command drains) run in this
1963    /// path.
1964    pub fn process_pipeline(&mut self, data: Data) {
1965        #[cfg(feature = "defi")]
1966        let data = match data {
1967            Data::Defi(defi) => {
1968                self.process_defi_data(*defi);
1969                return;
1970            }
1971            data => data,
1972        };
1973
1974        self.data_count += 1;
1975
1976        match data {
1977            Data::Instrument(instrument) => self.handle_instrument(&instrument),
1978            Data::BookDelta(delta) => self.handle_delta_pipeline(delta),
1979            Data::BookDeltas(deltas) => self.handle_deltas_pipeline(&deltas),
1980            Data::BookDepth(depth) => self.handle_depth_pipeline(&depth),
1981            Data::Quote(quote) => self.handle_quote_pipeline(quote),
1982            Data::Trade(trade) => self.handle_trade_pipeline(trade),
1983            Data::Bar(bar) => self.handle_bar_pipeline(bar),
1984            Data::MarkPrice(mark_price) => self.handle_mark_price_pipeline(mark_price),
1985            Data::IndexPrice(index_price) => self.handle_index_price_pipeline(index_price),
1986            Data::FundingRate(funding_rate) => {
1987                self.handle_funding_rate_pipeline(funding_rate);
1988            }
1989            Data::OptionGreeks(greeks) => self.handle_option_greeks_pipeline(greeks),
1990            Data::InstrumentStatus(status) => self.handle_instrument_status_pipeline(status),
1991            Data::InstrumentClose(close) => self.handle_instrument_close_pipeline(close),
1992            Data::Custom(custom) => self.handle_custom_data_pipeline(&custom),
1993            #[cfg(feature = "defi")]
1994            Data::Defi(_) => unreachable!("handled before market data dispatch"),
1995            #[cfg(not(feature = "defi"))]
1996            #[allow(
1997                unreachable_patterns,
1998                reason = "DeFi variants can exist without this crate's defi feature"
1999            )]
2000            other => log_defi_data_dropped(DataRef::from(&other)),
2001        }
2002    }
2003
2004    /// Processes a `DataResponse`, handling and publishing the response message.
2005    pub fn response(&mut self, mut resp: DataResponse) {
2006        if log::log_enabled!(log::Level::Debug) {
2007            let correlation_id = resp.correlation_id();
2008            match resp.record_count() {
2009                Some(count) => log::debug!(
2010                    "{RECV}{RES} {} correlation_id={correlation_id} records={count}",
2011                    resp.kind(),
2012                ),
2013                None => log::debug!(
2014                    "{RECV}{RES} {} correlation_id={correlation_id}",
2015                    resp.kind(),
2016                ),
2017            }
2018        }
2019        log::trace!("{RECV}{RES} {resp:?}");
2020
2021        self.response_count += 1;
2022
2023        resp.trim_to_bounds();
2024
2025        let Some(resp) = self.handle_request_pipeline_response(resp) else {
2026            return;
2027        };
2028
2029        // Catalog legs inherit the child's params, so route only the assembled segment
2030        if let Some(parent_id) = continuous_future_parent_request_id(response_params(&resp)) {
2031            self.handle_continuous_future_child_response(parent_id, &resp);
2032            return;
2033        }
2034
2035        if let Some(parent_id) = self
2036            .time_range_pipeline_parent_request_id
2037            .remove(resp.correlation_id())
2038        {
2039            self.handle_time_range_pipeline_child_response(parent_id, &resp);
2040            return;
2041        }
2042
2043        if self
2044            .parent_join_request_id
2045            .contains_key(resp.correlation_id())
2046        {
2047            self.finalize_request_join(resp);
2048            return;
2049        }
2050
2051        let correlation_id = *resp.correlation_id();
2052
2053        match &resp {
2054            DataResponse::Instrument(r) => {
2055                self.handle_instrument_response(r.data.clone());
2056            }
2057            DataResponse::Instruments(r) => {
2058                self.handle_instruments(&r.data);
2059            }
2060            DataResponse::Quotes(r) => {
2061                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2062                    self.handle_quotes(&r.data);
2063                }
2064            }
2065            DataResponse::Trades(r) => {
2066                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2067                    self.handle_trades(&r.data);
2068                }
2069            }
2070            DataResponse::FundingRates(r) => {
2071                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2072                    self.handle_funding_rates(&r.data);
2073                }
2074            }
2075            DataResponse::Bars(r) => {
2076                if !log_if_empty_response(&r.data, &r.bar_type, &correlation_id) {
2077                    self.handle_bars(&r.data);
2078                }
2079            }
2080            DataResponse::Book(r) => self.handle_book_response(&r.data),
2081            DataResponse::BookDeltas(r) => {
2082                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2083                    self.handle_book_deltas_response(r);
2084                }
2085            }
2086            DataResponse::BookDepth(r) => {
2087                if !log_if_empty_response(&r.data, &r.instrument_id, &correlation_id) {
2088                    self.handle_book_depth_response(r);
2089                }
2090            }
2091            DataResponse::OptionChainReferencePrice(r) => {
2092                self.process_request_bar_aggregation_response(&resp);
2093                return self.handle_option_chain_reference_price_response(&correlation_id, r);
2094            }
2095            DataResponse::Data(_) => {}
2096        }
2097
2098        self.process_request_bar_aggregation_response(&resp);
2099
2100        msgbus::send_response(&correlation_id, &resp);
2101    }
2102
2103    /// Registers a parent request whose response will be rebuilt from `n_components` leg responses.
2104    pub fn new_request_pipeline(&mut self, parent: RequestCommand, n_components: usize) {
2105        let parent_id = *parent.request_id();
2106        self.request_pipeline_n_components
2107            .insert(parent_id, n_components);
2108        self.request_pipeline_parent_request
2109            .insert(parent_id, parent);
2110        self.request_pipeline_responses
2111            .insert(parent_id, Vec::with_capacity(n_components));
2112    }
2113
2114    /// Registers a leg `request_id` as a child of the pipeline keyed by `parent_id`.
2115    pub fn register_request_pipeline_leg(&mut self, leg_id: UUID4, parent_id: UUID4) {
2116        self.request_pipeline_parent_request_id
2117            .insert(leg_id, parent_id);
2118    }
2119
2120    /// Fans a leg response into its parent pipeline and emits the rebuilt response when all legs arrive.
2121    ///
2122    /// Responses whose `correlation_id` is not part of any pipeline pass through unchanged.
2123    /// While accumulating legs, returns `None` so the caller skips further response handling.
2124    fn handle_request_pipeline_response(&mut self, resp: DataResponse) -> Option<DataResponse> {
2125        let leg_id = *resp.correlation_id();
2126        let Some(parent_id) = self.request_pipeline_parent_request_id.remove(&leg_id) else {
2127            return Some(resp);
2128        };
2129
2130        let Some(buf) = self.request_pipeline_responses.get_mut(&parent_id) else {
2131            log::error!("Pipeline response buffer missing for parent {parent_id} (leg {leg_id})");
2132            return Some(resp);
2133        };
2134        buf.push(resp);
2135
2136        let expected = self.request_pipeline_n_components.get(&parent_id).copied();
2137        let received = buf.len();
2138        match expected {
2139            Some(n) if received < n => return None,
2140            Some(_) => {}
2141            None => {
2142                log::error!("Pipeline n_components missing for parent {parent_id}");
2143                return None;
2144            }
2145        }
2146
2147        let mut legs = self.request_pipeline_responses.remove(&parent_id)?;
2148        self.request_pipeline_n_components.remove(&parent_id);
2149        let parent = self.request_pipeline_parent_request.remove(&parent_id);
2150
2151        for leg in &mut legs {
2152            leg.trim_to_bounds();
2153        }
2154
2155        let (parent_start, parent_end) = parent_request_window(parent.as_ref());
2156        let rebuilt = rebuild_pipeline_response(parent_id, parent.as_ref(), legs);
2157
2158        // If the rebuild failed (mixed-variant, unsupported-variant, or mixed-instrument
2159        // `BookDeltas` legs), drop the associated `RequestJoin` so its staging maps do not
2160        // leak. Without this the
2161        // original join request stays in `pending_join_requests` and its
2162        // `parent_join_request_id` mapping stays live, neither of which will ever
2163        // resolve through normal flow.
2164        if rebuilt.is_none()
2165            && let Some(original_id) = self.parent_join_request_id.remove(&parent_id)
2166        {
2167            self.pending_join_requests.remove(&original_id);
2168            log::error!(
2169                "Dropped RequestJoin {original_id} because pipeline rebuild failed for dated parent {parent_id}"
2170            );
2171        }
2172
2173        let mut rebuilt = rebuilt?;
2174
2175        // Replay must run before `trim_to_bounds`, which would otherwise discard the pre-start
2176        // deltas the replay folds into the snapshot.
2177        if let DataResponse::BookDeltas(r) = &mut rebuilt {
2178            self.book_deltas_snapshot_replay(r);
2179        }
2180
2181        // Trim against the parent window only when the parent supplied one. With no
2182        // parent window the rebuilt response inherits the first leg's bounds; legs are
2183        // already trimmed against their own bounds at the top of `response()`, so a
2184        // second pass would discard data from later legs whose bounds the parent never
2185        // constrained.
2186        if parent_start.is_some() || parent_end.is_some() {
2187            rebuilt.trim_to_bounds();
2188        }
2189
2190        Some(rebuilt)
2191    }
2192
2193    // Replays a day-start snapshot forward to the request's original start: when the first delta
2194    // is an F_SNAPSHOT on a UTC day boundary, rebuilds the book from the pre-start deltas and
2195    // replaces them with one snapshot keyed at the original start, then forwards the rest.
2196    fn book_deltas_snapshot_replay(&self, resp: &mut BookDeltasResponse) {
2197        let Some(original_start_ns) = resp.start else {
2198            return;
2199        };
2200
2201        let Some(first) = resp.data.first().copied() else {
2202            return;
2203        };
2204
2205        if !RecordFlag::F_SNAPSHOT.matches(first.flags) {
2206            return;
2207        }
2208
2209        if first.ts_init.as_u64() % NANOSECONDS_IN_DAY != 0 {
2210            return;
2211        }
2212
2213        // Nothing to fast-forward when the request starts at or before the day-start snapshot
2214        if original_start_ns <= first.ts_init {
2215            return;
2216        }
2217
2218        if self
2219            .cache
2220            .borrow()
2221            .instrument(&resp.instrument_id)
2222            .is_none()
2223        {
2224            log::warn!(
2225                "Instrument {} not found in cache, skipping snapshot replay",
2226                resp.instrument_id,
2227            );
2228            return;
2229        }
2230
2231        let book_type = resp
2232            .params
2233            .as_ref()
2234            .and_then(|p| p.get_str("book_type"))
2235            .and_then(|s| BookType::from_str(s).ok())
2236            .unwrap_or(BookType::L2_MBP);
2237
2238        let mut book = OrderBook::new(resp.instrument_id, book_type);
2239        let mut before: Vec<OrderBookDelta> = Vec::new();
2240        let mut after: Vec<OrderBookDelta> = Vec::new();
2241        let mut last_applied_ts: Option<UnixNanos> = None;
2242        let mut crossed = false;
2243
2244        for delta in &resp.data {
2245            if crossed {
2246                after.push(*delta);
2247            } else {
2248                before.push(*delta);
2249                if delta.ts_init >= original_start_ns {
2250                    crossed = true;
2251                    last_applied_ts = Some(delta.ts_init);
2252                }
2253            }
2254        }
2255
2256        if !before.is_empty() {
2257            if last_applied_ts.is_none() {
2258                last_applied_ts = before.last().map(|d| d.ts_init);
2259            }
2260
2261            let batch = OrderBookDeltas::new(resp.instrument_id, before);
2262            if let Err(e) = book.apply_deltas(&batch) {
2263                log::error!(
2264                    "Failed to rebuild book for snapshot replay on {}: {e}",
2265                    resp.instrument_id,
2266                );
2267                return;
2268            }
2269        }
2270
2271        let Some(last_ts) = last_applied_ts else {
2272            return;
2273        };
2274
2275        let snapshot_ts = last_ts.max(original_start_ns);
2276        let mut new_data = book.to_deltas(snapshot_ts, snapshot_ts).deltas;
2277        new_data.extend(after);
2278        resp.data = new_data;
2279    }
2280
2281    fn handle_request_join(&mut self, req: RequestJoin) -> anyhow::Result<()> {
2282        if has_time_range_pipeline_params(req.params.as_ref()) {
2283            return self.execute_time_range_pipeline_request(RequestCommand::Join(req));
2284        }
2285
2286        let now_ns = self.clock.borrow().timestamp_ns();
2287        let now_dt = now_ns.to_datetime_utc();
2288        let zero = jiff::Timestamp::UNIX_EPOCH;
2289        let start = req.start.unwrap_or(zero).min(now_dt);
2290        let end = req.end.unwrap_or(now_dt).min(now_dt);
2291        let dated = req.with_dates(Some(start), Some(end), now_ns);
2292
2293        let original_id = req.request_id;
2294        let dated_id = dated.request_id;
2295
2296        self.pending_join_requests.insert(original_id, req);
2297        self.parent_join_request_id.insert(dated_id, original_id);
2298
2299        let leg_ids: Vec<UUID4> = dated.request_ids.clone();
2300        self.new_request_pipeline(RequestCommand::Join(dated), leg_ids.len());
2301        for leg_id in leg_ids {
2302            self.register_request_pipeline_leg(leg_id, dated_id);
2303        }
2304
2305        Ok(())
2306    }
2307
2308    fn finalize_request_join(&mut self, resp: DataResponse) {
2309        let dated_id = *resp.correlation_id();
2310        let Some(original_id) = self.parent_join_request_id.remove(&dated_id) else {
2311            log::error!("parent_join_request_id missing for dated correlation {dated_id}");
2312            return;
2313        };
2314
2315        let Some(original) = self.pending_join_requests.remove(&original_id) else {
2316            log::error!("pending_join_requests missing for original {original_id}");
2317            return;
2318        };
2319
2320        let now_ns = self.clock.borrow().timestamp_ns();
2321
2322        // Empty leg responses fire each leg's callback so caller-side request
2323        // workflows clean up. Per-leg metadata is reconstructed from the
2324        // rebuilt parent response and may not match a leg's original
2325        // instrument_id/bar_type when the join spans heterogeneous legs;
2326        // tracked as a follow-up in #5 (needs an in-flight leg-request cache).
2327        for leg_request_id in &original.request_ids {
2328            let empty = empty_response_like(&resp, *leg_request_id, now_ns);
2329            msgbus::send_response(leg_request_id, &empty);
2330        }
2331
2332        // Route the final join response through the normal response path so
2333        // bounds-trim against the parent window runs and the per-variant
2334        // handlers (cache writes, request bar aggregators) fire. The pipeline
2335        // and join staging maps for this request have already been popped, so
2336        // the recursive call cannot re-enter either gate.
2337        let final_resp = rebind_response_correlation(resp, original_id);
2338        self.response(final_resp);
2339    }
2340
2341    fn process_request_bar_aggregation_response(&mut self, resp: &DataResponse) {
2342        let correlation_id = *resp.correlation_id();
2343        let Some(state) = self.request_bar_aggregations.get(&correlation_id).cloned() else {
2344            return;
2345        };
2346
2347        match resp {
2348            DataResponse::Quotes(r) => {
2349                for quote in &r.data {
2350                    self.update_request_bar_aggregators_from_quote(&state, correlation_id, *quote);
2351                }
2352            }
2353            DataResponse::Trades(r) => {
2354                for trade in &r.data {
2355                    self.update_request_bar_aggregators_from_trade(&state, correlation_id, *trade);
2356                }
2357            }
2358            DataResponse::Bars(r) => {
2359                for bar in &r.data {
2360                    self.update_request_bar_aggregators_from_bar(&state, correlation_id, *bar);
2361                }
2362            }
2363            _ => {}
2364        }
2365
2366        self.cleanup_request_bar_aggregators(&correlation_id);
2367    }
2368
2369    fn handle_continuous_future_child_response(&mut self, parent_id: UUID4, resp: &DataResponse) {
2370        if !self.continuous_future_requests.contains_key(&parent_id) {
2371            log::error!("No active continuous future request for child response {parent_id}");
2372            return;
2373        }
2374
2375        let data_count = response_params(resp)
2376            .and_then(|params| params.get("data_count"))
2377            .and_then(serde_json::Value::as_u64)
2378            .or_else(|| resp.record_count().map(|count| count as u64))
2379            .unwrap_or(0);
2380
2381        if let Some(state) = self.continuous_future_requests.get_mut(&parent_id) {
2382            state.data_count += data_count;
2383        }
2384
2385        match resp {
2386            DataResponse::Quotes(r) => {
2387                if !log_if_empty_response(&r.data, &r.instrument_id, resp.correlation_id()) {
2388                    self.handle_quotes(&r.data);
2389                }
2390            }
2391            DataResponse::Trades(r) => {
2392                if !log_if_empty_response(&r.data, &r.instrument_id, resp.correlation_id()) {
2393                    self.handle_trades(&r.data);
2394                }
2395            }
2396            DataResponse::Bars(r) => {
2397                if !log_if_empty_response(&r.data, &r.bar_type, resp.correlation_id()) {
2398                    self.handle_bars(&r.data);
2399                }
2400            }
2401            _ => {
2402                log::error!(
2403                    "Continuous future child response {parent_id} must contain quotes, trades, or bars"
2404                );
2405                return;
2406            }
2407        }
2408
2409        self.process_continuous_future_aggregation_response(parent_id, resp);
2410        if let Err(e) = self.dispatch_next_continuous_future_segment(parent_id) {
2411            log::error!("Error dispatching continuous future segment for {parent_id}: {e}");
2412            self.emit_empty_continuous_future_response(parent_id);
2413        }
2414    }
2415
2416    fn process_continuous_future_aggregation_response(
2417        &self,
2418        parent_id: UUID4,
2419        resp: &DataResponse,
2420    ) {
2421        let Some(state) = self.continuous_future_requests.get(&parent_id) else {
2422            return;
2423        };
2424        let primary_bar_type = state.request.primary_bar_type;
2425        let aggregator_request_id = Some(parent_id);
2426
2427        match resp {
2428            DataResponse::Quotes(r) => {
2429                for quote in &r.data {
2430                    self.update_request_bar_aggregator(
2431                        primary_bar_type,
2432                        aggregator_request_id,
2433                        |aggregator| {
2434                            aggregator.handle_quote(*quote);
2435                        },
2436                    );
2437                }
2438            }
2439            DataResponse::Trades(r) => {
2440                for trade in &r.data {
2441                    self.update_request_bar_aggregator(
2442                        primary_bar_type,
2443                        aggregator_request_id,
2444                        |aggregator| {
2445                            aggregator.handle_trade(*trade);
2446                        },
2447                    );
2448                }
2449            }
2450            DataResponse::Bars(r) => {
2451                for bar in &r.data {
2452                    self.update_request_bar_aggregator(
2453                        primary_bar_type,
2454                        aggregator_request_id,
2455                        |aggregator| {
2456                            aggregator.handle_bar(*bar);
2457                        },
2458                    );
2459                }
2460            }
2461            _ => {}
2462        }
2463    }
2464
2465    fn update_request_bar_aggregators_from_quote(
2466        &self,
2467        state: &RequestBarAggregation,
2468        request_id: UUID4,
2469        quote: QuoteTick,
2470    ) {
2471        let aggregator_request_id = state.aggregator_request_id(request_id);
2472
2473        for bar_type in &state.bar_types {
2474            if bar_type.is_composite()
2475                || bar_type.instrument_id() != quote.instrument_id
2476                || bar_type.spec().price_type == PriceType::Last
2477            {
2478                continue;
2479            }
2480
2481            self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
2482                aggregator.handle_quote(quote);
2483            });
2484        }
2485    }
2486
2487    fn update_request_bar_aggregators_from_trade(
2488        &self,
2489        state: &RequestBarAggregation,
2490        request_id: UUID4,
2491        trade: TradeTick,
2492    ) {
2493        let aggregator_request_id = state.aggregator_request_id(request_id);
2494
2495        for bar_type in &state.bar_types {
2496            if bar_type.is_composite()
2497                || bar_type.instrument_id() != trade.instrument_id
2498                || bar_type.spec().price_type != PriceType::Last
2499            {
2500                continue;
2501            }
2502
2503            self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
2504                aggregator.handle_trade(trade);
2505            });
2506        }
2507    }
2508
2509    fn update_request_bar_aggregators_from_bar(
2510        &self,
2511        state: &RequestBarAggregation,
2512        request_id: UUID4,
2513        bar: Bar,
2514    ) {
2515        let aggregator_request_id = state.aggregator_request_id(request_id);
2516
2517        for bar_type in &state.bar_types {
2518            if !bar_type.is_composite()
2519                || bar_type.composite().standard() != bar.bar_type.standard()
2520            {
2521                continue;
2522            }
2523
2524            self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
2525                aggregator.handle_bar(bar);
2526            });
2527        }
2528    }
2529
2530    fn update_request_bar_aggregator<F>(
2531        &self,
2532        bar_type: BarType,
2533        request_id: Option<UUID4>,
2534        update: F,
2535    ) where
2536        F: FnOnce(&mut dyn BarAggregator),
2537    {
2538        let key = bar_aggregator_key(bar_type, request_id);
2539        let Some(aggregator) = self.bar_aggregators.get(&key) else {
2540            log::error!("Cannot update request bar aggregator: no aggregator found for {bar_type}");
2541            return;
2542        };
2543
2544        update(aggregator.borrow_mut().as_mut());
2545    }
2546
2547    #[inline]
2548    fn pipeline_cache_writes_allowed(&self) -> bool {
2549        !self.config.disable_historical_cache
2550    }
2551
2552    pub(crate) fn handle_instrument(&mut self, instrument: &InstrumentAny) {
2553        log::debug!("Handling instrument: {}", instrument.id());
2554
2555        if let Err(e) = self
2556            .cache
2557            .as_ref()
2558            .borrow_mut()
2559            .add_instrument(instrument.clone())
2560        {
2561            log_error_on_cache_insert(&e);
2562        }
2563
2564        let topic = switchboard::get_instrument_topic(instrument.id());
2565        log::debug!("Publishing instrument to topic: {topic}");
2566        msgbus::publish_instrument(topic, instrument);
2567
2568        self.update_option_chains(instrument);
2569    }
2570
2571    fn update_option_chains(&mut self, instrument: &InstrumentAny) {
2572        let Some(underlying) = instrument.underlying() else {
2573            return;
2574        };
2575        let Some(expiration_ns) = instrument.expiration_ns() else {
2576            return;
2577        };
2578        let Some(strike) = instrument.strike_price() else {
2579            return;
2580        };
2581        let Some(kind) = instrument.option_kind() else {
2582            return;
2583        };
2584
2585        let venue = instrument.id().venue;
2586        let settlement = instrument.settlement_currency().code;
2587        let series_id = OptionSeriesId::new(venue, underlying, settlement, expiration_ns);
2588
2589        // Clone Rc to release borrow on self.option_chain_managers before accessing self.clients
2590        let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
2591            return;
2592        };
2593
2594        let clock = self.clock.clone();
2595        let client_id = manager_rc.borrow().client_id();
2596        let client = self.get_command_client(client_id.as_ref(), Some(&venue));
2597
2598        if manager_rc
2599            .borrow_mut()
2600            .add_instrument(instrument.id(), strike, kind, client, &clock)
2601        {
2602            self.option_chain_instrument_index
2603                .insert(instrument.id(), series_id);
2604        }
2605    }
2606
2607    fn handle_delta(&mut self, delta: OrderBookDelta) {
2608        let mut deltas = if self.config.buffer_deltas {
2609            self.buffer_delta(delta);
2610
2611            if !RecordFlag::F_LAST.matches(delta.flags) {
2612                return; // Not the last delta for event
2613            }
2614
2615            self.buffered_deltas_map
2616                .remove(&delta.instrument_id)
2617                .expect("buffered deltas exist")
2618        } else {
2619            self.single_delta_batch(delta)
2620        };
2621
2622        let topic = switchboard::get_book_deltas_topic(deltas.instrument_id);
2623        msgbus::publish_deltas(topic, &deltas);
2624        self.reclaim_deltas_frame(mem::take(&mut deltas.deltas));
2625    }
2626
2627    fn handle_deltas(&mut self, deltas: &OrderBookDeltas) {
2628        if self.config.buffer_deltas {
2629            let instrument_id = deltas.instrument_id;
2630
2631            for delta in &deltas.deltas {
2632                let is_last = RecordFlag::F_LAST.matches(delta.flags);
2633                self.buffer_delta(*delta);
2634
2635                if is_last {
2636                    let mut deltas_to_publish = self
2637                        .buffered_deltas_map
2638                        .remove(&instrument_id)
2639                        .expect("buffered deltas exist");
2640                    let topic = switchboard::get_book_deltas_topic(instrument_id);
2641                    msgbus::publish_deltas(topic, &deltas_to_publish);
2642                    self.reclaim_deltas_frame(mem::take(&mut deltas_to_publish.deltas));
2643                }
2644            }
2645        } else {
2646            let topic = switchboard::get_book_deltas_topic(deltas.instrument_id);
2647            msgbus::publish_deltas(topic, deltas);
2648        }
2649    }
2650
2651    fn handle_depth(&self, depth: &OrderBookDepth) {
2652        let topic = switchboard::get_book_depth_topic(depth.instrument_id);
2653        msgbus::publish_depth(topic, depth);
2654
2655        if self.config.emit_quotes_from_book_depths
2656            && let Some(quote) = derive_quote_from_depth(depth)
2657        {
2658            book::publish_quote_if_changed(&self.cache, quote);
2659        }
2660    }
2661
2662    fn handle_quote(&self, quote: QuoteTick) {
2663        if let Err(e) = self.cache.as_ref().borrow_mut().add_quote(quote) {
2664            log_error_on_cache_insert(&e);
2665        }
2666
2667        for synthetic_quote in self.synthetic_quotes_from_quote(quote) {
2668            let topic = switchboard::get_quotes_topic(synthetic_quote.instrument_id);
2669            msgbus::publish_quote(topic, &synthetic_quote);
2670        }
2671
2672        let topic = switchboard::get_quotes_topic(quote.instrument_id);
2673        msgbus::publish_quote(topic, &quote);
2674    }
2675
2676    fn handle_trade(&self, trade: TradeTick) {
2677        if let Err(e) = self.cache.as_ref().borrow_mut().add_trade(trade) {
2678            log_error_on_cache_insert(&e);
2679        }
2680
2681        for synthetic_trade in self.synthetic_trades_from_trade(trade) {
2682            let topic = switchboard::get_trades_topic(synthetic_trade.instrument_id);
2683            msgbus::publish_trade(topic, &synthetic_trade);
2684        }
2685
2686        let topic = switchboard::get_trades_topic(trade.instrument_id);
2687        msgbus::publish_trade(topic, &trade);
2688    }
2689
2690    fn synthetic_quotes_from_quote(&self, update: QuoteTick) -> Vec<QuoteTick> {
2691        let Some(synthetics) = self.synthetic_quote_feeds.get(&update.instrument_id) else {
2692            return Vec::new();
2693        };
2694
2695        synthetics
2696            .iter()
2697            .filter_map(|synthetic| self.synthetic_quote_from_update(synthetic, update))
2698            .collect()
2699    }
2700
2701    fn synthetic_quote_from_update(
2702        &self,
2703        synthetic: &SyntheticInstrument,
2704        update: QuoteTick,
2705    ) -> Option<QuoteTick> {
2706        let cache = self.cache.borrow();
2707        let mut bid_inputs = Vec::with_capacity(synthetic.components.len());
2708        let mut ask_inputs = Vec::with_capacity(synthetic.components.len());
2709
2710        for instrument_id in &synthetic.components {
2711            let (bid_price, ask_price) = if *instrument_id == update.instrument_id {
2712                (update.bid_price, update.ask_price)
2713            } else {
2714                let Some(component_quote) = cache.quote(instrument_id) else {
2715                    log::warn!(
2716                        "Cannot calculate synthetic instrument {} price, no quotes for {} yet",
2717                        synthetic.id,
2718                        instrument_id,
2719                    );
2720                    return None;
2721                };
2722                (component_quote.bid_price, component_quote.ask_price)
2723            };
2724
2725            bid_inputs.push(bid_price.as_f64());
2726            ask_inputs.push(ask_price.as_f64());
2727        }
2728        drop(cache);
2729
2730        let bid_price = match synthetic.calculate(&bid_inputs) {
2731            Ok(price) => price,
2732            Err(e) => {
2733                log::error!(
2734                    "Cannot calculate synthetic instrument {} bid price: {e}",
2735                    synthetic.id
2736                );
2737                return None;
2738            }
2739        };
2740        let ask_price = match synthetic.calculate(&ask_inputs) {
2741            Ok(price) => price,
2742            Err(e) => {
2743                log::error!(
2744                    "Cannot calculate synthetic instrument {} ask price: {e}",
2745                    synthetic.id
2746                );
2747                return None;
2748            }
2749        };
2750        let size_one = Quantity::from(1);
2751
2752        Some(QuoteTick::new(
2753            synthetic.id,
2754            bid_price,
2755            ask_price,
2756            size_one,
2757            size_one,
2758            update.ts_event,
2759            self.clock.borrow().timestamp_ns(),
2760        ))
2761    }
2762
2763    fn synthetic_trades_from_trade(&self, update: TradeTick) -> Vec<TradeTick> {
2764        let Some(synthetics) = self.synthetic_trade_feeds.get(&update.instrument_id) else {
2765            return Vec::new();
2766        };
2767
2768        synthetics
2769            .iter()
2770            .filter_map(|synthetic| self.synthetic_trade_from_update(synthetic, update))
2771            .collect()
2772    }
2773
2774    fn synthetic_trade_from_update(
2775        &self,
2776        synthetic: &SyntheticInstrument,
2777        update: TradeTick,
2778    ) -> Option<TradeTick> {
2779        let cache = self.cache.borrow();
2780        let mut inputs = Vec::with_capacity(synthetic.components.len());
2781
2782        for instrument_id in &synthetic.components {
2783            let price = if *instrument_id == update.instrument_id {
2784                update.price
2785            } else {
2786                let Some(component_trade) = cache.trade(instrument_id) else {
2787                    log::warn!(
2788                        "Cannot calculate synthetic instrument {} price, no trades for {} yet",
2789                        synthetic.id,
2790                        instrument_id,
2791                    );
2792                    return None;
2793                };
2794                component_trade.price
2795            };
2796
2797            inputs.push(price.as_f64());
2798        }
2799        drop(cache);
2800
2801        let price = match synthetic.calculate(&inputs) {
2802            Ok(price) => price,
2803            Err(e) => {
2804                log::error!(
2805                    "Cannot calculate synthetic instrument {} trade price: {e}",
2806                    synthetic.id
2807                );
2808                return None;
2809            }
2810        };
2811
2812        Some(TradeTick::new(
2813            synthetic.id,
2814            price,
2815            Quantity::from(1),
2816            update.aggressor_side,
2817            update.trade_id,
2818            update.ts_event,
2819            self.clock.borrow().timestamp_ns(),
2820        ))
2821    }
2822
2823    fn handle_bar(&self, bar: Bar) {
2824        process_engine_bar(&self.cache, self.config.validate_data_sequence, true, bar);
2825    }
2826
2827    fn handle_mark_price(&self, mark_price: MarkPriceUpdate) {
2828        if let Err(e) = self.cache.as_ref().borrow_mut().add_mark_price(mark_price) {
2829            log_error_on_cache_insert(&e);
2830        }
2831
2832        let topic = switchboard::get_mark_price_topic(mark_price.instrument_id);
2833        msgbus::publish_mark_price(topic, &mark_price);
2834    }
2835
2836    fn handle_index_price(&self, index_price: IndexPriceUpdate) {
2837        if let Err(e) = self
2838            .cache
2839            .as_ref()
2840            .borrow_mut()
2841            .add_index_price(index_price)
2842        {
2843            log_error_on_cache_insert(&e);
2844        }
2845
2846        let topic = switchboard::get_index_price_topic(index_price.instrument_id);
2847        msgbus::publish_index_price(topic, &index_price);
2848    }
2849
2850    /// Handles a funding rate update by adding it to the cache and publishing to the message bus.
2851    pub fn handle_funding_rate(&mut self, funding_rate: FundingRateUpdate) {
2852        if let Err(e) = self
2853            .cache
2854            .as_ref()
2855            .borrow_mut()
2856            .add_funding_rate(funding_rate)
2857        {
2858            log_error_on_cache_insert(&e);
2859        }
2860
2861        let topic = switchboard::get_funding_rate_topic(funding_rate.instrument_id);
2862        msgbus::publish_funding_rate(topic, &funding_rate);
2863    }
2864
2865    fn handle_instrument_status(&mut self, status: InstrumentStatus) {
2866        if let Err(e) = self
2867            .cache
2868            .as_ref()
2869            .borrow_mut()
2870            .add_instrument_status(status)
2871        {
2872            log_error_on_cache_insert(&e);
2873        }
2874
2875        let topic = switchboard::get_instrument_status_topic(status.instrument_id);
2876        msgbus::publish_any(topic, &status);
2877
2878        if self
2879            .option_chain_instrument_index
2880            .contains_key(&status.instrument_id)
2881            && matches!(
2882                status.action,
2883                MarketStatusAction::Close | MarketStatusAction::NotAvailableForTrading
2884            )
2885        {
2886            self.expire_option_chain_instrument(status.instrument_id);
2887        }
2888    }
2889
2890    /// Removes a settled/expired instrument from its option chain manager.
2891    ///
2892    /// Looks up the owning series via the reverse index, delegates removal to
2893    /// the manager (which unregisters msgbus handlers and pushes deferred wire
2894    /// unsubscribes), then drains those commands. When the series catalog
2895    /// becomes empty, the entire manager is torn down.
2896    fn expire_option_chain_instrument(&mut self, instrument_id: InstrumentId) {
2897        let Some(series_id) = self.option_chain_instrument_index.remove(&instrument_id) else {
2898            return;
2899        };
2900
2901        if self
2902            .option_chain_greeks_bootstraps
2903            .get(&series_id)
2904            .is_some_and(|bootstrap| bootstrap.instrument_id == instrument_id)
2905        {
2906            self.stop_option_chain_greeks_bootstrap(series_id);
2907        }
2908
2909        let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
2910            return;
2911        };
2912
2913        let series_empty = manager_rc
2914            .borrow_mut()
2915            .handle_instrument_expired(&instrument_id);
2916
2917        // Drain deferred unsubscribe commands pushed by the manager
2918        self.drain_deferred_commands();
2919
2920        log::info!(
2921            "Expired instrument {instrument_id} from option chain {series_id} (series_empty={series_empty})",
2922        );
2923
2924        if series_empty {
2925            manager_rc.borrow_mut().teardown(&self.clock);
2926            self.option_chain_managers.remove(&series_id);
2927
2928            log::info!("Torn down empty option chain manager for {series_id}");
2929        }
2930    }
2931
2932    fn handle_instrument_close(&self, close: InstrumentClose) {
2933        let topic = switchboard::get_instrument_close_topic(close.instrument_id);
2934        msgbus::publish_any(topic, &close);
2935    }
2936
2937    fn handle_custom_data(&self, custom: &CustomData) {
2938        log::debug!("Processing custom data: {}", custom.data.type_name());
2939        let topic = switchboard::get_custom_topic(&custom.data_type);
2940        msgbus::publish_any(topic, custom);
2941    }
2942
2943    fn handle_delta_pipeline(&mut self, delta: OrderBookDelta) {
2944        // Pipeline deltas are not buffered; replays arrive pre-batched
2945        let mut deltas = self.single_delta_batch(delta);
2946        let topic = switchboard::get_pipeline_book_deltas_topic(deltas.instrument_id);
2947        msgbus::publish_deltas(topic, &deltas);
2948        self.reclaim_deltas_frame(mem::take(&mut deltas.deltas));
2949    }
2950
2951    fn buffer_delta(&mut self, delta: OrderBookDelta) {
2952        if let Some(buffered_deltas) = self.buffered_deltas_map.get_mut(&delta.instrument_id) {
2953            buffered_deltas.deltas.push(delta);
2954            buffered_deltas.flags = delta.flags;
2955            buffered_deltas.sequence = delta.sequence;
2956            buffered_deltas.ts_event = delta.ts_event;
2957            buffered_deltas.ts_init = delta.ts_init;
2958            return;
2959        }
2960
2961        let instrument_id = delta.instrument_id;
2962        let buffered_deltas = self.single_delta_batch(delta);
2963        self.buffered_deltas_map
2964            .insert(instrument_id, buffered_deltas);
2965    }
2966
2967    fn single_delta_batch(&mut self, delta: OrderBookDelta) -> OrderBookDeltas {
2968        let instrument_id = delta.instrument_id;
2969        let mut frame = mem::take(&mut self.deltas_frame);
2970        frame.clear();
2971        frame.push(delta);
2972        OrderBookDeltas::new(instrument_id, frame)
2973    }
2974
2975    fn reclaim_deltas_frame(&mut self, mut frame: Vec<OrderBookDelta>) {
2976        frame.clear();
2977        self.deltas_frame = frame;
2978    }
2979
2980    fn handle_deltas_pipeline(&self, deltas: &OrderBookDeltas) {
2981        let topic = switchboard::get_pipeline_book_deltas_topic(deltas.instrument_id);
2982        msgbus::publish_deltas(topic, deltas);
2983    }
2984
2985    fn handle_depth_pipeline(&self, depth: &OrderBookDepth) {
2986        let topic = switchboard::get_pipeline_book_depth_topic(depth.instrument_id);
2987        msgbus::publish_depth(topic, depth);
2988    }
2989
2990    fn handle_quote_pipeline(&self, quote: QuoteTick) {
2991        if self.pipeline_cache_writes_allowed()
2992            && let Err(e) = self.cache.as_ref().borrow_mut().add_quote(quote)
2993        {
2994            log_error_on_cache_insert(&e);
2995        }
2996
2997        let topic = switchboard::get_pipeline_quotes_topic(quote.instrument_id);
2998        msgbus::publish_quote(topic, &quote);
2999    }
3000
3001    fn handle_trade_pipeline(&self, trade: TradeTick) {
3002        if self.pipeline_cache_writes_allowed()
3003            && let Err(e) = self.cache.as_ref().borrow_mut().add_trade(trade)
3004        {
3005            log_error_on_cache_insert(&e);
3006        }
3007
3008        let topic = switchboard::get_pipeline_trades_topic(trade.instrument_id);
3009        msgbus::publish_trade(topic, &trade);
3010    }
3011
3012    fn handle_bar_pipeline(&self, bar: Bar) {
3013        if !validate_bar_sequence(&self.cache, self.config.validate_data_sequence, &bar) {
3014            return;
3015        }
3016
3017        if self.pipeline_cache_writes_allowed()
3018            && let Err(e) = self.cache.as_ref().borrow_mut().add_bar(bar)
3019        {
3020            log_error_on_cache_insert(&e);
3021        }
3022
3023        let topic = switchboard::get_pipeline_bars_topic(bar.bar_type);
3024        msgbus::publish_bar(topic, &bar);
3025    }
3026
3027    fn handle_mark_price_pipeline(&self, mark_price: MarkPriceUpdate) {
3028        if self.pipeline_cache_writes_allowed()
3029            && let Err(e) = self.cache.as_ref().borrow_mut().add_mark_price(mark_price)
3030        {
3031            log_error_on_cache_insert(&e);
3032        }
3033
3034        let topic = switchboard::get_pipeline_mark_price_topic(mark_price.instrument_id);
3035        msgbus::publish_mark_price(topic, &mark_price);
3036    }
3037
3038    fn handle_index_price_pipeline(&self, index_price: IndexPriceUpdate) {
3039        if self.pipeline_cache_writes_allowed()
3040            && let Err(e) = self
3041                .cache
3042                .as_ref()
3043                .borrow_mut()
3044                .add_index_price(index_price)
3045        {
3046            log_error_on_cache_insert(&e);
3047        }
3048
3049        let topic = switchboard::get_pipeline_index_price_topic(index_price.instrument_id);
3050        msgbus::publish_index_price(topic, &index_price);
3051    }
3052
3053    fn handle_funding_rate_pipeline(&self, funding_rate: FundingRateUpdate) {
3054        if self.pipeline_cache_writes_allowed()
3055            && let Err(e) = self
3056                .cache
3057                .as_ref()
3058                .borrow_mut()
3059                .add_funding_rate(funding_rate)
3060        {
3061            log_error_on_cache_insert(&e);
3062        }
3063
3064        let topic = switchboard::get_pipeline_funding_rate_topic(funding_rate.instrument_id);
3065        msgbus::publish_funding_rate(topic, &funding_rate);
3066    }
3067
3068    fn handle_instrument_status_pipeline(&self, status: InstrumentStatus) {
3069        if self.pipeline_cache_writes_allowed()
3070            && let Err(e) = self
3071                .cache
3072                .as_ref()
3073                .borrow_mut()
3074                .add_instrument_status(status)
3075        {
3076            log_error_on_cache_insert(&e);
3077        }
3078
3079        let topic = switchboard::get_pipeline_instrument_status_topic(status.instrument_id);
3080        msgbus::publish_any(topic, &status);
3081    }
3082
3083    fn handle_option_greeks_pipeline(&self, greeks: OptionGreeks) {
3084        if self.pipeline_cache_writes_allowed() {
3085            self.cache.borrow_mut().add_option_greeks(greeks);
3086        }
3087
3088        let topic = switchboard::get_pipeline_option_greeks_topic(greeks.instrument_id);
3089        msgbus::publish_option_greeks(topic, &greeks);
3090    }
3091
3092    fn handle_instrument_close_pipeline(&self, close: InstrumentClose) {
3093        let topic = switchboard::get_pipeline_instrument_close_topic(close.instrument_id);
3094        msgbus::publish_any(topic, &close);
3095    }
3096
3097    fn handle_custom_data_pipeline(&self, custom: &CustomData) {
3098        log::debug!("Pipeline custom data: {}", custom.data.type_name());
3099        let topic = switchboard::get_pipeline_custom_topic(&custom.data_type);
3100        msgbus::publish_any(topic, custom);
3101    }
3102
3103    /// Drains deferred subscribe/unsubscribe commands pushed by option chain
3104    /// managers (or any other component) and executes them against the appropriate
3105    /// data client.
3106    fn drain_deferred_commands(&mut self) {
3107        // Loop because expire_series pushes Unsubscribe commands; converges in <= 3 iterations
3108        loop {
3109            let commands: VecDeque<DeferredCommand> =
3110                std::mem::take(&mut *self.deferred_cmd_queue.borrow_mut());
3111
3112            if commands.is_empty() {
3113                break;
3114            }
3115
3116            for cmd in commands {
3117                match cmd {
3118                    DeferredCommand::Subscribe(sub) => {
3119                        let client = self.get_command_client(sub.client_id(), sub.venue());
3120                        if let Some(client) = client {
3121                            client.execute_subscribe(sub);
3122                        }
3123                    }
3124                    DeferredCommand::Unsubscribe(unsub) => {
3125                        if let Err(e) = self.execute_unsubscribe(&unsub) {
3126                            log::error!("Failed to execute deferred unsubscribe: {e}");
3127                        }
3128                    }
3129                    DeferredCommand::ExpireInstrument(instrument_id) => {
3130                        self.expire_option_chain_instrument(instrument_id);
3131                    }
3132                    DeferredCommand::ExpireSeries(series_id) => {
3133                        self.expire_series(series_id);
3134                    }
3135                }
3136            }
3137        }
3138    }
3139
3140    /// Proactively expires all instruments for a series and tears down the manager.
3141    ///
3142    /// `handle_instrument_expired` removes each instrument from the aggregator and pushes
3143    /// deferred unsubscribe commands. `teardown` then cancels the snapshot timer and clears
3144    /// the handler lists (the aggregator is already empty at that point).
3145    fn expire_series(&mut self, series_id: OptionSeriesId) {
3146        self.stop_option_chain_greeks_bootstrap(series_id);
3147
3148        let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
3149            return;
3150        };
3151
3152        let instrument_ids: Vec<InstrumentId> = self
3153            .option_chain_instrument_index
3154            .iter()
3155            .filter(|(_, sid)| **sid == series_id)
3156            .map(|(id, _)| *id)
3157            .collect();
3158
3159        for id in &instrument_ids {
3160            self.option_chain_instrument_index.remove(id);
3161            manager_rc.borrow_mut().handle_instrument_expired(id);
3162        }
3163
3164        manager_rc.borrow_mut().teardown(&self.clock);
3165        self.option_chain_managers.remove(&series_id);
3166
3167        log::info!("Proactively torn down expired option chain {series_id}");
3168    }
3169
3170    fn subscribe_book_deltas(&mut self, cmd: &SubscribeBookDeltas) -> anyhow::Result<bool> {
3171        if cmd.instrument_id.is_synthetic() {
3172            anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDelta` data");
3173        }
3174
3175        let had_deltas =
3176            self.has_book_delta_subscription_key(cmd.instrument_id, cmd.client_id, cmd.venue);
3177
3178        self.retain_book_subscription(SubscribeCommand::BookDeltas(cmd.clone()))?;
3179
3180        self.increment_book_delta_subscription(cmd.instrument_id, cmd.client_id, cmd.venue);
3181
3182        Ok(!had_deltas)
3183    }
3184
3185    fn subscribe_book_depth(&mut self, cmd: &SubscribeBookDepth) -> anyhow::Result<bool> {
3186        if cmd.instrument_id.is_synthetic() {
3187            anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDepth` data");
3188        }
3189
3190        let had_depth =
3191            self.has_book_depth_subscription_key(cmd.instrument_id, cmd.client_id, cmd.venue);
3192
3193        self.retain_book_subscription(SubscribeCommand::BookDepth(cmd.clone()))?;
3194
3195        self.increment_book_depth_subscription(cmd.instrument_id, cmd.client_id, cmd.venue);
3196
3197        Ok(!had_depth)
3198    }
3199
3200    fn subscribe_book_snapshots(&mut self, cmd: &SubscribeBookSnapshots) -> anyhow::Result<()> {
3201        if cmd.instrument_id.is_synthetic() {
3202            anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDelta` data");
3203        }
3204
3205        let parent = resolve_parent_components(&cmd.instrument_id, cmd.params.as_ref())?;
3206
3207        let had_snapshots = self.has_book_snapshot_subscriptions(&cmd.instrument_id);
3208
3209        self.retain_book_subscription(SubscribeCommand::BookSnapshots(cmd.clone()))?;
3210
3211        self.increment_book_snapshot_subscription(cmd, parent);
3212
3213        if !had_snapshots {
3214            self.book_snapshot_sources.insert(
3215                cmd.instrument_id,
3216                BookSnapshotSource {
3217                    command: cmd.clone(),
3218                    client_command: SubscribeCommand::BookDeltas(SubscribeBookDeltas::new(
3219                        cmd.instrument_id,
3220                        cmd.book_type,
3221                        cmd.client_id,
3222                        cmd.venue,
3223                        UUID4::new(),
3224                        cmd.ts_init,
3225                        cmd.depth,
3226                        true, // managed
3227                        Some(cmd.command_id),
3228                        cmd.params.clone(),
3229                    )),
3230                },
3231            );
3232        }
3233
3234        let source = self
3235            .book_snapshot_sources
3236            .get(&cmd.instrument_id)
3237            .cloned()
3238            .expect("snapshot source command must exist after increment");
3239        self.subscribe_book_snapshot_source(&source.command, source.client_command);
3240
3241        Ok(())
3242    }
3243
3244    fn subscribe_book_snapshot_source(
3245        &mut self,
3246        cmd: &SubscribeBookSnapshots,
3247        client_command: SubscribeCommand,
3248    ) {
3249        if let Some(client_id) = cmd.client_id.as_ref()
3250            && self.external_clients.contains(client_id)
3251        {
3252            if self.config.debug {
3253                log::debug!("Skipping subscribe command for external client {client_id}: {cmd:?}");
3254            }
3255            return;
3256        }
3257
3258        log::debug!(
3259            "Forwarding BookSnapshots as BookDeltas for {}, client_id={:?}, venue={:?}",
3260            cmd.instrument_id,
3261            cmd.client_id,
3262            cmd.venue,
3263        );
3264
3265        if let Some(client) = self.get_command_client(cmd.client_id.as_ref(), cmd.venue.as_ref()) {
3266            log::debug!(
3267                "Calling client.execute_subscribe for BookDeltas: {}",
3268                cmd.instrument_id
3269            );
3270            client.execute_subscribe(client_command);
3271        } else {
3272            log::error!(
3273                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
3274                cmd.client_id,
3275                cmd.venue,
3276            );
3277        }
3278    }
3279
3280    fn subscribe_bars(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
3281        match cmd.bar_type.aggregation_source() {
3282            AggregationSource::Internal => self.start_bar_aggregation(cmd)?,
3283            AggregationSource::External => {
3284                if cmd.bar_type.instrument_id().is_synthetic() {
3285                    anyhow::bail!(
3286                        "Cannot subscribe for externally aggregated synthetic instrument bar data"
3287                    );
3288                }
3289            }
3290        }
3291
3292        Ok(())
3293    }
3294
3295    fn subscribe_synthetic_quotes(&mut self, instrument_id: InstrumentId) {
3296        let synthetic = match self.cache.borrow().try_synthetic(&instrument_id).cloned() {
3297            Ok(synthetic) => synthetic,
3298            Err(e) => {
3299                log::error!("Cannot subscribe to `QuoteTick` data for synthetic instrument: {e}");
3300                return;
3301            }
3302        };
3303
3304        if let Some(owners) = self.subscribed_synthetic_quotes.get_mut(&instrument_id) {
3305            *owners += 1;
3306            return;
3307        }
3308        self.subscribed_synthetic_quotes.insert(instrument_id, 1);
3309
3310        for component_id in &synthetic.components {
3311            let synthetics = self.synthetic_quote_feeds.entry(*component_id).or_default();
3312            if !synthetics
3313                .iter()
3314                .any(|registered| registered.id == synthetic.id)
3315            {
3316                synthetics.push(synthetic.clone());
3317            }
3318        }
3319    }
3320
3321    fn subscribe_synthetic_trades(&mut self, instrument_id: InstrumentId) {
3322        let synthetic = match self.cache.borrow().try_synthetic(&instrument_id).cloned() {
3323            Ok(synthetic) => synthetic,
3324            Err(e) => {
3325                log::error!("Cannot subscribe to `TradeTick` data for synthetic instrument: {e}");
3326                return;
3327            }
3328        };
3329
3330        if let Some(owners) = self.subscribed_synthetic_trades.get_mut(&instrument_id) {
3331            *owners += 1;
3332            return;
3333        }
3334        self.subscribed_synthetic_trades.insert(instrument_id, 1);
3335
3336        for component_id in &synthetic.components {
3337            let synthetics = self.synthetic_trade_feeds.entry(*component_id).or_default();
3338            if !synthetics
3339                .iter()
3340                .any(|registered| registered.id == synthetic.id)
3341            {
3342                synthetics.push(synthetic.clone());
3343            }
3344        }
3345    }
3346
3347    fn is_spread_quote_command(
3348        &self,
3349        instrument_id: InstrumentId,
3350        params: Option<&Params>,
3351    ) -> bool {
3352        if !params
3353            .and_then(|params| params.get_bool("aggregate_spread_quotes"))
3354            .unwrap_or(false)
3355        {
3356            return false;
3357        }
3358
3359        self.cache
3360            .borrow()
3361            .instrument(&instrument_id)
3362            .is_some_and(InstrumentAny::is_spread)
3363    }
3364
3365    fn subscribe_spread_quotes(&mut self, cmd: &SubscribeQuotes) {
3366        if let Some(state) = self.spread_quote_states.get_mut(&cmd.instrument_id) {
3367            state.owners += 1;
3368            let sources = state.sources.clone();
3369            for source in sources {
3370                self.execute(DataCommand::Subscribe(source));
3371            }
3372            return;
3373        }
3374
3375        let Some(instrument) = self.cache.borrow().instrument(&cmd.instrument_id).cloned() else {
3376            log::error!(
3377                "Cannot create spread quote aggregator: no instrument found for {}",
3378                cmd.instrument_id,
3379            );
3380            return;
3381        };
3382        let Some(legs) = spread_instrument_legs(&instrument) else {
3383            log::error!(
3384                "Cannot create spread quote aggregator: invalid spread legs for {}",
3385                cmd.instrument_id,
3386            );
3387            return;
3388        };
3389
3390        if legs.len() <= 1 {
3391            log::error!(
3392                "Cannot create spread quote aggregator: spread instrument {} should have more than one leg",
3393                cmd.instrument_id,
3394            );
3395            return;
3396        }
3397
3398        let cache = self.cache.clone();
3399        let handler = Box::new(move |quote: QuoteTick| {
3400            let exchange_endpoint = format!(
3401                "SimulatedExchange.process_new_quote.{}",
3402                quote.instrument_id.venue
3403            );
3404            let exchange_endpoint = exchange_endpoint.into();
3405            if msgbus::has_quote_endpoint(exchange_endpoint) {
3406                msgbus::send_quote(exchange_endpoint, &quote);
3407            }
3408
3409            if let Err(e) = cache.borrow_mut().add_quote(quote) {
3410                log_error_on_cache_insert(&e);
3411            }
3412            let topic = switchboard::get_quotes_topic(quote.instrument_id);
3413            msgbus::publish_quote(topic, &quote);
3414        });
3415        let aggregator = Rc::new(RefCell::new(SpreadQuoteAggregator::new(
3416            cmd.instrument_id,
3417            &legs,
3418            matches!(
3419                instrument,
3420                InstrumentAny::FuturesSpread(_) | InstrumentAny::CryptoFuturesSpread(_)
3421            ),
3422            instrument.price_precision(),
3423            instrument.size_precision(),
3424            handler,
3425            self.clock.clone(),
3426            false,
3427            spread_quote_update_interval_seconds(cmd.params.as_ref()),
3428            cmd.params
3429                .as_ref()
3430                .and_then(|params| params.get_u64("quote_build_delay"))
3431                .unwrap_or(0),
3432            cmd.params
3433                .as_ref()
3434                .and_then(|params| params.get_bool("disable_vega_pricing"))
3435                .unwrap_or(false),
3436            cmd.params
3437                .as_ref()
3438                .and_then(|params| params.get_u64("vega_pricing_timeout_seconds"))
3439                .unwrap_or(60),
3440            None,
3441            None,
3442        )));
3443
3444        let mut handlers = Vec::with_capacity(legs.len());
3445        for (leg_id, _) in &legs {
3446            let topic = switchboard::get_quotes_topic(*leg_id);
3447            let handler = TypedHandler::new(SpreadQuoteHandler::new(
3448                &aggregator,
3449                cmd.instrument_id,
3450                *leg_id,
3451            ));
3452            msgbus::subscribe_quotes(topic.into(), handler.clone(), Some(BAR_AGGREGATOR_PRIORITY));
3453            handlers.push((*leg_id, handler));
3454        }
3455
3456        aggregator
3457            .borrow_mut()
3458            .start_timer(Some(aggregator.clone()));
3459        aggregator.borrow_mut().set_running(true);
3460        let source_commands = legs
3461            .into_iter()
3462            .map(|(leg_id, _)| {
3463                SubscribeCommand::Quotes(SubscribeQuotes::new(
3464                    leg_id,
3465                    cmd.client_id,
3466                    cmd.venue,
3467                    UUID4::new(),
3468                    cmd.ts_init,
3469                    Some(cmd.command_id),
3470                    cmd.params.clone(),
3471                ))
3472            })
3473            .collect::<Vec<_>>();
3474        self.spread_quote_states.insert(
3475            cmd.instrument_id,
3476            SpreadQuoteState {
3477                aggregator,
3478                handlers,
3479                owners: 1,
3480                command: cmd.clone(),
3481                sources: source_commands.clone(),
3482            },
3483        );
3484
3485        for source_command in source_commands {
3486            self.execute(DataCommand::Subscribe(source_command));
3487        }
3488    }
3489
3490    fn unsubscribe_spread_quotes(&mut self, cmd: &UnsubscribeQuotes) {
3491        let Some(state) = self.spread_quote_states.get_mut(&cmd.instrument_id) else {
3492            log::warn!(
3493                "Cannot unsubscribe spread quotes for {}: not subscribed",
3494                cmd.instrument_id,
3495            );
3496            return;
3497        };
3498
3499        if state.owners > 1 {
3500            state.owners -= 1;
3501            return;
3502        }
3503
3504        let Some((subscribe, leg_ids)) = self.stop_spread_quote_aggregation(cmd.instrument_id)
3505        else {
3506            return;
3507        };
3508
3509        for leg_id in leg_ids {
3510            let unsubscribe = UnsubscribeQuotes::new(
3511                leg_id,
3512                subscribe.client_id,
3513                subscribe.venue,
3514                UUID4::new(),
3515                cmd.ts_init,
3516                Some(subscribe.command_id),
3517                subscribe.params.clone(),
3518            );
3519            self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(
3520                unsubscribe,
3521            )));
3522        }
3523    }
3524
3525    fn stop_spread_quote_aggregation(
3526        &mut self,
3527        spread_instrument_id: InstrumentId,
3528    ) -> Option<(SubscribeQuotes, Vec<InstrumentId>)> {
3529        let Some(state) = self.spread_quote_states.remove(&spread_instrument_id) else {
3530            log::warn!("Cannot stop spread quote aggregation: no state for {spread_instrument_id}");
3531            return None;
3532        };
3533
3534        state.aggregator.borrow_mut().stop_timer();
3535        state.aggregator.borrow_mut().set_running(false);
3536
3537        let mut leg_ids = Vec::with_capacity(state.handlers.len());
3538        for (leg_id, handler) in state.handlers {
3539            let topic = switchboard::get_quotes_topic(leg_id);
3540            msgbus::unsubscribe_quotes(topic.into(), &handler);
3541            leg_ids.push(leg_id);
3542        }
3543
3544        Some((state.command, leg_ids))
3545    }
3546
3547    fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> bool {
3548        match self.decrement_book_delta_subscription(cmd.instrument_id, cmd.client_id, cmd.venue) {
3549            BookDeltasUnsubscribeResult::NotSubscribed => {
3550                log::warn!("Cannot unsubscribe from `OrderBookDeltas` data: not subscribed");
3551                return false;
3552            }
3553            BookDeltasUnsubscribeResult::Decremented => return false,
3554            BookDeltasUnsubscribeResult::Removed => {}
3555        }
3556
3557        true
3558    }
3559
3560    fn unsubscribe_book_depth(&mut self, cmd: &UnsubscribeBookDepth) -> bool {
3561        match self.decrement_book_depth_subscription(cmd.instrument_id, cmd.client_id, cmd.venue) {
3562            BookDeltasUnsubscribeResult::NotSubscribed => {
3563                log::warn!("Cannot unsubscribe from `OrderBookDepth` data: not subscribed");
3564                return false;
3565            }
3566            BookDeltasUnsubscribeResult::Decremented => return false,
3567            BookDeltasUnsubscribeResult::Removed => {}
3568        }
3569
3570        true
3571    }
3572
3573    fn unsubscribe_book_snapshots(&mut self, cmd: &UnsubscribeBookSnapshots) {
3574        match self.decrement_book_snapshot_subscription(cmd.instrument_id, cmd.interval_ms) {
3575            BookSnapshotUnsubscribeResult::NotSubscribed => {
3576                log::warn!("Cannot unsubscribe from `OrderBook` snapshots: not subscribed");
3577                return;
3578            }
3579            BookSnapshotUnsubscribeResult::Decremented => return,
3580            BookSnapshotUnsubscribeResult::Removed => {}
3581        }
3582
3583        if self.has_book_snapshot_subscriptions(&cmd.instrument_id) {
3584            return;
3585        }
3586
3587        let Some(source) = self.book_snapshot_sources.remove(&cmd.instrument_id) else {
3588            log::error!(
3589                "Cannot release order book snapshot source for {}: command not retained",
3590                cmd.instrument_id,
3591            );
3592            return;
3593        };
3594
3595        if let Some(client_id) = source.command.client_id.as_ref()
3596            && self.external_clients.contains(client_id)
3597        {
3598            return;
3599        }
3600
3601        if let Some(client) = self.get_command_client(
3602            source.command.client_id.as_ref(),
3603            source.command.venue.as_ref(),
3604        ) {
3605            let deltas_cmd = UnsubscribeBookDeltas::new(
3606                source.command.instrument_id,
3607                source.command.client_id,
3608                source.command.venue,
3609                UUID4::new(),
3610                cmd.ts_init,
3611                Some(source.command.command_id),
3612                source.command.params,
3613            );
3614            client.execute_unsubscribe(&UnsubscribeCommand::BookDeltas(deltas_cmd));
3615        }
3616    }
3617
3618    fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) {
3619        let bar_type = cmd.bar_type;
3620
3621        // Don't remove aggregator if other exact-topic subscribers still exist
3622        let topic = switchboard::get_bars_topic(bar_type.standard());
3623        if msgbus::exact_subscriber_count_bars(topic) > 0 {
3624            return;
3625        }
3626
3627        let retained = self
3628            .subscriptions_bar_aggregation
3629            .get(&bar_type.standard())
3630            .map(|subscription| subscription.command.clone());
3631        let command = retained.map_or_else(
3632            || cmd.clone(),
3633            |subscribe| {
3634                UnsubscribeBars::new(
3635                    subscribe.bar_type,
3636                    subscribe.client_id,
3637                    subscribe.venue,
3638                    cmd.command_id,
3639                    cmd.ts_init,
3640                    Some(subscribe.command_id),
3641                    subscribe.params,
3642                )
3643            },
3644        );
3645
3646        if self
3647            .bar_aggregators
3648            .contains_key(&bar_aggregator_key(bar_type, None))
3649        {
3650            match self.stop_bar_aggregator(bar_type, None) {
3651                Ok(()) => {
3652                    self.subscriptions_bar_aggregation
3653                        .remove(&bar_type.standard());
3654                    self.unsubscribe_bar_aggregator(&command);
3655                }
3656                Err(e) => log::error!("Error stopping bar aggregator for {bar_type}: {e}"),
3657            }
3658        }
3659
3660        // After stopping a composite, release its source through `unsubscribe_bars`, which frees
3661        // the client feed recorded in the source's retained command.
3662        if command.bar_type.is_composite() {
3663            let source_type = command.bar_type.composite();
3664
3665            if self
3666                .bar_aggregators
3667                .contains_key(&bar_aggregator_key(source_type, None))
3668            {
3669                self.unsubscribe_bars(&UnsubscribeBars::new(
3670                    source_type,
3671                    command.client_id,
3672                    command.venue,
3673                    UUID4::new(),
3674                    command.ts_init,
3675                    Some(command.command_id),
3676                    command.params.clone(),
3677                ));
3678            }
3679        }
3680    }
3681
3682    fn unsubscribe_synthetic_quotes(&mut self, instrument_id: InstrumentId) {
3683        let Some(owners) = self.subscribed_synthetic_quotes.get_mut(&instrument_id) else {
3684            log::warn!("Cannot unsubscribe from synthetic `QuoteTick` data: not subscribed");
3685            return;
3686        };
3687
3688        if *owners > 1 {
3689            *owners -= 1;
3690            return;
3691        }
3692        self.subscribed_synthetic_quotes.remove(&instrument_id);
3693
3694        self.synthetic_quote_feeds.retain(|_, synthetics| {
3695            synthetics.retain(|synthetic| synthetic.id != instrument_id);
3696            !synthetics.is_empty()
3697        });
3698    }
3699
3700    fn unsubscribe_synthetic_trades(&mut self, instrument_id: InstrumentId) {
3701        let Some(owners) = self.subscribed_synthetic_trades.get_mut(&instrument_id) else {
3702            log::warn!("Cannot unsubscribe from synthetic `TradeTick` data: not subscribed");
3703            return;
3704        };
3705
3706        if *owners > 1 {
3707            *owners -= 1;
3708            return;
3709        }
3710        self.subscribed_synthetic_trades.remove(&instrument_id);
3711
3712        self.synthetic_trade_feeds.retain(|_, synthetics| {
3713            synthetics.retain(|synthetic| synthetic.id != instrument_id);
3714            !synthetics.is_empty()
3715        });
3716    }
3717
3718    fn subscribe_option_chain(&mut self, cmd: &SubscribeOptionChain) {
3719        self.drain_deferred_commands();
3720        let series_id = cmd.series_id;
3721        self.stop_option_chain_greeks_bootstrap(series_id);
3722
3723        // Handle edits to existing subscriptions by tearing down and re-setting up the OptionChainManager.
3724        if let Some(old) = self.option_chain_managers.remove(&series_id) {
3725            log::info!("Re-subscribing option chain for {series_id}, tearing down previous");
3726            let (active_ids, old_venue, old_client_id) = {
3727                let old = old.borrow();
3728                let active_ids = old
3729                    .all_instrument_ids()
3730                    .into_iter()
3731                    .filter(|instrument_id| old.is_instrument_active(instrument_id))
3732                    .collect::<Vec<_>>();
3733                (active_ids, old.venue(), old.client_id())
3734            };
3735            old.borrow_mut().teardown(&self.clock);
3736            self.forward_option_chain_unsubscribes(&active_ids, old_venue, old_client_id);
3737        }
3738
3739        self.cancel_pending_option_chain_requests(Some(series_id));
3740
3741        // For dynamic strike ranges, request a reference price from the adapter
3742        // to enable instant bootstrap without waiting for the first WebSocket tick.
3743        if !matches!(cmd.strike_range, StrikeRange::Fixed(_)) {
3744            let resolved_client_id = self
3745                .get_client(cmd.client_id.as_ref(), Some(&series_id.venue))
3746                .map(|c| c.client_id);
3747
3748            if let Some(client_id) = resolved_client_id {
3749                let request_id = UUID4::new();
3750                let ts_init = self.clock.borrow().timestamp_ns();
3751
3752                let sample_instrument_id = {
3753                    let cache = self.cache.borrow();
3754                    cache
3755                        .instruments(&series_id.venue, Some(&series_id.underlying))
3756                        .iter()
3757                        .filter(|i| {
3758                            i.instrument_class() == InstrumentClass::Option
3759                                && i.expiration_ns() == Some(series_id.expiration_ns)
3760                                && i.settlement_currency().code == series_id.settlement_currency
3761                        })
3762                        .min_by_key(|i| i.id())
3763                        .map(|i| i.id())
3764                };
3765
3766                if let Some(instrument_id) = sample_instrument_id {
3767                    let request = RequestOptionChainReferencePrice::new(
3768                        series_id,
3769                        instrument_id,
3770                        Some(client_id),
3771                        request_id,
3772                        ts_init,
3773                        None,
3774                    );
3775                    let deadline_ns = ts_init.saturating_add(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT);
3776                    self.pending_option_chain_requests.insert(
3777                        request_id,
3778                        PendingOptionChainRequest {
3779                            command: cmd.clone(),
3780                            sample_instrument_id: instrument_id,
3781                            deadline_ns,
3782                        },
3783                    );
3784
3785                    if !self.schedule_option_chain_reference_price_timeout() {
3786                        self.bootstrap_all_pending_option_chains();
3787                        return;
3788                    }
3789
3790                    let req_cmd = RequestCommand::OptionChainReferencePrice(request);
3791                    if let Err(e) = self.execute_request(req_cmd) {
3792                        log::warn!(
3793                            "Failed to request option-chain reference price for {series_id}: {e}"
3794                        );
3795
3796                        if let Some(pending) =
3797                            self.pending_option_chain_requests.remove(&request_id)
3798                        {
3799                            self.maintain_option_chain_reference_price_timeout();
3800                            self.create_option_chain_manager_with_greeks_bootstrap(pending);
3801                        }
3802                    }
3803
3804                    return;
3805                }
3806            }
3807        }
3808
3809        self.create_option_chain_manager(cmd, None);
3810    }
3811
3812    fn schedule_option_chain_reference_price_timeout(&self) -> bool {
3813        let Some(deadline_ns) = self
3814            .pending_option_chain_requests
3815            .values()
3816            .map(|pending| pending.deadline_ns)
3817            .min()
3818        else {
3819            self.clock
3820                .borrow_mut()
3821                .cancel_timer(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER);
3822            return true;
3823        };
3824
3825        if self
3826            .clock
3827            .borrow()
3828            .next_time_ns(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER)
3829            == Some(deadline_ns)
3830        {
3831            return true;
3832        }
3833
3834        let Some(bootstrapper) = self.option_chain_bootstrapper.clone() else {
3835            log::error!(
3836                "Cannot schedule option-chain reference price timeout: data engine message bus handlers are not registered"
3837            );
3838            return false;
3839        };
3840
3841        let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_| {
3842            bootstrapper.handle_timeout();
3843        });
3844        let callback = TimeEventCallback::from(callback_fn);
3845
3846        if let Err(e) = self.clock.borrow_mut().set_time_alert_ns(
3847            OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER,
3848            deadline_ns,
3849            Some(callback),
3850            Some(true),
3851        ) {
3852            log::error!("Failed to schedule option-chain reference price timeout: {e}");
3853            return false;
3854        }
3855
3856        true
3857    }
3858
3859    fn maintain_option_chain_reference_price_timeout(&mut self) {
3860        if !self.schedule_option_chain_reference_price_timeout() {
3861            self.bootstrap_all_pending_option_chains();
3862        }
3863    }
3864
3865    fn bootstrap_all_pending_option_chains(&mut self) {
3866        self.clock
3867            .borrow_mut()
3868            .cancel_timer(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER);
3869        let mut pending: Vec<PendingOptionChainRequest> =
3870            mem::take(&mut self.pending_option_chain_requests)
3871                .into_values()
3872                .collect();
3873        pending.sort_unstable_by_key(|pending| pending.command.series_id);
3874        for pending in pending {
3875            self.create_option_chain_manager_with_greeks_bootstrap(pending);
3876        }
3877    }
3878
3879    fn cancel_pending_option_chain_requests(&mut self, series_id: Option<OptionSeriesId>) -> bool {
3880        let request_ids: Vec<UUID4> = self
3881            .pending_option_chain_requests
3882            .iter()
3883            .filter_map(|(request_id, pending)| {
3884                (series_id.is_none() || series_id == Some(pending.command.series_id))
3885                    .then_some(*request_id)
3886            })
3887            .collect();
3888
3889        for request_id in &request_ids {
3890            self.pending_option_chain_requests.remove(request_id);
3891        }
3892        self.maintain_option_chain_reference_price_timeout();
3893
3894        !request_ids.is_empty()
3895    }
3896
3897    fn handle_option_chain_reference_price_timeout(&mut self) {
3898        let now_ns = self.clock.borrow().timestamp_ns();
3899        let mut requests: Vec<(OptionSeriesId, UUID4)> = self
3900            .pending_option_chain_requests
3901            .iter()
3902            .filter_map(|(request_id, pending)| {
3903                (pending.deadline_ns <= now_ns).then_some((pending.command.series_id, *request_id))
3904            })
3905            .collect();
3906        requests.sort_unstable_by_key(|(series_id, _)| *series_id);
3907
3908        for (_, request_id) in requests {
3909            let Some(pending) = self.pending_option_chain_requests.remove(&request_id) else {
3910                continue;
3911            };
3912            let series_id = pending.command.series_id;
3913            log::warn!(
3914                "Option-chain reference price request timed out for {series_id}; bootstrapping from live data"
3915            );
3916            self.create_option_chain_manager_with_greeks_bootstrap(pending);
3917        }
3918
3919        self.maintain_option_chain_reference_price_timeout();
3920    }
3921
3922    /// Creates and stores an `OptionChainManager` for the given subscription.
3923    fn create_option_chain_manager(
3924        &mut self,
3925        cmd: &SubscribeOptionChain,
3926        initial_atm_price: Option<Price>,
3927    ) -> Rc<RefCell<OptionChainManager>> {
3928        let series_id = cmd.series_id;
3929        let cache = self.cache.clone();
3930        let clock = self.clock.clone();
3931        let priority = self.msgbus_priority;
3932        let deferred_cmd_queue = self.deferred_cmd_queue.clone();
3933
3934        let manager_rc = {
3935            let client = self.get_command_client(cmd.client_id.as_ref(), Some(&series_id.venue));
3936            OptionChainManager::create_and_setup(
3937                series_id,
3938                &cache,
3939                cmd,
3940                &clock,
3941                priority,
3942                client,
3943                initial_atm_price,
3944                deferred_cmd_queue,
3945            )
3946        };
3947
3948        // Index all instruments for reverse lookup
3949        for id in manager_rc.borrow().all_instrument_ids() {
3950            self.option_chain_instrument_index.insert(id, series_id);
3951        }
3952
3953        self.option_chain_managers
3954            .insert(series_id, manager_rc.clone());
3955        manager_rc
3956    }
3957
3958    fn create_option_chain_manager_with_greeks_bootstrap(
3959        &mut self,
3960        pending: PendingOptionChainRequest,
3961    ) {
3962        let cmd = pending.command;
3963        let series_id = cmd.series_id;
3964        let instrument_id = pending.sample_instrument_id;
3965        let manager = self.create_option_chain_manager(&cmd, None);
3966        let Some(client_id) = manager.borrow().client_id() else {
3967            return;
3968        };
3969
3970        let ownership_handler = TypedHandler::from(|_: &OptionGreeks| {});
3971        let topic = switchboard::get_option_greeks_topic(instrument_id);
3972        msgbus::subscribe_option_greeks(
3973            topic.into(),
3974            ownership_handler.clone(),
3975            Some(self.msgbus_priority),
3976        );
3977        let replaced = self.option_chain_greeks_bootstraps.insert(
3978            series_id,
3979            OptionChainGreeksBootstrap {
3980                instrument_id,
3981                client_id,
3982                venue: series_id.venue,
3983                ownership_handler,
3984            },
3985        );
3986        debug_assert!(
3987            replaced.is_none(),
3988            "Invariant: each option series has at most one Greeks bootstrap subscription"
3989        );
3990
3991        let ts_init = self.clock.borrow().timestamp_ns();
3992
3993        if let Err(e) =
3994            self.execute_subscribe(SubscribeCommand::OptionGreeks(SubscribeOptionGreeks::new(
3995                instrument_id,
3996                Some(client_id),
3997                Some(series_id.venue),
3998                UUID4::new(),
3999                ts_init,
4000                None,
4001                None,
4002            )))
4003        {
4004            log::error!("Failed to subscribe option-chain bootstrap Greeks for {series_id}: {e}");
4005        }
4006    }
4007
4008    fn stop_option_chain_greeks_bootstrap(&mut self, series_id: OptionSeriesId) -> bool {
4009        let Some(bootstrap) = self.remove_option_chain_greeks_bootstrap(series_id) else {
4010            return false;
4011        };
4012        self.release_option_chain_greeks_bootstrap(&bootstrap);
4013        true
4014    }
4015
4016    fn remove_option_chain_greeks_bootstrap(
4017        &mut self,
4018        series_id: OptionSeriesId,
4019    ) -> Option<OptionChainGreeksBootstrap> {
4020        let bootstrap = self.option_chain_greeks_bootstraps.remove(&series_id)?;
4021        let topic = switchboard::get_option_greeks_topic(bootstrap.instrument_id);
4022        msgbus::unsubscribe_option_greeks(topic.into(), &bootstrap.ownership_handler);
4023        Some(bootstrap)
4024    }
4025
4026    fn release_option_chain_greeks_bootstrap(&mut self, bootstrap: &OptionChainGreeksBootstrap) {
4027        let cmd = UnsubscribeCommand::OptionGreeks(UnsubscribeOptionGreeks::new(
4028            bootstrap.instrument_id,
4029            Some(bootstrap.client_id),
4030            Some(bootstrap.venue),
4031            UUID4::new(),
4032            self.clock.borrow().timestamp_ns(),
4033            None,
4034            None,
4035        ));
4036
4037        if let Err(e) = self.execute_unsubscribe(&cmd) {
4038            log::error!(
4039                "Failed to unsubscribe option-chain bootstrap Greeks for {}: {e}",
4040                bootstrap.instrument_id
4041            );
4042        }
4043    }
4044
4045    fn clear_option_chain_greeks_bootstraps(&mut self) {
4046        let bootstraps = mem::take(&mut self.option_chain_greeks_bootstraps);
4047        for bootstrap in bootstraps.into_values() {
4048            let topic = switchboard::get_option_greeks_topic(bootstrap.instrument_id);
4049            msgbus::unsubscribe_option_greeks(topic.into(), &bootstrap.ownership_handler);
4050        }
4051    }
4052
4053    fn unsubscribe_option_chain(&mut self, cmd: &UnsubscribeOptionChain) {
4054        self.drain_deferred_commands();
4055        let series_id = cmd.series_id;
4056        let topic = switchboard::get_option_chain_topic(series_id);
4057        if msgbus::exact_subscriber_count_option_chain(topic) > 0 {
4058            return;
4059        }
4060
4061        let canceled_pending = self.cancel_pending_option_chain_requests(Some(series_id));
4062        let canceled_greeks_bootstrap = self.stop_option_chain_greeks_bootstrap(series_id);
4063
4064        let Some(manager_rc) = self.option_chain_managers.remove(&series_id) else {
4065            if !canceled_pending && !canceled_greeks_bootstrap {
4066                log::warn!("Cannot unsubscribe option chain for {series_id}: not subscribed");
4067            }
4068            return;
4069        };
4070
4071        // Extract info before teardown
4072        let (all_ids, active_ids, venue, client_id) = {
4073            let manager = manager_rc.borrow();
4074            let all_ids = manager.all_instrument_ids();
4075            let active_ids = all_ids
4076                .iter()
4077                .filter(|instrument_id| manager.is_instrument_active(instrument_id))
4078                .copied()
4079                .collect::<Vec<_>>();
4080            (all_ids, active_ids, manager.venue(), manager.client_id())
4081        };
4082
4083        // Remove all instruments from reverse index
4084        for id in &all_ids {
4085            self.option_chain_instrument_index.remove(id);
4086        }
4087
4088        manager_rc.borrow_mut().teardown(&self.clock);
4089
4090        // Forward wire-level unsubscribes to the data client
4091        self.forward_option_chain_unsubscribes(&active_ids, venue, client_id);
4092
4093        log::info!("Unsubscribed option chain for {series_id}");
4094    }
4095
4096    /// Forwards wire-level unsubscribe commands for all option chain instruments.
4097    fn forward_option_chain_unsubscribes(
4098        &mut self,
4099        instrument_ids: &[InstrumentId],
4100        venue: Venue,
4101        client_id: Option<ClientId>,
4102    ) {
4103        let ts_init = self.clock.borrow().timestamp_ns();
4104
4105        for instrument_id in instrument_ids {
4106            let quote_cmd = UnsubscribeCommand::Quotes(UnsubscribeQuotes::new(
4107                *instrument_id,
4108                client_id,
4109                Some(venue),
4110                UUID4::new(),
4111                ts_init,
4112                None,
4113                None,
4114            ));
4115            let greeks_cmd = UnsubscribeCommand::OptionGreeks(UnsubscribeOptionGreeks::new(
4116                *instrument_id,
4117                client_id,
4118                Some(venue),
4119                UUID4::new(),
4120                ts_init,
4121                None,
4122                None,
4123            ));
4124            let status_cmd =
4125                UnsubscribeCommand::InstrumentStatus(UnsubscribeInstrumentStatus::new(
4126                    *instrument_id,
4127                    client_id,
4128                    Some(venue),
4129                    UUID4::new(),
4130                    ts_init,
4131                    None,
4132                    None,
4133                ));
4134
4135            for cmd in [&quote_cmd, &greeks_cmd, &status_cmd] {
4136                if let Err(e) = self.execute_unsubscribe(cmd) {
4137                    log::error!("Failed to execute option chain unsubscribe: {e}");
4138                }
4139            }
4140        }
4141    }
4142
4143    fn retain_book_subscription(&mut self, command: SubscribeCommand) -> anyhow::Result<()> {
4144        let instrument_id = match &command {
4145            SubscribeCommand::BookDeltas(cmd) => cmd.instrument_id,
4146            SubscribeCommand::BookDepth(cmd) => cmd.instrument_id,
4147            SubscribeCommand::BookSnapshots(cmd) => cmd.instrument_id,
4148            _ => unreachable!("only book subscriptions are retained"),
4149        };
4150
4151        let parent = resolve_parent_components(&instrument_id, command.params())?;
4152
4153        let targets = if let Some((root, class)) = parent {
4154            self.cache
4155                .borrow()
4156                .instruments_by_parent(&instrument_id.venue, &root, class)
4157                .iter()
4158                .map(|instrument| instrument.id())
4159                .collect()
4160        } else {
4161            vec![instrument_id]
4162        };
4163
4164        let client_id = self
4165            .get_command_client(command.client_id(), command.venue())
4166            .map(|client| client.client_id);
4167
4168        let subscription = Rc::new(BookSubscriptionOwner {
4169            command,
4170            client_id,
4171            targets,
4172        });
4173
4174        let mut params = subscription.command.params().cloned().unwrap_or_default();
4175        params.shift_remove(PARAMS_IS_PARENT);
4176
4177        for active in subscription
4178            .targets
4179            .iter()
4180            .filter_map(|id| self.book_subscriptions.get(id))
4181            .flat_map(|book| &book.owners)
4182        {
4183            if !subscription.managed()
4184                && !active.managed()
4185                && subscription.client_id != active.client_id
4186            {
4187                continue;
4188            }
4189
4190            if subscription.is_depth() != active.is_depth() {
4191                anyhow::ensure!(
4192                    !subscription.managed() || !active.managed(),
4193                    "Conflicting managed book source for {instrument_id}: deltas and depth cannot both manage the same book; use managed=false for the other subscription"
4194                );
4195                continue;
4196            }
4197
4198            let mut active_params = active.command.params().cloned().unwrap_or_default();
4199            active_params.shift_remove(PARAMS_IS_PARENT);
4200            anyhow::ensure!(
4201                subscription.client_id == active.client_id
4202                    && subscription.config() == active.config()
4203                    && params == active_params,
4204                "Conflicting book subscription for {instrument_id}: shared book sources must use the same client, book type, depth, and parameters"
4205            );
4206        }
4207
4208        if subscription.managed() {
4209            self.setup_book_updater(
4210                &subscription.targets,
4211                subscription.config().0,
4212                subscription.is_depth(),
4213            )?;
4214        }
4215
4216        for target_id in &subscription.targets {
4217            self.book_subscriptions
4218                .entry(*target_id)
4219                .or_default()
4220                .owners
4221                .push(subscription.clone());
4222        }
4223
4224        self.book_subscription_owners
4225            .entry(instrument_id)
4226            .or_default()
4227            .push(subscription);
4228        Ok(())
4229    }
4230
4231    fn release_book_subscription(&mut self, command: &UnsubscribeCommand) -> bool {
4232        let key = SubscriptionKey::from_unsubscribe(command);
4233
4234        let instrument_id = match command {
4235            UnsubscribeCommand::BookDeltas(cmd) => cmd.instrument_id,
4236            UnsubscribeCommand::BookDepth(cmd) => cmd.instrument_id,
4237            UnsubscribeCommand::BookSnapshots(cmd) => cmd.instrument_id,
4238            _ => unreachable!("only book subscriptions are released"),
4239        };
4240
4241        let Some(owners) = self.book_subscription_owners.get_mut(&instrument_id) else {
4242            return false;
4243        };
4244
4245        let index = owners.iter().position(|subscription| {
4246            SubscriptionKey::from_subscribe(&subscription.command) == key
4247                && subscription.command.client_id() == command.client_id()
4248                && subscription.command.venue() == command.venue()
4249                && command
4250                    .correlation_id()
4251                    .is_none_or(|id| subscription.command.command_id() == id)
4252        });
4253
4254        let Some(index) = index else {
4255            return false;
4256        };
4257
4258        let subscription = owners.remove(index);
4259        if owners.is_empty() {
4260            self.book_subscription_owners.remove(&instrument_id);
4261        }
4262
4263        for &target_id in &subscription.targets {
4264            let book = self
4265                .book_subscriptions
4266                .get_mut(&target_id)
4267                .expect("retained book subscription");
4268            book.owners
4269                .retain(|owner| !Rc::ptr_eq(owner, &subscription));
4270            let managed = book.owners.iter().any(|owner| owner.managed());
4271            if book.owners.is_empty() {
4272                self.book_subscriptions.remove(&target_id);
4273            }
4274
4275            if managed {
4276                continue;
4277            }
4278
4279            let Some(updater) = self.book_updaters.remove(&target_id) else {
4280                continue;
4281            };
4282
4283            let deltas_handler: TypedHandler<OrderBookDeltas> = TypedHandler::new(updater.clone());
4284            let depth_handler: TypedHandler<OrderBookDepth> = TypedHandler::new(updater);
4285            msgbus::unsubscribe_book_deltas(
4286                switchboard::get_book_deltas_topic(target_id).into(),
4287                &deltas_handler,
4288            );
4289            msgbus::unsubscribe_book_depth(
4290                switchboard::get_book_depth_topic(target_id).into(),
4291                &depth_handler,
4292            );
4293        }
4294
4295        true
4296    }
4297
4298    fn has_book_snapshot_subscriptions(&self, instrument_id: &InstrumentId) -> bool {
4299        self.book_snapshot_counts
4300            .keys()
4301            .any(|(id, _)| id == instrument_id)
4302    }
4303
4304    fn has_book_delta_subscription_key(
4305        &self,
4306        instrument_id: InstrumentId,
4307        client_id: Option<ClientId>,
4308        venue: Option<Venue>,
4309    ) -> bool {
4310        self.book_deltas_counts
4311            .contains_key(&(instrument_id, client_id, venue))
4312    }
4313
4314    fn has_book_depth_subscription_key(
4315        &self,
4316        instrument_id: InstrumentId,
4317        client_id: Option<ClientId>,
4318        venue: Option<Venue>,
4319    ) -> bool {
4320        self.book_depth_counts
4321            .contains_key(&(instrument_id, client_id, venue))
4322    }
4323
4324    fn increment_book_delta_subscription(
4325        &mut self,
4326        instrument_id: InstrumentId,
4327        client_id: Option<ClientId>,
4328        venue: Option<Venue>,
4329    ) {
4330        let key = (instrument_id, client_id, venue);
4331
4332        if let Some(count) = self.book_deltas_counts.get_mut(&key) {
4333            *count += 1;
4334        } else {
4335            self.book_deltas_counts.insert(key, 1);
4336        }
4337    }
4338
4339    fn decrement_book_delta_subscription(
4340        &mut self,
4341        instrument_id: InstrumentId,
4342        client_id: Option<ClientId>,
4343        venue: Option<Venue>,
4344    ) -> BookDeltasUnsubscribeResult {
4345        let key = (instrument_id, client_id, venue);
4346
4347        let Some(count) = self.book_deltas_counts.get_mut(&key) else {
4348            return BookDeltasUnsubscribeResult::NotSubscribed;
4349        };
4350
4351        if *count > 1 {
4352            *count -= 1;
4353            return BookDeltasUnsubscribeResult::Decremented;
4354        }
4355
4356        self.book_deltas_counts.shift_remove(&key);
4357        BookDeltasUnsubscribeResult::Removed
4358    }
4359
4360    fn increment_book_depth_subscription(
4361        &mut self,
4362        instrument_id: InstrumentId,
4363        client_id: Option<ClientId>,
4364        venue: Option<Venue>,
4365    ) {
4366        let key = (instrument_id, client_id, venue);
4367        *self.book_depth_counts.entry(key).or_insert(0) += 1;
4368    }
4369
4370    fn decrement_book_depth_subscription(
4371        &mut self,
4372        instrument_id: InstrumentId,
4373        client_id: Option<ClientId>,
4374        venue: Option<Venue>,
4375    ) -> BookDeltasUnsubscribeResult {
4376        let key = (instrument_id, client_id, venue);
4377        let Some(count) = self.book_depth_counts.get_mut(&key) else {
4378            return BookDeltasUnsubscribeResult::NotSubscribed;
4379        };
4380
4381        if *count > 1 {
4382            *count -= 1;
4383            return BookDeltasUnsubscribeResult::Decremented;
4384        }
4385
4386        self.book_depth_counts.shift_remove(&key);
4387        BookDeltasUnsubscribeResult::Removed
4388    }
4389
4390    fn increment_book_snapshot_subscription(
4391        &mut self,
4392        cmd: &SubscribeBookSnapshots,
4393        parent: Option<(Ustr, InstrumentClass)>,
4394    ) -> bool {
4395        let key = (cmd.instrument_id, cmd.interval_ms);
4396
4397        if let Some(count) = self.book_snapshot_counts.get_mut(&key) {
4398            *count += 1;
4399            return false;
4400        }
4401
4402        self.book_snapshot_counts.insert(key, 1);
4403
4404        let snapshot_infos = if let Some(snapshot_infos) = self.book_intervals.get(&cmd.interval_ms)
4405        {
4406            snapshot_infos.clone()
4407        } else {
4408            let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
4409            self.book_intervals
4410                .insert(cmd.interval_ms, snapshot_infos.clone());
4411            self.schedule_book_snapshotter(cmd.interval_ms, snapshot_infos.clone());
4412            snapshot_infos
4413        };
4414
4415        let topic = switchboard::get_book_snapshots_topic(cmd.instrument_id, cmd.interval_ms);
4416        let snap_info = BookSnapshotInfo {
4417            instrument_id: cmd.instrument_id,
4418            venue: cmd.instrument_id.venue,
4419            parent,
4420            topic,
4421            interval_ms: cmd.interval_ms,
4422        };
4423
4424        snapshot_infos
4425            .borrow_mut()
4426            .insert(cmd.instrument_id, snap_info);
4427
4428        true
4429    }
4430
4431    fn decrement_book_snapshot_subscription(
4432        &mut self,
4433        instrument_id: InstrumentId,
4434        interval_ms: NonZeroUsize,
4435    ) -> BookSnapshotUnsubscribeResult {
4436        let key = (instrument_id, interval_ms);
4437
4438        let Some(count) = self.book_snapshot_counts.get_mut(&key) else {
4439            return BookSnapshotUnsubscribeResult::NotSubscribed;
4440        };
4441
4442        if *count > 1 {
4443            *count -= 1;
4444            return BookSnapshotUnsubscribeResult::Decremented;
4445        }
4446
4447        self.book_snapshot_counts.shift_remove(&key);
4448
4449        let remove_interval = if let Some(snapshot_infos) = self.book_intervals.get(&interval_ms) {
4450            let mut snapshot_infos = snapshot_infos.borrow_mut();
4451            snapshot_infos.shift_remove(&instrument_id);
4452            snapshot_infos.is_empty()
4453        } else {
4454            false
4455        };
4456
4457        if remove_interval {
4458            self.book_intervals.remove(&interval_ms);
4459
4460            if let Some(snapshotter) = self.book_snapshotters.remove(&interval_ms) {
4461                let timer_name = snapshotter.timer_name;
4462                let mut clock = self.clock.borrow_mut();
4463                if clock.timer_exists(&timer_name) {
4464                    clock.cancel_timer(&timer_name);
4465                }
4466            }
4467        }
4468
4469        BookSnapshotUnsubscribeResult::Removed
4470    }
4471
4472    fn schedule_book_snapshotter(
4473        &mut self,
4474        interval_ms: NonZeroUsize,
4475        snapshot_infos: BookSnapshotInfos,
4476    ) {
4477        let interval_ms_u64 =
4478            u64::try_from(interval_ms.get()).expect("Snapshot interval exceeds u64");
4479        let interval_ns = DurationNanos::from_millis(interval_ms_u64);
4480        let now_ns = self.clock.borrow().timestamp_ns();
4481        let start_time_ns = now_ns
4482            .floor(interval_ns)
4483            .checked_add(interval_ns)
4484            .expect("Book snapshot timer start exceeds UnixNanos range");
4485
4486        let snapshotter = Rc::new(BookSnapshotter::new(
4487            interval_ms,
4488            snapshot_infos,
4489            self.cache.clone(),
4490        ));
4491        let timer_name = snapshotter.timer_name;
4492        let snapshotter_callback = snapshotter.clone();
4493        let callback_fn: Rc<dyn Fn(TimeEvent)> =
4494            Rc::new(move |event| snapshotter_callback.snapshot(event));
4495        let callback = TimeEventCallback::from(callback_fn);
4496
4497        self.clock
4498            .borrow_mut()
4499            .set_timer_ns(
4500                &timer_name,
4501                interval_ns,
4502                Some(start_time_ns),
4503                None,
4504                Some(callback),
4505                None,
4506                None,
4507            )
4508            .expect(FAILED);
4509
4510        self.book_snapshotters.insert(interval_ms, snapshotter);
4511    }
4512
4513    fn handle_instrument_response(&self, instrument: InstrumentAny) {
4514        let mut cache = self.cache.as_ref().borrow_mut();
4515        if let Err(e) = cache.add_instrument(instrument) {
4516            log_error_on_cache_insert(&e);
4517        }
4518    }
4519
4520    fn handle_instruments(&self, instruments: &[InstrumentAny]) {
4521        // TODO: Improve by adding bulk update methods to cache and database
4522        let mut cache = self.cache.as_ref().borrow_mut();
4523        for instrument in instruments {
4524            if let Err(e) = cache.add_instrument(instrument.clone()) {
4525                log_error_on_cache_insert(&e);
4526            }
4527        }
4528    }
4529
4530    fn handle_quotes(&self, quotes: &[QuoteTick]) {
4531        if let Err(e) = self.cache.as_ref().borrow_mut().add_quotes(quotes) {
4532            log_error_on_cache_insert(&e);
4533        }
4534    }
4535
4536    fn handle_trades(&self, trades: &[TradeTick]) {
4537        if let Err(e) = self.cache.as_ref().borrow_mut().add_trades(trades) {
4538            log_error_on_cache_insert(&e);
4539        }
4540    }
4541
4542    fn handle_funding_rates(&self, funding_rates: &[FundingRateUpdate]) {
4543        if let Err(e) = self
4544            .cache
4545            .as_ref()
4546            .borrow_mut()
4547            .add_funding_rates(funding_rates)
4548        {
4549            log_error_on_cache_insert(&e);
4550        }
4551    }
4552
4553    fn handle_bars(&self, bars: &[Bar]) {
4554        if let Err(e) = self.cache.as_ref().borrow_mut().add_bars(bars) {
4555            log_error_on_cache_insert(&e);
4556        }
4557    }
4558
4559    // Skip cache writes that would regress a book a `BookUpdater` is maintaining.
4560    // Unmanaged subscriptions don't install a `BookUpdater`, so they don't gate writes.
4561    fn cache_is_owned_by_live_subscription(&self, instrument_id: &InstrumentId) -> bool {
4562        self.book_updaters.contains_key(instrument_id)
4563    }
4564
4565    fn handle_book_response(&self, book: &OrderBook) {
4566        if self.cache_is_owned_by_live_subscription(&book.instrument_id) {
4567            log::debug!(
4568                "Skipping cache write for order book {}: live subscription owns the book",
4569                book.instrument_id,
4570            );
4571            return;
4572        }
4573
4574        log::debug!("Adding order book {} to cache", book.instrument_id);
4575
4576        if let Err(e) = self
4577            .cache
4578            .as_ref()
4579            .borrow_mut()
4580            .add_order_book(book.clone())
4581        {
4582            log_error_on_cache_insert(&e);
4583        }
4584    }
4585
4586    fn handle_book_deltas_response(&self, resp: &BookDeltasResponse) {
4587        if !self.cache_is_owned_by_live_subscription(&resp.instrument_id) {
4588            let mut cache = self.cache.as_ref().borrow_mut();
4589            if let Some(book) = cache.order_book_mut(&resp.instrument_id) {
4590                for delta in &resp.data {
4591                    if let Err(e) = book.apply_delta(delta) {
4592                        log::error!("Failed to apply historical delta to cache: {e}");
4593                    }
4594                }
4595            } else {
4596                log::debug!(
4597                    "Skipping cache write for {} historical deltas on {}: no cache book yet",
4598                    resp.data.len(),
4599                    resp.instrument_id,
4600                );
4601            }
4602        }
4603
4604        // Group deltas by `F_LAST` so each published batch preserves the original event
4605        // boundary and metadata (timestamps and sequence from the closing delta), matching
4606        // the live `handle_delta` buffering semantic. Collapsing the whole response into
4607        // one batch would surface a synthetic event with the trailing delta's flags only.
4608        if resp.data.is_empty() {
4609            return;
4610        }
4611
4612        let topic = switchboard::get_pipeline_book_deltas_topic(resp.instrument_id);
4613        let mut frame: Vec<OrderBookDelta> = Vec::new();
4614
4615        for delta in &resp.data {
4616            frame.push(*delta);
4617            if RecordFlag::F_LAST.matches(delta.flags) {
4618                let batch = OrderBookDeltas::new(resp.instrument_id, std::mem::take(&mut frame));
4619                msgbus::publish_deltas(topic, &batch);
4620            }
4621        }
4622
4623        if !frame.is_empty() {
4624            let batch = OrderBookDeltas::new(resp.instrument_id, frame);
4625            msgbus::publish_deltas(topic, &batch);
4626        }
4627    }
4628
4629    fn handle_book_depth_response(&self, resp: &BookDepthResponse) {
4630        let topic = switchboard::get_pipeline_book_depth_topic(resp.instrument_id);
4631
4632        for depth in &resp.data {
4633            msgbus::publish_depth(topic, depth);
4634        }
4635    }
4636
4637    fn handle_option_chain_reference_price_response(
4638        &mut self,
4639        correlation_id: &UUID4,
4640        resp: &OptionChainReferencePriceResponse,
4641    ) {
4642        let Some(pending) = self.pending_option_chain_requests.get(correlation_id) else {
4643            log::debug!(
4644                "No pending option chain request for correlation_id={correlation_id}, ignoring"
4645            );
4646            return;
4647        };
4648
4649        if resp.series_id != pending.command.series_id {
4650            log::warn!(
4651                "Ignoring option-chain reference price response for {}: pending series is {}",
4652                resp.series_id,
4653                pending.command.series_id,
4654            );
4655            return;
4656        }
4657
4658        let pending = self
4659            .pending_option_chain_requests
4660            .remove(correlation_id)
4661            .expect("checked above");
4662        self.maintain_option_chain_reference_price_timeout();
4663        let series_id = pending.command.series_id;
4664
4665        if let Some(price) = resp.price {
4666            log::info!("Reference price for {series_id}: {price} (instant bootstrap)");
4667        } else {
4668            log::info!(
4669                "No reference price available for {series_id}, will bootstrap from live data",
4670            );
4671        }
4672
4673        if resp.price.is_some() {
4674            self.create_option_chain_manager(&pending.command, resp.price);
4675        } else {
4676            self.create_option_chain_manager_with_greeks_bootstrap(pending);
4677        }
4678    }
4679
4680    fn setup_book_updater(
4681        &mut self,
4682        target_ids: &[InstrumentId],
4683        book_type: BookType,
4684        depth: bool,
4685    ) -> anyhow::Result<()> {
4686        {
4687            let mut cache = self.cache.borrow_mut();
4688            for target_id in target_ids {
4689                if !cache.has_order_book(target_id) {
4690                    cache.add_order_book(OrderBook::new(*target_id, book_type))?;
4691                }
4692            }
4693        }
4694
4695        for target_id in target_ids {
4696            let updater = self
4697                .book_updaters
4698                .entry(*target_id)
4699                .or_insert_with(|| {
4700                    Rc::new(BookUpdater::new(
4701                        target_id,
4702                        self.cache.clone(),
4703                        self.config.emit_quotes_from_book,
4704                    ))
4705                })
4706                .clone();
4707
4708            if depth {
4709                msgbus::subscribe_book_depth(
4710                    switchboard::get_book_depth_topic(*target_id).into(),
4711                    TypedHandler::new(updater),
4712                    Some(self.msgbus_priority),
4713                );
4714            } else {
4715                msgbus::subscribe_book_deltas(
4716                    switchboard::get_book_deltas_topic(*target_id).into(),
4717                    TypedHandler::new(updater),
4718                    Some(self.msgbus_priority),
4719                );
4720            }
4721        }
4722
4723        Ok(())
4724    }
4725
4726    fn create_bar_aggregator(
4727        &self,
4728        instrument: &InstrumentAny,
4729        bar_type: BarType,
4730        skip_first_non_full_bar: Option<bool>,
4731    ) -> Box<dyn BarAggregator> {
4732        let cache = self.cache.clone();
4733        let validate_sequence = self.config.validate_data_sequence;
4734
4735        let handler = move |bar: Bar| {
4736            process_engine_bar(&cache, validate_sequence, true, bar);
4737        };
4738
4739        let clock = self.clock.clone();
4740        let config = self.config.clone();
4741
4742        let price_precision = instrument.price_precision();
4743        let size_precision = instrument.size_precision();
4744
4745        if bar_type.spec().is_time_aggregated() {
4746            let time_bars_origin_offset = config
4747                .time_bars_origin_offset
4748                .get(&bar_type.spec().aggregation)
4749                .map(|duration| jiff::SignedDuration::try_from(*duration).unwrap_or_default());
4750
4751            Box::new(TimeBarAggregator::new(
4752                bar_type,
4753                price_precision,
4754                size_precision,
4755                clock,
4756                handler,
4757                config.time_bars_build_with_no_updates,
4758                config.time_bars_timestamp_on_close,
4759                config.time_bars_interval_type,
4760                time_bars_origin_offset,
4761                config.time_bars_build_delay,
4762                skip_first_non_full_bar.unwrap_or(config.time_bars_skip_first_non_full_bar),
4763            ))
4764        } else {
4765            match bar_type.spec().aggregation {
4766                BarAggregation::Tick => Box::new(TickBarAggregator::new(
4767                    bar_type,
4768                    price_precision,
4769                    size_precision,
4770                    handler,
4771                )) as Box<dyn BarAggregator>,
4772                BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
4773                    bar_type,
4774                    price_precision,
4775                    size_precision,
4776                    handler,
4777                )) as Box<dyn BarAggregator>,
4778                BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
4779                    bar_type,
4780                    price_precision,
4781                    size_precision,
4782                    handler,
4783                )) as Box<dyn BarAggregator>,
4784                BarAggregation::Volume => Box::new(VolumeBarAggregator::new(
4785                    bar_type,
4786                    price_precision,
4787                    size_precision,
4788                    handler,
4789                )) as Box<dyn BarAggregator>,
4790                BarAggregation::VolumeImbalance => Box::new(VolumeImbalanceBarAggregator::new(
4791                    bar_type,
4792                    price_precision,
4793                    size_precision,
4794                    handler,
4795                )) as Box<dyn BarAggregator>,
4796                BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
4797                    bar_type,
4798                    price_precision,
4799                    size_precision,
4800                    handler,
4801                )) as Box<dyn BarAggregator>,
4802                BarAggregation::Value => Box::new(ValueBarAggregator::new(
4803                    bar_type,
4804                    price_precision,
4805                    size_precision,
4806                    handler,
4807                )) as Box<dyn BarAggregator>,
4808                BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
4809                    bar_type,
4810                    price_precision,
4811                    size_precision,
4812                    handler,
4813                )) as Box<dyn BarAggregator>,
4814                BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
4815                    bar_type,
4816                    price_precision,
4817                    size_precision,
4818                    handler,
4819                )) as Box<dyn BarAggregator>,
4820                BarAggregation::Renko => Box::new(RenkoBarAggregator::new(
4821                    bar_type,
4822                    price_precision,
4823                    size_precision,
4824                    instrument.price_increment(),
4825                    handler,
4826                )) as Box<dyn BarAggregator>,
4827                other => unreachable!(
4828                    "Unsupported internal bar aggregation dispatch for {other:?}; update `create_bar_aggregator`"
4829                ),
4830            }
4831        }
4832    }
4833
4834    fn create_bar_aggregator_for_key(
4835        &mut self,
4836        bar_type: BarType,
4837        request_id: Option<UUID4>,
4838        skip_first_non_full_bar: Option<bool>,
4839    ) -> anyhow::Result<()> {
4840        let key = bar_aggregator_key(bar_type, request_id);
4841        if self.bar_aggregators.contains_key(&key) {
4842            return Ok(());
4843        }
4844
4845        let instrument = {
4846            let cache = self.cache.borrow();
4847            cache
4848                .instrument(&bar_type.instrument_id())
4849                .ok_or_else(|| {
4850                    anyhow::anyhow!(
4851                        "Cannot start bar aggregation: no instrument found for {}",
4852                        bar_type.instrument_id(),
4853                    )
4854                })?
4855                .clone()
4856        };
4857        let aggregator = self.create_bar_aggregator(&instrument, bar_type, skip_first_non_full_bar);
4858        debug_assert_eq!(
4859            aggregator.bar_type(),
4860            key.0,
4861            "aggregator bar type must match its standardized key"
4862        );
4863        self.bar_aggregators
4864            .insert(key, Rc::new(RefCell::new(aggregator)));
4865
4866        Ok(())
4867    }
4868
4869    fn start_bar_aggregation(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
4870        let key = bar_aggregator_key(cmd.bar_type, None);
4871
4872        if self
4873            .bar_aggregators
4874            .get(&key)
4875            .is_some_and(|aggregator| aggregator.borrow().is_running())
4876            && self.bar_aggregator_handlers.contains_key(&key)
4877        {
4878            if let Some(source_command) = self
4879                .subscriptions_bar_aggregation
4880                .get(&cmd.bar_type.standard())
4881                .and_then(|subscription| subscription.source.clone())
4882            {
4883                self.execute(DataCommand::Subscribe(source_command));
4884            }
4885            log::warn!(
4886                "Aggregator for {} is currently in use, subscription can't be started",
4887                cmd.bar_type,
4888            );
4889            return Ok(());
4890        }
4891
4892        let skip_first_non_full_bar = cmd
4893            .params
4894            .as_ref()
4895            .and_then(|params| params.get_bool("skip_first_non_full_bar"));
4896        self.start_bar_aggregator(cmd.bar_type, None, skip_first_non_full_bar)?;
4897        let source = self.subscribe_bar_aggregator(cmd);
4898        self.subscriptions_bar_aggregation.insert(
4899            cmd.bar_type.standard(),
4900            BarAggregationSubscription {
4901                command: cmd.clone(),
4902                source,
4903            },
4904        );
4905
4906        Ok(())
4907    }
4908
4909    fn start_bar_aggregator(
4910        &mut self,
4911        bar_type: BarType,
4912        request_id: Option<UUID4>,
4913        skip_first_non_full_bar: Option<bool>,
4914    ) -> anyhow::Result<()> {
4915        let key = bar_aggregator_key(bar_type, request_id);
4916        let bar_type_std = bar_type.standard();
4917
4918        self.create_bar_aggregator_for_key(bar_type, request_id, skip_first_non_full_bar)?;
4919        let aggregator = self
4920            .bar_aggregators
4921            .get(&key)
4922            .ok_or_else(|| anyhow::anyhow!("Cannot start bar aggregation for {bar_type}"))?
4923            .clone();
4924        let defer_subscription_activation = request_id.is_none()
4925            && aggregator.borrow().is_running()
4926            && !self.bar_aggregator_handlers.contains_key(&key);
4927
4928        if !self.bar_aggregator_handlers.contains_key(&key) {
4929            // Subscribe to underlying data topics
4930            let mut subscriptions = Vec::new();
4931
4932            if bar_type.is_composite() {
4933                let topic = switchboard::get_bars_topic(bar_type.composite());
4934                let handler = TypedHandler::new(BarBarHandler::new(&aggregator, bar_type_std));
4935                msgbus::subscribe_bars(topic.into(), handler.clone(), None);
4936                subscriptions.push(BarAggregatorSubscription::Bar { topic, handler });
4937            } else if bar_type.spec().price_type == PriceType::Last {
4938                let topic = switchboard::get_trades_topic(bar_type.instrument_id());
4939                let handler = TypedHandler::new(BarTradeHandler::new(&aggregator, bar_type_std));
4940                msgbus::subscribe_trades(
4941                    topic.into(),
4942                    handler.clone(),
4943                    Some(BAR_AGGREGATOR_PRIORITY),
4944                );
4945                subscriptions.push(BarAggregatorSubscription::Trade { topic, handler });
4946            } else {
4947                // Warn if imbalance/runs aggregation is wired to quotes (needs aggressor_side from trades)
4948                if matches!(
4949                    bar_type.spec().aggregation,
4950                    BarAggregation::TickImbalance
4951                        | BarAggregation::VolumeImbalance
4952                        | BarAggregation::ValueImbalance
4953                        | BarAggregation::TickRuns
4954                        | BarAggregation::VolumeRuns
4955                        | BarAggregation::ValueRuns
4956                ) {
4957                    log::warn!(
4958                        "Bar type {bar_type} uses imbalance/runs aggregation which requires trade \
4959                         data with `aggressor_side`, but `price_type` is not LAST so it will receive \
4960                         quote data: bars will not emit correctly",
4961                    );
4962                }
4963
4964                let topic = switchboard::get_quotes_topic(bar_type.instrument_id());
4965                let handler = TypedHandler::new(BarQuoteHandler::new(&aggregator, bar_type_std));
4966                msgbus::subscribe_quotes(
4967                    topic.into(),
4968                    handler.clone(),
4969                    Some(BAR_AGGREGATOR_PRIORITY),
4970                );
4971                subscriptions.push(BarAggregatorSubscription::Quote { topic, handler });
4972            }
4973
4974            self.bar_aggregator_handlers.insert(key, subscriptions);
4975        }
4976
4977        if defer_subscription_activation {
4978            return Ok(());
4979        }
4980
4981        self.setup_bar_aggregator(bar_type, false, request_id)?;
4982
4983        aggregator.borrow_mut().set_is_running(true);
4984
4985        Ok(())
4986    }
4987
4988    fn subscribe_bar_aggregator(&mut self, cmd: &SubscribeBars) -> Option<SubscribeCommand> {
4989        let subscribe = self.bar_aggregator_source_command(cmd)?;
4990        self.execute(DataCommand::Subscribe(subscribe.clone()));
4991        Some(subscribe)
4992    }
4993
4994    fn bar_aggregator_source_command(&self, cmd: &SubscribeBars) -> Option<SubscribeCommand> {
4995        let key = bar_aggregator_key(cmd.bar_type, None);
4996        if !self.bar_aggregators.contains_key(&key) {
4997            log::error!(
4998                "Cannot subscribe bar aggregator: no aggregator found for {}",
4999                cmd.bar_type,
5000            );
5001            return None;
5002        }
5003
5004        if cmd.bar_type.is_composite() {
5005            let composite_bar_type = cmd.bar_type.composite();
5006            if composite_bar_type.is_externally_aggregated() {
5007                let subscribe = SubscribeBars::new(
5008                    composite_bar_type,
5009                    cmd.client_id,
5010                    cmd.venue,
5011                    UUID4::new(),
5012                    cmd.ts_init,
5013                    Some(cmd.command_id),
5014                    cmd.params.clone(),
5015                );
5016                return Some(SubscribeCommand::Bars(subscribe));
5017            }
5018        } else if cmd.bar_type.spec().price_type == PriceType::Last {
5019            let subscribe = SubscribeTrades::new(
5020                cmd.bar_type.instrument_id(),
5021                cmd.client_id,
5022                cmd.venue,
5023                UUID4::new(),
5024                cmd.ts_init,
5025                Some(cmd.command_id),
5026                cmd.params.clone(),
5027            );
5028            return Some(SubscribeCommand::Trades(subscribe));
5029        } else {
5030            let subscribe = SubscribeQuotes::new(
5031                cmd.bar_type.instrument_id(),
5032                cmd.client_id,
5033                cmd.venue,
5034                UUID4::new(),
5035                cmd.ts_init,
5036                Some(cmd.command_id),
5037                cmd.params.clone(),
5038            );
5039            return Some(SubscribeCommand::Quotes(subscribe));
5040        }
5041
5042        None
5043    }
5044
5045    /// Sets up a bar aggregator.
5046    ///
5047    /// This method handles historical mode, message bus subscriptions, and time bar aggregator setup.
5048    fn setup_bar_aggregator(
5049        &self,
5050        bar_type: BarType,
5051        historical: bool,
5052        request_id: Option<UUID4>,
5053    ) -> anyhow::Result<()> {
5054        let key = bar_aggregator_key(bar_type, request_id);
5055        let aggregator = self.bar_aggregators.get(&key).ok_or_else(|| {
5056            anyhow::anyhow!("Cannot setup bar aggregator: no aggregator found for {bar_type}")
5057        })?;
5058
5059        // Set historical mode and handler
5060        let cache = self.cache.clone();
5061        let validate_sequence = self.config.validate_data_sequence;
5062        let publish = !historical;
5063        let handler: Box<dyn FnMut(Bar)> = Box::new(move |bar: Bar| {
5064            process_engine_bar(&cache, validate_sequence, publish, bar);
5065        });
5066
5067        aggregator
5068            .borrow_mut()
5069            .set_historical_mode(historical, handler);
5070
5071        // For TimeBarAggregator, set clock and start timer
5072        if bar_type.spec().is_time_aggregated() {
5073            use nautilus_common::clock::VirtualClock;
5074
5075            if historical {
5076                // Each aggregator gets its own independent clock
5077                let test_clock = Rc::new(RefCell::new(VirtualClock::new()));
5078                aggregator.borrow_mut().set_clock(test_clock);
5079                // Set weak reference for historical mode (start_timer called later from preprocess_historical_events)
5080                // Store weak reference so start_timer can use it when called later
5081                let aggregator_weak = Rc::downgrade(aggregator);
5082                aggregator.borrow_mut().set_aggregator_weak(aggregator_weak);
5083            } else {
5084                aggregator.borrow_mut().set_clock(self.clock.clone());
5085                aggregator
5086                    .borrow_mut()
5087                    .start_timer(Some(aggregator.clone()));
5088            }
5089        }
5090
5091        Ok(())
5092    }
5093
5094    fn unsubscribe_bar_aggregator(&mut self, cmd: &UnsubscribeBars) {
5095        if cmd.bar_type.is_composite() {
5096            let composite_bar_type = cmd.bar_type.composite();
5097            if composite_bar_type.is_externally_aggregated() {
5098                let unsubscribe = UnsubscribeBars::new(
5099                    composite_bar_type,
5100                    cmd.client_id,
5101                    cmd.venue,
5102                    UUID4::new(),
5103                    cmd.ts_init,
5104                    Some(cmd.command_id),
5105                    cmd.params.clone(),
5106                );
5107                self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Bars(
5108                    unsubscribe,
5109                )));
5110            }
5111        } else if cmd.bar_type.spec().price_type == PriceType::Last {
5112            let unsubscribe = UnsubscribeTrades::new(
5113                cmd.bar_type.instrument_id(),
5114                cmd.client_id,
5115                cmd.venue,
5116                UUID4::new(),
5117                cmd.ts_init,
5118                Some(cmd.command_id),
5119                cmd.params.clone(),
5120            );
5121            self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Trades(
5122                unsubscribe,
5123            )));
5124        } else {
5125            let unsubscribe = UnsubscribeQuotes::new(
5126                cmd.bar_type.instrument_id(),
5127                cmd.client_id,
5128                cmd.venue,
5129                UUID4::new(),
5130                cmd.ts_init,
5131                Some(cmd.command_id),
5132                cmd.params.clone(),
5133            );
5134            self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(
5135                unsubscribe,
5136            )));
5137        }
5138    }
5139
5140    fn stop_bar_aggregator(
5141        &mut self,
5142        bar_type: BarType,
5143        request_id: Option<UUID4>,
5144    ) -> anyhow::Result<()> {
5145        let key = bar_aggregator_key(bar_type, request_id);
5146        let aggregator = self.bar_aggregators.shift_remove(&key).ok_or_else(|| {
5147            anyhow::anyhow!("Cannot stop bar aggregator: no aggregator to stop for {bar_type}")
5148        })?;
5149
5150        aggregator.borrow_mut().stop();
5151
5152        // Unsubscribe any registered message handlers
5153        if let Some(subs) = self.bar_aggregator_handlers.remove(&key) {
5154            for sub in subs {
5155                match sub {
5156                    BarAggregatorSubscription::Bar { topic, handler } => {
5157                        msgbus::unsubscribe_bars(topic.into(), &handler);
5158                    }
5159                    BarAggregatorSubscription::Trade { topic, handler } => {
5160                        msgbus::unsubscribe_trades(topic.into(), &handler);
5161                    }
5162                    BarAggregatorSubscription::Quote { topic, handler } => {
5163                        msgbus::unsubscribe_quotes(topic.into(), &handler);
5164                    }
5165                }
5166            }
5167        }
5168
5169        Ok(())
5170    }
5171
5172    fn subscribe_continuous_future_bars(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
5173        let target_bar_type = cmd.bar_type;
5174        let target_key = target_bar_type.standard();
5175
5176        if !target_bar_type.is_internally_aggregated() {
5177            anyhow::bail!(
5178                "Continuous future bar subscriptions require an internally aggregated target, was {target_bar_type}"
5179            );
5180        }
5181
5182        if self.continuous_future_roller.is_none() {
5183            anyhow::bail!(
5184                "Cannot subscribe continuous future bars for {target_bar_type}: roller is not initialized; ensure `register_msgbus_handlers` runs before subscribing"
5185            );
5186        }
5187
5188        let request = continuous_future_subscription_from_bars(cmd)?.ok_or_else(|| {
5189            anyhow::anyhow!(
5190                "Continuous future bar subscription requires `continuous_future_transitions`, was {cmd:?}"
5191            )
5192        })?;
5193
5194        self.ensure_continuous_future_target_instrument(&request);
5195
5196        if self
5197            .continuous_future_subscriptions
5198            .contains_key(&target_key)
5199        {
5200            log::warn!("Continuous future bars already subscribed for {target_bar_type}");
5201            return Ok(());
5202        }
5203
5204        let aggregator_key = bar_aggregator_key(target_bar_type, None);
5205        if let Some(aggregator) = self.bar_aggregators.get(&aggregator_key)
5206            && aggregator.borrow().is_running()
5207        {
5208            log::warn!(
5209                "Aggregator for {target_bar_type} is currently in use, continuous future subscription can't be started"
5210            );
5211            return Ok(());
5212        }
5213
5214        self.create_bar_aggregator_for_key(target_bar_type, None, None)?;
5215        self.setup_bar_aggregator(target_bar_type, false, None)?;
5216
5217        let now_ns = self.clock.borrow().timestamp_ns().as_u64();
5218        let Some(segment) = request.next_segment(now_ns, now_ns) else {
5219            log::error!("Cannot determine active continuous future segment for {target_bar_type}");
5220            if let Err(e) = self.stop_bar_aggregator(target_bar_type, None) {
5221                log::error!(
5222                    "Error rolling back continuous future aggregator for {target_bar_type}: {e}"
5223                );
5224            }
5225            return Ok(());
5226        };
5227
5228        self.apply_continuous_future_subscription_adjustment(&request, segment.index)?;
5229        let source = request.source_for_segment(segment.instrument_id);
5230        let source_subscription =
5231            self.subscribe_continuous_future_source(target_bar_type, source, segment.instrument_id);
5232
5233        if let Some(aggregator) = self.bar_aggregators.get(&aggregator_key) {
5234            aggregator.borrow_mut().set_is_running(true);
5235        }
5236
5237        let next_transition_index =
5238            (segment.index < request.transitions.len()).then_some(segment.index);
5239
5240        self.continuous_future_subscriptions.insert(
5241            target_key,
5242            ContinuousFutureSubscriptionState {
5243                target_bar_type,
5244                client_id: cmd.client_id,
5245                venue: cmd.venue,
5246                command_id: cmd.command_id,
5247                params: cmd.params.clone(),
5248                request,
5249                active_segment_instrument_id: segment.instrument_id,
5250                active_source: source,
5251                active_source_subscription: Some(source_subscription),
5252                next_transition_index,
5253                timer_name: None,
5254            },
5255        );
5256
5257        let child_cmd = self.build_continuous_future_subscribe_command(
5258            &target_key,
5259            source,
5260            segment.instrument_id,
5261            cmd.command_id,
5262            cmd.ts_init,
5263            true,
5264        );
5265
5266        if let Some(child) = child_cmd {
5267            self.execute(child);
5268        }
5269
5270        self.schedule_continuous_future_transition(target_key);
5271
5272        Ok(())
5273    }
5274
5275    fn unsubscribe_continuous_future_bars(&mut self, cmd: &UnsubscribeBars) {
5276        let target_key = cmd.bar_type.standard();
5277        let Some(mut state) = self.continuous_future_subscriptions.remove(&target_key) else {
5278            log::warn!(
5279                "Cannot unsubscribe continuous future bars: no subscription state for {target_key}"
5280            );
5281            return;
5282        };
5283
5284        if let Some(name) = state.timer_name.take() {
5285            self.clock.borrow_mut().cancel_timer(&name);
5286        }
5287
5288        let ts_init = self.clock.borrow().timestamp_ns();
5289        let segment_instrument_id = state.active_segment_instrument_id;
5290        let source = state.active_source;
5291        let source_subscription = state.active_source_subscription.take();
5292        let client_id = state.client_id;
5293        let venue = state.venue;
5294        let params = state.params.clone();
5295        let target_bar_type = state.target_bar_type;
5296        drop(state);
5297
5298        if let Some(subscription) = source_subscription {
5299            self.unsubscribe_continuous_future_source(target_bar_type, subscription);
5300        }
5301
5302        let child_cmd = build_continuous_future_unsubscribe_command(
5303            source,
5304            segment_instrument_id,
5305            client_id,
5306            venue,
5307            params.as_ref(),
5308            cmd.command_id,
5309            ts_init,
5310        );
5311        self.execute(child_cmd);
5312
5313        if let Err(e) = self.stop_bar_aggregator(target_bar_type, None) {
5314            log::error!("Error stopping continuous future aggregator for {target_bar_type}: {e}");
5315        }
5316    }
5317
5318    fn handle_continuous_future_subscription_transition(&mut self, event: &TimeEvent) {
5319        let event_name = event.name.as_str();
5320        let Some((target_key, transition_index)) = parse_transition_timer_name(event_name) else {
5321            log::warn!(
5322                "Ignoring continuous future transition event with unparsable name {event_name}"
5323            );
5324            return;
5325        };
5326
5327        let Some(state) = self.continuous_future_subscriptions.get_mut(&target_key) else {
5328            log::warn!(
5329                "Ignoring continuous future transition event {event_name}: no subscription state for {target_key}"
5330            );
5331            return;
5332        };
5333
5334        if state.timer_name.as_deref() != Some(event_name) {
5335            return;
5336        }
5337        state.timer_name = None;
5338
5339        let Some(next_index) = state.next_transition_index else {
5340            return;
5341        };
5342
5343        if next_index != transition_index || next_index >= state.request.transitions.len() {
5344            return;
5345        }
5346
5347        let prev_segment_instrument_id = state.active_segment_instrument_id;
5348        let next_segment_instrument_id = state.request.transitions[next_index].post_instrument_id;
5349        let new_segment_index = next_index + 1;
5350        state.active_segment_instrument_id = next_segment_instrument_id;
5351        state.next_transition_index =
5352            (new_segment_index < state.request.transitions.len()).then_some(new_segment_index);
5353
5354        let old_source = state.active_source;
5355        let old_source_subscription = state.active_source_subscription.take();
5356        let client_id = state.client_id;
5357        let venue = state.venue;
5358        let params = state.params.clone();
5359        let command_id = state.command_id;
5360        let target_bar_type = state.target_bar_type;
5361
5362        let ts_init = self.clock.borrow().timestamp_ns();
5363
5364        if let Some(subscription) = old_source_subscription {
5365            self.unsubscribe_continuous_future_source(target_bar_type, subscription);
5366        }
5367
5368        let unsub_child = build_continuous_future_unsubscribe_command(
5369            old_source,
5370            prev_segment_instrument_id,
5371            client_id,
5372            venue,
5373            params.as_ref(),
5374            command_id,
5375            ts_init,
5376        );
5377        self.execute(unsub_child);
5378
5379        if let Err(e) = self
5380            .apply_continuous_future_subscription_adjustment_for(target_bar_type, new_segment_index)
5381        {
5382            log::error!("Error applying continuous future adjustment for {target_bar_type}: {e}");
5383            return;
5384        }
5385
5386        let new_source = {
5387            let Some(state) = self.continuous_future_subscriptions.get(&target_key) else {
5388                return;
5389            };
5390            state.request.source_for_segment(next_segment_instrument_id)
5391        };
5392        let new_subscription = self.subscribe_continuous_future_source(
5393            target_bar_type,
5394            new_source,
5395            next_segment_instrument_id,
5396        );
5397
5398        if let Some(state) = self.continuous_future_subscriptions.get_mut(&target_key) {
5399            state.active_source = new_source;
5400            state.active_source_subscription = Some(new_subscription);
5401        }
5402
5403        let sub_child = self.build_continuous_future_subscribe_command(
5404            &target_key,
5405            new_source,
5406            next_segment_instrument_id,
5407            command_id,
5408            ts_init,
5409            true,
5410        );
5411
5412        if let Some(child) = sub_child {
5413            self.execute(child);
5414        }
5415
5416        self.schedule_continuous_future_transition(target_key);
5417    }
5418
5419    fn apply_continuous_future_subscription_adjustment(
5420        &self,
5421        request: &ContinuousFutureRequest,
5422        segment_index: usize,
5423    ) -> anyhow::Result<()> {
5424        let key = bar_aggregator_key(request.primary_bar_type, None);
5425        let aggregator = self.bar_aggregators.get(&key).ok_or_else(|| {
5426            anyhow::anyhow!(
5427                "No live aggregator for continuous future subscription {}",
5428                request.primary_bar_type
5429            )
5430        })?;
5431        let adjustment = request.adjustment_for_segment(segment_index);
5432        aggregator
5433            .borrow_mut()
5434            .set_adjustment(adjustment, request.adjustment_mode);
5435        Ok(())
5436    }
5437
5438    fn apply_continuous_future_subscription_adjustment_for(
5439        &self,
5440        target_bar_type: BarType,
5441        segment_index: usize,
5442    ) -> anyhow::Result<()> {
5443        let Some(state) = self
5444            .continuous_future_subscriptions
5445            .get(&target_bar_type.standard())
5446        else {
5447            anyhow::bail!("No continuous future subscription state for {target_bar_type}");
5448        };
5449        self.apply_continuous_future_subscription_adjustment(&state.request, segment_index)
5450    }
5451
5452    fn subscribe_continuous_future_source(
5453        &mut self,
5454        target_bar_type: BarType,
5455        source: ContinuousFutureSource,
5456        segment_instrument_id: InstrumentId,
5457    ) -> BarAggregatorSubscription {
5458        let key = bar_aggregator_key(target_bar_type, None);
5459        let aggregator = self
5460            .bar_aggregators
5461            .get(&key)
5462            .cloned()
5463            .expect("aggregator was created before subscribe_continuous_future_source");
5464
5465        let subscription = match source {
5466            ContinuousFutureSource::Bars(source_bar_type) => {
5467                let topic = switchboard::get_bars_topic(source_bar_type);
5468                let handler =
5469                    TypedHandler::new(BarBarHandler::new(&aggregator, target_bar_type.standard()));
5470                msgbus::subscribe_bars(topic.into(), handler.clone(), None);
5471                BarAggregatorSubscription::Bar { topic, handler }
5472            }
5473            ContinuousFutureSource::Trades => {
5474                let topic = switchboard::get_trades_topic(segment_instrument_id);
5475                let handler = TypedHandler::new(BarTradeHandler::new(
5476                    &aggregator,
5477                    target_bar_type.standard(),
5478                ));
5479                msgbus::subscribe_trades(
5480                    topic.into(),
5481                    handler.clone(),
5482                    Some(BAR_AGGREGATOR_PRIORITY),
5483                );
5484                BarAggregatorSubscription::Trade { topic, handler }
5485            }
5486            ContinuousFutureSource::Quotes => {
5487                let topic = switchboard::get_quotes_topic(segment_instrument_id);
5488                let handler = TypedHandler::new(BarQuoteHandler::new(
5489                    &aggregator,
5490                    target_bar_type.standard(),
5491                ));
5492                msgbus::subscribe_quotes(
5493                    topic.into(),
5494                    handler.clone(),
5495                    Some(BAR_AGGREGATOR_PRIORITY),
5496                );
5497                BarAggregatorSubscription::Quote { topic, handler }
5498            }
5499        };
5500
5501        self.bar_aggregator_handlers
5502            .entry(key)
5503            .or_default()
5504            .push(subscription.clone());
5505
5506        subscription
5507    }
5508
5509    fn unsubscribe_continuous_future_source(
5510        &mut self,
5511        target_bar_type: BarType,
5512        subscription: BarAggregatorSubscription,
5513    ) {
5514        let key = bar_aggregator_key(target_bar_type, None);
5515        if let Some(subs) = self.bar_aggregator_handlers.get_mut(&key) {
5516            subs.retain(|registered| !same_subscription(registered, &subscription));
5517        }
5518
5519        match subscription {
5520            BarAggregatorSubscription::Bar { topic, handler } => {
5521                msgbus::unsubscribe_bars(topic.into(), &handler);
5522            }
5523            BarAggregatorSubscription::Trade { topic, handler } => {
5524                msgbus::unsubscribe_trades(topic.into(), &handler);
5525            }
5526            BarAggregatorSubscription::Quote { topic, handler } => {
5527                msgbus::unsubscribe_quotes(topic.into(), &handler);
5528            }
5529        }
5530    }
5531
5532    fn build_continuous_future_subscribe_command(
5533        &self,
5534        target_key: &BarType,
5535        source: ContinuousFutureSource,
5536        segment_instrument_id: InstrumentId,
5537        command_id: UUID4,
5538        ts_init: UnixNanos,
5539        subscribe: bool,
5540    ) -> Option<DataCommand> {
5541        let state = self.continuous_future_subscriptions.get(target_key)?;
5542
5543        if !subscribe {
5544            return Some(build_continuous_future_unsubscribe_command(
5545                source,
5546                segment_instrument_id,
5547                state.client_id,
5548                state.venue,
5549                state.params.as_ref(),
5550                command_id,
5551                ts_init,
5552            ));
5553        }
5554
5555        let child_params = state
5556            .request
5557            .child_params(state.params.as_ref(), command_id);
5558
5559        Some(build_continuous_future_subscribe_inner(
5560            source,
5561            segment_instrument_id,
5562            state.client_id,
5563            state.venue,
5564            child_params,
5565            command_id,
5566            ts_init,
5567        ))
5568    }
5569
5570    fn schedule_continuous_future_transition(&mut self, target_key: BarType) {
5571        let Some(state) = self.continuous_future_subscriptions.get_mut(&target_key) else {
5572            return;
5573        };
5574
5575        if let Some(name) = state.timer_name.take() {
5576            self.clock.borrow_mut().cancel_timer(&name);
5577        }
5578
5579        let Some(transition_index) = state.next_transition_index else {
5580            return;
5581        };
5582        let Some(row) = state.request.transitions.get(transition_index) else {
5583            return;
5584        };
5585        let transition_ns = row.transition_time_ns;
5586        let timer_name = format!("continuous-future-roll:{target_key}:{transition_index}");
5587
5588        let Some(roller) = self.continuous_future_roller.clone() else {
5589            log::error!(
5590                "Cannot schedule continuous future transition timer for {target_key}: roller not initialized"
5591            );
5592            return;
5593        };
5594
5595        let callback_fn: Rc<dyn Fn(TimeEvent)> =
5596            Rc::new(move |event| roller.handle_transition(&event));
5597        let callback = TimeEventCallback::from(callback_fn);
5598
5599        if let Err(e) = self.clock.borrow_mut().set_time_alert_ns(
5600            &timer_name,
5601            UnixNanos::from(transition_ns),
5602            Some(callback),
5603            Some(true),
5604        ) {
5605            log::error!("Failed to schedule continuous future transition {timer_name}: {e}");
5606            return;
5607        }
5608
5609        if let Some(state) = self.continuous_future_subscriptions.get_mut(&target_key) {
5610            state.timer_name = Some(timer_name);
5611        }
5612    }
5613}
5614
5615// Resolves parent expansion components for a book subscription command.
5616//
5617// Returns Ok(Some((root, class))) when params carries PARAMS_IS_PARENT=true and
5618// the instrument_id parses as a recognized <root>.<class> shape; Ok(None) for
5619// concrete (non-parent) subscriptions; Err when the caller asserts a parent
5620// subscription but the id cannot be parsed, so subscribe entries can reject up
5621// front before touching state.
5622fn resolve_parent_components(
5623    instrument_id: &InstrumentId,
5624    params: Option<&Params>,
5625) -> anyhow::Result<Option<(Ustr, InstrumentClass)>> {
5626    if !is_parent_subscription(params) {
5627        return Ok(None);
5628    }
5629    let Some((root, class)) = instrument_id.parse_parent_components() else {
5630        anyhow::bail!(
5631            "Cannot expand parent subscription for {instrument_id}: \
5632             symbol does not parse as `<root>.<class>` with a recognized class suffix"
5633        );
5634    };
5635    Ok(Some((Ustr::from(root), class)))
5636}
5637
5638fn register_external_streaming_type(cmd: &SubscribeCommand) {
5639    if let Some(payload_type) = streaming_payload_type(cmd) {
5640        msgbus::get_message_bus()
5641            .borrow_mut()
5642            .add_streaming_type(payload_type);
5643    }
5644}
5645
5646fn publish_external_data_command<T>(client_id: ClientId, command: &T)
5647where
5648    T: Any,
5649{
5650    let topic = format!("commands.data.{client_id}");
5651    msgbus::publish_any(topic.into(), command);
5652}
5653
5654#[rustfmt::skip]
5655fn streaming_payload_type(cmd: &SubscribeCommand) -> Option<BusPayloadType> {
5656    match cmd {
5657        SubscribeCommand::Data(cmd) => Some(BusPayloadType::Custom(Ustr::from(
5658            cmd.data_type.type_name(),
5659        ))),
5660        SubscribeCommand::Instrument(_) | SubscribeCommand::Instruments(_) => Some(BusPayloadType::Instrument),
5661        SubscribeCommand::BookDeltas(_) | SubscribeCommand::BookSnapshots(_) => Some(BusPayloadType::OrderBookDeltas),
5662        SubscribeCommand::BookDepth(_) => Some(BusPayloadType::OrderBookDepth),
5663        SubscribeCommand::Quotes(_) => Some(BusPayloadType::QuoteTick),
5664        SubscribeCommand::Trades(_) => Some(BusPayloadType::TradeTick),
5665        SubscribeCommand::Bars(_) => Some(BusPayloadType::Bar),
5666        SubscribeCommand::MarkPrices(_) => Some(BusPayloadType::MarkPriceUpdate),
5667        SubscribeCommand::IndexPrices(_) => Some(BusPayloadType::IndexPriceUpdate),
5668        SubscribeCommand::FundingRates(_) => Some(BusPayloadType::FundingRateUpdate),
5669        SubscribeCommand::OptionGreeks(_) => Some(BusPayloadType::OptionGreeks),
5670        SubscribeCommand::InstrumentStatus(_)
5671        | SubscribeCommand::InstrumentClose(_)
5672        | SubscribeCommand::OptionChain(_) => None,
5673    }
5674}
5675
5676fn spread_quote_update_interval_seconds(params: Option<&Params>) -> Option<u64> {
5677    match params.and_then(|params| params.get("update_interval_seconds")) {
5678        Some(value) if value.is_null() => None,
5679        Some(value) => value.as_u64().filter(|interval| *interval > 0),
5680        None => Some(1),
5681    }
5682}
5683
5684fn spread_instrument_legs(instrument: &InstrumentAny) -> Option<Vec<(InstrumentId, i64)>> {
5685    if !instrument.is_spread() {
5686        return None;
5687    }
5688
5689    let instrument_id = instrument.id();
5690    let symbol = instrument_id.symbol.as_str();
5691    if !symbol.contains(GENERIC_SPREAD_ID_SEPARATOR) {
5692        return Some(vec![(instrument_id, 1)]);
5693    }
5694
5695    parse_generic_spread_id_legs(&instrument_id).ok()
5696}
5697
5698#[inline(always)]
5699fn log_error_on_cache_insert<T: Display>(e: &T) {
5700    log::error!("Error on cache insert: {e}");
5701}
5702
5703#[cfg(not(feature = "defi"))]
5704fn log_defi_data_dropped(data: DataRef<'_>) {
5705    log::error!("Cannot process data {data:?}, nautilus-data built without its `defi` feature");
5706}
5707
5708#[derive(Debug)]
5709struct OptionChainBootstrapper {
5710    engine: WeakCell<DataEngine>,
5711}
5712
5713impl OptionChainBootstrapper {
5714    fn new(engine: &Rc<RefCell<DataEngine>>) -> Self {
5715        Self {
5716            engine: WeakCell::from(Rc::downgrade(engine)),
5717        }
5718    }
5719
5720    fn handle_timeout(&self) {
5721        if let Some(engine) = self.engine.upgrade() {
5722            engine
5723                .borrow_mut()
5724                .handle_option_chain_reference_price_timeout();
5725        }
5726    }
5727}
5728
5729#[derive(Debug)]
5730struct PendingOptionChainRequest {
5731    command: SubscribeOptionChain,
5732    sample_instrument_id: InstrumentId,
5733    deadline_ns: UnixNanos,
5734}
5735
5736#[derive(Debug)]
5737struct OptionChainGreeksBootstrap {
5738    instrument_id: InstrumentId,
5739    client_id: ClientId,
5740    venue: Venue,
5741    ownership_handler: TypedHandler<OptionGreeks>,
5742}
5743
5744#[derive(Debug)]
5745struct SpreadQuoteState {
5746    aggregator: Rc<RefCell<SpreadQuoteAggregator>>,
5747    handlers: Vec<(InstrumentId, TypedHandler<QuoteTick>)>,
5748    owners: usize,
5749    command: SubscribeQuotes,
5750    sources: Vec<SubscribeCommand>,
5751}
5752
5753/// Routes continuous-future transition timer events back to the engine.
5754///
5755/// The clock owns the timer's callback closure; the closure must be able to
5756/// call back into the engine without creating an Rc cycle. The roller holds a
5757/// weak reference to the engine and upgrades on each fire.
5758#[derive(Debug)]
5759struct ContinuousFutureRoller {
5760    engine: WeakCell<DataEngine>,
5761}
5762
5763impl ContinuousFutureRoller {
5764    fn new(engine: &Rc<RefCell<DataEngine>>) -> Self {
5765        Self {
5766            engine: WeakCell::from(Rc::downgrade(engine)),
5767        }
5768    }
5769
5770    fn handle_transition(&self, event: &TimeEvent) {
5771        if let Some(engine) = self.engine.upgrade() {
5772            engine
5773                .borrow_mut()
5774                .handle_continuous_future_subscription_transition(event);
5775        }
5776    }
5777}
5778
5779#[derive(Debug)]
5780struct ContinuousFutureSubscriptionState {
5781    target_bar_type: BarType,
5782    client_id: Option<ClientId>,
5783    venue: Option<Venue>,
5784    command_id: UUID4,
5785    params: Option<Params>,
5786    request: ContinuousFutureRequest,
5787    active_segment_instrument_id: InstrumentId,
5788    active_source: ContinuousFutureSource,
5789    active_source_subscription: Option<BarAggregatorSubscription>,
5790    next_transition_index: Option<usize>,
5791    timer_name: Option<String>,
5792}
5793
5794fn same_subscription(a: &BarAggregatorSubscription, b: &BarAggregatorSubscription) -> bool {
5795    match (a, b) {
5796        (
5797            BarAggregatorSubscription::Bar { handler: h1, .. },
5798            BarAggregatorSubscription::Bar { handler: h2, .. },
5799        ) => h1.id() == h2.id(),
5800        (
5801            BarAggregatorSubscription::Trade { handler: h1, .. },
5802            BarAggregatorSubscription::Trade { handler: h2, .. },
5803        ) => h1.id() == h2.id(),
5804        (
5805            BarAggregatorSubscription::Quote { handler: h1, .. },
5806            BarAggregatorSubscription::Quote { handler: h2, .. },
5807        ) => h1.id() == h2.id(),
5808        _ => false,
5809    }
5810}
5811
5812fn parse_transition_timer_name(name: &str) -> Option<(BarType, usize)> {
5813    let rest = name.strip_prefix("continuous-future-roll:")?;
5814    let (target, index) = rest.rsplit_once(':')?;
5815    let bar_type = BarType::from_str(target).ok()?;
5816    let index = index.parse::<usize>().ok()?;
5817    Some((bar_type, index))
5818}
5819
5820fn build_continuous_future_subscribe_inner(
5821    source: ContinuousFutureSource,
5822    segment_instrument_id: InstrumentId,
5823    client_id: Option<ClientId>,
5824    _venue: Option<Venue>,
5825    child_params: Params,
5826    correlation_id: UUID4,
5827    ts_init: UnixNanos,
5828) -> DataCommand {
5829    let command_id = UUID4::new();
5830    let child_venue = Some(segment_instrument_id.venue);
5831
5832    match source {
5833        ContinuousFutureSource::Bars(source_bar_type) => {
5834            DataCommand::Subscribe(SubscribeCommand::Bars(SubscribeBars::new(
5835                source_bar_type,
5836                client_id,
5837                child_venue,
5838                command_id,
5839                ts_init,
5840                Some(correlation_id),
5841                Some(child_params),
5842            )))
5843        }
5844        ContinuousFutureSource::Trades => {
5845            DataCommand::Subscribe(SubscribeCommand::Trades(SubscribeTrades::new(
5846                segment_instrument_id,
5847                client_id,
5848                child_venue,
5849                command_id,
5850                ts_init,
5851                Some(correlation_id),
5852                Some(child_params),
5853            )))
5854        }
5855        ContinuousFutureSource::Quotes => {
5856            DataCommand::Subscribe(SubscribeCommand::Quotes(SubscribeQuotes::new(
5857                segment_instrument_id,
5858                client_id,
5859                child_venue,
5860                command_id,
5861                ts_init,
5862                Some(correlation_id),
5863                Some(child_params),
5864            )))
5865        }
5866    }
5867}
5868
5869fn build_continuous_future_unsubscribe_command(
5870    source: ContinuousFutureSource,
5871    segment_instrument_id: InstrumentId,
5872    client_id: Option<ClientId>,
5873    _venue: Option<Venue>,
5874    parent_params: Option<&Params>,
5875    correlation_id: UUID4,
5876    ts_init: UnixNanos,
5877) -> DataCommand {
5878    let mut child_params = parent_params.cloned().unwrap_or_default();
5879    child_params.shift_remove("continuous_future_transitions");
5880    child_params.shift_remove("continuous_future_adjustment_mode");
5881    child_params.shift_remove("last_post_instrument_id");
5882    child_params.shift_remove("first_pre_instrument_id");
5883    child_params.shift_remove("bar_types");
5884    let command_id = UUID4::new();
5885    let child_venue = Some(segment_instrument_id.venue);
5886
5887    match source {
5888        ContinuousFutureSource::Bars(source_bar_type) => {
5889            DataCommand::Unsubscribe(UnsubscribeCommand::Bars(UnsubscribeBars::new(
5890                source_bar_type,
5891                client_id,
5892                child_venue,
5893                command_id,
5894                ts_init,
5895                Some(correlation_id),
5896                Some(child_params),
5897            )))
5898        }
5899        ContinuousFutureSource::Trades => {
5900            DataCommand::Unsubscribe(UnsubscribeCommand::Trades(UnsubscribeTrades::new(
5901                segment_instrument_id,
5902                client_id,
5903                child_venue,
5904                command_id,
5905                ts_init,
5906                Some(correlation_id),
5907                Some(child_params),
5908            )))
5909        }
5910        ContinuousFutureSource::Quotes => {
5911            DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(UnsubscribeQuotes::new(
5912                segment_instrument_id,
5913                client_id,
5914                child_venue,
5915                command_id,
5916                ts_init,
5917                Some(correlation_id),
5918                Some(child_params),
5919            )))
5920        }
5921    }
5922}
5923
5924fn datetime_to_unix_nanos(datetime: jiff::Timestamp) -> anyhow::Result<UnixNanos> {
5925    let timestamp = datetime.as_nanosecond();
5926    let timestamp = u64::try_from(timestamp)
5927        .context("datetime is before the UNIX epoch and cannot be represented as UnixNanos")?;
5928    Ok(UnixNanos::from(timestamp))
5929}
5930
5931// Top-of-book `QuoteTick` from an `OrderBookDepth`. Returns `None` for
5932// missing-side padding or zero size.
5933fn derive_quote_from_depth(depth: &OrderBookDepth) -> Option<QuoteTick> {
5934    let bid = depth.bids.first()?;
5935    let ask = depth.asks.first()?;
5936
5937    if bid.side.is_none() || ask.side.is_none() || bid.size.is_zero() || ask.size.is_zero() {
5938        return None;
5939    }
5940
5941    Some(QuoteTick::new(
5942        depth.instrument_id,
5943        bid.price,
5944        ask.price,
5945        bid.size,
5946        ask.size,
5947        depth.ts_event,
5948        depth.ts_init,
5949    ))
5950}
5951
5952// Validates a bar against `last_bar` before writing and (optionally) publishing.
5953// Live bars and aggregator emissions honor `validate_data_sequence`;
5954// request-generated bars use `Cache::add_bar_historical`.
5955fn process_engine_bar(
5956    cache: &Rc<RefCell<Cache>>,
5957    validate_sequence: bool,
5958    publish: bool,
5959    bar: Bar,
5960) {
5961    debug_assert!(
5962        bar.bar_type.is_standard(),
5963        "bars must be published and cached under the standard bar type"
5964    );
5965
5966    if !validate_bar_sequence(cache, validate_sequence, &bar) {
5967        return;
5968    }
5969
5970    if let Err(e) = cache.as_ref().borrow_mut().add_bar(bar) {
5971        log_error_on_cache_insert(&e);
5972    }
5973
5974    if publish {
5975        let topic = switchboard::get_bars_topic(bar.bar_type);
5976        msgbus::publish_bar(topic, &bar);
5977    }
5978}
5979
5980fn validate_bar_sequence(cache: &Rc<RefCell<Cache>>, validate_sequence: bool, bar: &Bar) -> bool {
5981    if !validate_sequence {
5982        return true;
5983    }
5984
5985    let Some(last_bar) = cache.as_ref().borrow().bar(&bar.bar_type).copied() else {
5986        return true;
5987    };
5988
5989    if bar.ts_event < last_bar.ts_event {
5990        log::warn!(
5991            "Bar {bar} was prior to last bar `ts_event` {}",
5992            last_bar.ts_event,
5993        );
5994        return false;
5995    }
5996
5997    if bar.ts_init < last_bar.ts_init {
5998        log::warn!(
5999            "Bar {bar} was prior to last bar `ts_init` {}",
6000            last_bar.ts_init,
6001        );
6002        return false;
6003    }
6004
6005    true
6006}
6007
6008#[inline(always)]
6009fn log_if_empty_response<T, I: Display>(data: &[T], id: &I, correlation_id: &UUID4) -> bool {
6010    if data.is_empty() {
6011        let name = type_name::<T>();
6012        let short_name = name.rsplit("::").next().unwrap_or(name);
6013        log::warn!("Received empty {short_name} response for {id} {correlation_id}");
6014        return true;
6015    }
6016    false
6017}
6018
6019/// Concatenates same-variant leg payloads into a single rebuilt response keyed by `parent_id`.
6020///
6021/// Returns `None` when legs are mixed-variant or empty; pipelines only group legs of the same
6022/// variant. `BookDeltas` legs additionally return `None` when their wrapper instruments differ,
6023/// since a book-delta batch is keyed by one instrument and cannot carry another's children.
6024/// The rebuilt response inherits `start` and `end` from the parent request when the parent is a
6025/// `RequestJoin`; otherwise leg bounds are preserved on the first leg.
6026fn rebuild_pipeline_response(
6027    parent_id: UUID4,
6028    parent: Option<&RequestCommand>,
6029    legs: Vec<DataResponse>,
6030) -> Option<DataResponse> {
6031    if legs.is_empty() {
6032        return None;
6033    }
6034
6035    let (parent_start, parent_end) = parent_request_window(parent);
6036
6037    let mut iter = legs.into_iter();
6038    let first = iter.next()?;
6039
6040    match first {
6041        DataResponse::Data(mut acc) => {
6042            let mut data = custom_response_data(&acc, parent_id)?;
6043
6044            for leg in iter {
6045                let DataResponse::Data(other) = leg else {
6046                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6047                    return None;
6048                };
6049                data.extend(custom_response_data(&other, parent_id)?);
6050            }
6051
6052            data.sort_by_key(CustomData::ts_init);
6053            acc.data = std::sync::Arc::new(data);
6054            acc.correlation_id = parent_id;
6055            if parent_start.is_some() {
6056                acc.start = parent_start;
6057            }
6058
6059            if parent_end.is_some() {
6060                acc.end = parent_end;
6061            }
6062            Some(DataResponse::Data(acc))
6063        }
6064        DataResponse::Quotes(mut acc) => {
6065            for leg in iter {
6066                let DataResponse::Quotes(other) = leg else {
6067                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6068                    return None;
6069                };
6070                acc.data.extend(other.data);
6071            }
6072            acc.data.sort_by_key(|q| q.ts_init);
6073            acc.correlation_id = parent_id;
6074            if parent_start.is_some() {
6075                acc.start = parent_start;
6076            }
6077
6078            if parent_end.is_some() {
6079                acc.end = parent_end;
6080            }
6081            Some(DataResponse::Quotes(acc))
6082        }
6083        DataResponse::Trades(mut acc) => {
6084            for leg in iter {
6085                let DataResponse::Trades(other) = leg else {
6086                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6087                    return None;
6088                };
6089                acc.data.extend(other.data);
6090            }
6091            acc.data.sort_by_key(|t| t.ts_init);
6092            acc.correlation_id = parent_id;
6093            if parent_start.is_some() {
6094                acc.start = parent_start;
6095            }
6096
6097            if parent_end.is_some() {
6098                acc.end = parent_end;
6099            }
6100            Some(DataResponse::Trades(acc))
6101        }
6102        DataResponse::FundingRates(mut acc) => {
6103            for leg in iter {
6104                let DataResponse::FundingRates(other) = leg else {
6105                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6106                    return None;
6107                };
6108                acc.data.extend(other.data);
6109            }
6110            acc.data.sort_by_key(|r| r.ts_init);
6111            acc.correlation_id = parent_id;
6112            if parent_start.is_some() {
6113                acc.start = parent_start;
6114            }
6115
6116            if parent_end.is_some() {
6117                acc.end = parent_end;
6118            }
6119            Some(DataResponse::FundingRates(acc))
6120        }
6121        DataResponse::Bars(mut acc) => {
6122            for leg in iter {
6123                let DataResponse::Bars(other) = leg else {
6124                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6125                    return None;
6126                };
6127                acc.data.extend(other.data);
6128            }
6129            acc.data.sort_by_key(|b| b.ts_init);
6130            acc.correlation_id = parent_id;
6131            if parent_start.is_some() {
6132                acc.start = parent_start;
6133            }
6134
6135            if parent_end.is_some() {
6136                acc.end = parent_end;
6137            }
6138            Some(DataResponse::Bars(acc))
6139        }
6140        DataResponse::Instruments(mut acc) => {
6141            for leg in iter {
6142                let DataResponse::Instruments(other) = leg else {
6143                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6144                    return None;
6145                };
6146                acc.data.extend(other.data);
6147            }
6148            acc.correlation_id = parent_id;
6149            Some(DataResponse::Instruments(acc))
6150        }
6151        DataResponse::BookDeltas(mut acc) => {
6152            for leg in iter {
6153                let DataResponse::BookDeltas(other) = leg else {
6154                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6155                    return None;
6156                };
6157
6158                // A book-delta batch is keyed by one instrument, so legs for different
6159                // instruments cannot be concatenated into a single response. Matched by
6160                // value as well as identity, mirroring `OrderBookDeltas::new_checked`,
6161                // since legs crossing the FFI boundary do not share an intern pool.
6162                let same_instrument = other.instrument_id == acc.instrument_id
6163                    || (other.instrument_id.symbol.as_str() == acc.instrument_id.symbol.as_str()
6164                        && other.instrument_id.venue.as_str() == acc.instrument_id.venue.as_str());
6165
6166                if !same_instrument {
6167                    log::error!(
6168                        "Mixed-instrument BookDeltas legs in pipeline {parent_id}: {} and {}",
6169                        acc.instrument_id,
6170                        other.instrument_id,
6171                    );
6172                    return None;
6173                }
6174
6175                acc.data.extend(other.data);
6176            }
6177            acc.data.sort_by_key(|d| d.ts_init);
6178            acc.correlation_id = parent_id;
6179            if parent_start.is_some() {
6180                acc.start = parent_start;
6181            }
6182
6183            if parent_end.is_some() {
6184                acc.end = parent_end;
6185            }
6186            Some(DataResponse::BookDeltas(acc))
6187        }
6188        DataResponse::BookDepth(mut acc) => {
6189            for leg in iter {
6190                let DataResponse::BookDepth(other) = leg else {
6191                    log::error!("Mixed-variant legs in pipeline {parent_id}");
6192                    return None;
6193                };
6194                acc.data.extend(other.data);
6195            }
6196            acc.data.sort_by_key(|d| d.ts_init);
6197            acc.correlation_id = parent_id;
6198            if parent_start.is_some() {
6199                acc.start = parent_start;
6200            }
6201
6202            if parent_end.is_some() {
6203                acc.end = parent_end;
6204            }
6205            Some(DataResponse::BookDepth(acc))
6206        }
6207        other => {
6208            // Pipelines today rebuild same-variant time-series legs. Variants
6209            // without a per-item ts_init payload (singular Book/Instrument,
6210            // OptionChainReferencePrice) cannot be concatenated and would
6211            // otherwise leak a leg-keyed response. Drop rather than forward.
6212            log::error!(
6213                "Pipeline rebuild not supported for variant {} (parent {parent_id})",
6214                other.kind(),
6215            );
6216            None
6217        }
6218    }
6219}
6220
6221fn custom_response_data(resp: &CustomDataResponse, parent_id: UUID4) -> Option<Vec<CustomData>> {
6222    if let Some(data) = resp.data.as_ref().downcast_ref::<Vec<CustomData>>() {
6223        return Some(data.clone());
6224    }
6225
6226    if let Some(data) = resp.data.as_ref().downcast_ref::<CustomData>() {
6227        return Some(vec![data.clone()]);
6228    }
6229
6230    if let Some(data) = resp.data.as_ref().downcast_ref::<Vec<Data>>() {
6231        let mut custom = Vec::with_capacity(data.len());
6232        for item in data {
6233            let Data::Custom(value) = item else {
6234                log::error!("Custom data pipeline {parent_id} received non-custom data {item:?}");
6235                return None;
6236            };
6237            custom.push(value.clone());
6238        }
6239        return Some(custom);
6240    }
6241
6242    log::error!(
6243        "Custom data pipeline {parent_id} received unsupported payload for {}",
6244        resp.data_type,
6245    );
6246    None
6247}
6248
6249fn parent_request_window(
6250    parent: Option<&RequestCommand>,
6251) -> (Option<UnixNanos>, Option<UnixNanos>) {
6252    let Some(parent) = parent else {
6253        return (None, None);
6254    };
6255
6256    let (start, end) = match parent {
6257        RequestCommand::Data(cmd) => (cmd.start, cmd.end),
6258        RequestCommand::Instrument(cmd) => (cmd.start, cmd.end),
6259        RequestCommand::Instruments(cmd) => (cmd.start, cmd.end),
6260        RequestCommand::BookDeltas(cmd) => (cmd.start, cmd.end),
6261        RequestCommand::BookDepth(cmd) => (cmd.start, cmd.end),
6262        RequestCommand::Quotes(cmd) => (cmd.start, cmd.end),
6263        RequestCommand::Trades(cmd) => (cmd.start, cmd.end),
6264        RequestCommand::FundingRates(cmd) => (cmd.start, cmd.end),
6265        RequestCommand::Bars(cmd) => (cmd.start, cmd.end),
6266        RequestCommand::Join(cmd) => (cmd.start, cmd.end),
6267        RequestCommand::BookSnapshot(_) | RequestCommand::OptionChainReferencePrice(_) => {
6268            return (None, None);
6269        }
6270    };
6271
6272    (
6273        start.map(datetime_to_unix_nanos_or_zero),
6274        end.map(datetime_to_unix_nanos_or_zero),
6275    )
6276}
6277
6278fn datetime_to_unix_nanos_or_zero(dt: jiff::Timestamp) -> UnixNanos {
6279    UnixNanos::from(u64::try_from(dt.as_nanosecond().max(0)).unwrap_or(0))
6280}
6281
6282fn empty_response_like(
6283    template: &DataResponse,
6284    correlation_id: UUID4,
6285    ts_init: UnixNanos,
6286) -> DataResponse {
6287    match template {
6288        DataResponse::Quotes(r) => DataResponse::Quotes(QuotesResponse::new(
6289            correlation_id,
6290            r.client_id,
6291            r.instrument_id,
6292            Vec::new(),
6293            r.start,
6294            r.end,
6295            ts_init,
6296            r.params.clone(),
6297        )),
6298        DataResponse::Trades(r) => DataResponse::Trades(TradesResponse::new(
6299            correlation_id,
6300            r.client_id,
6301            r.instrument_id,
6302            Vec::new(),
6303            r.start,
6304            r.end,
6305            ts_init,
6306            r.params.clone(),
6307        )),
6308        DataResponse::FundingRates(r) => DataResponse::FundingRates(FundingRatesResponse::new(
6309            correlation_id,
6310            r.client_id,
6311            r.instrument_id,
6312            Vec::new(),
6313            r.start,
6314            r.end,
6315            ts_init,
6316            r.params.clone(),
6317        )),
6318        DataResponse::Bars(r) => DataResponse::Bars(BarsResponse::new(
6319            correlation_id,
6320            r.client_id,
6321            r.bar_type,
6322            Vec::new(),
6323            r.start,
6324            r.end,
6325            ts_init,
6326            r.params.clone(),
6327        )),
6328        DataResponse::BookDeltas(r) => DataResponse::BookDeltas(BookDeltasResponse::new(
6329            correlation_id,
6330            r.client_id,
6331            r.instrument_id,
6332            Vec::new(),
6333            r.start,
6334            r.end,
6335            ts_init,
6336            r.params.clone(),
6337        )),
6338        DataResponse::BookDepth(r) => DataResponse::BookDepth(BookDepthResponse::new(
6339            correlation_id,
6340            r.client_id,
6341            r.instrument_id,
6342            Vec::new(),
6343            r.start,
6344            r.end,
6345            ts_init,
6346            r.params.clone(),
6347        )),
6348        other => {
6349            log::error!(
6350                "Cannot fabricate empty leg response for variant {}",
6351                other.kind(),
6352            );
6353            other.clone()
6354        }
6355    }
6356}
6357
6358fn rebind_response_correlation(mut resp: DataResponse, new_id: UUID4) -> DataResponse {
6359    match &mut resp {
6360        DataResponse::Data(r) => r.correlation_id = new_id,
6361        DataResponse::Instrument(r) => r.correlation_id = new_id,
6362        DataResponse::Instruments(r) => r.correlation_id = new_id,
6363        DataResponse::Book(r) => r.correlation_id = new_id,
6364        DataResponse::BookDeltas(r) => r.correlation_id = new_id,
6365        DataResponse::BookDepth(r) => r.correlation_id = new_id,
6366        DataResponse::Quotes(r) => r.correlation_id = new_id,
6367        DataResponse::Trades(r) => r.correlation_id = new_id,
6368        DataResponse::FundingRates(r) => r.correlation_id = new_id,
6369        DataResponse::OptionChainReferencePrice(r) => r.correlation_id = new_id,
6370        DataResponse::Bars(r) => r.correlation_id = new_id,
6371    }
6372    resp
6373}