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