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}