1use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use ustr::Ustr;
19
20use crate::{enums::TardisExchange, parse::deserialize_uppercase};
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct BookLevel {
25 pub price: f64,
27 pub amount: f64,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct BookChangeMsg {
35 #[serde(deserialize_with = "deserialize_uppercase")]
37 pub symbol: Ustr,
38 pub exchange: TardisExchange,
40 pub is_snapshot: bool,
42 pub bids: Vec<BookLevel>,
44 pub asks: Vec<BookLevel>,
46 pub timestamp: DateTime<Utc>,
48 pub local_timestamp: DateTime<Utc>,
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookSnapshotMsg {
56 #[serde(deserialize_with = "deserialize_uppercase")]
58 pub symbol: Ustr,
59 pub exchange: TardisExchange,
61 pub name: String,
63 pub depth: u32,
65 pub interval: u32,
67 pub bids: Vec<BookLevel>,
69 pub asks: Vec<BookLevel>,
71 pub timestamp: DateTime<Utc>,
73 pub local_timestamp: DateTime<Utc>,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
79#[serde(tag = "type")]
80#[serde(rename_all = "camelCase")]
81pub struct TradeMsg {
82 #[serde(deserialize_with = "deserialize_uppercase")]
84 pub symbol: Ustr,
85 pub exchange: TardisExchange,
87 pub id: Option<String>,
89 pub price: f64,
91 pub amount: f64,
93 pub side: String,
95 pub timestamp: DateTime<Utc>,
97 pub local_timestamp: DateTime<Utc>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct DerivativeTickerMsg {
105 #[serde(deserialize_with = "deserialize_uppercase")]
107 pub symbol: Ustr,
108 pub exchange: TardisExchange,
110 pub last_price: Option<f64>,
112 pub open_interest: Option<f64>,
114 pub funding_rate: Option<f64>,
116 pub index_price: Option<f64>,
118 pub mark_price: Option<f64>,
120 pub timestamp: DateTime<Utc>,
122 pub local_timestamp: DateTime<Utc>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct BarMsg {
132 #[serde(deserialize_with = "deserialize_uppercase")]
134 pub symbol: Ustr,
135 pub exchange: TardisExchange,
137 pub name: String,
139 pub interval: u64,
141 pub open: f64,
143 pub high: f64,
145 pub low: f64,
147 pub close: f64,
149 pub volume: f64,
151 pub buy_volume: f64,
153 pub sell_volume: f64,
155 pub trades: u64,
157 pub vwap: f64,
159 pub open_timestamp: DateTime<Utc>,
161 pub close_timestamp: DateTime<Utc>,
163 pub timestamp: DateTime<Utc>,
165 pub local_timestamp: DateTime<Utc>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DisconnectMsg {
174 pub exchange: TardisExchange,
176 pub local_timestamp: DateTime<Utc>,
178}
179
180#[allow(missing_docs)]
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case", tag = "type")]
184pub enum WsMessage {
185 BookChange(BookChangeMsg),
186 BookSnapshot(BookSnapshotMsg),
187 Trade(TradeMsg),
188 TradeBar(BarMsg),
189 DerivativeTicker(DerivativeTickerMsg),
190 Disconnect(DisconnectMsg),
191}
192
193#[cfg(test)]
198mod tests {
199 use rstest::rstest;
200
201 use super::*;
202 use crate::tests::load_test_json;
203
204 #[rstest]
205 fn test_parse_book_change_message() {
206 let json_data = load_test_json("book_change.json");
207 let message: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
208
209 assert_eq!(message.symbol, "XBTUSD");
210 assert_eq!(message.exchange, TardisExchange::Bitmex);
211 assert!(!message.is_snapshot);
212 assert!(message.bids.is_empty());
213 assert_eq!(message.asks.len(), 1);
214 assert_eq!(message.asks[0].price, 7_985.0);
215 assert_eq!(message.asks[0].amount, 283_318.0);
216 assert_eq!(
217 message.timestamp,
218 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
219 );
220 assert_eq!(
221 message.local_timestamp,
222 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
223 );
224 }
225
226 #[rstest]
227 fn test_parse_book_snapshot_message() {
228 let json_data = load_test_json("book_snapshot.json");
229 let message: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
230
231 assert_eq!(message.symbol, "XBTUSD");
232 assert_eq!(message.exchange, TardisExchange::Bitmex);
233 assert_eq!(message.name, "book_snapshot_2_50ms");
234 assert_eq!(message.depth, 2);
235 assert_eq!(message.interval, 50);
236 assert_eq!(message.bids.len(), 2);
237 assert_eq!(message.asks.len(), 2);
238 assert_eq!(message.bids[0].price, 7_633.5);
239 assert_eq!(message.bids[0].amount, 1_906_067.0);
240 assert_eq!(message.asks[0].price, 7_634.0);
241 assert_eq!(message.asks[0].amount, 1_467_849.0);
242 assert_eq!(
243 message.timestamp,
244 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.950Z").unwrap(),
245 );
246 assert_eq!(
247 message.local_timestamp,
248 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.961Z").unwrap()
249 );
250 }
251
252 #[rstest]
253 fn test_parse_trade_message() {
254 let json_data = load_test_json("trade.json");
255 let message: TradeMsg = serde_json::from_str(&json_data).unwrap();
256
257 assert_eq!(message.symbol, "XBTUSD");
258 assert_eq!(message.exchange, TardisExchange::Bitmex);
259 assert_eq!(
260 message.id,
261 Some("282a0445-0e3a-abeb-f403-11003204ea1b".to_string())
262 );
263 assert_eq!(message.price, 7_996.0);
264 assert_eq!(message.amount, 50.0);
265 assert_eq!(message.side, "sell");
266 assert_eq!(
267 message.timestamp,
268 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z").unwrap()
269 );
270 assert_eq!(
271 message.local_timestamp,
272 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.740Z").unwrap()
273 );
274 }
275
276 #[rstest]
277 fn test_parse_derivative_ticker_message() {
278 let json_data = load_test_json("derivative_ticker.json");
279 let message: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
280
281 assert_eq!(message.symbol, "BTC-PERPETUAL");
282 assert_eq!(message.exchange, TardisExchange::Deribit);
283 assert_eq!(message.last_price, Some(7_987.5));
284 assert_eq!(message.open_interest, Some(84_129_491.0));
285 assert_eq!(message.funding_rate, Some(-0.00001568));
286 assert_eq!(message.index_price, Some(7_989.28));
287 assert_eq!(message.mark_price, Some(7_987.56));
288 assert_eq!(
289 message.timestamp,
290 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.302Z").unwrap()
291 );
292 assert_eq!(
293 message.local_timestamp,
294 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
295 );
296 }
297
298 #[rstest]
299 fn test_parse_bar_message() {
300 let json_data = load_test_json("bar.json");
301 let message: BarMsg = serde_json::from_str(&json_data).unwrap();
302
303 assert_eq!(message.symbol, "XBTUSD");
304 assert_eq!(message.exchange, TardisExchange::Bitmex);
305 assert_eq!(message.name, "trade_bar_10000ms");
306 assert_eq!(message.interval, 10_000);
307 assert_eq!(message.open, 7_623.5);
308 assert_eq!(message.high, 7_623.5);
309 assert_eq!(message.low, 7_623.0);
310 assert_eq!(message.close, 7_623.5);
311 assert_eq!(message.volume, 37_034.0);
312 assert_eq!(message.buy_volume, 24_244.0);
313 assert_eq!(message.sell_volume, 12_790.0);
314 assert_eq!(message.trades, 9);
315 assert_eq!(message.vwap, 7_623.327320840309);
316 assert_eq!(
317 message.open_timestamp,
318 DateTime::parse_from_rfc3339("2019-10-25T13:11:31.574Z").unwrap()
319 );
320 assert_eq!(
321 message.close_timestamp,
322 DateTime::parse_from_rfc3339("2019-10-25T13:11:39.212Z").unwrap()
323 );
324 assert_eq!(
325 message.local_timestamp,
326 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.369Z").unwrap()
327 );
328 assert_eq!(
329 message.timestamp,
330 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.000Z").unwrap()
331 );
332 }
333
334 #[rstest]
335 fn test_parse_disconnect_message() {
336 let json_data = load_test_json("disconnect.json");
337 let message: DisconnectMsg = serde_json::from_str(&json_data).unwrap();
338
339 assert_eq!(message.exchange, TardisExchange::Deribit);
340 assert_eq!(
341 message.local_timestamp,
342 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
343 );
344 }
345}