Skip to main content

rustrade_data/exchange/bybit/
trade.rs

1use crate::{
2    event::{MarketEvent, MarketIter},
3    exchange::bybit::message::BybitPayload,
4    subscription::trade::PublicTrade,
5};
6use chrono::{DateTime, Utc};
7use rust_decimal::Decimal;
8use rustrade_instrument::{Side, exchange::ExchangeId};
9use serde::{Deserialize, Serialize};
10
11/// Terse type alias for an [`BybitTrade`](BybitTradeInner) real-time trades WebSocket message.
12pub type BybitTrade = BybitPayload<Vec<BybitTradeInner>>;
13
14/// ### Raw Payload Examples
15/// See docs: <https://bybit-exchange.github.io/docs/v5/websocket/public/trade>
16/// Spot Side::Buy Trade
17///```json
18/// {
19///     "T": 1672304486865,
20///     "s": "BTCUSDT",
21///     "S": "Buy",
22///     "v": "0.001",
23///     "p": "16578.50",
24///     "L": "PlusTick",
25///     "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
26///     "BT": false
27/// }
28/// ```
29#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
30pub struct BybitTradeInner {
31    #[serde(
32        alias = "T",
33        deserialize_with = "rustrade_integration::serde::de::de_u64_epoch_ms_as_datetime_utc"
34    )]
35    pub time: DateTime<Utc>,
36
37    #[serde(rename = "s")]
38    pub market: String,
39
40    #[serde(rename = "S")]
41    pub side: Side,
42
43    #[serde(
44        alias = "v",
45        deserialize_with = "rustrade_integration::serde::de::de_str"
46    )]
47    pub amount: Decimal,
48
49    #[serde(
50        alias = "p",
51        deserialize_with = "rustrade_integration::serde::de::de_str"
52    )]
53    pub price: Decimal,
54
55    #[serde(rename = "i")]
56    pub id: String,
57}
58
59impl<InstrumentKey: Clone> From<(ExchangeId, InstrumentKey, BybitTrade)>
60    for MarketIter<InstrumentKey, PublicTrade>
61{
62    fn from((exchange, instrument, trades): (ExchangeId, InstrumentKey, BybitTrade)) -> Self {
63        Self(
64            trades
65                .data
66                .into_iter()
67                .map(|trade| {
68                    Ok(MarketEvent {
69                        time_exchange: trade.time,
70                        time_received: Utc::now(),
71                        exchange,
72                        instrument: instrument.clone(),
73                        kind: PublicTrade {
74                            id: trade.id.into(),
75                            price: trade.price,
76                            amount: trade.amount,
77                            side: Some(trade.side),
78                        },
79                    })
80                })
81                .collect(),
82        )
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    mod de {
91        use crate::exchange::bybit::message::BybitPayloadKind;
92
93        use super::*;
94        use rust_decimal_macros::dec;
95        use rustrade_integration::{
96            error::SocketError, serde::de::datetime_utc_from_epoch_duration,
97            subscription::SubscriptionId,
98        };
99        use smol_str::ToSmolStr;
100        use std::time::Duration;
101
102        #[test]
103        fn test_bybit_trade() {
104            struct TestCase {
105                input: &'static str,
106                expected: Result<BybitTradeInner, SocketError>,
107            }
108
109            let tests = vec![
110                // TC0: input BybitTradeInner is deserialised
111                TestCase {
112                    input: r#"
113                        {
114                            "T": 1672304486865,
115                            "s": "BTCUSDT",
116                            "S": "Buy",
117                            "v": "0.001",
118                            "p": "16578.50",
119                            "L": "PlusTick",
120                            "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
121                            "BT": false
122                        }
123                    "#,
124                    expected: Ok(BybitTradeInner {
125                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
126                            1672304486865,
127                        )),
128                        market: "BTCUSDT".to_string(),
129                        side: Side::Buy,
130                        amount: dec!(0.001),
131                        price: dec!(16578.50),
132                        id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
133                    }),
134                },
135                // TC1: input BybitTradeInner is deserialised
136                TestCase {
137                    input: r#"
138                        {
139                            "T": 1672304486865,
140                            "s": "BTCUSDT",
141                            "S": "Sell",
142                            "v": "0.001",
143                            "p": "16578.50",
144                            "L": "PlusTick",
145                            "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
146                            "BT": false
147                        }
148                    "#,
149                    expected: Ok(BybitTradeInner {
150                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
151                            1672304486865,
152                        )),
153                        market: "BTCUSDT".to_string(),
154                        side: Side::Sell,
155                        amount: dec!(0.001),
156                        price: dec!(16578.50),
157                        id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
158                    }),
159                },
160                // TC2: input BybitTradeInner is unable to be deserialised
161                TestCase {
162                    input: r#"
163                        {
164                            "T": 1672304486865,
165                            "s": "BTCUSDT",
166                            "S": "Unknown",
167                            "v": "0.001",
168                            "p": "16578.50",
169                            "L": "PlusTick",
170                            "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
171                            "BT": false
172                        }
173                    "#,
174                    expected: Err(SocketError::Unsupported {
175                        entity: "".to_string(),
176                        item: "".to_string(),
177                    }),
178                },
179            ];
180
181            for (index, test) in tests.into_iter().enumerate() {
182                let actual = serde_json::from_str::<BybitTradeInner>(test.input);
183                match (actual, test.expected) {
184                    (Ok(actual), Ok(expected)) => {
185                        assert_eq!(actual, expected, "TC{} failed", index)
186                    }
187                    (Err(_), Err(_)) => {
188                        // Test passed
189                    }
190                    (actual, expected) => {
191                        // Test failed
192                        panic!(
193                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
194                        );
195                    }
196                }
197            }
198        }
199
200        #[test]
201        fn test_bybit_trade_payload() {
202            struct TestCase {
203                input: &'static str,
204                expected: Result<BybitTrade, SocketError>,
205            }
206
207            let tests = vec![
208                // TC0: input BybitTrade is deserialised
209                TestCase {
210                    input: r#"
211                        {
212                        "topic": "publicTrade.BTCUSDT",
213                        "type": "snapshot",
214                        "ts": 1672304486868,
215                            "data": [
216                                {
217                                    "T": 1672304486865,
218                                    "s": "BTCUSDT",
219                                    "S": "Buy",
220                                    "v": "0.001",
221                                    "p": "16578.50",
222                                    "L": "PlusTick",
223                                    "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
224                                    "BT": false
225                                },
226                                {
227                                    "T": 1672304486865,
228                                    "s": "BTCUSDT",
229                                    "S": "Sell",
230                                    "v": "0.001",
231                                    "p": "16578.50",
232                                    "L": "PlusTick",
233                                    "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
234                                    "BT": false
235                                }
236                            ]
237                        }
238                    "#,
239                    expected: Ok(BybitTrade {
240                        subscription_id: SubscriptionId("publicTrade|BTCUSDT".to_smolstr()),
241                        kind: BybitPayloadKind::Snapshot,
242                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
243                            1672304486868,
244                        )),
245                        data: vec![
246                            BybitTradeInner {
247                                time: datetime_utc_from_epoch_duration(Duration::from_millis(
248                                    1672304486865,
249                                )),
250                                market: "BTCUSDT".to_string(),
251                                side: Side::Buy,
252                                amount: dec!(0.001),
253                                price: dec!(16578.50),
254                                id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
255                            },
256                            BybitTradeInner {
257                                time: datetime_utc_from_epoch_duration(Duration::from_millis(
258                                    1672304486865,
259                                )),
260                                market: "BTCUSDT".to_string(),
261                                side: Side::Sell,
262                                amount: dec!(0.001),
263                                price: dec!(16578.50),
264                                id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
265                            },
266                        ],
267                    }),
268                },
269                // TC1: input BybitTrade is invalid w/ no subscription_id
270                TestCase {
271                    input: r#"
272                        {
273                            "data": [
274                                {
275                                    "T": 1672304486865,
276                                    "s": "BTCUSDT",
277                                    "S": "Unknown",
278                                    "v": "0.001",
279                                    "p": "16578.50",
280                                    "L": "PlusTick",
281                                    "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
282                                    "BT": false
283                                }
284                            ]
285                        }
286                    "#,
287                    expected: Err(SocketError::Unsupported {
288                        entity: "".to_string(),
289                        item: "".to_string(),
290                    }),
291                },
292                // TC1: input BybitTrade is invalid w/ invalid subscription_id format
293                TestCase {
294                    input: r#"
295                        {
296                        "topic": "publicTrade.BTCUSDT.should_not_be_present",
297                        "type": "snapshot",
298                        "ts": 1672304486868,
299                            "data": [
300                                {
301                                    "T": 1672304486865,
302                                    "s": "BTCUSDT",
303                                    "S": "Buy",
304                                    "v": "0.001",
305                                    "p": "16578.50",
306                                    "L": "PlusTick",
307                                    "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
308                                    "BT": false
309                                },
310                                {
311                                    "T": 1672304486865,
312                                    "s": "BTCUSDT",
313                                    "S": "Sell",
314                                    "v": "0.001",
315                                    "p": "16578.50",
316                                    "L": "PlusTick",
317                                    "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
318                                    "BT": false
319                                }
320                            ]
321                        }
322                    "#,
323                    expected: Err(SocketError::Unsupported {
324                        entity: "".to_string(),
325                        item: "".to_string(),
326                    }),
327                },
328            ];
329
330            for (index, test) in tests.into_iter().enumerate() {
331                let actual = serde_json::from_str::<BybitTrade>(test.input);
332                match (actual, test.expected) {
333                    (Ok(actual), Ok(expected)) => {
334                        assert_eq!(actual, expected, "TC{} failed", index)
335                    }
336                    (Err(_), Err(_)) => {
337                        // Test passed
338                    }
339                    (actual, expected) => {
340                        // Test failed
341                        panic!(
342                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
343                        );
344                    }
345                }
346            }
347        }
348    }
349}