Skip to main content

rustrade_data/exchange/kraken/
trade.rs

1use super::{
2    KrakenMessage,
3    channel::KrakenChannel,
4    message::{KrakenChannelData, KrakenData},
5};
6use crate::{
7    event::{MarketEvent, MarketIter},
8    subscription::trade::PublicTrade,
9};
10use chrono::{DateTime, Utc};
11use rust_decimal::Decimal;
12use rustrade_instrument::{Side, exchange::ExchangeId};
13use serde::{Deserialize, Serialize};
14use smol_str::{SmolStr, format_smolstr};
15
16/// Terse type alias for an [`Kraken`](super::Kraken) real-time trades WebSocket message.
17pub type KrakenTrades = KrakenMessage<KrakenData<KrakenTrade>>;
18
19/// [`Kraken`](super::Kraken) trade.
20///
21/// See [`KrakenMessage`] for full raw payload examples.
22///
23/// See docs: <https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/trade>
24#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
25pub struct KrakenTrade {
26    pub symbol: SmolStr,
27    pub side: Side,
28    #[serde(deserialize_with = "rust_decimal::serde::float::deserialize")]
29    pub price: Decimal,
30    #[serde(
31        rename = "qty",
32        deserialize_with = "rust_decimal::serde::float::deserialize"
33    )]
34    pub amount: Decimal,
35    pub trade_id: u64,
36    pub timestamp: DateTime<Utc>,
37}
38
39impl KrakenChannelData for KrakenTrade {
40    const CHANNEL: KrakenChannel = KrakenChannel::TRADES;
41
42    fn symbol(&self) -> &str {
43        &self.symbol
44    }
45}
46
47impl<InstrumentKey: Clone> From<(ExchangeId, InstrumentKey, KrakenTrades)>
48    for MarketIter<InstrumentKey, PublicTrade>
49{
50    fn from((exchange, instrument, trades): (ExchangeId, InstrumentKey, KrakenTrades)) -> Self {
51        match trades {
52            KrakenTrades::Data(trades) => {
53                let time_received = Utc::now();
54                trades
55                    .data
56                    .into_iter()
57                    .map(|trade| {
58                        Ok(MarketEvent {
59                            time_exchange: trade.timestamp,
60                            time_received,
61                            exchange,
62                            instrument: instrument.clone(),
63                            kind: PublicTrade {
64                                id: format_smolstr!("{}", trade.trade_id),
65                                price: trade.price,
66                                amount: trade.amount,
67                                side: Some(trade.side),
68                            },
69                        })
70                    })
71                    .collect()
72            }
73            KrakenTrades::Event(_) => Self(vec![]),
74        }
75    }
76}
77
78#[cfg(test)]
79#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
80mod tests {
81    use super::*;
82    use crate::Identifier;
83    use chrono::TimeZone;
84    use rust_decimal_macros::dec;
85    use rustrade_integration::subscription::SubscriptionId;
86
87    fn trade(symbol: &str, side: &str, price: &str, qty: &str, trade_id: u64) -> String {
88        format!(
89            r#"{{"symbol":"{symbol}","side":"{side}","price":{price},"qty":{qty},"ord_type":"limit","trade_id":{trade_id},"timestamp":"2018-08-18T17:40:57.321597Z"}}"#
90        )
91    }
92
93    fn message(trades: &[String]) -> String {
94        format!(
95            r#"{{"channel":"trade","type":"update","data":[{}]}}"#,
96            trades.join(",")
97        )
98    }
99
100    #[test]
101    fn test_kraken_message_trades() {
102        let input = message(&[
103            trade("BTC/USD", "sell", "5541.2", "0.15850568", 1001),
104            trade("BTC/USD", "buy", "6060", "5.006e-05", 1002),
105        ]);
106
107        let actual: KrakenTrades = serde_json::from_str(&input).unwrap();
108
109        let time = Utc
110            .with_ymd_and_hms(2018, 8, 18, 17, 40, 57)
111            .unwrap()
112            .checked_add_signed(chrono::Duration::microseconds(321_597))
113            .unwrap();
114        assert_eq!(
115            actual,
116            KrakenTrades::Data(KrakenData {
117                subscription_id: Some(SubscriptionId::from("trade|BTC/USD")),
118                data: vec![
119                    KrakenTrade {
120                        symbol: "BTC/USD".into(),
121                        side: Side::Sell,
122                        price: dec!(5541.2),
123                        amount: dec!(0.15850568),
124                        trade_id: 1001,
125                        timestamp: time,
126                    },
127                    KrakenTrade {
128                        symbol: "BTC/USD".into(),
129                        side: Side::Buy,
130                        price: dec!(6060),
131                        amount: dec!(0.00005006),
132                        trade_id: 1002,
133                        timestamp: time,
134                    },
135                ],
136            })
137        );
138    }
139
140    #[test]
141    fn trades_become_public_trades_keyed_by_kraken_trade_id() {
142        let input = message(&[trade("ETH/USD", "sell", "2509.41", "0.02625345", 67552113)]);
143        let trades: KrakenTrades = serde_json::from_str(&input).unwrap();
144
145        let events = MarketIter::<&str, PublicTrade>::from((ExchangeId::Kraken, "eth", trades)).0;
146
147        assert_eq!(events.len(), 1);
148        let event = events.into_iter().next().unwrap().unwrap();
149        assert_eq!(event.instrument, "eth");
150        assert_eq!(
151            event.kind,
152            PublicTrade {
153                id: "67552113".into(),
154                price: dec!(2509.41),
155                amount: dec!(0.02625345),
156                side: Some(Side::Sell),
157            }
158        );
159    }
160
161    #[test]
162    fn a_message_without_trades_routes_nowhere() {
163        let trades: KrakenTrades = serde_json::from_str(&message(&[])).unwrap();
164        assert_eq!(trades.id(), None);
165    }
166
167    #[test]
168    fn messages_that_carry_no_trades_route_nowhere() {
169        for message in [
170            r#"{"channel":"heartbeat"}"#,
171            r#"{"channel":"status","type":"update","data":[{"api_version":"v2","connection_id":1,"system":"online","version":"2.0.0"}]}"#,
172            r#"{"method":"pong","time_in":"2018-11-12T21:14:59.545897Z","time_out":"2018-11-12T21:14:59.545901Z"}"#,
173        ] {
174            let trades: KrakenTrades = serde_json::from_str(message).unwrap();
175            assert!(matches!(trades, KrakenMessage::Event(_)), "{message}");
176            assert_eq!(trades.id(), None, "{message}");
177        }
178    }
179}