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