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