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}