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