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 serde::{Deserialize, Serialize};
18use serde_json::Value;
19use ustr::Ustr;
20
21use crate::{
22    common::enums::{
23        BybitCancelType, BybitCreateType, BybitExecType, BybitOrderSide, BybitOrderStatus,
24        BybitOrderType, BybitProductType, BybitStopOrderType, BybitTimeInForce, BybitTpSlMode,
25        BybitTriggerDirection, BybitTriggerType, BybitWsOrderRequestOp,
26    },
27    websocket::enums::BybitWsOperation,
28};
29
30/// Bybit WebSocket subscription message.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct BybitSubscription {
33    pub op: BybitWsOperation,
34    pub args: Vec<String>,
35}
36
37/// Bybit WebSocket authentication message.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BybitAuthRequest {
40    pub op: BybitWsOperation,
41    pub args: Vec<serde_json::Value>,
42}
43
44/// High level message emitted by the Bybit WebSocket client.
45#[derive(Debug, Clone)]
46pub enum BybitWebSocketMessage {
47    /// Generic response (subscribe/auth acknowledgement).
48    Response(BybitWsResponse),
49    /// Authentication acknowledgement.
50    Auth(BybitWsAuthResponse),
51    /// Subscription acknowledgement.
52    Subscription(BybitWsSubscriptionMsg),
53    /// Orderbook snapshot or delta.
54    Orderbook(BybitWsOrderbookDepthMsg),
55    /// Trade updates.
56    Trade(BybitWsTradeMsg),
57    /// Kline updates.
58    Kline(BybitWsKlineMsg),
59    /// Linear/inverse ticker update.
60    TickerLinear(BybitWsTickerLinearMsg),
61    /// Option ticker update.
62    TickerOption(BybitWsTickerOptionMsg),
63    /// Order updates from private channel.
64    AccountOrder(BybitWsAccountOrderMsg),
65    /// Execution/fill updates from private channel.
66    AccountExecution(BybitWsAccountExecutionMsg),
67    /// Wallet/balance updates from private channel.
68    AccountWallet(BybitWsAccountWalletMsg),
69    /// Position updates from private channel.
70    AccountPosition(BybitWsAccountPositionMsg),
71    /// Error received from the venue or client lifecycle.
72    Error(BybitWebSocketError),
73    /// Raw message payload that does not yet have a typed representation.
74    Raw(Value),
75    /// Notification that the underlying connection reconnected.
76    Reconnected,
77    /// Explicit pong event (text-based heartbeat acknowledgement).
78    Pong,
79}
80
81/// Represents an error event surfaced by the WebSocket client.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84#[cfg_attr(feature = "python", pyo3::pyclass)]
85pub struct BybitWebSocketError {
86    /// Error/return code reported by Bybit.
87    pub code: i64,
88    /// Human readable message.
89    pub message: String,
90    /// Optional connection identifier.
91    #[serde(default)]
92    pub conn_id: Option<String>,
93    /// Optional topic associated with the error (when applicable).
94    #[serde(default)]
95    pub topic: Option<String>,
96    /// Optional request identifier related to the failure.
97    #[serde(default)]
98    pub req_id: Option<String>,
99}
100
101impl BybitWebSocketError {
102    /// Creates a new error with the provided code/message.
103    #[must_use]
104    pub fn new(code: i64, message: impl Into<String>) -> Self {
105        Self {
106            code,
107            message: message.into(),
108            conn_id: None,
109            topic: None,
110            req_id: None,
111        }
112    }
113
114    /// Builds an error payload from a generic response frame.
115    #[must_use]
116    pub fn from_response(response: &BybitWsResponse) -> Self {
117        // Build a more informative error message when ret_msg is missing
118        let message = response.ret_msg.clone().unwrap_or_else(|| {
119            let mut parts = vec![];
120
121            if let Some(op) = &response.op {
122                parts.push(format!("op={}", op));
123            }
124            if let Some(topic) = &response.topic {
125                parts.push(format!("topic={}", topic));
126            }
127            if let Some(success) = response.success {
128                parts.push(format!("success={}", success));
129            }
130
131            if parts.is_empty() {
132                "Bybit websocket error (no error message provided)".to_string()
133            } else {
134                format!("Bybit websocket error: {}", parts.join(", "))
135            }
136        });
137
138        Self {
139            code: response.ret_code.unwrap_or_default(),
140            message,
141            conn_id: response.conn_id.clone(),
142            topic: response.topic.map(|t| t.to_string()),
143            req_id: response.req_id.clone(),
144        }
145    }
146
147    /// Convenience constructor for client-side errors (e.g. parsing failures).
148    #[must_use]
149    pub fn from_message(message: impl Into<String>) -> Self {
150        Self::new(-1, message)
151    }
152}
153
154/// Generic WebSocket request for Bybit trading commands.
155#[derive(Debug, Clone, Serialize)]
156pub struct BybitWsRequest<T> {
157    /// Operation type (order.create, order.amend, order.cancel, etc.).
158    pub op: BybitWsOrderRequestOp,
159    /// Request header containing timestamp and other metadata.
160    pub header: BybitWsHeader,
161    /// Arguments payload for the operation.
162    pub args: Vec<T>,
163}
164
165/// Header for WebSocket trade requests.
166#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
168pub struct BybitWsHeader {
169    /// Timestamp in milliseconds.
170    pub x_bapi_timestamp: String,
171}
172
173impl BybitWsHeader {
174    /// Creates a new header with the current timestamp.
175    #[must_use]
176    pub fn now() -> Self {
177        use nautilus_core::time::get_atomic_clock_realtime;
178        Self {
179            x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
180        }
181    }
182}
183
184/// Parameters for placing an order via WebSocket.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct BybitWsPlaceOrderParams {
188    pub category: BybitProductType,
189    pub symbol: Ustr,
190    pub side: BybitOrderSide,
191    pub order_type: BybitOrderType,
192    pub qty: String,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub price: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub time_in_force: Option<BybitTimeInForce>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub order_link_id: Option<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub reduce_only: Option<bool>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub close_on_trigger: Option<bool>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub trigger_price: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub trigger_by: Option<BybitTriggerType>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub trigger_direction: Option<i32>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub tpsl_mode: Option<String>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub take_profit: Option<String>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub stop_loss: Option<String>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub tp_trigger_by: Option<BybitTriggerType>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub sl_trigger_by: Option<BybitTriggerType>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub sl_trigger_price: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub tp_trigger_price: Option<String>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub sl_order_type: Option<BybitOrderType>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub tp_order_type: Option<BybitOrderType>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub sl_limit_price: Option<String>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub tp_limit_price: Option<String>,
231}
232
233/// Parameters for amending an order via WebSocket.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct BybitWsAmendOrderParams {
237    pub category: BybitProductType,
238    pub symbol: Ustr,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub order_id: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub order_link_id: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub qty: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub price: Option<String>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub trigger_price: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub take_profit: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub stop_loss: Option<String>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub tp_trigger_by: Option<BybitTriggerType>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub sl_trigger_by: Option<BybitTriggerType>,
257}
258
259/// Parameters for canceling an order via WebSocket.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct BybitWsCancelOrderParams {
263    pub category: BybitProductType,
264    pub symbol: Ustr,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub order_id: Option<String>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub order_link_id: Option<String>,
269}
270
271/// Subscription acknowledgement returned by Bybit.
272#[derive(Clone, Debug, Serialize, Deserialize)]
273pub struct BybitWsSubscriptionMsg {
274    pub success: bool,
275    pub op: BybitWsOperation,
276    #[serde(default)]
277    pub conn_id: Option<String>,
278    #[serde(default)]
279    pub req_id: Option<String>,
280    #[serde(default)]
281    pub ret_msg: Option<String>,
282}
283
284/// Generic response returned by the endpoint when subscribing or authenticating.
285#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct BybitWsResponse {
287    #[serde(default)]
288    pub op: Option<BybitWsOperation>,
289    #[serde(default)]
290    pub topic: Option<Ustr>,
291    #[serde(default)]
292    pub success: Option<bool>,
293    #[serde(default)]
294    pub conn_id: Option<String>,
295    #[serde(default)]
296    pub req_id: Option<String>,
297    #[serde(default)]
298    pub ret_code: Option<i64>,
299    #[serde(default)]
300    pub ret_msg: Option<String>,
301}
302
303/// Authentication acknowledgement for private channels.
304#[derive(Clone, Debug, Serialize, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct BybitWsAuthResponse {
307    pub op: BybitWsOperation,
308    #[serde(default)]
309    pub conn_id: Option<String>,
310    #[serde(default)]
311    pub ret_code: Option<i64>,
312    #[serde(default)]
313    pub ret_msg: Option<String>,
314    #[serde(default)]
315    pub success: Option<bool>,
316}
317
318/// Representation of a kline/candlestick event on the public stream.
319#[derive(Clone, Debug, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct BybitWsKline {
322    pub start: i64,
323    pub end: i64,
324    pub interval: Ustr,
325    pub open: String,
326    pub close: String,
327    pub high: String,
328    pub low: String,
329    pub volume: String,
330    pub turnover: String,
331    pub confirm: bool,
332    pub timestamp: i64,
333}
334
335/// Envelope for kline updates.
336#[derive(Clone, Debug, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct BybitWsKlineMsg {
339    pub topic: Ustr,
340    pub ts: i64,
341    #[serde(rename = "type")]
342    pub msg_type: Ustr,
343    pub data: Vec<BybitWsKline>,
344}
345
346/// Orderbook depth payload consisting of raw ladder deltas.
347#[derive(Clone, Debug, Serialize, Deserialize)]
348pub struct BybitWsOrderbookDepth {
349    /// Symbol.
350    pub s: Ustr,
351    /// Bid levels represented as `[price, size]` string pairs.
352    pub b: Vec<Vec<String>>,
353    /// Ask levels represented as `[price, size]` string pairs.
354    pub a: Vec<Vec<String>>,
355    /// Update identifier.
356    pub u: i64,
357    /// Cross sequence number.
358    pub seq: i64,
359}
360
361/// Envelope for orderbook depth snapshots and updates.
362#[derive(Clone, Debug, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct BybitWsOrderbookDepthMsg {
365    pub topic: Ustr,
366    #[serde(rename = "type")]
367    pub msg_type: Ustr,
368    pub ts: i64,
369    pub data: BybitWsOrderbookDepth,
370    #[serde(default)]
371    pub cts: Option<i64>,
372}
373
374/// Linear/Inverse ticker event payload.
375#[derive(Clone, Debug, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct BybitWsTickerLinear {
378    pub symbol: Ustr,
379    #[serde(default)]
380    pub tick_direction: Option<String>,
381    #[serde(default)]
382    pub price24h_pcnt: Option<String>,
383    #[serde(default)]
384    pub last_price: Option<String>,
385    #[serde(default)]
386    pub prev_price24h: Option<String>,
387    #[serde(default)]
388    pub high_price24h: Option<String>,
389    #[serde(default)]
390    pub low_price24h: Option<String>,
391    #[serde(default)]
392    pub prev_price1h: Option<String>,
393    #[serde(default)]
394    pub mark_price: Option<String>,
395    #[serde(default)]
396    pub index_price: Option<String>,
397    #[serde(default)]
398    pub open_interest: Option<String>,
399    #[serde(default)]
400    pub open_interest_value: Option<String>,
401    #[serde(default)]
402    pub turnover24h: Option<String>,
403    #[serde(default)]
404    pub volume24h: Option<String>,
405    #[serde(default)]
406    pub next_funding_time: Option<String>,
407    #[serde(default)]
408    pub funding_rate: Option<String>,
409    #[serde(default)]
410    pub bid1_price: Option<String>,
411    #[serde(default)]
412    pub bid1_size: Option<String>,
413    #[serde(default)]
414    pub ask1_price: Option<String>,
415    #[serde(default)]
416    pub ask1_size: Option<String>,
417}
418
419/// Envelope for linear ticker updates.
420#[derive(Clone, Debug, Serialize, Deserialize)]
421#[serde(rename_all = "camelCase")]
422pub struct BybitWsTickerLinearMsg {
423    pub topic: Ustr,
424    #[serde(rename = "type")]
425    pub msg_type: Ustr,
426    pub ts: i64,
427    #[serde(default)]
428    pub cs: Option<i64>,
429    pub data: BybitWsTickerLinear,
430}
431
432/// Option ticker event payload.
433#[derive(Clone, Debug, Serialize, Deserialize)]
434#[serde(rename_all = "camelCase")]
435pub struct BybitWsTickerOption {
436    pub symbol: Ustr,
437    pub bid_price: String,
438    pub bid_size: String,
439    pub bid_iv: String,
440    pub ask_price: String,
441    pub ask_size: String,
442    pub ask_iv: String,
443    pub last_price: String,
444    pub high_price24h: String,
445    pub low_price24h: String,
446    pub mark_price: String,
447    pub index_price: String,
448    pub mark_price_iv: String,
449    pub underlying_price: String,
450    pub open_interest: String,
451    pub turnover24h: String,
452    pub volume24h: String,
453    pub total_volume: String,
454    pub total_turnover: String,
455    pub delta: String,
456    pub gamma: String,
457    pub vega: String,
458    pub theta: String,
459    pub predicted_delivery_price: String,
460    pub change24h: String,
461}
462
463/// Envelope for option ticker updates.
464#[derive(Clone, Debug, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct BybitWsTickerOptionMsg {
467    #[serde(default)]
468    pub id: Option<String>,
469    pub topic: Ustr,
470    #[serde(rename = "type")]
471    pub msg_type: Ustr,
472    pub ts: i64,
473    pub data: BybitWsTickerOption,
474}
475
476/// Trade event payload containing trade executions on public feeds.
477#[derive(Clone, Debug, Serialize, Deserialize)]
478pub struct BybitWsTrade {
479    #[serde(rename = "T")]
480    pub t: i64,
481    #[serde(rename = "s")]
482    pub s: Ustr,
483    #[serde(rename = "S")]
484    pub taker_side: BybitOrderSide,
485    #[serde(rename = "v")]
486    pub v: String,
487    #[serde(rename = "p")]
488    pub p: String,
489    #[serde(rename = "i")]
490    pub i: String,
491    #[serde(rename = "BT")]
492    pub bt: bool,
493    #[serde(rename = "L")]
494    #[serde(default)]
495    pub l: Option<String>,
496    #[serde(rename = "id")]
497    #[serde(default)]
498    pub id: Option<Ustr>,
499    #[serde(rename = "mP")]
500    #[serde(default)]
501    pub m_p: Option<String>,
502    #[serde(rename = "iP")]
503    #[serde(default)]
504    pub i_p: Option<String>,
505    #[serde(rename = "mIv")]
506    #[serde(default)]
507    pub m_iv: Option<String>,
508    #[serde(rename = "iv")]
509    #[serde(default)]
510    pub iv: Option<String>,
511}
512
513/// Envelope for public trade updates.
514#[derive(Clone, Debug, Serialize, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct BybitWsTradeMsg {
517    pub topic: Ustr,
518    #[serde(rename = "type")]
519    pub msg_type: Ustr,
520    pub ts: i64,
521    pub data: Vec<BybitWsTrade>,
522}
523
524/// Private order stream payload.
525#[derive(Clone, Debug, Serialize, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct BybitWsAccountOrder {
528    pub category: BybitProductType,
529    pub symbol: Ustr,
530    pub order_id: Ustr,
531    pub side: BybitOrderSide,
532    pub order_type: BybitOrderType,
533    pub cancel_type: BybitCancelType,
534    pub price: String,
535    pub qty: String,
536    pub order_iv: String,
537    pub time_in_force: BybitTimeInForce,
538    pub order_status: BybitOrderStatus,
539    pub order_link_id: Ustr,
540    pub last_price_on_created: Ustr,
541    pub reduce_only: bool,
542    pub leaves_qty: String,
543    pub leaves_value: String,
544    pub cum_exec_qty: String,
545    pub cum_exec_value: String,
546    pub avg_price: String,
547    pub block_trade_id: Ustr,
548    pub position_idx: i32,
549    pub cum_exec_fee: String,
550    pub created_time: String,
551    pub updated_time: String,
552    pub reject_reason: Ustr,
553    pub trigger_price: String,
554    pub take_profit: String,
555    pub stop_loss: String,
556    pub tp_trigger_by: BybitTriggerType,
557    pub sl_trigger_by: BybitTriggerType,
558    pub tp_limit_price: String,
559    pub sl_limit_price: String,
560    pub close_on_trigger: bool,
561    pub place_type: Ustr,
562    pub smp_type: Ustr,
563    pub smp_group: i32,
564    pub smp_order_id: Ustr,
565    pub fee_currency: Ustr,
566    pub trigger_by: BybitTriggerType,
567    pub stop_order_type: BybitStopOrderType,
568    pub trigger_direction: BybitTriggerDirection,
569    #[serde(default)]
570    pub tpsl_mode: Option<BybitTpSlMode>,
571    #[serde(default)]
572    pub create_type: Option<BybitCreateType>,
573}
574
575/// Envelope for account order updates.
576#[derive(Clone, Debug, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase")]
578pub struct BybitWsAccountOrderMsg {
579    pub topic: String,
580    pub id: String,
581    pub creation_time: i64,
582    pub data: Vec<BybitWsAccountOrder>,
583}
584
585/// Private execution (fill) stream payload.
586#[derive(Clone, Debug, Serialize, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct BybitWsAccountExecution {
589    pub category: BybitProductType,
590    pub symbol: Ustr,
591    pub exec_fee: String,
592    pub exec_id: String,
593    pub exec_price: String,
594    pub exec_qty: String,
595    pub exec_type: BybitExecType,
596    pub exec_value: String,
597    pub is_maker: bool,
598    pub fee_rate: String,
599    pub trade_iv: String,
600    pub mark_iv: String,
601    pub block_trade_id: Ustr,
602    pub mark_price: String,
603    pub index_price: String,
604    pub underlying_price: String,
605    pub leaves_qty: String,
606    pub order_id: Ustr,
607    pub order_link_id: Ustr,
608    pub order_price: String,
609    pub order_qty: String,
610    pub order_type: BybitOrderType,
611    pub side: BybitOrderSide,
612    pub exec_time: String,
613    pub is_leverage: String,
614    pub closed_size: String,
615    pub seq: i64,
616    pub stop_order_type: BybitStopOrderType,
617}
618
619/// Envelope for account execution updates.
620#[derive(Clone, Debug, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct BybitWsAccountExecutionMsg {
623    pub topic: String,
624    pub id: String,
625    pub creation_time: i64,
626    pub data: Vec<BybitWsAccountExecution>,
627}
628
629/// Coin level wallet update payload on private streams.
630#[derive(Clone, Debug, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct BybitWsAccountWalletCoin {
633    pub coin: Ustr,
634    pub wallet_balance: String,
635    pub available_to_withdraw: String,
636    pub available_to_borrow: String,
637    pub accrued_interest: String,
638    #[serde(default, rename = "totalOrderIM")]
639    pub total_order_im: Option<String>,
640    #[serde(default, rename = "totalPositionIM")]
641    pub total_position_im: Option<String>,
642    #[serde(default, rename = "totalPositionMM")]
643    pub total_position_mm: Option<String>,
644    pub equity: String,
645}
646
647/// Wallet summary payload covering all coins.
648#[derive(Clone, Debug, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct BybitWsAccountWallet {
651    pub total_wallet_balance: String,
652    pub total_equity: String,
653    pub total_available_balance: String,
654    pub total_margin_balance: String,
655    pub total_initial_margin: String,
656    pub total_maintenance_margin: String,
657    #[serde(rename = "accountIMRate")]
658    pub account_im_rate: String,
659    #[serde(rename = "accountMMRate")]
660    pub account_mm_rate: String,
661    #[serde(rename = "accountLTV")]
662    pub account_ltv: String,
663    pub coin: Vec<BybitWsAccountWalletCoin>,
664}
665
666/// Envelope for wallet updates on private streams.
667#[derive(Clone, Debug, Serialize, Deserialize)]
668#[serde(rename_all = "camelCase")]
669pub struct BybitWsAccountWalletMsg {
670    pub topic: String,
671    pub id: String,
672    pub creation_time: i64,
673    pub data: Vec<BybitWsAccountWallet>,
674}
675
676/// Position data from private position stream.
677#[derive(Clone, Debug, Serialize, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct BybitWsAccountPosition {
680    pub position_idx: i32,
681    pub risk_id: i64,
682    pub risk_limit_value: String,
683    pub symbol: Ustr,
684    pub side: String,
685    pub size: String,
686    #[serde(default)]
687    pub avg_price: Option<String>,
688    pub position_value: String,
689    pub trade_mode: i32,
690    pub position_status: String,
691    pub auto_add_margin: i32,
692    pub adl_rank_indicator: i32,
693    pub leverage: String,
694    pub position_balance: String,
695    pub mark_price: String,
696    pub liq_price: String,
697    pub bust_price: String,
698    #[serde(rename = "positionMM")]
699    pub position_mm: String,
700    #[serde(rename = "positionIM")]
701    pub position_im: String,
702    pub tpsl_mode: String,
703    pub take_profit: String,
704    pub stop_loss: String,
705    pub trailing_stop: String,
706    pub unrealised_pnl: String,
707    pub cur_realised_pnl: String,
708    pub cum_realised_pnl: String,
709    pub seq: i64,
710    #[serde(default)]
711    pub is_reduce_only: bool,
712    pub created_time: String,
713    pub updated_time: String,
714}
715
716/// Envelope for position updates on private streams.
717#[derive(Clone, Debug, Serialize, Deserialize)]
718#[serde(rename_all = "camelCase")]
719pub struct BybitWsAccountPositionMsg {
720    pub topic: String,
721    pub id: String,
722    pub creation_time: i64,
723    pub data: Vec<BybitWsAccountPosition>,
724}
725
726////////////////////////////////////////////////////////////////////////////////
727// Tests
728////////////////////////////////////////////////////////////////////////////////
729
730#[cfg(test)]
731mod tests {
732    use rstest::rstest;
733
734    use super::*;
735    use crate::common::testing::load_test_json;
736
737    #[rstest]
738    fn deserialize_account_order_frame_uses_enums() {
739        let json = load_test_json("ws_account_order.json");
740        let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
741        let order = &frame.data[0];
742
743        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
744        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
745        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
746        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
747        assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
748        assert_eq!(order.side, BybitOrderSide::Buy);
749    }
750}