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