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