Skip to main content

crypto_ws_client/clients/mexc/
mexc_spot.rs

1use async_trait::async_trait;
2use std::collections::HashMap;
3use tokio_tungstenite::tungstenite::Message;
4
5use super::EXCHANGE_NAME;
6use crate::{
7    clients::common_traits::{
8        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,
9    },
10    common::{
11        command_translator::CommandTranslator,
12        message_handler::{MessageHandler, MiscMessage},
13        ws_client_internal::WSClientInternal,
14    },
15    WSClient,
16};
17use log::*;
18use serde_json::Value;
19
20pub(super) const SPOT_WEBSOCKET_URL: &str = "wss://wbs.mexc.com/raw/ws";
21
22/// MEXC Spot market.
23///
24///   * WebSocket API doc: <https://github.com/mxcdevelop/APIDoc/blob/master/websocket/spot/websocket-api.md>
25///   * Trading at: <https://www.mexc.com/exchange/BTC_USDT>
26pub struct MexcSpotWSClient {
27    client: WSClientInternal<MexcMessageHandler>,
28    translator: MexcCommandTranslator,
29}
30
31impl_new_constructor!(
32    MexcSpotWSClient,
33    EXCHANGE_NAME,
34    SPOT_WEBSOCKET_URL,
35    MexcMessageHandler {},
36    MexcCommandTranslator {}
37);
38
39#[rustfmt::skip]
40impl_trait!(Trade, MexcSpotWSClient, subscribe_trade, "deal");
41#[rustfmt::skip]
42impl_trait!(OrderBook, MexcSpotWSClient, subscribe_orderbook, "depth");
43#[rustfmt::skip]
44impl_trait!(OrderBookTopK, MexcSpotWSClient, subscribe_orderbook_topk, "limit.depth");
45impl_candlestick!(MexcSpotWSClient);
46
47panic_bbo!(MexcSpotWSClient);
48panic_ticker!(MexcSpotWSClient);
49panic_l3_orderbook!(MexcSpotWSClient);
50
51impl_ws_client_trait!(MexcSpotWSClient);
52
53struct MexcMessageHandler {}
54struct MexcCommandTranslator {}
55
56impl MessageHandler for MexcMessageHandler {
57    fn handle_message(&mut self, msg: &str) -> MiscMessage {
58        if msg == "pong" {
59            return MiscMessage::Pong;
60        }
61        if let Ok(obj) = serde_json::from_str::<HashMap<String, Value>>(msg) {
62            if obj.contains_key("channel") && obj.contains_key("data") {
63                let channel = obj.get("channel").unwrap().as_str().unwrap();
64                match channel {
65                    "push.deal" | "push.depth" | "push.limit.depth" | "push.kline" => {
66                        if obj.contains_key("symbol") {
67                            MiscMessage::Normal
68                        } else {
69                            warn!("Received {} from {}", msg, EXCHANGE_NAME);
70                            MiscMessage::Other
71                        }
72                    }
73                    "push.overview" => MiscMessage::Normal,
74                    _ => {
75                        warn!("Received {} from {}", msg, EXCHANGE_NAME);
76                        MiscMessage::Other
77                    }
78                }
79            } else {
80                warn!("Received {} from {}", msg, EXCHANGE_NAME);
81                MiscMessage::Other
82            }
83        } else {
84            warn!("Received {} from {}", msg, EXCHANGE_NAME);
85            MiscMessage::Other
86        }
87    }
88
89    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
90        Some((Message::Text("ping".to_string()), 5))
91    }
92}
93
94impl MexcCommandTranslator {
95    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {
96        if channel == "limit.depth" {
97            format!(
98                r#"{{"op":"{}.{}","symbol":"{}","depth": 5}}"#,
99                if subscribe { "sub" } else { "unsub" },
100                channel,
101                symbol
102            )
103        } else {
104            format!(
105                r#"{{"op":"{}.{}","symbol":"{}"}}"#,
106                if subscribe { "sub" } else { "unsub" },
107                channel,
108                symbol
109            )
110        }
111    }
112
113    fn interval_to_string(interval: usize) -> String {
114        let tmp = match interval {
115            60 => "Min1",
116            300 => "Min5",
117            900 => "Min15",
118            1800 => "Min30",
119            3600 => "Min60",
120            14400 => "Hour4",
121            28800 => "Hour8",
122            86400 => "Day1",
123            604800 => "Week1",
124            2592000 => "Month1",
125            _ => panic!(
126                "MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1"
127            ),
128        };
129        tmp.to_string()
130    }
131}
132
133impl CommandTranslator for MexcCommandTranslator {
134    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
135        topics
136            .iter()
137            .map(|(channel, symbol)| {
138                MexcCommandTranslator::topic_to_command(channel, symbol, subscribe)
139            })
140            .collect()
141    }
142
143    fn translate_to_candlestick_commands(
144        &self,
145        subscribe: bool,
146        symbol_interval_list: &[(String, usize)],
147    ) -> Vec<String> {
148        symbol_interval_list
149            .iter()
150            .map(|(symbol, interval)| {
151                format!(
152                    r#"{{"op":"{}.kline","symbol":"{}","interval":"{}"}}"#,
153                    if subscribe { "sub" } else { "unsub" },
154                    symbol,
155                    Self::interval_to_string(*interval)
156                )
157            })
158            .collect()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use crate::common::command_translator::CommandTranslator;
165
166    #[test]
167    fn test_one_topic() {
168        let translator = super::MexcCommandTranslator {};
169        let commands =
170            translator.translate_to_commands(true, &[("deal".to_string(), "BTC_USDT".to_string())]);
171
172        assert_eq!(1, commands.len());
173        assert_eq!(r#"{"op":"sub.deal","symbol":"BTC_USDT"}"#, commands[0]);
174    }
175
176    #[test]
177    fn test_two_topic() {
178        let translator = super::MexcCommandTranslator {};
179        let commands = translator.translate_to_commands(
180            true,
181            &[
182                ("deal".to_string(), "BTC_USDT".to_string()),
183                ("depth".to_string(), "ETH_USDT".to_string()),
184            ],
185        );
186
187        assert_eq!(2, commands.len());
188        assert_eq!(r#"{"op":"sub.deal","symbol":"BTC_USDT"}"#, commands[0]);
189        assert_eq!(r#"{"op":"sub.depth","symbol":"ETH_USDT"}"#, commands[1]);
190    }
191
192    #[test]
193    fn test_candlestick() {
194        let translator = super::MexcCommandTranslator {};
195        let commands =
196            translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]);
197
198        assert_eq!(1, commands.len());
199        assert_eq!(r#"{"op":"sub.kline","symbol":"BTC_USDT","interval":"Min1"}"#, commands[0]);
200    }
201}