barter_data/exchange/coinbase/
trade.rs

1use super::CoinbaseChannel;
2use crate::{
3    Identifier,
4    event::{MarketEvent, MarketIter},
5    exchange::ExchangeSub,
6    subscription::trade::PublicTrade,
7};
8use barter_instrument::{Side, exchange::ExchangeId};
9use barter_integration::subscription::SubscriptionId;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13/// Coinbase real-time trade WebSocket message.
14///
15/// ### Raw Payload Examples
16/// See docs: <https://docs.cloud.coinbase.com/exchange/docs/websocket-channels#match>
17/// ```json
18/// {
19///     "type": "match",
20///     "trade_id": 10,
21///     "sequence": 50,
22///     "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
23///     "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
24///     "time": "2014-11-07T08:19:27.028459Z",
25///     "product_id": "BTC-USD",
26///     "size": "5.23512",
27///     "price":
28///     "400.23",
29///     "side": "sell"
30/// }
31/// ```
32#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
33pub struct CoinbaseTrade {
34    #[serde(alias = "product_id", deserialize_with = "de_trade_subscription_id")]
35    pub subscription_id: SubscriptionId,
36    #[serde(alias = "trade_id")]
37    pub id: u64,
38    pub time: DateTime<Utc>,
39    #[serde(alias = "size", deserialize_with = "barter_integration::de::de_str")]
40    pub amount: f64,
41    #[serde(deserialize_with = "barter_integration::de::de_str")]
42    pub price: f64,
43    pub side: Side,
44}
45
46impl Identifier<Option<SubscriptionId>> for CoinbaseTrade {
47    fn id(&self) -> Option<SubscriptionId> {
48        Some(self.subscription_id.clone())
49    }
50}
51
52impl<InstrumentKey> From<(ExchangeId, InstrumentKey, CoinbaseTrade)>
53    for MarketIter<InstrumentKey, PublicTrade>
54{
55    fn from((exchange_id, instrument, trade): (ExchangeId, InstrumentKey, CoinbaseTrade)) -> Self {
56        Self(vec![Ok(MarketEvent {
57            time_exchange: trade.time,
58            time_received: Utc::now(),
59            exchange: exchange_id,
60            instrument,
61            kind: PublicTrade {
62                id: trade.id.to_string(),
63                price: trade.price,
64                amount: trade.amount,
65                side: trade.side,
66            },
67        })])
68    }
69}
70
71/// Deserialize a [`CoinbaseTrade`] "product_id" (eg/ "BTC-USD") as the associated [`SubscriptionId`]
72/// (eg/ SubscriptionId("matches|BTC-USD").
73pub fn de_trade_subscription_id<'de, D>(deserializer: D) -> Result<SubscriptionId, D::Error>
74where
75    D: serde::de::Deserializer<'de>,
76{
77    <&str as Deserialize>::deserialize(deserializer)
78        .map(|product_id| ExchangeSub::from((CoinbaseChannel::TRADES, product_id)).id())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use barter_integration::error::SocketError;
85    use chrono::NaiveDateTime;
86    use serde::de::Error;
87    use std::str::FromStr;
88
89    #[test]
90    fn test_de_coinbase_trade() {
91        struct TestCase {
92            input: &'static str,
93            expected: Result<CoinbaseTrade, SocketError>,
94        }
95
96        let cases = vec![
97            TestCase {
98                // TC0: invalid Coinbase message w/ unknown tag
99                input: r#"{"type": "unknown", "sequence": 50,"product_id": "BTC-USD"}"#,
100                expected: Err(SocketError::Deserialise {
101                    error: serde_json::Error::custom(""),
102                    payload: "".to_owned(),
103                }),
104            },
105            TestCase {
106                // TC1: valid Spot CoinbaseTrade
107                input: r#"
108                {
109                    "type": "match","trade_id": 10,"sequence": 50,
110                    "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
111                    "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
112                    "time": "2014-11-07T08:19:27.028459Z",
113                    "product_id": "BTC-USD", "size": "5.23512", "price": "400.23", "side": "sell"
114                }"#,
115                expected: Ok(CoinbaseTrade {
116                    subscription_id: SubscriptionId::from("matches|BTC-USD"),
117                    id: 10,
118                    price: 400.23,
119                    amount: 5.23512,
120                    side: Side::Sell,
121                    time: NaiveDateTime::from_str("2014-11-07T08:19:27.028459")
122                        .unwrap()
123                        .and_utc(),
124                }),
125            },
126        ];
127
128        for (index, test) in cases.into_iter().enumerate() {
129            let actual = serde_json::from_str::<CoinbaseTrade>(test.input);
130            match (actual, test.expected) {
131                (Ok(actual), Ok(expected)) => {
132                    assert_eq!(actual, expected, "TC{} failed", index)
133                }
134                (Err(_), Err(_)) => {
135                    // Test passed
136                }
137                (actual, expected) => {
138                    // Test failed
139                    panic!(
140                        "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
141                    );
142                }
143            }
144        }
145    }
146}