Skip to main content

nautilus_data/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Base data client functionality.
17//!
18//! Provides the `DataClientAdapter` for managing subscriptions and requests,
19//! and utilities for constructing data responses.
20
21use std::{
22    fmt::{Debug, Display},
23    hash::Hash,
24    ops::{Deref, DerefMut},
25};
26
27use ahash::AHashSet;
28use indexmap::IndexMap;
29#[cfg(feature = "defi")]
30use nautilus_common::messages::defi::DefiSubscribeCommand;
31use nautilus_common::{
32    clients::{DataClient, log_command_error},
33    enums::LogColor,
34    log_info,
35    messages::data::{
36        RequestBars, RequestBookDepth, RequestBookSnapshot, RequestCustomData, RequestFundingRates,
37        RequestInstrument, RequestInstruments, RequestOptionChainReferencePrice, RequestQuotes,
38        RequestTrades, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth, SubscribeCommand,
39        SubscribeCustomData, SubscribeFundingRates, SubscribeIndexPrices, SubscribeInstrument,
40        SubscribeInstrumentClose, SubscribeInstrumentStatus, SubscribeInstruments,
41        SubscribeMarkPrices, SubscribeOptionGreeks, SubscribeQuotes, SubscribeTrades,
42        SubscriptionError, UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth,
43        UnsubscribeCommand, UnsubscribeCustomData, UnsubscribeFundingRates, UnsubscribeIndexPrices,
44        UnsubscribeInstrument, UnsubscribeInstrumentClose, UnsubscribeInstrumentStatus,
45        UnsubscribeInstruments, UnsubscribeMarkPrices, UnsubscribeOptionGreeks, UnsubscribeQuotes,
46        UnsubscribeTrades, ensure_routable_bar_types, ensure_routable_instrument_ids,
47    },
48};
49#[cfg(feature = "defi")]
50use nautilus_model::defi::Blockchain;
51use nautilus_model::{
52    data::{BarType, DataType},
53    identifiers::{ClientId, InstrumentId, Venue},
54};
55
56#[cfg(feature = "defi")]
57#[allow(unused_imports)] // Brings DeFi impl blocks into scope
58use crate::defi::client as _;
59#[cfg(feature = "defi")]
60use crate::subscription::DefiSubscriptionKey;
61use crate::subscription::{
62    SubscriptionKey, SubscriptionRegistry, SubscriptionRelease, split_subscribe, split_unsubscribe,
63};
64
65/// Wraps a [`DataClient`], managing subscription state and forwarding commands.
66pub struct DataClientAdapter {
67    pub(crate) client: Box<dyn DataClient>,
68    pub client_id: ClientId,
69    pub venue: Option<Venue>,
70    pub handles_book_deltas: bool,
71    pub handles_book_snapshots: bool,
72    pub subscriptions_custom: AHashSet<DataType>,
73    pub subscriptions_book_deltas: AHashSet<InstrumentId>,
74    pub subscriptions_book_depth: AHashSet<InstrumentId>,
75    pub subscriptions_quotes: AHashSet<InstrumentId>,
76    pub subscriptions_trades: AHashSet<InstrumentId>,
77    pub subscriptions_bars: AHashSet<BarType>,
78    pub subscriptions_instrument_status: AHashSet<InstrumentId>,
79    pub subscriptions_instrument_close: AHashSet<InstrumentId>,
80    pub subscriptions_instrument: AHashSet<InstrumentId>,
81    pub subscriptions_instrument_venue: AHashSet<Venue>,
82    pub subscriptions_mark_prices: AHashSet<InstrumentId>,
83    pub subscriptions_index_prices: AHashSet<InstrumentId>,
84    pub subscriptions_funding_rates: AHashSet<InstrumentId>,
85    pub subscriptions_option_greeks: AHashSet<InstrumentId>,
86    subscriptions_active: SubscriptionRegistry<SubscriptionKey, SubscribeCommand>,
87    #[cfg(feature = "defi")]
88    pub(crate) subscriptions_active_defi:
89        SubscriptionRegistry<DefiSubscriptionKey, DefiSubscribeCommand>,
90    #[cfg(feature = "defi")]
91    pub subscriptions_blocks: AHashSet<Blockchain>,
92    #[cfg(feature = "defi")]
93    pub subscriptions_pools: AHashSet<InstrumentId>,
94    #[cfg(feature = "defi")]
95    pub subscriptions_pool_swaps: AHashSet<InstrumentId>,
96    #[cfg(feature = "defi")]
97    pub subscriptions_pool_liquidity_updates: AHashSet<InstrumentId>,
98    #[cfg(feature = "defi")]
99    pub subscriptions_pool_fee_collects: AHashSet<InstrumentId>,
100    #[cfg(feature = "defi")]
101    pub subscriptions_pool_flash: AHashSet<InstrumentId>,
102}
103
104impl Deref for DataClientAdapter {
105    type Target = Box<dyn DataClient>;
106
107    fn deref(&self) -> &Self::Target {
108        &self.client
109    }
110}
111
112impl DerefMut for DataClientAdapter {
113    fn deref_mut(&mut self) -> &mut Self::Target {
114        &mut self.client
115    }
116}
117
118impl Debug for DataClientAdapter {
119    #[rustfmt::skip]
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct(stringify!(DataClientAdapter))
122            .field("client_id", &self.client_id)
123            .field("venue", &self.venue)
124            .field("handles_book_deltas", &self.handles_book_deltas)
125            .field("handles_book_snapshots", &self.handles_book_snapshots)
126            .field("subscriptions_custom", &self.subscriptions_custom)
127            .field("subscriptions_book_deltas", &self.subscriptions_book_deltas)
128            .field("subscriptions_book_depth", &self.subscriptions_book_depth)
129            .field("subscriptions_quotes", &self.subscriptions_quotes)
130            .field("subscriptions_trades", &self.subscriptions_trades)
131            .field("subscriptions_bars", &self.subscriptions_bars)
132            .field("subscriptions_mark_prices", &self.subscriptions_mark_prices)
133            .field("subscriptions_index_prices", &self.subscriptions_index_prices)
134            .field("subscriptions_instrument_status", &self.subscriptions_instrument_status)
135            .field("subscriptions_instrument_close", &self.subscriptions_instrument_close)
136            .field("subscriptions_instrument", &self.subscriptions_instrument)
137            .field("subscriptions_instrument_venue", &self.subscriptions_instrument_venue)
138            .finish()
139    }
140}
141
142impl DataClientAdapter {
143    /// Creates a new [`DataClientAdapter`] with the given client and clock.
144    #[must_use]
145    pub fn new(
146        client_id: ClientId,
147        venue: Option<Venue>,
148        handles_order_book_deltas: bool,
149        handles_order_book_snapshots: bool,
150        client: Box<dyn DataClient>,
151    ) -> Self {
152        Self {
153            client,
154            client_id,
155            venue,
156            handles_book_deltas: handles_order_book_deltas,
157            handles_book_snapshots: handles_order_book_snapshots,
158            subscriptions_custom: AHashSet::new(),
159            subscriptions_book_deltas: AHashSet::new(),
160            subscriptions_book_depth: AHashSet::new(),
161            subscriptions_quotes: AHashSet::new(),
162            subscriptions_trades: AHashSet::new(),
163            subscriptions_mark_prices: AHashSet::new(),
164            subscriptions_index_prices: AHashSet::new(),
165            subscriptions_funding_rates: AHashSet::new(),
166            subscriptions_option_greeks: AHashSet::new(),
167            subscriptions_bars: AHashSet::new(),
168            subscriptions_instrument_status: AHashSet::new(),
169            subscriptions_instrument_close: AHashSet::new(),
170            subscriptions_instrument: AHashSet::new(),
171            subscriptions_instrument_venue: AHashSet::new(),
172            subscriptions_active: SubscriptionRegistry::default(),
173            #[cfg(feature = "defi")]
174            subscriptions_active_defi: SubscriptionRegistry::default(),
175            #[cfg(feature = "defi")]
176            subscriptions_blocks: AHashSet::new(),
177            #[cfg(feature = "defi")]
178            subscriptions_pools: AHashSet::new(),
179            #[cfg(feature = "defi")]
180            subscriptions_pool_swaps: AHashSet::new(),
181            #[cfg(feature = "defi")]
182            subscriptions_pool_liquidity_updates: AHashSet::new(),
183            #[cfg(feature = "defi")]
184            subscriptions_pool_fee_collects: AHashSet::new(),
185            #[cfg(feature = "defi")]
186            subscriptions_pool_flash: AHashSet::new(),
187        }
188    }
189
190    #[expect(clippy::borrowed_box)]
191    #[must_use]
192    pub fn get_client(&self) -> &Box<dyn DataClient> {
193        &self.client
194    }
195
196    /// Connects the underlying client to the data provider.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if the connection fails.
201    pub async fn connect(&mut self) -> anyhow::Result<()> {
202        self.client.connect().await
203    }
204
205    /// Disconnects the underlying client from the data provider.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if the disconnection fails.
210    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
211        self.client.disconnect().await
212    }
213
214    #[inline]
215    pub fn execute_subscribe(&mut self, cmd: SubscribeCommand) {
216        self.execute_subscribe_with_retained(cmd.clone(), cmd, false);
217    }
218
219    pub(crate) fn execute_subscribe_intent(&mut self, cmd: SubscribeCommand) {
220        self.execute_subscribe_with_retained(cmd.clone(), cmd, true);
221    }
222
223    pub(crate) fn execute_subscribe_with_retained(
224        &mut self,
225        cmd: SubscribeCommand,
226        retained: SubscribeCommand,
227        retain_on_failure: bool,
228    ) {
229        for command in [&cmd, &retained] {
230            if let Some(bar_types) = command.bar_types()
231                && let Err(e) = ensure_routable_bar_types(bar_types, command.client_id().is_some())
232            {
233                log_command_error(
234                    &format_args!(
235                        "Subscribe {} command {}",
236                        self.client_id(),
237                        command.command_id()
238                    ),
239                    &e,
240                );
241                return;
242            }
243
244            if let Some(ids) = command.instrument_ids()
245                && let Err(e) = ensure_routable_instrument_ids(ids, command.client_id().is_some())
246            {
247                log_command_error(
248                    &format_args!(
249                        "Subscribe {} command {}",
250                        self.client_id(),
251                        command.command_id()
252                    ),
253                    &e,
254                );
255                return;
256            }
257        }
258
259        self.subscribe_retained(cmd, retained, retain_on_failure);
260    }
261
262    pub(crate) fn retain_intent(&mut self, command: &SubscribeCommand) {
263        for member in split_subscribe(command.clone()) {
264            self.subscriptions_active.retain(
265                SubscriptionKey::from_subscribe(&member),
266                member.command_id(),
267                member,
268            );
269        }
270    }
271
272    fn subscribe_retained(
273        &mut self,
274        cmd: SubscribeCommand,
275        retained: SubscribeCommand,
276        retain_on_failure: bool,
277    ) {
278        let mut pending = Vec::new();
279
280        for member in split_subscribe(retained) {
281            let key = SubscriptionKey::from_subscribe(&member);
282            if self.has_active_subscription(&member) {
283                self.subscriptions_active
284                    .retain(key, member.command_id(), member);
285            } else {
286                pending.push((key, member));
287            }
288        }
289
290        if pending.is_empty() {
291            return;
292        }
293
294        let command_id = cmd.command_id();
295
296        let result = match cmd {
297            SubscribeCommand::Data(cmd) => self.subscribe(cmd),
298            SubscribeCommand::Instrument(cmd) => self.subscribe_instrument(&cmd),
299            SubscribeCommand::Instruments(cmd) => self.subscribe_instruments(cmd),
300            SubscribeCommand::BookDeltas(cmd) => self.subscribe_book_deltas(&cmd),
301            SubscribeCommand::BookDepth(cmd) => self.subscribe_book_depth(&cmd),
302            SubscribeCommand::BookSnapshots(_) => Ok(()), // Handled internally by engine
303            SubscribeCommand::Quotes(cmd) => self.subscribe_quotes(&cmd),
304            SubscribeCommand::Trades(cmd) => self.subscribe_trades(&cmd),
305            SubscribeCommand::MarkPrices(cmd) => self.subscribe_mark_prices(&cmd),
306            SubscribeCommand::IndexPrices(cmd) => self.subscribe_index_prices(&cmd),
307            SubscribeCommand::FundingRates(cmd) => self.subscribe_funding_rates(&cmd),
308            SubscribeCommand::Bars(cmd) => self.subscribe_bars(&cmd),
309            SubscribeCommand::InstrumentStatus(cmd) => self.subscribe_instrument_status(&cmd),
310            SubscribeCommand::InstrumentClose(cmd) => self.subscribe_instrument_close(&cmd),
311            SubscribeCommand::OptionGreeks(cmd) => self.subscribe_option_greeks(&cmd),
312            SubscribeCommand::OptionChain(_) => Ok(()), // Handled internally by engine
313        };
314
315        if let Err(e) = &result {
316            let subscription = pending
317                .iter()
318                .filter(|(_, member)| !self.has_active_subscription(member))
319                .map(|(key, _)| key.log_context())
320                .collect::<Vec<_>>()
321                .join(", ");
322
323            log_command_error(
324                &format_args!(
325                    "Subscribe {} unaccepted [{subscription}] command {command_id}",
326                    self.client_id()
327                ),
328                e,
329            );
330        }
331
332        self.retain_subscriptions(pending, &result, retain_on_failure);
333    }
334
335    fn retain_subscriptions(
336        &mut self,
337        pending: Vec<(SubscriptionKey, SubscribeCommand)>,
338        result: &anyhow::Result<()>,
339        retain_on_failure: bool,
340    ) {
341        for (key, member) in pending {
342            let accepted = result.is_ok() || self.has_active_subscription(&member);
343            // Engine-owned intent survives failure until its matching release
344            if !accepted && !retain_on_failure {
345                continue;
346            }
347
348            if accepted && let Some(subscription) = self.subscriptions_active.get_mut(&key) {
349                subscription.command = member.clone();
350            }
351
352            self.subscriptions_active
353                .retain(key, member.command_id(), member);
354        }
355    }
356
357    #[must_use]
358    #[rustfmt::skip]
359    pub(crate) fn has_active_subscription(&self, cmd: &SubscribeCommand) -> bool {
360        match cmd {
361            SubscribeCommand::Data(cmd) => self.subscriptions_custom.contains(&cmd.data_type),
362            SubscribeCommand::Instrument(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_instrument.contains(id)),
363            SubscribeCommand::Instruments(cmd) => self.subscriptions_instrument_venue.contains(&cmd.venue),
364            SubscribeCommand::BookDeltas(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_book_deltas.contains(id)),
365            SubscribeCommand::BookDepth(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_book_depth.contains(id)),
366            SubscribeCommand::Quotes(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_quotes.contains(id)),
367            SubscribeCommand::Trades(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_trades.contains(id)),
368            SubscribeCommand::Bars(cmd) => !cmd.bar_types.is_empty() && cmd.bar_types.iter().all(|bar_type| self.subscriptions_bars.contains(bar_type)),
369            SubscribeCommand::MarkPrices(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_mark_prices.contains(id)),
370            SubscribeCommand::IndexPrices(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_index_prices.contains(id)),
371            SubscribeCommand::FundingRates(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_funding_rates.contains(id)),
372            SubscribeCommand::InstrumentStatus(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_instrument_status.contains(id)),
373            SubscribeCommand::InstrumentClose(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_instrument_close.contains(id)),
374            SubscribeCommand::OptionGreeks(cmd) => !cmd.instrument_ids.is_empty() && cmd.instrument_ids.iter().all(|id| self.subscriptions_option_greeks.contains(id)),
375            SubscribeCommand::BookSnapshots(_) | SubscribeCommand::OptionChain(_) => self.subscriptions_active.contains(&SubscriptionKey::from_subscribe(cmd)),
376        }
377    }
378
379    pub(crate) fn clear_subscription_state(&mut self) {
380        self.subscriptions_custom.clear();
381        self.subscriptions_book_deltas.clear();
382        self.subscriptions_book_depth.clear();
383        self.subscriptions_quotes.clear();
384        self.subscriptions_trades.clear();
385        self.subscriptions_bars.clear();
386        self.subscriptions_instrument_status.clear();
387        self.subscriptions_instrument_close.clear();
388        self.subscriptions_instrument.clear();
389        self.subscriptions_instrument_venue.clear();
390        self.subscriptions_mark_prices.clear();
391        self.subscriptions_index_prices.clear();
392        self.subscriptions_funding_rates.clear();
393        self.subscriptions_option_greeks.clear();
394        self.subscriptions_active.clear();
395
396        #[cfg(feature = "defi")]
397        {
398            self.subscriptions_active_defi.clear();
399            self.subscriptions_blocks.clear();
400            self.subscriptions_pools.clear();
401            self.subscriptions_pool_swaps.clear();
402            self.subscriptions_pool_liquidity_updates.clear();
403            self.subscriptions_pool_fee_collects.clear();
404            self.subscriptions_pool_flash.clear();
405        }
406    }
407
408    #[inline]
409    pub fn execute_unsubscribe(&mut self, cmd: &UnsubscribeCommand) {
410        if let Some(bar_types) = cmd.bar_types()
411            && let Err(e) = ensure_routable_bar_types(bar_types, cmd.client_id().is_some())
412        {
413            log_command_error(
414                &format_args!(
415                    "Unsubscribe {} command {}",
416                    self.client_id(),
417                    cmd.command_id()
418                ),
419                &e,
420            );
421            return;
422        }
423
424        if let Some(ids) = cmd.instrument_ids()
425            && let Err(e) = ensure_routable_instrument_ids(ids, cmd.client_id().is_some())
426        {
427            log_command_error(
428                &format_args!(
429                    "Unsubscribe {} command {}",
430                    self.client_id(),
431                    cmd.command_id()
432                ),
433                &e,
434            );
435            return;
436        }
437
438        let mut releases = IndexMap::new();
439
440        for member in split_unsubscribe(cmd) {
441            let key = SubscriptionKey::from_unsubscribe(&member);
442
443            let (acquisition_id, command) = match self.subscriptions_active.release(&key) {
444                SubscriptionRelease::Retained => continue,
445                SubscriptionRelease::Final(subscribe) => (
446                    subscribe.command_id(),
447                    subscribe.into_unsubscribe(
448                        cmd.command_id(),
449                        cmd.ts_init(),
450                        cmd.correlation_id(),
451                    ),
452                ),
453                SubscriptionRelease::Untracked => (cmd.command_id(), member),
454            };
455
456            releases
457                .entry(acquisition_id)
458                .or_insert_with(Vec::new)
459                .push(command);
460        }
461
462        for command in releases.into_values().flat_map(UnsubscribeCommand::group) {
463            self.release_subscription(&command);
464        }
465    }
466
467    fn release_subscription(&mut self, command: &UnsubscribeCommand) {
468        let result = match command {
469            UnsubscribeCommand::Data(cmd) => self.unsubscribe(cmd),
470            UnsubscribeCommand::Instrument(cmd) => self.unsubscribe_instrument(cmd),
471            UnsubscribeCommand::Instruments(cmd) => self.unsubscribe_instruments(cmd),
472            UnsubscribeCommand::BookDeltas(cmd) => self.unsubscribe_book_deltas(cmd),
473            UnsubscribeCommand::BookDepth(cmd) => self.unsubscribe_book_depth(cmd),
474            UnsubscribeCommand::BookSnapshots(_) => Ok(()), // Handled internally by engine
475            UnsubscribeCommand::Quotes(cmd) => self.unsubscribe_quotes(cmd),
476            UnsubscribeCommand::Trades(cmd) => self.unsubscribe_trades(cmd),
477            UnsubscribeCommand::Bars(cmd) => self.unsubscribe_bars(cmd),
478            UnsubscribeCommand::MarkPrices(cmd) => self.unsubscribe_mark_prices(cmd),
479            UnsubscribeCommand::IndexPrices(cmd) => self.unsubscribe_index_prices(cmd),
480            UnsubscribeCommand::FundingRates(cmd) => self.unsubscribe_funding_rates(cmd),
481            UnsubscribeCommand::InstrumentStatus(cmd) => self.unsubscribe_instrument_status(cmd),
482            UnsubscribeCommand::InstrumentClose(cmd) => self.unsubscribe_instrument_close(cmd),
483            UnsubscribeCommand::OptionGreeks(cmd) => self.unsubscribe_option_greeks(cmd),
484            UnsubscribeCommand::OptionChain(_) => Ok(()), // Handled internally by engine
485        };
486
487        let members = split_unsubscribe(command);
488        for member in &members {
489            let key = SubscriptionKey::from_unsubscribe(member);
490            let subscribe = self
491                .subscriptions_active
492                .get_mut(&key)
493                .map(|entry| entry.command.clone());
494
495            if subscribe.is_none_or(|subscribe| !self.has_active_subscription(&subscribe)) {
496                self.subscriptions_active.remove(&key);
497            }
498        }
499
500        if let Err(e) = result {
501            let subscription = members
502                .iter()
503                .map(SubscriptionKey::from_unsubscribe)
504                .filter(|key| self.subscriptions_active.contains(key))
505                .map(|key| key.log_context())
506                .collect::<Vec<_>>()
507                .join(", ");
508
509            log_command_error(
510                &format_args!(
511                    "Unsubscribe {} unreleased [{subscription}] command {}",
512                    self.client_id(),
513                    command.command_id()
514                ),
515                &e,
516            );
517        }
518    }
519
520    /// Subscribes to a custom data type, updating internal state and forwarding to the client.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if the underlying client subscribe operation fails.
525    pub fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
526        let data_type = cmd.data_type.clone();
527        Self::execute_tracked_subscribe(
528            self.client.as_mut(),
529            &mut self.subscriptions_custom,
530            data_type,
531            "",
532            |client| client.subscribe(cmd),
533        )
534    }
535
536    /// Unsubscribes from a custom data type, updating internal state and forwarding to the client.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if the underlying client unsubscribe operation fails.
541    pub fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
542        Self::execute_tracked_unsubscribe(
543            self.client.as_mut(),
544            &mut self.subscriptions_custom,
545            &cmd.data_type,
546            "",
547            |client| client.unsubscribe(cmd),
548        )
549    }
550
551    /// Subscribes to instrument definitions for a venue, updating internal state and forwarding to the client.
552    ///
553    /// # Errors
554    ///
555    /// Returns an error if the underlying client subscribe operation fails.
556    fn subscribe_instruments(&mut self, cmd: SubscribeInstruments) -> anyhow::Result<()> {
557        Self::execute_tracked_subscribe(
558            self.client.as_mut(),
559            &mut self.subscriptions_instrument_venue,
560            cmd.venue,
561            "instruments",
562            |client| client.subscribe_instruments(cmd),
563        )
564    }
565
566    /// Unsubscribes from instrument definition updates for a venue, updating internal state and forwarding to the client.
567    ///
568    /// # Errors
569    ///
570    /// Returns an error if the underlying client unsubscribe operation fails.
571    fn unsubscribe_instruments(&mut self, cmd: &UnsubscribeInstruments) -> anyhow::Result<()> {
572        Self::execute_tracked_unsubscribe(
573            self.client.as_mut(),
574            &mut self.subscriptions_instrument_venue,
575            &cmd.venue,
576            "instruments",
577            |client| client.unsubscribe_instruments(cmd),
578        )
579    }
580
581    /// Subscribes to instrument definitions for a single instrument, updating internal state and forwarding to the client.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if the underlying client subscribe operation fails.
586    fn subscribe_instrument(&mut self, cmd: &SubscribeInstrument) -> anyhow::Result<()> {
587        Self::subscribe_batch(
588            &mut self.subscriptions_instrument,
589            &cmd.instrument_ids,
590            "instrument",
591            |partial| &partial.instrument_ids_accepted,
592            |ids| {
593                self.client
594                    .subscribe_instrument(cmd.with_instrument_ids(ids))
595            },
596        )
597    }
598
599    /// Unsubscribes from instrument definition updates for a single instrument, updating internal state and forwarding to the client.
600    ///
601    /// # Errors
602    ///
603    /// Returns an error if the underlying client unsubscribe operation fails.
604    fn unsubscribe_instrument(&mut self, cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
605        Self::unsubscribe_batch(
606            &mut self.subscriptions_instrument,
607            &cmd.instrument_ids,
608            "instrument",
609            |partial| &partial.instrument_ids_accepted,
610            |ids| {
611                self.client
612                    .unsubscribe_instrument(&cmd.with_instrument_ids(ids))
613            },
614        )
615    }
616
617    /// Subscribes to book deltas updates for an instrument, updating internal state and forwarding to the client.
618    ///
619    /// # Errors
620    ///
621    /// Returns an error if the underlying client subscribe operation fails.
622    fn subscribe_book_deltas(&mut self, cmd: &SubscribeBookDeltas) -> anyhow::Result<()> {
623        Self::subscribe_batch(
624            &mut self.subscriptions_book_deltas,
625            &cmd.instrument_ids,
626            "order book deltas",
627            |partial| &partial.instrument_ids_accepted,
628            |ids| {
629                self.client
630                    .subscribe_book_deltas(cmd.with_instrument_ids(ids))
631            },
632        )
633    }
634
635    /// Unsubscribes from book deltas for an instrument, updating internal state and forwarding to the client.
636    ///
637    /// # Errors
638    ///
639    /// Returns an error if the underlying client unsubscribe operation fails.
640    fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
641        Self::unsubscribe_batch(
642            &mut self.subscriptions_book_deltas,
643            &cmd.instrument_ids,
644            "order book deltas",
645            |partial| &partial.instrument_ids_accepted,
646            |ids| {
647                self.client
648                    .unsubscribe_book_deltas(&cmd.with_instrument_ids(ids))
649            },
650        )
651    }
652
653    /// Subscribes to book depth updates for an instrument, updating internal state and forwarding to the client.
654    ///
655    /// # Errors
656    ///
657    /// Returns an error if the underlying client subscribe operation fails.
658    fn subscribe_book_depth(&mut self, cmd: &SubscribeBookDepth) -> anyhow::Result<()> {
659        Self::subscribe_batch(
660            &mut self.subscriptions_book_depth,
661            &cmd.instrument_ids,
662            "order book depth",
663            |partial| &partial.instrument_ids_accepted,
664            |ids| {
665                self.client
666                    .subscribe_book_depth(cmd.with_instrument_ids(ids))
667            },
668        )
669    }
670
671    /// Unsubscribes from book depth updates for an instrument, updating internal state and forwarding to the client.
672    ///
673    /// # Errors
674    ///
675    /// Returns an error if the underlying client unsubscribe operation fails.
676    fn unsubscribe_book_depth(&mut self, cmd: &UnsubscribeBookDepth) -> anyhow::Result<()> {
677        Self::unsubscribe_batch(
678            &mut self.subscriptions_book_depth,
679            &cmd.instrument_ids,
680            "order book depth",
681            |partial| &partial.instrument_ids_accepted,
682            |ids| {
683                self.client
684                    .unsubscribe_book_depth(&cmd.with_instrument_ids(ids))
685            },
686        )
687    }
688
689    /// Subscribes to quotes for instruments, updating internal state and forwarding to the client.
690    ///
691    /// # Errors
692    ///
693    /// Returns an error if the underlying client subscribe operation fails.
694    fn subscribe_quotes(&mut self, cmd: &SubscribeQuotes) -> anyhow::Result<()> {
695        Self::subscribe_batch(
696            &mut self.subscriptions_quotes,
697            &cmd.instrument_ids,
698            "quotes",
699            |partial| &partial.instrument_ids_accepted,
700            |ids| self.client.subscribe_quotes(cmd.with_instrument_ids(ids)),
701        )
702    }
703
704    /// Unsubscribes from quotes for instruments, updating internal state and forwarding to the client.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if the underlying client unsubscribe operation fails.
709    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
710        Self::unsubscribe_batch(
711            &mut self.subscriptions_quotes,
712            &cmd.instrument_ids,
713            "quotes",
714            |partial| &partial.instrument_ids_accepted,
715            |ids| {
716                self.client
717                    .unsubscribe_quotes(&cmd.with_instrument_ids(ids))
718            },
719        )
720    }
721
722    /// Subscribes to trades for instruments, updating internal state and forwarding to the client.
723    ///
724    /// # Errors
725    ///
726    /// Returns an error if the underlying client subscribe operation fails.
727    fn subscribe_trades(&mut self, cmd: &SubscribeTrades) -> anyhow::Result<()> {
728        Self::subscribe_batch(
729            &mut self.subscriptions_trades,
730            &cmd.instrument_ids,
731            "trades",
732            |partial| &partial.instrument_ids_accepted,
733            |ids| self.client.subscribe_trades(cmd.with_instrument_ids(ids)),
734        )
735    }
736
737    /// Unsubscribes from trades for instruments, updating internal state and forwarding to the client.
738    ///
739    /// # Errors
740    ///
741    /// Returns an error if the underlying client unsubscribe operation fails.
742    fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
743        Self::unsubscribe_batch(
744            &mut self.subscriptions_trades,
745            &cmd.instrument_ids,
746            "trades",
747            |partial| &partial.instrument_ids_accepted,
748            |ids| {
749                self.client
750                    .unsubscribe_trades(&cmd.with_instrument_ids(ids))
751            },
752        )
753    }
754
755    fn subscribe_batch<T: Copy + Eq + Hash + Display>(
756        subscriptions: &mut AHashSet<T>,
757        members: &[T],
758        label: &str,
759        accepted: fn(&SubscriptionError) -> &[T],
760        dispatch: impl FnOnce(&[T]) -> anyhow::Result<()>,
761    ) -> anyhow::Result<()> {
762        let ids: Vec<_> = members
763            .iter()
764            .copied()
765            .filter(|id| !subscriptions.contains(id))
766            .collect();
767
768        if ids.is_empty() {
769            return Ok(());
770        }
771
772        let result = dispatch(&ids);
773
774        let reported: AHashSet<_> = match &result {
775            Ok(()) => AHashSet::new(),
776            Err(e) => e
777                .downcast_ref::<SubscriptionError>()
778                .map_or(&[][..], accepted)
779                .iter()
780                .copied()
781                .collect(),
782        };
783
784        for id in ids
785            .iter()
786            .filter(|id| result.is_ok() || reported.contains(*id))
787        {
788            subscriptions.insert(*id);
789            log_info!("Subscribed {id} {label}", color = LogColor::Blue);
790        }
791
792        result
793    }
794
795    fn unsubscribe_batch<T: Copy + Eq + Hash + Display>(
796        subscriptions: &mut AHashSet<T>,
797        members: &[T],
798        label: &str,
799        accepted: fn(&SubscriptionError) -> &[T],
800        dispatch: impl FnOnce(&[T]) -> anyhow::Result<()>,
801    ) -> anyhow::Result<()> {
802        let ids: Vec<_> = members
803            .iter()
804            .copied()
805            .filter(|id| subscriptions.contains(id))
806            .collect();
807
808        if ids.is_empty() {
809            return Ok(());
810        }
811
812        let result = dispatch(&ids);
813
814        let reported: AHashSet<_> = match &result {
815            Ok(()) => AHashSet::new(),
816            Err(e) => e
817                .downcast_ref::<SubscriptionError>()
818                .map_or(&[][..], accepted)
819                .iter()
820                .copied()
821                .collect(),
822        };
823
824        for id in ids
825            .iter()
826            .filter(|id| result.is_ok() || reported.contains(*id))
827        {
828            subscriptions.remove(id);
829            log_info!("Unsubscribed {id} {label}", color = LogColor::Blue);
830        }
831
832        result
833    }
834
835    /// Subscribes to bars for a bar type, updating internal state and forwarding to the client.
836    ///
837    /// # Errors
838    ///
839    /// Returns an error if the underlying client subscribe operation fails.
840    fn subscribe_bars(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
841        Self::subscribe_batch(
842            &mut self.subscriptions_bars,
843            &cmd.bar_types,
844            "bars",
845            |partial| &partial.bar_types_accepted,
846            |ids| self.client.subscribe_bars(cmd.with_bar_types(ids)),
847        )
848    }
849
850    /// Unsubscribes from bars for a bar type, updating internal state and forwarding to the client.
851    ///
852    /// # Errors
853    ///
854    /// Returns an error if the underlying client unsubscribe operation fails.
855    fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
856        Self::unsubscribe_batch(
857            &mut self.subscriptions_bars,
858            &cmd.bar_types,
859            "bars",
860            |partial| &partial.bar_types_accepted,
861            |ids| self.client.unsubscribe_bars(&cmd.with_bar_types(ids)),
862        )
863    }
864
865    /// Subscribes to mark price updates for an instrument, updating internal state and forwarding to the client.
866    ///
867    /// # Errors
868    ///
869    /// Returns an error if the underlying client subscribe operation fails.
870    fn subscribe_mark_prices(&mut self, cmd: &SubscribeMarkPrices) -> anyhow::Result<()> {
871        Self::subscribe_batch(
872            &mut self.subscriptions_mark_prices,
873            &cmd.instrument_ids,
874            "mark prices",
875            |partial| &partial.instrument_ids_accepted,
876            |ids| {
877                self.client
878                    .subscribe_mark_prices(cmd.with_instrument_ids(ids))
879            },
880        )
881    }
882
883    /// Unsubscribes from mark price updates for an instrument, updating internal state and forwarding to the client.
884    ///
885    /// # Errors
886    ///
887    /// Returns an error if the underlying client unsubscribe operation fails.
888    fn unsubscribe_mark_prices(&mut self, cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
889        Self::unsubscribe_batch(
890            &mut self.subscriptions_mark_prices,
891            &cmd.instrument_ids,
892            "mark prices",
893            |partial| &partial.instrument_ids_accepted,
894            |ids| {
895                self.client
896                    .unsubscribe_mark_prices(&cmd.with_instrument_ids(ids))
897            },
898        )
899    }
900
901    /// Subscribes to index price updates for an instrument, updating internal state and forwarding to the client.
902    ///
903    /// # Errors
904    ///
905    /// Returns an error if the underlying client subscribe operation fails.
906    fn subscribe_index_prices(&mut self, cmd: &SubscribeIndexPrices) -> anyhow::Result<()> {
907        Self::subscribe_batch(
908            &mut self.subscriptions_index_prices,
909            &cmd.instrument_ids,
910            "index prices",
911            |partial| &partial.instrument_ids_accepted,
912            |ids| {
913                self.client
914                    .subscribe_index_prices(cmd.with_instrument_ids(ids))
915            },
916        )
917    }
918
919    /// Unsubscribes from index price updates for an instrument, updating internal state and forwarding to the client.
920    ///
921    /// # Errors
922    ///
923    /// Returns an error if the underlying client unsubscribe operation fails.
924    fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
925        Self::unsubscribe_batch(
926            &mut self.subscriptions_index_prices,
927            &cmd.instrument_ids,
928            "index prices",
929            |partial| &partial.instrument_ids_accepted,
930            |ids| {
931                self.client
932                    .unsubscribe_index_prices(&cmd.with_instrument_ids(ids))
933            },
934        )
935    }
936
937    /// Subscribes to funding rate updates for an instrument, updating internal state and forwarding to the client.
938    ///
939    /// # Errors
940    ///
941    /// Returns an error if the underlying client subscribe operation fails.
942    fn subscribe_funding_rates(&mut self, cmd: &SubscribeFundingRates) -> anyhow::Result<()> {
943        Self::subscribe_batch(
944            &mut self.subscriptions_funding_rates,
945            &cmd.instrument_ids,
946            "funding rates",
947            |partial| &partial.instrument_ids_accepted,
948            |ids| {
949                self.client
950                    .subscribe_funding_rates(cmd.with_instrument_ids(ids))
951            },
952        )
953    }
954
955    /// Unsubscribes from funding rate updates for an instrument, updating internal state and forwarding to the client.
956    ///
957    /// # Errors
958    ///
959    /// Returns an error if the underlying client unsubscribe operation fails.
960    fn unsubscribe_funding_rates(&mut self, cmd: &UnsubscribeFundingRates) -> anyhow::Result<()> {
961        Self::unsubscribe_batch(
962            &mut self.subscriptions_funding_rates,
963            &cmd.instrument_ids,
964            "funding rates",
965            |partial| &partial.instrument_ids_accepted,
966            |ids| {
967                self.client
968                    .unsubscribe_funding_rates(&cmd.with_instrument_ids(ids))
969            },
970        )
971    }
972
973    /// Subscribes to instrument status updates for the specified instrument.
974    ///
975    /// # Errors
976    ///
977    /// Returns an error if the underlying client subscribe operation fails.
978    fn subscribe_instrument_status(
979        &mut self,
980        cmd: &SubscribeInstrumentStatus,
981    ) -> anyhow::Result<()> {
982        Self::subscribe_batch(
983            &mut self.subscriptions_instrument_status,
984            &cmd.instrument_ids,
985            "instrument status",
986            |partial| &partial.instrument_ids_accepted,
987            |ids| {
988                self.client
989                    .subscribe_instrument_status(cmd.with_instrument_ids(ids))
990            },
991        )
992    }
993
994    /// Unsubscribes from instrument status updates for the specified instrument.
995    ///
996    /// # Errors
997    ///
998    /// Returns an error if the underlying client unsubscribe operation fails.
999    fn unsubscribe_instrument_status(
1000        &mut self,
1001        cmd: &UnsubscribeInstrumentStatus,
1002    ) -> anyhow::Result<()> {
1003        Self::unsubscribe_batch(
1004            &mut self.subscriptions_instrument_status,
1005            &cmd.instrument_ids,
1006            "instrument status",
1007            |partial| &partial.instrument_ids_accepted,
1008            |ids| {
1009                self.client
1010                    .unsubscribe_instrument_status(&cmd.with_instrument_ids(ids))
1011            },
1012        )
1013    }
1014
1015    /// Subscribes to instrument close events for the specified instrument.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns an error if the underlying client subscribe operation fails.
1020    fn subscribe_instrument_close(&mut self, cmd: &SubscribeInstrumentClose) -> anyhow::Result<()> {
1021        Self::subscribe_batch(
1022            &mut self.subscriptions_instrument_close,
1023            &cmd.instrument_ids,
1024            "instrument close",
1025            |partial| &partial.instrument_ids_accepted,
1026            |ids| {
1027                self.client
1028                    .subscribe_instrument_close(cmd.with_instrument_ids(ids))
1029            },
1030        )
1031    }
1032
1033    /// Unsubscribes from instrument close events for the specified instrument.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if the underlying client unsubscribe operation fails.
1038    fn unsubscribe_instrument_close(
1039        &mut self,
1040        cmd: &UnsubscribeInstrumentClose,
1041    ) -> anyhow::Result<()> {
1042        Self::unsubscribe_batch(
1043            &mut self.subscriptions_instrument_close,
1044            &cmd.instrument_ids,
1045            "instrument close",
1046            |partial| &partial.instrument_ids_accepted,
1047            |ids| {
1048                self.client
1049                    .unsubscribe_instrument_close(&cmd.with_instrument_ids(ids))
1050            },
1051        )
1052    }
1053
1054    /// Subscribes to option greeks for an instrument, updating internal state and forwarding to the client.
1055    ///
1056    /// # Errors
1057    ///
1058    /// Returns an error if the underlying client subscribe operation fails.
1059    fn subscribe_option_greeks(&mut self, cmd: &SubscribeOptionGreeks) -> anyhow::Result<()> {
1060        Self::subscribe_batch(
1061            &mut self.subscriptions_option_greeks,
1062            &cmd.instrument_ids,
1063            "option greeks",
1064            |partial| &partial.instrument_ids_accepted,
1065            |ids| {
1066                self.client
1067                    .subscribe_option_greeks(cmd.with_instrument_ids(ids))
1068            },
1069        )
1070    }
1071
1072    /// Unsubscribes from option greeks for an instrument, updating internal state and forwarding to the client.
1073    ///
1074    /// # Errors
1075    ///
1076    /// Returns an error if the underlying client unsubscribe operation fails.
1077    fn unsubscribe_option_greeks(&mut self, cmd: &UnsubscribeOptionGreeks) -> anyhow::Result<()> {
1078        Self::unsubscribe_batch(
1079            &mut self.subscriptions_option_greeks,
1080            &cmd.instrument_ids,
1081            "option greeks",
1082            |partial| &partial.instrument_ids_accepted,
1083            |ids| {
1084                self.client
1085                    .unsubscribe_option_greeks(&cmd.with_instrument_ids(ids))
1086            },
1087        )
1088    }
1089
1090    pub(crate) fn execute_tracked_subscribe<T>(
1091        client: &mut dyn DataClient,
1092        set: &mut AHashSet<T>,
1093        key: T,
1094        data_type: &str,
1095        subscribe: impl FnOnce(&mut dyn DataClient) -> anyhow::Result<()>,
1096    ) -> anyhow::Result<()>
1097    where
1098        T: Eq + Hash + Display,
1099    {
1100        if set.contains(&key) {
1101            return Ok(());
1102        }
1103
1104        subscribe(client)?;
1105
1106        if data_type.is_empty() {
1107            log_info!("Subscribed {key}", color = LogColor::Blue);
1108        } else {
1109            log_info!("Subscribed {key} {data_type}", color = LogColor::Blue);
1110        }
1111        set.insert(key);
1112        Ok(())
1113    }
1114
1115    pub(crate) fn execute_tracked_unsubscribe<T>(
1116        client: &mut dyn DataClient,
1117        set: &mut AHashSet<T>,
1118        key: &T,
1119        data_type: &str,
1120        unsubscribe: impl FnOnce(&mut dyn DataClient) -> anyhow::Result<()>,
1121    ) -> anyhow::Result<()>
1122    where
1123        T: Eq + Hash + Display,
1124    {
1125        if !set.contains(key) {
1126            return Ok(());
1127        }
1128
1129        unsubscribe(client)?;
1130        set.remove(key);
1131        if data_type.is_empty() {
1132            log_info!("Unsubscribed {key}", color = LogColor::Blue);
1133        } else {
1134            log_info!("Unsubscribed {key} {data_type}", color = LogColor::Blue);
1135        }
1136        Ok(())
1137    }
1138
1139    /// Sends a data request to the underlying client.
1140    ///
1141    /// # Errors
1142    ///
1143    /// Returns an error if the client request fails.
1144    pub fn request_data(&self, req: RequestCustomData) -> anyhow::Result<()> {
1145        self.client.request_data(req)
1146    }
1147
1148    /// Sends a single instrument request to the client.
1149    ///
1150    /// # Errors
1151    ///
1152    /// Returns an error if the client fails to process the request.
1153    pub fn request_instrument(&self, req: RequestInstrument) -> anyhow::Result<()> {
1154        self.client.request_instrument(req)
1155    }
1156
1157    /// Sends a batch instruments request to the client.
1158    ///
1159    /// # Errors
1160    ///
1161    /// Returns an error if the client fails to process the request.
1162    pub fn request_instruments(&self, req: RequestInstruments) -> anyhow::Result<()> {
1163        self.client.request_instruments(req)
1164    }
1165
1166    /// Sends a book snapshot request for a given instrument.
1167    ///
1168    /// # Errors
1169    ///
1170    /// Returns an error if the client fails to process the book snapshot request.
1171    pub fn request_book_snapshot(&self, req: RequestBookSnapshot) -> anyhow::Result<()> {
1172        self.client.request_book_snapshot(req)
1173    }
1174
1175    /// Sends a quotes request for a given instrument.
1176    ///
1177    /// # Errors
1178    ///
1179    /// Returns an error if the client fails to process the quotes request.
1180    pub fn request_quotes(&self, req: RequestQuotes) -> anyhow::Result<()> {
1181        self.client.request_quotes(req)
1182    }
1183
1184    /// Sends a trades request for a given instrument.
1185    ///
1186    /// # Errors
1187    ///
1188    /// Returns an error if the client fails to process the trades request.
1189    pub fn request_trades(&self, req: RequestTrades) -> anyhow::Result<()> {
1190        self.client.request_trades(req)
1191    }
1192
1193    /// Sends a funding rates request for a given instrument.
1194    ///
1195    /// # Errors
1196    ///
1197    /// Returns an error if the client fails to process the trades request.
1198    pub fn request_funding_rates(&self, req: RequestFundingRates) -> anyhow::Result<()> {
1199        self.client.request_funding_rates(req)
1200    }
1201
1202    /// Sends an option-chain reference price request.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns an error if the client fails to process the option-chain reference price request.
1207    pub fn request_option_chain_reference_price(
1208        &self,
1209        req: RequestOptionChainReferencePrice,
1210    ) -> anyhow::Result<()> {
1211        self.client.request_option_chain_reference_price(req)
1212    }
1213
1214    /// Sends a bars request for a given instrument and bar type.
1215    ///
1216    /// # Errors
1217    ///
1218    /// Returns an error if the client fails to process the bars request.
1219    pub fn request_bars(&self, req: RequestBars) -> anyhow::Result<()> {
1220        self.client.request_bars(req)
1221    }
1222
1223    /// Sends an order book depths request for a given instrument.
1224    ///
1225    /// # Errors
1226    ///
1227    /// Returns an error if the client fails to process the order book depths request.
1228    pub fn request_book_depth(&self, req: RequestBookDepth) -> anyhow::Result<()> {
1229        self.client.request_book_depth(req)
1230    }
1231}