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