Skip to main content

rustrade_data/exchange/kraken/book/
l1.rs

1use super::super::{
2    KrakenMessage,
3    channel::KrakenChannel,
4    message::{KrakenChannelData, KrakenData},
5};
6use crate::{
7    books::Level,
8    event::{MarketEvent, MarketIter},
9    subscription::book::OrderBookL1,
10};
11use chrono::{DateTime, Utc};
12use rust_decimal::Decimal;
13use rustrade_instrument::exchange::ExchangeId;
14use serde::{Deserialize, Serialize};
15use smol_str::SmolStr;
16
17/// Terse type alias for an [`Kraken`](super::super::Kraken) real-time OrderBook Level1
18/// (top of books) WebSocket message.
19pub type KrakenOrderBookL1 = KrakenMessage<KrakenData<KrakenTicker>>;
20
21/// [`Kraken`](super::super::Kraken) best bid and ask, from the ticker channel.
22///
23/// The other ticker fields (last trade, volume, daily range) are not read.
24///
25/// See [`KrakenMessage`] for full raw payload examples.
26///
27/// See docs: <https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/ticker>
28#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
29pub struct KrakenTicker {
30    pub symbol: SmolStr,
31    #[serde(
32        rename = "bid",
33        deserialize_with = "rust_decimal::serde::float::deserialize"
34    )]
35    pub best_bid_price: Decimal,
36    #[serde(
37        rename = "bid_qty",
38        deserialize_with = "rust_decimal::serde::float::deserialize"
39    )]
40    pub best_bid_amount: Decimal,
41    #[serde(
42        rename = "ask",
43        deserialize_with = "rust_decimal::serde::float::deserialize"
44    )]
45    pub best_ask_price: Decimal,
46    #[serde(
47        rename = "ask_qty",
48        deserialize_with = "rust_decimal::serde::float::deserialize"
49    )]
50    pub best_ask_amount: Decimal,
51    pub timestamp: DateTime<Utc>,
52}
53
54impl KrakenChannelData for KrakenTicker {
55    const CHANNEL: KrakenChannel = KrakenChannel::ORDER_BOOK_L1;
56
57    fn symbol(&self) -> &str {
58        &self.symbol
59    }
60}
61
62impl<InstrumentKey: Clone> From<(ExchangeId, InstrumentKey, KrakenOrderBookL1)>
63    for MarketIter<InstrumentKey, OrderBookL1>
64{
65    fn from(
66        (exchange_id, instrument, book): (ExchangeId, InstrumentKey, KrakenOrderBookL1),
67    ) -> Self {
68        match book {
69            KrakenOrderBookL1::Data(book) => {
70                let time_received = Utc::now();
71                book.data
72                    .into_iter()
73                    .map(|ticker| {
74                        // A side with no orders is sent as a zero price
75                        let level = |price: Decimal, amount| {
76                            (!price.is_zero()).then(|| Level::new(price, amount))
77                        };
78
79                        Ok(MarketEvent {
80                            time_exchange: ticker.timestamp,
81                            time_received,
82                            exchange: exchange_id,
83                            instrument: instrument.clone(),
84                            kind: OrderBookL1 {
85                                last_update_time: ticker.timestamp,
86                                best_bid: level(ticker.best_bid_price, ticker.best_bid_amount),
87                                best_ask: level(ticker.best_ask_price, ticker.best_ask_amount),
88                            },
89                        })
90                    })
91                    .collect()
92            }
93            KrakenOrderBookL1::Event(_) => MarketIter(vec![]),
94        }
95    }
96}
97
98#[cfg(test)]
99#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
100mod tests {
101    use super::*;
102    use chrono::TimeZone;
103    use rust_decimal_macros::dec;
104    use rustrade_integration::subscription::SubscriptionId;
105
106    const TICKER: &str = r#"{"channel":"ticker","type":"update","data":[{"symbol":"BTC/USD","bid":5698.4,"bid_qty":1.01234567,"ask":5700.0,"ask_qty":0.98765432,"last":5699.1,"volume":1234.5,"vwap":5650.2,"low":5600.0,"high":5710.0,"change":12.3,"change_pct":0.22,"timestamp":"2018-11-12T21:14:59.545897Z"}]}"#;
107
108    fn time() -> DateTime<Utc> {
109        Utc.with_ymd_and_hms(2018, 11, 12, 21, 14, 59)
110            .unwrap()
111            .checked_add_signed(chrono::Duration::microseconds(545_897))
112            .unwrap()
113    }
114
115    #[test]
116    fn test_kraken_message_order_book_l1() {
117        let actual: KrakenOrderBookL1 = serde_json::from_str(TICKER).unwrap();
118
119        assert_eq!(
120            actual,
121            KrakenOrderBookL1::Data(KrakenData {
122                subscription_id: Some(SubscriptionId::from("ticker|BTC/USD")),
123                data: vec![KrakenTicker {
124                    symbol: "BTC/USD".into(),
125                    best_bid_price: dec!(5698.4),
126                    best_bid_amount: dec!(1.01234567),
127                    best_ask_price: dec!(5700),
128                    best_ask_amount: dec!(0.98765432),
129                    timestamp: time(),
130                }],
131            })
132        );
133    }
134
135    #[test]
136    fn a_ticker_becomes_the_top_of_book() {
137        let book: KrakenOrderBookL1 = serde_json::from_str(TICKER).unwrap();
138
139        let events = MarketIter::<&str, OrderBookL1>::from((ExchangeId::Kraken, "btc", book)).0;
140
141        assert_eq!(events.len(), 1);
142        let event = events.into_iter().next().unwrap().unwrap();
143        assert_eq!(event.time_exchange, time());
144        assert_eq!(
145            event.kind,
146            OrderBookL1 {
147                last_update_time: time(),
148                best_bid: Some(Level::new(dec!(5698.4), dec!(1.01234567))),
149                best_ask: Some(Level::new(dec!(5700), dec!(0.98765432))),
150            }
151        );
152    }
153
154    #[test]
155    fn a_zero_price_is_an_empty_side() {
156        let input = TICKER.replace(r#""bid":5698.4"#, r#""bid":0"#);
157        let book: KrakenOrderBookL1 = serde_json::from_str(&input).unwrap();
158
159        let events = MarketIter::<&str, OrderBookL1>::from((ExchangeId::Kraken, "btc", book)).0;
160
161        let event = events.into_iter().next().unwrap().unwrap();
162        assert_eq!(event.kind.best_bid, None);
163        assert!(event.kind.best_ask.is_some());
164    }
165}