nautilus_deribit/websocket/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data structures for Deribit WebSocket JSON-RPC messages.
17
18use std::fmt::Display;
19
20use nautilus_model::{
21    data::{Data, FundingRateUpdate, OrderBookDeltas},
22    instruments::InstrumentAny,
23};
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::enums::{DeribitBookAction, DeribitBookMsgType, DeribitHeartbeatType};
28pub use crate::common::rpc::{DeribitJsonRpcError, DeribitJsonRpcRequest, DeribitJsonRpcResponse};
29use crate::websocket::error::DeribitWsError;
30
31/// JSON-RPC subscription notification from Deribit.
32#[derive(Debug, Clone, Deserialize)]
33pub struct DeribitSubscriptionNotification<T> {
34    /// JSON-RPC version.
35    pub jsonrpc: String,
36    /// Method name (always "subscription").
37    pub method: String,
38    /// Subscription parameters containing channel and data.
39    pub params: DeribitSubscriptionParams<T>,
40}
41
42/// Subscription notification parameters.
43#[derive(Debug, Clone, Deserialize)]
44pub struct DeribitSubscriptionParams<T> {
45    /// Channel name (e.g., "trades.BTC-PERPETUAL.raw").
46    pub channel: String,
47    /// Channel-specific data.
48    pub data: T,
49}
50
51/// Authentication request parameters for client_signature grant.
52#[derive(Debug, Clone, Serialize)]
53pub struct DeribitAuthParams {
54    /// Grant type (client_signature for HMAC auth).
55    pub grant_type: String,
56    /// Client ID (API key).
57    pub client_id: String,
58    /// Unix timestamp in milliseconds.
59    pub timestamp: u64,
60    /// HMAC-SHA256 signature.
61    pub signature: String,
62    /// Random nonce.
63    pub nonce: String,
64    /// Data string (empty for WebSocket auth).
65    pub data: String,
66    /// Optional scope for session-based authentication.
67    /// Use "session:name" for persistent session auth (allows skipping access_token in private requests).
68    /// Use "connection" (default) for per-connection auth (requires access_token in each private request).
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub scope: Option<String>,
71}
72
73/// Token refresh request parameters.
74#[derive(Debug, Clone, Serialize)]
75pub struct DeribitRefreshTokenParams {
76    /// Grant type (always "refresh_token").
77    pub grant_type: String,
78    /// The refresh token obtained from authentication.
79    pub refresh_token: String,
80}
81
82/// Authentication response result.
83#[derive(Debug, Clone, Deserialize)]
84pub struct DeribitAuthResult {
85    /// Access token.
86    pub access_token: String,
87    /// Token expiration time in seconds.
88    pub expires_in: u64,
89    /// Refresh token.
90    pub refresh_token: String,
91    /// Granted scope.
92    pub scope: String,
93    /// Token type (bearer).
94    pub token_type: String,
95    /// Enabled features.
96    #[serde(default)]
97    pub enabled_features: Vec<String>,
98}
99
100/// Subscription request parameters.
101#[derive(Debug, Clone, Serialize)]
102pub struct DeribitSubscribeParams {
103    /// List of channels to subscribe to.
104    pub channels: Vec<String>,
105}
106
107/// Subscription response result.
108#[derive(Debug, Clone, Deserialize)]
109pub struct DeribitSubscribeResult(pub Vec<String>);
110
111/// Heartbeat enable request parameters.
112#[derive(Debug, Clone, Serialize)]
113pub struct DeribitHeartbeatParams {
114    /// Heartbeat interval in seconds (minimum 10).
115    pub interval: u64,
116}
117
118/// Heartbeat notification data.
119#[derive(Debug, Clone, Deserialize)]
120pub struct DeribitHeartbeatData {
121    /// Heartbeat type.
122    #[serde(rename = "type")]
123    pub heartbeat_type: DeribitHeartbeatType,
124}
125
126/// Trade data from trades.{instrument}.raw channel.
127#[derive(Debug, Clone, Deserialize)]
128pub struct DeribitTradeMsg {
129    /// Trade ID.
130    pub trade_id: String,
131    /// Instrument name.
132    pub instrument_name: Ustr,
133    /// Trade price.
134    pub price: f64,
135    /// Trade amount (contracts).
136    pub amount: f64,
137    /// Trade direction ("buy" or "sell").
138    pub direction: String,
139    /// Trade timestamp in milliseconds.
140    pub timestamp: u64,
141    /// Trade sequence number.
142    pub trade_seq: u64,
143    /// Tick direction (0-3).
144    pub tick_direction: i8,
145    /// Index price at trade time.
146    pub index_price: f64,
147    /// Mark price at trade time.
148    pub mark_price: f64,
149    /// IV (for options).
150    pub iv: Option<f64>,
151    /// Liquidation indicator.
152    pub liquidation: Option<String>,
153    /// Combo trade ID (if part of combo).
154    pub combo_trade_id: Option<i64>,
155    /// Block trade ID.
156    pub block_trade_id: Option<String>,
157    /// Combo ID.
158    pub combo_id: Option<String>,
159}
160
161/// Order book data from book.{instrument}.raw channel.
162#[derive(Debug, Clone, Deserialize)]
163pub struct DeribitBookMsg {
164    /// Message type (snapshot or change).
165    #[serde(rename = "type")]
166    pub msg_type: DeribitBookMsgType,
167    /// Instrument name.
168    pub instrument_name: Ustr,
169    /// Timestamp in milliseconds.
170    pub timestamp: u64,
171    /// Change ID for sequence tracking.
172    pub change_id: u64,
173    /// Previous change ID (for delta validation).
174    pub prev_change_id: Option<u64>,
175    /// Bid levels: [action, price, amount] where action is "new" for snapshot, "new"/"change"/"delete" for change.
176    pub bids: Vec<Vec<serde_json::Value>>,
177    /// Ask levels: [action, price, amount] where action is "new" for snapshot, "new"/"change"/"delete" for change.
178    pub asks: Vec<Vec<serde_json::Value>>,
179}
180
181/// Parsed order book level.
182#[derive(Debug, Clone)]
183pub struct DeribitBookLevel {
184    /// Price level.
185    pub price: f64,
186    /// Amount at this level.
187    pub amount: f64,
188    /// Action for delta updates.
189    pub action: Option<DeribitBookAction>,
190}
191
192/// Ticker data from ticker.{instrument}.raw channel.
193#[derive(Debug, Clone, Deserialize)]
194pub struct DeribitTickerMsg {
195    /// Instrument name.
196    pub instrument_name: Ustr,
197    /// Timestamp in milliseconds.
198    pub timestamp: u64,
199    /// Best bid price.
200    pub best_bid_price: Option<f64>,
201    /// Best bid amount.
202    pub best_bid_amount: Option<f64>,
203    /// Best ask price.
204    pub best_ask_price: Option<f64>,
205    /// Best ask amount.
206    pub best_ask_amount: Option<f64>,
207    /// Last trade price.
208    pub last_price: Option<f64>,
209    /// Mark price.
210    pub mark_price: f64,
211    /// Index price.
212    pub index_price: f64,
213    /// Open interest.
214    pub open_interest: f64,
215    /// Current funding rate (perpetuals).
216    pub current_funding: Option<f64>,
217    /// Funding 8h rate (perpetuals).
218    pub funding_8h: Option<f64>,
219    /// Settlement price (expired instruments).
220    pub settlement_price: Option<f64>,
221    /// 24h volume.
222    pub volume: Option<f64>,
223    /// 24h volume in USD.
224    pub volume_usd: Option<f64>,
225    /// 24h high.
226    pub high: Option<f64>,
227    /// 24h low.
228    pub low: Option<f64>,
229    /// 24h price change.
230    pub price_change: Option<f64>,
231    /// State of the instrument.
232    pub state: String,
233    // Options-specific fields
234    /// Greeks (options).
235    pub greeks: Option<DeribitGreeks>,
236    /// Underlying price (options).
237    pub underlying_price: Option<f64>,
238    /// Underlying index (options).
239    pub underlying_index: Option<String>,
240}
241
242/// Greeks for options.
243#[derive(Debug, Clone, Deserialize)]
244pub struct DeribitGreeks {
245    pub delta: f64,
246    pub gamma: f64,
247    pub vega: f64,
248    pub theta: f64,
249    pub rho: f64,
250}
251
252/// Quote data from quote.{instrument} channel.
253#[derive(Debug, Clone, Deserialize)]
254pub struct DeribitQuoteMsg {
255    /// Instrument name.
256    pub instrument_name: Ustr,
257    /// Timestamp in milliseconds.
258    pub timestamp: u64,
259    /// Best bid price.
260    pub best_bid_price: f64,
261    /// Best bid amount.
262    pub best_bid_amount: f64,
263    /// Best ask price.
264    pub best_ask_price: f64,
265    /// Best ask amount.
266    pub best_ask_amount: f64,
267}
268
269/// Instrument lifecycle state from Deribit.
270///
271/// Represents the current state of an instrument in its lifecycle.
272#[derive(
273    Debug,
274    Clone,
275    Copy,
276    PartialEq,
277    Eq,
278    Hash,
279    Serialize,
280    Deserialize,
281    strum::AsRefStr,
282    strum::EnumIter,
283    strum::EnumString,
284)]
285#[serde(rename_all = "snake_case")]
286#[strum(serialize_all = "snake_case")]
287#[cfg_attr(
288    feature = "python",
289    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.deribit")
290)]
291pub enum DeribitInstrumentState {
292    /// Instrument has been created but not yet active.
293    Created,
294    /// Instrument is active and trading.
295    Started,
296    /// Instrument has been settled (options/futures at expiry).
297    Settled,
298    /// Instrument is closed for trading.
299    Closed,
300    /// Instrument has been terminated.
301    Terminated,
302}
303
304impl Display for DeribitInstrumentState {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        match self {
307            Self::Created => write!(f, "created"),
308            Self::Started => write!(f, "started"),
309            Self::Settled => write!(f, "settled"),
310            Self::Closed => write!(f, "closed"),
311            Self::Terminated => write!(f, "terminated"),
312        }
313    }
314}
315
316/// Instrument state notification from `instrument.state.{kind}.{currency}` channel.
317///
318/// Notifications are sent when an instrument's lifecycle state changes.
319/// Example: `{"instrument_name":"BTC-22MAR19","state":"created","timestamp":1553080940000}`
320#[derive(Debug, Clone, Deserialize)]
321pub struct DeribitInstrumentStateMsg {
322    /// Name of the instrument.
323    pub instrument_name: Ustr,
324    /// Current state of the instrument.
325    pub state: DeribitInstrumentState,
326    /// Timestamp of the state change in milliseconds.
327    pub timestamp: u64,
328}
329
330/// Deribit perpetual interest rate message.
331///
332/// Sent via the `perpetual.{instrument_name}.{interval}` channel.
333/// Only available for perpetual instruments.
334/// Example: `{"index_price":7872.88,"interest":0.004999511380756577,"timestamp":1571386349530}`
335#[derive(Debug, Clone, Deserialize)]
336pub struct DeribitPerpetualMsg {
337    /// Current index price.
338    pub index_price: f64,
339    /// Current interest rate (funding rate).
340    pub interest: f64,
341    /// Timestamp in milliseconds since Unix epoch.
342    pub timestamp: u64,
343}
344
345/// Chart/OHLC bar data from chart.trades.{instrument}.{resolution} channel.
346///
347/// Sent via the `chart.trades.{instrument_name}.{resolution}` channel.
348/// Example: `{"tick":1767199200000,"open":87699.5,"high":87699.5,"low":87699.5,"close":87699.5,"volume":1.1403e-4,"cost":10.0}`
349#[derive(Debug, Clone, Deserialize)]
350pub struct DeribitChartMsg {
351    /// Bar timestamp in milliseconds since Unix epoch.
352    pub tick: u64,
353    /// Opening price.
354    pub open: f64,
355    /// Highest price.
356    pub high: f64,
357    /// Lowest price.
358    pub low: f64,
359    /// Closing price.
360    pub close: f64,
361    /// Volume in base currency.
362    pub volume: f64,
363    /// Volume in USD.
364    pub cost: f64,
365}
366
367/// Raw Deribit WebSocket message variants.
368#[derive(Debug, Clone)]
369pub enum DeribitWsMessage {
370    /// JSON-RPC response to a request.
371    Response(DeribitJsonRpcResponse<serde_json::Value>),
372    /// Subscription notification (trade, book, ticker data).
373    Notification(DeribitSubscriptionNotification<serde_json::Value>),
374    /// Heartbeat message.
375    Heartbeat(DeribitHeartbeatData),
376    /// JSON-RPC error.
377    Error(DeribitJsonRpcError),
378    /// Reconnection event (internal).
379    Reconnected,
380}
381
382/// Deribit WebSocket error for external consumers.
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct DeribitWebSocketError {
385    /// Error code from Deribit.
386    pub code: i64,
387    /// Error message.
388    pub message: String,
389    /// Timestamp when error occurred.
390    pub timestamp: u64,
391}
392
393impl From<DeribitJsonRpcError> for DeribitWebSocketError {
394    fn from(err: DeribitJsonRpcError) -> Self {
395        Self {
396            code: err.code,
397            message: err.message,
398            timestamp: 0,
399        }
400    }
401}
402
403/// Normalized Nautilus domain message after parsing.
404#[derive(Debug, Clone)]
405pub enum NautilusWsMessage {
406    /// Market data (trades, bars, quotes).
407    Data(Vec<Data>),
408    /// Order book deltas.
409    Deltas(OrderBookDeltas),
410    /// Instrument definition update.
411    Instrument(Box<InstrumentAny>),
412    /// Funding rate updates (for perpetual instruments).
413    FundingRates(Vec<FundingRateUpdate>),
414    /// Error from venue.
415    Error(DeribitWsError),
416    /// Unhandled/raw message for debugging.
417    Raw(serde_json::Value),
418    /// Reconnection completed.
419    Reconnected,
420    /// Authentication succeeded with tokens.
421    Authenticated(Box<DeribitAuthResult>),
422}
423
424/// Parses a raw JSON message into a DeribitWsMessage.
425///
426/// # Errors
427///
428/// Returns an error if JSON parsing fails or the message format is unrecognized.
429pub fn parse_raw_message(text: &str) -> Result<DeribitWsMessage, DeribitWsError> {
430    let value: serde_json::Value =
431        serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
432
433    // Check for subscription notification (has "method": "subscription")
434    if let Some(method) = value.get("method").and_then(|m| m.as_str()) {
435        if method == "subscription" {
436            let notification: DeribitSubscriptionNotification<serde_json::Value> =
437                serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
438            return Ok(DeribitWsMessage::Notification(notification));
439        }
440        // Check for heartbeat
441        if method == "heartbeat"
442            && let Some(params) = value.get("params")
443        {
444            let heartbeat: DeribitHeartbeatData = serde_json::from_value(params.clone())
445                .map_err(|e| DeribitWsError::Json(e.to_string()))?;
446            return Ok(DeribitWsMessage::Heartbeat(heartbeat));
447        }
448    }
449
450    // Check for JSON-RPC response (has "id" field)
451    if value.get("id").is_some() {
452        // Check for error response
453        if value.get("error").is_some() {
454            let response: DeribitJsonRpcResponse<serde_json::Value> =
455                serde_json::from_value(value.clone())
456                    .map_err(|e| DeribitWsError::Json(e.to_string()))?;
457            if let Some(err) = response.error {
458                return Ok(DeribitWsMessage::Error(err));
459            }
460        }
461        // Success response
462        let response: DeribitJsonRpcResponse<serde_json::Value> =
463            serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
464        return Ok(DeribitWsMessage::Response(response));
465    }
466
467    // Fallback: try to parse as generic response
468    let response: DeribitJsonRpcResponse<serde_json::Value> =
469        serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
470    Ok(DeribitWsMessage::Response(response))
471}
472
473/// Extracts the instrument name from a channel string.
474///
475/// For example: "trades.BTC-PERPETUAL.raw" -> "BTC-PERPETUAL"
476pub fn extract_instrument_from_channel(channel: &str) -> Option<&str> {
477    let parts: Vec<&str> = channel.split('.').collect();
478    if parts.len() >= 2 {
479        Some(parts[1])
480    } else {
481        None
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use rstest::rstest;
488
489    use super::*;
490
491    #[rstest]
492    fn test_parse_subscription_notification() {
493        let json = r#"{
494            "jsonrpc": "2.0",
495            "method": "subscription",
496            "params": {
497                "channel": "trades.BTC-PERPETUAL.raw",
498                "data": [{"trade_id": "123", "price": 50000.0}]
499            }
500        }"#;
501
502        let msg = parse_raw_message(json).unwrap();
503        assert!(matches!(msg, DeribitWsMessage::Notification(_)));
504    }
505
506    #[rstest]
507    fn test_parse_response() {
508        let json = r#"{
509            "jsonrpc": "2.0",
510            "id": 1,
511            "result": ["trades.BTC-PERPETUAL.raw"],
512            "testnet": true,
513            "usIn": 1234567890,
514            "usOut": 1234567891,
515            "usDiff": 1
516        }"#;
517
518        let msg = parse_raw_message(json).unwrap();
519        assert!(matches!(msg, DeribitWsMessage::Response(_)));
520    }
521
522    #[rstest]
523    fn test_parse_error_response() {
524        let json = r#"{
525            "jsonrpc": "2.0",
526            "id": 1,
527            "error": {
528                "code": 10028,
529                "message": "too_many_requests"
530            }
531        }"#;
532
533        let msg = parse_raw_message(json).unwrap();
534        assert!(matches!(msg, DeribitWsMessage::Error(_)));
535    }
536
537    #[rstest]
538    fn test_extract_instrument_from_channel() {
539        assert_eq!(
540            extract_instrument_from_channel("trades.BTC-PERPETUAL.raw"),
541            Some("BTC-PERPETUAL")
542        );
543        assert_eq!(
544            extract_instrument_from_channel("book.ETH-25DEC25.raw"),
545            Some("ETH-25DEC25")
546        );
547        assert_eq!(extract_instrument_from_channel("platform_state"), None);
548    }
549}