crypto_ws_client/clients/
bitstamp.rs

1use async_trait::async_trait;
2use std::collections::HashMap;
3use tokio_tungstenite::tungstenite::Message;
4
5use crate::{
6    clients::common_traits::{
7        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,
8    },
9    common::{
10        command_translator::CommandTranslator,
11        message_handler::{MessageHandler, MiscMessage},
12        ws_client_internal::WSClientInternal,
13    },
14    WSClient,
15};
16use log::*;
17use serde_json::Value;
18
19pub(super) const EXCHANGE_NAME: &str = "bitstamp";
20
21const WEBSOCKET_URL: &str = "wss://ws.bitstamp.net";
22
23/// The WebSocket client for Bitstamp Spot market.
24///
25/// Bitstamp has only Spot market.
26///
27///   * WebSocket API doc: <https://www.bitstamp.net/websocket/v2/>
28///   * Trading at: <https://www.bitstamp.net/market/tradeview/>
29pub struct BitstampWSClient {
30    client: WSClientInternal<BitstampMessageHandler>,
31    translator: BitstampCommandTranslator,
32}
33
34impl_new_constructor!(
35    BitstampWSClient,
36    EXCHANGE_NAME,
37    WEBSOCKET_URL,
38    BitstampMessageHandler {},
39    BitstampCommandTranslator {}
40);
41
42impl_trait!(Trade, BitstampWSClient, subscribe_trade, "live_trades");
43#[rustfmt::skip]
44impl_trait!(OrderBook, BitstampWSClient, subscribe_orderbook, "diff_order_book");
45#[rustfmt::skip]
46impl_trait!(OrderBookTopK, BitstampWSClient, subscribe_orderbook_topk, "order_book");
47#[rustfmt::skip]
48impl_trait!(Level3OrderBook, BitstampWSClient, subscribe_l3_orderbook, "live_orders");
49
50panic_bbo!(BitstampWSClient);
51impl_candlestick!(BitstampWSClient);
52panic_ticker!(BitstampWSClient);
53
54impl_ws_client_trait!(BitstampWSClient);
55
56struct BitstampMessageHandler {}
57struct BitstampCommandTranslator {}
58
59impl MessageHandler for BitstampMessageHandler {
60    fn handle_message(&mut self, msg: &str) -> MiscMessage {
61        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);
62        if resp.is_err() {
63            error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME);
64            return MiscMessage::Other;
65        }
66        let obj = resp.unwrap();
67
68        let event = obj.get("event").unwrap().as_str().unwrap();
69        match event {
70            "bts:subscription_succeeded" | "bts:unsubscription_succeeded" | "bts:heartbeat" => {
71                debug!("Received {} from {}", msg, EXCHANGE_NAME);
72                MiscMessage::Other
73            }
74            "bts:error" => {
75                error!("Received {} from {}", msg, EXCHANGE_NAME);
76                panic!("Received {msg} from {EXCHANGE_NAME}");
77            }
78            "bts:request_reconnect" => {
79                warn!("Received {}, which means Bitstamp is under maintenance", msg);
80                std::thread::sleep(std::time::Duration::from_secs(20));
81                MiscMessage::Reconnect
82            }
83            _ => MiscMessage::Normal,
84        }
85    }
86
87    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
88        // See "Heartbeat" at https://www.bitstamp.net/websocket/v2/
89        Some((Message::Text(r#"{"event": "bts:heartbeat"}"#.to_string()), 10))
90    }
91}
92
93impl CommandTranslator for BitstampCommandTranslator {
94    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
95        topics
96            .iter()
97            .map(|(channel, symbol)| {
98                format!(
99                    r#"{{"event":"bts:{}","data":{{"channel":"{}_{}"}}}}"#,
100                    if subscribe { "subscribe" } else { "unsubscribe" },
101                    channel,
102                    symbol,
103                )
104            })
105            .collect()
106    }
107
108    fn translate_to_candlestick_commands(
109        &self,
110        _subscribe: bool,
111        _symbol_interval_list: &[(String, usize)],
112    ) -> Vec<String> {
113        panic!("Bitstamp does NOT have candlestick channel");
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::common::command_translator::CommandTranslator;
120
121    #[test]
122    fn test_one_topic() {
123        let translator = super::BitstampCommandTranslator {};
124        let commands = translator
125            .translate_to_commands(true, &[("live_trades".to_string(), "btcusd".to_string())]);
126
127        assert_eq!(1, commands.len());
128        assert_eq!(
129            r#"{"event":"bts:subscribe","data":{"channel":"live_trades_btcusd"}}"#,
130            commands[0]
131        );
132    }
133
134    #[test]
135    fn test_two_topics() {
136        let translator = super::BitstampCommandTranslator {};
137        let commands = translator.translate_to_commands(
138            true,
139            &[
140                ("live_trades".to_string(), "btcusd".to_string()),
141                ("diff_order_book".to_string(), "btcusd".to_string()),
142            ],
143        );
144
145        assert_eq!(2, commands.len());
146        assert_eq!(
147            r#"{"event":"bts:subscribe","data":{"channel":"live_trades_btcusd"}}"#,
148            commands[0]
149        );
150        assert_eq!(
151            r#"{"event":"bts:subscribe","data":{"channel":"diff_order_book_btcusd"}}"#,
152            commands[1]
153        );
154    }
155}