Skip to main content

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::str::FromStr;
19
20use nautilus_core::serialization::{deserialize_decimal, deserialize_optional_decimal};
21use nautilus_model::{
22    data::{
23        Data, FundingRateUpdate, InstrumentStatus, OrderBookDeltas, greeks::OptionGreekValues,
24        option_chain::OptionGreeks,
25    },
26    events::{
27        AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired, OrderFilled,
28        OrderModifyRejected, OrderRejected, OrderUpdated,
29    },
30    instruments::InstrumentAny,
31    reports::{FillReport, OrderStatusReport},
32};
33use rust_decimal::{Decimal, prelude::ToPrimitive};
34use serde::{Deserialize, Deserializer, Serialize, de};
35use ustr::Ustr;
36
37use super::enums::{DeribitBookAction, DeribitBookMsgType, DeribitHeartbeatType};
38pub use crate::common::{
39    enums::DeribitInstrumentState,
40    rpc::{DeribitJsonRpcError, DeribitJsonRpcRequest, DeribitJsonRpcResponse},
41};
42use crate::{common::models::DeribitTradeLeg, websocket::error::DeribitWsError};
43
44/// JSON-RPC subscription notification from Deribit.
45#[derive(Debug, Clone, Deserialize)]
46pub struct DeribitSubscriptionNotification<T> {
47    /// JSON-RPC version.
48    pub jsonrpc: String,
49    /// Method name (always "subscription").
50    pub method: String,
51    /// Subscription parameters containing channel and data.
52    pub params: DeribitSubscriptionParams<T>,
53}
54
55/// Subscription notification parameters.
56#[derive(Debug, Clone, Deserialize)]
57pub struct DeribitSubscriptionParams<T> {
58    /// Channel name (e.g., "trades.BTC-PERPETUAL.raw").
59    pub channel: String,
60    /// Channel-specific data.
61    pub data: T,
62}
63
64/// Authentication request parameters for client_signature grant.
65#[derive(Debug, Clone, Serialize)]
66pub struct DeribitAuthParams {
67    /// Grant type (client_signature for HMAC auth).
68    pub grant_type: String,
69    /// Client ID (API key).
70    pub client_id: String,
71    /// Unix timestamp in milliseconds.
72    pub timestamp: u64,
73    /// HMAC-SHA256 signature.
74    pub signature: String,
75    /// Random nonce.
76    pub nonce: String,
77    /// Data string (empty for WebSocket auth).
78    pub data: String,
79    /// Optional scope for session-based authentication.
80    /// Use "session:name" for persistent session auth (allows skipping access_token in private requests).
81    /// Use "connection" (default) for per-connection auth (requires access_token in each private request).
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub scope: Option<String>,
84}
85
86/// Token refresh request parameters.
87#[derive(Debug, Clone, Serialize)]
88pub struct DeribitRefreshTokenParams {
89    /// Grant type (always "refresh_token").
90    pub grant_type: String,
91    /// The refresh token obtained from authentication.
92    pub refresh_token: String,
93}
94
95/// Authentication response result.
96#[derive(Debug, Clone, Deserialize)]
97pub struct DeribitAuthResult {
98    /// Access token.
99    pub access_token: String,
100    /// Token expiration time in seconds.
101    pub expires_in: u64,
102    /// Refresh token.
103    pub refresh_token: String,
104    /// Granted scope.
105    pub scope: String,
106    /// Token type (bearer).
107    pub token_type: String,
108    /// Enabled features.
109    #[serde(default)]
110    pub enabled_features: Vec<String>,
111}
112
113/// Subscription request parameters.
114#[derive(Debug, Clone, Serialize)]
115pub struct DeribitSubscribeParams {
116    /// List of channels to subscribe to.
117    pub channels: Vec<String>,
118}
119
120/// Subscription response result.
121#[derive(Debug, Clone, Deserialize)]
122pub struct DeribitSubscribeResult(pub Vec<String>);
123
124/// Heartbeat enable request parameters.
125#[derive(Debug, Clone, Serialize)]
126pub struct DeribitHeartbeatParams {
127    /// Heartbeat interval in seconds (minimum 10).
128    pub interval: u64,
129}
130
131/// Heartbeat notification data.
132#[derive(Debug, Clone, Deserialize)]
133pub struct DeribitHeartbeatData {
134    /// Heartbeat type.
135    #[serde(rename = "type")]
136    pub heartbeat_type: DeribitHeartbeatType,
137}
138
139/// Trade data from trades.{instrument}.raw channel.
140#[derive(Debug, Clone, Deserialize)]
141pub struct DeribitTradeMsg {
142    /// Trade ID.
143    pub trade_id: String,
144    /// Instrument name.
145    pub instrument_name: Ustr,
146    /// Trade price.
147    #[serde(deserialize_with = "deserialize_decimal")]
148    pub price: Decimal,
149    /// Trade amount (contracts).
150    #[serde(deserialize_with = "deserialize_decimal")]
151    pub amount: Decimal,
152    /// Trade direction ("buy" or "sell").
153    pub direction: String,
154    /// Trade timestamp in milliseconds.
155    pub timestamp: u64,
156    /// Trade sequence number.
157    pub trade_seq: u64,
158    /// Tick direction (0-3).
159    pub tick_direction: i8,
160    /// Index price at trade time.
161    #[serde(deserialize_with = "deserialize_decimal")]
162    pub index_price: Decimal,
163    /// Mark price at trade time.
164    #[serde(deserialize_with = "deserialize_decimal")]
165    pub mark_price: Decimal,
166    /// IV (for options).
167    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
168    pub iv: Option<Decimal>,
169    /// Liquidation indicator.
170    pub liquidation: Option<String>,
171    /// Combo trade ID (if part of combo).
172    pub combo_trade_id: Option<String>,
173    /// Block trade ID.
174    pub block_trade_id: Option<String>,
175    /// Block RFQ ID (if the trade originated from a Block RFQ).
176    #[serde(default)]
177    pub block_rfq_id: Option<i64>,
178    /// Combo ID.
179    pub combo_id: Option<String>,
180    /// Per-leg trades when this is the parent combo trade.
181    #[serde(default)]
182    pub legs: Option<Vec<DeribitTradeLeg>>,
183}
184
185/// Order book data from book.{instrument}.{interval} or book.{instrument}.{group}.{depth}.{interval} channels.
186///
187/// Note: The grouped book channel (`book.{instrument}.{group}.{depth}.{interval}`) does not include
188/// a `type` field since it always sends complete snapshots. We default to `Snapshot` when not present.
189#[derive(Debug, Clone, Deserialize)]
190pub struct DeribitBookMsg {
191    /// Message type (snapshot or change). Defaults to Snapshot for grouped channels.
192    #[serde(rename = "type", default = "default_book_msg_type")]
193    pub msg_type: DeribitBookMsgType,
194    /// Instrument name.
195    pub instrument_name: Ustr,
196    /// Timestamp in milliseconds.
197    pub timestamp: u64,
198    /// Change ID for sequence tracking.
199    pub change_id: u64,
200    /// Previous change ID (for delta validation).
201    pub prev_change_id: Option<u64>,
202    /// Bid levels: [action, price, amount] where action is "new" for snapshot, "new"/"change"/"delete" for change.
203    pub bids: Vec<Vec<serde_json::Value>>,
204    /// Ask levels: [action, price, amount] where action is "new" for snapshot, "new"/"change"/"delete" for change.
205    pub asks: Vec<Vec<serde_json::Value>>,
206}
207
208/// Default book message type for grouped channels (always snapshot).
209fn default_book_msg_type() -> DeribitBookMsgType {
210    DeribitBookMsgType::Snapshot
211}
212
213/// Parsed order book level.
214#[derive(Debug, Clone)]
215pub struct DeribitBookLevel {
216    /// Price level.
217    pub price: Decimal,
218    /// Amount at this level.
219    pub amount: Decimal,
220    /// Action for delta updates.
221    pub action: Option<DeribitBookAction>,
222}
223
224/// Ticker data from ticker.{instrument}.raw channel.
225#[derive(Debug, Clone, Deserialize)]
226pub struct DeribitTickerMsg {
227    /// Instrument name.
228    pub instrument_name: Ustr,
229    /// Timestamp in milliseconds.
230    pub timestamp: u64,
231    /// Best bid price.
232    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
233    pub best_bid_price: Option<Decimal>,
234    /// Best bid amount.
235    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
236    pub best_bid_amount: Option<Decimal>,
237    /// Best ask price.
238    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
239    pub best_ask_price: Option<Decimal>,
240    /// Best ask amount.
241    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
242    pub best_ask_amount: Option<Decimal>,
243    /// Last trade price.
244    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
245    pub last_price: Option<Decimal>,
246    /// Mark price.
247    #[serde(deserialize_with = "deserialize_decimal")]
248    pub mark_price: Decimal,
249    /// Index price.
250    #[serde(deserialize_with = "deserialize_decimal")]
251    pub index_price: Decimal,
252    /// Open interest.
253    #[serde(deserialize_with = "deserialize_decimal")]
254    pub open_interest: Decimal,
255    /// Current funding rate (perpetuals).
256    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
257    pub current_funding: Option<Decimal>,
258    /// Funding 8h rate (perpetuals).
259    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
260    pub funding_8h: Option<Decimal>,
261    /// Settlement price (expired instruments).
262    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
263    pub settlement_price: Option<Decimal>,
264    /// 24h volume.
265    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
266    pub volume: Option<Decimal>,
267    /// 24h volume in USD.
268    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
269    pub volume_usd: Option<Decimal>,
270    /// 24h high.
271    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
272    pub high: Option<Decimal>,
273    /// 24h low.
274    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
275    pub low: Option<Decimal>,
276    /// 24h price change.
277    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
278    pub price_change: Option<Decimal>,
279    /// State of the instrument.
280    pub state: String,
281    // Options-specific fields
282    /// Greeks (options).
283    pub greeks: Option<DeribitGreeks>,
284    /// Mark implied volatility (options).
285    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
286    pub mark_iv: Option<Decimal>,
287    /// Bid implied volatility (options).
288    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
289    pub bid_iv: Option<Decimal>,
290    /// Ask implied volatility (options).
291    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
292    pub ask_iv: Option<Decimal>,
293    /// Underlying price (options).
294    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
295    pub underlying_price: Option<Decimal>,
296    /// Underlying index (options).
297    pub underlying_index: Option<String>,
298}
299
300/// Greeks for options.
301#[derive(Debug, Clone, Deserialize)]
302pub struct DeribitGreeks {
303    #[serde(deserialize_with = "deserialize_decimal")]
304    pub delta: Decimal,
305    #[serde(deserialize_with = "deserialize_decimal")]
306    pub gamma: Decimal,
307    #[serde(deserialize_with = "deserialize_decimal")]
308    pub vega: Decimal,
309    #[serde(deserialize_with = "deserialize_decimal")]
310    pub theta: Decimal,
311    #[serde(deserialize_with = "deserialize_decimal")]
312    pub rho: Decimal,
313}
314
315impl DeribitGreeks {
316    /// Converts Deribit Greeks (Decimal) to Nautilus `OptionGreekValues` (f64).
317    pub fn to_greek_values(&self) -> OptionGreekValues {
318        OptionGreekValues {
319            delta: self.delta.to_f64().unwrap_or(0.0),
320            gamma: self.gamma.to_f64().unwrap_or(0.0),
321            vega: self.vega.to_f64().unwrap_or(0.0),
322            theta: self.theta.to_f64().unwrap_or(0.0),
323            rho: self.rho.to_f64().unwrap_or(0.0),
324        }
325    }
326}
327
328/// Quote data from quote.{instrument} channel.
329#[derive(Debug, Clone, Deserialize)]
330pub struct DeribitQuoteMsg {
331    /// Instrument name.
332    pub instrument_name: Ustr,
333    /// Timestamp in milliseconds.
334    pub timestamp: u64,
335    /// Best bid price.
336    #[serde(deserialize_with = "deserialize_decimal")]
337    pub best_bid_price: Decimal,
338    /// Best bid amount.
339    #[serde(deserialize_with = "deserialize_decimal")]
340    pub best_bid_amount: Decimal,
341    /// Best ask price.
342    #[serde(deserialize_with = "deserialize_decimal")]
343    pub best_ask_price: Decimal,
344    /// Best ask amount.
345    #[serde(deserialize_with = "deserialize_decimal")]
346    pub best_ask_amount: Decimal,
347}
348
349/// Instrument state notification from `instrument.state.{kind}.{currency}` channel.
350///
351/// Notifications are sent when an instrument's lifecycle state changes.
352/// Example: `{"instrument_name":"BTC-22MAR19","state":"created","timestamp":1553080940000}`
353#[derive(Debug, Clone, Deserialize)]
354pub struct DeribitInstrumentStateMsg {
355    /// Name of the instrument.
356    pub instrument_name: Ustr,
357    /// Current state of the instrument.
358    pub state: DeribitInstrumentState,
359    /// Timestamp of the state change in milliseconds.
360    pub timestamp: u64,
361}
362
363/// Deribit perpetual interest rate message.
364///
365/// Sent via the `perpetual.{instrument_name}.{interval}` channel.
366/// Only available for perpetual instruments.
367/// Example: `{"index_price":7872.88,"interest":0.004999511380756577,"timestamp":1571386349530}`
368#[derive(Debug, Clone, Deserialize)]
369pub struct DeribitPerpetualMsg {
370    /// Current index price.
371    #[serde(deserialize_with = "deserialize_decimal")]
372    pub index_price: Decimal,
373    /// Current interest rate (funding rate).
374    #[serde(deserialize_with = "deserialize_decimal")]
375    pub interest: Decimal,
376    /// Timestamp in milliseconds since Unix epoch.
377    pub timestamp: u64,
378}
379
380/// Volatility index data from the `deribit_volatility_index.{index_name}` channel.
381#[derive(Debug, Clone, Deserialize)]
382pub struct DeribitVolatilityIndexMsg {
383    /// Timestamp in milliseconds since Unix epoch.
384    pub timestamp: u64,
385    /// Current volatility index value.
386    pub volatility: f64,
387    /// Index identifier (for example `"btc_usd"`).
388    pub index_name: String,
389}
390
391/// Chart/OHLC bar data from chart.trades.{instrument}.{resolution} channel.
392///
393/// Sent via the `chart.trades.{instrument_name}.{resolution}` channel.
394/// Status of a chart/candle bar from Deribit.
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
396#[serde(rename_all = "lowercase")]
397pub enum DeribitChartStatus {
398    /// Bar is closed/confirmed.
399    #[default]
400    Ok,
401    /// Bar is still in progress (imputed/partial data).
402    Imputed,
403}
404
405/// Example: `{"tick":1767199200000,"open":87699.5,"high":87699.5,"low":87699.5,"close":87699.5,"volume":1.1403e-4,"cost":10.0,"status":"ok"}`
406#[derive(Debug, Clone, Deserialize)]
407pub struct DeribitChartMsg {
408    /// Bar timestamp in milliseconds since Unix epoch.
409    pub tick: u64,
410    /// Opening price.
411    pub open: f64,
412    /// Highest price.
413    pub high: f64,
414    /// Lowest price.
415    pub low: f64,
416    /// Closing price.
417    pub close: f64,
418    /// Volume in base currency.
419    pub volume: f64,
420    /// Volume in USD.
421    pub cost: f64,
422    /// Bar status: `Ok` for closed bar, `Imputed` for in-progress bar.
423    #[serde(default)]
424    pub status: DeribitChartStatus,
425}
426
427/// Order parameters for private/buy and private/sell requests.
428///
429/// Note: Decimal fields are serialized as JSON floats per Deribit API requirements,
430/// which may cause precision loss for values with more than ~15 significant digits.
431#[derive(Debug, Clone, Serialize)]
432pub struct DeribitOrderParams {
433    /// Instrument name (e.g., "BTC-PERPETUAL").
434    pub instrument_name: String,
435    /// Order amount in contracts.
436    #[serde(with = "rust_decimal::serde::float")]
437    pub amount: Decimal,
438    /// Order type: "limit", "market", "stop_limit", "stop_market", "take_limit", "take_market".
439    #[serde(rename = "type")]
440    pub order_type: String,
441    /// User-defined label (client order ID), max 64 chars alphanumeric.
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub label: Option<String>,
444    /// Limit price (required for limit orders).
445    #[serde(
446        skip_serializing_if = "Option::is_none",
447        with = "rust_decimal::serde::float_option"
448    )]
449    pub price: Option<Decimal>,
450    /// Time in force: "good_til_cancelled", "good_til_day", "fill_or_kill", "immediate_or_cancel".
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub time_in_force: Option<String>,
453    /// Post-only flag. If true and order would take liquidity, price is adjusted
454    /// to be just below the spread (unless reject_post_only is true).
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub post_only: Option<bool>,
457    /// If true with post_only, order is rejected instead of price being adjusted.
458    /// Only valid when post_only is true.
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub reject_post_only: Option<bool>,
461    /// Reduce-only flag (only reduces position).
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub reduce_only: Option<bool>,
464    /// Trigger price for stop/take orders.
465    #[serde(
466        skip_serializing_if = "Option::is_none",
467        with = "rust_decimal::serde::float_option"
468    )]
469    pub trigger_price: Option<Decimal>,
470    /// Trigger type: "last_price", "index_price", "mark_price".
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub trigger: Option<String>,
473    /// Maximum display quantity for iceberg orders.
474    #[serde(
475        skip_serializing_if = "Option::is_none",
476        with = "rust_decimal::serde::float_option"
477    )]
478    pub max_show: Option<Decimal>,
479    /// GTD expiration timestamp in milliseconds.
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub valid_until: Option<u64>,
482}
483
484/// Cancel order parameters for private/cancel request.
485#[derive(Debug, Clone, Serialize)]
486pub struct DeribitCancelParams {
487    /// Venue order ID to cancel.
488    pub order_id: String,
489}
490
491/// Cancel all orders parameters for private/cancel_all_by_instrument request.
492#[derive(Debug, Clone, Serialize)]
493pub struct DeribitCancelAllByInstrumentParams {
494    /// Instrument name.
495    pub instrument_name: String,
496    /// Optional order type filter.
497    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
498    pub order_type: Option<String>,
499}
500
501/// Edit order parameters for private/edit request.
502///
503/// Note: Decimal fields are serialized as JSON floats per Deribit API requirements,
504/// which may cause precision loss for values with more than ~15 significant digits.
505#[derive(Debug, Clone, Serialize)]
506pub struct DeribitEditParams {
507    /// Venue order ID to modify.
508    pub order_id: String,
509    /// New amount.
510    #[serde(with = "rust_decimal::serde::float")]
511    pub amount: Decimal,
512    /// New price (for limit orders).
513    #[serde(
514        skip_serializing_if = "Option::is_none",
515        with = "rust_decimal::serde::float_option"
516    )]
517    pub price: Option<Decimal>,
518    /// New trigger price (for stop orders).
519    #[serde(
520        skip_serializing_if = "Option::is_none",
521        with = "rust_decimal::serde::float_option"
522    )]
523    pub trigger_price: Option<Decimal>,
524    /// Post-only flag. If true and order would take liquidity, price is adjusted
525    /// to be just below the spread (unless reject_post_only is true).
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub post_only: Option<bool>,
528    /// If true with post_only, order is rejected instead of price being adjusted.
529    /// Only valid when post_only is true.
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub reject_post_only: Option<bool>,
532    /// Reduce-only flag.
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub reduce_only: Option<bool>,
535}
536
537/// Get order state parameters for private/get_order_state request.
538#[derive(Debug, Clone, Serialize)]
539pub struct DeribitGetOrderStateParams {
540    /// Venue order ID.
541    pub order_id: String,
542}
543
544// Deribit returns the literal string `"market_price"` for the price of trigger
545// market orders (`stop_market`, `take_market`) since they have no limit price.
546// Such values are mapped to `None`; other inputs delegate to the standard
547// optional decimal deserialization.
548fn deserialize_optional_decimal_or_market<'de, D>(
549    deserializer: D,
550) -> Result<Option<Decimal>, D::Error>
551where
552    D: Deserializer<'de>,
553{
554    struct Visitor;
555
556    impl<'de> de::Visitor<'de> for Visitor {
557        type Value = Option<Decimal>;
558
559        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
560            formatter.write_str(
561                "null, a decimal as string/integer/float, or the literal \"market_price\"",
562            )
563        }
564
565        fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
566            if v.is_empty() || v == "market_price" {
567                return Ok(None);
568            }
569
570            if v.contains('e') || v.contains('E') {
571                Decimal::from_scientific(v).map(Some).map_err(E::custom)
572            } else {
573                Decimal::from_str(v).map(Some).map_err(E::custom)
574            }
575        }
576
577        fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
578            self.visit_str(&v)
579        }
580
581        fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
582            Ok(Some(Decimal::from(v)))
583        }
584
585        fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
586            Ok(Some(Decimal::from(v)))
587        }
588
589        fn visit_i128<E: de::Error>(self, v: i128) -> Result<Self::Value, E> {
590            Ok(Some(Decimal::from(v)))
591        }
592
593        fn visit_u128<E: de::Error>(self, v: u128) -> Result<Self::Value, E> {
594            Ok(Some(Decimal::from(v)))
595        }
596
597        fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
598            if v.is_nan() || v.is_infinite() {
599                return Err(E::invalid_value(de::Unexpected::Float(v), &self));
600            }
601            Decimal::try_from(v).map(Some).map_err(E::custom)
602        }
603
604        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
605            Ok(None)
606        }
607
608        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
609            Ok(None)
610        }
611    }
612
613    deserializer.deserialize_any(Visitor)
614}
615
616/// Order response from buy/sell/edit operations.
617///
618/// Contains the order details and any trades that resulted from the order.
619#[derive(Debug, Clone, Deserialize)]
620pub struct DeribitOrderResponse {
621    /// The order details.
622    pub order: DeribitOrderMsg,
623    /// Any trades executed as part of this order.
624    #[serde(default)]
625    pub trades: Vec<DeribitUserTradeMsg>,
626}
627
628/// Order message structure from Deribit.
629///
630/// Received from order responses and user.orders subscription.
631#[derive(Debug, Clone, Deserialize)]
632pub struct DeribitOrderMsg {
633    /// Unique order ID assigned by Deribit.
634    pub order_id: String,
635    /// User-defined label (client order ID).
636    pub label: Option<String>,
637    /// Instrument name.
638    pub instrument_name: Ustr,
639    /// Order direction: "buy" or "sell".
640    pub direction: String,
641    /// Order type: "limit", "market", "stop_limit", "stop_market", "take_limit", "take_market".
642    pub order_type: String,
643    /// Order state: "open", "filled", "rejected", "cancelled", "untriggered".
644    pub order_state: String,
645    /// Whether this update reflects an order replacement or amendment.
646    #[serde(default)]
647    pub replaced: bool,
648    /// Limit price (None for market orders, or when Deribit returns the
649    /// literal `"market_price"` for trigger market orders).
650    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_market")]
651    pub price: Option<Decimal>,
652    /// Original order amount in contracts.
653    #[serde(deserialize_with = "nautilus_core::serialization::deserialize_decimal")]
654    pub amount: Decimal,
655    /// Amount filled so far. Deribit omits this field for untriggered trigger
656    /// orders (e.g. `stop_market`, `stop_limit`); treat the missing case as zero.
657    #[serde(default, deserialize_with = "deserialize_decimal")]
658    pub filled_amount: Decimal,
659    /// Average fill price.
660    #[serde(
661        default,
662        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
663    )]
664    pub average_price: Option<Decimal>,
665    /// Order creation timestamp in milliseconds.
666    pub creation_timestamp: u64,
667    /// Last update timestamp in milliseconds.
668    pub last_update_timestamp: u64,
669    /// Time in force setting.
670    pub time_in_force: String,
671    /// Commission paid in base currency.
672    #[serde(
673        default,
674        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
675    )]
676    pub commission: Decimal,
677    /// Post-only flag.
678    #[serde(default)]
679    pub post_only: bool,
680    /// Reduce-only flag.
681    #[serde(default)]
682    pub reduce_only: bool,
683    /// Trigger price for stop/take orders.
684    #[serde(
685        default,
686        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
687    )]
688    pub trigger_price: Option<Decimal>,
689    /// Trigger type: "last_price", "index_price", "mark_price".
690    pub trigger: Option<String>,
691    /// Max show quantity for iceberg orders.
692    #[serde(
693        default,
694        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
695    )]
696    pub max_show: Option<Decimal>,
697    /// API request flag.
698    #[serde(default)]
699    pub api: bool,
700    /// Reject reason if order was rejected.
701    pub reject_reason: Option<String>,
702    /// Cancel reason if order was cancelled.
703    pub cancel_reason: Option<String>,
704}
705
706/// User trade message from Deribit.
707///
708/// Received from order responses and user.trades subscription.
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct DeribitUserTradeMsg {
711    /// Unique trade ID.
712    pub trade_id: String,
713    /// Associated order ID.
714    pub order_id: String,
715    /// Instrument name.
716    pub instrument_name: Ustr,
717    /// Trade direction: "buy" or "sell".
718    pub direction: String,
719    /// Execution price.
720    #[serde(
721        serialize_with = "nautilus_core::serialization::serialize_decimal",
722        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
723    )]
724    pub price: Decimal,
725    /// Trade amount in contracts.
726    #[serde(
727        serialize_with = "nautilus_core::serialization::serialize_decimal",
728        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
729    )]
730    pub amount: Decimal,
731    /// Fee amount.
732    #[serde(
733        serialize_with = "nautilus_core::serialization::serialize_decimal",
734        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
735    )]
736    pub fee: Decimal,
737    /// Fee currency.
738    pub fee_currency: String,
739    /// Trade timestamp in milliseconds.
740    pub timestamp: u64,
741    /// Trade sequence number.
742    pub trade_seq: u64,
743    /// Liquidity: "M" (maker) or "T" (taker).
744    pub liquidity: String,
745    /// Order type.
746    pub order_type: String,
747    /// Index price at trade time.
748    #[serde(
749        serialize_with = "nautilus_core::serialization::serialize_decimal",
750        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
751    )]
752    pub index_price: Decimal,
753    /// Mark price at trade time.
754    #[serde(
755        serialize_with = "nautilus_core::serialization::serialize_decimal",
756        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
757    )]
758    pub mark_price: Decimal,
759    /// Tick direction (0-3).
760    pub tick_direction: i8,
761    /// Order state after this trade.
762    pub state: String,
763    /// User-defined label (client order ID).
764    pub label: Option<String>,
765    /// Reduce-only flag.
766    #[serde(default)]
767    pub reduce_only: bool,
768    /// Post-only flag.
769    #[serde(default)]
770    pub post_only: bool,
771    /// Liquidation indicator for trades caused by liquidation.
772    #[serde(default)]
773    pub liquidation: Option<String>,
774    /// Profit/loss for this trade.
775    #[serde(
776        default,
777        serialize_with = "nautilus_core::serialization::serialize_optional_decimal",
778        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
779    )]
780    pub profit_loss: Option<Decimal>,
781}
782
783/// Portfolio/margin message from user.portfolio subscription.
784#[derive(Debug, Clone, Deserialize)]
785pub struct DeribitPortfolioMsg {
786    /// Currency code (e.g., "BTC", "ETH", "USDC", "USDT").
787    pub currency: String,
788    /// Account equity (balance + unrealized PnL). Used for zero-balance filtering.
789    #[serde(with = "rust_decimal::serde::float")]
790    pub equity: Decimal,
791    /// Account balance. Used for zero-balance filtering.
792    #[serde(with = "rust_decimal::serde::float")]
793    pub balance: Decimal,
794    /// Available funds for trading. Maps to AccountBalance.free.
795    #[serde(with = "rust_decimal::serde::float")]
796    pub available_funds: Decimal,
797    /// Margin balance. Maps to AccountBalance.total.
798    #[serde(with = "rust_decimal::serde::float")]
799    pub margin_balance: Decimal,
800    /// Initial margin requirement. Maps to MarginBalance.initial.
801    #[serde(with = "rust_decimal::serde::float")]
802    pub initial_margin: Decimal,
803    /// Maintenance margin requirement. Maps to MarginBalance.maintenance.
804    #[serde(with = "rust_decimal::serde::float")]
805    pub maintenance_margin: Decimal,
806    /// Margin model (e.g., "segregated_sm", "cross_sm", "cross_pm")
807    #[serde(default)]
808    pub margin_model: Option<String>,
809    /// Whether cross-collateral is enabled for this currency
810    #[serde(default)]
811    pub cross_collateral_enabled: Option<bool>,
812    /// Available withdrawal funds (per-currency withdrawable amount)
813    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
814    pub available_withdrawal_funds: Option<Decimal>,
815}
816
817/// Raw Deribit WebSocket message variants.
818#[derive(Debug, Clone)]
819pub enum DeribitWsMessage {
820    /// JSON-RPC response to a request.
821    Response(DeribitJsonRpcResponse<serde_json::Value>),
822    /// Subscription notification (trade, book, ticker data).
823    Notification(DeribitSubscriptionNotification<serde_json::Value>),
824    /// Heartbeat message.
825    Heartbeat(DeribitHeartbeatData),
826    /// JSON-RPC error.
827    Error(DeribitJsonRpcError),
828    /// Reconnection event (internal).
829    Reconnected,
830}
831
832/// Deribit WebSocket error for external consumers.
833#[derive(Debug, Clone, Serialize, Deserialize)]
834pub struct DeribitWebSocketError {
835    /// Error code from Deribit.
836    pub code: i64,
837    /// Error message.
838    pub message: String,
839    /// Timestamp when error occurred.
840    pub timestamp: u64,
841}
842
843impl From<DeribitJsonRpcError> for DeribitWebSocketError {
844    fn from(err: DeribitJsonRpcError) -> Self {
845        Self {
846            code: err.code,
847            message: err.message,
848            timestamp: 0,
849        }
850    }
851}
852
853/// Normalized Nautilus domain message after parsing.
854#[derive(Debug, Clone)]
855pub enum NautilusWsMessage {
856    /// Market data (trades, bars, quotes).
857    Data(Vec<Data>),
858    /// Order book deltas.
859    Deltas(OrderBookDeltas),
860    /// Instrument definition update.
861    Instrument(Box<InstrumentAny>),
862    /// Funding rate updates (for perpetual instruments).
863    FundingRates(Vec<FundingRateUpdate>),
864    /// Exchange-provided option Greeks from ticker data.
865    OptionGreeks(OptionGreeks),
866    /// Order status reports (for reconciliation, not real-time events).
867    OrderStatusReports(Vec<OrderStatusReport>),
868    /// Fill reports from user.trades subscription or order responses.
869    FillReports(Vec<FillReport>),
870    /// Fill for an order tracked by this execution client.
871    OrderFilled(OrderFilled),
872    /// Order accepted by venue.
873    OrderAccepted(OrderAccepted),
874    /// Order canceled by venue or user.
875    OrderCanceled(OrderCanceled),
876    /// Order expired.
877    OrderExpired(OrderExpired),
878    /// Order rejected by venue.
879    OrderRejected(OrderRejected),
880    /// Cancel request rejected by venue.
881    OrderCancelRejected(OrderCancelRejected),
882    /// Modify request rejected by venue.
883    OrderModifyRejected(OrderModifyRejected),
884    /// Order updated (price/quantity amended).
885    OrderUpdated(OrderUpdated),
886    /// Account state update from user.portfolio subscription.
887    AccountState(AccountState),
888    /// Instrument status change.
889    InstrumentStatus(InstrumentStatus),
890    /// Error from venue.
891    Error(DeribitWsError),
892    /// Unhandled/raw message for debugging.
893    Raw(serde_json::Value),
894    /// Reconnection completed.
895    Reconnected,
896    /// Authentication succeeded with tokens.
897    Authenticated(Box<DeribitAuthResult>),
898    /// Authentication failed with reason.
899    AuthenticationFailed(String),
900}
901
902/// Parses a raw JSON message into a DeribitWsMessage.
903///
904/// # Errors
905///
906/// Returns an error if JSON parsing fails or the message format is unrecognized.
907pub fn parse_raw_message(text: &str) -> Result<DeribitWsMessage, DeribitWsError> {
908    let value: serde_json::Value =
909        serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
910
911    // Check for subscription notification (has "method": "subscription")
912    if let Some(method) = value.get("method").and_then(|m| m.as_str()) {
913        if method == "subscription" {
914            let notification: DeribitSubscriptionNotification<serde_json::Value> =
915                serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
916            return Ok(DeribitWsMessage::Notification(notification));
917        }
918        // Check for heartbeat
919        if method == "heartbeat"
920            && let Some(params) = value.get("params")
921        {
922            let heartbeat: DeribitHeartbeatData = serde_json::from_value(params.clone())
923                .map_err(|e| DeribitWsError::Json(e.to_string()))?;
924            return Ok(DeribitWsMessage::Heartbeat(heartbeat));
925        }
926    }
927
928    // Check for JSON-RPC response (has "id" field)
929    // IMPORTANT: Both success and error responses should be returned as Response
930    // so the handler can correlate them with pending requests using the ID.
931    // This allows proper cleanup of pending_requests and emission of rejection events.
932    if value.get("id").is_some() {
933        let response: DeribitJsonRpcResponse<serde_json::Value> =
934            serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
935        return Ok(DeribitWsMessage::Response(response));
936    }
937
938    // Fallback: try to parse as generic response
939    let response: DeribitJsonRpcResponse<serde_json::Value> =
940        serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
941    Ok(DeribitWsMessage::Response(response))
942}
943
944/// Extracts the instrument name from a channel string.
945///
946/// For example: "trades.BTC-PERPETUAL.raw" -> "BTC-PERPETUAL"
947pub fn extract_instrument_from_channel(channel: &str) -> Option<&str> {
948    let parts: Vec<&str> = channel.split('.').collect();
949    if parts.len() >= 2 {
950        Some(parts[1])
951    } else {
952        None
953    }
954}
955
956#[cfg(test)]
957mod tests {
958    use rstest::rstest;
959
960    use super::*;
961
962    #[rstest]
963    fn test_parse_subscription_notification() {
964        let json = r#"{
965            "jsonrpc": "2.0",
966            "method": "subscription",
967            "params": {
968                "channel": "trades.BTC-PERPETUAL.raw",
969                "data": [{"trade_id": "123", "price": 50000.0}]
970            }
971        }"#;
972
973        let msg = parse_raw_message(json).unwrap();
974        assert!(matches!(msg, DeribitWsMessage::Notification(_)));
975    }
976
977    #[rstest]
978    fn test_parse_response() {
979        let json = r#"{
980            "jsonrpc": "2.0",
981            "id": 1,
982            "result": ["trades.BTC-PERPETUAL.raw"],
983            "testnet": true,
984            "usIn": 1234567890,
985            "usOut": 1234567891,
986            "usDiff": 1
987        }"#;
988
989        let msg = parse_raw_message(json).unwrap();
990        assert!(matches!(msg, DeribitWsMessage::Response(_)));
991    }
992
993    #[rstest]
994    fn test_parse_error_response() {
995        // Error responses with an ID are returned as Response (not Error)
996        // so the handler can correlate them with pending requests
997        let json = r#"{
998            "jsonrpc": "2.0",
999            "id": 1,
1000            "error": {
1001                "code": 10028,
1002                "message": "too_many_requests"
1003            }
1004        }"#;
1005
1006        let msg = parse_raw_message(json).unwrap();
1007        match msg {
1008            DeribitWsMessage::Response(resp) => {
1009                assert!(resp.error.is_some());
1010                let error = resp.error.unwrap();
1011                assert_eq!(error.code, 10028);
1012                assert_eq!(error.message, "too_many_requests");
1013            }
1014            _ => panic!("Expected Response with error, was {msg:?}"),
1015        }
1016    }
1017
1018    #[rstest]
1019    fn test_extract_instrument_from_channel() {
1020        assert_eq!(
1021            extract_instrument_from_channel("trades.BTC-PERPETUAL.raw"),
1022            Some("BTC-PERPETUAL")
1023        );
1024        assert_eq!(
1025            extract_instrument_from_channel("book.ETH-25DEC25.raw"),
1026            Some("ETH-25DEC25")
1027        );
1028        assert_eq!(extract_instrument_from_channel("platform_state"), None);
1029    }
1030
1031    #[rstest]
1032    fn test_parse_volatility_index_payload() {
1033        let value = serde_json::json!({
1034            "timestamp": 1619777946007_u64,
1035            "volatility": 129.36_f64,
1036            "index_name": "btc_usd",
1037        });
1038
1039        let payload: DeribitVolatilityIndexMsg = serde_json::from_value(value).unwrap();
1040        assert_eq!(payload.index_name, "btc_usd");
1041        assert_eq!(payload.volatility, 129.36);
1042        assert_eq!(payload.timestamp, 1619777946007_u64);
1043    }
1044}