Skip to main content

nautilus_deribit/websocket/
enums.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//! Enumerations for Deribit WebSocket channels and operations.
17
18use std::fmt::Display;
19
20use nautilus_model::enums::BookAction;
21use serde::{Deserialize, Serialize};
22use strum::{AsRefStr, Display, EnumIter, EnumString};
23
24/// Deribit data stream update intervals.
25///
26/// Controls how frequently updates are sent for subscribed channels.
27/// Raw updates require authentication while aggregated updates are public.
28#[derive(
29    Clone,
30    Copy,
31    Debug,
32    Default,
33    PartialEq,
34    Eq,
35    Hash,
36    AsRefStr,
37    EnumIter,
38    EnumString,
39    Serialize,
40    Deserialize,
41)]
42#[serde(rename_all = "snake_case")]
43pub enum DeribitUpdateInterval {
44    /// Raw updates - immediate delivery of each event.
45    /// Requires authentication.
46    #[strum(serialize = "raw", serialize = "Raw")]
47    Raw,
48    /// Aggregated updates every 100 milliseconds (default).
49    #[default]
50    #[strum(serialize = "100ms", serialize = "Ms100")]
51    Ms100,
52    /// Aggregated updates every 2 ticks.
53    #[strum(serialize = "agg2", serialize = "Agg2")]
54    Agg2,
55}
56
57impl DeribitUpdateInterval {
58    /// Returns the string representation for Deribit channel subscription.
59    #[must_use]
60    pub const fn as_str(&self) -> &'static str {
61        match self {
62            Self::Raw => "raw",
63            Self::Ms100 => "100ms",
64            Self::Agg2 => "agg2",
65        }
66    }
67
68    /// Returns whether this interval requires authentication.
69    #[must_use]
70    pub const fn requires_auth(&self) -> bool {
71        matches!(self, Self::Raw)
72    }
73}
74
75impl Display for DeribitUpdateInterval {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.as_str())
78    }
79}
80
81/// Deribit WebSocket public data channels.
82///
83/// Channels follow the format: `{channel_type}.{instrument_or_currency}.{interval}`
84#[derive(
85    Clone,
86    Copy,
87    Debug,
88    Display,
89    PartialEq,
90    Eq,
91    Hash,
92    AsRefStr,
93    EnumIter,
94    EnumString,
95    Serialize,
96    Deserialize,
97)]
98pub enum DeribitWsChannel {
99    // Public Market Data Channels
100    /// Raw trade stream: `trades.{instrument}.raw`
101    Trades,
102    /// Order book updates: `book.{instrument}.{group}.{depth}.{interval}`
103    Book,
104    /// Ticker updates: `ticker.{instrument}.{interval}`
105    Ticker,
106    /// Quote updates (best bid/ask): `quote.{instrument}`
107    Quote,
108    /// Index price: `deribit_price_index.{currency}`
109    PriceIndex,
110    /// Price ranking: `deribit_price_ranking.{currency}`
111    PriceRanking,
112    /// Volatility index: `deribit_volatility_index.{currency}`
113    VolatilityIndex,
114    /// Estimated expiration price: `estimated_expiration_price.{currency}`
115    EstimatedExpirationPrice,
116    /// Perpetual interest rate: `perpetual.{instrument}.{interval}`
117    Perpetual,
118    /// Mark price options: `markprice.options.{currency}`
119    MarkPriceOptions,
120    /// Platform state: `platform_state`
121    PlatformState,
122    /// Announcements: `announcements`
123    Announcements,
124    /// Chart trades: `chart.trades.{instrument}.{resolution}`
125    ChartTrades,
126    /// Instrument state changes: `instrument.state.{kind}.{currency}`
127    /// Used for instrument lifecycle notifications (created, started, settled, closed, terminated)
128    InstrumentState,
129
130    // Private User Channels (for future execution support)
131    /// User orders: `user.orders.{instrument}.{interval}`
132    UserOrders,
133    /// User trades/fills: `user.trades.{instrument}.{interval}`
134    UserTrades,
135    /// User portfolio: `user.portfolio.{currency}`
136    UserPortfolio,
137    /// User changes (combined orders/trades/positions): `user.changes.{instrument}.{interval}`
138    UserChanges,
139    /// User access log: `user.access_log`
140    UserAccessLog,
141}
142
143impl DeribitWsChannel {
144    /// Formats the channel name for subscription with the given instrument or currency.
145    ///
146    /// Returns the full channel string for Deribit subscription.
147    ///
148    /// # Arguments
149    ///
150    /// * `instrument_or_currency` - The instrument name (e.g., "BTC-PERPETUAL") or currency (e.g., "BTC")
151    /// * `interval` - Optional update interval. Defaults to `Ms100` (100ms) if not specified.
152    ///
153    /// # Panics
154    ///
155    /// Panics if called on `InstrumentState` variant. Use `format_instrument_state_channel()` instead.
156    ///
157    /// # Note
158    ///
159    /// `Raw` subscriptions require authentication. Use `Ms100` for public/unauthenticated access.
160    #[must_use]
161    pub fn format_channel(
162        &self,
163        instrument_or_currency: &str,
164        interval: Option<DeribitUpdateInterval>,
165    ) -> String {
166        let interval_str = interval.unwrap_or_default().as_str();
167        match self {
168            Self::Trades => format!("trades.{instrument_or_currency}.{interval_str}"),
169            Self::Book => format!("book.{instrument_or_currency}.{interval_str}"),
170            Self::Ticker => format!("ticker.{instrument_or_currency}.{interval_str}"),
171            Self::Quote => format!("quote.{instrument_or_currency}"),
172            Self::PriceIndex => format!("deribit_price_index.{instrument_or_currency}"),
173            Self::PriceRanking => format!("deribit_price_ranking.{instrument_or_currency}"),
174            Self::VolatilityIndex => format!("deribit_volatility_index.{instrument_or_currency}"),
175            Self::EstimatedExpirationPrice => {
176                format!("estimated_expiration_price.{instrument_or_currency}")
177            }
178            Self::Perpetual => format!("perpetual.{instrument_or_currency}.{interval_str}"),
179            Self::MarkPriceOptions => format!("markprice.options.{instrument_or_currency}"),
180            Self::PlatformState => "platform_state".to_string(),
181            Self::Announcements => "announcements".to_string(),
182            Self::ChartTrades => format!("chart.trades.{instrument_or_currency}.{interval_str}"),
183            Self::UserOrders => format!("user.orders.{instrument_or_currency}.{interval_str}"),
184            Self::UserTrades => format!("user.trades.{instrument_or_currency}.{interval_str}"),
185            Self::UserPortfolio => format!("user.portfolio.{instrument_or_currency}"),
186            Self::UserChanges => format!("user.changes.{instrument_or_currency}.{interval_str}"),
187            Self::UserAccessLog => "user.access_log".to_string(),
188            Self::InstrumentState => {
189                // InstrumentState requires kind and currency, use format_instrument_state_channel() instead
190                panic!(
191                    "InstrumentState channel requires kind and currency parameters, use format_instrument_state_channel() instead"
192                )
193            }
194        }
195    }
196
197    /// Formats the instrument status channel for subscription.
198    ///
199    /// Returns the full channel string: `instrument.state.{kind}.{currency}`
200    ///
201    /// # Arguments
202    ///
203    /// * `kind` - Instrument kind: "future", "option", "spot", "future_combo", "option_combo", or "any"
204    /// * `currency` - Currency: "BTC", "ETH", "USDC", "USDT", "EURR", or "any"
205    #[must_use]
206    pub fn format_instrument_state_channel(kind: &str, currency: &str) -> String {
207        format!("instrument.state.{kind}.{currency}")
208    }
209
210    /// Parses a channel string to extract the channel type.
211    ///
212    /// Returns the channel enum variant if recognized.
213    #[must_use]
214    pub fn from_channel_string(channel: &str) -> Option<Self> {
215        if channel.starts_with("trades.") {
216            Some(Self::Trades)
217        } else if channel.starts_with("book.") {
218            Some(Self::Book)
219        } else if channel.starts_with("ticker.") {
220            Some(Self::Ticker)
221        } else if channel.starts_with("quote.") {
222            Some(Self::Quote)
223        } else if channel.starts_with("deribit_price_index.") {
224            Some(Self::PriceIndex)
225        } else if channel.starts_with("deribit_price_ranking.") {
226            Some(Self::PriceRanking)
227        } else if channel.starts_with("deribit_volatility_index.") {
228            Some(Self::VolatilityIndex)
229        } else if channel.starts_with("estimated_expiration_price.") {
230            Some(Self::EstimatedExpirationPrice)
231        } else if channel.starts_with("perpetual.") {
232            Some(Self::Perpetual)
233        } else if channel.starts_with("markprice.options.") {
234            Some(Self::MarkPriceOptions)
235        } else if channel == "platform_state" {
236            Some(Self::PlatformState)
237        } else if channel == "announcements" {
238            Some(Self::Announcements)
239        } else if channel.starts_with("chart.trades.") {
240            Some(Self::ChartTrades)
241        } else if channel.starts_with("user.orders.") {
242            Some(Self::UserOrders)
243        } else if channel.starts_with("user.trades.") {
244            Some(Self::UserTrades)
245        } else if channel.starts_with("user.portfolio.") {
246            Some(Self::UserPortfolio)
247        } else if channel.starts_with("user.changes.") {
248            Some(Self::UserChanges)
249        } else if channel == "user.access_log" {
250            Some(Self::UserAccessLog)
251        } else if channel.starts_with("instrument.state.") {
252            Some(Self::InstrumentState)
253        } else {
254            None
255        }
256    }
257
258    /// Returns whether this is a private (authenticated) channel.
259    #[must_use]
260    pub const fn is_private(&self) -> bool {
261        matches!(
262            self,
263            Self::UserOrders
264                | Self::UserTrades
265                | Self::UserPortfolio
266                | Self::UserChanges
267                | Self::UserAccessLog
268        )
269    }
270
271    /// Returns whether a channel string requires authentication.
272    ///
273    /// This includes private `user.*` channels and any channel with
274    /// a `.raw` interval (book, trades, ticker) which Deribit gates
275    /// behind auth.
276    #[must_use]
277    pub fn requires_auth(channel: &str) -> bool {
278        match Self::from_channel_string(channel) {
279            Some(ch) if ch.is_private() => true,
280            Some(_) => channel.ends_with(".raw"),
281            None => false,
282        }
283    }
284}
285
286/// Deribit JSON-RPC WebSocket methods.
287#[derive(
288    Clone,
289    Debug,
290    Display,
291    PartialEq,
292    Eq,
293    Hash,
294    AsRefStr,
295    EnumIter,
296    EnumString,
297    Serialize,
298    Deserialize,
299)]
300pub enum DeribitWsMethod {
301    // Public methods
302    /// Subscribe to public channels.
303    #[serde(rename = "public/subscribe")]
304    #[strum(serialize = "public/subscribe")]
305    PublicSubscribe,
306    /// Unsubscribe from public channels.
307    #[serde(rename = "public/unsubscribe")]
308    #[strum(serialize = "public/unsubscribe")]
309    PublicUnsubscribe,
310    /// Authenticate with API credentials.
311    #[serde(rename = "public/auth")]
312    #[strum(serialize = "public/auth")]
313    PublicAuth,
314    /// Enable heartbeat mechanism.
315    #[serde(rename = "public/set_heartbeat")]
316    #[strum(serialize = "public/set_heartbeat")]
317    SetHeartbeat,
318    /// Disable heartbeat mechanism.
319    #[serde(rename = "public/disable_heartbeat")]
320    #[strum(serialize = "public/disable_heartbeat")]
321    DisableHeartbeat,
322    /// Test connectivity (used for heartbeat response).
323    #[serde(rename = "public/test")]
324    #[strum(serialize = "public/test")]
325    Test,
326    /// Hello/handshake message.
327    #[serde(rename = "public/hello")]
328    #[strum(serialize = "public/hello")]
329    Hello,
330    /// Get server time.
331    #[serde(rename = "public/get_time")]
332    #[strum(serialize = "public/get_time")]
333    GetTime,
334
335    /// Subscribe to private channels.
336    #[serde(rename = "private/subscribe")]
337    #[strum(serialize = "private/subscribe")]
338    PrivateSubscribe,
339    /// Unsubscribe from private channels.
340    #[serde(rename = "private/unsubscribe")]
341    #[strum(serialize = "private/unsubscribe")]
342    PrivateUnsubscribe,
343    /// Logout and close session.
344    #[serde(rename = "private/logout")]
345    #[strum(serialize = "private/logout")]
346    Logout,
347    /// Submit a buy order.
348    #[serde(rename = "private/buy")]
349    #[strum(serialize = "private/buy")]
350    Buy,
351    /// Submit a sell order.
352    #[serde(rename = "private/sell")]
353    #[strum(serialize = "private/sell")]
354    Sell,
355    /// Modify an order.
356    #[serde(rename = "private/edit")]
357    #[strum(serialize = "private/edit")]
358    Edit,
359    /// Cancel an order.
360    #[serde(rename = "private/cancel")]
361    #[strum(serialize = "private/cancel")]
362    Cancel,
363    /// Cancel all orders for an instrument.
364    #[serde(rename = "private/cancel_all_by_instrument")]
365    #[strum(serialize = "private/cancel_all_by_instrument")]
366    CancelAllByInstrument,
367    /// Get order state.
368    #[serde(rename = "private/get_order_state")]
369    #[strum(serialize = "private/get_order_state")]
370    GetOrderState,
371}
372
373impl DeribitWsMethod {
374    /// Returns the JSON-RPC method string.
375    #[must_use]
376    pub fn as_method_str(&self) -> &str {
377        self.as_ref()
378    }
379}
380
381/// Deribit order book update action types.
382#[derive(
383    Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumString, Serialize, Deserialize,
384)]
385#[serde(rename_all = "snake_case")]
386#[strum(serialize_all = "snake_case")]
387pub enum DeribitBookAction {
388    /// New price level added.
389    #[serde(rename = "new")]
390    New,
391    /// Existing price level changed.
392    #[serde(rename = "change")]
393    Change,
394    /// Price level removed.
395    #[serde(rename = "delete")]
396    Delete,
397}
398
399impl From<DeribitBookAction> for BookAction {
400    fn from(action: DeribitBookAction) -> Self {
401        match action {
402            DeribitBookAction::New => Self::Add,
403            DeribitBookAction::Change => Self::Update,
404            DeribitBookAction::Delete => Self::Delete,
405        }
406    }
407}
408
409/// Deribit order book message type.
410#[derive(
411    Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumString, Serialize, Deserialize,
412)]
413#[serde(rename_all = "snake_case")]
414pub enum DeribitBookMsgType {
415    /// Full order book snapshot.
416    #[serde(rename = "snapshot")]
417    Snapshot,
418    /// Incremental update.
419    #[serde(rename = "change")]
420    Change,
421}
422
423#[cfg(test)]
424mod tests {
425    use rstest::rstest;
426
427    use super::*;
428
429    #[rstest]
430    fn test_requires_auth_user_channels() {
431        assert!(DeribitWsChannel::requires_auth("user.orders.any.any.raw"));
432        assert!(DeribitWsChannel::requires_auth("user.trades.any.any.raw"));
433        assert!(DeribitWsChannel::requires_auth("user.portfolio.any"));
434        assert!(DeribitWsChannel::requires_auth("user.changes.any.any.raw"));
435        assert!(DeribitWsChannel::requires_auth("user.access_log"));
436    }
437
438    #[rstest]
439    fn test_requires_auth_raw_channels() {
440        assert!(DeribitWsChannel::requires_auth("book.BTC-PERPETUAL.raw"));
441        assert!(DeribitWsChannel::requires_auth("book.ETH-25DEC25.raw"));
442        assert!(DeribitWsChannel::requires_auth("trades.BTC-PERPETUAL.raw"));
443        assert!(DeribitWsChannel::requires_auth("ticker.BTC-PERPETUAL.raw"));
444    }
445
446    #[rstest]
447    fn test_requires_auth_public_channels() {
448        assert!(!DeribitWsChannel::requires_auth(
449            "book.BTC-PERPETUAL.none.10.100ms"
450        ));
451        assert!(!DeribitWsChannel::requires_auth(
452            "book.BTC-PERPETUAL.none.20.agg2"
453        ));
454        assert!(!DeribitWsChannel::requires_auth(
455            "trades.BTC-PERPETUAL.100ms"
456        ));
457        assert!(!DeribitWsChannel::requires_auth(
458            "ticker.BTC-PERPETUAL.100ms"
459        ));
460        assert!(!DeribitWsChannel::requires_auth("quote.BTC-PERPETUAL"));
461        assert!(!DeribitWsChannel::requires_auth("deribit_price_index.btc"));
462        assert!(!DeribitWsChannel::requires_auth("platform_state"));
463        assert!(!DeribitWsChannel::requires_auth("announcements"));
464    }
465}
466
467/// Deribit heartbeat types.
468#[derive(
469    Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumString, Serialize, Deserialize,
470)]
471#[serde(rename_all = "snake_case")]
472pub enum DeribitHeartbeatType {
473    /// Server heartbeat notification.
474    #[serde(rename = "heartbeat")]
475    Heartbeat,
476    /// Server requesting client response.
477    #[serde(rename = "test_request")]
478    TestRequest,
479}