1use std::collections::HashMap;
24
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use crate::common::{
30 enums::{
31 CoinbaseContractExpiryType, CoinbaseMarginLevel, CoinbaseMarginWindowType,
32 CoinbaseOrderSide, CoinbaseOrderStatus, CoinbaseOrderType, CoinbaseProductStatus,
33 CoinbaseProductType, CoinbaseRiskManagedBy, CoinbaseTimeInForce, CoinbaseTriggerStatus,
34 CoinbaseWsChannel,
35 },
36 parse::deserialize_decimal_from_str,
37};
38
39#[derive(Debug, Clone, Serialize)]
45pub struct CoinbaseWsSubscription {
46 #[serde(rename = "type")]
48 pub msg_type: CoinbaseWsAction,
49 #[serde(skip_serializing_if = "Vec::is_empty")]
51 pub product_ids: Vec<Ustr>,
52 pub channel: CoinbaseWsChannel,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub jwt: Option<String>,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum CoinbaseWsAction {
63 Subscribe,
64 Unsubscribe,
65}
66
67#[derive(Debug, Clone, Deserialize)]
72#[serde(tag = "channel")]
73pub enum CoinbaseWsMessage {
74 #[serde(rename = "l2_data")]
76 L2Data {
77 timestamp: String,
78 sequence_num: u64,
79 events: Vec<WsL2DataEvent>,
80 },
81
82 #[serde(rename = "market_trades")]
84 MarketTrades {
85 timestamp: String,
86 sequence_num: u64,
87 events: Vec<WsMarketTradesEvent>,
88 },
89
90 #[serde(rename = "ticker")]
92 Ticker {
93 timestamp: String,
94 sequence_num: u64,
95 events: Vec<WsTickerEvent>,
96 },
97
98 #[serde(rename = "ticker_batch")]
100 TickerBatch {
101 timestamp: String,
102 sequence_num: u64,
103 events: Vec<WsTickerEvent>,
104 },
105
106 #[serde(rename = "candles")]
108 Candles {
109 timestamp: String,
110 sequence_num: u64,
111 events: Vec<WsCandlesEvent>,
112 },
113
114 #[serde(rename = "user")]
119 User {
120 timestamp: String,
121 sequence_num: u64,
122 events: Vec<WsUserEvent>,
123 },
124
125 #[serde(rename = "heartbeats")]
127 Heartbeats {
128 timestamp: String,
129 sequence_num: u64,
130 events: Vec<WsHeartbeatEvent>,
131 },
132
133 #[serde(rename = "futures_balance_summary")]
138 FuturesBalanceSummary {
139 timestamp: String,
140 sequence_num: u64,
141 events: Vec<WsFuturesBalanceSummaryEvent>,
142 },
143
144 #[serde(rename = "status")]
149 Status {
150 timestamp: String,
151 sequence_num: u64,
152 events: Vec<WsStatusEvent>,
153 },
154
155 #[serde(rename = "subscriptions")]
157 Subscriptions {
158 timestamp: String,
159 sequence_num: u64,
160 events: Vec<WsSubscriptionsEvent>,
161 },
162}
163
164#[derive(Debug, Clone, Deserialize)]
166pub struct WsL2DataEvent {
167 #[serde(rename = "type")]
169 pub event_type: WsEventType,
170 pub product_id: Ustr,
171 pub updates: Vec<WsL2Update>,
172}
173
174#[derive(Debug, Clone, Deserialize)]
176pub struct WsL2Update {
177 pub side: WsBookSide,
178 pub event_time: String,
179 pub price_level: String,
180 pub new_quantity: String,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum WsBookSide {
187 Bid,
188 Offer,
189}
190
191#[derive(Debug, Clone, Deserialize)]
193pub struct WsMarketTradesEvent {
194 #[serde(rename = "type")]
196 pub event_type: WsEventType,
197 pub trades: Vec<WsTrade>,
198}
199
200#[derive(Debug, Clone, Deserialize)]
202pub struct WsTrade {
203 pub trade_id: String,
204 pub product_id: Ustr,
205 pub price: String,
206 pub size: String,
207 pub side: CoinbaseOrderSide,
208 pub time: String,
209}
210
211#[derive(Debug, Clone, Deserialize)]
213pub struct WsTickerEvent {
214 #[serde(rename = "type")]
216 pub event_type: WsEventType,
217 pub tickers: Vec<WsTicker>,
218}
219
220#[derive(Debug, Clone, Deserialize)]
222pub struct WsTicker {
223 pub product_id: Ustr,
224 pub price: String,
225 pub volume_24_h: String,
226 pub low_24_h: String,
227 pub high_24_h: String,
228 #[serde(default)]
229 pub low_52_w: String,
230 #[serde(default)]
231 pub high_52_w: String,
232 pub price_percent_chg_24_h: String,
233 pub best_bid: String,
234 pub best_bid_quantity: String,
235 pub best_ask: String,
236 pub best_ask_quantity: String,
237}
238
239#[derive(Debug, Clone, Deserialize)]
241pub struct WsCandlesEvent {
242 #[serde(rename = "type")]
244 pub event_type: WsEventType,
245 pub candles: Vec<WsCandle>,
246}
247
248#[derive(Debug, Clone, Deserialize)]
250pub struct WsCandle {
251 pub start: String,
252 pub high: String,
253 pub low: String,
254 pub open: String,
255 pub close: String,
256 pub volume: String,
257 pub product_id: Ustr,
258}
259
260#[derive(Debug, Clone, Deserialize)]
262pub struct WsUserEvent {
263 #[serde(rename = "type")]
265 pub event_type: WsEventType,
266 pub orders: Vec<WsOrderUpdate>,
267}
268
269#[derive(Debug, Clone, Deserialize)]
271pub struct WsOrderUpdate {
272 pub order_id: String,
273 pub client_order_id: String,
274 pub contract_expiry_type: CoinbaseContractExpiryType,
275 pub cumulative_quantity: String,
276 pub leaves_quantity: String,
277 pub avg_price: String,
278 pub total_fees: String,
279 pub status: CoinbaseOrderStatus,
280 pub product_id: Ustr,
281 pub product_type: CoinbaseProductType,
282 pub creation_time: String,
283 pub order_side: CoinbaseOrderSide,
284 pub order_type: CoinbaseOrderType,
285 pub risk_managed_by: CoinbaseRiskManagedBy,
286 pub time_in_force: CoinbaseTimeInForce,
287 pub trigger_status: CoinbaseTriggerStatus,
288 #[serde(default)]
289 pub cancel_reason: String,
290 #[serde(default)]
291 pub reject_reason: String,
292 #[serde(default)]
293 pub total_value_after_fees: String,
294}
295
296#[derive(Debug, Clone, Deserialize)]
298pub struct WsHeartbeatEvent {
299 pub current_time: String,
300 pub heartbeat_counter: u64,
301}
302
303#[derive(Debug, Clone, Deserialize)]
305pub struct WsFuturesBalanceSummaryEvent {
306 #[serde(rename = "type")]
307 pub event_type: WsEventType,
308 pub fcm_balance_summary: WsFcmBalanceSummary,
309}
310
311#[derive(Debug, Clone, Deserialize)]
313pub struct WsFcmBalanceSummary {
314 #[serde(deserialize_with = "deserialize_decimal_from_str")]
315 pub futures_buying_power: Decimal,
316 #[serde(deserialize_with = "deserialize_decimal_from_str")]
317 pub total_usd_balance: Decimal,
318 #[serde(deserialize_with = "deserialize_decimal_from_str")]
319 pub cbi_usd_balance: Decimal,
320 #[serde(deserialize_with = "deserialize_decimal_from_str")]
321 pub cfm_usd_balance: Decimal,
322 #[serde(deserialize_with = "deserialize_decimal_from_str")]
323 pub total_open_orders_hold_amount: Decimal,
324 #[serde(deserialize_with = "deserialize_decimal_from_str")]
325 pub unrealized_pnl: Decimal,
326 #[serde(deserialize_with = "deserialize_decimal_from_str")]
327 pub daily_realized_pnl: Decimal,
328 #[serde(deserialize_with = "deserialize_decimal_from_str")]
329 pub initial_margin: Decimal,
330 #[serde(deserialize_with = "deserialize_decimal_from_str")]
331 pub available_margin: Decimal,
332 #[serde(deserialize_with = "deserialize_decimal_from_str")]
333 pub liquidation_threshold: Decimal,
334 #[serde(deserialize_with = "deserialize_decimal_from_str")]
335 pub liquidation_buffer_amount: Decimal,
336 #[serde(deserialize_with = "deserialize_decimal_from_str")]
337 pub liquidation_buffer_percentage: Decimal,
338 pub intraday_margin_window_measure: WsMarginWindowMeasure,
339 pub overnight_margin_window_measure: WsMarginWindowMeasure,
340}
341
342#[derive(Debug, Clone, Deserialize)]
344pub struct WsMarginWindowMeasure {
345 pub margin_window_type: CoinbaseMarginWindowType,
346 pub margin_level: CoinbaseMarginLevel,
347 #[serde(deserialize_with = "deserialize_decimal_from_str")]
348 pub initial_margin: Decimal,
349 #[serde(deserialize_with = "deserialize_decimal_from_str")]
350 pub maintenance_margin: Decimal,
351 #[serde(deserialize_with = "deserialize_decimal_from_str")]
352 pub liquidation_buffer_percentage: Decimal,
353 #[serde(deserialize_with = "deserialize_decimal_from_str")]
354 pub total_hold: Decimal,
355 #[serde(deserialize_with = "deserialize_decimal_from_str")]
356 pub futures_buying_power: Decimal,
357}
358
359#[derive(Debug, Clone, Deserialize)]
361pub struct WsStatusEvent {
362 #[serde(rename = "type")]
363 pub event_type: WsEventType,
364 #[serde(default)]
365 pub products: Vec<WsStatusProduct>,
366}
367
368#[derive(Debug, Clone, Deserialize)]
370pub struct WsStatusProduct {
371 pub product_type: CoinbaseProductType,
372 pub id: Ustr,
373 pub base_currency: Ustr,
374 pub quote_currency: Ustr,
375 pub base_increment: String,
376 pub quote_increment: String,
377 pub display_name: String,
378 pub status: CoinbaseProductStatus,
379 pub status_message: String,
380 #[serde(deserialize_with = "deserialize_decimal_from_str")]
381 pub min_market_funds: Decimal,
382}
383
384#[derive(Debug, Clone, Deserialize)]
386pub struct WsSubscriptionsEvent {
387 pub subscriptions: HashMap<CoinbaseWsChannel, Vec<Ustr>>,
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
392#[serde(rename_all = "snake_case")]
393pub enum WsEventType {
394 Snapshot,
395 Update,
396}
397
398#[cfg(test)]
399mod tests {
400 use rstest::rstest;
401
402 use super::*;
403 use crate::common::testing::load_test_fixture;
404
405 #[rstest]
406 fn test_deserialize_l2_snapshot() {
407 let json = load_test_fixture("ws_l2_data_snapshot.json");
408 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
409
410 match msg {
411 CoinbaseWsMessage::L2Data {
412 timestamp,
413 sequence_num,
414 events,
415 } => {
416 assert!(!timestamp.is_empty());
417 assert_eq!(sequence_num, 0);
418 assert_eq!(events.len(), 1);
419
420 let event = &events[0];
421 assert_eq!(event.event_type, WsEventType::Snapshot);
422 assert_eq!(event.product_id, "BTC-USD");
423 assert!(!event.updates.is_empty());
424
425 let bid = event
426 .updates
427 .iter()
428 .find(|u| u.side == WsBookSide::Bid)
429 .expect("should have a bid update");
430 assert!(!bid.price_level.is_empty());
431 assert!(!bid.new_quantity.is_empty());
432 }
433 other => panic!("Expected L2Data, was {other:?}"),
434 }
435 }
436
437 #[rstest]
438 fn test_deserialize_l2_update() {
439 let json = load_test_fixture("ws_l2_data_update.json");
440 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
441
442 match msg {
443 CoinbaseWsMessage::L2Data {
444 sequence_num,
445 events,
446 ..
447 } => {
448 assert!(sequence_num > 0);
449 assert_eq!(events[0].event_type, WsEventType::Update);
450 }
451 other => panic!("Expected L2Data, was {other:?}"),
452 }
453 }
454
455 #[rstest]
456 fn test_deserialize_market_trades() {
457 let json = load_test_fixture("ws_market_trades.json");
458 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
459
460 match msg {
461 CoinbaseWsMessage::MarketTrades { events, .. } => {
462 assert_eq!(events.len(), 1);
463 assert!(!events[0].trades.is_empty());
464
465 let trade = &events[0].trades[0];
466 assert_eq!(trade.product_id, "BTC-USD");
467 assert!(!trade.price.is_empty());
468 assert!(!trade.size.is_empty());
469 assert!(!trade.trade_id.is_empty());
470 }
471 other => panic!("Expected MarketTrades, was {other:?}"),
472 }
473 }
474
475 #[rstest]
476 fn test_deserialize_ticker() {
477 let json = load_test_fixture("ws_ticker.json");
478 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
479
480 match msg {
481 CoinbaseWsMessage::Ticker { events, .. } => {
482 assert_eq!(events.len(), 1);
483 assert!(!events[0].tickers.is_empty());
484
485 let ticker = &events[0].tickers[0];
486 assert_eq!(ticker.product_id, "BTC-USD");
487 assert!(!ticker.best_bid.is_empty());
488 assert!(!ticker.best_ask.is_empty());
489 assert!(!ticker.best_bid_quantity.is_empty());
490 assert!(!ticker.best_ask_quantity.is_empty());
491 }
492 other => panic!("Expected Ticker, was {other:?}"),
493 }
494 }
495
496 #[rstest]
497 fn test_deserialize_candles() {
498 let json = load_test_fixture("ws_candles.json");
499 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
500
501 match msg {
502 CoinbaseWsMessage::Candles { events, .. } => {
503 assert_eq!(events.len(), 1);
504 assert!(!events[0].candles.is_empty());
505
506 let candle = &events[0].candles[0];
507 assert_eq!(candle.product_id, "BTC-USD");
508 assert!(!candle.open.is_empty());
509 assert!(!candle.high.is_empty());
510 assert!(!candle.low.is_empty());
511 assert!(!candle.close.is_empty());
512 assert!(!candle.volume.is_empty());
513 }
514 other => panic!("Expected Candles, was {other:?}"),
515 }
516 }
517
518 #[rstest]
519 fn test_deserialize_user_order_update() {
520 let json = load_test_fixture("ws_user.json");
521 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
522
523 match msg {
524 CoinbaseWsMessage::User { events, .. } => {
525 assert_eq!(events.len(), 1);
526 assert!(!events[0].orders.is_empty());
527
528 let order = &events[0].orders[0];
529 assert!(!order.order_id.is_empty());
530 assert_eq!(order.product_id, "BTC-USD");
531 assert_eq!(order.status, CoinbaseOrderStatus::Open);
532 assert_eq!(order.order_side, CoinbaseOrderSide::Buy);
533 assert_eq!(order.order_type, CoinbaseOrderType::Limit);
534 assert_eq!(
535 order.contract_expiry_type,
536 CoinbaseContractExpiryType::Unknown
537 );
538 assert_eq!(order.product_type, CoinbaseProductType::Spot);
539 assert_eq!(order.risk_managed_by, CoinbaseRiskManagedBy::Unknown);
540 assert_eq!(order.time_in_force, CoinbaseTimeInForce::GoodUntilCancelled);
541 assert_eq!(
542 order.trigger_status,
543 CoinbaseTriggerStatus::InvalidOrderType
544 );
545 }
546 other => panic!("Expected User, was {other:?}"),
547 }
548 }
549
550 #[rstest]
551 fn test_deserialize_heartbeat() {
552 let json = load_test_fixture("ws_heartbeats.json");
553 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
554
555 match msg {
556 CoinbaseWsMessage::Heartbeats { events, .. } => {
557 assert_eq!(events.len(), 1);
558 assert!(!events[0].current_time.is_empty());
559 assert!(events[0].heartbeat_counter > 0);
560 }
561 other => panic!("Expected Heartbeats, was {other:?}"),
562 }
563 }
564
565 #[rstest]
566 fn test_deserialize_status_channel() {
567 let json = r#"{
568 "channel": "status",
569 "client_id": "",
570 "timestamp": "2023-02-09T20:29:49.753424311Z",
571 "sequence_num": 0,
572 "events": [
573 {
574 "type": "snapshot",
575 "products": [
576 {
577 "product_type": "SPOT",
578 "id": "BTC-USD",
579 "base_currency": "BTC",
580 "quote_currency": "USD",
581 "base_increment": "0.00000001",
582 "quote_increment": "0.01",
583 "display_name": "BTC/USD",
584 "status": "online",
585 "status_message": "",
586 "min_market_funds": "1"
587 }
588 ]
589 }
590 ]
591 }"#;
592 let msg: CoinbaseWsMessage = serde_json::from_str(json).unwrap();
593
594 match msg {
595 CoinbaseWsMessage::Status { events, .. } => {
596 assert_eq!(events.len(), 1);
597 assert_eq!(events[0].event_type, WsEventType::Snapshot);
598 assert_eq!(events[0].products.len(), 1);
599 let product = &events[0].products[0];
600 assert_eq!(product.id, "BTC-USD");
601 assert_eq!(product.product_type, CoinbaseProductType::Spot);
602 assert_eq!(product.status, CoinbaseProductStatus::Online);
603 assert_eq!(product.min_market_funds, Decimal::ONE);
604 }
605 other => panic!("Expected Status, was {other:?}"),
606 }
607 }
608
609 #[rstest]
610 fn test_deserialize_futures_balance_summary_channel() {
611 let json = r#"{
612 "channel": "futures_balance_summary",
613 "client_id": "",
614 "timestamp": "2023-02-09T20:33:57.609931463Z",
615 "sequence_num": 0,
616 "events": [
617 {
618 "type": "snapshot",
619 "fcm_balance_summary": {
620 "futures_buying_power": "100.00",
621 "total_usd_balance": "200.00",
622 "cbi_usd_balance": "300.00",
623 "cfm_usd_balance": "400.00",
624 "total_open_orders_hold_amount": "500.00",
625 "unrealized_pnl": "600.00",
626 "daily_realized_pnl": "0",
627 "initial_margin": "700.00",
628 "available_margin": "800.00",
629 "liquidation_threshold": "900.00",
630 "liquidation_buffer_amount": "1000.00",
631 "liquidation_buffer_percentage": "1000",
632 "intraday_margin_window_measure": {
633 "margin_window_type": "FCM_MARGIN_WINDOW_TYPE_INTRADAY",
634 "margin_level": "MARGIN_LEVEL_TYPE_BASE",
635 "initial_margin": "100.00",
636 "maintenance_margin": "200.00",
637 "liquidation_buffer_percentage": "1000",
638 "total_hold": "100.00",
639 "futures_buying_power": "400.00"
640 },
641 "overnight_margin_window_measure": {
642 "margin_window_type": "FCM_MARGIN_WINDOW_TYPE_OVERNIGHT",
643 "margin_level": "MARGIN_LEVEL_TYPE_BASE",
644 "initial_margin": "300.00",
645 "maintenance_margin": "200.00",
646 "liquidation_buffer_percentage": "1000",
647 "total_hold": "-30.00",
648 "futures_buying_power": "2000.00"
649 }
650 }
651 }
652 ]
653 }"#;
654 let msg: CoinbaseWsMessage = serde_json::from_str(json).unwrap();
655
656 match msg {
657 CoinbaseWsMessage::FuturesBalanceSummary { events, .. } => {
658 assert_eq!(events.len(), 1);
659 assert_eq!(events[0].event_type, WsEventType::Snapshot);
660 let summary = &events[0].fcm_balance_summary;
661 assert_eq!(summary.futures_buying_power, Decimal::from(100));
662 assert_eq!(summary.daily_realized_pnl, Decimal::ZERO);
663 assert_eq!(
664 summary.intraday_margin_window_measure.margin_window_type,
665 CoinbaseMarginWindowType::Intraday
666 );
667 assert_eq!(
668 summary.overnight_margin_window_measure.margin_level,
669 CoinbaseMarginLevel::Base
670 );
671 assert_eq!(
672 summary.overnight_margin_window_measure.total_hold,
673 "-30.00".parse::<Decimal>().unwrap()
674 );
675 }
676 other => panic!("Expected FuturesBalanceSummary, was {other:?}"),
677 }
678 }
679
680 #[rstest]
681 fn test_deserialize_subscriptions() {
682 let json = load_test_fixture("ws_subscriptions.json");
683 let msg: CoinbaseWsMessage = serde_json::from_str(&json).unwrap();
684
685 match msg {
686 CoinbaseWsMessage::Subscriptions { events, .. } => {
687 assert_eq!(events.len(), 1);
688 assert_eq!(
689 events[0].subscriptions.get(&CoinbaseWsChannel::Level2),
690 Some(&vec![Ustr::from("BTC-USD")])
691 );
692 assert_eq!(
693 events[0]
694 .subscriptions
695 .get(&CoinbaseWsChannel::MarketTrades),
696 Some(&vec![Ustr::from("BTC-USD"), Ustr::from("ETH-USD")])
697 );
698 }
699 other => panic!("Expected Subscriptions, was {other:?}"),
700 }
701 }
702
703 #[rstest]
704 fn test_serialize_subscribe_request_with_jwt() {
705 let sub = CoinbaseWsSubscription {
706 msg_type: CoinbaseWsAction::Subscribe,
707 product_ids: vec![Ustr::from("BTC-USD")],
708 channel: CoinbaseWsChannel::User,
709 jwt: Some("test-jwt-token".to_string()),
710 };
711
712 let json = serde_json::to_value(&sub).unwrap();
713 assert_eq!(json["type"], "subscribe");
714 assert_eq!(json["channel"], "user");
715 assert_eq!(json["product_ids"][0], "BTC-USD");
716 assert_eq!(json["jwt"], "test-jwt-token");
717 }
718
719 #[rstest]
720 fn test_serialize_subscribe_request_public_omits_jwt() {
721 let sub = CoinbaseWsSubscription {
722 msg_type: CoinbaseWsAction::Subscribe,
723 product_ids: vec![Ustr::from("BTC-USD")],
724 channel: CoinbaseWsChannel::Level2,
725 jwt: None,
726 };
727
728 let json = serde_json::to_value(&sub).unwrap();
729 assert_eq!(json["type"], "subscribe");
730 assert_eq!(json["channel"], "level2");
731 assert!(json.get("jwt").is_none());
732 }
733
734 #[rstest]
735 fn test_serialize_unsubscribe_request() {
736 let sub = CoinbaseWsSubscription {
737 msg_type: CoinbaseWsAction::Unsubscribe,
738 product_ids: vec![Ustr::from("ETH-USD")],
739 channel: CoinbaseWsChannel::MarketTrades,
740 jwt: None,
741 };
742
743 let json = serde_json::to_value(&sub).unwrap();
744 assert_eq!(json["type"], "unsubscribe");
745 assert_eq!(json["channel"], "market_trades");
746 assert!(json.get("jwt").is_none());
747 }
748
749 #[rstest]
750 fn test_serialize_channel_level_subscription_omits_product_ids() {
751 let sub = CoinbaseWsSubscription {
752 msg_type: CoinbaseWsAction::Subscribe,
753 product_ids: vec![],
754 channel: CoinbaseWsChannel::Heartbeats,
755 jwt: None,
756 };
757
758 let json = serde_json::to_value(&sub).unwrap();
759 assert_eq!(json["type"], "subscribe");
760 assert_eq!(json["channel"], "heartbeats");
761 assert!(json.get("product_ids").is_none());
762 assert!(json.get("jwt").is_none());
763 }
764
765 #[rstest]
766 fn test_ws_event_type_values() {
767 let snapshot: WsEventType = serde_json::from_str("\"snapshot\"").unwrap();
768 assert_eq!(snapshot, WsEventType::Snapshot);
769
770 let update: WsEventType = serde_json::from_str("\"update\"").unwrap();
771 assert_eq!(update, WsEventType::Update);
772 }
773
774 #[rstest]
775 fn test_ws_book_side_values() {
776 let bid: WsBookSide = serde_json::from_str("\"bid\"").unwrap();
777 assert_eq!(bid, WsBookSide::Bid);
778
779 let offer: WsBookSide = serde_json::from_str("\"offer\"").unwrap();
780 assert_eq!(offer, WsBookSide::Offer);
781 }
782}