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