1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct BybitSubscription {
33 pub op: BybitWsOperation,
34 pub args: Vec<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BybitAuthRequest {
40 pub op: BybitWsOperation,
41 pub args: Vec<serde_json::Value>,
42}
43
44#[derive(Debug, Clone)]
46pub enum BybitWebSocketMessage {
47 Response(BybitWsResponse),
49 Auth(BybitWsAuthResponse),
51 Subscription(BybitWsSubscriptionMsg),
53 Orderbook(BybitWsOrderbookDepthMsg),
55 Trade(BybitWsTradeMsg),
57 Kline(BybitWsKlineMsg),
59 TickerLinear(BybitWsTickerLinearMsg),
61 TickerOption(BybitWsTickerOptionMsg),
63 AccountOrder(BybitWsAccountOrderMsg),
65 AccountExecution(BybitWsAccountExecutionMsg),
67 AccountWallet(BybitWsAccountWalletMsg),
69 AccountPosition(BybitWsAccountPositionMsg),
71 Error(BybitWebSocketError),
73 Raw(Value),
75 Reconnected,
77 Pong,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84#[cfg_attr(feature = "python", pyo3::pyclass)]
85pub struct BybitWebSocketError {
86 pub code: i64,
88 pub message: String,
90 #[serde(default)]
92 pub conn_id: Option<String>,
93 #[serde(default)]
95 pub topic: Option<String>,
96 #[serde(default)]
98 pub req_id: Option<String>,
99}
100
101impl BybitWebSocketError {
102 #[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 #[must_use]
116 pub fn from_response(response: &BybitWsResponse) -> Self {
117 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 #[must_use]
149 pub fn from_message(message: impl Into<String>) -> Self {
150 Self::new(-1, message)
151 }
152}
153
154#[derive(Debug, Clone, Serialize)]
156pub struct BybitWsRequest<T> {
157 pub op: BybitWsOrderRequestOp,
159 pub header: BybitWsHeader,
161 pub args: Vec<T>,
163}
164
165#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
168pub struct BybitWsHeader {
169 pub x_bapi_timestamp: String,
171}
172
173impl BybitWsHeader {
174 #[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
348pub struct BybitWsOrderbookDepth {
349 pub s: Ustr,
351 pub b: Vec<Vec<String>>,
353 pub a: Vec<Vec<String>>,
355 pub u: i64,
357 pub seq: i64,
359}
360
361#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}