crypto_ws_client/clients/
binance_option.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(crate) const EXCHANGE_NAME: &str = "binance";
20
21pub(super) const WEBSOCKET_URL: &str = "wss://stream.opsnest.com/stream";
22
23/// Binance Option market
24///
25///   * WebSocket API doc: <https://binance-docs.github.io/apidocs/voptions/en/>
26///   * Trading at: <https://voptions.binance.com/en>
27pub 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    // see https://binance-docs.github.io/apidocs/voptions/en/#payload-candle
69    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        // https://binance-docs.github.io/apidocs/voptions/en/#push-websocket-account-info
120        // The client will send a ping frame every 2 minutes. If the websocket server
121        // does not receive a ping frame back from the connection within a 2
122        // minute period, the connection will be disconnected. Unsolicited ping
123        // frames are allowed.
124        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}