Skip to main content

nautilus_data/engine/
mod.rs

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