Skip to main content

bybit_client/ws/
types.rs

1//! WebSocket message types.
2
3use serde::{Deserialize, Serialize};
4
5use crate::types::Category;
6
7
8/// WebSocket operation message (subscribe, unsubscribe, ping, auth).
9#[derive(Debug, Clone, Serialize)]
10pub struct WsOperation {
11    /// Operation type.
12    pub op: String,
13    /// Operation arguments.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub args: Option<Vec<String>>,
16    /// Request ID (for tracking responses).
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub req_id: Option<String>,
19}
20
21impl WsOperation {
22    /// Create a ping message.
23    pub fn ping() -> Self {
24        Self {
25            op: "ping".to_string(),
26            args: None,
27            req_id: None,
28        }
29    }
30
31    /// Create a subscribe message.
32    pub fn subscribe(topics: Vec<String>) -> Self {
33        Self {
34            op: "subscribe".to_string(),
35            args: Some(topics),
36            req_id: None,
37        }
38    }
39
40    /// Create an unsubscribe message.
41    pub fn unsubscribe(topics: Vec<String>) -> Self {
42        Self {
43            op: "unsubscribe".to_string(),
44            args: Some(topics),
45            req_id: None,
46        }
47    }
48
49    /// Create an auth message for private connections.
50    pub fn auth(api_key: &str, expires: u64, signature: &str) -> Self {
51        Self {
52            op: "auth".to_string(),
53            args: Some(vec![
54                api_key.to_string(),
55                expires.to_string(),
56                signature.to_string(),
57            ]),
58            req_id: None,
59        }
60    }
61
62    /// Set request ID.
63    pub fn with_req_id(mut self, req_id: impl Into<String>) -> Self {
64        self.req_id = Some(req_id.into());
65        self
66    }
67}
68
69/// WebSocket response to an operation (subscribe, auth, etc.).
70#[derive(Debug, Clone, Deserialize)]
71pub struct WsOperationResponse {
72    /// Whether the operation was successful.
73    pub success: bool,
74    /// Response message.
75    #[serde(default)]
76    pub ret_msg: Option<String>,
77    /// Connection ID.
78    #[serde(default)]
79    pub conn_id: Option<String>,
80    /// Request ID (if provided in request).
81    #[serde(default)]
82    pub req_id: Option<String>,
83    /// Operation that was performed.
84    #[serde(default)]
85    pub op: Option<String>,
86}
87
88/// Pong response from server.
89#[derive(Debug, Clone, Deserialize)]
90pub struct WsPong {
91    /// Whether successful.
92    pub success: bool,
93    /// Response message.
94    #[serde(default)]
95    pub ret_msg: Option<String>,
96    /// Connection ID.
97    #[serde(default)]
98    pub conn_id: Option<String>,
99    /// Request ID.
100    #[serde(default)]
101    pub req_id: Option<String>,
102    /// Operation type ("pong").
103    pub op: String,
104}
105
106
107/// Generic WebSocket message wrapper for stream data.
108#[derive(Debug, Clone, Deserialize)]
109pub struct WsStreamMessage<T> {
110    /// Topic name (e.g., "orderbook.50.BTCUSDT").
111    pub topic: String,
112    /// Message type: "snapshot" or "delta".
113    #[serde(rename = "type")]
114    pub update_type: String,
115    /// Timestamp (milliseconds).
116    pub ts: u64,
117    /// The actual data.
118    pub data: T,
119    /// Timestamp of cross sequence.
120    #[serde(default)]
121    pub cts: Option<u64>,
122}
123
124/// Orderbook entry (price level).
125#[derive(Debug, Clone, Deserialize)]
126pub struct OrderbookEntry {
127    /// Price.
128    #[serde(rename = "0")]
129    pub price: String,
130    /// Size/quantity.
131    #[serde(rename = "1")]
132    pub size: String,
133}
134
135/// Orderbook data.
136#[derive(Debug, Clone, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct OrderbookData {
139    /// Symbol.
140    #[serde(rename = "s")]
141    pub symbol: String,
142    /// Bids (buy orders).
143    #[serde(rename = "b")]
144    pub bids: Vec<OrderbookEntry>,
145    /// Asks (sell orders).
146    #[serde(rename = "a")]
147    pub asks: Vec<OrderbookEntry>,
148    /// Update ID.
149    #[serde(rename = "u")]
150    pub update_id: u64,
151    /// Sequence number.
152    #[serde(default)]
153    pub seq: Option<u64>,
154}
155
156/// Public trade data.
157#[derive(Debug, Clone, Deserialize)]
158pub struct TradeData {
159    /// Timestamp (milliseconds).
160    #[serde(rename = "T")]
161    pub timestamp: u64,
162    /// Symbol.
163    #[serde(rename = "s")]
164    pub symbol: String,
165    /// Side (Buy/Sell).
166    #[serde(rename = "S")]
167    pub side: String,
168    /// Trade size/quantity.
169    #[serde(rename = "v")]
170    pub size: String,
171    /// Trade price.
172    #[serde(rename = "p")]
173    pub price: String,
174    /// Tick direction.
175    #[serde(rename = "L")]
176    pub tick_direction: String,
177    /// Trade ID.
178    #[serde(rename = "i")]
179    pub trade_id: String,
180    /// Block trade flag.
181    #[serde(rename = "BT")]
182    pub is_block_trade: bool,
183}
184
185/// Ticker data for linear/inverse.
186#[derive(Debug, Clone, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TickerData {
189    /// Symbol.
190    pub symbol: String,
191    /// Tick direction.
192    #[serde(default)]
193    pub tick_direction: Option<String>,
194    /// 24h price change percentage.
195    #[serde(default)]
196    pub price24h_pcnt: Option<String>,
197    /// Last price.
198    #[serde(default)]
199    pub last_price: Option<String>,
200    /// Previous 24h price.
201    #[serde(default)]
202    pub prev_price24h: Option<String>,
203    /// Highest price in 24h.
204    #[serde(default)]
205    pub high_price24h: Option<String>,
206    /// Lowest price in 24h.
207    #[serde(default)]
208    pub low_price24h: Option<String>,
209    /// Previous 1h price.
210    #[serde(default)]
211    pub prev_price1h: Option<String>,
212    /// Mark price.
213    #[serde(default)]
214    pub mark_price: Option<String>,
215    /// Index price.
216    #[serde(default)]
217    pub index_price: Option<String>,
218    /// Open interest.
219    #[serde(default)]
220    pub open_interest: Option<String>,
221    /// Open interest value.
222    #[serde(default)]
223    pub open_interest_value: Option<String>,
224    /// Turnover in 24h.
225    #[serde(default)]
226    pub turnover24h: Option<String>,
227    /// Volume in 24h.
228    #[serde(default)]
229    pub volume24h: Option<String>,
230    /// Next funding timestamp.
231    #[serde(default)]
232    pub next_funding_time: Option<String>,
233    /// Funding rate.
234    #[serde(default)]
235    pub funding_rate: Option<String>,
236    /// Best bid price.
237    #[serde(default)]
238    pub bid1_price: Option<String>,
239    /// Best bid size.
240    #[serde(default)]
241    pub bid1_size: Option<String>,
242    /// Best ask price.
243    #[serde(default)]
244    pub ask1_price: Option<String>,
245    /// Best ask size.
246    #[serde(default)]
247    pub ask1_size: Option<String>,
248}
249
250/// Kline (candlestick) data.
251#[derive(Debug, Clone, Deserialize)]
252pub struct KlineData {
253    /// Start time (milliseconds).
254    pub start: u64,
255    /// End time (milliseconds).
256    pub end: u64,
257    /// Interval.
258    pub interval: String,
259    /// Open price.
260    pub open: String,
261    /// Close price.
262    pub close: String,
263    /// High price.
264    pub high: String,
265    /// Low price.
266    pub low: String,
267    /// Volume.
268    pub volume: String,
269    /// Turnover.
270    pub turnover: String,
271    /// Whether this kline is confirmed.
272    pub confirm: bool,
273    /// Timestamp.
274    pub timestamp: u64,
275}
276
277/// Liquidation data.
278#[derive(Debug, Clone, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct LiquidationData {
281    /// Symbol.
282    pub symbol: String,
283    /// Side (Buy/Sell).
284    pub side: String,
285    /// Liquidation price.
286    pub price: String,
287    /// Liquidation size.
288    pub size: String,
289    /// Update time (milliseconds).
290    pub updated_time: u64,
291}
292
293
294/// Generic private WebSocket message wrapper.
295#[derive(Debug, Clone, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct WsPrivateMessage<T> {
298    /// Message ID.
299    #[serde(default)]
300    pub id: Option<String>,
301    /// Topic name.
302    pub topic: String,
303    /// Creation timestamp (milliseconds).
304    pub creation_time: u64,
305    /// The actual data.
306    pub data: T,
307}
308
309/// Position update data from private WebSocket stream.
310#[derive(Debug, Clone, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct PositionData {
313    /// Category (linear, inverse, option).
314    pub category: String,
315    /// Symbol.
316    pub symbol: String,
317    /// Position side (Buy/Sell for one-way, None for two-way).
318    pub side: String,
319    /// Position size.
320    pub size: String,
321    /// Position index (0: one-way, 1: buy hedge, 2: sell hedge).
322    #[serde(default)]
323    pub position_idx: i32,
324    /// Trade mode (0: cross, 1: isolated).
325    #[serde(default)]
326    pub trade_mode: i32,
327    /// Position value.
328    #[serde(default)]
329    pub position_value: Option<String>,
330    /// Risk ID.
331    #[serde(default)]
332    pub risk_id: Option<i32>,
333    /// Risk limit value.
334    #[serde(default)]
335    pub risk_limit_value: Option<String>,
336    /// Entry price.
337    #[serde(default)]
338    pub entry_price: Option<String>,
339    /// Mark price.
340    #[serde(default)]
341    pub mark_price: Option<String>,
342    /// Leverage.
343    #[serde(default)]
344    pub leverage: Option<String>,
345    /// Position balance.
346    #[serde(default)]
347    pub position_balance: Option<String>,
348    /// Auto add margin (0: no, 1: yes).
349    #[serde(default)]
350    pub auto_add_margin: Option<i32>,
351    /// Position maintenance margin.
352    #[serde(default)]
353    pub position_m_m: Option<String>,
354    /// Position initial margin.
355    #[serde(default)]
356    pub position_i_m: Option<String>,
357    /// Liquidation price.
358    #[serde(default)]
359    pub liq_price: Option<String>,
360    /// Bankruptcy price.
361    #[serde(default)]
362    pub bust_price: Option<String>,
363    /// TP/SL mode (Full/Partial).
364    #[serde(default)]
365    pub tpsl_mode: Option<String>,
366    /// Take profit price.
367    #[serde(default)]
368    pub take_profit: Option<String>,
369    /// Stop loss price.
370    #[serde(default)]
371    pub stop_loss: Option<String>,
372    /// Trailing stop.
373    #[serde(default)]
374    pub trailing_stop: Option<String>,
375    /// Unrealised PnL.
376    #[serde(default)]
377    pub unrealised_pnl: Option<String>,
378    /// Current realised PnL.
379    #[serde(default)]
380    pub cur_realised_pnl: Option<String>,
381    /// Cumulative realised PnL.
382    #[serde(default)]
383    pub cum_realised_pnl: Option<String>,
384    /// Session average price.
385    #[serde(default)]
386    pub session_avg_price: Option<String>,
387    /// Position status (Normal, Liq, Adl).
388    #[serde(default)]
389    pub position_status: Option<String>,
390    /// ADL rank indicator.
391    #[serde(default)]
392    pub adl_rank_indicator: Option<i32>,
393    /// Is reduce only.
394    #[serde(default)]
395    pub is_reduce_only: Option<bool>,
396    /// Created time.
397    #[serde(default)]
398    pub created_time: Option<String>,
399    /// Updated time.
400    #[serde(default)]
401    pub updated_time: Option<String>,
402    /// Sequence number.
403    #[serde(default)]
404    pub seq: Option<i64>,
405}
406
407/// Order update data from private WebSocket stream.
408#[derive(Debug, Clone, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct OrderData {
411    /// Category (spot, linear, inverse, option).
412    pub category: String,
413    /// Order ID.
414    pub order_id: String,
415    /// User custom order ID.
416    #[serde(default)]
417    pub order_link_id: Option<String>,
418    /// Is leverage order.
419    #[serde(default)]
420    pub is_leverage: Option<String>,
421    /// Block trade ID.
422    #[serde(default)]
423    pub block_trade_id: Option<String>,
424    /// Symbol.
425    pub symbol: String,
426    /// Order price.
427    pub price: String,
428    /// Order quantity.
429    pub qty: String,
430    /// Side (Buy/Sell).
431    pub side: String,
432    /// Position index.
433    #[serde(default)]
434    pub position_idx: Option<i32>,
435    /// Order status.
436    pub order_status: String,
437    /// Create type.
438    #[serde(default)]
439    pub create_type: Option<String>,
440    /// Cancel type.
441    #[serde(default)]
442    pub cancel_type: Option<String>,
443    /// Reject reason.
444    #[serde(default)]
445    pub reject_reason: Option<String>,
446    /// Average fill price.
447    #[serde(default)]
448    pub avg_price: Option<String>,
449    /// Remaining quantity.
450    #[serde(default)]
451    pub leaves_qty: Option<String>,
452    /// Remaining value.
453    #[serde(default)]
454    pub leaves_value: Option<String>,
455    /// Cumulative executed quantity.
456    #[serde(default)]
457    pub cum_exec_qty: Option<String>,
458    /// Cumulative executed value.
459    #[serde(default)]
460    pub cum_exec_value: Option<String>,
461    /// Cumulative executed fee.
462    #[serde(default)]
463    pub cum_exec_fee: Option<String>,
464    /// Closed PnL.
465    #[serde(default)]
466    pub closed_pnl: Option<String>,
467    /// Fee currency.
468    #[serde(default)]
469    pub fee_currency: Option<String>,
470    /// Time in force.
471    #[serde(default)]
472    pub time_in_force: Option<String>,
473    /// Order type (Limit/Market).
474    pub order_type: String,
475    /// Stop order type.
476    #[serde(default)]
477    pub stop_order_type: Option<String>,
478    /// Trigger price.
479    #[serde(default)]
480    pub trigger_price: Option<String>,
481    /// Take profit.
482    #[serde(default)]
483    pub take_profit: Option<String>,
484    /// Stop loss.
485    #[serde(default)]
486    pub stop_loss: Option<String>,
487    /// TP/SL mode.
488    #[serde(default)]
489    pub tpsl_mode: Option<String>,
490    /// Reduce only.
491    #[serde(default)]
492    pub reduce_only: Option<bool>,
493    /// Close on trigger.
494    #[serde(default)]
495    pub close_on_trigger: Option<bool>,
496    /// SMP type.
497    #[serde(default)]
498    pub smp_type: Option<String>,
499    /// SMP group.
500    #[serde(default)]
501    pub smp_group: Option<i32>,
502    /// SMP order ID.
503    #[serde(default)]
504    pub smp_order_id: Option<String>,
505    /// Created time.
506    #[serde(default)]
507    pub created_time: Option<String>,
508    /// Updated time.
509    #[serde(default)]
510    pub updated_time: Option<String>,
511}
512
513/// Execution (trade fill) data from private WebSocket stream.
514#[derive(Debug, Clone, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct ExecutionData {
517    /// Category.
518    pub category: String,
519    /// Symbol.
520    pub symbol: String,
521    /// Is leverage.
522    #[serde(default)]
523    pub is_leverage: Option<String>,
524    /// Order ID.
525    pub order_id: String,
526    /// User custom order ID.
527    #[serde(default)]
528    pub order_link_id: Option<String>,
529    /// Side.
530    pub side: String,
531    /// Order price.
532    #[serde(default)]
533    pub order_price: Option<String>,
534    /// Order quantity.
535    #[serde(default)]
536    pub order_qty: Option<String>,
537    /// Remaining quantity.
538    #[serde(default)]
539    pub leaves_qty: Option<String>,
540    /// Create type.
541    #[serde(default)]
542    pub create_type: Option<String>,
543    /// Order type.
544    #[serde(default)]
545    pub order_type: Option<String>,
546    /// Stop order type.
547    #[serde(default)]
548    pub stop_order_type: Option<String>,
549    /// Execution fee.
550    #[serde(default)]
551    pub exec_fee: Option<String>,
552    /// Fee currency.
553    #[serde(default)]
554    pub fee_currency: Option<String>,
555    /// Execution ID.
556    pub exec_id: String,
557    /// Execution price.
558    pub exec_price: String,
559    /// Execution quantity.
560    pub exec_qty: String,
561    /// Execution PnL.
562    #[serde(default)]
563    pub exec_pnl: Option<String>,
564    /// Execution type (Trade, Funding, AdlTrade, BustTrade).
565    #[serde(default)]
566    pub exec_type: Option<String>,
567    /// Execution value.
568    #[serde(default)]
569    pub exec_value: Option<String>,
570    /// Execution time.
571    pub exec_time: String,
572    /// Is maker.
573    #[serde(default)]
574    pub is_maker: Option<bool>,
575    /// Fee rate.
576    #[serde(default)]
577    pub fee_rate: Option<String>,
578    /// Mark price.
579    #[serde(default)]
580    pub mark_price: Option<String>,
581    /// Index price.
582    #[serde(default)]
583    pub index_price: Option<String>,
584    /// Closed size.
585    #[serde(default)]
586    pub closed_size: Option<String>,
587    /// Sequence number.
588    #[serde(default)]
589    pub seq: Option<i64>,
590}
591
592/// Fast execution data (low-latency) from private WebSocket stream.
593#[derive(Debug, Clone, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct ExecutionFastData {
596    /// Category.
597    pub category: String,
598    /// Symbol.
599    pub symbol: String,
600    /// Execution ID.
601    pub exec_id: String,
602    /// Execution price.
603    pub exec_price: String,
604    /// Execution quantity.
605    pub exec_qty: String,
606    /// Order ID.
607    pub order_id: String,
608    /// Is maker.
609    #[serde(default)]
610    pub is_maker: Option<bool>,
611    /// User custom order ID.
612    #[serde(default)]
613    pub order_link_id: Option<String>,
614    /// Side.
615    pub side: String,
616    /// Execution time.
617    pub exec_time: String,
618    /// Sequence number.
619    #[serde(default)]
620    pub seq: Option<i64>,
621}
622
623/// Coin balance data within wallet update.
624#[derive(Debug, Clone, Deserialize)]
625#[serde(rename_all = "camelCase")]
626pub struct CoinData {
627    /// Coin name.
628    pub coin: String,
629    /// Equity.
630    #[serde(default)]
631    pub equity: Option<String>,
632    /// USD value.
633    #[serde(default)]
634    pub usd_value: Option<String>,
635    /// Wallet balance.
636    #[serde(default)]
637    pub wallet_balance: Option<String>,
638    /// Free balance.
639    #[serde(default)]
640    pub free: Option<String>,
641    /// Locked balance.
642    #[serde(default)]
643    pub locked: Option<String>,
644    /// Borrow amount.
645    #[serde(default)]
646    pub borrow_amount: Option<String>,
647    /// Available to borrow.
648    #[serde(default)]
649    pub available_to_borrow: Option<String>,
650    /// Available to withdraw.
651    #[serde(default)]
652    pub available_to_withdraw: Option<String>,
653    /// Accrued interest.
654    #[serde(default)]
655    pub accrued_interest: Option<String>,
656    /// Total order initial margin.
657    #[serde(default)]
658    pub total_order_i_m: Option<String>,
659    /// Total position initial margin.
660    #[serde(default)]
661    pub total_position_i_m: Option<String>,
662    /// Total position maintenance margin.
663    #[serde(default)]
664    pub total_position_m_m: Option<String>,
665    /// Unrealised PnL.
666    #[serde(default)]
667    pub unrealised_pnl: Option<String>,
668    /// Cumulative realised PnL.
669    #[serde(default)]
670    pub cum_realised_pnl: Option<String>,
671    /// Bonus.
672    #[serde(default)]
673    pub bonus: Option<String>,
674}
675
676/// Wallet update data from private WebSocket stream.
677#[derive(Debug, Clone, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct WalletData {
680    /// Account type (UNIFIED, CONTRACT, SPOT).
681    pub account_type: String,
682    /// Account LTV (loan-to-value).
683    #[serde(default)]
684    pub account_l_t_v: Option<String>,
685    /// Account initial margin rate.
686    #[serde(default)]
687    pub account_i_m_rate: Option<String>,
688    /// Account maintenance margin rate.
689    #[serde(default)]
690    pub account_m_m_rate: Option<String>,
691    /// Total equity.
692    #[serde(default)]
693    pub total_equity: Option<String>,
694    /// Total wallet balance.
695    #[serde(default)]
696    pub total_wallet_balance: Option<String>,
697    /// Total margin balance.
698    #[serde(default)]
699    pub total_margin_balance: Option<String>,
700    /// Total available balance.
701    #[serde(default)]
702    pub total_available_balance: Option<String>,
703    /// Total perpetual unrealised PnL.
704    #[serde(default)]
705    pub total_perp_u_p_l: Option<String>,
706    /// Total initial margin.
707    #[serde(default)]
708    pub total_initial_margin: Option<String>,
709    /// Total maintenance margin.
710    #[serde(default)]
711    pub total_maintenance_margin: Option<String>,
712    /// Coin balances.
713    #[serde(default)]
714    pub coin: Vec<CoinData>,
715}
716
717/// Greeks update data from private WebSocket stream (options).
718#[derive(Debug, Clone, Deserialize)]
719#[serde(rename_all = "camelCase")]
720pub struct GreeksData {
721    /// Base coin.
722    pub base_coin: String,
723    /// Total delta.
724    #[serde(default)]
725    pub total_delta: Option<String>,
726    /// Total gamma.
727    #[serde(default)]
728    pub total_gamma: Option<String>,
729    /// Total vega.
730    #[serde(default)]
731    pub total_vega: Option<String>,
732    /// Total theta.
733    #[serde(default)]
734    pub total_theta: Option<String>,
735}
736
737
738/// High-level WebSocket message types.
739#[derive(Debug, Clone)]
740pub enum WsMessage {
741    /// Pong response.
742    Pong(WsPong),
743    /// Operation response (subscribe, unsubscribe, auth).
744    OperationResponse(WsOperationResponse),
745    /// Orderbook update.
746    Orderbook(Box<WsStreamMessage<OrderbookData>>),
747    /// Trade update.
748    Trade(Box<WsStreamMessage<Vec<TradeData>>>),
749    /// Ticker update.
750    Ticker(Box<WsStreamMessage<TickerData>>),
751    /// Kline update.
752    Kline(Box<WsStreamMessage<Vec<KlineData>>>),
753    /// Liquidation update.
754    Liquidation(Box<WsStreamMessage<LiquidationData>>),
755    /// Position update (private stream).
756    Position(Box<WsPrivateMessage<Vec<PositionData>>>),
757    /// Order update (private stream).
758    Order(Box<WsPrivateMessage<Vec<OrderData>>>),
759    /// Execution update (private stream).
760    Execution(Box<WsPrivateMessage<Vec<ExecutionData>>>),
761    /// Fast execution update (private stream).
762    ExecutionFast(Box<WsPrivateMessage<Vec<ExecutionFastData>>>),
763    /// Wallet update (private stream).
764    Wallet(Box<WsPrivateMessage<Vec<WalletData>>>),
765    /// Greeks update (private stream, options).
766    Greeks(Box<WsPrivateMessage<Vec<GreeksData>>>),
767    /// Unknown/raw message.
768    Raw(String),
769}
770
771
772/// WebSocket channel type (determines which URL to connect to).
773#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
774pub enum WsChannel {
775    /// Public spot channel.
776    PublicSpot,
777    /// Public linear perpetual channel.
778    PublicLinear,
779    /// Public inverse perpetual channel.
780    PublicInverse,
781    /// Public options channel.
782    PublicOption,
783    /// Private channel (requires authentication).
784    Private,
785    /// Trade channel (WebSocket API for orders).
786    Trade,
787}
788
789impl WsChannel {
790    /// Get the URL path for this channel.
791    pub fn path(&self) -> &'static str {
792        match self {
793            WsChannel::PublicSpot => "/v5/public/spot",
794            WsChannel::PublicLinear => "/v5/public/linear",
795            WsChannel::PublicInverse => "/v5/public/inverse",
796            WsChannel::PublicOption => "/v5/public/option",
797            WsChannel::Private => "/v5/private",
798            WsChannel::Trade => "/v5/trade",
799        }
800    }
801
802    /// Create a public channel from a Category.
803    pub fn from_category(category: Category) -> Self {
804        match category {
805            Category::Spot => WsChannel::PublicSpot,
806            Category::Linear => WsChannel::PublicLinear,
807            Category::Inverse => WsChannel::PublicInverse,
808            Category::Option => WsChannel::PublicOption,
809        }
810    }
811
812    /// Check if this channel requires authentication.
813    pub fn requires_auth(&self) -> bool {
814        matches!(self, WsChannel::Private | WsChannel::Trade)
815    }
816}
817
818/// Connection state for a WebSocket connection.
819#[derive(Debug, Clone, Copy, PartialEq, Eq)]
820pub enum ConnectionState {
821    /// Not connected.
822    Disconnected,
823    /// Connection in progress.
824    Connecting,
825    /// Connected and ready.
826    Connected,
827    /// Reconnecting after disconnect.
828    Reconnecting,
829    /// Connection closed.
830    Closed,
831}
832
833#[cfg(test)]
834mod tests {
835    use super::*;
836
837    #[test]
838    fn test_ws_operation_serialize() {
839        let op = WsOperation::subscribe(vec!["orderbook.50.BTCUSDT".to_string()]);
840        let json = match serde_json::to_string(&op) {
841            Ok(json) => json,
842            Err(err) => panic!("Failed to serialize operation: {}", err),
843        };
844        assert!(json.contains("\"op\":\"subscribe\""));
845        assert!(json.contains("orderbook.50.BTCUSDT"));
846    }
847
848    #[test]
849    fn test_ws_operation_ping() {
850        let op = WsOperation::ping();
851        let json = match serde_json::to_string(&op) {
852            Ok(json) => json,
853            Err(err) => panic!("Failed to serialize ping: {}", err),
854        };
855        assert_eq!(json, r#"{"op":"ping"}"#);
856    }
857
858    #[test]
859    fn test_ws_channel_path() {
860        assert_eq!(WsChannel::PublicLinear.path(), "/v5/public/linear");
861        assert_eq!(WsChannel::Private.path(), "/v5/private");
862    }
863
864    #[test]
865    fn test_orderbook_data_deserialize() {
866        let json = r#"{
867            "s": "BTCUSDT",
868            "b": [["50000", "1.5"], ["49999", "2.0"]],
869            "a": [["50001", "0.8"]],
870            "u": 12345
871        }"#;
872        let data: OrderbookData = match serde_json::from_str(json) {
873            Ok(data) => data,
874            Err(err) => panic!("Failed to parse orderbook data: {}", err),
875        };
876        assert_eq!(data.symbol, "BTCUSDT");
877        assert_eq!(data.bids.len(), 2);
878        assert_eq!(data.bids[0].price, "50000");
879        assert_eq!(data.bids[0].size, "1.5");
880    }
881
882    #[test]
883    fn test_position_data_deserialize() {
884        let json = r#"{
885            "category": "linear",
886            "symbol": "BTCUSDT",
887            "side": "Buy",
888            "size": "0.01",
889            "positionIdx": 0,
890            "tradeMode": 0,
891            "positionValue": "500.0",
892            "entryPrice": "50000",
893            "markPrice": "50100",
894            "leverage": "10",
895            "unrealisedPnl": "1.0",
896            "cumRealisedPnl": "100.0",
897            "positionStatus": "Normal",
898            "createdTime": "1658384314791",
899            "updatedTime": "1658384314792"
900        }"#;
901        let data: PositionData = match serde_json::from_str(json) {
902            Ok(data) => data,
903            Err(err) => panic!("Failed to parse position data: {}", err),
904        };
905        assert_eq!(data.category, "linear");
906        assert_eq!(data.symbol, "BTCUSDT");
907        assert_eq!(data.side, "Buy");
908        assert_eq!(data.size, "0.01");
909        assert_eq!(data.entry_price, Some("50000".to_string()));
910    }
911
912    #[test]
913    fn test_order_data_deserialize() {
914        let json = r#"{
915            "category": "linear",
916            "orderId": "order-123",
917            "orderLinkId": "my-order-1",
918            "symbol": "BTCUSDT",
919            "price": "50000",
920            "qty": "0.01",
921            "side": "Buy",
922            "orderStatus": "New",
923            "orderType": "Limit",
924            "timeInForce": "GTC",
925            "createdTime": "1658384314791",
926            "updatedTime": "1658384314792"
927        }"#;
928        let data: OrderData = match serde_json::from_str(json) {
929            Ok(data) => data,
930            Err(err) => panic!("Failed to parse order data: {}", err),
931        };
932        assert_eq!(data.category, "linear");
933        assert_eq!(data.order_id, "order-123");
934        assert_eq!(data.symbol, "BTCUSDT");
935        assert_eq!(data.side, "Buy");
936        assert_eq!(data.order_status, "New");
937        assert_eq!(data.order_type, "Limit");
938    }
939
940    #[test]
941    fn test_execution_data_deserialize() {
942        let json = r#"{
943            "category": "linear",
944            "symbol": "BTCUSDT",
945            "orderId": "order-123",
946            "side": "Buy",
947            "execId": "exec-456",
948            "execPrice": "50000",
949            "execQty": "0.01",
950            "execTime": "1658384314791",
951            "execType": "Trade",
952            "isMaker": false,
953            "feeRate": "0.0006"
954        }"#;
955        let data: ExecutionData = match serde_json::from_str(json) {
956            Ok(data) => data,
957            Err(err) => panic!("Failed to parse execution data: {}", err),
958        };
959        assert_eq!(data.category, "linear");
960        assert_eq!(data.symbol, "BTCUSDT");
961        assert_eq!(data.exec_id, "exec-456");
962        assert_eq!(data.exec_price, "50000");
963        assert_eq!(data.exec_qty, "0.01");
964        assert_eq!(data.is_maker, Some(false));
965    }
966
967    #[test]
968    fn test_wallet_data_deserialize() {
969        let json = r#"{
970            "accountType": "UNIFIED",
971            "totalEquity": "10000.0",
972            "totalWalletBalance": "9500.0",
973            "totalAvailableBalance": "8000.0",
974            "coin": [
975                {
976                    "coin": "USDT",
977                    "equity": "5000.0",
978                    "walletBalance": "5000.0",
979                    "availableToWithdraw": "4500.0"
980                },
981                {
982                    "coin": "BTC",
983                    "equity": "0.1",
984                    "walletBalance": "0.1"
985                }
986            ]
987        }"#;
988        let data: WalletData = match serde_json::from_str(json) {
989            Ok(data) => data,
990            Err(err) => panic!("Failed to parse wallet data: {}", err),
991        };
992        assert_eq!(data.account_type, "UNIFIED");
993        assert_eq!(data.total_equity, Some("10000.0".to_string()));
994        assert_eq!(data.coin.len(), 2);
995        assert_eq!(data.coin[0].coin, "USDT");
996        assert_eq!(data.coin[1].coin, "BTC");
997    }
998
999    #[test]
1000    fn test_greeks_data_deserialize() {
1001        let json = r#"{
1002            "baseCoin": "BTC",
1003            "totalDelta": "0.5",
1004            "totalGamma": "0.001",
1005            "totalVega": "100.0",
1006            "totalTheta": "-50.0"
1007        }"#;
1008        let data: GreeksData = match serde_json::from_str(json) {
1009            Ok(data) => data,
1010            Err(err) => panic!("Failed to parse greeks data: {}", err),
1011        };
1012        assert_eq!(data.base_coin, "BTC");
1013        assert_eq!(data.total_delta, Some("0.5".to_string()));
1014        assert_eq!(data.total_gamma, Some("0.001".to_string()));
1015    }
1016
1017    #[test]
1018    fn test_private_message_deserialize() {
1019        let json = r#"{
1020            "topic": "position",
1021            "creationTime": 1658384314791,
1022            "data": [{
1023                "category": "linear",
1024                "symbol": "BTCUSDT",
1025                "side": "Buy",
1026                "size": "0.01"
1027            }]
1028        }"#;
1029        let msg: WsPrivateMessage<Vec<PositionData>> = match serde_json::from_str(json) {
1030            Ok(msg) => msg,
1031            Err(err) => panic!("Failed to parse private message: {}", err),
1032        };
1033        assert_eq!(msg.topic, "position");
1034        assert_eq!(msg.creation_time, 1658384314791);
1035        assert_eq!(msg.data.len(), 1);
1036        assert_eq!(msg.data[0].symbol, "BTCUSDT");
1037    }
1038}