1use 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#[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 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 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 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 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 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 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 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 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 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 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 self.http_client.cache_instruments(&fetched);
725
726 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 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 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 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 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 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, ¤cy).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 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 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 self.spawn_command(async move {
881 if let Err(e) = ws.subscribe_instrument_status(&kind, ¤cy).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 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 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 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, ¤cy).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 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, ¤cy).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, ¤cy).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 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, ¤cy).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 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 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 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 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 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 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 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 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(¤cy, 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(¤cy, 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 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}