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::user_orders`] | User order updates |
103/// | [`Topic::all_tickers`] | All symbol mini tickers |
104/// | [`Topic::all_mark_prices`] | All mark prices |
105/// | [`Topic::all_book_tickers`] | All best bid/ask |
106/// | [`Topic::all_force_orders`] | All liquidations |
107#[derive(Clone, Debug, PartialEq, Eq, Hash)]
108pub enum Topic {
109    /// Aggregated trade stream for a symbol.
110    AggTrade { symbol: String },
111
112    /// Order book depth stream with configurable levels.
113    Depth { symbol: String, depth: OrderbookDepth },
114
115    /// Best bid/ask stream for a symbol.
116    BookTicker { symbol: String },
117
118    /// Mark price stream for a symbol.
119    MarkPrice { symbol: String },
120
121    /// Kline/candlestick stream for a symbol at an interval.
122    Kline { symbol: String, interval: KlineInterval },
123
124    /// Liquidation order stream for a symbol.
125    ForceOrder { symbol: String },
126
127    /// User order stream for an address.
128    UserOrders { address: String },
129
130    /// All symbols mini ticker stream.
131    AllTickers,
132
133    /// All symbols mark price stream.
134    AllMarkPrices,
135
136    /// All symbols book ticker stream.
137    AllBookTickers,
138
139    /// All symbols liquidation stream.
140    AllForceOrders,
141}
142
143impl Topic {
144    /// Subscribe to aggregated trades for a symbol.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use bullet_rust_sdk::ws::topics::Topic;
150    ///
151    /// let topic = Topic::agg_trade("BTC-USD");
152    /// assert_eq!(topic.to_string(), "BTC-USD@aggTrade");
153    /// ```
154    pub fn agg_trade(symbol: impl Into<String>) -> Self {
155        Self::AggTrade { symbol: symbol.into() }
156    }
157
158    /// Subscribe to order book depth for a symbol.
159    ///
160    /// # Example
161    ///
162    /// ```
163    /// use bullet_rust_sdk::ws::topics::{OrderbookDepth, Topic};
164    ///
165    /// let topic = Topic::depth("BTC-USD", OrderbookDepth::D10);
166    /// assert_eq!(topic.to_string(), "BTC-USD@depth10");
167    /// ```
168    pub fn depth(symbol: impl Into<String>, depth: OrderbookDepth) -> Self {
169        Self::Depth { symbol: symbol.into(), depth }
170    }
171
172    /// Subscribe to best bid/ask for a symbol.
173    ///
174    /// # Example
175    ///
176    /// ```
177    /// use bullet_rust_sdk::ws::topics::Topic;
178    ///
179    /// let topic = Topic::book_ticker("BTC-USD");
180    /// assert_eq!(topic.to_string(), "BTC-USD@bookTicker");
181    /// ```
182    pub fn book_ticker(symbol: impl Into<String>) -> Self {
183        Self::BookTicker { symbol: symbol.into() }
184    }
185
186    /// Subscribe to mark price updates for a symbol.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use bullet_rust_sdk::ws::topics::Topic;
192    ///
193    /// let topic = Topic::mark_price("BTC-USD");
194    /// assert_eq!(topic.to_string(), "BTC-USD@markPrice");
195    /// ```
196    pub fn mark_price(symbol: impl Into<String>) -> Self {
197        Self::MarkPrice { symbol: symbol.into() }
198    }
199
200    /// Subscribe to kline/candlestick data for a symbol.
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// use bullet_rust_sdk::ws::topics::{KlineInterval, Topic};
206    ///
207    /// let topic = Topic::kline("BTC-USD", KlineInterval::H1);
208    /// assert_eq!(topic.to_string(), "BTC-USD@kline_1h");
209    /// ```
210    pub fn kline(symbol: impl Into<String>, interval: KlineInterval) -> Self {
211        Self::Kline { symbol: symbol.into(), interval }
212    }
213
214    /// Subscribe to liquidation orders for a symbol.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// use bullet_rust_sdk::ws::topics::Topic;
220    ///
221    /// let topic = Topic::force_order("BTC-USD");
222    /// assert_eq!(topic.to_string(), "BTC-USD@forceOrder");
223    /// ```
224    pub fn force_order(symbol: impl Into<String>) -> Self {
225        Self::ForceOrder { symbol: symbol.into() }
226    }
227
228    /// Subscribe to user order updates for an address.
229    ///
230    /// # Example
231    ///
232    /// ```
233    /// use bullet_rust_sdk::ws::topics::Topic;
234    ///
235    /// let topic = Topic::user_orders("0xabc");
236    /// assert_eq!(topic.to_string(), "0xabc@user.orders");
237    /// ```
238    pub fn user_orders(address: impl Into<String>) -> Self {
239        Self::UserOrders { address: address.into() }
240    }
241
242    /// Subscribe to mini ticker updates for all symbols.
243    ///
244    /// # Example
245    ///
246    /// ```
247    /// use bullet_rust_sdk::ws::topics::Topic;
248    ///
249    /// let topic = Topic::all_tickers();
250    /// assert_eq!(topic.to_string(), "!ticker@arr");
251    /// ```
252    pub fn all_tickers() -> Self {
253        Self::AllTickers
254    }
255
256    /// Subscribe to mark price updates for all symbols.
257    ///
258    /// # Example
259    ///
260    /// ```
261    /// use bullet_rust_sdk::ws::topics::Topic;
262    ///
263    /// let topic = Topic::all_mark_prices();
264    /// assert_eq!(topic.to_string(), "!markPrice@arr");
265    /// ```
266    pub fn all_mark_prices() -> Self {
267        Self::AllMarkPrices
268    }
269
270    /// Subscribe to book ticker updates for all symbols.
271    ///
272    /// # Example
273    ///
274    /// ```
275    /// use bullet_rust_sdk::ws::topics::Topic;
276    ///
277    /// let topic = Topic::all_book_tickers();
278    /// assert_eq!(topic.to_string(), "!bookTicker@arr");
279    /// ```
280    pub fn all_book_tickers() -> Self {
281        Self::AllBookTickers
282    }
283
284    /// Subscribe to liquidation orders for all symbols.
285    ///
286    /// # Example
287    ///
288    /// ```
289    /// use bullet_rust_sdk::ws::topics::Topic;
290    ///
291    /// let topic = Topic::all_force_orders();
292    /// assert_eq!(topic.to_string(), "!forceOrder@arr");
293    /// ```
294    pub fn all_force_orders() -> Self {
295        Self::AllForceOrders
296    }
297}
298
299impl fmt::Display for Topic {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        match self {
302            Topic::AggTrade { symbol } => write!(f, "{symbol}@aggTrade"),
303            Topic::Depth { symbol, depth } => write!(f, "{symbol}@depth{}", depth.as_str()),
304            Topic::BookTicker { symbol } => write!(f, "{symbol}@bookTicker"),
305            Topic::MarkPrice { symbol } => write!(f, "{symbol}@markPrice"),
306            Topic::Kline { symbol, interval } => write!(f, "{symbol}@kline_{}", interval.as_str()),
307            Topic::ForceOrder { symbol } => write!(f, "{symbol}@forceOrder"),
308            Topic::UserOrders { address } => write!(f, "{address}@user.orders"),
309            Topic::AllTickers => write!(f, "!ticker@arr"),
310            Topic::AllMarkPrices => write!(f, "!markPrice@arr"),
311            Topic::AllBookTickers => write!(f, "!bookTicker@arr"),
312            Topic::AllForceOrders => write!(f, "!forceOrder@arr"),
313        }
314    }
315}
316
317impl From<Topic> for String {
318    fn from(topic: Topic) -> Self {
319        topic.to_string()
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_agg_trade() {
329        assert_eq!(Topic::agg_trade("BTC-USD").to_string(), "BTC-USD@aggTrade");
330    }
331
332    #[test]
333    fn test_depth() {
334        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D5).to_string(), "BTC-USD@depth5");
335        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D10).to_string(), "BTC-USD@depth10");
336        assert_eq!(Topic::depth("BTC-USD", OrderbookDepth::D20).to_string(), "BTC-USD@depth20");
337    }
338
339    #[test]
340    fn test_book_ticker() {
341        assert_eq!(Topic::book_ticker("ETH-USD").to_string(), "ETH-USD@bookTicker");
342    }
343
344    #[test]
345    fn test_mark_price() {
346        assert_eq!(Topic::mark_price("SOL-USD").to_string(), "SOL-USD@markPrice");
347    }
348
349    #[test]
350    fn test_kline() {
351        assert_eq!(Topic::kline("BTC-USD", KlineInterval::M1).to_string(), "BTC-USD@kline_1m");
352        assert_eq!(Topic::kline("BTC-USD", KlineInterval::H4).to_string(), "BTC-USD@kline_4h");
353        assert_eq!(Topic::kline("BTC-USD", KlineInterval::D1).to_string(), "BTC-USD@kline_1d");
354    }
355
356    #[test]
357    fn test_force_order() {
358        assert_eq!(Topic::force_order("BTC-USD").to_string(), "BTC-USD@forceOrder");
359    }
360
361    #[test]
362    fn test_user_orders() {
363        assert_eq!(Topic::user_orders("0xabc").to_string(), "0xabc@user.orders");
364    }
365
366    #[test]
367    fn test_all_streams() {
368        assert_eq!(Topic::all_tickers().to_string(), "!ticker@arr");
369        assert_eq!(Topic::all_mark_prices().to_string(), "!markPrice@arr");
370        assert_eq!(Topic::all_book_tickers().to_string(), "!bookTicker@arr");
371        assert_eq!(Topic::all_force_orders().to_string(), "!forceOrder@arr");
372    }
373
374    #[test]
375    fn test_into_string() {
376        let topic = Topic::agg_trade("BTC-USD");
377        let s: String = topic.into();
378        assert_eq!(s, "BTC-USD@aggTrade");
379    }
380}