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