Skip to main content

nautilus_data/engine/
mod.rs

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