barter_data/exchange/bybit/
trade.rs

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