crypto_ws_client/clients/
ftx.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};
16
17use log::*;
18use serde_json::Value;
19
20pub(super) const EXCHANGE_NAME: &str = "ftx";
21
22const WEBSOCKET_URL: &str = "wss://ftx.com/ws/";
23
24pub struct FtxWSClient {
31 client: WSClientInternal<FtxMessageHandler>,
32 translator: FtxCommandTranslator,
33}
34
35impl_new_constructor!(
36 FtxWSClient,
37 EXCHANGE_NAME,
38 WEBSOCKET_URL,
39 FtxMessageHandler {},
40 FtxCommandTranslator {}
41);
42
43impl_trait!(Trade, FtxWSClient, subscribe_trade, "trades");
44impl_trait!(BBO, FtxWSClient, subscribe_bbo, "ticker");
45#[rustfmt::skip]
46impl_trait!(OrderBook, FtxWSClient, subscribe_orderbook, "orderbook");
47panic_candlestick!(FtxWSClient);
48panic_l2_topk!(FtxWSClient);
49panic_l3_orderbook!(FtxWSClient);
50panic_ticker!(FtxWSClient);
51
52impl_ws_client_trait!(FtxWSClient);
53
54struct FtxMessageHandler {}
55struct FtxCommandTranslator {}
56
57impl MessageHandler for FtxMessageHandler {
58 fn handle_message(&mut self, msg: &str) -> MiscMessage {
59 let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();
60 let msg_type = obj.get("type").unwrap().as_str().unwrap();
61
62 match msg_type {
63 "pong" => MiscMessage::Pong,
65 "subscribed" | "unsubscribed" | "info" => {
66 info!("Received {} from {}", msg, EXCHANGE_NAME);
67 MiscMessage::Other
68 }
69 "partial" | "update" => MiscMessage::Normal,
70 "error" => {
71 let code = obj.get("code").unwrap().as_i64().unwrap();
72 match code {
73 400 => {
74 warn!("Received {} from {}", msg, EXCHANGE_NAME);
76 }
77 _ => panic!("Received {msg} from {EXCHANGE_NAME}"),
78 }
79 MiscMessage::Other
80 }
81 _ => {
82 warn!("Received {} from {}", msg, EXCHANGE_NAME);
83 MiscMessage::Other
84 }
85 }
86 }
87
88 fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
89 Some((Message::Text(r#"{"op":"ping"}"#.to_string()), 15))
92 }
93}
94
95impl CommandTranslator for FtxCommandTranslator {
96 fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
97 topics
98 .iter()
99 .map(|(channel, symbol)| {
100 format!(
101 r#"{{"op":"{}","channel":"{}","market":"{}"}}"#,
102 if subscribe { "subscribe" } else { "unsubscribe" },
103 channel,
104 symbol
105 )
106 })
107 .collect()
108 }
109
110 fn translate_to_candlestick_commands(
111 &self,
112 _subscribe: bool,
113 _symbol_interval_list: &[(String, usize)],
114 ) -> Vec<String> {
115 panic!("FTX does NOT have candlestick channel");
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use crate::common::command_translator::CommandTranslator;
122
123 #[test]
124 fn test_one_topic() {
125 let translator = super::FtxCommandTranslator {};
126 let commands = translator
127 .translate_to_commands(true, &[("trades".to_string(), "BTC/USD".to_string())]);
128
129 assert_eq!(1, commands.len());
130 assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#, commands[0]);
131 }
132
133 #[test]
134 fn test_two_topic() {
135 let translator = super::FtxCommandTranslator {};
136 let commands = translator.translate_to_commands(
137 true,
138 &[
139 ("trades".to_string(), "BTC/USD".to_string()),
140 ("orderbook".to_string(), "BTC/USD".to_string()),
141 ],
142 );
143
144 assert_eq!(2, commands.len());
145 assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#, commands[0]);
146 assert_eq!(r#"{"op":"subscribe","channel":"orderbook","market":"BTC/USD"}"#, commands[1]);
147 }
148}