1use std::{
23 fmt::Debug,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, AtomicU8, Ordering},
27 },
28 time::Duration,
29};
30
31use arc_swap::ArcSwap;
32use futures_util::{FutureExt, Stream, StreamExt, stream::FuturesUnordered};
33use nautilus_common::{enums::LogColor, log_debug};
34use nautilus_core::{
35 AtomicMap, AtomicSet, env::get_or_env_var_opt, string::secret::SecretString,
36 time::get_atomic_clock_realtime,
37};
38use nautilus_live::{SocketControl, task::TaskGroup};
39use nautilus_model::{
40 data::BarType,
41 enums::OrderSide,
42 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
43 instruments::{Instrument, InstrumentAny},
44};
45use nautilus_network::{
46 ConnectionUrl,
47 http::create_standard_nautilus_headers,
48 mode::ConnectionMode,
49 websocket::{
50 AuthTracker, SubscriptionState, TransportBackend, WebSocketClient, WebSocketConfig,
51 channel_message_handler,
52 },
53};
54use parking_lot::{Mutex, RwLock, RwLockReadGuard};
55use tokio_util::sync::CancellationToken;
56use ustr::Ustr;
57
58use super::{
59 auth::{AuthState, refresh_token_after_delay, send_auth_request},
60 enums::{DeribitUpdateInterval, DeribitWsChannel},
61 error::{DeribitWsError, DeribitWsResult},
62 handler::{DeribitWsFeedHandler, HandlerCommand},
63 messages::{
64 DeribitCancelAllByInstrumentParams, DeribitCancelParams, DeribitEditParams,
65 DeribitOrderParams, NautilusWsMessage,
66 },
67};
68use crate::common::{
69 consts::{
70 DERIBIT_BOOK_DEFAULT_DEPTH, DERIBIT_TESTNET_WS_URL, DERIBIT_WS_HEARTBEAT_SECS,
71 DERIBIT_WS_ORDER_KEY, DERIBIT_WS_ORDER_QUOTA, DERIBIT_WS_SUBSCRIPTION_KEY,
72 DERIBIT_WS_SUBSCRIPTION_QUOTA, DERIBIT_WS_URL,
73 },
74 credential::{Credential, credential_env_vars},
75 enums::DeribitEnvironment,
76 parse::bar_spec_to_resolution,
77};
78
79const AUTHENTICATION_TIMEOUT_SECS: u64 = 30;
81
82type CommandSender = tokio::sync::mpsc::UnboundedSender<HandlerCommand>;
83
84#[derive(Clone)]
86pub struct DeribitWebSocketClient {
87 url: ConnectionUrl,
88 environment: DeribitEnvironment,
89 heartbeat_interval: Option<u64>,
90 auth_timeout_secs: u64,
91 credential: Option<Credential>,
92 auth_state: Arc<tokio::sync::RwLock<Option<AuthState>>>,
93 signal: Arc<AtomicBool>,
94 connection_mode: Arc<ArcSwap<AtomicU8>>,
95 auth_tracker: AuthTracker,
96 cmd_tx: Arc<RwLock<CommandSender>>,
97 out_rx: Option<Arc<tokio::sync::mpsc::UnboundedReceiver<NautilusWsMessage>>>,
98 handler_tasks: Arc<TaskGroup>,
99 connect_lock: Arc<tokio::sync::Mutex<()>>,
100 subscriptions_state: SubscriptionState,
101 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
102 option_greeks_subs: Arc<AtomicSet<InstrumentId>>,
103 mark_price_subs: Arc<AtomicSet<InstrumentId>>,
104 index_price_subs: Arc<AtomicSet<InstrumentId>>,
105 book_depth_subs: Arc<tokio::sync::Mutex<SubscriptionState>>,
106 cancellation_token: CancellationToken,
107 account_id: Option<AccountId>,
108 bars_timestamp_on_close: bool,
109 subscribe_errors: Arc<Mutex<Vec<String>>>,
110 transport_backend: TransportBackend,
111 proxy_url: Option<SecretString>,
112 socket_control: Option<SocketControl>,
113}
114
115impl Debug for DeribitWebSocketClient {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct(stringify!(DeribitWebSocketClient))
118 .field("url", &self.url)
119 .field("environment", &self.environment)
120 .field("has_credentials", &self.credential.is_some())
121 .field("is_authenticated", &self.auth_tracker.is_authenticated())
122 .field(
123 "has_auth_state",
124 &self.auth_state.try_read().is_ok_and(|s| s.is_some()),
125 )
126 .field("heartbeat_interval", &self.heartbeat_interval)
127 .finish_non_exhaustive()
128 }
129}
130
131impl DeribitWebSocketClient {
132 #[expect(clippy::too_many_arguments)]
140 pub fn new(
141 url: Option<String>,
142 api_key: Option<String>,
143 api_secret: Option<String>,
144 heartbeat_interval: u64,
145 auth_timeout_secs: Option<u64>,
146 environment: DeribitEnvironment,
147 transport_backend: TransportBackend,
148 proxy_url: Option<String>,
149 ) -> anyhow::Result<Self> {
150 Self::new_inner(
151 url,
152 api_key,
153 api_secret,
154 heartbeat_interval,
155 auth_timeout_secs,
156 environment,
157 true,
158 transport_backend,
159 proxy_url,
160 )
161 }
162
163 #[expect(clippy::too_many_arguments)]
165 fn new_inner(
166 url: Option<String>,
167 api_key: Option<String>,
168 api_secret: Option<String>,
169 heartbeat_interval: u64,
170 auth_timeout_secs: Option<u64>,
171 environment: DeribitEnvironment,
172 env_fallback: bool,
173 transport_backend: TransportBackend,
174 proxy_url: Option<String>,
175 ) -> anyhow::Result<Self> {
176 let url = url.unwrap_or_else(|| match environment {
177 DeribitEnvironment::Testnet => DERIBIT_TESTNET_WS_URL.to_string(),
178 DeribitEnvironment::Mainnet => DERIBIT_WS_URL.to_string(),
179 });
180
181 let credential =
183 Credential::resolve_with_env_fallback(api_key, api_secret, environment, env_fallback)?;
184
185 if credential.is_some() {
186 log::debug!("Credentials loaded ({environment})");
187 } else {
188 log::debug!("No credentials configured - unauthenticated mode");
189 }
190
191 let signal = Arc::new(AtomicBool::new(false));
192 let subscriptions_state = SubscriptionState::new('.');
193
194 Ok(Self {
195 url: url.into(),
196 environment,
197 heartbeat_interval: Some(heartbeat_interval),
198 auth_timeout_secs: auth_timeout_secs.unwrap_or(AUTHENTICATION_TIMEOUT_SECS),
199 credential,
200 auth_state: Arc::new(tokio::sync::RwLock::new(None)),
201 signal,
202 connection_mode: Arc::new(ArcSwap::from_pointee(AtomicU8::new(
203 ConnectionMode::Closed.as_u8(),
204 ))),
205 auth_tracker: AuthTracker::new(),
206 cmd_tx: {
207 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
208 Arc::new(RwLock::new(tx))
209 },
210 out_rx: None,
211 handler_tasks: Arc::new(TaskGroup::new()),
212 connect_lock: Arc::new(tokio::sync::Mutex::new(())),
213 subscriptions_state,
214 instruments_cache: Arc::new(AtomicMap::new()),
215 option_greeks_subs: Arc::new(AtomicSet::new()),
216 mark_price_subs: Arc::new(AtomicSet::new()),
217 index_price_subs: Arc::new(AtomicSet::new()),
218 book_depth_subs: Arc::new(tokio::sync::Mutex::new(SubscriptionState::new('.'))),
219 cancellation_token: CancellationToken::new(),
220 account_id: None,
221 bars_timestamp_on_close: true,
222 subscribe_errors: Arc::new(Mutex::new(Vec::new())),
223 transport_backend,
224 proxy_url: proxy_url.map(SecretString::from),
225 socket_control: None,
226 })
227 }
228
229 pub(crate) fn begin_shutdown(&self) {
230 self.handler_tasks.begin_shutdown();
231 self.signal.store(true, Ordering::Relaxed);
232 }
233
234 #[must_use]
236 pub fn with_socket_control(mut self, control: SocketControl) -> Self {
237 self.socket_control = Some(control);
238 self
239 }
240
241 pub fn new_public(
249 environment: DeribitEnvironment,
250 proxy_url: Option<String>,
251 ) -> anyhow::Result<Self> {
252 Self::new_inner(
253 None,
254 None,
255 None,
256 DERIBIT_WS_HEARTBEAT_SECS,
257 None,
258 environment,
259 false,
260 TransportBackend::default(),
261 proxy_url,
262 )
263 }
264
265 pub fn new_unauthenticated(
274 url: Option<String>,
275 heartbeat_interval: u64,
276 environment: DeribitEnvironment,
277 ) -> anyhow::Result<Self> {
278 Self::new_inner(
279 url,
280 None,
281 None,
282 heartbeat_interval,
283 None,
284 environment,
285 false,
286 TransportBackend::default(),
287 None,
288 )
289 }
290
291 pub fn with_credentials(
303 environment: DeribitEnvironment,
304 api_key: Option<String>,
305 api_secret: Option<String>,
306 auth_timeout_secs: Option<u64>,
307 proxy_url: Option<String>,
308 ) -> anyhow::Result<Self> {
309 let (key_env, secret_env) = credential_env_vars(environment);
310
311 let api_key = get_or_env_var_opt(api_key, key_env)
312 .ok_or_else(|| anyhow::anyhow!("Missing environment variable: {key_env}"))?;
313 let api_secret = get_or_env_var_opt(api_secret, secret_env)
314 .ok_or_else(|| anyhow::anyhow!("Missing environment variable: {secret_env}"))?;
315
316 Self::new(
317 None,
318 Some(api_key),
319 Some(api_secret),
320 DERIBIT_WS_HEARTBEAT_SECS,
321 auth_timeout_secs,
322 environment,
323 TransportBackend::default(),
324 proxy_url,
325 )
326 }
327
328 fn connection_mode(&self) -> ConnectionMode {
330 let mode_u8 = self.connection_mode.load().load(Ordering::Relaxed);
331 ConnectionMode::from_u8(mode_u8)
332 }
333
334 #[must_use]
336 pub fn is_active(&self) -> bool {
337 self.connection_mode() == ConnectionMode::Active
338 }
339
340 #[must_use]
342 pub fn url(&self) -> &str {
343 self.url.expose()
344 }
345
346 #[must_use]
348 pub fn environment(&self) -> DeribitEnvironment {
349 self.environment
350 }
351
352 #[must_use]
354 pub fn is_closed(&self) -> bool {
355 let mode = self.connection_mode();
356 mode == ConnectionMode::Disconnect || mode == ConnectionMode::Closed
357 }
358
359 pub fn cancel_all_requests(&self) {
361 self.cancellation_token.cancel();
362 }
363
364 #[must_use]
366 pub fn cancellation_token(&self) -> &CancellationToken {
367 &self.cancellation_token
368 }
369
370 pub async fn wait_until_active(&self, timeout_secs: f64) -> DeribitWsResult<()> {
376 let timeout = tokio::time::Duration::from_secs_f64(timeout_secs);
377
378 tokio::time::timeout(timeout, async {
379 while !self.is_active() {
380 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
381 }
382 })
383 .await
384 .map_err(|_| {
385 DeribitWsError::Timeout(format!(
386 "WebSocket connection timeout after {timeout_secs} seconds"
387 ))
388 })?;
389
390 Ok(())
391 }
392
393 pub async fn wait_for_subscriptions_confirmed(&self, timeout_secs: f64) -> DeribitWsResult<()> {
399 let timeout = Duration::from_secs_f64(timeout_secs);
400
401 tokio::time::timeout(timeout, async {
402 loop {
403 let subscribe_error = {
405 let mut errors = self.subscribe_errors.lock();
406 if errors.is_empty() {
407 None
408 } else {
409 let msg = errors.join("; ");
410 errors.clear();
411 Some(msg)
412 }
413 };
414
415 if let Some(msg) = subscribe_error {
416 return Err(DeribitWsError::Subscribe(msg));
417 }
418
419 let pending = self.subscriptions_state.pending_subscribe_topics();
420 if pending.is_empty() {
421 return Ok(());
422 }
423 tokio::time::sleep(Duration::from_millis(10)).await;
424 }
425 })
426 .await
427 .map_err(|_| {
428 let pending = self.subscriptions_state.pending_subscribe_topics();
429 DeribitWsError::Timeout(format!(
430 "Subscription confirmation timeout after {timeout_secs}s, \
431 still pending: {pending:?}"
432 ))
433 })?
434 }
435
436 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
438 let tx = self.command_sender();
439
440 self.instruments_cache.rcu(|m| {
441 for inst in instruments {
442 m.insert(inst.raw_symbol().inner(), inst.clone());
443 }
444 });
445 log::debug!("Cached {} instruments", self.instruments_cache.len());
446
447 if self.is_active() {
450 for inst in instruments {
451 let _ = tx.send(HandlerCommand::UpdateInstrument(Box::new(inst.clone())));
452 }
453 }
454 }
455
456 pub fn cache_instrument(&self, instrument: InstrumentAny) {
458 let tx = self.command_sender();
459 let symbol = instrument.raw_symbol().inner();
460 self.instruments_cache.insert(symbol, instrument);
461
462 if self.is_active() {
464 let inst = self.instruments_cache.get_cloned(&symbol);
465
466 if let Some(inst) = inst {
467 let _ = tx.send(HandlerCommand::UpdateInstrument(Box::new(inst)));
468 }
469 }
470 }
471
472 pub fn set_option_greeks_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
474 self.option_greeks_subs = subs;
475 }
476
477 pub fn set_mark_price_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
479 self.mark_price_subs = subs;
480 }
481
482 pub fn set_index_price_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
484 self.index_price_subs = subs;
485 }
486
487 pub fn add_mark_price_sub(&self, instrument_id: InstrumentId) {
489 self.mark_price_subs.insert(instrument_id);
490 }
491
492 pub fn remove_mark_price_sub(&self, instrument_id: &InstrumentId) {
494 self.mark_price_subs.remove(instrument_id);
495 }
496
497 pub fn add_index_price_sub(&self, instrument_id: InstrumentId) {
499 self.index_price_subs.insert(instrument_id);
500 }
501
502 pub fn remove_index_price_sub(&self, instrument_id: &InstrumentId) {
504 self.index_price_subs.remove(instrument_id);
505 }
506
507 pub fn add_option_greeks_sub(&self, instrument_id: InstrumentId) {
509 self.option_greeks_subs.insert(instrument_id);
510 }
511
512 pub fn remove_option_greeks_sub(&self, instrument_id: &InstrumentId) {
514 self.option_greeks_subs.remove(instrument_id);
515 }
516
517 pub async fn connect(&mut self) -> anyhow::Result<()> {
523 let connect_lock = Arc::clone(&self.connect_lock);
524 let _connect_guard = connect_lock.lock().await;
525
526 log_debug!(
527 "Connecting to WebSocket: {}",
528 self.url.origin(),
529 color = LogColor::Blue
530 );
531
532 if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
533 self.handler_tasks.begin_shutdown();
534 self.signal.store(true, Ordering::Relaxed);
535 self.finish_handler()
536 .await
537 .map_err(|e| anyhow::anyhow!("failed to stop prior WebSocket handler: {e}"))?;
538 self.handler_tasks.start_generation().map_err(|e| {
539 anyhow::anyhow!("failed to start WebSocket handler task generation: {e}")
540 })?;
541 }
542 let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
543 anyhow::anyhow!("failed to acquire WebSocket handler task spawner: {e}")
544 })?;
545
546 self.signal.store(false, Ordering::Relaxed);
549 self.subscriptions_state.clear();
550 self.book_depth_subs.lock().await.clear();
551
552 let (message_handler, raw_rx) = channel_message_handler();
554
555 let headers = create_standard_nautilus_headers();
561
562 let config = WebSocketConfig {
563 url: self.url.expose().to_owned(),
564 headers,
565 heartbeat_interval_secs: self.heartbeat_interval,
566 heartbeat_payload: None, connect_timeout_ms: Some(5_000),
568 reconnect_delay_initial_ms: None,
569 reconnect_delay_max_ms: None,
570 reconnect_backoff_factor: None,
571 reconnect_jitter_ms: None,
572 reconnect_max_attempts: None,
573 heartbeat_timeout_secs: None,
574 idle_timeout_ms: None,
575 writer_capacity: None,
576 backend: self.transport_backend,
577 proxy_url: self
578 .proxy_url
579 .as_ref()
580 .map(|value| value.expose_secret().to_owned()),
581 max_message_size_bytes: None,
582 max_frame_size_bytes: None,
583 };
584
585 let keyed_quotas = vec![
587 (
588 DERIBIT_WS_SUBSCRIPTION_KEY.to_string(),
589 *DERIBIT_WS_SUBSCRIPTION_QUOTA,
590 ),
591 (DERIBIT_WS_ORDER_KEY.to_string(), *DERIBIT_WS_ORDER_QUOTA),
592 ];
593
594 let ws_client = WebSocketClient::builder()
596 .config(config)
597 .message_handler(message_handler)
598 .keyed_quotas(keyed_quotas)
599 .default_quota(*DERIBIT_WS_SUBSCRIPTION_QUOTA)
600 .maybe_state_sink(self.socket_control.as_ref().map(SocketControl::sink))
601 .connect()
602 .await?;
603
604 let connection_mode = ws_client.connection_mode_atomic();
605 let reconnect_handle = ws_client.reconnect_handle();
606
607 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
609 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel();
610
611 self.out_rx = Some(Arc::new(out_rx));
612
613 self.subscribe_errors.lock().clear();
614
615 let mut handler = DeribitWsFeedHandler::new(
617 self.signal.clone(),
618 cmd_rx,
619 raw_rx,
620 out_tx,
621 self.auth_tracker.clone(),
622 self.subscriptions_state.clone(),
623 self.option_greeks_subs.clone(),
624 self.mark_price_subs.clone(),
625 self.index_price_subs.clone(),
626 self.account_id,
627 self.bars_timestamp_on_close,
628 self.subscribe_errors.clone(),
629 );
630
631 handler.set_book_depth_subs(self.book_depth_subs.clone());
632
633 if let Some(control) = &self.socket_control {
634 control.register(move || reconnect_handle.request_reconnect());
635 }
636
637 {
640 let mut command_sender = self.cmd_tx.write();
641 let _ = cmd_tx.send(HandlerCommand::SetClient(ws_client));
642
643 let instruments: Vec<InstrumentAny> =
644 self.instruments_cache.load().values().cloned().collect();
645
646 if !instruments.is_empty() {
647 log::debug!(
648 "Sending {} cached instruments to handler",
649 instruments.len()
650 );
651 let _ = cmd_tx.send(HandlerCommand::InitializeInstruments(instruments));
652 }
653
654 if let Some(interval) = self.heartbeat_interval {
655 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
656 }
657
658 *command_sender = cmd_tx.clone();
659 self.connection_mode.store(connection_mode);
660 }
661
662 let subscriptions_state = self.subscriptions_state.clone();
664 let credential = self.credential.clone();
665 let auth_tracker = self.auth_tracker.clone();
666 let auth_state = self.auth_state.clone();
667 let heartbeat_interval = self.heartbeat_interval;
668
669 let handler_task = async move {
670 const MAX_REAUTH_ATTEMPTS: u32 = 3;
671
672 let mut pending_reauth = false;
673 let mut reauth_attempts: u32 = 0;
674
675 let mut refresh_cancel = CancellationToken::new();
676 let mut retry_cancel = CancellationToken::new();
677 let mut lifecycle_futures = FuturesUnordered::new();
678
679 loop {
680 let message = {
681 let next = handler.next();
684 tokio::pin!(next);
685
686 loop {
687 tokio::select! {
688 message = &mut next => break message,
689 _ = lifecycle_futures.next(), if !lifecycle_futures.is_empty() => {}
690 }
691 }
692 };
693
694 match message {
695 Some(msg) => match msg {
696 NautilusWsMessage::Reconnected => {
697 log::info!("Reconnected to WebSocket");
698
699 refresh_cancel.cancel();
701 refresh_cancel = CancellationToken::new();
702 retry_cancel.cancel();
703 retry_cancel = CancellationToken::new();
704
705 if let Some(interval) = heartbeat_interval {
709 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
710 }
711
712 let channels = subscriptions_state.reset_after_reconnect();
713
714 if let Some(cred) = &credential {
716 log::info!("Re-authenticating after reconnection...");
717
718 let _rx = auth_tracker.begin();
719 pending_reauth = true;
720 reauth_attempts = 1;
721
722 let previous_scope =
723 auth_state.read().await.as_ref().map(|s| s.scope.clone());
724
725 send_auth_request(cred, previous_scope, &cmd_tx);
726 } else {
727 if !channels.is_empty() {
729 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
730 }
731 }
732 }
733 NautilusWsMessage::Authenticated(result) => {
734 let result = *result;
735 let timestamp = get_atomic_clock_realtime().get_time_ms();
736 let new_auth_state = AuthState::from_auth_result(&result, timestamp);
737 *auth_state.write().await = Some(new_auth_state);
738
739 refresh_cancel.cancel();
740 refresh_cancel = CancellationToken::new();
741 retry_cancel.cancel();
742 retry_cancel = CancellationToken::new();
743
744 lifecycle_futures.push(
745 refresh_token_after_delay(
746 result.expires_in,
747 result.refresh_token.clone(),
748 cmd_tx.clone(),
749 refresh_cancel.clone(),
750 )
751 .boxed(),
752 );
753
754 if pending_reauth {
755 pending_reauth = false;
756 reauth_attempts = 0;
757 log::info!(
758 "Re-authentication successful (scope: {}), resubscribing to channels",
759 result.scope
760 );
761
762 let channels = subscriptions_state.all_topics();
763
764 if !channels.is_empty() {
765 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
766 }
767 } else {
768 log::debug!(
769 "Auth state stored: scope={}, expires_in={}s",
770 result.scope,
771 result.expires_in
772 );
773 }
774 }
775 NautilusWsMessage::AuthenticationFailed(reason) => {
776 if pending_reauth && reauth_attempts < MAX_REAUTH_ATTEMPTS {
777 let delay_secs = 1u64 << reauth_attempts; log::warn!(
779 "Re-authentication attempt {reauth_attempts}/{MAX_REAUTH_ATTEMPTS} \
780 failed: {reason} - retrying in {delay_secs}s",
781 );
782 reauth_attempts += 1;
783
784 if let Some(cred) = &credential {
787 let cred = cred.clone();
788 let auth_state = auth_state.clone();
789 let auth_tracker = auth_tracker.clone();
790 let cmd_tx = cmd_tx.clone();
791 let cancel = retry_cancel.clone();
792
793 lifecycle_futures.push(
794 async move {
795 tokio::select! {
796 () = tokio::time::sleep(Duration::from_secs(delay_secs)) => {}
797 () = cancel.cancelled() => return,
798 }
799 let _rx = auth_tracker.begin();
800 let previous_scope = auth_state
801 .read()
802 .await
803 .as_ref()
804 .map(|s| s.scope.clone());
805 send_auth_request(&cred, previous_scope, &cmd_tx);
806 }
807 .boxed(),
808 );
809 }
810 } else if pending_reauth {
811 pending_reauth = false;
812 reauth_attempts = 0;
813 log::error!(
814 "Re-authentication failed after {MAX_REAUTH_ATTEMPTS} \
815 attempts: {reason} \
816 - resubscribing to public channels only"
817 );
818
819 let all = subscriptions_state.all_topics();
820 let mut public_channels = Vec::new();
821
822 for ch in &all {
823 if DeribitWsChannel::requires_auth(ch) {
824 subscriptions_state.mark_unsubscribe(ch);
827 subscriptions_state.confirm_unsubscribe(ch);
828 subscriptions_state.remove_reference(ch);
829 } else {
830 public_channels.push(ch.clone());
831 }
832 }
833
834 if !public_channels.is_empty() {
835 let _ = cmd_tx.send(HandlerCommand::Subscribe {
836 channels: public_channels,
837 });
838 }
839 } else {
840 log::error!("Authentication failed: {reason}");
841 }
842 }
843 _ => {}
844 },
845 None => {
846 log::debug!("Handler returned None, stopping task");
847 break;
848 }
849 }
850 }
851 };
852
853 if let Err(e) = handler_spawner.spawn(handler_task) {
854 if let Some(control) = &self.socket_control {
855 control.deregister();
856 }
857 self.out_rx = None;
858 anyhow::bail!("failed to register WebSocket handler task: {e}");
859 }
860 log::debug!("Connected to WebSocket");
861
862 Ok(())
863 }
864
865 pub async fn close(&self) -> DeribitWsResult<()> {
871 self.begin_shutdown();
872 let connect_lock = Arc::clone(&self.connect_lock);
873 let _connect_guard = connect_lock.lock().await;
874 self.close_locked().await
875 }
876
877 async fn close_locked(&self) -> DeribitWsResult<()> {
878 log::debug!("Closing WebSocket connection");
879 self.begin_shutdown();
880
881 let _ = self.command_sender().send(HandlerCommand::Disconnect);
882
883 self.finish_handler().await?;
884
885 self.auth_tracker.invalidate();
886
887 if let Some(control) = &self.socket_control {
888 control.deregister();
889 }
890 Ok(())
891 }
892
893 async fn finish_handler(&self) -> DeribitWsResult<()> {
894 self.handler_tasks
895 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
896 .await
897 .map_err(|e| {
898 DeribitWsError::ClientError(format!("WebSocket handler shutdown failed: {e}"))
899 })
900 }
901
902 pub fn stream(&mut self) -> DeribitWsResult<impl Stream<Item = NautilusWsMessage> + 'static> {
908 let rx = self.out_rx.take().ok_or_else(|| {
909 DeribitWsError::ClientError(
910 "Stream receiver already taken or not connected".to_string(),
911 )
912 })?;
913 let mut rx = Arc::try_unwrap(rx).map_err(|_| {
914 DeribitWsError::ClientError(
915 "Cannot take stream ownership - other references exist".to_string(),
916 )
917 })?;
918
919 Ok(async_stream::stream! {
920 while let Some(msg) = rx.recv().await {
921 yield msg;
922 }
923 })
924 }
925
926 #[must_use]
928 pub fn has_credentials(&self) -> bool {
929 self.credential.is_some()
930 }
931
932 #[must_use]
934 pub fn is_authenticated(&self) -> bool {
935 self.auth_tracker.is_authenticated()
936 }
937
938 pub async fn authenticate(&self, session_name: Option<&str>) -> DeribitWsResult<()> {
957 let credential = self.credential.as_ref().ok_or_else(|| {
958 DeribitWsError::Authentication("API credentials not configured".to_string())
959 })?;
960
961 let scope = session_name.map(|name| format!("session:{name}"));
963
964 log::debug!("Authenticating WebSocket...");
965
966 let rx = self.auth_tracker.begin();
967
968 let cmd_tx = self.command_sender().clone();
970 send_auth_request(credential, scope, &cmd_tx);
971
972 match self
974 .auth_tracker
975 .wait_for_result::<DeribitWsError>(Duration::from_secs(self.auth_timeout_secs), rx)
976 .await
977 {
978 Ok(()) => {
979 log::debug!("WebSocket authenticated successfully");
980 Ok(())
981 }
982 Err(e) => {
983 log::error!("WebSocket authentication failed: error={e}");
984 Err(e)
985 }
986 }
987 }
988
989 pub async fn authenticate_session(&self, session_name: &str) -> DeribitWsResult<()> {
998 self.authenticate(Some(session_name)).await
999 }
1000
1001 pub async fn auth_state(&self) -> Option<AuthState> {
1005 self.auth_state.read().await.clone()
1006 }
1007
1008 pub async fn access_token(&self) -> Option<SecretString> {
1010 self.auth_state
1011 .read()
1012 .await
1013 .as_ref()
1014 .map(|s| s.access_token.clone())
1015 }
1016
1017 pub fn set_account_id(&mut self, account_id: AccountId) {
1019 self.account_id = Some(account_id);
1020 }
1021
1022 pub fn set_bars_timestamp_on_close(&mut self, value: bool) {
1026 self.bars_timestamp_on_close = value;
1027 }
1028
1029 async fn send_subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1030 let mut channels_to_subscribe = Vec::new();
1031
1032 for channel in channels {
1033 if self.subscriptions_state.add_reference(&channel) {
1034 self.subscriptions_state.mark_subscribe(&channel);
1035 channels_to_subscribe.push(channel);
1036 } else {
1037 log::debug!("Already subscribed to {channel}, skipping duplicate subscription");
1038 }
1039 }
1040
1041 if channels_to_subscribe.is_empty() {
1042 return Ok(());
1043 }
1044
1045 if let Err(e) = self.command_sender().send(HandlerCommand::Subscribe {
1046 channels: channels_to_subscribe.clone(),
1047 }) {
1048 for channel in &channels_to_subscribe {
1050 self.subscriptions_state.remove_reference(channel);
1051 self.subscriptions_state.mark_unsubscribe(channel);
1052 self.subscriptions_state.confirm_unsubscribe(channel);
1053 }
1054 return Err(DeribitWsError::Send(e.to_string()));
1055 }
1056
1057 log::debug!(
1058 "Sent subscribe for {} channels",
1059 channels_to_subscribe.len()
1060 );
1061 Ok(())
1062 }
1063
1064 async fn send_unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1065 let mut channels_to_unsubscribe = Vec::new();
1066
1067 for channel in channels {
1068 if self.subscriptions_state.remove_reference(&channel) {
1069 self.subscriptions_state.mark_unsubscribe(&channel);
1070 channels_to_unsubscribe.push(channel);
1071 } else {
1072 log::debug!("Still has references to {channel}, skipping unsubscription");
1073 }
1074 }
1075
1076 if channels_to_unsubscribe.is_empty() {
1077 return Ok(());
1078 }
1079
1080 if let Err(e) = self.command_sender().send(HandlerCommand::Unsubscribe {
1081 channels: channels_to_unsubscribe.clone(),
1082 }) {
1083 for channel in &channels_to_unsubscribe {
1090 self.subscriptions_state.confirm_unsubscribe(channel);
1091 self.subscriptions_state.add_reference(channel);
1092 self.subscriptions_state.mark_subscribe(channel);
1093 self.subscriptions_state.confirm_subscribe(channel);
1094 }
1095 return Err(DeribitWsError::Send(e.to_string()));
1096 }
1097
1098 log::debug!(
1099 "Sent unsubscribe for {} channels",
1100 channels_to_unsubscribe.len()
1101 );
1102 Ok(())
1103 }
1104
1105 pub async fn subscribe_trades(
1116 &self,
1117 instrument_id: InstrumentId,
1118 interval: Option<DeribitUpdateInterval>,
1119 ) -> DeribitWsResult<()> {
1120 let interval = interval.unwrap_or_default();
1121 self.check_auth_requirement(interval)?;
1122 let channel =
1123 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1124 self.send_subscribe(vec![channel]).await
1125 }
1126
1127 pub async fn unsubscribe_trades(
1133 &self,
1134 instrument_id: InstrumentId,
1135 interval: Option<DeribitUpdateInterval>,
1136 ) -> DeribitWsResult<()> {
1137 let interval = interval.unwrap_or_default();
1138 let channel =
1139 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1140 self.send_unsubscribe(vec![channel]).await
1141 }
1142
1143 pub async fn subscribe_book(
1154 &self,
1155 instrument_id: InstrumentId,
1156 interval: Option<DeribitUpdateInterval>,
1157 ) -> DeribitWsResult<()> {
1158 let interval = interval.unwrap_or_default();
1159 self.check_auth_requirement(interval)?;
1160 let channel =
1161 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1162 self.send_subscribe(vec![channel]).await
1163 }
1164
1165 pub async fn unsubscribe_book(
1171 &self,
1172 instrument_id: InstrumentId,
1173 interval: Option<DeribitUpdateInterval>,
1174 ) -> DeribitWsResult<()> {
1175 let interval = interval.unwrap_or_default();
1176 let channel =
1177 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1178 self.send_unsubscribe(vec![channel]).await
1179 }
1180
1181 pub async fn subscribe_book_grouped(
1191 &self,
1192 instrument_id: InstrumentId,
1193 group: &str,
1194 depth: u32,
1195 interval: Option<DeribitUpdateInterval>,
1196 ) -> DeribitWsResult<()> {
1197 let channel = grouped_book_channel(instrument_id, group, depth, interval);
1198 log::debug!("Subscribing to grouped book channel: {channel}");
1199 self.send_subscribe(vec![channel]).await
1200 }
1201
1202 pub(crate) async fn subscribe_book_depth(
1203 &self,
1204 instrument_id: InstrumentId,
1205 group: &str,
1206 interval: Option<DeribitUpdateInterval>,
1207 ) -> DeribitWsResult<()> {
1208 let channel =
1209 grouped_book_channel(instrument_id, group, DERIBIT_BOOK_DEFAULT_DEPTH, interval);
1210 let depth_subs = self.book_depth_subs.lock().await;
1211 depth_subs.add_reference(&channel);
1212 if let Err(e) = self.send_subscribe(vec![channel.clone()]).await {
1213 depth_subs.remove_reference(&channel);
1214 return Err(e);
1215 }
1216
1217 Ok(())
1218 }
1219
1220 pub async fn unsubscribe_book_grouped(
1228 &self,
1229 instrument_id: InstrumentId,
1230 group: &str,
1231 depth: u32,
1232 interval: Option<DeribitUpdateInterval>,
1233 ) -> DeribitWsResult<()> {
1234 let channel = grouped_book_channel(instrument_id, group, depth, interval);
1235 self.send_unsubscribe(vec![channel]).await
1236 }
1237
1238 pub(crate) async fn unsubscribe_book_depth(
1239 &self,
1240 instrument_id: InstrumentId,
1241 group: &str,
1242 interval: Option<DeribitUpdateInterval>,
1243 ) -> DeribitWsResult<()> {
1244 let channel =
1245 grouped_book_channel(instrument_id, group, DERIBIT_BOOK_DEFAULT_DEPTH, interval);
1246 let depth_subs = self.book_depth_subs.lock().await;
1247 if depth_subs.get_reference_count(&channel) == 0 {
1248 return Ok(());
1249 }
1250
1251 self.send_unsubscribe(vec![channel.clone()]).await?;
1252 depth_subs.remove_reference(&channel);
1253 Ok(())
1254 }
1255
1256 pub async fn subscribe_ticker(
1267 &self,
1268 instrument_id: InstrumentId,
1269 interval: Option<DeribitUpdateInterval>,
1270 ) -> DeribitWsResult<()> {
1271 let interval = interval.unwrap_or_default();
1272 self.check_auth_requirement(interval)?;
1273 let channel =
1274 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1275 self.send_subscribe(vec![channel]).await
1276 }
1277
1278 pub async fn unsubscribe_ticker(
1284 &self,
1285 instrument_id: InstrumentId,
1286 interval: Option<DeribitUpdateInterval>,
1287 ) -> DeribitWsResult<()> {
1288 let interval = interval.unwrap_or_default();
1289 let channel =
1290 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1291 self.send_unsubscribe(vec![channel]).await
1292 }
1293
1294 pub async fn subscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1302 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1303 self.send_subscribe(vec![channel]).await
1304 }
1305
1306 pub async fn unsubscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1312 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1313 self.send_unsubscribe(vec![channel]).await
1314 }
1315
1316 pub async fn subscribe_instrument_status(
1324 &self,
1325 kind: &str,
1326 currency: &str,
1327 ) -> DeribitWsResult<()> {
1328 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1329 self.send_subscribe(vec![channel]).await
1330 }
1331
1332 pub async fn unsubscribe_instrument_status(
1338 &self,
1339 kind: &str,
1340 currency: &str,
1341 ) -> DeribitWsResult<()> {
1342 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1343 self.send_unsubscribe(vec![channel]).await
1344 }
1345
1346 pub async fn subscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1354 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1355 self.send_subscribe(vec![channel]).await
1356 }
1357
1358 pub async fn unsubscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1364 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1365 self.send_unsubscribe(vec![channel]).await
1366 }
1367
1368 pub async fn subscribe_perpetual_interests_rates_updates(
1376 &self,
1377 instrument_id: InstrumentId,
1378 interval: Option<DeribitUpdateInterval>,
1379 ) -> DeribitWsResult<()> {
1380 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1381 let channel = DeribitWsChannel::Perpetual
1382 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1383
1384 self.send_subscribe(vec![channel]).await
1385 }
1386
1387 pub async fn unsubscribe_perpetual_interest_rates_updates(
1393 &self,
1394 instrument_id: InstrumentId,
1395 interval: Option<DeribitUpdateInterval>,
1396 ) -> DeribitWsResult<()> {
1397 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1398 let channel = DeribitWsChannel::Perpetual
1399 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1400
1401 self.send_unsubscribe(vec![channel]).await
1402 }
1403
1404 pub async fn subscribe_chart(
1416 &self,
1417 instrument_id: InstrumentId,
1418 resolution: &str,
1419 ) -> DeribitWsResult<()> {
1420 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1422 self.send_subscribe(vec![channel]).await
1423 }
1424
1425 pub async fn unsubscribe_chart(
1431 &self,
1432 instrument_id: InstrumentId,
1433 resolution: &str,
1434 ) -> DeribitWsResult<()> {
1435 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1436 self.send_unsubscribe(vec![channel]).await
1437 }
1438
1439 pub async fn subscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1448 let resolution = bar_spec_to_resolution(&bar_type);
1449 self.subscribe_chart(bar_type.instrument_id(), &resolution)
1450 .await
1451 }
1452
1453 pub async fn unsubscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1459 let resolution = bar_spec_to_resolution(&bar_type);
1460 self.unsubscribe_chart(bar_type.instrument_id(), &resolution)
1461 .await
1462 }
1463
1464 fn check_auth_requirement(&self, interval: DeribitUpdateInterval) -> DeribitWsResult<()> {
1470 if interval.requires_auth() && !self.is_authenticated() {
1471 return Err(DeribitWsError::Authentication(
1472 "Raw streams require authentication. Call authenticate() first.".to_string(),
1473 ));
1474 }
1475 Ok(())
1476 }
1477
1478 pub async fn subscribe_user_orders(&self) -> DeribitWsResult<()> {
1486 if !self.is_authenticated() {
1487 return Err(DeribitWsError::Authentication(
1488 "User orders subscription requires authentication".to_string(),
1489 ));
1490 }
1491 self.send_subscribe(vec!["user.orders.any.any.raw".to_string()])
1492 .await
1493 }
1494
1495 pub async fn unsubscribe_user_orders(&self) -> DeribitWsResult<()> {
1501 self.send_unsubscribe(vec!["user.orders.any.any.raw".to_string()])
1502 .await
1503 }
1504
1505 pub async fn subscribe_user_trades(&self) -> DeribitWsResult<()> {
1513 if !self.is_authenticated() {
1514 return Err(DeribitWsError::Authentication(
1515 "User trades subscription requires authentication".to_string(),
1516 ));
1517 }
1518 self.send_subscribe(vec!["user.trades.any.any.raw".to_string()])
1519 .await
1520 }
1521
1522 pub async fn unsubscribe_user_trades(&self) -> DeribitWsResult<()> {
1528 self.send_unsubscribe(vec!["user.trades.any.any.raw".to_string()])
1529 .await
1530 }
1531
1532 pub async fn subscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1542 if !self.is_authenticated() {
1543 return Err(DeribitWsError::Authentication(
1544 "User portfolio subscription requires authentication".to_string(),
1545 ));
1546 }
1547 self.send_subscribe(vec!["user.portfolio.any".to_string()])
1548 .await
1549 }
1550
1551 pub async fn unsubscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1557 self.send_unsubscribe(vec!["user.portfolio.any".to_string()])
1558 .await
1559 }
1560
1561 pub async fn subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1567 self.send_subscribe(channels).await
1568 }
1569
1570 pub async fn unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1576 self.send_unsubscribe(channels).await
1577 }
1578
1579 pub async fn submit_order(
1590 &self,
1591 order_side: OrderSide,
1592 params: DeribitOrderParams,
1593 client_order_id: ClientOrderId,
1594 trader_id: TraderId,
1595 strategy_id: StrategyId,
1596 instrument_id: InstrumentId,
1597 ) -> DeribitWsResult<()> {
1598 if !self.is_authenticated() {
1599 return Err(DeribitWsError::Authentication(
1600 "Submit order requires authentication. Call authenticate_session() first."
1601 .to_string(),
1602 ));
1603 }
1604
1605 log::debug!(
1606 "Sending {} order: instrument={}, amount={}, price={:?}, client_order_id={}",
1607 order_side,
1608 params.instrument_name,
1609 params.amount,
1610 params.price,
1611 client_order_id
1612 );
1613
1614 let cmd = match order_side {
1615 OrderSide::Buy => HandlerCommand::Buy {
1616 params,
1617 client_order_id,
1618 trader_id,
1619 strategy_id,
1620 instrument_id,
1621 },
1622 OrderSide::Sell => HandlerCommand::Sell {
1623 params,
1624 client_order_id,
1625 trader_id,
1626 strategy_id,
1627 instrument_id,
1628 },
1629 };
1630
1631 self.command_sender()
1632 .send(cmd)
1633 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1634
1635 Ok(())
1636 }
1637
1638 pub async fn modify_order(
1649 &self,
1650 params: DeribitEditParams,
1651 client_order_id: ClientOrderId,
1652 trader_id: TraderId,
1653 strategy_id: StrategyId,
1654 instrument_id: InstrumentId,
1655 ) -> DeribitWsResult<()> {
1656 if !self.is_authenticated() {
1657 return Err(DeribitWsError::Authentication(
1658 "Modify order requires authentication. Call authenticate_session() first."
1659 .to_string(),
1660 ));
1661 }
1662
1663 log::debug!(
1664 "Sending modify order: order_id={}, amount={}, price={:?}, client_order_id={client_order_id}",
1665 params.order_id,
1666 params.amount,
1667 params.price,
1668 );
1669
1670 self.command_sender()
1671 .send(HandlerCommand::Edit {
1672 params,
1673 client_order_id,
1674 trader_id,
1675 strategy_id,
1676 instrument_id,
1677 })
1678 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1679
1680 Ok(())
1681 }
1682
1683 pub async fn cancel_order(
1694 &self,
1695 order_id: &str,
1696 client_order_id: ClientOrderId,
1697 trader_id: TraderId,
1698 strategy_id: StrategyId,
1699 instrument_id: InstrumentId,
1700 ) -> DeribitWsResult<()> {
1701 if !self.is_authenticated() {
1702 return Err(DeribitWsError::Authentication(
1703 "Cancel order requires authentication. Call authenticate_session() first."
1704 .to_string(),
1705 ));
1706 }
1707
1708 let params = DeribitCancelParams {
1709 order_id: order_id.to_string(),
1710 };
1711
1712 log::debug!("Sending cancel order: order_id={order_id}, client_order_id={client_order_id}");
1713
1714 self.command_sender()
1715 .send(HandlerCommand::Cancel {
1716 params,
1717 client_order_id,
1718 trader_id,
1719 strategy_id,
1720 instrument_id,
1721 })
1722 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1723
1724 Ok(())
1725 }
1726
1727 pub async fn cancel_all_orders(
1738 &self,
1739 instrument_id: InstrumentId,
1740 order_type: Option<String>,
1741 ) -> DeribitWsResult<()> {
1742 if !self.is_authenticated() {
1743 return Err(DeribitWsError::Authentication(
1744 "Cancel all orders requires authentication. Call authenticate_session() first."
1745 .to_string(),
1746 ));
1747 }
1748
1749 let instrument_name = instrument_id.symbol.to_string();
1750 let params = DeribitCancelAllByInstrumentParams {
1751 instrument_name: instrument_name.clone(),
1752 order_type,
1753 };
1754
1755 log::debug!("Sending cancel_all_orders: instrument={instrument_name}");
1756
1757 self.command_sender()
1758 .send(HandlerCommand::CancelAllByInstrument {
1759 params,
1760 instrument_id,
1761 })
1762 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1763
1764 Ok(())
1765 }
1766
1767 pub async fn query_order(
1778 &self,
1779 order_id: &str,
1780 client_order_id: ClientOrderId,
1781 trader_id: TraderId,
1782 strategy_id: StrategyId,
1783 instrument_id: InstrumentId,
1784 ) -> DeribitWsResult<()> {
1785 if !self.is_authenticated() {
1786 return Err(DeribitWsError::Authentication(
1787 "Query order state requires authentication. Call authenticate_session() first."
1788 .to_string(),
1789 ));
1790 }
1791
1792 log::debug!("Sending query_order: order_id={order_id}, client_order_id={client_order_id}");
1793
1794 self.command_sender()
1795 .send(HandlerCommand::GetOrderState {
1796 order_id: order_id.to_string(),
1797 client_order_id,
1798 trader_id,
1799 strategy_id,
1800 instrument_id,
1801 })
1802 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1803
1804 Ok(())
1805 }
1806
1807 fn command_sender(&self) -> RwLockReadGuard<'_, CommandSender> {
1808 self.cmd_tx.read()
1809 }
1810}
1811
1812fn grouped_book_channel(
1813 instrument_id: InstrumentId,
1814 group: &str,
1815 depth: u32,
1816 interval: Option<DeribitUpdateInterval>,
1817) -> String {
1818 let interval = match interval {
1819 Some(DeribitUpdateInterval::Raw) | None => DeribitUpdateInterval::Ms100,
1820 Some(i) => i,
1821 };
1822
1823 let depth = if depth < 5 {
1824 1
1825 } else if depth < 15 {
1826 10
1827 } else {
1828 20
1829 };
1830
1831 format!(
1832 "book.{}.{group}.{depth}.{}",
1833 instrument_id.symbol,
1834 interval.as_str()
1835 )
1836}
1837
1838#[cfg(test)]
1839mod tests {
1840 use futures_util::StreamExt;
1841 use rstest::rstest;
1842
1843 use super::*;
1844
1845 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1846 async fn active_state_waits_for_sender_publication() {
1847 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1848 let addr = listener.local_addr().unwrap();
1849 let server = tokio::spawn(async move {
1850 let (socket, _) = listener.accept().await.unwrap();
1851 let mut socket = tokio_tungstenite::accept_async(socket).await.unwrap();
1852
1853 while matches!(socket.next().await, Some(Ok(_))) {}
1854 });
1855
1856 let mut ws_client = DeribitWebSocketClient::new_unauthenticated(
1857 Some(format!("ws://{addr}")),
1858 30,
1859 DeribitEnvironment::Testnet,
1860 )
1861 .unwrap();
1862 let observer = ws_client.clone();
1863 let sender = Arc::clone(&ws_client.cmd_tx);
1864 let (held_tx, held_rx) = tokio::sync::oneshot::channel();
1865
1866 let publication = tokio::task::spawn_blocking(move || {
1867 let guard = sender.read();
1868 held_tx.send(()).unwrap();
1869 let deadline = std::time::Instant::now() + Duration::from_secs(5);
1870
1871 while sender.try_read().is_some() {
1872 assert!(
1873 std::time::Instant::now() < deadline,
1874 "connect must queue its writer"
1875 );
1876 std::thread::yield_now();
1877 }
1878
1879 let active = observer.is_active();
1880 drop(guard);
1881 active
1882 });
1883
1884 held_rx.await.unwrap();
1885 ws_client.connect().await.unwrap();
1886 let active_before_publication = publication.await.unwrap();
1887 let active_after_publication = ws_client.is_active();
1888 ws_client.close().await.unwrap();
1889 server.abort();
1890
1891 assert!(!active_before_publication);
1892 assert!(active_after_publication);
1893 }
1894
1895 struct DropSignal(Arc<AtomicBool>);
1896
1897 impl Drop for DropSignal {
1898 fn drop(&mut self) {
1899 self.0.store(true, Ordering::Release);
1900 }
1901 }
1902
1903 #[tokio::test]
1904 async fn test_last_client_owner_drop_aborts_handler_task() {
1905 let client = DeribitWebSocketClient::new_unauthenticated(
1906 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
1907 30,
1908 DeribitEnvironment::Testnet,
1909 )
1910 .unwrap();
1911 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1912 let dropped = Arc::new(AtomicBool::new(false));
1913 let drop_signal = DropSignal(Arc::clone(&dropped));
1914 client
1915 .handler_tasks
1916 .spawn(async move {
1917 let _drop_signal = drop_signal;
1918 started_tx.send(()).expect("started receiver");
1919 std::future::pending::<()>().await;
1920 })
1921 .expect("handler task should register");
1922 started_rx.await.expect("handler task started");
1923 let clone = client.clone();
1924
1925 drop(client);
1926 assert!(!dropped.load(Ordering::Acquire));
1927 drop(clone);
1928
1929 tokio::time::timeout(Duration::from_secs(1), async {
1930 while !dropped.load(Ordering::Acquire) {
1931 tokio::task::yield_now().await;
1932 }
1933 })
1934 .await
1935 .expect("handler task aborted");
1936 }
1937
1938 #[rstest]
1939 #[case::depth_last(true)]
1940 #[case::deltas_last(false)]
1941 #[tokio::test]
1942 async fn test_book_depth_shared_subscription_lifetime(#[case] depth_last: bool) {
1943 let client = DeribitWebSocketClient::new_public(DeribitEnvironment::Testnet, None).unwrap();
1944 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1945 *client.cmd_tx.write() = tx;
1946 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1947 let channel = "book.BTC-PERPETUAL.none.10.100ms";
1948 client
1949 .subscribe_book_depth(instrument_id, "none", None)
1950 .await
1951 .unwrap();
1952 client
1953 .subscribe_book_depth(instrument_id, "none", None)
1954 .await
1955 .unwrap();
1956 client
1957 .subscribe_book_grouped(instrument_id, "none", 10, None)
1958 .await
1959 .unwrap();
1960
1961 assert_eq!(
1962 client
1963 .book_depth_subs
1964 .lock()
1965 .await
1966 .get_reference_count(channel),
1967 2
1968 );
1969 assert_eq!(client.subscriptions_state.get_reference_count(channel), 3);
1970
1971 let HandlerCommand::Subscribe { channels } = rx.try_recv().unwrap() else {
1972 panic!("expected subscribe");
1973 };
1974
1975 assert_eq!(channels, [channel]);
1976 assert!(rx.try_recv().is_err());
1977
1978 client
1979 .unsubscribe_book_depth(instrument_id, "none", None)
1980 .await
1981 .unwrap();
1982 assert_eq!(
1983 client
1984 .book_depth_subs
1985 .lock()
1986 .await
1987 .get_reference_count(channel),
1988 1
1989 );
1990 assert_eq!(client.subscriptions_state.get_reference_count(channel), 2);
1991 assert!(rx.try_recv().is_err());
1992
1993 if depth_last {
1994 client
1995 .unsubscribe_book_grouped(instrument_id, "none", 10, None)
1996 .await
1997 .unwrap();
1998 } else {
1999 client
2000 .unsubscribe_book_depth(instrument_id, "none", None)
2001 .await
2002 .unwrap();
2003 }
2004
2005 assert_eq!(
2006 client
2007 .book_depth_subs
2008 .lock()
2009 .await
2010 .get_reference_count(channel),
2011 usize::from(depth_last)
2012 );
2013 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
2014 assert!(rx.try_recv().is_err());
2015
2016 if depth_last {
2017 client
2018 .unsubscribe_book_depth(instrument_id, "none", None)
2019 .await
2020 .unwrap();
2021 } else {
2022 client
2023 .unsubscribe_book_grouped(instrument_id, "none", 10, None)
2024 .await
2025 .unwrap();
2026 }
2027
2028 let HandlerCommand::Unsubscribe { channels } = rx.try_recv().unwrap() else {
2029 panic!("expected unsubscribe");
2030 };
2031
2032 assert_eq!(channels, [channel]);
2033 assert_eq!(
2034 client
2035 .book_depth_subs
2036 .lock()
2037 .await
2038 .get_reference_count(channel),
2039 0
2040 );
2041 assert_eq!(client.subscriptions_state.get_reference_count(channel), 0);
2042 assert!(rx.try_recv().is_err());
2043 }
2044
2045 #[tokio::test]
2046 async fn test_book_depth_unsubscribe_without_reference_preserves_grouped_subscription() {
2047 let client = DeribitWebSocketClient::new_public(DeribitEnvironment::Testnet, None).unwrap();
2048 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2049 *client.cmd_tx.write() = tx;
2050 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2051 let channel = "book.BTC-PERPETUAL.none.10.100ms";
2052 client
2053 .subscribe_book_grouped(instrument_id, "none", 10, None)
2054 .await
2055 .unwrap();
2056
2057 let HandlerCommand::Subscribe { channels } = rx.try_recv().unwrap() else {
2058 panic!("expected subscribe");
2059 };
2060
2061 assert_eq!(channels, [channel]);
2062
2063 client
2064 .unsubscribe_book_depth(instrument_id, "none", None)
2065 .await
2066 .unwrap();
2067
2068 assert_eq!(
2069 client
2070 .book_depth_subs
2071 .lock()
2072 .await
2073 .get_reference_count(channel),
2074 0
2075 );
2076 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
2077 assert_eq!(client.subscriptions_state.all_topics(), [channel]);
2078 assert!(rx.try_recv().is_err());
2079 }
2080
2081 #[tokio::test]
2082 async fn test_book_depth_subscribe_send_failure_restores_intent() {
2083 let client = DeribitWebSocketClient::new_public(DeribitEnvironment::Testnet, None).unwrap();
2084 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2085 let error = client
2086 .subscribe_book_depth(instrument_id, "none", None)
2087 .await
2088 .unwrap_err();
2089
2090 assert!(matches!(error, DeribitWsError::Send(_)));
2091 assert_eq!(
2092 client
2093 .book_depth_subs
2094 .lock()
2095 .await
2096 .get_reference_count("book.BTC-PERPETUAL.none.10.100ms"),
2097 0
2098 );
2099 assert_eq!(
2100 client
2101 .subscriptions_state
2102 .get_reference_count("book.BTC-PERPETUAL.none.10.100ms"),
2103 0
2104 );
2105 }
2106
2107 #[tokio::test]
2108 async fn test_book_depth_unsubscribe_send_failure_preserves_intent() {
2109 let client = DeribitWebSocketClient::new_public(DeribitEnvironment::Testnet, None).unwrap();
2110 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2111 let channel = "book.BTC-PERPETUAL.none.10.100ms";
2112 client.book_depth_subs.lock().await.add_reference(channel);
2113 client.subscriptions_state.add_reference(channel);
2114 client.subscriptions_state.mark_subscribe(channel);
2115 client.subscriptions_state.confirm_subscribe(channel);
2116 let error = client
2117 .unsubscribe_book_depth(instrument_id, "none", None)
2118 .await
2119 .unwrap_err();
2120
2121 assert!(matches!(error, DeribitWsError::Send(_)));
2122 assert_eq!(
2123 client
2124 .book_depth_subs
2125 .lock()
2126 .await
2127 .get_reference_count(channel),
2128 1
2129 );
2130 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
2131 assert_eq!(client.subscriptions_state.all_topics(), [channel]);
2132 }
2133
2134 #[rstest]
2135 #[tokio::test]
2136 async fn test_unsubscribe_send_failure_restores_subscription() {
2137 let client = DeribitWebSocketClient::new_unauthenticated(
2138 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
2139 30,
2140 DeribitEnvironment::Testnet,
2141 )
2142 .unwrap();
2143 let channel = "trades.BTC-PERPETUAL.raw";
2144 client.subscriptions_state.add_reference(channel);
2145 client.subscriptions_state.mark_subscribe(channel);
2146 client.subscriptions_state.confirm_subscribe(channel);
2147
2148 let error = client
2149 .send_unsubscribe(vec![channel.to_string()])
2150 .await
2151 .unwrap_err();
2152
2153 assert!(matches!(error, DeribitWsError::Send(_)));
2154 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
2155 assert_eq!(client.subscriptions_state.len(), 1);
2156 assert_eq!(client.subscriptions_state.all_topics(), [channel]);
2157 assert!(
2158 client
2159 .subscriptions_state
2160 .pending_subscribe_topics()
2161 .is_empty()
2162 );
2163 assert!(
2164 client
2165 .subscriptions_state
2166 .pending_unsubscribe_topics()
2167 .is_empty()
2168 );
2169 }
2170}