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,
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    /// Limit price (None for market orders, or when Deribit returns the
646    /// literal `"market_price"` for trigger market orders).
647    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_market")]
648    pub price: Option<Decimal>,
649    /// Original order amount in contracts.
650    #[serde(deserialize_with = "nautilus_core::serialization::deserialize_decimal")]
651    pub amount: Decimal,
652    /// Amount filled so far. Deribit omits this field for untriggered trigger
653    /// orders (e.g. `stop_market`, `stop_limit`); treat the missing case as zero.
654    #[serde(default, deserialize_with = "deserialize_decimal")]
655    pub filled_amount: Decimal,
656    /// Average fill price.
657    #[serde(
658        default,
659        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
660    )]
661    pub average_price: Option<Decimal>,
662    /// Order creation timestamp in milliseconds.
663    pub creation_timestamp: u64,
664    /// Last update timestamp in milliseconds.
665    pub last_update_timestamp: u64,
666    /// Time in force setting.
667    pub time_in_force: String,
668    /// Commission paid in base currency.
669    #[serde(
670        default,
671        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
672    )]
673    pub commission: Decimal,
674    /// Post-only flag.
675    #[serde(default)]
676    pub post_only: bool,
677    /// Reduce-only flag.
678    #[serde(default)]
679    pub reduce_only: bool,
680    /// Trigger price for stop/take orders.
681    #[serde(
682        default,
683        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
684    )]
685    pub trigger_price: Option<Decimal>,
686    /// Trigger type: "last_price", "index_price", "mark_price".
687    pub trigger: Option<String>,
688    /// Max show quantity for iceberg orders.
689    #[serde(
690        default,
691        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
692    )]
693    pub max_show: Option<Decimal>,
694    /// API request flag.
695    #[serde(default)]
696    pub api: bool,
697    /// Reject reason if order was rejected.
698    pub reject_reason: Option<String>,
699    /// Cancel reason if order was cancelled.
700    pub cancel_reason: Option<String>,
701}
702
703/// User trade message from Deribit.
704///
705/// Received from order responses and user.trades subscription.
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct DeribitUserTradeMsg {
708    /// Unique trade ID.
709    pub trade_id: String,
710    /// Associated order ID.
711    pub order_id: String,
712    /// Instrument name.
713    pub instrument_name: Ustr,
714    /// Trade direction: "buy" or "sell".
715    pub direction: String,
716    /// Execution price.
717    #[serde(
718        serialize_with = "nautilus_core::serialization::serialize_decimal",
719        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
720    )]
721    pub price: Decimal,
722    /// Trade amount in contracts.
723    #[serde(
724        serialize_with = "nautilus_core::serialization::serialize_decimal",
725        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
726    )]
727    pub amount: Decimal,
728    /// Fee amount.
729    #[serde(
730        serialize_with = "nautilus_core::serialization::serialize_decimal",
731        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
732    )]
733    pub fee: Decimal,
734    /// Fee currency.
735    pub fee_currency: String,
736    /// Trade timestamp in milliseconds.
737    pub timestamp: u64,
738    /// Trade sequence number.
739    pub trade_seq: u64,
740    /// Liquidity: "M" (maker) or "T" (taker).
741    pub liquidity: String,
742    /// Order type.
743    pub order_type: String,
744    /// Index price at trade time.
745    #[serde(
746        serialize_with = "nautilus_core::serialization::serialize_decimal",
747        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
748    )]
749    pub index_price: Decimal,
750    /// Mark price at trade time.
751    #[serde(
752        serialize_with = "nautilus_core::serialization::serialize_decimal",
753        deserialize_with = "nautilus_core::serialization::deserialize_decimal"
754    )]
755    pub mark_price: Decimal,
756    /// Tick direction (0-3).
757    pub tick_direction: i8,
758    /// Order state after this trade.
759    pub state: String,
760    /// User-defined label (client order ID).
761    pub label: Option<String>,
762    /// Reduce-only flag.
763    #[serde(default)]
764    pub reduce_only: bool,
765    /// Post-only flag.
766    #[serde(default)]
767    pub post_only: bool,
768    /// Liquidation indicator for trades caused by liquidation.
769    #[serde(default)]
770    pub liquidation: Option<String>,
771    /// Profit/loss for this trade.
772    #[serde(
773        default,
774        serialize_with = "nautilus_core::serialization::serialize_optional_decimal",
775        deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
776    )]
777    pub profit_loss: Option<Decimal>,
778}
779
780/// Portfolio/margin message from user.portfolio subscription.
781#[derive(Debug, Clone, Deserialize)]
782pub struct DeribitPortfolioMsg {
783    /// Currency code (e.g., "BTC", "ETH", "USDC", "USDT").
784    pub currency: String,
785    /// Account equity (balance + unrealized PnL). Used for zero-balance filtering.
786    #[serde(with = "rust_decimal::serde::float")]
787    pub equity: Decimal,
788    /// Account balance. Used for zero-balance filtering.
789    #[serde(with = "rust_decimal::serde::float")]
790    pub balance: Decimal,
791    /// Available funds for trading. Maps to AccountBalance.free.
792    #[serde(with = "rust_decimal::serde::float")]
793    pub available_funds: Decimal,
794    /// Margin balance. Maps to AccountBalance.total.
795    #[serde(with = "rust_decimal::serde::float")]
796    pub margin_balance: Decimal,
797    /// Initial margin requirement. Maps to MarginBalance.initial.
798    #[serde(with = "rust_decimal::serde::float")]
799    pub initial_margin: Decimal,
800    /// Maintenance margin requirement. Maps to MarginBalance.maintenance.
801    #[serde(with = "rust_decimal::serde::float")]
802    pub maintenance_margin: Decimal,
803    /// Margin model (e.g., "segregated_sm", "cross_sm", "cross_pm")
804    #[serde(default)]
805    pub margin_model: Option<String>,
806    /// Whether cross-collateral is enabled for this currency
807    #[serde(default)]
808    pub cross_collateral_enabled: Option<bool>,
809    /// Available withdrawal funds (per-currency withdrawable amount)
810    #[serde(default, deserialize_with = "deserialize_optional_decimal")]
811    pub available_withdrawal_funds: Option<Decimal>,
812}
813
814/// Raw Deribit WebSocket message variants.
815#[derive(Debug, Clone)]
816pub enum DeribitWsMessage {
817    /// JSON-RPC response to a request.
818    Response(DeribitJsonRpcResponse<serde_json::Value>),
819    /// Subscription notification (trade, book, ticker data).
820    Notification(DeribitSubscriptionNotification<serde_json::Value>),
821    /// Heartbeat message.
822    Heartbeat(DeribitHeartbeatData),
823    /// JSON-RPC error.
824    Error(DeribitJsonRpcError),
825    /// Reconnection event (internal).
826    Reconnected,
827}
828
829/// Deribit WebSocket error for external consumers.
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct DeribitWebSocketError {
832    /// Error code from Deribit.
833    pub code: i64,
834    /// Error message.
835    pub message: String,
836    /// Timestamp when error occurred.
837    pub timestamp: u64,
838}
839
840impl From<DeribitJsonRpcError> for DeribitWebSocketError {
841    fn from(err: DeribitJsonRpcError) -> Self {
842        Self {
843            code: err.code,
844            message: err.message,
845            timestamp: 0,
846        }
847    }
848}
849
850/// Normalized Nautilus domain message after parsing.
851#[derive(Debug, Clone)]
852pub enum NautilusWsMessage {
853    /// Market data (trades, bars, quotes).
854    Data(Vec<Data>),
855    /// Order book deltas.
856    Deltas(OrderBookDeltas),
857    /// Instrument definition update.
858    Instrument(Box<InstrumentAny>),
859    /// Funding rate updates (for perpetual instruments).
860    FundingRates(Vec<FundingRateUpdate>),
861    /// Exchange-provided option Greeks from ticker data.
862    OptionGreeks(OptionGreeks),
863    /// Order status reports (for reconciliation, not real-time events).
864    OrderStatusReports(Vec<OrderStatusReport>),
865    /// Fill reports from user.trades subscription or order responses.
866    FillReports(Vec<FillReport>),
867    /// Order accepted by venue.
868    OrderAccepted(OrderAccepted),
869    /// Order canceled by venue or user.
870    OrderCanceled(OrderCanceled),
871    /// Order expired.
872    OrderExpired(OrderExpired),
873    /// Order rejected by venue.
874    OrderRejected(OrderRejected),
875    /// Cancel request rejected by venue.
876    OrderCancelRejected(OrderCancelRejected),
877    /// Modify request rejected by venue.
878    OrderModifyRejected(OrderModifyRejected),
879    /// Order updated (price/quantity amended).
880    OrderUpdated(OrderUpdated),
881    /// Account state update from user.portfolio subscription.
882    AccountState(AccountState),
883    /// Instrument status change.
884    InstrumentStatus(InstrumentStatus),
885    /// Error from venue.
886    Error(DeribitWsError),
887    /// Unhandled/raw message for debugging.
888    Raw(serde_json::Value),
889    /// Reconnection completed.
890    Reconnected,
891    /// Authentication succeeded with tokens.
892    Authenticated(Box<DeribitAuthResult>),
893    /// Authentication failed with reason.
894    AuthenticationFailed(String),
895}
896
897/// Parses a raw JSON message into a DeribitWsMessage.
898///
899/// # Errors
900///
901/// Returns an error if JSON parsing fails or the message format is unrecognized.
902pub fn parse_raw_message(text: &str) -> Result<DeribitWsMessage, DeribitWsError> {
903    let value: serde_json::Value =
904        serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
905
906    // Check for subscription notification (has "method": "subscription")
907    if let Some(method) = value.get("method").and_then(|m| m.as_str()) {
908        if method == "subscription" {
909            let notification: DeribitSubscriptionNotification<serde_json::Value> =
910                serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
911            return Ok(DeribitWsMessage::Notification(notification));
912        }
913        // Check for heartbeat
914        if method == "heartbeat"
915            && let Some(params) = value.get("params")
916        {
917            let heartbeat: DeribitHeartbeatData = serde_json::from_value(params.clone())
918                .map_err(|e| DeribitWsError::Json(e.to_string()))?;
919            return Ok(DeribitWsMessage::Heartbeat(heartbeat));
920        }
921    }
922
923    // Check for JSON-RPC response (has "id" field)
924    // IMPORTANT: Both success and error responses should be returned as Response
925    // so the handler can correlate them with pending requests using the ID.
926    // This allows proper cleanup of pending_requests and emission of rejection events.
927    if value.get("id").is_some() {
928        let response: DeribitJsonRpcResponse<serde_json::Value> =
929            serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
930        return Ok(DeribitWsMessage::Response(response));
931    }
932
933    // Fallback: try to parse as generic response
934    let response: DeribitJsonRpcResponse<serde_json::Value> =
935        serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
936    Ok(DeribitWsMessage::Response(response))
937}
938
939/// Extracts the instrument name from a channel string.
940///
941/// For example: "trades.BTC-PERPETUAL.raw" -> "BTC-PERPETUAL"
942pub fn extract_instrument_from_channel(channel: &str) -> Option<&str> {
943    let parts: Vec<&str> = channel.split('.').collect();
944    if parts.len() >= 2 {
945        Some(parts[1])
946    } else {
947        None
948    }
949}
950
951#[cfg(test)]
952mod tests {
953    use rstest::rstest;
954
955    use super::*;
956
957    #[rstest]
958    fn test_parse_subscription_notification() {
959        let json = r#"{
960            "jsonrpc": "2.0",
961            "method": "subscription",
962            "params": {
963                "channel": "trades.BTC-PERPETUAL.raw",
964                "data": [{"trade_id": "123", "price": 50000.0}]
965            }
966        }"#;
967
968        let msg = parse_raw_message(json).unwrap();
969        assert!(matches!(msg, DeribitWsMessage::Notification(_)));
970    }
971
972    #[rstest]
973    fn test_parse_response() {
974        let json = r#"{
975            "jsonrpc": "2.0",
976            "id": 1,
977            "result": ["trades.BTC-PERPETUAL.raw"],
978            "testnet": true,
979            "usIn": 1234567890,
980            "usOut": 1234567891,
981            "usDiff": 1
982        }"#;
983
984        let msg = parse_raw_message(json).unwrap();
985        assert!(matches!(msg, DeribitWsMessage::Response(_)));
986    }
987
988    #[rstest]
989    fn test_parse_error_response() {
990        // Error responses with an ID are returned as Response (not Error)
991        // so the handler can correlate them with pending requests
992        let json = r#"{
993            "jsonrpc": "2.0",
994            "id": 1,
995            "error": {
996                "code": 10028,
997                "message": "too_many_requests"
998            }
999        }"#;
1000
1001        let msg = parse_raw_message(json).unwrap();
1002        match msg {
1003            DeribitWsMessage::Response(resp) => {
1004                assert!(resp.error.is_some());
1005                let error = resp.error.unwrap();
1006                assert_eq!(error.code, 10028);
1007                assert_eq!(error.message, "too_many_requests");
1008            }
1009            _ => panic!("Expected Response with error, was {msg:?}"),
1010        }
1011    }
1012
1013    #[rstest]
1014    fn test_extract_instrument_from_channel() {
1015        assert_eq!(
1016            extract_instrument_from_channel("trades.BTC-PERPETUAL.raw"),
1017            Some("BTC-PERPETUAL")
1018        );
1019        assert_eq!(
1020            extract_instrument_from_channel("book.ETH-25DEC25.raw"),
1021            Some("ETH-25DEC25")
1022        );
1023        assert_eq!(extract_instrument_from_channel("platform_state"), None);
1024    }
1025
1026    #[rstest]
1027    fn test_parse_volatility_index_payload() {
1028        let value = serde_json::json!({
1029            "timestamp": 1619777946007_u64,
1030            "volatility": 129.36_f64,
1031            "index_name": "btc_usd",
1032        });
1033
1034        let payload: DeribitVolatilityIndexMsg = serde_json::from_value(value).unwrap();
1035        assert_eq!(payload.index_name, "btc_usd");
1036        assert_eq!(payload.volatility, 129.36);
1037        assert_eq!(payload.timestamp, 1619777946007_u64);
1038    }
1039}