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