nautilus_bybit/websocket/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
7//
8//  Unless required by applicable law or agreed to in writing, software
9//  distributed under the License is distributed on an "AS IS" BASIS,
10//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11//  See the License for the specific language governing permissions and
12//  limitations under the License.
13// -------------------------------------------------------------------------------------------------
14
15//! WebSocket message types for Bybit public and private channels.
16
17use nautilus_model::{
18    data::{Data, FundingRateUpdate, OrderBookDeltas},
19    events::{AccountState, OrderCancelRejected, OrderModifyRejected, OrderRejected},
20    reports::{FillReport, OrderStatusReport, PositionStatusReport},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use ustr::Ustr;
26
27use crate::{
28    common::{
29        enums::{
30            BybitCancelType, BybitCreateType, BybitExecType, BybitOrderSide, BybitOrderStatus,
31            BybitOrderType, BybitProductType, BybitStopOrderType, BybitTimeInForce, BybitTpSlMode,
32            BybitTriggerDirection, BybitTriggerType, BybitWsOrderRequestOp,
33        },
34        parse::{
35            deserialize_decimal_or_zero, deserialize_optional_decimal,
36            deserialize_optional_decimal_or_zero,
37        },
38    },
39    websocket::enums::BybitWsOperation,
40};
41
42/// Bybit WebSocket subscription message.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct BybitSubscription {
45    pub op: BybitWsOperation,
46    pub args: Vec<String>,
47}
48
49/// Bybit WebSocket authentication message.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct BybitAuthRequest {
52    pub op: BybitWsOperation,
53    pub args: Vec<serde_json::Value>,
54}
55
56/// High level message emitted by the Bybit WebSocket client.
57#[derive(Debug, Clone)]
58pub enum BybitWsMessage {
59    /// Generic response (subscribe/auth acknowledgement).
60    Response(BybitWsResponse),
61    /// Authentication acknowledgement.
62    Auth(BybitWsAuthResponse),
63    /// Subscription acknowledgement.
64    Subscription(BybitWsSubscriptionMsg),
65    /// Order operation response (create/amend/cancel) from trade WebSocket.
66    OrderResponse(BybitWsOrderResponse),
67    /// Orderbook snapshot or delta.
68    Orderbook(BybitWsOrderbookDepthMsg),
69    /// Trade updates.
70    Trade(BybitWsTradeMsg),
71    /// Kline updates.
72    Kline(BybitWsKlineMsg),
73    /// Linear/inverse ticker update.
74    TickerLinear(BybitWsTickerLinearMsg),
75    /// Option ticker update.
76    TickerOption(BybitWsTickerOptionMsg),
77    /// Order updates from private channel.
78    AccountOrder(BybitWsAccountOrderMsg),
79    /// Execution/fill updates from private channel.
80    AccountExecution(BybitWsAccountExecutionMsg),
81    /// Wallet/balance updates from private channel.
82    AccountWallet(BybitWsAccountWalletMsg),
83    /// Position updates from private channel.
84    AccountPosition(BybitWsAccountPositionMsg),
85    /// Error received from the venue or client lifecycle.
86    Error(BybitWebSocketError),
87    /// Raw message payload that does not yet have a typed representation.
88    Raw(Value),
89    /// Notification that the underlying connection reconnected.
90    Reconnected,
91    /// Explicit pong event (text-based heartbeat acknowledgement).
92    Pong,
93}
94
95/// Nautilus domain message emitted after parsing Bybit WebSocket events.
96///
97/// This enum contains fully-parsed Nautilus domain objects ready for consumption
98/// by the Python layer without additional processing.
99#[derive(Debug, Clone)]
100pub enum NautilusWsMessage {
101    /// Market data (trades, quotes, bars).
102    Data(Vec<Data>),
103    /// Order book deltas.
104    Deltas(OrderBookDeltas),
105    /// Funding rate updates from ticker stream.
106    FundingRates(Vec<FundingRateUpdate>),
107    /// Order status reports from account stream or operation responses.
108    OrderStatusReports(Vec<OrderStatusReport>),
109    /// Fill reports from executions.
110    FillReports(Vec<FillReport>),
111    /// Position status report.
112    PositionStatusReport(PositionStatusReport),
113    /// Account state from wallet updates.
114    AccountState(AccountState),
115    /// Order rejected event (from failed order submission).
116    OrderRejected(OrderRejected),
117    /// Order cancel rejected event (from failed cancel operation).
118    OrderCancelRejected(OrderCancelRejected),
119    /// Order modify rejected event (from failed amend operation).
120    OrderModifyRejected(OrderModifyRejected),
121    /// Error from venue or client.
122    Error(BybitWebSocketError),
123    /// WebSocket reconnected notification.
124    Reconnected,
125    /// Authentication successful notification.
126    Authenticated,
127}
128
129/// Represents an error event surfaced by the WebSocket client.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132#[cfg_attr(feature = "python", pyo3::pyclass)]
133pub struct BybitWebSocketError {
134    /// Error/return code reported by Bybit.
135    pub code: i64,
136    /// Human readable message.
137    pub message: String,
138    /// Optional connection identifier.
139    #[serde(default)]
140    pub conn_id: Option<String>,
141    /// Optional topic associated with the error (when applicable).
142    #[serde(default)]
143    pub topic: Option<String>,
144    /// Optional request identifier related to the failure.
145    #[serde(default)]
146    pub req_id: Option<String>,
147}
148
149impl BybitWebSocketError {
150    /// Creates a new error with the provided code/message.
151    #[must_use]
152    pub fn new(code: i64, message: impl Into<String>) -> Self {
153        Self {
154            code,
155            message: message.into(),
156            conn_id: None,
157            topic: None,
158            req_id: None,
159        }
160    }
161
162    /// Builds an error payload from a generic response frame.
163    #[must_use]
164    pub fn from_response(response: &BybitWsResponse) -> Self {
165        // Build a more informative error message when ret_msg is missing
166        let message = response.ret_msg.clone().unwrap_or_else(|| {
167            let mut parts = vec![];
168
169            if let Some(op) = &response.op {
170                parts.push(format!("op={}", op));
171            }
172            if let Some(topic) = &response.topic {
173                parts.push(format!("topic={}", topic));
174            }
175            if let Some(success) = response.success {
176                parts.push(format!("success={}", success));
177            }
178
179            if parts.is_empty() {
180                "Bybit websocket error (no error message provided)".to_string()
181            } else {
182                format!("Bybit websocket error: {}", parts.join(", "))
183            }
184        });
185
186        Self {
187            code: response.ret_code.unwrap_or_default(),
188            message,
189            conn_id: response.conn_id.clone(),
190            topic: response.topic.map(|t| t.to_string()),
191            req_id: response.req_id.clone(),
192        }
193    }
194
195    /// Convenience constructor for client-side errors (e.g. parsing failures).
196    #[must_use]
197    pub fn from_message(message: impl Into<String>) -> Self {
198        Self::new(-1, message)
199    }
200}
201
202/// Generic WebSocket request for Bybit trading commands.
203#[derive(Debug, Clone, Serialize)]
204#[serde(rename_all = "camelCase")]
205pub struct BybitWsRequest<T> {
206    /// Request ID for correlation (will be echoed back in response).
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub req_id: Option<String>,
209    /// Operation type (order.create, order.amend, order.cancel, etc.).
210    pub op: BybitWsOrderRequestOp,
211    /// Request header containing timestamp and other metadata.
212    pub header: BybitWsHeader,
213    /// Arguments payload for the operation.
214    pub args: Vec<T>,
215}
216
217/// Header for WebSocket trade requests.
218#[derive(Debug, Clone, Serialize)]
219#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
220pub struct BybitWsHeader {
221    /// Timestamp in milliseconds.
222    pub x_bapi_timestamp: String,
223    /// Optional referer ID.
224    #[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
225    pub referer: Option<String>,
226}
227
228impl BybitWsHeader {
229    /// Creates a new header with the current timestamp.
230    #[must_use]
231    pub fn now() -> Self {
232        Self::with_referer(None)
233    }
234
235    /// Creates a new header with the current timestamp and optional referer.
236    #[must_use]
237    pub fn with_referer(referer: Option<String>) -> Self {
238        use nautilus_core::time::get_atomic_clock_realtime;
239        Self {
240            x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
241            referer,
242        }
243    }
244}
245
246/// Parameters for placing an order via WebSocket.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct BybitWsPlaceOrderParams {
250    pub category: BybitProductType,
251    pub symbol: Ustr,
252    pub side: BybitOrderSide,
253    pub order_type: BybitOrderType,
254    pub qty: String,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub is_leverage: Option<i32>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub market_unit: Option<String>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub price: Option<String>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub time_in_force: Option<BybitTimeInForce>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub order_link_id: Option<String>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub reduce_only: Option<bool>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub close_on_trigger: Option<bool>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub trigger_price: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub trigger_by: Option<BybitTriggerType>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub trigger_direction: Option<i32>,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub tpsl_mode: Option<String>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub take_profit: Option<String>,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub stop_loss: Option<String>,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub tp_trigger_by: Option<BybitTriggerType>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub sl_trigger_by: Option<BybitTriggerType>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub sl_trigger_price: Option<String>,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub tp_trigger_price: Option<String>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub sl_order_type: Option<BybitOrderType>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub tp_order_type: Option<BybitOrderType>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub sl_limit_price: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub tp_limit_price: Option<String>,
297}
298
299/// Parameters for amending an order via WebSocket.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct BybitWsAmendOrderParams {
303    pub category: BybitProductType,
304    pub symbol: Ustr,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub order_id: Option<String>,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub order_link_id: Option<String>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub qty: Option<String>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub price: Option<String>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub trigger_price: Option<String>,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub take_profit: Option<String>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub stop_loss: Option<String>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub tp_trigger_by: Option<BybitTriggerType>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub sl_trigger_by: Option<BybitTriggerType>,
323}
324
325/// Parameters for canceling an order via WebSocket.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct BybitWsCancelOrderParams {
329    pub category: BybitProductType,
330    pub symbol: Ustr,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub order_id: Option<String>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub order_link_id: Option<String>,
335}
336
337/// Item in a batch cancel request (without category field).
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct BybitWsBatchCancelItem {
341    pub symbol: Ustr,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub order_id: Option<String>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub order_link_id: Option<String>,
346}
347
348/// Arguments for batch cancel order operation via WebSocket.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct BybitWsBatchCancelOrderArgs {
351    pub category: BybitProductType,
352    pub request: Vec<BybitWsBatchCancelItem>,
353}
354
355/// Item in a batch place request (same as BybitWsPlaceOrderParams but without category).
356#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct BybitWsBatchPlaceItem {
359    pub symbol: Ustr,
360    pub side: BybitOrderSide,
361    pub order_type: BybitOrderType,
362    pub qty: String,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub is_leverage: Option<i32>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub market_unit: Option<String>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub price: Option<String>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub time_in_force: Option<BybitTimeInForce>,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub order_link_id: Option<String>,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub reduce_only: Option<bool>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub close_on_trigger: Option<bool>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub trigger_price: Option<String>,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub trigger_by: Option<BybitTriggerType>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub trigger_direction: Option<i32>,
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub tpsl_mode: Option<String>,
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub take_profit: Option<String>,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub stop_loss: Option<String>,
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub tp_trigger_by: Option<BybitTriggerType>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub sl_trigger_by: Option<BybitTriggerType>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub sl_trigger_price: Option<String>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub tp_trigger_price: Option<String>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub sl_order_type: Option<BybitOrderType>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub tp_order_type: Option<BybitOrderType>,
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub sl_limit_price: Option<String>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub tp_limit_price: Option<String>,
405}
406
407/// Arguments for batch place order operation via WebSocket.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct BybitWsBatchPlaceOrderArgs {
410    pub category: BybitProductType,
411    pub request: Vec<BybitWsBatchPlaceItem>,
412}
413
414/// Subscription acknowledgement returned by Bybit.
415#[derive(Clone, Debug, Serialize, Deserialize)]
416pub struct BybitWsSubscriptionMsg {
417    pub success: bool,
418    pub op: BybitWsOperation,
419    #[serde(default)]
420    pub conn_id: Option<String>,
421    #[serde(default)]
422    pub req_id: Option<String>,
423    #[serde(default)]
424    pub ret_msg: Option<String>,
425}
426
427/// Generic response returned by the endpoint when subscribing or authenticating.
428#[derive(Clone, Debug, Serialize, Deserialize)]
429pub struct BybitWsResponse {
430    #[serde(default)]
431    pub op: Option<BybitWsOperation>,
432    #[serde(default)]
433    pub topic: Option<Ustr>,
434    #[serde(default)]
435    pub success: Option<bool>,
436    #[serde(default)]
437    pub conn_id: Option<String>,
438    #[serde(default)]
439    pub req_id: Option<String>,
440    #[serde(default)]
441    pub ret_code: Option<i64>,
442    #[serde(default)]
443    pub ret_msg: Option<String>,
444}
445
446/// Order operation response from WebSocket trade API.
447#[derive(Clone, Debug, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct BybitWsOrderResponse {
450    /// Operation type (order.create, order.amend, order.cancel).
451    pub op: Ustr,
452    /// Connection ID.
453    #[serde(default)]
454    pub conn_id: Option<String>,
455    /// Return code (0 = success, non-zero = error).
456    pub ret_code: i64,
457    /// Return message.
458    pub ret_msg: String,
459    /// Response data (usually empty for errors, may contain order details for success).
460    #[serde(default)]
461    pub data: Value,
462    /// Request ID for correlation (echoed back if provided in request).
463    #[serde(default)]
464    pub req_id: Option<String>,
465    /// Request header containing timestamp and rate limit info.
466    #[serde(default)]
467    pub header: Option<Value>,
468    /// Extended info for errors.
469    #[serde(default)]
470    pub ret_ext_info: Option<Value>,
471}
472
473impl BybitWsOrderResponse {
474    /// Extracts individual order errors from retExtInfo for batch operations.
475    ///
476    /// For batch operations, even when ret_code is 0, individual orders may fail.
477    /// These failures are reported in retExtInfo.list as an array of {code, msg} objects.
478    #[must_use]
479    pub fn extract_batch_errors(&self) -> Vec<BybitBatchOrderError> {
480        self.ret_ext_info
481            .as_ref()
482            .and_then(|ext| ext.get("list"))
483            .and_then(|list| list.as_array())
484            .map(|arr| {
485                arr.iter()
486                    .filter_map(|item| {
487                        let code = item.get("code")?.as_i64()?;
488                        let msg = item.get("msg")?.as_str()?.to_string();
489                        Some(BybitBatchOrderError { code, msg })
490                    })
491                    .collect()
492            })
493            .unwrap_or_default()
494    }
495}
496
497/// Error information for individual orders in a batch operation.
498#[derive(Clone, Debug)]
499pub struct BybitBatchOrderError {
500    /// Error code (0 = success, non-zero = error).
501    pub code: i64,
502    /// Error message.
503    pub msg: String,
504}
505
506/// Authentication acknowledgement for private channels.
507#[derive(Clone, Debug, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct BybitWsAuthResponse {
510    pub op: BybitWsOperation,
511    #[serde(default)]
512    pub conn_id: Option<String>,
513    #[serde(default)]
514    pub ret_code: Option<i64>,
515    #[serde(default)]
516    pub ret_msg: Option<String>,
517    #[serde(default)]
518    pub success: Option<bool>,
519}
520
521/// Representation of a kline/candlestick event on the public stream.
522#[derive(Clone, Debug, Serialize, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct BybitWsKline {
525    pub start: i64,
526    pub end: i64,
527    pub interval: Ustr,
528    pub open: String,
529    pub close: String,
530    pub high: String,
531    pub low: String,
532    pub volume: String,
533    pub turnover: String,
534    pub confirm: bool,
535    pub timestamp: i64,
536}
537
538/// Envelope for kline updates.
539#[derive(Clone, Debug, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct BybitWsKlineMsg {
542    pub topic: Ustr,
543    pub ts: i64,
544    #[serde(rename = "type")]
545    pub msg_type: Ustr,
546    pub data: Vec<BybitWsKline>,
547}
548
549/// Orderbook depth payload consisting of raw ladder deltas.
550#[derive(Clone, Debug, Serialize, Deserialize)]
551pub struct BybitWsOrderbookDepth {
552    /// Symbol.
553    pub s: Ustr,
554    /// Bid levels represented as `[price, size]` string pairs.
555    pub b: Vec<Vec<String>>,
556    /// Ask levels represented as `[price, size]` string pairs.
557    pub a: Vec<Vec<String>>,
558    /// Update identifier.
559    pub u: i64,
560    /// Cross sequence number.
561    pub seq: i64,
562}
563
564/// Envelope for orderbook depth snapshots and updates.
565#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct BybitWsOrderbookDepthMsg {
568    pub topic: Ustr,
569    #[serde(rename = "type")]
570    pub msg_type: Ustr,
571    pub ts: i64,
572    pub data: BybitWsOrderbookDepth,
573    #[serde(default)]
574    pub cts: Option<i64>,
575}
576
577/// Linear/Inverse ticker event payload.
578#[derive(Clone, Debug, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct BybitWsTickerLinear {
581    pub symbol: Ustr,
582    #[serde(default)]
583    pub tick_direction: Option<String>,
584    #[serde(default)]
585    pub price24h_pcnt: Option<String>,
586    #[serde(default)]
587    pub last_price: Option<String>,
588    #[serde(default)]
589    pub prev_price24h: Option<String>,
590    #[serde(default)]
591    pub high_price24h: Option<String>,
592    #[serde(default)]
593    pub low_price24h: Option<String>,
594    #[serde(default)]
595    pub prev_price1h: Option<String>,
596    #[serde(default)]
597    pub mark_price: Option<String>,
598    #[serde(default)]
599    pub index_price: Option<String>,
600    #[serde(default)]
601    pub open_interest: Option<String>,
602    #[serde(default)]
603    pub open_interest_value: Option<String>,
604    #[serde(default)]
605    pub turnover24h: Option<String>,
606    #[serde(default)]
607    pub volume24h: Option<String>,
608    #[serde(default)]
609    pub next_funding_time: Option<String>,
610    #[serde(default)]
611    pub funding_rate: Option<String>,
612    #[serde(default)]
613    pub bid1_price: Option<String>,
614    #[serde(default)]
615    pub bid1_size: Option<String>,
616    #[serde(default)]
617    pub ask1_price: Option<String>,
618    #[serde(default)]
619    pub ask1_size: Option<String>,
620}
621
622/// Envelope for linear ticker updates.
623#[derive(Clone, Debug, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625pub struct BybitWsTickerLinearMsg {
626    pub topic: Ustr,
627    #[serde(rename = "type")]
628    pub msg_type: Ustr,
629    pub ts: i64,
630    #[serde(default)]
631    pub cs: Option<i64>,
632    pub data: BybitWsTickerLinear,
633}
634
635/// Option ticker event payload.
636#[derive(Clone, Debug, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct BybitWsTickerOption {
639    pub symbol: Ustr,
640    pub bid_price: String,
641    pub bid_size: String,
642    pub bid_iv: String,
643    pub ask_price: String,
644    pub ask_size: String,
645    pub ask_iv: String,
646    pub last_price: String,
647    pub high_price24h: String,
648    pub low_price24h: String,
649    pub mark_price: String,
650    pub index_price: String,
651    pub mark_price_iv: String,
652    pub underlying_price: String,
653    pub open_interest: String,
654    pub turnover24h: String,
655    pub volume24h: String,
656    pub total_volume: String,
657    pub total_turnover: String,
658    pub delta: String,
659    pub gamma: String,
660    pub vega: String,
661    pub theta: String,
662    pub predicted_delivery_price: String,
663    pub change24h: String,
664}
665
666/// Envelope for option ticker updates.
667#[derive(Clone, Debug, Serialize, Deserialize)]
668#[serde(rename_all = "camelCase")]
669pub struct BybitWsTickerOptionMsg {
670    #[serde(default)]
671    pub id: Option<String>,
672    pub topic: Ustr,
673    #[serde(rename = "type")]
674    pub msg_type: Ustr,
675    pub ts: i64,
676    pub data: BybitWsTickerOption,
677}
678
679/// Trade event payload containing trade executions on public feeds.
680#[derive(Clone, Debug, Serialize, Deserialize)]
681pub struct BybitWsTrade {
682    #[serde(rename = "T")]
683    pub t: i64,
684    #[serde(rename = "s")]
685    pub s: Ustr,
686    #[serde(rename = "S")]
687    pub taker_side: BybitOrderSide,
688    #[serde(rename = "v")]
689    pub v: String,
690    #[serde(rename = "p")]
691    pub p: String,
692    #[serde(rename = "i")]
693    pub i: String,
694    #[serde(rename = "BT")]
695    pub bt: bool,
696    #[serde(rename = "L")]
697    #[serde(default)]
698    pub l: Option<String>,
699    #[serde(rename = "id")]
700    #[serde(default)]
701    pub id: Option<Ustr>,
702    #[serde(rename = "mP")]
703    #[serde(default)]
704    pub m_p: Option<String>,
705    #[serde(rename = "iP")]
706    #[serde(default)]
707    pub i_p: Option<String>,
708    #[serde(rename = "mIv")]
709    #[serde(default)]
710    pub m_iv: Option<String>,
711    #[serde(rename = "iv")]
712    #[serde(default)]
713    pub iv: Option<String>,
714}
715
716/// Envelope for public trade updates.
717#[derive(Clone, Debug, Serialize, Deserialize)]
718#[serde(rename_all = "camelCase")]
719pub struct BybitWsTradeMsg {
720    pub topic: Ustr,
721    #[serde(rename = "type")]
722    pub msg_type: Ustr,
723    pub ts: i64,
724    pub data: Vec<BybitWsTrade>,
725}
726
727/// Private order stream payload.
728#[derive(Clone, Debug, Serialize, Deserialize)]
729#[serde(rename_all = "camelCase")]
730pub struct BybitWsAccountOrder {
731    pub category: BybitProductType,
732    pub symbol: Ustr,
733    pub order_id: Ustr,
734    pub side: BybitOrderSide,
735    pub order_type: BybitOrderType,
736    pub cancel_type: BybitCancelType,
737    pub price: String,
738    pub qty: String,
739    pub order_iv: String,
740    pub time_in_force: BybitTimeInForce,
741    pub order_status: BybitOrderStatus,
742    pub order_link_id: Ustr,
743    pub last_price_on_created: Ustr,
744    pub reduce_only: bool,
745    pub leaves_qty: String,
746    pub leaves_value: String,
747    pub cum_exec_qty: String,
748    pub cum_exec_value: String,
749    pub avg_price: String,
750    pub block_trade_id: Ustr,
751    pub position_idx: i32,
752    pub cum_exec_fee: String,
753    pub created_time: String,
754    pub updated_time: String,
755    pub reject_reason: Ustr,
756    pub trigger_price: String,
757    pub take_profit: String,
758    pub stop_loss: String,
759    pub tp_trigger_by: BybitTriggerType,
760    pub sl_trigger_by: BybitTriggerType,
761    pub tp_limit_price: String,
762    pub sl_limit_price: String,
763    pub close_on_trigger: bool,
764    pub place_type: Ustr,
765    pub smp_type: Ustr,
766    pub smp_group: i32,
767    pub smp_order_id: Ustr,
768    pub fee_currency: Ustr,
769    pub trigger_by: BybitTriggerType,
770    pub stop_order_type: BybitStopOrderType,
771    pub trigger_direction: BybitTriggerDirection,
772    #[serde(default)]
773    pub tpsl_mode: Option<BybitTpSlMode>,
774    #[serde(default)]
775    pub create_type: Option<BybitCreateType>,
776}
777
778/// Envelope for account order updates.
779#[derive(Clone, Debug, Serialize, Deserialize)]
780#[serde(rename_all = "camelCase")]
781pub struct BybitWsAccountOrderMsg {
782    pub topic: Ustr,
783    pub id: String,
784    pub creation_time: i64,
785    pub data: Vec<BybitWsAccountOrder>,
786}
787
788/// Private execution (fill) stream payload.
789#[derive(Clone, Debug, Serialize, Deserialize)]
790#[serde(rename_all = "camelCase")]
791pub struct BybitWsAccountExecution {
792    pub category: BybitProductType,
793    pub symbol: Ustr,
794    pub exec_fee: String,
795    pub exec_id: String,
796    pub exec_price: String,
797    pub exec_qty: String,
798    pub exec_type: BybitExecType,
799    pub exec_value: String,
800    pub is_maker: bool,
801    pub fee_rate: String,
802    pub trade_iv: String,
803    pub mark_iv: String,
804    pub block_trade_id: Ustr,
805    pub mark_price: String,
806    pub index_price: String,
807    pub underlying_price: String,
808    pub leaves_qty: String,
809    pub order_id: Ustr,
810    pub order_link_id: Ustr,
811    pub order_price: String,
812    pub order_qty: String,
813    pub order_type: BybitOrderType,
814    pub side: BybitOrderSide,
815    pub exec_time: String,
816    pub is_leverage: String,
817    pub closed_size: String,
818    pub seq: i64,
819    pub stop_order_type: BybitStopOrderType,
820}
821
822/// Envelope for account execution updates.
823#[derive(Clone, Debug, Serialize, Deserialize)]
824#[serde(rename_all = "camelCase")]
825pub struct BybitWsAccountExecutionMsg {
826    pub topic: Ustr,
827    pub id: String,
828    pub creation_time: i64,
829    pub data: Vec<BybitWsAccountExecution>,
830}
831
832/// Coin level wallet update payload on private streams.
833#[derive(Clone, Debug, Serialize, Deserialize)]
834#[serde(rename_all = "camelCase")]
835pub struct BybitWsAccountWalletCoin {
836    pub coin: Ustr,
837    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
838    pub wallet_balance: Decimal,
839    pub available_to_withdraw: String,
840    pub available_to_borrow: String,
841    pub accrued_interest: String,
842    #[serde(
843        default,
844        rename = "totalOrderIM",
845        deserialize_with = "deserialize_optional_decimal_or_zero"
846    )]
847    pub total_order_im: Decimal,
848    #[serde(
849        default,
850        rename = "totalPositionIM",
851        deserialize_with = "deserialize_optional_decimal_or_zero"
852    )]
853    pub total_position_im: Decimal,
854    #[serde(default, rename = "totalPositionMM")]
855    pub total_position_mm: Option<String>,
856    pub equity: String,
857    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
858    pub spot_borrow: Decimal,
859}
860
861/// Wallet summary payload covering all coins.
862#[derive(Clone, Debug, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase")]
864pub struct BybitWsAccountWallet {
865    pub total_wallet_balance: String,
866    pub total_equity: String,
867    pub total_available_balance: String,
868    pub total_margin_balance: String,
869    pub total_initial_margin: String,
870    pub total_maintenance_margin: String,
871    #[serde(rename = "accountIMRate")]
872    pub account_im_rate: String,
873    #[serde(rename = "accountMMRate")]
874    pub account_mm_rate: String,
875    #[serde(rename = "accountLTV")]
876    pub account_ltv: String,
877    pub coin: Vec<BybitWsAccountWalletCoin>,
878}
879
880/// Envelope for wallet updates on private streams.
881#[derive(Clone, Debug, Serialize, Deserialize)]
882#[serde(rename_all = "camelCase")]
883pub struct BybitWsAccountWalletMsg {
884    pub topic: Ustr,
885    pub id: String,
886    pub creation_time: i64,
887    pub data: Vec<BybitWsAccountWallet>,
888}
889
890/// Position data from private position stream.
891#[derive(Clone, Debug, Serialize, Deserialize)]
892#[serde(rename_all = "camelCase")]
893pub struct BybitWsAccountPosition {
894    pub category: BybitProductType,
895    pub symbol: Ustr,
896    pub side: Ustr,
897    pub size: String,
898    pub position_idx: i32,
899    pub trade_mode: i32,
900    pub position_value: String,
901    pub risk_id: i64,
902    pub risk_limit_value: String,
903    #[serde(deserialize_with = "deserialize_optional_decimal")]
904    pub entry_price: Option<Decimal>,
905    pub mark_price: String,
906    pub leverage: String,
907    pub position_balance: String,
908    pub auto_add_margin: i32,
909    #[serde(rename = "positionIM")]
910    pub position_im: String,
911    #[serde(rename = "positionIMByMp")]
912    pub position_im_by_mp: String,
913    #[serde(rename = "positionMM")]
914    pub position_mm: String,
915    #[serde(rename = "positionMMByMp")]
916    pub position_mm_by_mp: String,
917    pub liq_price: String,
918    pub bust_price: String,
919    pub tpsl_mode: Ustr,
920    pub take_profit: String,
921    pub stop_loss: String,
922    pub trailing_stop: String,
923    pub unrealised_pnl: String,
924    pub session_avg_price: String,
925    pub cur_realised_pnl: String,
926    pub cum_realised_pnl: String,
927    pub position_status: Ustr,
928    pub adl_rank_indicator: i32,
929    pub created_time: String,
930    pub updated_time: String,
931    pub seq: i64,
932    pub is_reduce_only: bool,
933    pub mmr_sys_updated_time: String,
934    pub leverage_sys_updated_time: String,
935}
936
937/// Envelope for position updates on private streams.
938#[derive(Clone, Debug, Serialize, Deserialize)]
939#[serde(rename_all = "camelCase")]
940pub struct BybitWsAccountPositionMsg {
941    pub topic: Ustr,
942    pub id: String,
943    pub creation_time: i64,
944    pub data: Vec<BybitWsAccountPosition>,
945}
946
947////////////////////////////////////////////////////////////////////////////////
948// Tests
949////////////////////////////////////////////////////////////////////////////////
950
951#[cfg(test)]
952mod tests {
953    use rstest::rstest;
954
955    use super::*;
956    use crate::common::testing::load_test_json;
957
958    #[rstest]
959    fn deserialize_account_order_frame_uses_enums() {
960        let json = load_test_json("ws_account_order.json");
961        let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
962        let order = &frame.data[0];
963
964        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
965        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
966        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
967        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
968        assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
969        assert_eq!(order.side, BybitOrderSide::Buy);
970    }
971}