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