1use std::fmt::Display;
19
20use nautilus_model::enums::BookAction;
21use serde::{Deserialize, Serialize};
22use strum::{AsRefStr, Display, EnumIter, EnumString};
23
24#[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 #[strum(serialize = "raw", serialize = "Raw")]
61 Raw,
62 #[default]
64 #[strum(serialize = "100ms", serialize = "Ms100")]
65 Ms100,
66 #[strum(serialize = "agg2", serialize = "Agg2")]
68 Agg2,
69}
70
71impl DeribitUpdateInterval {
72 #[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 #[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#[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 Trades,
129 Book,
131 Ticker,
133 Quote,
135 PriceIndex,
137 PriceRanking,
139 VolatilityIndex,
141 EstimatedExpirationPrice,
143 Perpetual,
145 MarkPriceOptions,
147 PlatformState,
149 Announcements,
151 ChartTrades,
153 InstrumentState,
156
157 UserOrders,
160 UserTrades,
162 UserPortfolio,
164 UserChanges,
166 UserAccessLog,
168}
169
170impl DeribitWsChannel {
171 #[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 panic!(
218 "InstrumentState channel requires kind and currency parameters, use format_instrument_state_channel() instead"
219 )
220 }
221 }
222 }
223
224 #[must_use]
233 pub fn format_instrument_state_channel(kind: &str, currency: &str) -> String {
234 format!("instrument.state.{kind}.{currency}")
235 }
236
237 #[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 #[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 #[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#[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 #[serde(rename = "public/subscribe")]
331 #[strum(serialize = "public/subscribe")]
332 PublicSubscribe,
333 #[serde(rename = "public/unsubscribe")]
335 #[strum(serialize = "public/unsubscribe")]
336 PublicUnsubscribe,
337 #[serde(rename = "public/auth")]
339 #[strum(serialize = "public/auth")]
340 PublicAuth,
341 #[serde(rename = "public/set_heartbeat")]
343 #[strum(serialize = "public/set_heartbeat")]
344 SetHeartbeat,
345 #[serde(rename = "public/disable_heartbeat")]
347 #[strum(serialize = "public/disable_heartbeat")]
348 DisableHeartbeat,
349 #[serde(rename = "public/test")]
351 #[strum(serialize = "public/test")]
352 Test,
353 #[serde(rename = "public/hello")]
355 #[strum(serialize = "public/hello")]
356 Hello,
357 #[serde(rename = "public/get_time")]
359 #[strum(serialize = "public/get_time")]
360 GetTime,
361
362 #[serde(rename = "private/subscribe")]
365 #[strum(serialize = "private/subscribe")]
366 PrivateSubscribe,
367 #[serde(rename = "private/unsubscribe")]
369 #[strum(serialize = "private/unsubscribe")]
370 PrivateUnsubscribe,
371 #[serde(rename = "private/logout")]
373 #[strum(serialize = "private/logout")]
374 Logout,
375}
376
377impl DeribitWsMethod {
378 #[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#[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 #[serde(rename = "new")]
406 New,
407 #[serde(rename = "change")]
409 Change,
410 #[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#[derive(
427 Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumString, Serialize, Deserialize,
428)]
429#[serde(rename_all = "snake_case")]
430pub enum DeribitBookMsgType {
431 #[serde(rename = "snapshot")]
433 Snapshot,
434 #[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#[derive(
485 Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumString, Serialize, Deserialize,
486)]
487#[serde(rename_all = "snake_case")]
488pub enum DeribitHeartbeatType {
489 #[serde(rename = "heartbeat")]
491 Heartbeat,
492 #[serde(rename = "test_request")]
494 TestRequest,
495}