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}