Skip to main content

nautilus_deribit/
data.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//! Live market data client implementation for the Deribit adapter.
17
18use std::sync::{
19    Arc,
20    atomic::{AtomicBool, Ordering},
21};
22
23use ahash::{AHashMap, AHashSet};
24use anyhow::Context;
25use async_trait::async_trait;
26use futures_util::StreamExt;
27use nautilus_common::{
28    clients::DataClient,
29    live::{runner::get_data_event_sender, runtime::get_runtime},
30    log_debug, log_info,
31    messages::{
32        DataEvent, DataResponse,
33        data::{
34            BarsResponse, BookResponse, ForwardPricesResponse, InstrumentResponse,
35            InstrumentsResponse, RequestBars, RequestBookSnapshot, RequestForwardPrices,
36            RequestInstrument, RequestInstruments, RequestTrades, SubscribeBars,
37            SubscribeBookDeltas, SubscribeBookDepth10, SubscribeCustomData, SubscribeFundingRates,
38            SubscribeIndexPrices, SubscribeInstrument, SubscribeInstrumentStatus,
39            SubscribeInstruments, SubscribeMarkPrices, SubscribeOptionGreeks, SubscribeQuotes,
40            SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
41            UnsubscribeBookDepth10, UnsubscribeCustomData, UnsubscribeFundingRates,
42            UnsubscribeIndexPrices, UnsubscribeInstrument, UnsubscribeInstrumentStatus,
43            UnsubscribeInstruments, UnsubscribeMarkPrices, UnsubscribeOptionGreeks,
44            UnsubscribeQuotes, UnsubscribeTrades,
45        },
46    },
47};
48use nautilus_core::{
49    AtomicMap, AtomicSet, Params,
50    datetime::datetime_to_unix_nanos,
51    time::{AtomicTime, get_atomic_clock_realtime},
52};
53use nautilus_model::{
54    data::{Data, ForwardPrice, OrderBookDeltas_API},
55    enums::BookType,
56    identifiers::{ClientId, InstrumentId, Symbol, Venue},
57    instruments::{Instrument, InstrumentAny},
58};
59use tokio::task::JoinHandle;
60use tokio_util::sync::CancellationToken;
61
62use crate::{
63    common::{
64        consts::{
65            DERIBIT_BOOK_DEFAULT_DEPTH, DERIBIT_BOOK_DEFAULT_GROUP, DERIBIT_BOOK_VALID_DEPTHS,
66            DERIBIT_VENUE,
67        },
68        parse::{bar_spec_to_resolution, parse_instrument_kind_currency},
69    },
70    config::DeribitDataClientConfig,
71    data_types::register_deribit_custom_data,
72    http::{
73        client::DeribitHttpClient,
74        models::{DeribitCurrency, DeribitProductType},
75    },
76    websocket::{
77        auth::DERIBIT_DATA_SESSION_NAME, client::DeribitWebSocketClient,
78        enums::DeribitUpdateInterval, messages::NautilusWsMessage,
79    },
80};
81
82/// Deribit live data client.
83#[derive(Debug)]
84pub struct DeribitDataClient {
85    client_id: ClientId,
86    config: DeribitDataClientConfig,
87    http_client: DeribitHttpClient,
88    ws_client: Option<DeribitWebSocketClient>,
89    is_connected: AtomicBool,
90    cancellation_token: CancellationToken,
91    tasks: Vec<JoinHandle<()>>,
92    data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
93    instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
94    mark_price_subs: Arc<AtomicSet<InstrumentId>>,
95    index_price_subs: Arc<AtomicSet<InstrumentId>>,
96    option_greeks_subs: Arc<AtomicSet<InstrumentId>>,
97    combo_leg_trade_subs: Arc<AtomicMap<InstrumentId, AHashMap<InstrumentId, usize>>>,
98    clock: &'static AtomicTime,
99}
100
101impl DeribitDataClient {
102    /// Creates a new [`DeribitDataClient`] instance.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the client fails to initialize.
107    pub fn new(client_id: ClientId, config: DeribitDataClientConfig) -> anyhow::Result<Self> {
108        let clock = get_atomic_clock_realtime();
109        let data_sender = get_data_event_sender();
110
111        let http_client = if config.has_api_credentials() {
112            DeribitHttpClient::new_with_env(
113                config.api_key.clone(),
114                config.api_secret.clone(),
115                config.base_url_http.clone(),
116                config.environment,
117                config.http_timeout_secs,
118                config.max_retries,
119                config.retry_delay_initial_ms,
120                config.retry_delay_max_ms,
121                config.proxy_url.clone(),
122            )?
123        } else {
124            DeribitHttpClient::new(
125                config.base_url_http.clone(),
126                config.environment,
127                config.http_timeout_secs,
128                config.max_retries,
129                config.retry_delay_initial_ms,
130                config.retry_delay_max_ms,
131                config.proxy_url.clone(),
132            )?
133        };
134
135        let ws_client = DeribitWebSocketClient::new(
136            Some(config.ws_url()),
137            config.api_key.clone(),
138            config.api_secret.clone(),
139            config.heartbeat_interval_secs,
140            config.environment,
141            config.transport_backend,
142            config.proxy_url.clone(),
143        )?;
144
145        Ok(Self {
146            client_id,
147            config,
148            http_client,
149            ws_client: Some(ws_client),
150            is_connected: AtomicBool::new(false),
151            cancellation_token: CancellationToken::new(),
152            tasks: Vec::new(),
153            data_sender,
154            instruments: Arc::new(AtomicMap::new()),
155            mark_price_subs: Arc::new(AtomicSet::new()),
156            index_price_subs: Arc::new(AtomicSet::new()),
157            option_greeks_subs: Arc::new(AtomicSet::new()),
158            combo_leg_trade_subs: Arc::new(AtomicMap::new()),
159            clock,
160        })
161    }
162
163    /// Returns a mutable reference to the WebSocket client.
164    fn ws_client_mut(&mut self) -> anyhow::Result<&mut DeribitWebSocketClient> {
165        self.ws_client
166            .as_mut()
167            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))
168    }
169
170    /// Gets the interval from params, defaulting to Raw if authenticated.
171    ///
172    /// If authenticated, we prefer Raw interval for best data quality.
173    /// Users can still override via params if they want 100ms or agg2.
174    fn get_interval(&self, params: &Option<Params>) -> Option<DeribitUpdateInterval> {
175        if let Some(interval) = params
176            .as_ref()
177            .and_then(|p| p.get_str("interval"))
178            .and_then(|s| s.parse::<DeribitUpdateInterval>().ok())
179        {
180            return Some(interval);
181        }
182
183        // Default to Raw if authenticated, otherwise None (100ms default)
184        if let Some(ws) = self.ws_client.as_ref()
185            && ws.is_authenticated()
186        {
187            return Some(DeribitUpdateInterval::Raw);
188        }
189        None
190    }
191
192    /// Spawns a task to process WebSocket messages.
193    fn spawn_stream_task(
194        &mut self,
195        stream: impl futures_util::Stream<Item = NautilusWsMessage> + Send + 'static,
196    ) {
197        let data_sender = self.data_sender.clone();
198        let instruments = Arc::clone(&self.instruments);
199        let cancellation = self.cancellation_token.clone();
200
201        let handle = get_runtime().spawn(async move {
202            tokio::pin!(stream);
203
204            loop {
205                tokio::select! {
206                    maybe_msg = stream.next() => {
207                        match maybe_msg {
208                            Some(msg) => Self::handle_ws_message(msg, &data_sender, &instruments),
209                            None => {
210                                log::debug!("WebSocket stream ended");
211                                break;
212                            }
213                        }
214                    }
215                    () = cancellation.cancelled() => {
216                        log::debug!("WebSocket stream task cancelled");
217                        break;
218                    }
219                }
220            }
221        });
222
223        self.tasks.push(handle);
224    }
225
226    /// Handles incoming WebSocket messages.
227    fn handle_ws_message(
228        message: NautilusWsMessage,
229        sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
230        instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
231    ) {
232        match message {
233            NautilusWsMessage::Data(payloads) => {
234                for data in payloads {
235                    Self::send_data(sender, data);
236                }
237            }
238            NautilusWsMessage::Deltas(deltas) => {
239                Self::send_data(sender, Data::Deltas(OrderBookDeltas_API::new(deltas)));
240            }
241            NautilusWsMessage::Instrument(instrument) => {
242                let instrument_any = *instrument;
243                instruments.insert(instrument_any.id(), instrument_any.clone());
244
245                if let Err(e) = sender.send(DataEvent::Instrument(instrument_any)) {
246                    log::warn!("Failed to send instrument update: {e}");
247                }
248            }
249            NautilusWsMessage::OptionGreeks(greeks) => {
250                if let Err(e) = sender.send(DataEvent::OptionGreeks(greeks)) {
251                    log::error!("Failed to send option greeks: {e}");
252                }
253            }
254            NautilusWsMessage::Error(e) => {
255                log::warn!("WebSocket error: {e:?}");
256            }
257            NautilusWsMessage::Raw(value) => {
258                log::debug!("Unhandled raw message: {value}");
259            }
260            NautilusWsMessage::Reconnected => {
261                log::info!("WebSocket reconnected");
262            }
263            NautilusWsMessage::Authenticated(auth) => {
264                log::debug!("WebSocket authenticated: expires_in={}s", auth.expires_in);
265            }
266            NautilusWsMessage::FundingRates(funding_rates) => {
267                for funding_rate in funding_rates {
268                    if let Err(e) = sender.send(DataEvent::FundingRate(funding_rate)) {
269                        log::error!("Failed to send funding rate: {e}");
270                    }
271                }
272            }
273            NautilusWsMessage::InstrumentStatus(status) => {
274                if let Err(e) = sender.send(DataEvent::InstrumentStatus(status)) {
275                    log::error!("Failed to send instrument status event: {e}");
276                }
277            }
278            NautilusWsMessage::OrderStatusReports(reports) => {
279                log::warn!(
280                    "Data client received OrderStatusReports message (should be handled by execution client): {} reports",
281                    reports.len()
282                );
283            }
284            NautilusWsMessage::FillReports(reports) => {
285                log::warn!(
286                    "Data client received FillReports message (should be handled by execution client): {} reports",
287                    reports.len()
288                );
289            }
290            NautilusWsMessage::OrderRejected(order) => {
291                log::warn!(
292                    "Data client received OrderRejected message (should be handled by execution client): {order:?}"
293                );
294            }
295            NautilusWsMessage::OrderAccepted(order) => {
296                log::warn!(
297                    "Data client received OrderAccepted message (should be handled by execution client): {order:?}"
298                );
299            }
300            NautilusWsMessage::OrderCanceled(order) => {
301                log::warn!(
302                    "Data client received OrderCanceled message (should be handled by execution client): {order:?}"
303                );
304            }
305            NautilusWsMessage::OrderExpired(order) => {
306                log::warn!(
307                    "Data client received OrderExpired message (should be handled by execution client): {order:?}"
308                );
309            }
310            NautilusWsMessage::OrderUpdated(order) => {
311                log::warn!(
312                    "Data client received OrderUpdated message (should be handled by execution client): {order:?}"
313                );
314            }
315            NautilusWsMessage::OrderCancelRejected(order) => {
316                log::warn!(
317                    "Data client received OrderCancelRejected message (should be handled by execution client): {order:?}"
318                );
319            }
320            NautilusWsMessage::OrderModifyRejected(order) => {
321                log::warn!(
322                    "Data client received OrderModifyRejected message (should be handled by execution client): {order:?}"
323                );
324            }
325            NautilusWsMessage::AccountState(state) => {
326                log::warn!(
327                    "Data client received AccountState message (should be handled by execution client): {state:?}"
328                );
329            }
330            NautilusWsMessage::AuthenticationFailed(reason) => {
331                log::error!("Authentication failed in data client: {reason}");
332            }
333        }
334    }
335
336    /// Sends data to the data channel.
337    fn send_data(sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>, data: Data) {
338        if let Err(e) = sender.send(DataEvent::Data(data)) {
339            log::error!("Failed to send data: {e}");
340        }
341    }
342
343    // Returns whether a subscribe should lazy-load the instrument before sending,
344    // erroring up front when the instrument is missing and the flag is disabled
345    // (so the WebSocket handler does not silently drop later frames).
346    fn prepare_subscribe(&self, instrument_id: InstrumentId) -> anyhow::Result<bool> {
347        if self.instruments.contains_key(&instrument_id) {
348            return Ok(false);
349        }
350
351        if !self.config.auto_load_missing_instruments {
352            anyhow::bail!(
353                "Instrument {instrument_id} not found and `auto_load_missing_instruments` is disabled"
354            );
355        }
356        Ok(true)
357    }
358
359    // Fetches an instrument over HTTP and seeds the local, HTTP, and WebSocket caches.
360    async fn lazy_load_instrument(
361        http_client: &DeribitHttpClient,
362        ws: &DeribitWebSocketClient,
363        instruments: &AtomicMap<InstrumentId, InstrumentAny>,
364        instrument_id: InstrumentId,
365    ) -> anyhow::Result<()> {
366        let instrument = http_client
367            .request_instrument(instrument_id)
368            .await
369            .with_context(|| format!("failed to lazy-load instrument {instrument_id}"))?;
370        instruments.insert(instrument.id(), instrument.clone());
371        http_client.cache_instruments(std::slice::from_ref(&instrument));
372        ws.cache_instruments(std::slice::from_ref(&instrument));
373        Ok(())
374    }
375
376    fn subscribe_combo_legs(params: &Option<Params>) -> bool {
377        params
378            .as_ref()
379            .and_then(|params| params.get_bool("subscribe_combo_legs"))
380            .unwrap_or(false)
381    }
382
383    fn combo_leg_trade_ids(
384        instruments: &AtomicMap<InstrumentId, InstrumentAny>,
385        instrument_id: InstrumentId,
386    ) -> Vec<InstrumentId> {
387        let Some(instrument) = instruments.get_cloned(&instrument_id) else {
388            log::warn!("Cannot expand Deribit combo legs for missing instrument {instrument_id}");
389            return Vec::new();
390        };
391
392        let info = match instrument {
393            InstrumentAny::CryptoOptionSpread(spread) => spread.info,
394            InstrumentAny::CryptoFuturesSpread(spread) => spread.info,
395            _ => return Vec::new(),
396        };
397        let Some(info) = info else {
398            return Vec::new();
399        };
400        let Some(legs) = info
401            .get("deribit_combo_legs")
402            .and_then(serde_json::Value::as_array)
403        else {
404            return Vec::new();
405        };
406
407        let mut leg_ids = Vec::new();
408        let mut seen = AHashSet::new();
409
410        for leg in legs {
411            let Some(leg_id_str) = leg.get("instrument_id").and_then(serde_json::Value::as_str)
412            else {
413                continue;
414            };
415
416            match InstrumentId::from_as_ref(leg_id_str) {
417                Ok(leg_id) if leg_id != instrument_id && seen.insert(leg_id) => {
418                    leg_ids.push(leg_id);
419                }
420                Ok(_) => {}
421                Err(e) => {
422                    log::warn!(
423                        "Skipping invalid Deribit combo leg instrument ID {leg_id_str}: {e}"
424                    );
425                }
426            }
427        }
428
429        leg_ids
430    }
431
432    fn track_combo_leg_trade_subs(
433        subscriptions: &AtomicMap<InstrumentId, AHashMap<InstrumentId, usize>>,
434        instrument_id: InstrumentId,
435        leg_ids: &[InstrumentId],
436    ) {
437        if leg_ids.is_empty() {
438            return;
439        }
440
441        subscriptions.rcu(|subscriptions| {
442            let counts = subscriptions.entry(instrument_id).or_default();
443
444            for leg_id in leg_ids {
445                counts
446                    .entry(*leg_id)
447                    .and_modify(|count| *count += 1)
448                    .or_insert(1);
449            }
450        });
451    }
452
453    fn combo_leg_trade_unsubs(
454        subscriptions: &AtomicMap<InstrumentId, AHashMap<InstrumentId, usize>>,
455        instrument_id: InstrumentId,
456    ) -> Vec<InstrumentId> {
457        let mut leg_ids = Vec::new();
458
459        subscriptions.rcu(|subscriptions| {
460            let remove_instrument = if let Some(counts) = subscriptions.get_mut(&instrument_id) {
461                leg_ids = counts.keys().copied().collect();
462                counts.retain(|_, count| {
463                    if *count > 1 {
464                        *count -= 1;
465                        true
466                    } else {
467                        false
468                    }
469                });
470                counts.is_empty()
471            } else {
472                leg_ids = Vec::new();
473                false
474            };
475
476            if remove_instrument {
477                subscriptions.remove(&instrument_id);
478            }
479        });
480
481        leg_ids
482    }
483}
484
485#[async_trait(?Send)]
486impl DataClient for DeribitDataClient {
487    fn client_id(&self) -> ClientId {
488        self.client_id
489    }
490
491    fn venue(&self) -> Option<Venue> {
492        Some(*DERIBIT_VENUE)
493    }
494
495    fn start(&mut self) -> anyhow::Result<()> {
496        log::info!(
497            "Starting data client: client_id={}, environment={}",
498            self.client_id,
499            self.config.environment
500        );
501        Ok(())
502    }
503
504    fn stop(&mut self) -> anyhow::Result<()> {
505        log::info!("Stopping data client: {}", self.client_id);
506        self.cancellation_token.cancel();
507        self.is_connected.store(false, Ordering::Relaxed);
508        Ok(())
509    }
510
511    fn reset(&mut self) -> anyhow::Result<()> {
512        log::info!("Resetting data client: {}", self.client_id);
513        self.is_connected.store(false, Ordering::Relaxed);
514
515        // Cancel running stream tasks before replacing the token
516        self.cancellation_token.cancel();
517
518        for handle in self.tasks.drain(..) {
519            handle.abort();
520        }
521        self.cancellation_token = CancellationToken::new();
522
523        self.instruments.store(AHashMap::new());
524        self.combo_leg_trade_subs.store(AHashMap::new());
525        Ok(())
526    }
527
528    fn dispose(&mut self) -> anyhow::Result<()> {
529        log::debug!("Disposing data client: {}", self.client_id);
530        self.stop()
531    }
532
533    fn is_connected(&self) -> bool {
534        self.is_connected.load(Ordering::SeqCst)
535    }
536
537    fn is_disconnected(&self) -> bool {
538        !self.is_connected()
539    }
540
541    async fn connect(&mut self) -> anyhow::Result<()> {
542        if self.is_connected() {
543            return Ok(());
544        }
545
546        register_deribit_custom_data();
547
548        // Fetch instruments for each configured product type
549        let product_types = if self.config.product_types.is_empty() {
550            vec![DeribitProductType::Future]
551        } else {
552            self.config.product_types.clone()
553        };
554
555        let mut all_instruments = Vec::new();
556
557        for product_type in &product_types {
558            let fetched = self
559                .http_client
560                .request_instruments(DeribitCurrency::ANY, Some(*product_type))
561                .await
562                .with_context(|| format!("failed to request instruments for {product_type:?}"))?;
563
564            // Cache in http client
565            self.http_client.cache_instruments(&fetched);
566
567            // Cache locally
568            self.instruments.rcu(|m| {
569                for instrument in &fetched {
570                    m.insert(instrument.id(), instrument.clone());
571                }
572            });
573
574            all_instruments.extend(fetched);
575        }
576
577        log::debug!(
578            "Cached instruments: client_id={}, total={}",
579            self.client_id,
580            all_instruments.len()
581        );
582
583        for instrument in &all_instruments {
584            if let Err(e) = self
585                .data_sender
586                .send(DataEvent::Instrument(instrument.clone()))
587            {
588                log::warn!("Failed to send instrument: {e}");
589            }
590        }
591
592        // Cache instruments and set subscription filters in WebSocket client before connecting
593        let mark_price_subs = self.mark_price_subs.clone();
594        let index_price_subs = self.index_price_subs.clone();
595        let option_greeks_subs = self.option_greeks_subs.clone();
596        let ws = self.ws_client_mut()?;
597        ws.cache_instruments(&all_instruments);
598        ws.set_mark_price_subs(mark_price_subs);
599        ws.set_index_price_subs(index_price_subs);
600        ws.set_option_greeks_subs(option_greeks_subs);
601
602        // Connect WebSocket and wait until active
603        ws.connect().await.context("failed to connect WebSocket")?;
604        ws.wait_until_active(10.0)
605            .await
606            .context("WebSocket failed to become active")?;
607
608        // Authenticate if credentials are configured (required for raw streams)
609        if ws.has_credentials() {
610            ws.authenticate_session(DERIBIT_DATA_SESSION_NAME)
611                .await
612                .context("failed to authenticate WebSocket")?;
613            log_debug!("WebSocket authenticated");
614        }
615
616        // Get the stream and spawn processing task
617        let stream = self.ws_client_mut()?.stream()?;
618        self.spawn_stream_task(stream);
619
620        self.is_connected.store(true, Ordering::Release);
621        log_info!("Connected ({})", self.config.environment);
622        Ok(())
623    }
624
625    async fn disconnect(&mut self) -> anyhow::Result<()> {
626        if self.is_disconnected() {
627            return Ok(());
628        }
629
630        // Cancel all tasks
631        self.cancellation_token.cancel();
632
633        // Close WebSocket connection
634        if let Some(ws) = self.ws_client.as_ref()
635            && let Err(e) = ws.close().await
636        {
637            log::warn!("Error while closing WebSocket: {e:?}");
638        }
639
640        // Wait for all tasks to complete
641        for handle in self.tasks.drain(..) {
642            if let Err(e) = handle.await {
643                log::error!("Error joining WebSocket task: {e:?}");
644            }
645        }
646
647        // Reset cancellation token for potential reconnection
648        self.cancellation_token = CancellationToken::new();
649        self.is_connected.store(false, Ordering::Relaxed);
650
651        log_info!("Disconnected");
652        Ok(())
653    }
654
655    fn subscribe_instruments(&mut self, cmd: SubscribeInstruments) -> anyhow::Result<()> {
656        // Extract kind and currency from params, defaulting to "any.any" (all instruments)
657        let kind = cmd
658            .params
659            .as_ref()
660            .and_then(|p| p.get_str("kind"))
661            .unwrap_or("any")
662            .to_string();
663        let currency = cmd
664            .params
665            .as_ref()
666            .and_then(|p| p.get_str("currency"))
667            .unwrap_or("any")
668            .to_string();
669
670        let ws = self
671            .ws_client
672            .as_ref()
673            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
674            .clone();
675
676        log::debug!("Subscribing to instrument state changes for {kind}.{currency}");
677
678        get_runtime().spawn(async move {
679            if let Err(e) = ws.subscribe_instrument_status(&kind, &currency).await {
680                log::error!("Failed to subscribe to instrument status for {kind}.{currency}: {e}");
681            }
682        });
683
684        Ok(())
685    }
686
687    fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
688        let instrument_id = cmd.instrument_id;
689
690        // Check if instrument is in cache (should be from connect())
691        if !self.instruments.contains_key(&instrument_id) {
692            log::warn!(
693                "Instrument {instrument_id} not in cache - it may have been created after connect()"
694            );
695        }
696
697        // Determine kind and currency from instrument_id
698        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
699
700        let ws = self
701            .ws_client
702            .as_ref()
703            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
704            .clone();
705
706        log::debug!(
707            "Subscribing to instrument state for {instrument_id} (channel: {kind}.{currency})"
708        );
709
710        // Subscribe to broader kind/currency channel (filter in handler)
711        get_runtime().spawn(async move {
712            if let Err(e) = ws.subscribe_instrument_status(&kind, &currency).await {
713                log::error!("Failed to subscribe to instrument status for {instrument_id}: {e}");
714            }
715        });
716
717        Ok(())
718    }
719
720    fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
721        if cmd.book_type != BookType::L2_MBP {
722            anyhow::bail!("Deribit only supports L2_MBP order book deltas");
723        }
724
725        let instrument_id = cmd.instrument_id;
726        let needs_load = self.prepare_subscribe(instrument_id)?;
727
728        let ws = self
729            .ws_client
730            .as_ref()
731            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
732            .clone();
733        let http_client = self.http_client.clone();
734        let instruments = Arc::clone(&self.instruments);
735        let interval = self.get_interval(&cmd.params);
736
737        let depth = cmd
738            .depth
739            .map(|d| d.get() as u32)
740            .or_else(|| {
741                cmd.params
742                    .as_ref()
743                    .and_then(|p| p.get_u64("depth"))
744                    .map(|n| n as u32)
745            })
746            .unwrap_or(DERIBIT_BOOK_DEFAULT_DEPTH);
747
748        if !DERIBIT_BOOK_VALID_DEPTHS.contains(&depth) {
749            anyhow::bail!("invalid depth {depth}; supported depths: {DERIBIT_BOOK_VALID_DEPTHS:?}");
750        }
751
752        let group = cmd
753            .params
754            .as_ref()
755            .and_then(|p| p.get_str("group"))
756            .unwrap_or(DERIBIT_BOOK_DEFAULT_GROUP)
757            .to_string();
758
759        log::debug!(
760            "Subscribing to book deltas for {} (group: {}, depth: {}, interval: {}, book_type: {:?})",
761            instrument_id,
762            group,
763            depth,
764            interval.map_or("100ms (default)".to_string(), |i| i.to_string()),
765            cmd.book_type
766        );
767
768        get_runtime().spawn(async move {
769            if needs_load
770                && let Err(e) =
771                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
772            {
773                log::error!("Lazy-load failed for {instrument_id} (book deltas): {e}");
774                return;
775            }
776
777            let result = if interval == Some(DeribitUpdateInterval::Raw) {
778                ws.subscribe_book(instrument_id, interval).await
779            } else {
780                ws.subscribe_book_grouped(instrument_id, &group, depth, interval)
781                    .await
782            };
783
784            if let Err(e) = result {
785                log::error!("Failed to subscribe to book deltas for {instrument_id}: {e}");
786            }
787        });
788
789        Ok(())
790    }
791
792    fn subscribe_book_depth10(&mut self, cmd: SubscribeBookDepth10) -> anyhow::Result<()> {
793        if cmd.book_type != BookType::L2_MBP {
794            anyhow::bail!("Deribit only supports L2_MBP order book depth");
795        }
796
797        let instrument_id = cmd.instrument_id;
798        let needs_load = self.prepare_subscribe(instrument_id)?;
799
800        let ws = self
801            .ws_client
802            .as_ref()
803            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
804            .clone();
805        let http_client = self.http_client.clone();
806        let instruments = Arc::clone(&self.instruments);
807        let interval = self.get_interval(&cmd.params);
808        let group = cmd
809            .params
810            .as_ref()
811            .and_then(|p| p.get_str("group"))
812            .unwrap_or(DERIBIT_BOOK_DEFAULT_GROUP)
813            .to_string();
814
815        log::debug!(
816            "Subscribing to book depth10 for {} (group: {}, interval: {}, book_type: {:?})",
817            instrument_id,
818            group,
819            interval.map_or("100ms (default)".to_string(), |i| i.to_string()),
820            cmd.book_type
821        );
822
823        get_runtime().spawn(async move {
824            if needs_load
825                && let Err(e) =
826                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
827            {
828                log::error!("Lazy-load failed for {instrument_id} (book depth10): {e}");
829                return;
830            }
831
832            if let Err(e) = ws
833                .subscribe_book_grouped(instrument_id, &group, 10, interval)
834                .await
835            {
836                log::error!("Failed to subscribe to book depth10 for {instrument_id}: {e}");
837            }
838        });
839
840        Ok(())
841    }
842
843    fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
844        let instrument_id = cmd.instrument_id;
845        let needs_load = self.prepare_subscribe(instrument_id)?;
846
847        let ws = self
848            .ws_client
849            .as_ref()
850            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
851            .clone();
852        let http_client = self.http_client.clone();
853        let instruments = Arc::clone(&self.instruments);
854
855        get_runtime().spawn(async move {
856            if needs_load
857                && let Err(e) =
858                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
859            {
860                log::error!("Lazy-load failed for {instrument_id} (quotes): {e}");
861                return;
862            }
863
864            if let Err(e) = ws.subscribe_quotes(instrument_id).await {
865                log::error!("Failed to subscribe to quotes for {instrument_id}: {e}");
866            }
867        });
868
869        Ok(())
870    }
871
872    fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
873        let instrument_id = cmd.instrument_id;
874        let needs_load = self.prepare_subscribe(instrument_id)?;
875        let subscribe_combo_legs = Self::subscribe_combo_legs(&cmd.params);
876
877        let ws = self
878            .ws_client
879            .as_ref()
880            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
881            .clone();
882        let http_client = self.http_client.clone();
883        let instruments = Arc::clone(&self.instruments);
884        let combo_leg_trade_subs = Arc::clone(&self.combo_leg_trade_subs);
885        let auto_load_missing_instruments = self.config.auto_load_missing_instruments;
886        let interval = self.get_interval(&cmd.params);
887
888        log::debug!(
889            "Subscribing to trades for {} (interval: {})",
890            instrument_id,
891            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
892        );
893
894        get_runtime().spawn(async move {
895            if needs_load
896                && let Err(e) =
897                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
898            {
899                log::error!("Lazy-load failed for {instrument_id} (trades): {e}");
900                return;
901            }
902
903            let mut subscription_ids = vec![instrument_id];
904
905            if subscribe_combo_legs {
906                let leg_ids = Self::combo_leg_trade_ids(&instruments, instrument_id);
907                if leg_ids.is_empty() {
908                    log::warn!(
909                        "No Deribit combo legs found for trade subscription opt-in on {instrument_id}"
910                    );
911                }
912
913                for leg_id in leg_ids {
914                    if !instruments.contains_key(&leg_id) {
915                        if !auto_load_missing_instruments {
916                            log::error!(
917                                "Instrument {leg_id} not found and `auto_load_missing_instruments` is disabled"
918                            );
919                            continue;
920                        }
921
922                        if let Err(e) =
923                            Self::lazy_load_instrument(&http_client, &ws, &instruments, leg_id)
924                                .await
925                        {
926                            log::error!("Lazy-load failed for {leg_id} (combo leg trades): {e}");
927                            continue;
928                        }
929                    }
930
931                    subscription_ids.push(leg_id);
932                }
933            }
934
935            let mut opened_leg_ids = Vec::new();
936
937            for subscription_id in subscription_ids {
938                if let Err(e) = ws.subscribe_trades(subscription_id, interval).await {
939                    log::error!("Failed to subscribe to trades for {subscription_id}: {e}");
940                    continue;
941                }
942
943                if subscription_id != instrument_id {
944                    opened_leg_ids.push(subscription_id);
945                }
946            }
947
948            Self::track_combo_leg_trade_subs(
949                &combo_leg_trade_subs,
950                instrument_id,
951                &opened_leg_ids,
952            );
953        });
954
955        Ok(())
956    }
957
958    fn subscribe_mark_prices(&mut self, cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
959        let instrument_id = cmd.instrument_id;
960        let needs_load = self.prepare_subscribe(instrument_id)?;
961
962        let ws = self
963            .ws_client
964            .as_ref()
965            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
966            .clone();
967        let http_client = self.http_client.clone();
968        let instruments = Arc::clone(&self.instruments);
969        let interval = self.get_interval(&cmd.params);
970
971        // Track subscription so handler gates MarkPriceUpdate emission
972        self.mark_price_subs.insert(instrument_id);
973
974        log::debug!(
975            "Subscribing to mark prices for {} (via ticker channel, interval: {})",
976            instrument_id,
977            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
978        );
979
980        get_runtime().spawn(async move {
981            if needs_load
982                && let Err(e) =
983                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
984            {
985                log::error!("Lazy-load failed for {instrument_id} (mark prices): {e}");
986                return;
987            }
988
989            if let Err(e) = ws.subscribe_ticker(instrument_id, interval).await {
990                log::error!("Failed to subscribe to mark prices for {instrument_id}: {e}");
991            }
992        });
993
994        Ok(())
995    }
996
997    fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
998        let instrument_id = cmd.instrument_id;
999        let needs_load = self.prepare_subscribe(instrument_id)?;
1000
1001        let ws = self
1002            .ws_client
1003            .as_ref()
1004            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1005            .clone();
1006        let http_client = self.http_client.clone();
1007        let instruments = Arc::clone(&self.instruments);
1008        let interval = self.get_interval(&cmd.params);
1009
1010        // Track subscription so handler gates IndexPriceUpdate emission
1011        self.index_price_subs.insert(instrument_id);
1012
1013        log::debug!(
1014            "Subscribing to index prices for {} (via ticker channel, interval: {})",
1015            instrument_id,
1016            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1017        );
1018
1019        get_runtime().spawn(async move {
1020            if needs_load
1021                && let Err(e) =
1022                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
1023            {
1024                log::error!("Lazy-load failed for {instrument_id} (index prices): {e}");
1025                return;
1026            }
1027
1028            if let Err(e) = ws.subscribe_ticker(instrument_id, interval).await {
1029                log::error!("Failed to subscribe to index prices for {instrument_id}: {e}");
1030            }
1031        });
1032
1033        Ok(())
1034    }
1035
1036    fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
1037        let instrument_id = cmd.bar_type.instrument_id();
1038        let needs_load = self.prepare_subscribe(instrument_id)?;
1039
1040        let ws = self
1041            .ws_client
1042            .as_ref()
1043            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1044            .clone();
1045        let http_client = self.http_client.clone();
1046        let instruments = Arc::clone(&self.instruments);
1047        let resolution = bar_spec_to_resolution(&cmd.bar_type);
1048
1049        get_runtime().spawn(async move {
1050            if needs_load
1051                && let Err(e) =
1052                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
1053            {
1054                log::error!("Lazy-load failed for {instrument_id} (bars): {e}");
1055                return;
1056            }
1057
1058            if let Err(e) = ws.subscribe_chart(instrument_id, &resolution).await {
1059                log::error!("Failed to subscribe to bars for {instrument_id}: {e}");
1060            }
1061        });
1062
1063        Ok(())
1064    }
1065
1066    fn subscribe_funding_rates(&mut self, cmd: SubscribeFundingRates) -> anyhow::Result<()> {
1067        let instrument_id = cmd.instrument_id;
1068        let needs_load = self.prepare_subscribe(instrument_id)?;
1069
1070        let ws = self
1071            .ws_client
1072            .as_ref()
1073            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1074            .clone();
1075        let http_client = self.http_client.clone();
1076        let instruments = Arc::clone(&self.instruments);
1077        let interval = self.get_interval(&cmd.params);
1078
1079        log::debug!(
1080            "Subscribing to funding rates for {} (perpetual channel, interval: {})",
1081            instrument_id,
1082            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1083        );
1084
1085        get_runtime().spawn(async move {
1086            if needs_load
1087                && let Err(e) =
1088                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
1089            {
1090                log::error!("Lazy-load failed for {instrument_id} (funding rates): {e}");
1091                return;
1092            }
1093
1094            // Funding rates only apply to perpetual contracts; check after any lazy-load
1095            let is_perpetual = instruments
1096                .load()
1097                .get(&instrument_id)
1098                .is_some_and(|inst| matches!(inst, InstrumentAny::CryptoPerpetual(_)));
1099            if !is_perpetual {
1100                log::warn!(
1101                    "Funding rates subscription rejected for {instrument_id}: only available for perpetual instruments"
1102                );
1103                return;
1104            }
1105
1106            if let Err(e) = ws
1107                .subscribe_perpetual_interests_rates_updates(instrument_id, interval)
1108                .await
1109            {
1110                log::error!("Failed to subscribe to funding rates for {instrument_id}: {e}");
1111            }
1112        });
1113
1114        Ok(())
1115    }
1116
1117    fn subscribe_instrument_status(
1118        &mut self,
1119        cmd: SubscribeInstrumentStatus,
1120    ) -> anyhow::Result<()> {
1121        let instrument_id = cmd.instrument_id;
1122        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
1123
1124        let ws = self
1125            .ws_client
1126            .as_ref()
1127            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1128            .clone();
1129
1130        log::debug!("Subscribing to instrument status for {instrument_id} ({kind}.{currency})");
1131
1132        get_runtime().spawn(async move {
1133            if let Err(e) = ws.subscribe_instrument_status(&kind, &currency).await {
1134                log::error!("Failed to subscribe to instrument status for {instrument_id}: {e}");
1135            }
1136        });
1137
1138        Ok(())
1139    }
1140
1141    fn subscribe_option_greeks(&mut self, cmd: SubscribeOptionGreeks) -> anyhow::Result<()> {
1142        let instrument_id = cmd.instrument_id;
1143        let needs_load = self.prepare_subscribe(instrument_id)?;
1144
1145        let ws = self
1146            .ws_client
1147            .as_ref()
1148            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1149            .clone();
1150        let http_client = self.http_client.clone();
1151        let instruments = Arc::clone(&self.instruments);
1152        let interval = self.get_interval(&cmd.params);
1153
1154        // Track subscription so handler gates OptionGreeks emission
1155        self.option_greeks_subs.insert(instrument_id);
1156
1157        log::debug!(
1158            "Subscribing to option greeks for {} (via ticker channel, interval: {})",
1159            instrument_id,
1160            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1161        );
1162
1163        get_runtime().spawn(async move {
1164            if needs_load
1165                && let Err(e) =
1166                    Self::lazy_load_instrument(&http_client, &ws, &instruments, instrument_id).await
1167            {
1168                log::error!("Lazy-load failed for {instrument_id} (option greeks): {e}");
1169                return;
1170            }
1171
1172            if let Err(e) = ws.subscribe_ticker(instrument_id, interval).await {
1173                log::error!("Failed to subscribe to option greeks for {instrument_id}: {e}");
1174            }
1175        });
1176
1177        Ok(())
1178    }
1179
1180    fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
1181        let data_type = cmd.data_type.type_name();
1182        if data_type != "DeribitVolatilityIndex" {
1183            log::warn!("Unsupported custom data subscription: {data_type}");
1184            return Ok(());
1185        }
1186
1187        let Some(index_name) = cmd
1188            .data_type
1189            .metadata()
1190            .as_ref()
1191            .and_then(|m| m.get("index_name"))
1192            .and_then(|v| v.as_str())
1193            .map(str::trim)
1194            .filter(|value| !value.is_empty())
1195            .map(ToString::to_string)
1196        else {
1197            log::warn!(
1198                "Rejected Deribit volatility index subscription: missing required metadata `index_name`"
1199            );
1200            return Ok(());
1201        };
1202
1203        log::debug!("Subscribing to Deribit volatility index: {index_name}");
1204
1205        let ws = self
1206            .ws_client
1207            .as_ref()
1208            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1209            .clone();
1210
1211        get_runtime().spawn(async move {
1212            if let Err(e) = ws.subscribe_volatility_index(&index_name).await {
1213                log::error!("Failed to subscribe to volatility index {index_name}: {e}");
1214            }
1215        });
1216
1217        Ok(())
1218    }
1219
1220    fn unsubscribe_instrument_status(
1221        &mut self,
1222        cmd: &UnsubscribeInstrumentStatus,
1223    ) -> anyhow::Result<()> {
1224        let instrument_id = cmd.instrument_id;
1225        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
1226
1227        let ws = self
1228            .ws_client
1229            .as_ref()
1230            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1231            .clone();
1232
1233        log::debug!("Unsubscribing from instrument status for {instrument_id} ({kind}.{currency})");
1234
1235        get_runtime().spawn(async move {
1236            if let Err(e) = ws.unsubscribe_instrument_status(&kind, &currency).await {
1237                log::error!(
1238                    "Failed to unsubscribe from instrument status for {instrument_id}: {e}"
1239                );
1240            }
1241        });
1242
1243        Ok(())
1244    }
1245
1246    fn unsubscribe_instruments(&mut self, cmd: &UnsubscribeInstruments) -> anyhow::Result<()> {
1247        let kind = cmd
1248            .params
1249            .as_ref()
1250            .and_then(|p| p.get_str("kind"))
1251            .unwrap_or("any")
1252            .to_string();
1253        let currency = cmd
1254            .params
1255            .as_ref()
1256            .and_then(|p| p.get_str("currency"))
1257            .unwrap_or("any")
1258            .to_string();
1259
1260        let ws = self
1261            .ws_client
1262            .as_ref()
1263            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1264            .clone();
1265
1266        log::debug!("Unsubscribing from instrument state changes for {kind}.{currency}");
1267
1268        get_runtime().spawn(async move {
1269            if let Err(e) = ws.unsubscribe_instrument_status(&kind, &currency).await {
1270                log::error!(
1271                    "Failed to unsubscribe from instrument status for {kind}.{currency}: {e}"
1272                );
1273            }
1274        });
1275
1276        Ok(())
1277    }
1278
1279    fn unsubscribe_instrument(&mut self, cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
1280        let instrument_id = cmd.instrument_id;
1281
1282        // Determine kind and currency from instrument_id
1283        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
1284
1285        let ws = self
1286            .ws_client
1287            .as_ref()
1288            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1289            .clone();
1290
1291        log::debug!(
1292            "Unsubscribing from instrument state for {instrument_id} (channel: {kind}.{currency})"
1293        );
1294
1295        get_runtime().spawn(async move {
1296            if let Err(e) = ws.unsubscribe_instrument_status(&kind, &currency).await {
1297                log::error!(
1298                    "Failed to unsubscribe from instrument status for {instrument_id}: {e}"
1299                );
1300            }
1301        });
1302
1303        Ok(())
1304    }
1305
1306    fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
1307        let ws = self
1308            .ws_client
1309            .as_ref()
1310            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1311            .clone();
1312        let instrument_id = cmd.instrument_id;
1313        let interval = self.get_interval(&cmd.params);
1314
1315        let depth = cmd
1316            .params
1317            .as_ref()
1318            .and_then(|p| p.get_u64("depth"))
1319            .map_or(DERIBIT_BOOK_DEFAULT_DEPTH, |n| n as u32);
1320
1321        if !DERIBIT_BOOK_VALID_DEPTHS.contains(&depth) {
1322            anyhow::bail!("invalid depth {depth}; supported depths: {DERIBIT_BOOK_VALID_DEPTHS:?}");
1323        }
1324
1325        let group = cmd
1326            .params
1327            .as_ref()
1328            .and_then(|p| p.get_str("group"))
1329            .unwrap_or(DERIBIT_BOOK_DEFAULT_GROUP)
1330            .to_string();
1331
1332        log::debug!(
1333            "Unsubscribing from book deltas for {} (group: {}, depth: {}, interval: {})",
1334            instrument_id,
1335            group,
1336            depth,
1337            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1338        );
1339
1340        get_runtime().spawn(async move {
1341            let result = if interval == Some(DeribitUpdateInterval::Raw) {
1342                ws.unsubscribe_book(instrument_id, interval).await
1343            } else {
1344                ws.unsubscribe_book_grouped(instrument_id, &group, depth, interval)
1345                    .await
1346            };
1347
1348            if let Err(e) = result {
1349                log::error!("Failed to unsubscribe from book deltas for {instrument_id}: {e}");
1350            }
1351        });
1352
1353        Ok(())
1354    }
1355
1356    fn unsubscribe_book_depth10(&mut self, cmd: &UnsubscribeBookDepth10) -> anyhow::Result<()> {
1357        let ws = self
1358            .ws_client
1359            .as_ref()
1360            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1361            .clone();
1362        let instrument_id = cmd.instrument_id;
1363        let interval = self.get_interval(&cmd.params);
1364        let group = cmd
1365            .params
1366            .as_ref()
1367            .and_then(|p| p.get_str("group"))
1368            .unwrap_or(DERIBIT_BOOK_DEFAULT_GROUP)
1369            .to_string();
1370
1371        log::debug!(
1372            "Unsubscribing from book depth10 for {} (group: {}, interval: {})",
1373            instrument_id,
1374            group,
1375            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1376        );
1377
1378        get_runtime().spawn(async move {
1379            if let Err(e) = ws
1380                .unsubscribe_book_grouped(instrument_id, &group, 10, interval)
1381                .await
1382            {
1383                log::error!("Failed to unsubscribe from book depth10 for {instrument_id}: {e}");
1384            }
1385        });
1386
1387        Ok(())
1388    }
1389
1390    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
1391        let ws = self
1392            .ws_client
1393            .as_ref()
1394            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1395            .clone();
1396        let instrument_id = cmd.instrument_id;
1397
1398        get_runtime().spawn(async move {
1399            if let Err(e) = ws.unsubscribe_quotes(instrument_id).await {
1400                log::error!("Failed to unsubscribe from quotes for {instrument_id}: {e}");
1401            }
1402        });
1403
1404        Ok(())
1405    }
1406
1407    fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1408        let ws = self
1409            .ws_client
1410            .as_ref()
1411            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1412            .clone();
1413        let instrument_id = cmd.instrument_id;
1414        let interval = self.get_interval(&cmd.params);
1415        let mut subscription_ids = vec![instrument_id];
1416        subscription_ids.extend(Self::combo_leg_trade_unsubs(
1417            &self.combo_leg_trade_subs,
1418            instrument_id,
1419        ));
1420
1421        log::debug!(
1422            "Unsubscribing from trades for {} instruments from {} (interval: {})",
1423            subscription_ids.len(),
1424            instrument_id,
1425            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1426        );
1427
1428        get_runtime().spawn(async move {
1429            for subscription_id in subscription_ids {
1430                if let Err(e) = ws.unsubscribe_trades(subscription_id, interval).await {
1431                    log::error!("Failed to unsubscribe from trades for {subscription_id}: {e}");
1432                }
1433            }
1434        });
1435
1436        Ok(())
1437    }
1438
1439    fn unsubscribe_mark_prices(&mut self, cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
1440        let ws = self
1441            .ws_client
1442            .as_ref()
1443            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1444            .clone();
1445        let instrument_id = cmd.instrument_id;
1446        let interval = self.get_interval(&cmd.params);
1447
1448        // Remove subscription tracking so handler stops emitting MarkPriceUpdate
1449        self.mark_price_subs.remove(&instrument_id);
1450
1451        log::debug!(
1452            "Unsubscribing from mark prices for {} (via ticker channel, interval: {})",
1453            instrument_id,
1454            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1455        );
1456
1457        get_runtime().spawn(async move {
1458            if let Err(e) = ws.unsubscribe_ticker(instrument_id, interval).await {
1459                log::error!("Failed to unsubscribe from mark prices for {instrument_id}: {e}");
1460            }
1461        });
1462
1463        Ok(())
1464    }
1465
1466    fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
1467        let ws = self
1468            .ws_client
1469            .as_ref()
1470            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1471            .clone();
1472        let instrument_id = cmd.instrument_id;
1473        let interval = self.get_interval(&cmd.params);
1474
1475        // Remove subscription tracking so handler stops emitting IndexPriceUpdate
1476        self.index_price_subs.remove(&instrument_id);
1477
1478        log::debug!(
1479            "Unsubscribing from index prices for {} (via ticker channel, interval: {})",
1480            instrument_id,
1481            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1482        );
1483
1484        get_runtime().spawn(async move {
1485            if let Err(e) = ws.unsubscribe_ticker(instrument_id, interval).await {
1486                log::error!("Failed to unsubscribe from index prices for {instrument_id}: {e}");
1487            }
1488        });
1489
1490        Ok(())
1491    }
1492
1493    fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1494        let ws = self
1495            .ws_client
1496            .as_ref()
1497            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1498            .clone();
1499        let instrument_id = cmd.bar_type.instrument_id();
1500        let resolution = bar_spec_to_resolution(&cmd.bar_type);
1501
1502        get_runtime().spawn(async move {
1503            if let Err(e) = ws.unsubscribe_chart(instrument_id, &resolution).await {
1504                log::error!("Failed to unsubscribe from bars for {instrument_id}: {e}");
1505            }
1506        });
1507
1508        Ok(())
1509    }
1510
1511    fn unsubscribe_funding_rates(&mut self, cmd: &UnsubscribeFundingRates) -> anyhow::Result<()> {
1512        let instrument_id = cmd.instrument_id;
1513
1514        // Validate instrument is a perpetual - funding rates only apply to perpetual contracts
1515        let is_perpetual = self
1516            .instruments
1517            .load()
1518            .get(&instrument_id)
1519            .is_some_and(|inst| matches!(inst, InstrumentAny::CryptoPerpetual(_)));
1520
1521        if !is_perpetual {
1522            log::warn!(
1523                "Funding rates unsubscription rejected for {instrument_id}: only available for perpetual instruments"
1524            );
1525            return Ok(());
1526        }
1527
1528        let ws = self
1529            .ws_client
1530            .as_ref()
1531            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1532            .clone();
1533        let interval = self.get_interval(&cmd.params);
1534
1535        log::debug!(
1536            "Unsubscribing from funding rates for {} (perpetual channel, interval: {})",
1537            instrument_id,
1538            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1539        );
1540
1541        get_runtime().spawn(async move {
1542            if let Err(e) = ws
1543                .unsubscribe_perpetual_interest_rates_updates(instrument_id, interval)
1544                .await
1545            {
1546                log::error!("Failed to unsubscribe from funding rates for {instrument_id}: {e}");
1547            }
1548        });
1549
1550        Ok(())
1551    }
1552
1553    fn unsubscribe_option_greeks(&mut self, cmd: &UnsubscribeOptionGreeks) -> anyhow::Result<()> {
1554        let ws = self
1555            .ws_client
1556            .as_ref()
1557            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1558            .clone();
1559        let instrument_id = cmd.instrument_id;
1560        let interval = self.get_interval(&cmd.params);
1561
1562        // Remove subscription tracking so handler stops emitting OptionGreeks
1563        self.option_greeks_subs.remove(&instrument_id);
1564
1565        log::debug!(
1566            "Unsubscribing from option greeks for {} (via ticker channel, interval: {})",
1567            instrument_id,
1568            interval.map_or("100ms (default)".to_string(), |i| i.to_string())
1569        );
1570
1571        get_runtime().spawn(async move {
1572            if let Err(e) = ws.unsubscribe_ticker(instrument_id, interval).await {
1573                log::error!("Failed to unsubscribe from option greeks for {instrument_id}: {e}");
1574            }
1575        });
1576
1577        Ok(())
1578    }
1579
1580    fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
1581        let data_type = cmd.data_type.type_name();
1582        if data_type != "DeribitVolatilityIndex" {
1583            log::warn!("Unsupported custom data unsubscription: {data_type}");
1584            return Ok(());
1585        }
1586
1587        let Some(index_name) = cmd
1588            .data_type
1589            .metadata()
1590            .as_ref()
1591            .and_then(|m| m.get("index_name"))
1592            .and_then(|v| v.as_str())
1593            .map(str::trim)
1594            .filter(|value| !value.is_empty())
1595            .map(ToString::to_string)
1596        else {
1597            log::warn!(
1598                "Rejected Deribit volatility index unsubscription: missing required metadata `index_name`"
1599            );
1600            return Ok(());
1601        };
1602
1603        log::debug!("Unsubscribing from Deribit volatility index: {index_name}");
1604
1605        let ws = self
1606            .ws_client
1607            .as_ref()
1608            .ok_or_else(|| anyhow::anyhow!("WebSocket client not initialized"))?
1609            .clone();
1610
1611        get_runtime().spawn(async move {
1612            if let Err(e) = ws.unsubscribe_volatility_index(&index_name).await {
1613                log::error!("Failed to unsubscribe from volatility index {index_name}: {e}");
1614            }
1615        });
1616
1617        Ok(())
1618    }
1619
1620    fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
1621        if request.start.is_some() {
1622            log::warn!(
1623                "Requesting instruments for {:?} with specified `start` which has no effect",
1624                request.venue
1625            );
1626        }
1627
1628        if request.end.is_some() {
1629            log::warn!(
1630                "Requesting instruments for {:?} with specified `end` which has no effect",
1631                request.venue
1632            );
1633        }
1634
1635        let http_client = self.http_client.clone();
1636        let ws_client = self.ws_client.clone();
1637        let instruments_cache = Arc::clone(&self.instruments);
1638        let sender = self.data_sender.clone();
1639        let request_id = request.request_id;
1640        let client_id = request.client_id.unwrap_or(self.client_id);
1641        let start_nanos = datetime_to_unix_nanos(request.start);
1642        let end_nanos = datetime_to_unix_nanos(request.end);
1643        let params = request.params;
1644        let clock = self.clock;
1645        let venue = *DERIBIT_VENUE;
1646
1647        // Get product types from config, default to Future if empty
1648        let product_types = if self.config.product_types.is_empty() {
1649            vec![crate::http::models::DeribitProductType::Future]
1650        } else {
1651            self.config.product_types.clone()
1652        };
1653
1654        get_runtime().spawn(async move {
1655            let mut all_instruments = Vec::new();
1656
1657            for product_type in &product_types {
1658                log::debug!(
1659                    "Requesting instruments for currency=ANY, product_type={product_type:?}"
1660                );
1661
1662                match http_client
1663                    .request_instruments(DeribitCurrency::ANY, Some(*product_type))
1664                    .await
1665                {
1666                    Ok(instruments) => {
1667                        log::debug!(
1668                            "Fetched {} instruments for ANY/{:?}",
1669                            instruments.len(),
1670                            product_type
1671                        );
1672
1673                        instruments_cache.rcu(|m| {
1674                            for instrument in &instruments {
1675                                m.insert(instrument.id(), instrument.clone());
1676                            }
1677                        });
1678                        all_instruments.extend(instruments);
1679                    }
1680                    Err(e) => {
1681                        log::error!("Failed to fetch instruments for ANY/{product_type:?}: {e:?}");
1682                    }
1683                }
1684            }
1685
1686            // Propagate to HTTP and WebSocket caches so downstream
1687            // requests use correct precisions.
1688            if !all_instruments.is_empty() {
1689                http_client.cache_instruments(&all_instruments);
1690
1691                if let Some(ws) = &ws_client {
1692                    ws.cache_instruments(&all_instruments);
1693                }
1694            }
1695
1696            // Send response with all collected instruments
1697            let response = DataResponse::Instruments(InstrumentsResponse::new(
1698                request_id,
1699                client_id,
1700                venue,
1701                all_instruments,
1702                start_nanos,
1703                end_nanos,
1704                clock.get_time_ns(),
1705                params,
1706            ));
1707
1708            if let Err(e) = sender.send(DataEvent::Response(response)) {
1709                log::error!("Failed to send instruments response: {e}");
1710            }
1711        });
1712
1713        Ok(())
1714    }
1715
1716    fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
1717        if request.start.is_some() {
1718            log::warn!(
1719                "Requesting instrument {} with specified `start` which has no effect",
1720                request.instrument_id
1721            );
1722        }
1723
1724        if request.end.is_some() {
1725            log::warn!(
1726                "Requesting instrument {} with specified `end` which has no effect",
1727                request.instrument_id
1728            );
1729        }
1730
1731        log::debug!("Fetching instrument {} from API", request.instrument_id);
1732
1733        let http_client = self.http_client.clone();
1734        let ws_client = self.ws_client.clone();
1735        let instruments_cache = Arc::clone(&self.instruments);
1736        let sender = self.data_sender.clone();
1737        let instrument_id = request.instrument_id;
1738        let request_id = request.request_id;
1739        let client_id = request.client_id.unwrap_or(self.client_id);
1740        let start_nanos = datetime_to_unix_nanos(request.start);
1741        let end_nanos = datetime_to_unix_nanos(request.end);
1742        let params = request.params;
1743        let clock = self.clock;
1744
1745        get_runtime().spawn(async move {
1746            match http_client
1747                .request_instrument(instrument_id)
1748                .await
1749                .context("failed to request instrument from Deribit")
1750            {
1751                Ok(instrument) => {
1752                    log::debug!("Successfully fetched instrument: {instrument_id}");
1753
1754                    instruments_cache.insert(instrument.id(), instrument.clone());
1755                    http_client.cache_instruments(std::slice::from_ref(&instrument));
1756
1757                    if let Some(ws) = &ws_client {
1758                        ws.cache_instruments(std::slice::from_ref(&instrument));
1759                    }
1760
1761                    // Send response
1762                    let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1763                        request_id,
1764                        client_id,
1765                        instrument.id(),
1766                        instrument,
1767                        start_nanos,
1768                        end_nanos,
1769                        clock.get_time_ns(),
1770                        params,
1771                    )));
1772
1773                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1774                        log::error!("Failed to send instrument response: {e}");
1775                    }
1776                }
1777                Err(e) => {
1778                    log::error!("Instrument request failed for {instrument_id}: {e:?}");
1779                }
1780            }
1781        });
1782
1783        Ok(())
1784    }
1785
1786    fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
1787        let http_client = self.http_client.clone();
1788        let sender = self.data_sender.clone();
1789        let instrument_id = request.instrument_id;
1790        let start = request.start;
1791        let end = request.end;
1792        let limit = request.limit.map(|n| n.get() as u32);
1793        let request_id = request.request_id;
1794        let client_id = request.client_id.unwrap_or(self.client_id);
1795        let params = request.params;
1796        let clock = self.clock;
1797        let start_nanos = datetime_to_unix_nanos(start);
1798        let end_nanos = datetime_to_unix_nanos(end);
1799
1800        get_runtime().spawn(async move {
1801            match http_client
1802                .request_trades(instrument_id, start, end, limit)
1803                .await
1804                .context("failed to request trades from Deribit")
1805            {
1806                Ok(trades) => {
1807                    let response = DataResponse::Trades(TradesResponse::new(
1808                        request_id,
1809                        client_id,
1810                        instrument_id,
1811                        trades,
1812                        start_nanos,
1813                        end_nanos,
1814                        clock.get_time_ns(),
1815                        params,
1816                    ));
1817
1818                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1819                        log::error!("Failed to send trades response: {e}");
1820                    }
1821                }
1822                Err(e) => log::error!("Trades request failed for {instrument_id}: {e:?}"),
1823            }
1824        });
1825
1826        Ok(())
1827    }
1828
1829    fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1830        let http_client = self.http_client.clone();
1831        let sender = self.data_sender.clone();
1832        let bar_type = request.bar_type;
1833        let start = request.start;
1834        let end = request.end;
1835        let limit = request.limit.map(|n| n.get() as u32);
1836        let request_id = request.request_id;
1837        let client_id = request.client_id.unwrap_or(self.client_id);
1838        let params = request.params;
1839        let clock = self.clock;
1840        let start_nanos = datetime_to_unix_nanos(start);
1841        let end_nanos = datetime_to_unix_nanos(end);
1842
1843        get_runtime().spawn(async move {
1844            match http_client
1845                .request_bars(bar_type, start, end, limit)
1846                .await
1847                .context("failed to request bars from Deribit")
1848            {
1849                Ok(bars) => {
1850                    let response = DataResponse::Bars(BarsResponse::new(
1851                        request_id,
1852                        client_id,
1853                        bar_type,
1854                        bars,
1855                        start_nanos,
1856                        end_nanos,
1857                        clock.get_time_ns(),
1858                        params,
1859                    ));
1860
1861                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1862                        log::error!("Failed to send bars response: {e}");
1863                    }
1864                }
1865                Err(e) => log::error!("Bars request failed for {bar_type}: {e:?}"),
1866            }
1867        });
1868
1869        Ok(())
1870    }
1871
1872    fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
1873        let http_client = self.http_client.clone();
1874        let sender = self.data_sender.clone();
1875        let instrument_id = request.instrument_id;
1876        let depth = request.depth.map(|n| n.get() as u32);
1877        let request_id = request.request_id;
1878        let client_id = request.client_id.unwrap_or(self.client_id);
1879        let params = request.params;
1880        let clock = self.clock;
1881
1882        get_runtime().spawn(async move {
1883            match http_client
1884                .request_book_snapshot(instrument_id, depth)
1885                .await
1886                .context("failed to request book snapshot from Deribit")
1887            {
1888                Ok(book) => {
1889                    let response = DataResponse::Book(BookResponse::new(
1890                        request_id,
1891                        client_id,
1892                        instrument_id,
1893                        book,
1894                        None,
1895                        None,
1896                        clock.get_time_ns(),
1897                        params,
1898                    ));
1899
1900                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1901                        log::error!("Failed to send book snapshot response: {e}");
1902                    }
1903                }
1904                Err(e) => {
1905                    log::error!("Book snapshot request failed for {instrument_id}: {e:?}");
1906                }
1907            }
1908        });
1909
1910        Ok(())
1911    }
1912
1913    fn request_forward_prices(&self, request: RequestForwardPrices) -> anyhow::Result<()> {
1914        let currency = request.underlying.to_string();
1915        let instrument_id = request.instrument_id;
1916        let http_client = self.http_client.clone();
1917        let sender = self.data_sender.clone();
1918        let request_id = request.request_id;
1919        let client_id = request.client_id.unwrap_or(self.client_id());
1920        let params = request.params;
1921        let clock = self.clock;
1922        let venue = *DERIBIT_VENUE;
1923
1924        get_runtime().spawn(async move {
1925            let result = if let Some(inst_id) = instrument_id {
1926                // Single-instrument path: 1 HTTP call to public/ticker
1927                let instrument_name = inst_id.symbol.to_string();
1928                log::debug!(
1929                    "Requesting forward price for {currency} (single instrument: {instrument_name})"
1930                );
1931
1932                match http_client.request_ticker(&instrument_name).await {
1933                    Ok(ticker) => {
1934                        let ts = clock.get_time_ns();
1935                        let forward_prices: Vec<ForwardPrice> = ticker
1936                            .underlying_price
1937                            .map(|up| {
1938                                vec![ForwardPrice::new(
1939                                    inst_id,
1940                                    up,
1941                                    ticker.underlying_index.filter(|s| !s.is_empty()),
1942                                    ts,
1943                                    ts,
1944                                )]
1945                            })
1946                            .unwrap_or_default();
1947
1948                        log::debug!(
1949                            "Fetched {} forward price for {currency} (single instrument: {instrument_name})",
1950                            forward_prices.len(),
1951                        );
1952                        Ok((forward_prices, ts))
1953                    }
1954                    Err(e) => Err(e),
1955                }
1956            } else {
1957                // Bulk path: fetch all book summaries
1958                log::debug!("Requesting option forward prices for currency={currency} (bulk)");
1959
1960                match http_client.request_book_summaries(&currency).await {
1961                    Ok(summaries) => {
1962                        let ts = clock.get_time_ns();
1963
1964                        // Deduplicate: all options at the same expiry share the same
1965                        // forward price, so keep only one entry per underlying_index.
1966                        let mut seen_indices = std::collections::HashSet::new();
1967                        let forward_prices: Vec<ForwardPrice> = summaries
1968                            .into_iter()
1969                            .filter_map(|s| {
1970                                let up = s.underlying_price?;
1971                                let idx = s.underlying_index.clone().unwrap_or_default();
1972                                if !seen_indices.insert(idx.clone()) {
1973                                    return None;
1974                                }
1975                                Some(ForwardPrice::new(
1976                                    InstrumentId::new(
1977                                        Symbol::new(&s.instrument_name),
1978                                        *DERIBIT_VENUE,
1979                                    ),
1980                                    up,
1981                                    Some(idx).filter(|s| !s.is_empty()),
1982                                    ts,
1983                                    ts,
1984                                ))
1985                            })
1986                            .collect();
1987
1988                        log::debug!(
1989                            "Fetched {} forward prices (per-expiry) for {currency}",
1990                            forward_prices.len(),
1991                        );
1992                        Ok((forward_prices, ts))
1993                    }
1994                    Err(e) => Err(e),
1995                }
1996            };
1997
1998            match result {
1999                Ok((forward_prices, ts)) => {
2000                    let response = DataResponse::ForwardPrices(ForwardPricesResponse::new(
2001                        request_id,
2002                        client_id,
2003                        venue,
2004                        forward_prices,
2005                        ts,
2006                        params,
2007                    ));
2008
2009                    if let Err(e) = sender.send(DataEvent::Response(response)) {
2010                        log::error!("Failed to send forward prices response: {e}");
2011                    }
2012                }
2013                Err(e) => {
2014                    log::error!("Forward prices request failed for {currency}: {e:?}");
2015                }
2016            }
2017        });
2018
2019        Ok(())
2020    }
2021}