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