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