1use 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#[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 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 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 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 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 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 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 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 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 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 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 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 self.http_client.cache_instruments(&fetched);
566
567 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 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 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 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 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 self.cancellation_token.cancel();
632
633 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 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 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 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, ¤cy).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 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 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 get_runtime().spawn(async move {
712 if let Err(e) = ws.subscribe_instrument_status(&kind, ¤cy).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 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 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 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, ¤cy).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 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, ¤cy).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, ¤cy).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 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, ¤cy).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 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 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 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 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 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 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 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 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 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 log::debug!("Requesting option forward prices for currency={currency} (bulk)");
1959
1960 match http_client.request_book_summaries(¤cy).await {
1961 Ok(summaries) => {
1962 let ts = clock.get_time_ns();
1963
1964 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}