Skip to main content

nautilus_bybit/websocket/
messages.rs

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