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