crypto_ws_client/clients/mexc/
mexc_spot.rs1use 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
22pub 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}