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