crypto_ws_client/clients/
ftx.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};
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
24/// The WebSocket client for FTX.
25///
26/// FTX has Spot, LinearFuture, LinearSwap, Option, Move and BVOL markets.
27///
28/// * WebSocket API doc: <https://docs.ftx.com/#websocket-api>
29/// * Trading at <https://ftx.com/markets>
30pub 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            // see https://docs.ftx.com/#response-format
64            "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                        // Already subscribed
75                        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        // Send pings at regular intervals (every 15 seconds): {'op': 'ping'}.
90        // You will see an {'type': 'pong'} response.
91        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}