crypto_ws_client/clients/
bitstamp.rs1use 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
23pub 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 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}