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