Skip to main content

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