Skip to main content

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