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)]
194mod tests {
195 use rstest::rstest;
196
197 use super::*;
198 use crate::tests::load_test_json;
199
200 #[rstest]
201 fn test_parse_book_change_message() {
202 let json_data = load_test_json("book_change.json");
203 let message: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
204
205 assert_eq!(message.symbol, "XBTUSD");
206 assert_eq!(message.exchange, TardisExchange::Bitmex);
207 assert!(!message.is_snapshot);
208 assert!(message.bids.is_empty());
209 assert_eq!(message.asks.len(), 1);
210 assert_eq!(message.asks[0].price, 7_985.0);
211 assert_eq!(message.asks[0].amount, 283_318.0);
212 assert_eq!(
213 message.timestamp,
214 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
215 );
216 assert_eq!(
217 message.local_timestamp,
218 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
219 );
220 }
221
222 #[rstest]
223 fn test_parse_book_snapshot_message() {
224 let json_data = load_test_json("book_snapshot.json");
225 let message: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
226
227 assert_eq!(message.symbol, "XBTUSD");
228 assert_eq!(message.exchange, TardisExchange::Bitmex);
229 assert_eq!(message.name, "book_snapshot_2_50ms");
230 assert_eq!(message.depth, 2);
231 assert_eq!(message.interval, 50);
232 assert_eq!(message.bids.len(), 2);
233 assert_eq!(message.asks.len(), 2);
234 assert_eq!(message.bids[0].price, 7_633.5);
235 assert_eq!(message.bids[0].amount, 1_906_067.0);
236 assert_eq!(message.asks[0].price, 7_634.0);
237 assert_eq!(message.asks[0].amount, 1_467_849.0);
238 assert_eq!(
239 message.timestamp,
240 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.950Z").unwrap(),
241 );
242 assert_eq!(
243 message.local_timestamp,
244 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.961Z").unwrap()
245 );
246 }
247
248 #[rstest]
249 fn test_parse_trade_message() {
250 let json_data = load_test_json("trade.json");
251 let message: TradeMsg = serde_json::from_str(&json_data).unwrap();
252
253 assert_eq!(message.symbol, "XBTUSD");
254 assert_eq!(message.exchange, TardisExchange::Bitmex);
255 assert_eq!(
256 message.id,
257 Some("282a0445-0e3a-abeb-f403-11003204ea1b".to_string())
258 );
259 assert_eq!(message.price, 7_996.0);
260 assert_eq!(message.amount, 50.0);
261 assert_eq!(message.side, "sell");
262 assert_eq!(
263 message.timestamp,
264 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z").unwrap()
265 );
266 assert_eq!(
267 message.local_timestamp,
268 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.740Z").unwrap()
269 );
270 }
271
272 #[rstest]
273 fn test_parse_derivative_ticker_message() {
274 let json_data = load_test_json("derivative_ticker.json");
275 let message: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
276
277 assert_eq!(message.symbol, "BTC-PERPETUAL");
278 assert_eq!(message.exchange, TardisExchange::Deribit);
279 assert_eq!(message.last_price, Some(7_987.5));
280 assert_eq!(message.open_interest, Some(84_129_491.0));
281 assert_eq!(message.funding_rate, Some(-0.00001568));
282 assert_eq!(message.index_price, Some(7_989.28));
283 assert_eq!(message.mark_price, Some(7_987.56));
284 assert_eq!(
285 message.timestamp,
286 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.302Z").unwrap()
287 );
288 assert_eq!(
289 message.local_timestamp,
290 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
291 );
292 }
293
294 #[rstest]
295 fn test_parse_bar_message() {
296 let json_data = load_test_json("bar.json");
297 let message: BarMsg = serde_json::from_str(&json_data).unwrap();
298
299 assert_eq!(message.symbol, "XBTUSD");
300 assert_eq!(message.exchange, TardisExchange::Bitmex);
301 assert_eq!(message.name, "trade_bar_10000ms");
302 assert_eq!(message.interval, 10_000);
303 assert_eq!(message.open, 7_623.5);
304 assert_eq!(message.high, 7_623.5);
305 assert_eq!(message.low, 7_623.0);
306 assert_eq!(message.close, 7_623.5);
307 assert_eq!(message.volume, 37_034.0);
308 assert_eq!(message.buy_volume, 24_244.0);
309 assert_eq!(message.sell_volume, 12_790.0);
310 assert_eq!(message.trades, 9);
311 assert_eq!(message.vwap, 7_623.327320840309);
312 assert_eq!(
313 message.open_timestamp,
314 DateTime::parse_from_rfc3339("2019-10-25T13:11:31.574Z").unwrap()
315 );
316 assert_eq!(
317 message.close_timestamp,
318 DateTime::parse_from_rfc3339("2019-10-25T13:11:39.212Z").unwrap()
319 );
320 assert_eq!(
321 message.local_timestamp,
322 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.369Z").unwrap()
323 );
324 assert_eq!(
325 message.timestamp,
326 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.000Z").unwrap()
327 );
328 }
329
330 #[rstest]
331 fn test_parse_disconnect_message() {
332 let json_data = load_test_json("disconnect.json");
333 let message: DisconnectMsg = serde_json::from_str(&json_data).unwrap();
334
335 assert_eq!(message.exchange, TardisExchange::Deribit);
336 assert_eq!(
337 message.local_timestamp,
338 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
339 );
340 }
341}