Skip to main content

deribit_websocket/model/
ws_types.rs

1//! WebSocket-specific types and models for Deribit API
2//!
3//! This module contains data structures specific to WebSocket communication,
4//! including JSON-RPC message types, connection states, and WebSocket-specific
5//! request/response structures.
6
7use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10/// WebSocket message types for JSON-RPC communication
11#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
12pub enum WebSocketMessage {
13    /// JSON-RPC request message
14    Request(JsonRpcRequest),
15    /// JSON-RPC response message
16    Response(JsonRpcResponse),
17    /// JSON-RPC notification message (no response expected)
18    Notification(JsonRpcNotification),
19}
20
21/// JSON-RPC 2.0 request structure
22#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
23pub struct JsonRpcRequest {
24    /// JSON-RPC version (always "2.0")
25    pub jsonrpc: String,
26    /// Request identifier for correlation with response
27    pub id: serde_json::Value,
28    /// Method name to call
29    pub method: String,
30    /// Optional parameters for the method
31    pub params: Option<serde_json::Value>,
32}
33
34/// JSON-RPC 2.0 response structure
35#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
36pub struct JsonRpcResponse {
37    /// JSON-RPC version (always "2.0")
38    pub jsonrpc: String,
39    /// Request identifier for correlation
40    pub id: serde_json::Value,
41    /// Result or error information
42    #[serde(flatten)]
43    pub result: JsonRpcResult,
44}
45
46/// JSON-RPC 2.0 result or error union
47#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
48#[serde(untagged)]
49pub enum JsonRpcResult {
50    /// Successful result
51    Success {
52        /// Result data
53        result: serde_json::Value,
54    },
55    /// Error result
56    Error {
57        /// Error information
58        error: JsonRpcError,
59    },
60}
61
62/// JSON-RPC 2.0 error structure
63#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
64pub struct JsonRpcError {
65    /// Error code
66    pub code: i32,
67    /// Error message
68    pub message: String,
69    /// Optional additional error data
70    pub data: Option<serde_json::Value>,
71}
72
73/// Authentication response from Deribit API
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75pub struct AuthResponse {
76    /// Access token for authenticated requests
77    pub access_token: String,
78    /// Token type (usually "bearer")
79    pub token_type: String,
80    /// Token expiration time in seconds
81    pub expires_in: i64,
82    /// Refresh token for token renewal
83    pub refresh_token: String,
84    /// Scope of the token
85    pub scope: String,
86}
87
88/// Hello response containing API version information
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct HelloResponse {
91    /// API version string
92    pub version: String,
93}
94
95/// Test connection response
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct TestResponse {
98    /// API version string
99    pub version: String,
100}
101
102/// JSON-RPC 2.0 notification structure (no response expected)
103#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
104pub struct JsonRpcNotification {
105    /// JSON-RPC version (always "2.0")
106    pub jsonrpc: String,
107    /// Method name
108    pub method: String,
109    /// Optional parameters
110    pub params: Option<serde_json::Value>,
111}
112
113/// WebSocket connection state
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub enum ConnectionState {
116    /// Not connected
117    Disconnected,
118    /// Attempting to connect
119    Connecting,
120    /// Connected but not authenticated
121    Connected,
122    /// Connected and authenticated
123    Authenticated,
124    /// Attempting to reconnect
125    Reconnecting,
126    /// Connection failed
127    Failed,
128}
129
130/// Heartbeat monitoring status
131#[derive(Debug, Clone)]
132pub struct HeartbeatStatus {
133    /// Last ping sent timestamp
134    pub last_ping: Option<std::time::Instant>,
135    /// Last pong received timestamp
136    pub last_pong: Option<std::time::Instant>,
137    /// Number of consecutive missed pongs
138    pub missed_pongs: u32,
139}
140
141/// WebSocket subscription channel types
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
143pub enum SubscriptionChannel {
144    /// Ticker data for a specific instrument
145    Ticker(String),
146    /// Order book data for a specific instrument
147    OrderBook(String),
148    /// Trade data for a specific instrument
149    Trades(String),
150    /// Chart trade data for a specific instrument with resolution
151    ChartTrades {
152        /// The trading instrument (e.g., "BTC-PERPETUAL")
153        instrument: String,
154        /// Chart resolution (e.g., "1", "5", "15", "60" for minutes)
155        resolution: String,
156    },
157    /// User's order updates
158    UserOrders,
159    /// User's trade updates
160    UserTrades,
161    /// User's portfolio updates
162    UserPortfolio,
163    /// User's position changes for a specific instrument with interval
164    UserChanges {
165        /// The trading instrument (e.g., "BTC-PERPETUAL")
166        instrument: String,
167        /// Update interval (e.g., "raw", "100ms")
168        interval: String,
169    },
170    /// Price index updates
171    PriceIndex(String),
172    /// Estimated delivery price
173    EstimatedExpirationPrice(String),
174    /// Mark price updates
175    MarkPrice(String),
176    /// Funding rate updates
177    Funding(String),
178    /// Perpetual updates with configurable interval
179    Perpetual {
180        /// The trading instrument (e.g., "BTC-PERPETUAL")
181        instrument: String,
182        /// Update interval (e.g., "raw", "100ms")
183        interval: String,
184    },
185    /// Quote updates
186    Quote(String),
187    /// Platform state updates
188    PlatformState,
189    /// Platform state public methods state updates
190    PlatformStatePublicMethods,
191    /// Instrument state changes for a specific kind and currency
192    InstrumentState {
193        /// Instrument kind (e.g., "future", "option", "spot")
194        kind: String,
195        /// Currency (e.g., "BTC", "ETH")
196        currency: String,
197    },
198    /// Grouped order book with configurable depth and interval
199    GroupedOrderBook {
200        /// The trading instrument (e.g., "BTC-PERPETUAL")
201        instrument: String,
202        /// Grouping level for aggregation
203        group: String,
204        /// Order book depth (e.g., "1", "10", "20")
205        depth: String,
206        /// Update interval (e.g., "100ms", "agg2")
207        interval: String,
208    },
209    /// Incremental ticker updates for a specific instrument
210    IncrementalTicker(String),
211    /// Trades by instrument kind (e.g., future, option) and currency
212    TradesByKind {
213        /// Instrument kind (e.g., "future", "option", "spot", "any")
214        kind: String,
215        /// Currency (e.g., "BTC", "ETH", "any")
216        currency: String,
217        /// Update interval (e.g., "raw", "100ms")
218        interval: String,
219    },
220    /// Price ranking data for an index
221    PriceRanking(String),
222    /// Price statistics for an index
223    PriceStatistics(String),
224    /// Volatility index data
225    VolatilityIndex(String),
226    /// Block RFQ trades for a specific currency
227    BlockRfqTrades(String),
228    /// Block trade confirmations (all currencies)
229    BlockTradeConfirmations,
230    /// Block trade confirmations for a specific currency
231    BlockTradeConfirmationsByCurrency(String),
232    /// User MMP (Market Maker Protection) trigger for a specific index
233    UserMmpTrigger(String),
234    /// User API access log
235    UserAccessLog,
236    /// User account lock status
237    UserLock,
238    /// Unknown or unrecognized channel
239    Unknown(String),
240}
241
242/// WebSocket request structure for Deribit API
243#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
244pub struct WsRequest {
245    /// JSON-RPC version
246    pub jsonrpc: String,
247    /// Request ID for correlation with responses
248    pub id: serde_json::Value,
249    /// API method name to call
250    pub method: String,
251    /// Parameters for the API method
252    pub params: Option<serde_json::Value>,
253}
254
255/// WebSocket response structure for Deribit API
256#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
257pub struct WsResponse {
258    /// JSON-RPC version
259    pub jsonrpc: String,
260    /// Request ID for correlation (None for notifications)
261    pub id: Option<serde_json::Value>,
262    /// Result data if the request was successful
263    pub result: Option<serde_json::Value>,
264    /// Error information if the request failed
265    pub error: Option<JsonRpcError>,
266}
267
268impl JsonRpcRequest {
269    /// Create a new JSON-RPC request
270    pub fn new<T: Serialize>(id: serde_json::Value, method: &str, params: Option<T>) -> Self {
271        Self {
272            jsonrpc: "2.0".to_string(),
273            id,
274            method: method.to_string(),
275            params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
276        }
277    }
278}
279
280impl JsonRpcResponse {
281    /// Create a new successful JSON-RPC response
282    pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
283        Self {
284            jsonrpc: "2.0".to_string(),
285            id,
286            result: JsonRpcResult::Success { result },
287        }
288    }
289
290    /// Create a new error JSON-RPC response
291    pub fn error(id: serde_json::Value, error: JsonRpcError) -> Self {
292        Self {
293            jsonrpc: "2.0".to_string(),
294            id,
295            result: JsonRpcResult::Error { error },
296        }
297    }
298}
299
300impl JsonRpcNotification {
301    /// Create a new JSON-RPC notification
302    pub fn new<T: Serialize>(method: &str, params: Option<T>) -> Self {
303        Self {
304            jsonrpc: "2.0".to_string(),
305            method: method.to_string(),
306            params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
307        }
308    }
309}
310
311impl SubscriptionChannel {
312    /// Convert subscription channel to channel name
313    pub fn channel_name(&self) -> String {
314        match self {
315            SubscriptionChannel::Ticker(instrument) => format!("ticker.{}", instrument),
316            SubscriptionChannel::OrderBook(instrument) => format!("book.{}.raw", instrument),
317            SubscriptionChannel::Trades(instrument) => format!("trades.{}.raw", instrument),
318            SubscriptionChannel::ChartTrades {
319                instrument,
320                resolution,
321            } => {
322                format!("chart.trades.{}.{}", instrument, resolution)
323            }
324            SubscriptionChannel::UserOrders => "user.orders.any.any.raw".to_string(),
325            SubscriptionChannel::UserTrades => "user.trades.any.any.raw".to_string(),
326            SubscriptionChannel::UserPortfolio => "user.portfolio.any".to_string(),
327            SubscriptionChannel::UserChanges {
328                instrument,
329                interval,
330            } => {
331                format!("user.changes.{}.{}", instrument, interval)
332            }
333            SubscriptionChannel::PriceIndex(currency) => {
334                format!("deribit_price_index.{}_usd", currency.to_lowercase())
335            }
336            SubscriptionChannel::EstimatedExpirationPrice(instrument) => {
337                format!("estimated_expiration_price.{}", instrument)
338            }
339            SubscriptionChannel::MarkPrice(instrument) => {
340                format!("markprice.options.{}", instrument)
341            }
342            SubscriptionChannel::Funding(instrument) => format!("perpetual.{}.raw", instrument),
343            SubscriptionChannel::Perpetual {
344                instrument,
345                interval,
346            } => {
347                format!("perpetual.{}.{}", instrument, interval)
348            }
349            SubscriptionChannel::Quote(instrument) => format!("quote.{}", instrument),
350            SubscriptionChannel::PlatformState => "platform_state".to_string(),
351            SubscriptionChannel::PlatformStatePublicMethods => {
352                "platform_state.public_methods_state".to_string()
353            }
354            SubscriptionChannel::InstrumentState { kind, currency } => {
355                format!("instrument.state.{}.{}", kind, currency)
356            }
357            SubscriptionChannel::GroupedOrderBook {
358                instrument,
359                group,
360                depth,
361                interval,
362            } => {
363                format!("book.{}.{}.{}.{}", instrument, group, depth, interval)
364            }
365            SubscriptionChannel::IncrementalTicker(instrument) => {
366                format!("incremental_ticker.{}", instrument)
367            }
368            SubscriptionChannel::TradesByKind {
369                kind,
370                currency,
371                interval,
372            } => {
373                format!("trades.{}.{}.{}", kind, currency, interval)
374            }
375            SubscriptionChannel::PriceRanking(index_name) => {
376                format!("deribit_price_ranking.{}", index_name)
377            }
378            SubscriptionChannel::PriceStatistics(index_name) => {
379                format!("deribit_price_statistics.{}", index_name)
380            }
381            SubscriptionChannel::VolatilityIndex(index_name) => {
382                format!("deribit_volatility_index.{}", index_name)
383            }
384            SubscriptionChannel::BlockRfqTrades(currency) => {
385                format!("block_rfq.trades.{}", currency)
386            }
387            SubscriptionChannel::BlockTradeConfirmations => "block_trade_confirmations".to_string(),
388            SubscriptionChannel::BlockTradeConfirmationsByCurrency(currency) => {
389                format!("block_trade_confirmations.{}", currency)
390            }
391            SubscriptionChannel::UserMmpTrigger(index_name) => {
392                format!("user.mmp_trigger.{}", index_name)
393            }
394            SubscriptionChannel::UserAccessLog => "user.access_log".to_string(),
395            SubscriptionChannel::UserLock => "user.lock".to_string(),
396            SubscriptionChannel::Unknown(channel) => channel.clone(),
397        }
398    }
399
400    /// Parse subscription channel from string
401    ///
402    /// Returns the appropriate `SubscriptionChannel` variant for recognized channel patterns,
403    /// or `Unknown(String)` for unrecognized patterns.
404    #[must_use]
405    pub fn from_string(s: &str) -> Self {
406        let parts: Vec<&str> = s.split('.').collect();
407        match parts.as_slice() {
408            ["ticker", instrument] => SubscriptionChannel::Ticker(instrument.to_string()),
409            ["ticker", instrument, _interval] => {
410                SubscriptionChannel::Ticker(instrument.to_string())
411            }
412            ["book", instrument, "raw"] => SubscriptionChannel::OrderBook(instrument.to_string()),
413            ["book", instrument, group, depth, interval] => SubscriptionChannel::GroupedOrderBook {
414                instrument: instrument.to_string(),
415                group: group.to_string(),
416                depth: depth.to_string(),
417                interval: interval.to_string(),
418            },
419            ["book", instrument, _depth, _interval] => {
420                SubscriptionChannel::OrderBook(instrument.to_string())
421            }
422            ["incremental_ticker", instrument] => {
423                SubscriptionChannel::IncrementalTicker(instrument.to_string())
424            }
425            ["trades", instrument, "raw"] => SubscriptionChannel::Trades(instrument.to_string()),
426            ["trades", kind, currency, interval] if !Self::looks_like_instrument(kind) => {
427                SubscriptionChannel::TradesByKind {
428                    kind: kind.to_string(),
429                    currency: currency.to_string(),
430                    interval: interval.to_string(),
431                }
432            }
433            ["trades", instrument, _interval] => {
434                SubscriptionChannel::Trades(instrument.to_string())
435            }
436            ["chart", "trades", instrument, resolution] => SubscriptionChannel::ChartTrades {
437                instrument: instrument.to_string(),
438                resolution: resolution.to_string(),
439            },
440            ["user", "orders", ..] => SubscriptionChannel::UserOrders,
441            ["user", "trades", ..] => SubscriptionChannel::UserTrades,
442            ["user", "portfolio", ..] => SubscriptionChannel::UserPortfolio,
443            ["user", "changes", instrument, interval] => SubscriptionChannel::UserChanges {
444                instrument: instrument.to_string(),
445                interval: interval.to_string(),
446            },
447            ["deribit_price_index", currency_pair] => {
448                let currency = currency_pair
449                    .strip_suffix("_usd")
450                    .map(|c| c.to_uppercase())
451                    .unwrap_or_else(|| currency_pair.to_uppercase());
452                SubscriptionChannel::PriceIndex(currency)
453            }
454            ["estimated_expiration_price", instrument] => {
455                SubscriptionChannel::EstimatedExpirationPrice(instrument.to_string())
456            }
457            ["markprice", "options", instrument] => {
458                SubscriptionChannel::MarkPrice(instrument.to_string())
459            }
460            ["perpetual", instrument, interval] => SubscriptionChannel::Perpetual {
461                instrument: instrument.to_string(),
462                interval: interval.to_string(),
463            },
464            ["quote", instrument] => SubscriptionChannel::Quote(instrument.to_string()),
465            ["platform_state"] => SubscriptionChannel::PlatformState,
466            ["platform_state", "public_methods_state"] => {
467                SubscriptionChannel::PlatformStatePublicMethods
468            }
469            ["instrument", "state", kind, currency] => SubscriptionChannel::InstrumentState {
470                kind: kind.to_string(),
471                currency: currency.to_string(),
472            },
473            ["deribit_price_ranking", index_name] => {
474                SubscriptionChannel::PriceRanking(index_name.to_string())
475            }
476            ["deribit_price_statistics", index_name] => {
477                SubscriptionChannel::PriceStatistics(index_name.to_string())
478            }
479            ["deribit_volatility_index", index_name] => {
480                SubscriptionChannel::VolatilityIndex(index_name.to_string())
481            }
482            ["block_rfq", "trades", currency] => {
483                SubscriptionChannel::BlockRfqTrades(currency.to_string())
484            }
485            ["block_trade_confirmations"] => SubscriptionChannel::BlockTradeConfirmations,
486            ["block_trade_confirmations", currency] => {
487                SubscriptionChannel::BlockTradeConfirmationsByCurrency(currency.to_string())
488            }
489            ["user", "mmp_trigger", index_name] => {
490                SubscriptionChannel::UserMmpTrigger(index_name.to_string())
491            }
492            ["user", "access_log"] => SubscriptionChannel::UserAccessLog,
493            ["user", "lock"] => SubscriptionChannel::UserLock,
494            _ => SubscriptionChannel::Unknown(s.to_string()),
495        }
496    }
497
498    /// Check if this channel is unknown/unrecognized
499    #[must_use]
500    pub fn is_unknown(&self) -> bool {
501        matches!(self, SubscriptionChannel::Unknown(_))
502    }
503
504    /// Check if a string looks like an instrument name (contains hyphen).
505    ///
506    /// Used to distinguish between `trades.{instrument}.{interval}` and
507    /// `trades.{kind}.{currency}.{interval}` patterns.
508    #[must_use]
509    fn looks_like_instrument(s: &str) -> bool {
510        s.contains('-')
511    }
512}
513
514impl std::fmt::Display for SubscriptionChannel {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        write!(f, "{}", self.channel_name())
517    }
518}
519
520impl ConnectionState {
521    /// Check if the connection is in a connected state
522    pub fn is_connected(&self) -> bool {
523        matches!(
524            self,
525            ConnectionState::Connected | ConnectionState::Authenticated
526        )
527    }
528
529    /// Check if the connection is authenticated
530    pub fn is_authenticated(&self) -> bool {
531        matches!(self, ConnectionState::Authenticated)
532    }
533
534    /// Check if the connection is in a transitional state
535    pub fn is_transitional(&self) -> bool {
536        matches!(
537            self,
538            ConnectionState::Connecting | ConnectionState::Reconnecting
539        )
540    }
541}
542
543impl HeartbeatStatus {
544    /// Create a new heartbeat status
545    pub fn new() -> Self {
546        Self {
547            last_ping: None,
548            last_pong: None,
549            missed_pongs: 0,
550        }
551    }
552
553    /// Record a ping sent
554    pub fn ping_sent(&mut self) {
555        self.last_ping = Some(std::time::Instant::now());
556    }
557
558    /// Record a pong received
559    pub fn pong_received(&mut self) {
560        self.last_pong = Some(std::time::Instant::now());
561        self.missed_pongs = 0;
562    }
563
564    /// Record a missed pong
565    pub fn missed_pong(&mut self) {
566        self.missed_pongs += 1;
567    }
568
569    /// Check if connection is considered stale
570    pub fn is_stale(&self, max_missed_pongs: u32) -> bool {
571        self.missed_pongs >= max_missed_pongs
572    }
573}
574
575impl Default for HeartbeatStatus {
576    fn default() -> Self {
577        Self::new()
578    }
579}