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