Skip to main content

bullet_rust_sdk/ws/
topics.rs

1//! Strongly-typed WebSocket subscription topics.
2//!
3//! This module provides type-safe topic builders for WebSocket subscriptions,
4//! eliminating the need to remember string formats like `"BTC-USD@depth10"`.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use bullet_rust_sdk::types::RequestId;
10//! use bullet_rust_sdk::ws::topics::{KlineInterval, OrderbookDepth, Topic};
11//!
12//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
13//! # let api = bullet_rust_sdk::Client::mainnet().await?;
14//! # let mut ws = api.connect_ws().call().await?;
15//! // Type-safe subscriptions
16//! ws.subscribe(
17//!     [
18//!         Topic::agg_trade("BTC-USD"),
19//!         Topic::depth("ETH-USD", OrderbookDepth::D10),
20//!         Topic::book_ticker("SOL-USD"),
21//!         Topic::kline("BTC-USD", KlineInterval::H1),
22//!     ],
23//!     Some(RequestId::new(1)),
24//! )
25//! .await?;
26//! # Ok(())
27//! # }
28//! ```
29
30use std::fmt;
31
32/// Orderbook depth levels for depth subscriptions.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum OrderbookDepth {
35    /// 5 levels
36    D5,
37    /// 10 levels (default)
38    #[default]
39    D10,
40    /// 20 levels
41    D20,
42}
43
44impl OrderbookDepth {
45    fn as_str(&self) -> &'static str {
46        match self {
47            OrderbookDepth::D5 => "5",
48            OrderbookDepth::D10 => "10",
49            OrderbookDepth::D20 => "20",
50        }
51    }
52}
53
54/// Kline (candlestick) intervals.
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub enum KlineInterval {
57    /// 1 minute
58    M1,
59    /// 5 minutes
60    M5,
61    /// 15 minutes
62    M15,
63    /// 30 minutes
64    M30,
65    /// 1 hour
66    H1,
67    /// 4 hours
68    H4,
69    /// 1 day
70    D1,
71}
72
73impl KlineInterval {
74    fn as_str(&self) -> &'static str {
75        match self {
76            KlineInterval::M1 => "1m",
77            KlineInterval::M5 => "5m",
78            KlineInterval::M15 => "15m",
79            KlineInterval::M30 => "30m",
80            KlineInterval::H1 => "1h",
81            KlineInterval::H4 => "4h",
82            KlineInterval::D1 => "1d",
83        }
84    }
85}
86
87/// A WebSocket subscription topic.
88///
89/// Topics are created using the static constructor methods and converted to
90/// the wire format automatically when passed to [`subscribe()`](super::WebsocketHandle::subscribe).
91///
92/// # Available Topics
93///
94/// | Topic | Description |
95/// |-------|-------------|
96/// | [`Topic::agg_trade`] | Aggregated trade updates |
97/// | [`Topic::depth`] | Order book depth snapshots |
98/// | [`Topic::book_ticker`] | Best bid/ask prices |
99/// | [`Topic::mark_price`] | Mark price updates |
100/// | [`Topic::kline`] | Candlestick/kline data |
101/// | [`Topic::force_order`] | Liquidation orders |
102/// | [`Topic::all_tickers`] | All symbol mini tickers |
103/// | [`Topic::all_mark_prices`] | All mark prices |
104/// | [`Topic::all_book_tickers`] | All best bid/ask |
105/// | [`Topic::all_force_orders`] | All liquidations |
106#[derive(Clone, Debug, PartialEq, Eq, Hash)]
107pub enum Topic {
108    /// Aggregated trade stream for a symbol.
109    AggTrade { symbol: String },
110
111    /// Order book depth stream with configurable levels.
112    Depth { symbol: String, depth: OrderbookDepth },
113
114    /// Best bid/ask stream for a symbol.
115    BookTicker { symbol: String },
116
117    /// Mark price stream for a symbol.
118    MarkPrice { symbol: String },
119
120    /// Kline/candlestick stream for a symbol at an interval.
121    Kline { symbol: String, interval: KlineInterval },
122
123    /// Liquidation order stream for a symbol.
124    ForceOrder { symbol: String },
125
126    /// All symbols mini ticker stream.
127    AllTickers,
128
129    /// All symbols mark price stream.
130    AllMarkPrices,
131
132    /// All symbols book ticker stream.
133    AllBookTickers,
134
135    /// All symbols liquidation stream.
136    AllForceOrders,
137}
138
139impl Topic {
140    /// Subscribe to aggregated trades for a symbol.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use bullet_rust_sdk::ws::topics::Topic;
146    ///
147    /// let topic = Topic::agg_trade("BTC-USD");
148    /// assert_eq!(topic.to_string(), "BTC-USD@aggTrade");
149    /// ```
150    pub fn agg_trade(symbol: impl Into<String>) -> Self {
151        Self::AggTrade { symbol: symbol.into() }
152    }
153
154    /// Subscribe to order book depth for a symbol.
155    ///
156    /// # Example
157    ///
158    /// ```
159    /// use bullet_rust_sdk::ws::topics::{OrderbookDepth, Topic};
160    ///
161    /// let topic = Topic::depth("BTC-USD", OrderbookDepth::D10);
162    /// assert_eq!(topic.to_string(), "BTC-USD@depth10");
163    /// ```
164    pub fn depth(symbol: impl Into<String>, depth: OrderbookDepth) -> Self {
165        Self::Depth { symbol: symbol.into(), depth }
166    }
167
168    /// Subscribe to best bid/ask for a symbol.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use bullet_rust_sdk::ws::topics::Topic;
174    ///
175    /// let topic = Topic::book_ticker("BTC-USD");
176    /// assert_eq!(topic.to_string(), "BTC-USD@bookTicker");
177    /// ```
178    pub fn book_ticker(symbol: impl Into<String>) -> Self {
179        Self::BookTicker { symbol: symbol.into() }
180    }
181
182    /// Subscribe to mark price updates for a symbol.
183    ///
184    /// # Example
185    ///
186    /// ```
187    /// use bullet_rust_sdk::ws::topics::Topic;
188    ///
189    /// let topic = Topic::mark_price("BTC-USD");
190    /// assert_eq!(topic.to_string(), "BTC-USD@markPrice");
191    /// ```
192    pub fn mark_price(symbol: impl Into<String>) -> Self {
193        Self::MarkPrice { symbol: symbol.into() }
194    }
195
196    /// Subscribe to kline/candlestick data for a symbol.
197    ///
198    /// # Example
199    ///
200    /// ```
201    /// use bullet_rust_sdk::ws::topics::{KlineInterval, Topic};
202    ///
203    /// let topic = Topic::kline("BTC-USD", KlineInterval::H1);
204    /// assert_eq!(topic.to_string(), "BTC-USD@kline_1h");
205    /// ```
206    pub fn kline(symbol: impl Into<String>, interval: KlineInterval) -> Self {
207        Self::Kline { symbol: symbol.into(), interval }
208    }
209
210    /// Subscribe to liquidation orders for a symbol.
211    ///
212    /// # Example
213    ///
214    /// ```
215    /// use bullet_rust_sdk::ws::topics::Topic;
216    ///
217    /// let topic = Topic::force_order("BTC-USD");
218    /// assert_eq!(topic.to_string(), "BTC-USD@forceOrder");
219    /// ```
220    pub fn force_order(symbol: impl Into<String>) -> Self {
221        Self::ForceOrder { symbol: symbol.into() }
222    }
223
224    /// Subscribe to mini ticker updates for all symbols.
225    ///
226    /// # Example
227    ///
228    /// ```
229    /// use bullet_rust_sdk::ws::topics::Topic;
230    ///
231    /// let topic = Topic::all_tickers();
232    /// assert_eq!(topic.to_string(), "!ticker@arr");
233    /// ```
234    pub fn all_tickers() -> Self {
235        Self::AllTickers
236    }
237
238    /// Subscribe to mark price updates for all symbols.
239    ///
240    /// # Example
241    ///
242    /// ```
243    /// use bullet_rust_sdk::ws::topics::Topic;
244    ///
245    /// let topic = Topic::all_mark_prices();
246    /// assert_eq!(topic.to_string(), "!markPrice@arr");
247    /// ```
248    pub fn all_mark_prices() -> Self {
249        Self::AllMarkPrices
250    }
251
252    /// Subscribe to book ticker updates for all symbols.
253    ///
254    /// # Example
255    ///
256    /// ```
257    /// use bullet_rust_sdk::ws::topics::Topic;
258    ///
259    /// let topic = Topic::all_book_tickers();
260    /// assert_eq!(topic.to_string(), "!bookTicker@arr");
261    /// ```
262    pub fn all_book_tickers() -> Self {
263        Self::AllBookTickers
264    }
265
266    /// Subscribe to liquidation orders for all symbols.
267    ///
268    /// # Example
269    ///
270    /// ```
271    /// use bullet_rust_sdk::ws::topics::Topic;
272    ///
273    /// let topic = Topic::all_force_orders();
274    /// assert_eq!(topic.to_string(), "!forceOrder@arr");
275    /// ```
276    pub fn all_force_orders() -> Self {
277        Self::AllForceOrders
278    }
279}
280
281impl fmt::Display for Topic {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self {
284            Topic::AggTrade { symbol } => write!(f, "{symbol}@aggTrade"),
285            Topic::Depth { symbol, depth } => write!(f, "{symbol}@depth{}", depth.as_str()),
286            Topic::BookTicker { symbol } => write!(f, "{symbol}@bookTicker"),
287            Topic::MarkPrice { symbol } => write!(f, "{symbol}@markPrice"),
288            Topic::Kline { symbol, interval } => write!(f, "{symbol}@kline_{}", interval.as_str()),
289            Topic::ForceOrder { symbol } => write!(f, "{symbol}@forceOrder"),
290            Topic::AllTickers => write!(f, "!ticker@arr"),
291            Topic::AllMarkPrices => write!(f, "!markPrice@arr"),
292            Topic::AllBookTickers => write!(f, "!bookTicker@arr"),
293            Topic::AllForceOrders => write!(f, "!forceOrder@arr"),
294        }
295    }
296}
297
298impl From<Topic> for String {
299    fn from(topic: Topic) -> Self {
300        topic.to_string()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_agg_trade() {
310        assert_eq!(Topic::agg_trade("BTC-USD").to_string(), "BTC-USD@aggTrade");
311    }
312
313    #[test]
314    fn test_depth() {
315        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D5).to_string(), "BTC-USD@depth5");
316        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D10).to_string(), "BTC-USD@depth10");
317        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D20).to_string(), "BTC-USD@depth20");
318    }
319
320    #[test]
321    fn test_book_ticker() {
322        assert_eq!(Topic::book_ticker("ETH-USD").to_string(), "ETH-USD@bookTicker");
323    }
324
325    #[test]
326    fn test_mark_price() {
327        assert_eq!(Topic::mark_price("SOL-USD").to_string(), "SOL-USD@markPrice");
328    }
329
330    #[test]
331    fn test_kline() {
332        assert_eq!(Topic::kline("BTC-USD", KlineInterval::M1).to_string(), "BTC-USD@kline_1m");
333        assert_eq!(Topic::kline("BTC-USD", KlineInterval::H4).to_string(), "BTC-USD@kline_4h");
334        assert_eq!(Topic::kline("BTC-USD", KlineInterval::D1).to_string(), "BTC-USD@kline_1d");
335    }
336
337    #[test]
338    fn test_force_order() {
339        assert_eq!(Topic::force_order("BTC-USD").to_string(), "BTC-USD@forceOrder");
340    }
341
342    #[test]
343    fn test_all_streams() {
344        assert_eq!(Topic::all_tickers().to_string(), "!ticker@arr");
345        assert_eq!(Topic::all_mark_prices().to_string(), "!markPrice@arr");
346        assert_eq!(Topic::all_book_tickers().to_string(), "!bookTicker@arr");
347        assert_eq!(Topic::all_force_orders().to_string(), "!forceOrder@arr");
348    }
349
350    #[test]
351    fn test_into_string() {
352        let topic = Topic::agg_trade("BTC-USD");
353        let s: String = topic.into();
354        assert_eq!(s, "BTC-USD@aggTrade");
355    }
356}