Skip to main content

rustrade_data/exchange/kraken/
mod.rs

1use self::{
2    book::l1::KrakenOrderBookL1, channel::KrakenChannel, market::KrakenMarket,
3    message::KrakenMessage, subscription::KrakenSubResponse, trade::KrakenTrades,
4};
5use crate::{
6    ExchangeWsStream, NoInitialSnapshots,
7    exchange::{Connector, ExchangeSub, StreamSelector},
8    instrument::InstrumentData,
9    subscriber::{WebSocketSubscriber, validator::WebSocketSubValidator},
10    subscription::{book::OrderBooksL1, trade::PublicTrades},
11    transformer::stateless::StatelessTransformer,
12};
13use derive_more::Display;
14use rustrade_instrument::exchange::ExchangeId;
15use rustrade_integration::{
16    protocol::websocket::{WebSocketSerdeParser, WsMessage},
17    subscription::SubscriptionId,
18};
19use rustrade_macro::{DeExchange, SerExchange};
20use serde_json::json;
21use url::Url;
22
23/// OrderBook types for [`Kraken`].
24pub mod book;
25
26/// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription)
27/// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`].
28pub mod channel;
29
30/// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription)
31/// into an exchange [`Connector`]  specific market used for generating [`Connector::requests`].
32pub mod market;
33
34/// [`KrakenMessage`] type for [`Kraken`].
35pub mod message;
36
37/// [`Subscription`](crate::subscription::Subscription) response type and response
38/// [`Validator`](rustrade_integration) for [`Kraken`].
39pub mod subscription;
40
41/// Public trade types for [`Kraken`].
42pub mod trade;
43
44/// [`Kraken`] WebSocket v2 server base url.
45///
46/// See docs: <https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/trade>
47pub const BASE_URL_KRAKEN: &str = "wss://ws.kraken.com/v2";
48
49/// Convenient type alias for a Kraken [`ExchangeWsStream`] using [`WebSocketSerdeParser`].
50pub type KrakenWsStream<Transformer> = ExchangeWsStream<WebSocketSerdeParser, Transformer>;
51
52/// [`Kraken`] exchange, over its spot WebSocket v2 API.
53///
54/// Symbols are spelled `BASE/QUOTE` in upper case, eg/ `BTC/USD`. Kraken refuses v1 spellings
55/// such as `XBT/USD` per symbol, and the failed subscription is named in the error.
56///
57/// See docs: <https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/trade>
58#[derive(
59    Copy,
60    Clone,
61    Eq,
62    PartialEq,
63    Ord,
64    PartialOrd,
65    Hash,
66    Debug,
67    Default,
68    Display,
69    DeExchange,
70    SerExchange,
71)]
72pub struct Kraken;
73
74impl Connector for Kraken {
75    const ID: ExchangeId = ExchangeId::Kraken;
76    type Channel = KrakenChannel;
77    type Market = KrakenMarket;
78    type Subscriber = WebSocketSubscriber;
79    type SubValidator = WebSocketSubValidator;
80    type SubResponse = KrakenSubResponse;
81
82    fn url() -> Result<Url, url::ParseError> {
83        Url::parse(BASE_URL_KRAKEN)
84    }
85
86    fn requests(exchange_subs: Vec<ExchangeSub<Self::Channel, Self::Market>>) -> Vec<WsMessage> {
87        exchange_subs
88            .into_iter()
89            .map(|ExchangeSub { channel, market }| {
90                let mut params = json!({
91                    "channel": channel.as_ref(),
92                    "symbol": [market.as_ref()],
93                });
94                if channel == KrakenChannel::ORDER_BOOK_L1 {
95                    // Update on every change to the best bid or offer, and open with a snapshot
96                    // of it, rather than updating on trades
97                    params["event_trigger"] = json!("bbo");
98                    params["snapshot"] = json!(true);
99                } else if channel == KrakenChannel::TRADES {
100                    // A trade snapshot replays recent trades, which would read as new ones
101                    params["snapshot"] = json!(false);
102                }
103
104                WsMessage::text(
105                    json!({
106                        "method": "subscribe",
107                        "params": params,
108                    })
109                    .to_string(),
110                )
111            })
112            .collect()
113    }
114
115    fn acknowledged_subscription(response: &Self::SubResponse) -> Option<SubscriptionId> {
116        response.subscription_id()
117    }
118}
119
120impl<Instrument> StreamSelector<Instrument, PublicTrades> for Kraken
121where
122    Instrument: InstrumentData,
123{
124    type SnapFetcher = NoInitialSnapshots;
125    type Stream =
126        KrakenWsStream<StatelessTransformer<Self, Instrument::Key, PublicTrades, KrakenTrades>>;
127}
128
129impl<Instrument> StreamSelector<Instrument, OrderBooksL1> for Kraken
130where
131    Instrument: InstrumentData,
132{
133    type SnapFetcher = NoInitialSnapshots;
134    type Stream = KrakenWsStream<
135        StatelessTransformer<Self, Instrument::Key, OrderBooksL1, KrakenOrderBookL1>,
136    >;
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
141mod tests {
142    use super::*;
143    use crate::exchange::kraken::market::KrakenMarket;
144
145    fn request(channel: KrakenChannel) -> serde_json::Value {
146        let request = Kraken::requests(vec![ExchangeSub {
147            channel,
148            market: KrakenMarket("BTC/USD".into()),
149        }])
150        .remove(0);
151        serde_json::from_str(request.to_text().unwrap()).unwrap()
152    }
153
154    #[test]
155    fn requests_subscribe_in_the_v2_shape() {
156        assert_eq!(
157            request(KrakenChannel::TRADES),
158            json!({
159                "method": "subscribe",
160                "params": {"channel": "trade", "symbol": ["BTC/USD"], "snapshot": false},
161            })
162        );
163        assert_eq!(
164            request(KrakenChannel::ORDER_BOOK_L1),
165            json!({
166                "method": "subscribe",
167                "params": {
168                    "channel": "ticker",
169                    "symbol": ["BTC/USD"],
170                    "event_trigger": "bbo",
171                    "snapshot": true,
172                },
173            })
174        );
175    }
176}