Skip to main content

bybit_rust_api/ws/
messages.rs

1//! WebSocket message types for Bybit V5 WebSocket API.
2//!
3//! Bybit WS messages follow this structure:
4//! ```json
5//! {
6//!   "topic": "orderbook.1.BTCUSDT",
7//!   "type": "snapshot",
8//!   "ts": 1672828800000,
9//!   "data": { ... }
10//! }
11//! ```
12//!
13//! For subscribe/unsubscribe operations:
14//! ```json
15//! { "op": "subscribe", "args": ["orderbook.1.BTCUSDT"] }
16//! ```
17
18use serde::{Deserialize, Serialize};
19
20/// Operation types sent to the server
21#[derive(Debug, Clone, Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum WsOp {
24    Subscribe,
25    Unsubscribe,
26    Auth,
27    Ping,
28}
29
30/// A subscription/unsubscription request to Bybit
31#[derive(Debug, Clone, Serialize)]
32pub struct WsRequest {
33    pub op: WsOp,
34    pub args: Vec<serde_json::Value>,
35}
36
37impl WsRequest {
38    pub fn subscribe(topics: Vec<String>) -> Self {
39        WsRequest {
40            op: WsOp::Subscribe,
41            args: topics.into_iter().map(serde_json::Value::String).collect(),
42        }
43    }
44
45    pub fn unsubscribe(topics: Vec<String>) -> Self {
46        WsRequest {
47            op: WsOp::Unsubscribe,
48            args: topics.into_iter().map(serde_json::Value::String).collect(),
49        }
50    }
51
52    pub fn auth(api_key: &str, expires: u64, signature: &str) -> Self {
53        WsRequest {
54            op: WsOp::Auth,
55            args: vec![
56                serde_json::Value::String(api_key.to_string()),
57                serde_json::Value::Number(serde_json::Number::from(expires)),
58                serde_json::Value::String(signature.to_string()),
59            ],
60        }
61    }
62
63    pub fn ping() -> Self {
64        WsRequest {
65            op: WsOp::Ping,
66            args: vec![],
67        }
68    }
69}
70
71/// Response type from Bybit WS (topic-based data)
72#[derive(Debug, Clone, Deserialize)]
73pub struct WsResponse {
74    /// Topic name (e.g. "orderbook.1.BTCUSDT")
75    #[serde(default)]
76    pub topic: Option<String>,
77    /// Message type: "snapshot" or "delta" for streaming data
78    #[serde(rename = "type")]
79    #[serde(default)]
80    pub msg_type: Option<String>,
81    /// Timestamp in milliseconds
82    #[serde(default)]
83    pub ts: Option<i64>,
84    /// The actual data payload
85    #[serde(default)]
86    pub data: Option<serde_json::Value>,
87}
88
89/// Operation-level response (subscribe success/error)
90#[derive(Debug, Clone, Deserialize)]
91pub struct WsOpResponse {
92    /// "subscribe", "unsubscribe", "auth", "pong"
93    #[serde(default)]
94    pub op: Option<String>,
95    /// true if operation succeeded
96    #[serde(default)]
97    pub success: Option<bool>,
98    /// Return message
99    #[serde(default)]
100    pub ret_msg: Option<String>,
101    /// Connection ID
102    #[serde(default)]
103    pub conn_id: Option<String>,
104    /// Request arguments echoed back
105    #[serde(default)]
106    pub req_id: Option<String>,
107}
108
109/// Combined response enum to handle both topic messages and op responses
110#[derive(Debug, Clone, Deserialize)]
111#[serde(untagged)]
112pub enum WsMessage {
113    /// Topic-based data push
114    Data(WsResponse),
115    /// Operation-level response (subscribe success, pong, auth result)
116    Op(WsOpResponse),
117}
118
119impl WsMessage {
120    /// Returns true if this is a successful subscription confirmation
121    pub fn is_subscribe_success(&self) -> bool {
122        matches!(self, WsMessage::Op(WsOpResponse {
123            op: Some(op),
124            success: Some(true),
125            ..
126        }) if op == "subscribe")
127    }
128
129    /// Returns true if this is a successful auth confirmation
130    pub fn is_auth_success(&self) -> bool {
131        matches!(self, WsMessage::Op(WsOpResponse {
132            op: Some(op),
133            success: Some(true),
134            ..
135        }) if op == "auth")
136    }
137
138    /// Returns true if this is a pong response
139    pub fn is_pong(&self) -> bool {
140        matches!(self, WsMessage::Op(WsOpResponse {
141            op: Some(op),
142            ..
143        }) if op == "pong")
144    }
145
146    /// Extract topic name from data messages
147    pub fn topic(&self) -> Option<&str> {
148        match self {
149            WsMessage::Data(r) => r.topic.as_deref(),
150            _ => None,
151        }
152    }
153
154    /// Extract message type from data messages
155    pub fn msg_type(&self) -> Option<&str> {
156        match self {
157            WsMessage::Data(r) => r.msg_type.as_deref(),
158            _ => None,
159        }
160    }
161}
162
163/// Subscription topic builder
164pub mod topics {
165    /// Build orderbook topic: orderbook.{depth}.{symbol}
166    pub fn orderbook(depth: u16, symbol: &str) -> String {
167        format!("orderbook.{}.{}", depth, symbol)
168    }
169
170    /// Build public trade topic
171    pub fn trade(symbol: &str) -> String {
172        format!("publicTrade.{}", symbol)
173    }
174
175    /// Build ticker topic variants
176    pub mod ticker {
177        pub fn linear(symbol: &str) -> String {
178            format!("tickers.{}", symbol)
179        }
180        pub fn inverse(symbol: &str) -> String {
181            format!("tickers.{}", symbol)
182        }
183        pub fn spot(symbol: &str) -> String {
184            format!("tickers.{}", symbol)
185        }
186        pub fn option(symbol: &str) -> String {
187            format!("tickers.{}", symbol)
188        }
189    }
190
191    /// Build kline topic: kline.{interval}.{symbol}
192    pub fn kline(interval: &str, symbol: &str) -> String {
193        format!("kline.{}.{}", symbol, interval)
194    }
195
196    /// Build liquidation topic
197    pub fn liquidation(symbol: &str) -> String {
198        format!("liquidation.{}", symbol)
199    }
200
201    // --- Private topics ---
202
203    /// Private position topic
204    pub mod position {
205        pub fn all() -> String {
206            "position".to_string()
207        }
208        pub fn linear() -> String {
209            "position.linear".to_string()
210        }
211        pub fn inverse() -> String {
212            "position.inverse".to_string()
213        }
214        pub fn option() -> String {
215            "position.option".to_string()
216        }
217    }
218
219    /// Private execution topic
220    pub mod execution {
221        pub fn all() -> String {
222            "execution".to_string()
223        }
224        pub fn linear() -> String {
225            "execution.linear".to_string()
226        }
227    }
228
229    /// Private order topic
230    pub mod order {
231        pub fn all() -> String {
232            "order".to_string()
233        }
234        pub fn linear() -> String {
235            "order.linear".to_string()
236        }
237    }
238
239    /// Private wallet topic
240    pub mod wallet {
241        pub fn all() -> String {
242            "wallet".to_string()
243        }
244        pub fn linear() -> String {
245            "wallet.linear".to_string()
246        }
247    }
248}