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