crypto_ws_client/clients/
binance_option.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(crate) const EXCHANGE_NAME: &str = "binance";
20
21pub(super) const WEBSOCKET_URL: &str = "wss://stream.opsnest.com/stream";
22
23pub struct BinanceOptionWSClient {
28 client: WSClientInternal<BinanceOptionMessageHandler>,
29 translator: BinanceOptionCommandTranslator,
30}
31
32impl_new_constructor!(
33 BinanceOptionWSClient,
34 EXCHANGE_NAME,
35 WEBSOCKET_URL,
36 BinanceOptionMessageHandler {},
37 BinanceOptionCommandTranslator {}
38);
39
40impl_trait!(Trade, BinanceOptionWSClient, subscribe_trade, "trade");
41impl_trait!(Ticker, BinanceOptionWSClient, subscribe_ticker, "ticker");
42impl_trait!(BBO, BinanceOptionWSClient, subscribe_bbo, "bookTicker");
43#[rustfmt::skip]
44impl_trait!(OrderBook, BinanceOptionWSClient, subscribe_orderbook, "depth@100ms");
45#[rustfmt::skip]
46impl_trait!(OrderBookTopK, BinanceOptionWSClient, subscribe_orderbook_topk, "depth10");
47impl_candlestick!(BinanceOptionWSClient);
48panic_l3_orderbook!(BinanceOptionWSClient);
49
50impl_ws_client_trait!(BinanceOptionWSClient);
51
52struct BinanceOptionMessageHandler {}
53struct BinanceOptionCommandTranslator {}
54
55impl BinanceOptionCommandTranslator {
56 fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {
57 let raw_topics = topics
58 .iter()
59 .map(|(topic, symbol)| format!("{symbol}@{topic}"))
60 .collect::<Vec<String>>();
61 format!(
62 r#"{{"id":9527,"method":"{}","params":{}}}"#,
63 if subscribe { "SUBSCRIBE" } else { "UNSUBSCRIBE" },
64 serde_json::to_string(&raw_topics).unwrap()
65 )
66 }
67
68 fn to_candlestick_raw_channel(interval: usize) -> String {
70 let interval_str = match interval {
71 60 => "1m",
72 300 => "5m",
73 900 => "15m",
74 1800 => "30m",
75 3600 => "1h",
76 14400 => "4h",
77 86400 => "1d",
78 604800 => "1w",
79 _ => panic!("Binance Option has intervals 1m,5m,15m,30m,1h4h,1d,1w"),
80 };
81 format!("kline_{interval_str}")
82 }
83}
84
85impl MessageHandler for BinanceOptionMessageHandler {
86 fn handle_message(&mut self, msg: &str) -> MiscMessage {
87 if msg == r#"{"id":9527}"# {
88 return MiscMessage::Other;
89 } else if msg == r#"{"event":"pong"}"# {
90 return MiscMessage::Pong;
91 }
92
93 let resp = serde_json::from_str::<HashMap<String, Value>>(msg);
94 if resp.is_err() {
95 error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME);
96 return MiscMessage::Other;
97 }
98 let obj = resp.unwrap();
99
100 if obj.contains_key("code") {
101 panic!("Received {msg} from {EXCHANGE_NAME}");
102 }
103
104 if let Some(result) = obj.get("result") {
105 if serde_json::Value::Null == *result {
106 return MiscMessage::Other;
107 }
108 }
109
110 if !obj.contains_key("stream") || !obj.contains_key("data") {
111 warn!("Received {} from {}", msg, EXCHANGE_NAME);
112 return MiscMessage::Other;
113 }
114
115 MiscMessage::Normal
116 }
117
118 fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
119 Some((Message::Text(r#"{"event":"ping"}"#.to_string()), 120))
125 }
126}
127
128impl CommandTranslator for BinanceOptionCommandTranslator {
129 fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
130 let command = Self::topics_to_command(topics, subscribe);
131 vec![command]
132 }
133
134 fn translate_to_candlestick_commands(
135 &self,
136 subscribe: bool,
137 symbol_interval_list: &[(String, usize)],
138 ) -> Vec<String> {
139 let topics = symbol_interval_list
140 .iter()
141 .map(|(symbol, interval)| {
142 let channel = Self::to_candlestick_raw_channel(*interval);
143 (channel, symbol.to_string())
144 })
145 .collect::<Vec<(String, String)>>();
146 self.translate_to_commands(subscribe, &topics)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use crate::common::command_translator::CommandTranslator;
153
154 #[test]
155 fn test_one_topic() {
156 let translator = super::BinanceOptionCommandTranslator {};
157 let commands = translator.translate_to_commands(
158 true,
159 &[("trade".to_string(), "BTC-220429-50000-C".to_string())],
160 );
161
162 assert_eq!(1, commands.len());
163 assert_eq!(
164 r#"{"id":9527,"method":"SUBSCRIBE","params":["BTC-220429-50000-C@trade"]}"#,
165 commands[0]
166 );
167 }
168
169 #[test]
170 fn test_two_topics() {
171 let translator = super::BinanceOptionCommandTranslator {};
172 let commands = translator.translate_to_commands(
173 true,
174 &[
175 ("trade".to_string(), "BTC-220429-50000-C".to_string()),
176 ("ticker".to_string(), "BTC-220429-50000-C".to_string()),
177 ],
178 );
179
180 assert_eq!(1, commands.len());
181 assert_eq!(
182 r#"{"id":9527,"method":"SUBSCRIBE","params":["BTC-220429-50000-C@trade","BTC-220429-50000-C@ticker"]}"#,
183 commands[0]
184 );
185 }
186}