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