rusty_feeder/exchange/bithumb/data/
subscription.rs

1/*
2 * Bithumb WebSocket subscription management
3 * Handles subscription requests and formats
4 */
5
6use serde::{Deserialize, Serialize};
7use smartstring::alias::String;
8use std::fmt;
9use std::time::Duration;
10use uuid::Uuid;
11
12/// Bithumb WebSocket subscription types
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SubscriptionType {
16    /// Trade data
17    #[serde(rename = "trade")]
18    Trade,
19
20    /// Orderbook data
21    #[serde(rename = "orderbook")]
22    Orderbook,
23
24    /// Ticker data
25    #[serde(rename = "ticker")]
26    Ticker,
27}
28
29impl fmt::Display for SubscriptionType {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Trade => write!(f, "trade"),
33            Self::Orderbook => write!(f, "orderbook"),
34            Self::Ticker => write!(f, "ticker"),
35        }
36    }
37}
38
39/// Bithumb WebSocket subscription message
40#[derive(Debug, Clone, Serialize)]
41pub struct BithumbSubscription {
42    /// Ticket for subscription
43    pub ticket: String,
44
45    /// Type field
46    #[serde(rename = "type")]
47    pub data_type: String,
48
49    /// Market codes to subscribe to
50    pub codes: Vec<String>,
51
52    /// Price level aggregation (for orderbook)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub level: Option<f64>,
55
56    /// Whether to only get snapshot data
57    #[serde(skip_serializing_if = "Option::is_none", rename = "isOnlySnapshot")]
58    pub is_only_snapshot: Option<bool>,
59
60    /// Whether to only get realtime data
61    #[serde(skip_serializing_if = "Option::is_none", rename = "isOnlyRealtime")]
62    pub is_only_realtime: Option<bool>,
63}
64
65impl BithumbSubscription {
66    /// Create a new subscription with a ticket
67    #[must_use]
68    pub fn new(
69        subscription_type: SubscriptionType,
70        codes: Vec<String>,
71        level: Option<f64>,
72        snapshot_only: bool,
73        realtime_only: bool,
74    ) -> Self {
75        // Generate a unique ticket ID
76        let ticket = String::from(Uuid::new_v4().to_string());
77
78        // Set optional parameters
79        let is_only_snapshot = if snapshot_only { Some(true) } else { None };
80        let is_only_realtime = if realtime_only { Some(true) } else { None };
81
82        Self {
83            ticket,
84            data_type: subscription_type.to_string().into(),
85            codes,
86            level,
87            is_only_snapshot,
88            is_only_realtime,
89        }
90    }
91
92    /// Create a trade subscription
93    #[must_use]
94    pub fn trade(codes: Vec<String>) -> Self {
95        Self::new(SubscriptionType::Trade, codes, None, false, true)
96    }
97
98    /// Create an orderbook subscription
99    #[must_use]
100    pub fn orderbook(codes: Vec<String>, level: Option<f64>) -> Self {
101        Self::new(SubscriptionType::Orderbook, codes, level, false, true)
102    }
103
104    /// Create a ticker subscription
105    #[must_use]
106    pub fn ticker(codes: Vec<String>) -> Self {
107        Self::new(SubscriptionType::Ticker, codes, None, false, true)
108    }
109}
110
111/// Format specifier for responses
112#[derive(Debug, Clone, Serialize)]
113pub struct ResponseFormat {
114    /// Format of the response
115    pub format: String,
116}
117
118impl ResponseFormat {
119    /// Create a new format specifier
120    #[must_use]
121    pub fn new(simple: bool) -> Self {
122        let format = if simple { "SIMPLE" } else { "DEFAULT" };
123        Self {
124            format: format.into(),
125        }
126    }
127
128    /// Create a simple format
129    #[must_use]
130    pub fn simple() -> Self {
131        Self::new(true)
132    }
133}
134
135impl Default for ResponseFormat {
136    fn default() -> Self {
137        Self::new(false)
138    }
139}
140
141/// Connection status message
142#[derive(Debug, Clone, Deserialize)]
143pub struct StatusMessage {
144    /// Status of the connection
145    pub status: String,
146}
147
148/// Reconnection parameters
149#[derive(Debug, Clone)]
150pub struct ReconnectionParams {
151    /// Initial delay before reconnection attempt
152    pub initial_delay: Duration,
153
154    /// Maximum delay between reconnection attempts
155    pub max_delay: Duration,
156
157    /// Backoff factor for exponential backoff
158    pub backoff_factor: f64,
159
160    /// Maximum number of reconnection attempts
161    pub max_attempts: usize,
162
163    /// Current reconnection attempt
164    pub attempt: usize,
165}
166
167impl Default for ReconnectionParams {
168    fn default() -> Self {
169        Self {
170            initial_delay: Duration::from_millis(100),
171            max_delay: Duration::from_secs(30),
172            backoff_factor: 1.5,
173            max_attempts: 20,
174            attempt: 0,
175        }
176    }
177}
178
179impl ReconnectionParams {
180    /// Calculate the next delay based on the current attempt
181    pub fn next_delay(&self) -> Duration {
182        let delay_millis =
183            (self.initial_delay.as_millis() as f64) * self.backoff_factor.powf(self.attempt as f64);
184
185        let delay_millis = delay_millis.min(self.max_delay.as_millis() as f64) as u64;
186        Duration::from_millis(delay_millis)
187    }
188
189    /// Increment the attempt counter
190    pub const fn increment(&mut self) {
191        self.attempt += 1;
192    }
193
194    /// Reset the attempt counter
195    pub const fn reset(&mut self) {
196        self.attempt = 0;
197    }
198
199    /// Check if maximum attempts reached
200    pub const fn is_max_attempts_reached(&self) -> bool {
201        self.attempt >= self.max_attempts
202    }
203}