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}