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