1use std::{borrow::Cow, fmt::Debug};
22
23use nautilus_core::{
24 serialization::{
25 decimal, deserialize_decimal_token_borrowed, deserialize_optional_decimal_token_borrowed,
26 },
27 string::secret::{REDACTED, SecretString},
28};
29use nautilus_model::{
30 data::{
31 Data, FundingRateUpdate, InstrumentStatus, OrderBookDeltas, greeks::OptionGreekValues,
32 option_chain::OptionGreeks,
33 },
34 events::{
35 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired, OrderFilled,
36 OrderModifyRejected, OrderRejected, OrderUpdated,
37 },
38 instruments::InstrumentAny,
39 reports::{FillReport, OrderStatusReport},
40};
41use rust_decimal::{Decimal, prelude::ToPrimitive};
42use serde::{Deserialize, Deserializer, Serialize, de};
43use serde_json::value::RawValue;
44use ustr::Ustr;
45use zeroize::{Zeroize, ZeroizeOnDrop};
46
47use super::enums::{DeribitBookAction, DeribitBookMsgType, DeribitHeartbeatType};
48pub use crate::common::{
49 enums::DeribitInstrumentState,
50 rpc::{DeribitJsonRpcError, DeribitJsonRpcRequest, DeribitJsonRpcResponse},
51};
52use crate::{
53 common::{models::DeribitTradeLeg, serialization::deserialize_decimal_token_or_zero_borrowed},
54 websocket::error::DeribitWsError,
55};
56
57#[derive(Debug, Clone, Deserialize)]
59pub struct DeribitSubscriptionNotification<T> {
60 pub jsonrpc: String,
62 pub method: String,
64 pub params: DeribitSubscriptionParams<T>,
66}
67
68#[derive(Debug, Clone, Deserialize)]
70pub struct DeribitSubscriptionParams<T> {
71 pub channel: String,
73 pub data: T,
75}
76
77#[derive(Debug, Clone, Serialize, Zeroize)]
79pub struct DeribitAuthParams {
80 pub grant_type: String,
82 pub client_id: SecretString,
84 pub timestamp: u64,
86 pub signature: SecretString,
88 pub nonce: String,
90 pub data: SecretString,
92 #[serde(skip_serializing_if = "Option::is_none")]
96 pub scope: Option<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Zeroize)]
101pub struct DeribitRefreshTokenParams {
102 pub grant_type: String,
104 pub refresh_token: SecretString,
106}
107
108#[derive(Debug, Clone, Deserialize, Zeroize, ZeroizeOnDrop)]
110pub struct DeribitAuthResult {
111 pub access_token: SecretString,
113 pub expires_in: u64,
115 pub refresh_token: SecretString,
117 pub scope: String,
119 pub token_type: String,
121 #[serde(default)]
123 pub enabled_features: Vec<String>,
124}
125
126#[derive(Debug, Clone, Serialize)]
128pub struct DeribitSubscribeParams {
129 pub channels: Vec<String>,
131}
132
133#[derive(Debug, Clone, Deserialize)]
135pub struct DeribitSubscribeResult(pub Vec<String>);
136
137#[derive(Debug, Clone, Serialize)]
139pub struct DeribitHeartbeatParams {
140 pub interval: u64,
142}
143
144#[derive(Debug, Clone, Deserialize)]
146pub struct DeribitHeartbeatData {
147 #[serde(rename = "type")]
149 pub heartbeat_type: DeribitHeartbeatType,
150}
151
152#[derive(Debug, Clone, Deserialize)]
154pub struct DeribitTradeMsg {
155 pub trade_id: String,
157 pub instrument_name: Ustr,
159 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
161 pub price: Decimal,
162 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
164 pub amount: Decimal,
165 pub direction: String,
167 pub timestamp: u64,
169 pub trade_seq: u64,
171 pub tick_direction: i8,
173 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
175 pub index_price: Decimal,
176 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
178 pub mark_price: Decimal,
179 #[serde(
181 default,
182 deserialize_with = "deserialize_optional_decimal_token_borrowed"
183 )]
184 pub iv: Option<Decimal>,
185 pub liquidation: Option<String>,
187 pub combo_trade_id: Option<String>,
189 pub block_trade_id: Option<String>,
191 #[serde(default)]
193 pub block_rfq_id: Option<i64>,
194 pub combo_id: Option<String>,
196 #[serde(default)]
198 pub legs: Option<Vec<DeribitTradeLeg>>,
199}
200
201#[derive(Debug, Clone, Deserialize)]
206pub struct DeribitBookMsg<'a> {
207 #[serde(rename = "type", default = "default_book_msg_type")]
209 pub msg_type: DeribitBookMsgType,
210 pub instrument_name: Ustr,
212 pub timestamp: u64,
214 pub change_id: u64,
216 pub prev_change_id: Option<u64>,
218 #[serde(borrow)]
220 pub bids: Vec<Vec<&'a RawValue>>,
221 #[serde(borrow)]
223 pub asks: Vec<Vec<&'a RawValue>>,
224}
225
226fn default_book_msg_type() -> DeribitBookMsgType {
228 DeribitBookMsgType::Snapshot
229}
230
231#[derive(Debug, Clone)]
233pub struct DeribitBookLevel {
234 pub price: Decimal,
236 pub amount: Decimal,
238 pub action: Option<DeribitBookAction>,
240}
241
242#[derive(Debug, Clone, Deserialize)]
244pub struct DeribitTickerMsg {
245 pub instrument_name: Ustr,
247 pub timestamp: u64,
249 #[serde(
251 default,
252 deserialize_with = "deserialize_optional_decimal_token_borrowed"
253 )]
254 pub best_bid_price: Option<Decimal>,
255 #[serde(
257 default,
258 deserialize_with = "deserialize_optional_decimal_token_borrowed"
259 )]
260 pub best_bid_amount: Option<Decimal>,
261 #[serde(
263 default,
264 deserialize_with = "deserialize_optional_decimal_token_borrowed"
265 )]
266 pub best_ask_price: Option<Decimal>,
267 #[serde(
269 default,
270 deserialize_with = "deserialize_optional_decimal_token_borrowed"
271 )]
272 pub best_ask_amount: Option<Decimal>,
273 #[serde(
275 default,
276 deserialize_with = "deserialize_optional_decimal_token_borrowed"
277 )]
278 pub last_price: Option<Decimal>,
279 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
281 pub mark_price: Decimal,
282 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
284 pub index_price: Decimal,
285 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
287 pub open_interest: Decimal,
288 #[serde(
290 default,
291 deserialize_with = "deserialize_optional_decimal_token_borrowed"
292 )]
293 pub current_funding: Option<Decimal>,
294 #[serde(
296 default,
297 deserialize_with = "deserialize_optional_decimal_token_borrowed"
298 )]
299 pub funding_8h: Option<Decimal>,
300 #[serde(
302 default,
303 deserialize_with = "deserialize_optional_decimal_token_borrowed"
304 )]
305 pub settlement_price: Option<Decimal>,
306 #[serde(
308 default,
309 deserialize_with = "deserialize_optional_decimal_token_borrowed"
310 )]
311 pub volume: Option<Decimal>,
312 #[serde(
314 default,
315 deserialize_with = "deserialize_optional_decimal_token_borrowed"
316 )]
317 pub volume_usd: Option<Decimal>,
318 #[serde(
320 default,
321 deserialize_with = "deserialize_optional_decimal_token_borrowed"
322 )]
323 pub high: Option<Decimal>,
324 #[serde(
326 default,
327 deserialize_with = "deserialize_optional_decimal_token_borrowed"
328 )]
329 pub low: Option<Decimal>,
330 #[serde(
332 default,
333 deserialize_with = "deserialize_optional_decimal_token_borrowed"
334 )]
335 pub price_change: Option<Decimal>,
336 pub state: String,
338 pub greeks: Option<DeribitGreeks>,
341 #[serde(
343 default,
344 deserialize_with = "deserialize_optional_decimal_token_borrowed"
345 )]
346 pub mark_iv: Option<Decimal>,
347 #[serde(
349 default,
350 deserialize_with = "deserialize_optional_decimal_token_borrowed"
351 )]
352 pub bid_iv: Option<Decimal>,
353 #[serde(
355 default,
356 deserialize_with = "deserialize_optional_decimal_token_borrowed"
357 )]
358 pub ask_iv: Option<Decimal>,
359 #[serde(
361 default,
362 deserialize_with = "deserialize_optional_decimal_token_borrowed"
363 )]
364 pub underlying_price: Option<Decimal>,
365 pub underlying_index: Option<String>,
367}
368
369#[derive(Debug, Clone, Deserialize)]
371pub struct DeribitGreeks {
372 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
373 pub delta: Decimal,
374 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
375 pub gamma: Decimal,
376 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
377 pub vega: Decimal,
378 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
379 pub theta: Decimal,
380 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
381 pub rho: Decimal,
382}
383
384impl DeribitGreeks {
385 pub fn to_greek_values(&self) -> OptionGreekValues {
387 OptionGreekValues {
388 delta: self.delta.to_f64().unwrap_or(0.0),
389 gamma: self.gamma.to_f64().unwrap_or(0.0),
390 vega: self.vega.to_f64().unwrap_or(0.0),
391 theta: self.theta.to_f64().unwrap_or(0.0),
392 rho: self.rho.to_f64().unwrap_or(0.0),
393 }
394 }
395}
396
397#[derive(Debug, Clone, Deserialize)]
399pub struct DeribitQuoteMsg {
400 pub instrument_name: Ustr,
402 pub timestamp: u64,
404 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
406 pub best_bid_price: Decimal,
407 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
409 pub best_bid_amount: Decimal,
410 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
412 pub best_ask_price: Decimal,
413 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
415 pub best_ask_amount: Decimal,
416}
417
418#[derive(Debug, Clone, Deserialize)]
423pub struct DeribitInstrumentStateMsg {
424 pub instrument_name: Ustr,
426 pub state: DeribitInstrumentState,
428 pub timestamp: u64,
430}
431
432#[derive(Debug, Clone, Deserialize)]
438pub struct DeribitPerpetualMsg {
439 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
441 pub index_price: Decimal,
442 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
444 pub interest: Decimal,
445 pub timestamp: u64,
447}
448
449#[derive(Debug, Clone, Deserialize)]
451pub struct DeribitVolatilityIndexMsg {
452 pub timestamp: u64,
454 pub volatility: f64,
456 pub index_name: String,
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
465#[serde(rename_all = "lowercase")]
466pub enum DeribitChartStatus {
467 #[default]
469 Ok,
470 Imputed,
472}
473
474#[derive(Debug, Clone, Deserialize)]
476pub struct DeribitChartMsg {
477 pub tick: u64,
479 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
481 pub open: Decimal,
482 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
484 pub high: Decimal,
485 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
487 pub low: Decimal,
488 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
490 pub close: Decimal,
491 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
493 pub volume: Decimal,
494 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
496 pub cost: Decimal,
497 #[serde(default)]
499 pub status: DeribitChartStatus,
500}
501
502#[derive(Debug, Clone, Serialize)]
507pub struct DeribitOrderParams {
508 pub instrument_name: String,
510 #[serde(serialize_with = "decimal::serialize")]
512 pub amount: Decimal,
513 #[serde(rename = "type")]
515 pub order_type: String,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub label: Option<String>,
519 #[serde(
521 skip_serializing_if = "Option::is_none",
522 serialize_with = "decimal::serialize_optional"
523 )]
524 pub price: Option<Decimal>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub time_in_force: Option<String>,
528 #[serde(skip_serializing_if = "Option::is_none")]
531 pub post_only: Option<bool>,
532 #[serde(skip_serializing_if = "Option::is_none")]
535 pub reject_post_only: Option<bool>,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 pub reduce_only: Option<bool>,
539 #[serde(
541 skip_serializing_if = "Option::is_none",
542 serialize_with = "decimal::serialize_optional"
543 )]
544 pub trigger_price: Option<Decimal>,
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub trigger: Option<String>,
548 #[serde(
550 skip_serializing_if = "Option::is_none",
551 serialize_with = "decimal::serialize_optional"
552 )]
553 pub max_show: Option<Decimal>,
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub valid_until: Option<u64>,
557}
558
559#[derive(Debug, Clone, Serialize)]
561pub struct DeribitCancelParams {
562 pub order_id: String,
564}
565
566#[derive(Debug, Clone, Serialize)]
568pub struct DeribitCancelAllByInstrumentParams {
569 pub instrument_name: String,
571 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
573 pub order_type: Option<String>,
574}
575
576#[derive(Debug, Clone, Serialize)]
581pub struct DeribitEditParams {
582 pub order_id: String,
584 #[serde(serialize_with = "decimal::serialize")]
586 pub amount: Decimal,
587 #[serde(
589 skip_serializing_if = "Option::is_none",
590 serialize_with = "decimal::serialize_optional"
591 )]
592 pub price: Option<Decimal>,
593 #[serde(
595 skip_serializing_if = "Option::is_none",
596 serialize_with = "decimal::serialize_optional"
597 )]
598 pub trigger_price: Option<Decimal>,
599 #[serde(skip_serializing_if = "Option::is_none")]
602 pub post_only: Option<bool>,
603 #[serde(skip_serializing_if = "Option::is_none")]
606 pub reject_post_only: Option<bool>,
607 #[serde(skip_serializing_if = "Option::is_none")]
609 pub reduce_only: Option<bool>,
610}
611
612#[derive(Debug, Clone, Serialize)]
614pub struct DeribitGetOrderStateParams {
615 pub order_id: String,
617}
618
619fn deserialize_optional_decimal_or_market<'de, D>(
624 deserializer: D,
625) -> Result<Option<Decimal>, D::Error>
626where
627 D: Deserializer<'de>,
628{
629 match Option::<&'de RawValue>::deserialize(deserializer)? {
630 Some(raw) if raw.get() == "\"market_price\"" => Ok(None),
631 Some(raw) => deserialize_optional_decimal_token_borrowed(raw)
632 .map_err(|_| de::Error::custom("expected a decimal or market_price")),
633 None => Ok(None),
634 }
635}
636
637#[derive(Debug, Clone, Deserialize)]
641pub struct DeribitOrderResponse {
642 pub order: DeribitOrderMsg,
644 #[serde(default)]
646 pub trades: Vec<DeribitUserTradeMsg>,
647}
648
649#[derive(Debug, Clone, Deserialize)]
653pub struct DeribitOrderMsg {
654 pub order_id: String,
656 pub label: Option<String>,
658 pub instrument_name: Ustr,
660 pub direction: String,
662 pub order_type: String,
664 pub order_state: String,
666 #[serde(default)]
668 pub replaced: bool,
669 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_market")]
672 pub price: Option<Decimal>,
673 #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
675 pub amount: Decimal,
676 #[serde(
679 default,
680 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
681 )]
682 pub filled_amount: Decimal,
683 #[serde(
685 default,
686 deserialize_with = "deserialize_optional_decimal_token_borrowed"
687 )]
688 pub average_price: Option<Decimal>,
689 pub creation_timestamp: u64,
691 pub last_update_timestamp: u64,
693 pub time_in_force: String,
695 #[serde(
697 default,
698 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
699 )]
700 pub commission: Decimal,
701 #[serde(default)]
703 pub post_only: bool,
704 #[serde(default)]
706 pub reduce_only: bool,
707 #[serde(
709 default,
710 deserialize_with = "deserialize_optional_decimal_token_borrowed"
711 )]
712 pub trigger_price: Option<Decimal>,
713 pub trigger: Option<String>,
715 #[serde(
717 default,
718 deserialize_with = "deserialize_optional_decimal_token_borrowed"
719 )]
720 pub max_show: Option<Decimal>,
721 #[serde(default)]
723 pub api: bool,
724 pub reject_reason: Option<String>,
726 pub cancel_reason: Option<String>,
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct DeribitUserTradeMsg {
735 pub trade_id: String,
737 pub order_id: String,
739 pub instrument_name: Ustr,
741 pub direction: String,
743 #[serde(
745 serialize_with = "decimal::serialize",
746 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
747 )]
748 pub price: Decimal,
749 #[serde(
751 serialize_with = "decimal::serialize",
752 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
753 )]
754 pub amount: Decimal,
755 #[serde(
757 serialize_with = "decimal::serialize",
758 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
759 )]
760 pub fee: Decimal,
761 pub fee_currency: String,
763 pub timestamp: u64,
765 pub trade_seq: u64,
767 pub liquidity: String,
769 pub order_type: String,
771 #[serde(
773 serialize_with = "decimal::serialize",
774 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
775 )]
776 pub index_price: Decimal,
777 #[serde(
779 serialize_with = "decimal::serialize",
780 deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
781 )]
782 pub mark_price: Decimal,
783 pub tick_direction: i8,
785 pub state: String,
787 pub label: Option<String>,
789 #[serde(default)]
791 pub reduce_only: bool,
792 #[serde(default)]
794 pub post_only: bool,
795 #[serde(default)]
797 pub liquidation: Option<String>,
798 #[serde(
800 default,
801 serialize_with = "decimal::serialize_optional",
802 deserialize_with = "deserialize_optional_decimal_token_borrowed"
803 )]
804 pub profit_loss: Option<Decimal>,
805}
806
807#[derive(Debug, Clone, Deserialize)]
809pub struct DeribitPortfolioMsg {
810 pub currency: String,
812 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
814 pub equity: Decimal,
815 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
817 pub balance: Decimal,
818 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
820 pub available_funds: Decimal,
821 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
823 pub margin_balance: Decimal,
824 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
826 pub initial_margin: Decimal,
827 #[serde(deserialize_with = "deserialize_decimal_token_borrowed")]
829 pub maintenance_margin: Decimal,
830 #[serde(default)]
832 pub margin_model: Option<String>,
833 #[serde(default)]
835 pub cross_collateral_enabled: Option<bool>,
836 #[serde(
838 default,
839 deserialize_with = "deserialize_optional_decimal_token_borrowed"
840 )]
841 pub available_withdrawal_funds: Option<Decimal>,
842}
843
844#[derive(Clone)]
849pub enum DeribitWsMessage {
850 Response(DeribitJsonRpcResponse<Box<RawValue>>),
852 Notification(DeribitSubscriptionNotification<Box<RawValue>>),
854 Heartbeat(DeribitHeartbeatData),
856 Error(DeribitJsonRpcError),
858 Reconnected,
860}
861
862impl Debug for DeribitWsMessage {
863 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
864 match self {
865 Self::Response(response)
866 if response.result.as_ref().is_some_and(|result| {
867 serde_json::from_str::<DeribitAuthTokenKeys>(result.get()).is_ok_and(|keys| {
868 keys.access_token.is_some() || keys.refresh_token.is_some()
869 })
870 }) =>
871 {
872 f.debug_tuple("Response").field(&REDACTED).finish()
873 }
874 Self::Response(response) => f.debug_tuple("Response").field(response).finish(),
875 Self::Notification(notification) => {
876 f.debug_tuple("Notification").field(notification).finish()
877 }
878 Self::Heartbeat(heartbeat) => f.debug_tuple("Heartbeat").field(heartbeat).finish(),
879 Self::Error(error) => f.debug_tuple("Error").field(error).finish(),
880 Self::Reconnected => f.write_str("Reconnected"),
881 }
882 }
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize)]
887pub struct DeribitWebSocketError {
888 pub code: i64,
890 pub message: String,
892 pub timestamp: u64,
894}
895
896impl From<DeribitJsonRpcError> for DeribitWebSocketError {
897 fn from(err: DeribitJsonRpcError) -> Self {
898 Self {
899 code: err.code,
900 message: err.message,
901 timestamp: 0,
902 }
903 }
904}
905
906#[derive(Debug, Clone)]
908pub enum NautilusWsMessage {
909 Data(Vec<Data>),
911 Deltas(OrderBookDeltas),
913 Instrument(Box<InstrumentAny>),
915 FundingRates(Vec<FundingRateUpdate>),
917 OptionGreeks(OptionGreeks),
919 OrderStatusReports(Vec<OrderStatusReport>),
921 FillReports(Vec<FillReport>),
923 OrderFilled(OrderFilled),
925 OrderAccepted(OrderAccepted),
927 OrderCanceled(OrderCanceled),
929 OrderExpired(OrderExpired),
931 OrderRejected(OrderRejected),
933 OrderCancelRejected(OrderCancelRejected),
935 OrderModifyRejected(OrderModifyRejected),
937 OrderUpdated(OrderUpdated),
939 AccountState(AccountState),
941 InstrumentStatus(InstrumentStatus),
943 Error(DeribitWsError),
945 Raw(Box<RawValue>),
947 Reconnected,
949 Authenticated(Box<DeribitAuthResult>),
951 AuthenticationFailed(String),
953}
954
955pub fn parse_raw_message(text: &str) -> Result<DeribitWsMessage, DeribitWsError> {
961 let header: DeribitWsHeader =
962 serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
963
964 match header.method.as_deref() {
965 Some("subscription") => {
966 let notification: DeribitSubscriptionNotification<Box<RawValue>> =
967 serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
968 return Ok(DeribitWsMessage::Notification(notification));
969 }
970 Some("heartbeat") => {
971 let heartbeat: DeribitHeartbeatNotification =
972 serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
973
974 if let Some(params) = heartbeat.params {
975 return Ok(DeribitWsMessage::Heartbeat(params));
976 }
977 }
978 _ => {}
979 }
980
981 let response: DeribitJsonRpcResponse<Box<RawValue>> =
985 serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
986 Ok(DeribitWsMessage::Response(response))
987}
988
989#[derive(Deserialize)]
990struct DeribitWsHeader<'a> {
991 #[serde(borrow, default)]
992 method: Option<Cow<'a, str>>,
993}
994
995#[derive(Deserialize)]
996struct DeribitHeartbeatNotification {
997 #[serde(default)]
998 params: Option<DeribitHeartbeatData>,
999}
1000
1001#[derive(Deserialize)]
1002struct DeribitAuthTokenKeys {
1003 access_token: Option<de::IgnoredAny>,
1004 refresh_token: Option<de::IgnoredAny>,
1005}
1006
1007pub fn extract_instrument_from_channel(channel: &str) -> Option<&str> {
1011 let parts: Vec<&str> = channel.split('.').collect();
1012 if parts.len() >= 2 {
1013 Some(parts[1])
1014 } else {
1015 None
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use rstest::rstest;
1022
1023 use super::*;
1024
1025 #[rstest]
1026 #[case(f64::NAN)]
1027 #[case(f64::INFINITY)]
1028 #[case(f64::NEG_INFINITY)]
1029 fn test_order_price_rejects_non_finite_floats(#[case] value: f64) {
1030 let value = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(value);
1031 assert!(deserialize_optional_decimal_or_market(value).is_err());
1032 }
1033
1034 #[rstest]
1035 #[case(serde_json::json!("market_price"), None)]
1036 #[case(serde_json::json!(""), None)]
1037 #[case(serde_json::Value::Null, None)]
1038 #[case(serde_json::json!("0.1234567890123456789012345678"), Some(Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()))]
1039 #[case(serde_json::json!(9007199254740993u64), Some(Decimal::from(9_007_199_254_740_993u64)))]
1040 fn test_order_price_routes(
1041 #[case] price: serde_json::Value,
1042 #[case] expected: Option<Decimal>,
1043 ) {
1044 let mut response: serde_json::Value = serde_json::from_str(include_str!(
1045 "../../test_data/ws_order_stop_market_response.json"
1046 ))
1047 .unwrap();
1048 response["result"]["order"]["price"] = price;
1049 let order: DeribitOrderMsg =
1050 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
1051
1052 assert_eq!(order.price, expected);
1053 }
1054
1055 #[rstest]
1056 #[case("abc")]
1057 #[case("MARKET_PRICE")]
1058 fn test_order_price_rejects_invalid_strings(#[case] price: &str) {
1059 let mut response: serde_json::Value = serde_json::from_str(include_str!(
1060 "../../test_data/ws_order_stop_market_response.json"
1061 ))
1062 .unwrap();
1063 response["result"]["order"]["price"] = serde_json::json!(price);
1064 let error =
1065 serde_json::from_str::<DeribitOrderMsg>(&response["result"]["order"].to_string())
1066 .unwrap_err();
1067
1068 assert!(
1069 error
1070 .to_string()
1071 .contains("expected a decimal or market_price")
1072 );
1073 }
1074
1075 #[rstest]
1076 #[case(None, "55.00055")]
1077 #[case(
1078 Some("0.12345678901234567890123456789"),
1079 "0.1234567890123456789012345679"
1080 )]
1081 fn test_portfolio_decimal_routes(#[case] equity: Option<&str>, #[case] expected: &str) {
1082 let response: serde_json::Value =
1083 serde_json::from_str(include_str!("../../test_data/ws_portfolio.json")).unwrap();
1084 let mut data = response["params"]["data"].clone();
1085
1086 if let Some(equity) = equity {
1087 data["equity"] = serde_json::json!(equity);
1088 }
1089
1090 let portfolio: DeribitPortfolioMsg = serde_json::from_str(&data.to_string()).unwrap();
1091 assert_eq!(portfolio.currency, "USDT");
1092 assert_eq!(portfolio.equity, Decimal::from_str_exact(expected).unwrap());
1093 assert_eq!(portfolio.balance, Decimal::new(5500055, 5));
1094 assert_eq!(portfolio.available_funds, Decimal::new(53868247, 6));
1095 assert_eq!(portfolio.margin_balance, Decimal::new(54968258, 6));
1096 assert_eq!(portfolio.initial_margin, Decimal::new(1100011, 6));
1097 assert_eq!(portfolio.maintenance_margin, Decimal::ZERO);
1098 assert_eq!(portfolio.margin_model.as_deref(), Some("cross_sm"));
1099 assert_eq!(portfolio.cross_collateral_enabled, Some(true));
1100 assert_eq!(
1101 portfolio.available_withdrawal_funds,
1102 Some(Decimal::new(54968257, 6))
1103 );
1104
1105 for invalid in [
1106 serde_json::Value::Null,
1107 serde_json::json!(""),
1108 serde_json::json!(true),
1109 ] {
1110 let mut data = data.clone();
1111 data["equity"] = invalid;
1112 assert!(serde_json::from_str::<DeribitPortfolioMsg>(&data.to_string()).is_err());
1113 }
1114 }
1115
1116 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
1117
1118 #[rstest]
1119 fn auth_messages_preserve_wire_values_and_redact_debug() {
1120 let params = DeribitAuthParams {
1121 grant_type: "client_signature".to_string(),
1122 client_id: SecretString::from("client-id-value"),
1123 timestamp: 1_700_000_000_000,
1124 signature: SecretString::from("signature-value"),
1125 nonce: "nonce-value".to_string(),
1126 data: SecretString::from("data-value"),
1127 scope: Some("session:test".to_string()),
1128 };
1129 let refresh = DeribitRefreshTokenParams {
1130 grant_type: "refresh_token".to_string(),
1131 refresh_token: SecretString::from("refresh-token-value"),
1132 };
1133
1134 let params_json = serde_json::to_value(¶ms).unwrap();
1135 let refresh_json = serde_json::to_value(&refresh).unwrap();
1136 let formatted = format!("{params:?} {refresh:?}");
1137
1138 assert_eq!(params_json["client_id"], "client-id-value");
1139 assert_eq!(params_json["signature"], "signature-value");
1140 assert_eq!(params_json["data"], "data-value");
1141 assert_eq!(refresh_json["refresh_token"], "refresh-token-value");
1142 assert!(!formatted.contains("client-id-value"));
1143 assert!(!formatted.contains("signature-value"));
1144 assert!(!formatted.contains("data-value"));
1145 assert!(!formatted.contains("refresh-token-value"));
1146
1147 let DeribitAuthParams {
1148 client_id,
1149 signature,
1150 data,
1151 ..
1152 } = params;
1153 let DeribitRefreshTokenParams { refresh_token, .. } = refresh;
1154 assert_eq!(client_id.expose_secret(), "client-id-value");
1155 assert_eq!(signature.expose_secret(), "signature-value");
1156 assert_eq!(data.expose_secret(), "data-value");
1157 assert_eq!(refresh_token.expose_secret(), "refresh-token-value");
1158 }
1159
1160 #[rstest]
1161 fn auth_result_zeroizes_on_drop() {
1162 assert_zeroize_on_drop::<DeribitAuthResult>();
1163
1164 let result = DeribitAuthResult {
1165 access_token: SecretString::from("access-token-value"),
1166 expires_in: 900,
1167 refresh_token: SecretString::from("refresh-token-value"),
1168 scope: "session:test".to_string(),
1169 token_type: "bearer".to_string(),
1170 enabled_features: vec!["feature".to_string()],
1171 };
1172 let formatted = format!("{result:?}");
1173
1174 assert_eq!(formatted.matches(REDACTED).count(), 2);
1175 assert!(!formatted.contains(result.access_token.expose_secret()));
1176 assert!(!formatted.contains(result.refresh_token.expose_secret()));
1177 }
1178
1179 #[rstest]
1180 fn test_parse_subscription_notification() {
1181 let json = r#"{
1182 "jsonrpc": "2.0",
1183 "method": "subscription",
1184 "params": {
1185 "channel": "trades.BTC-PERPETUAL.raw",
1186 "data": [{"trade_id": "123", "price": 50000.0}]
1187 }
1188 }"#;
1189
1190 let msg = parse_raw_message(json).unwrap();
1191 assert!(matches!(msg, DeribitWsMessage::Notification(_)));
1192 }
1193
1194 #[rstest]
1195 fn test_parse_response() {
1196 let json = r#"{
1197 "jsonrpc": "2.0",
1198 "id": 1,
1199 "result": ["trades.BTC-PERPETUAL.raw"],
1200 "testnet": true,
1201 "usIn": 1234567890,
1202 "usOut": 1234567891,
1203 "usDiff": 1
1204 }"#;
1205
1206 let msg = parse_raw_message(json).unwrap();
1207 assert!(matches!(msg, DeribitWsMessage::Response(_)));
1208 }
1209
1210 #[rstest]
1211 #[case::plain_keys("access_token", "refresh_token")]
1212 #[case::escaped_keys(r"access\u005ftoken", r"refresh\u005ftoken")]
1213 fn test_auth_response_debug_redacts_tokens(
1214 #[case] access_key: &str,
1215 #[case] refresh_key: &str,
1216 ) {
1217 let access_token = "access-token-value";
1218 let refresh_token = "refresh-token-value";
1219 let json = format!(
1220 r#"{{
1221 "jsonrpc": "2.0",
1222 "id": 1,
1223 "result": {{
1224 "{access_key}": "{access_token}",
1225 "{refresh_key}": "{refresh_token}"
1226 }}
1227 }}"#,
1228 );
1229
1230 let msg = parse_raw_message(&json).unwrap();
1231 let debug = format!("{msg:?}");
1232
1233 assert!(debug.contains(REDACTED));
1234 assert!(!debug.contains(access_token));
1235 assert!(!debug.contains(refresh_token));
1236 }
1237
1238 #[rstest]
1239 fn test_parse_error_response() {
1240 let json = r#"{
1243 "jsonrpc": "2.0",
1244 "id": 1,
1245 "error": {
1246 "code": 10028,
1247 "message": "too_many_requests"
1248 }
1249 }"#;
1250
1251 let msg = parse_raw_message(json).unwrap();
1252 match msg {
1253 DeribitWsMessage::Response(resp) => {
1254 assert!(resp.error.is_some());
1255 let error = resp.error.unwrap();
1256 assert_eq!(error.code, 10028);
1257 assert_eq!(error.message, "too_many_requests");
1258 }
1259 _ => panic!("Expected Response with error, was {msg:?}"),
1260 }
1261 }
1262
1263 #[rstest]
1264 fn test_extract_instrument_from_channel() {
1265 assert_eq!(
1266 extract_instrument_from_channel("trades.BTC-PERPETUAL.raw"),
1267 Some("BTC-PERPETUAL")
1268 );
1269 assert_eq!(
1270 extract_instrument_from_channel("book.ETH-25DEC25.raw"),
1271 Some("ETH-25DEC25")
1272 );
1273 assert_eq!(extract_instrument_from_channel("platform_state"), None);
1274 }
1275
1276 #[rstest]
1277 fn test_parse_volatility_index_payload() {
1278 let value = serde_json::json!({
1279 "timestamp": 1619777946007_u64,
1280 "volatility": 129.36_f64,
1281 "index_name": "btc_usd",
1282 });
1283
1284 let payload: DeribitVolatilityIndexMsg = serde_json::from_value(value).unwrap();
1285 assert_eq!(payload.index_name, "btc_usd");
1286 assert_eq!(payload.volatility, 129.36);
1287 assert_eq!(payload.timestamp, 1619777946007_u64);
1288 }
1289}