Skip to main content

binance/spot/ws/
incoming_message.rs

1use rust_decimal::Decimal;
2use serde::Deserialize;
3
4use crate::{
5    Timestamp,
6    spot::{
7        KlineInterval,
8        ws::{MessageID, StreamName},
9    },
10    ws::ReceivedMessage,
11};
12
13#[derive(PartialEq, Deserialize, Debug)]
14#[serde(untagged)]
15#[allow(clippy::large_enum_variant)]
16pub enum IncomingMessage {
17    CombinedStream(CombinedStreamMessage<StreamMessage>),
18    Stream(StreamMessage),
19    Error(ErrorMessage),
20    Response(ResponseMessage), // last.
21}
22
23impl ReceivedMessage for IncomingMessage {
24    fn server_shutdown_event_time(&self) -> Option<u64> {
25        match self {
26            IncomingMessage::Stream(StreamMessage::ServerShutdown(ServerShutdownMsg {
27                event_time,
28            })) => Some(*event_time),
29            IncomingMessage::CombinedStream(CombinedStreamMessage {
30                data: StreamMessage::ServerShutdown(ServerShutdownMsg { event_time }),
31                ..
32            }) => Some(*event_time),
33            _ => None,
34        }
35    }
36}
37
38#[derive(PartialEq, Deserialize, Debug)]
39pub struct ResponseMessage {
40    pub id: Option<MessageID>,
41    pub status: Option<i64>,
42    pub result: Option<serde_json::Value>,
43    pub rate_limits: Option<Vec<serde_json::Value>>,
44}
45
46#[derive(Debug, Deserialize, PartialEq)]
47pub struct CombinedStreamMessage<T> {
48    pub stream: StreamName,
49    pub data: T,
50}
51
52#[derive(PartialEq, Deserialize, Debug)]
53#[serde(tag = "e")]
54pub enum StreamMessage {
55    #[serde(rename = "aggTrade")]
56    AggTrade(AggTradeMsg),
57    #[serde(rename = "trade")]
58    Trade(TradeMsg),
59    #[serde(rename = "kline")]
60    Kline(KlineMsg),
61    #[serde(rename = "24hrMiniTicker")]
62    MiniTicker24(MiniTicker24Msg),
63    #[serde(rename = "serverShutdown")]
64    ServerShutdown(ServerShutdownMsg),
65}
66
67/// The Aggregate Trade Streams push trade information that is aggregated for a single taker order.
68#[derive(PartialEq, Deserialize, Debug)]
69pub struct AggTradeMsg {
70    /// Event time
71    #[serde(rename = "E")]
72    pub event_time: Timestamp,
73    /// Symbol
74    #[serde(rename = "s")]
75    pub symbol: String,
76    /// Aggregate trade ID
77    #[serde(rename = "a")]
78    pub trade_id: i64,
79    /// Price
80    #[serde(rename = "p")]
81    pub price: Decimal,
82    /// Quantity
83    #[serde(rename = "q")]
84    pub qty: Decimal,
85    /// First trade ID
86    #[serde(rename = "f")]
87    pub first_trade_id: i64,
88    /// Last trade ID
89    #[serde(rename = "l")]
90    pub last_trade_id: i64,
91    /// Trade time
92    #[serde(rename = "T")]
93    pub trade_time: Timestamp,
94    /// Is the buyer the market maker?
95    #[serde(rename = "m")]
96    pub is_buyer: bool,
97}
98
99/// The Trade Streams push raw trade information; each trade has a unique buyer and seller.
100#[derive(PartialEq, Deserialize, Debug)]
101pub struct TradeMsg {
102    /// Event time
103    #[serde(rename = "E")]
104    pub event_time: Timestamp,
105    /// Symbol
106    #[serde(rename = "s")]
107    pub symbol: String,
108    /// Aggregate trade ID
109    #[serde(rename = "t")]
110    pub trade_id: i64,
111    /// Price
112    #[serde(rename = "p")]
113    pub price: Decimal,
114    /// Quantity
115    #[serde(rename = "q")]
116    pub qty: Decimal,
117    /// Trade time
118    #[serde(rename = "T")]
119    pub trade_time: Timestamp,
120    /// Is the buyer the market maker?
121    #[serde(rename = "m")]
122    pub is_buyer: bool,
123}
124
125/// The Kline/Candlestick Stream push updates to the current klines/candlestick every second in UTC+0 timezone
126#[derive(PartialEq, Deserialize, Debug)]
127pub struct KlineMsg {
128    /// Event time
129    #[serde(rename = "E")]
130    pub event_time: Timestamp,
131    /// Symbol
132    #[serde(rename = "s")]
133    pub symbol: String,
134    #[serde(rename = "k")]
135    pub kline: Kline,
136}
137#[derive(PartialEq, Deserialize, Debug)]
138pub struct Kline {
139    /// Kline start time
140    #[serde(rename = "t")]
141    pub start_time: Timestamp,
142    /// Kline close time
143    #[serde(rename = "T")]
144    pub close_time: Timestamp,
145    /// Symbol
146    #[serde(rename = "s")]
147    pub symbol: String,
148    /// Interval
149    #[serde(rename = "i")]
150    pub interval: KlineInterval,
151    /// First trade ID
152    #[serde(rename = "f")]
153    pub first_trade_id: i64,
154    /// Last trade ID
155    #[serde(rename = "L")]
156    pub last_trade_id: i64,
157    /// Open price
158    #[serde(rename = "o")]
159    pub open_price: Decimal,
160    /// Close price
161    #[serde(rename = "c")]
162    pub close_price: Decimal,
163    /// High price
164    #[serde(rename = "h")]
165    pub high_price: Decimal,
166    /// Low price
167    #[serde(rename = "l")]
168    pub low_price: Decimal,
169    /// Base asset volume
170    #[serde(rename = "v")]
171    pub base_asset_volume: Decimal,
172    /// Number of trades
173    #[serde(rename = "n")]
174    pub trade_number: i64,
175    /// Is this kline closed?
176    #[serde(rename = "x")]
177    pub is_closed: bool,
178    /// Quote asset volume
179    #[serde(rename = "q")]
180    pub quote_asset_volume: Decimal,
181    /// Taker buy base asset volume
182    #[serde(rename = "V")]
183    pub taker_buy_base_asset_volume: Decimal,
184    /// Taker buy quote asset volume
185    #[serde(rename = "Q")]
186    pub taker_buy_quote_asset_volume: Decimal,
187}
188
189/// 24hr rolling window mini-ticker statistics. These are NOT the statistics of the UTC day, but a 24hr rolling window for the previous 24hrs.
190#[derive(PartialEq, Deserialize, Debug)]
191pub struct MiniTicker24Msg {
192    /// Event time
193    #[serde(rename = "E")]
194    pub event_time: Timestamp,
195    /// Symbol
196    #[serde(rename = "s")]
197    pub symbol: String,
198    /// Open price
199    #[serde(rename = "o")]
200    pub open_price: Decimal,
201    /// Close price
202    #[serde(rename = "c")]
203    pub close_price: Decimal,
204    /// High price
205    #[serde(rename = "h")]
206    pub high_price: Decimal,
207    /// Low price
208    #[serde(rename = "l")]
209    pub low_price: Decimal,
210    /// Total traded base asset volume
211    #[serde(rename = "v")]
212    pub total_base_asset_volume: Decimal,
213    /// Total traded quote asset volume
214    #[serde(rename = "q")]
215    pub total_quote_asset_volume: Decimal,
216}
217
218#[derive(PartialEq, Deserialize, Debug)]
219pub struct ServerShutdownMsg {
220    /// Event time
221    #[serde(rename = "E")]
222    pub event_time: Timestamp,
223}
224
225#[derive(PartialEq, Deserialize, Debug)]
226pub struct ErrorMessage {
227    pub error: ErrorValueMessage,
228    pub id: Option<MessageID>,
229}
230
231#[derive(PartialEq, Deserialize, Debug)]
232pub struct ErrorValueMessage {
233    pub code: i64,
234    pub msg: String,
235}
236
237#[cfg(test)]
238mod tests {
239    use rust_decimal::dec;
240
241    use crate::serde::deserialize_json;
242
243    use super::*;
244
245    #[test]
246    fn test_deserialize_combined_stream_event() {
247        let json = r#"{
248            "stream": "bnbbtc@trade",
249            "data": "DATA"
250        }"#;
251        let expected = CombinedStreamMessage {
252            stream: StreamName::Trade {
253                symbol: String::from("BNBBTC").to_lowercase(),
254            },
255            data: String::from("DATA"),
256        };
257
258        let current = deserialize_json(json).unwrap();
259
260        assert_eq!(expected, current);
261    }
262
263    #[test]
264    fn test_deserialize_stream_message_agg_trade() {
265        let json = r#"{
266            "e": "aggTrade",
267            "E": 1672515782136,
268            "s": "BNBBTC",
269            "a": 12345,
270            "p": "0.001",
271            "q": "100",
272            "f": 100,
273            "l": 105,
274            "T": 1672515782136,
275            "m": true,
276            "M": true
277        }"#;
278        let expected = AggTradeMsg {
279            event_time: 1672515782136,
280            symbol: String::from("BNBBTC"),
281            trade_id: 12345,
282            price: dec!(0.001),
283            qty: dec!(100),
284            first_trade_id: 100,
285            last_trade_id: 105,
286            trade_time: 1672515782136,
287            is_buyer: true,
288        };
289
290        let current = deserialize_json(json).unwrap();
291
292        assert_eq!(expected, current);
293    }
294
295    #[test]
296    fn test_deserialize_stream_message_trade() {
297        let json = r#"{
298            "e": "trade",
299            "E": 1672515782136,
300            "s": "BNBBTC",
301            "t": 12345,
302            "p": "0.001",
303            "q": "100",
304            "T": 1672515782136,
305            "m": true,
306            "M": true
307        }"#;
308        let expected = TradeMsg {
309            event_time: 1672515782136,
310            symbol: String::from("BNBBTC"),
311            trade_id: 12345,
312            price: dec!(0.001),
313            qty: dec!(100),
314            trade_time: 1672515782136,
315            is_buyer: true,
316        };
317
318        let current = deserialize_json(json).unwrap();
319
320        assert_eq!(expected, current);
321    }
322
323    #[test]
324    fn test_deserialize_stream_message_kline() {
325        let json = r#"{
326            "e": "kline",
327            "E": 1672515782136,
328            "s": "BNBBTC",
329            "k": {
330                "t": 1672515780000,
331                "T": 1672515839999,
332                "s": "BNBBTC",
333                "i": "1m",
334                "f": 100,
335                "L": 200,
336                "o": "0.0010",
337                "c": "0.0020",
338                "h": "0.0025",
339                "l": "0.0015",
340                "v": "1000",
341                "n": 100,
342                "x": false,
343                "q": "1.0000",
344                "V": "500",
345                "Q": "0.500",
346                "B": "123456"
347            }
348        }"#;
349        let symbol = String::from("BNBBTC");
350        let expected = KlineMsg {
351            event_time: 1672515782136,
352            symbol: symbol.clone(),
353            kline: Kline {
354                start_time: 1672515780000,
355                close_time: 1672515839999,
356                symbol,
357                interval: KlineInterval::Minute1,
358                first_trade_id: 100,
359                last_trade_id: 200,
360                open_price: dec!(0.0010),
361                close_price: dec!(0.0020),
362                high_price: dec!(0.0025),
363                low_price: dec!(0.0015),
364                base_asset_volume: dec!(1000),
365                trade_number: 100,
366                is_closed: false,
367                quote_asset_volume: dec!(1.0000),
368                taker_buy_base_asset_volume: dec!(500),
369                taker_buy_quote_asset_volume: dec!(0.500),
370            },
371        };
372
373        let current = deserialize_json(json).unwrap();
374
375        assert_eq!(expected, current);
376    }
377
378    #[test]
379    fn test_deserialize_stream_message_mini_ticker24() {
380        let json = r#"{
381            "e": "24hrMiniTicker",
382            "E": 1672515782136,
383            "s": "BNBBTC",
384            "c": "0.0025",
385            "o": "0.0010",
386            "h": "0.0025",
387            "l": "0.0010",
388            "v": "10000",
389            "q": "18"
390        }"#;
391        let expected = MiniTicker24Msg {
392            event_time: 1672515782136,
393            symbol: String::from("BNBBTC"),
394            open_price: dec!(0.0010),
395            close_price: dec!(0.0025),
396            high_price: dec!(0.0025),
397            low_price: dec!(0.0010),
398            total_base_asset_volume: dec!(10000),
399            total_quote_asset_volume: dec!(18),
400        };
401
402        let current = deserialize_json(json).unwrap();
403
404        assert_eq!(expected, current);
405    }
406
407    #[test]
408    fn test_deserialize_stream_message_response() {
409        let json = r#"{"result":null,"id":"message-id"}"#;
410        let parsed: IncomingMessage = deserialize_json(json).unwrap();
411        assert!(
412            matches!(parsed, IncomingMessage::Response(_)),
413            "expected Response variant, got {parsed:?}",
414        );
415    }
416}