1use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use ustr::Ustr;
21
22use crate::{
23 common::{
24 enums::{
25 BybitCancelType, BybitCreateType, BybitExecType, BybitMarketUnit, BybitOrderSide,
26 BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide,
27 BybitPositionStatus, BybitProductType, BybitSmpType, BybitStopOrderType,
28 BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection, BybitTriggerType,
29 BybitWsOrderRequestOp,
30 },
31 parse::{
32 deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero,
33 deserialize_optional_decimal_str,
34 },
35 },
36 websocket::enums::BybitWsOperation,
37};
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BybitSubscription {
42 pub op: BybitWsOperation,
43 pub args: Vec<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub req_id: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct BybitAuthRequest {
51 pub op: BybitWsOperation,
52 pub args: Vec<serde_json::Value>,
53}
54
55#[derive(Debug, Clone)]
60pub enum BybitWsFrame {
61 Auth(BybitWsAuthResponse),
63 Subscription(BybitWsSubscriptionMsg),
65 OrderResponse(BybitWsOrderResponse),
67 ErrorResponse(BybitWsResponse),
69 Orderbook(BybitWsOrderbookDepthMsg),
71 Trade(BybitWsTradeMsg),
73 Kline(BybitWsKlineMsg),
75 TickerLinear(BybitWsTickerLinearMsg),
77 TickerOption(BybitWsTickerOptionMsg),
79 AccountOrder(BybitWsAccountOrderMsg),
81 AccountExecution(BybitWsAccountExecutionMsg),
83 AccountWallet(BybitWsAccountWalletMsg),
85 AccountPosition(BybitWsAccountPositionMsg),
87 Unknown(Value),
89 Reconnected,
91}
92
93#[derive(Debug, Clone)]
95pub enum BybitWsMessage {
96 Auth(BybitWsAuthResponse),
98 OrderResponse(BybitWsOrderResponse),
100 Orderbook(BybitWsOrderbookDepthMsg),
102 Trade(BybitWsTradeMsg),
104 Kline(BybitWsKlineMsg),
106 TickerLinear(BybitWsTickerLinearMsg),
108 TickerOption(BybitWsTickerOptionMsg),
110 AccountOrder(BybitWsAccountOrderMsg),
112 AccountExecution(BybitWsAccountExecutionMsg),
114 AccountWallet(BybitWsAccountWalletMsg),
116 AccountPosition(BybitWsAccountPositionMsg),
118 Error(BybitWebSocketError),
120 Reconnected,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
128#[cfg_attr(
129 feature = "python",
130 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
131)]
132pub struct BybitWebSocketError {
133 pub code: i64,
135 pub message: String,
137 #[serde(default)]
139 pub conn_id: Option<String>,
140 #[serde(default)]
142 pub topic: Option<String>,
143 #[serde(default)]
145 pub req_id: Option<String>,
146}
147
148impl BybitWebSocketError {
149 #[must_use]
151 pub fn new(code: i64, message: impl Into<String>) -> Self {
152 Self {
153 code,
154 message: message.into(),
155 conn_id: None,
156 topic: None,
157 req_id: None,
158 }
159 }
160
161 #[must_use]
163 pub fn from_response(response: &BybitWsResponse) -> Self {
164 let message = response.ret_msg.clone().unwrap_or_else(|| {
166 let mut parts = vec![];
167
168 if let Some(op) = &response.op {
169 parts.push(format!("op={op}"));
170 }
171
172 if let Some(topic) = &response.topic {
173 parts.push(format!("topic={topic}"));
174 }
175
176 if let Some(success) = response.success {
177 parts.push(format!("success={success}"));
178 }
179
180 if parts.is_empty() {
181 "Bybit websocket error (no error message provided)".to_string()
182 } else {
183 format!("Bybit websocket error: {}", parts.join(", "))
184 }
185 });
186
187 Self {
188 code: response.ret_code.unwrap_or_default(),
189 message,
190 conn_id: response.conn_id.clone(),
191 topic: response.topic.map(|t| t.to_string()),
192 req_id: response.req_id.clone(),
193 }
194 }
195
196 #[must_use]
198 pub fn from_message(message: impl Into<String>) -> Self {
199 Self::new(-1, message)
200 }
201}
202
203#[derive(Debug, Clone, Serialize)]
205#[serde(rename_all = "camelCase")]
206pub struct BybitWsRequest<T> {
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub req_id: Option<String>,
210 pub op: BybitWsOrderRequestOp,
212 pub header: BybitWsHeader,
214 pub args: Vec<T>,
216}
217
218#[derive(Debug, Clone, Serialize)]
220#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
221pub struct BybitWsHeader {
222 pub x_bapi_timestamp: String,
224 #[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
226 pub referer: Option<String>,
227}
228
229impl BybitWsHeader {
230 #[must_use]
232 pub fn now() -> Self {
233 Self::with_referer(None)
234 }
235
236 #[must_use]
238 pub fn with_referer(referer: Option<String>) -> Self {
239 use nautilus_core::time::get_atomic_clock_realtime;
240 Self {
241 x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
242 referer,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct BybitWsPlaceOrderParams {
251 pub category: BybitProductType,
252 pub symbol: Ustr,
253 pub side: BybitOrderSide,
254 pub order_type: BybitOrderType,
255 pub qty: String,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub is_leverage: Option<i32>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub market_unit: Option<BybitMarketUnit>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub price: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub time_in_force: Option<BybitTimeInForce>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub order_link_id: Option<String>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub reduce_only: Option<bool>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub close_on_trigger: Option<bool>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub trigger_price: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub trigger_by: Option<BybitTriggerType>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub trigger_direction: Option<i32>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub tpsl_mode: Option<BybitTpSlMode>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub take_profit: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub stop_loss: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub tp_trigger_by: Option<BybitTriggerType>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub sl_trigger_by: Option<BybitTriggerType>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub sl_trigger_price: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub tp_trigger_price: Option<String>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub sl_order_type: Option<BybitOrderType>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub tp_order_type: Option<BybitOrderType>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub sl_limit_price: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub tp_limit_price: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub order_iv: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub mmp: Option<bool>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub position_idx: Option<BybitPositionIdx>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct BybitWsAmendOrderParams {
310 pub category: BybitProductType,
311 pub symbol: Ustr,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub order_id: Option<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub order_link_id: Option<String>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub qty: Option<String>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub price: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub trigger_price: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub take_profit: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub stop_loss: Option<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub tp_trigger_by: Option<BybitTriggerType>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub sl_trigger_by: Option<BybitTriggerType>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub order_iv: Option<String>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct BybitWsCancelOrderParams {
338 pub category: BybitProductType,
339 pub symbol: Ustr,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub order_id: Option<String>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub order_link_id: Option<String>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct BybitWsBatchCancelItem {
350 pub symbol: Ustr,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub order_id: Option<String>,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 pub order_link_id: Option<String>,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct BybitWsBatchCancelOrderArgs {
360 pub category: BybitProductType,
361 pub request: Vec<BybitWsBatchCancelItem>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct BybitWsBatchPlaceItem {
368 pub symbol: Ustr,
369 pub side: BybitOrderSide,
370 pub order_type: BybitOrderType,
371 pub qty: String,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub is_leverage: Option<i32>,
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub market_unit: Option<BybitMarketUnit>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub price: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub time_in_force: Option<BybitTimeInForce>,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub order_link_id: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub reduce_only: Option<bool>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub close_on_trigger: Option<bool>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub trigger_price: Option<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub trigger_by: Option<BybitTriggerType>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub trigger_direction: Option<i32>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub tpsl_mode: Option<BybitTpSlMode>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub take_profit: Option<String>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub stop_loss: Option<String>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub tp_trigger_by: Option<BybitTriggerType>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub sl_trigger_by: Option<BybitTriggerType>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub sl_trigger_price: Option<String>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub tp_trigger_price: Option<String>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub sl_order_type: Option<BybitOrderType>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub tp_order_type: Option<BybitOrderType>,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub sl_limit_price: Option<String>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub tp_limit_price: Option<String>,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub order_iv: Option<String>,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub mmp: Option<bool>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub position_idx: Option<BybitPositionIdx>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct BybitWsBatchPlaceOrderArgs {
425 pub category: BybitProductType,
426 pub request: Vec<BybitWsBatchPlaceItem>,
427}
428
429#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct BybitWsSubscriptionMsg {
432 pub success: bool,
433 pub op: BybitWsOperation,
434 #[serde(default)]
435 pub conn_id: Option<String>,
436 #[serde(default)]
437 pub req_id: Option<String>,
438 #[serde(default)]
439 pub ret_msg: Option<String>,
440}
441
442#[derive(Clone, Debug, Serialize, Deserialize)]
444pub struct BybitWsResponse {
445 #[serde(default)]
446 pub op: Option<BybitWsOperation>,
447 #[serde(default)]
448 pub topic: Option<Ustr>,
449 #[serde(default)]
450 pub success: Option<bool>,
451 #[serde(default)]
452 pub conn_id: Option<String>,
453 #[serde(default)]
454 pub req_id: Option<String>,
455 #[serde(default)]
456 pub ret_code: Option<i64>,
457 #[serde(default)]
458 pub ret_msg: Option<String>,
459}
460
461#[derive(Clone, Debug, Serialize, Deserialize)]
463#[serde(rename_all = "camelCase")]
464pub struct BybitWsOrderResponse {
465 pub op: Ustr,
467 #[serde(default)]
469 pub conn_id: Option<String>,
470 pub ret_code: i64,
472 pub ret_msg: String,
474 #[serde(default)]
476 pub data: Value,
477 #[serde(default)]
479 pub req_id: Option<String>,
480 #[serde(default)]
482 pub header: Option<Value>,
483 #[serde(default)]
485 pub ret_ext_info: Option<Value>,
486}
487
488impl BybitWsOrderResponse {
489 #[must_use]
494 pub fn extract_batch_errors(&self) -> Vec<BybitBatchOrderError> {
495 self.ret_ext_info
496 .as_ref()
497 .and_then(|ext| ext.get("list"))
498 .and_then(|list| list.as_array())
499 .map(|arr| {
500 arr.iter()
501 .filter_map(|item| {
502 let code = item.get("code")?.as_i64()?;
503 let msg = item.get("msg")?.as_str()?.to_string();
504 Some(BybitBatchOrderError { code, msg })
505 })
506 .collect()
507 })
508 .unwrap_or_default()
509 }
510}
511
512#[derive(Clone, Debug)]
514pub struct BybitBatchOrderError {
515 pub code: i64,
517 pub msg: String,
519}
520
521#[derive(Clone, Debug, Serialize, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct BybitWsAuthResponse {
525 pub op: BybitWsOperation,
526 #[serde(default)]
527 pub conn_id: Option<String>,
528 #[serde(default)]
529 pub ret_code: Option<i64>,
530 #[serde(default)]
531 pub ret_msg: Option<String>,
532 #[serde(default)]
533 pub success: Option<bool>,
534}
535
536#[derive(Clone, Debug, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct BybitWsKline {
540 pub start: i64,
541 pub end: i64,
542 pub interval: Ustr,
543 pub open: String,
544 pub close: String,
545 pub high: String,
546 pub low: String,
547 pub volume: String,
548 pub turnover: String,
549 pub confirm: bool,
550 pub timestamp: i64,
551}
552
553#[derive(Clone, Debug, Serialize, Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct BybitWsKlineMsg {
557 pub topic: Ustr,
558 pub ts: i64,
559 #[serde(rename = "type")]
560 pub msg_type: Ustr,
561 pub data: Vec<BybitWsKline>,
562}
563
564#[derive(Clone, Debug, Serialize, Deserialize)]
566pub struct BybitWsOrderbookDepth {
567 pub s: Ustr,
569 pub b: Vec<Vec<String>>,
571 pub a: Vec<Vec<String>>,
573 pub u: i64,
575 pub seq: i64,
577}
578
579#[derive(Clone, Debug, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub struct BybitWsOrderbookDepthMsg {
583 pub topic: Ustr,
584 #[serde(rename = "type")]
585 pub msg_type: Ustr,
586 pub ts: i64,
587 pub data: BybitWsOrderbookDepth,
588 #[serde(default)]
589 pub cts: Option<i64>,
590}
591
592#[derive(Clone, Debug, Serialize, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct BybitWsTickerLinear {
596 pub symbol: Ustr,
597 #[serde(default)]
598 pub tick_direction: Option<String>,
599 #[serde(default)]
600 pub price24h_pcnt: Option<String>,
601 #[serde(default)]
602 pub last_price: Option<String>,
603 #[serde(default)]
604 pub prev_price24h: Option<String>,
605 #[serde(default)]
606 pub high_price24h: Option<String>,
607 #[serde(default)]
608 pub low_price24h: Option<String>,
609 #[serde(default)]
610 pub prev_price1h: Option<String>,
611 #[serde(default)]
612 pub mark_price: Option<String>,
613 #[serde(default)]
614 pub index_price: Option<String>,
615 #[serde(default)]
616 pub open_interest: Option<String>,
617 #[serde(default)]
618 pub open_interest_value: Option<String>,
619 #[serde(default)]
620 pub turnover24h: Option<String>,
621 #[serde(default)]
622 pub volume24h: Option<String>,
623 #[serde(default)]
624 pub next_funding_time: Option<String>,
625 #[serde(default)]
626 pub funding_rate: Option<String>,
627 #[serde(default)]
628 pub bid1_price: Option<String>,
629 #[serde(default)]
630 pub bid1_size: Option<String>,
631 #[serde(default)]
632 pub ask1_price: Option<String>,
633 #[serde(default)]
634 pub ask1_size: Option<String>,
635 #[serde(default)]
636 pub funding_interval_hour: Option<String>,
637}
638
639#[derive(Clone, Debug, Serialize, Deserialize)]
641#[serde(rename_all = "camelCase")]
642pub struct BybitWsTickerLinearMsg {
643 pub topic: Ustr,
644 #[serde(rename = "type")]
645 pub msg_type: Ustr,
646 pub ts: i64,
647 #[serde(default)]
648 pub cs: Option<i64>,
649 pub data: BybitWsTickerLinear,
650}
651
652#[derive(Clone, Debug, Serialize, Deserialize)]
654#[serde(rename_all = "camelCase")]
655pub struct BybitWsTickerOption {
656 pub symbol: Ustr,
657 pub bid_price: String,
658 pub bid_size: String,
659 pub bid_iv: String,
660 pub ask_price: String,
661 pub ask_size: String,
662 pub ask_iv: String,
663 pub last_price: String,
664 pub high_price24h: String,
665 pub low_price24h: String,
666 pub mark_price: String,
667 pub index_price: String,
668 pub mark_price_iv: String,
669 pub underlying_price: String,
670 pub open_interest: String,
671 pub turnover24h: String,
672 pub volume24h: String,
673 pub total_volume: String,
674 pub total_turnover: String,
675 pub delta: String,
676 pub gamma: String,
677 pub vega: String,
678 pub theta: String,
679 pub predicted_delivery_price: String,
680 pub change24h: String,
681}
682
683#[derive(Clone, Debug, Serialize, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct BybitWsTickerOptionMsg {
687 #[serde(default)]
688 pub id: Option<String>,
689 pub topic: Ustr,
690 #[serde(rename = "type")]
691 pub msg_type: Ustr,
692 pub ts: i64,
693 pub data: BybitWsTickerOption,
694}
695
696#[derive(Clone, Debug, Serialize, Deserialize)]
698pub struct BybitWsTrade {
699 #[serde(rename = "T")]
700 pub t: i64,
701 #[serde(rename = "s")]
702 pub s: Ustr,
703 #[serde(rename = "S")]
704 pub taker_side: BybitOrderSide,
705 #[serde(rename = "v")]
706 pub v: String,
707 #[serde(rename = "p")]
708 pub p: String,
709 #[serde(rename = "i")]
710 pub i: String,
711 #[serde(rename = "BT")]
712 pub bt: bool,
713 #[serde(rename = "L")]
714 #[serde(default)]
715 pub l: Option<String>,
716 #[serde(rename = "id")]
717 #[serde(default)]
718 pub id: Option<Ustr>,
719 #[serde(rename = "mP")]
720 #[serde(default)]
721 pub m_p: Option<String>,
722 #[serde(rename = "iP")]
723 #[serde(default)]
724 pub i_p: Option<String>,
725 #[serde(rename = "mIv")]
726 #[serde(default)]
727 pub m_iv: Option<String>,
728 #[serde(rename = "iv")]
729 #[serde(default)]
730 pub iv: Option<String>,
731}
732
733#[derive(Clone, Debug, Serialize, Deserialize)]
735#[serde(rename_all = "camelCase")]
736pub struct BybitWsTradeMsg {
737 pub topic: Ustr,
738 #[serde(rename = "type")]
739 pub msg_type: Ustr,
740 pub ts: i64,
741 pub data: Vec<BybitWsTrade>,
742}
743
744#[derive(Clone, Debug, Serialize, Deserialize)]
746#[serde(rename_all = "camelCase")]
747pub struct BybitWsAccountOrder {
748 pub category: BybitProductType,
749 pub symbol: Ustr,
750 pub order_id: Ustr,
751 pub side: BybitOrderSide,
752 pub order_type: BybitOrderType,
753 pub cancel_type: BybitCancelType,
754 pub price: String,
755 pub qty: String,
756 pub order_iv: String,
757 pub time_in_force: BybitTimeInForce,
758 pub order_status: BybitOrderStatus,
759 pub order_link_id: Ustr,
760 pub last_price_on_created: Ustr,
761 pub reduce_only: bool,
762 pub leaves_qty: String,
763 pub leaves_value: String,
764 pub cum_exec_qty: String,
765 pub cum_exec_value: String,
766 pub avg_price: String,
767 pub block_trade_id: Ustr,
768 pub position_idx: i32,
769 pub cum_exec_fee: String,
770 pub created_time: String,
771 pub updated_time: String,
772 pub reject_reason: Ustr,
773 pub trigger_price: String,
774 pub take_profit: String,
775 pub stop_loss: String,
776 pub tp_trigger_by: BybitTriggerType,
777 pub sl_trigger_by: BybitTriggerType,
778 pub tp_limit_price: String,
779 pub sl_limit_price: String,
780 pub close_on_trigger: bool,
781 pub place_type: Ustr,
782 pub smp_type: BybitSmpType,
783 pub smp_group: i32,
784 pub smp_order_id: Ustr,
785 pub fee_currency: Ustr,
786 pub trigger_by: BybitTriggerType,
787 pub stop_order_type: BybitStopOrderType,
788 pub trigger_direction: BybitTriggerDirection,
789 #[serde(default)]
790 pub tpsl_mode: Option<BybitTpSlMode>,
791 #[serde(default)]
792 pub create_type: Option<BybitCreateType>,
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub struct BybitWsAccountOrderMsg {
799 pub topic: Ustr,
800 pub id: String,
801 pub creation_time: i64,
802 pub data: Vec<BybitWsAccountOrder>,
803}
804
805#[derive(Clone, Debug, Serialize, Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct BybitWsAccountExecution {
809 pub category: BybitProductType,
810 pub symbol: Ustr,
811 pub exec_fee: String,
812 pub exec_id: String,
813 pub exec_price: String,
814 pub exec_qty: String,
815 pub exec_type: BybitExecType,
816 pub exec_value: String,
817 pub is_maker: bool,
818 pub fee_rate: String,
819 pub trade_iv: String,
820 pub mark_iv: String,
821 pub block_trade_id: Ustr,
822 pub mark_price: String,
823 pub index_price: String,
824 pub underlying_price: String,
825 pub leaves_qty: String,
826 pub order_id: Ustr,
827 pub order_link_id: Ustr,
828 pub order_price: String,
829 pub order_qty: String,
830 pub order_type: BybitOrderType,
831 pub side: BybitOrderSide,
832 pub exec_time: String,
833 pub is_leverage: String,
834 pub closed_size: String,
835 pub seq: i64,
836 pub stop_order_type: BybitStopOrderType,
837}
838
839#[derive(Clone, Debug, Serialize, Deserialize)]
841#[serde(rename_all = "camelCase")]
842pub struct BybitWsAccountExecutionMsg {
843 pub topic: Ustr,
844 pub id: String,
845 pub creation_time: i64,
846 pub data: Vec<BybitWsAccountExecution>,
847}
848
849#[derive(Clone, Debug, Serialize, Deserialize)]
851#[serde(rename_all = "camelCase")]
852pub struct BybitWsAccountWalletCoin {
853 pub coin: Ustr,
854 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
855 pub wallet_balance: Decimal,
856 pub available_to_withdraw: String,
857 pub available_to_borrow: String,
858 pub accrued_interest: String,
859 #[serde(
860 default,
861 rename = "totalOrderIM",
862 deserialize_with = "deserialize_optional_decimal_or_zero"
863 )]
864 pub total_order_im: Decimal,
865 #[serde(
866 default,
867 rename = "totalPositionIM",
868 deserialize_with = "deserialize_optional_decimal_or_zero"
869 )]
870 pub total_position_im: Decimal,
871 #[serde(default, rename = "totalPositionMM")]
872 pub total_position_mm: Option<String>,
873 pub equity: String,
874 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
875 pub spot_borrow: Decimal,
876}
877
878#[derive(Clone, Debug, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881pub struct BybitWsAccountWallet {
882 pub total_wallet_balance: String,
883 pub total_equity: String,
884 pub total_available_balance: String,
885 pub total_margin_balance: String,
886 pub total_initial_margin: String,
887 pub total_maintenance_margin: String,
888 #[serde(rename = "accountIMRate")]
889 pub account_im_rate: String,
890 #[serde(rename = "accountMMRate")]
891 pub account_mm_rate: String,
892 #[serde(rename = "accountLTV")]
893 pub account_ltv: String,
894 pub coin: Vec<BybitWsAccountWalletCoin>,
895}
896
897#[derive(Clone, Debug, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct BybitWsAccountWalletMsg {
901 pub topic: Ustr,
902 pub id: String,
903 pub creation_time: i64,
904 pub data: Vec<BybitWsAccountWallet>,
905}
906
907#[derive(Clone, Debug, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct BybitWsAccountPosition {
911 pub category: BybitProductType,
912 pub symbol: Ustr,
913 pub side: BybitPositionSide,
914 pub size: String,
915 pub position_idx: i32,
916 pub trade_mode: i32,
917 pub position_value: String,
918 pub risk_id: i64,
919 pub risk_limit_value: String,
920 #[serde(deserialize_with = "deserialize_optional_decimal_str")]
921 pub entry_price: Option<Decimal>,
922 pub mark_price: String,
923 pub leverage: String,
924 pub position_balance: String,
925 pub auto_add_margin: i32,
926 #[serde(rename = "positionIM")]
927 pub position_im: String,
928 #[serde(rename = "positionIMByMp")]
929 pub position_im_by_mp: String,
930 #[serde(rename = "positionMM")]
931 pub position_mm: String,
932 #[serde(rename = "positionMMByMp")]
933 pub position_mm_by_mp: String,
934 pub liq_price: String,
935 pub bust_price: String,
936 pub tpsl_mode: BybitTpSlMode,
937 pub take_profit: String,
938 pub stop_loss: String,
939 pub trailing_stop: String,
940 pub unrealised_pnl: String,
941 pub session_avg_price: String,
942 pub cur_realised_pnl: String,
943 pub cum_realised_pnl: String,
944 pub position_status: BybitPositionStatus,
945 pub adl_rank_indicator: i32,
946 pub created_time: String,
947 pub updated_time: String,
948 #[serde(default = "default_ws_position_seq")]
949 pub seq: i64,
950 #[serde(default)]
951 pub is_reduce_only: bool,
952 #[serde(default)]
953 pub mmr_sys_updated_time: String,
954 #[serde(default)]
955 pub leverage_sys_updated_time: String,
956}
957
958const fn default_ws_position_seq() -> i64 {
959 -1
960}
961
962#[derive(Clone, Debug, Serialize, Deserialize)]
964#[serde(rename_all = "camelCase")]
965pub struct BybitWsAccountPositionMsg {
966 pub topic: Ustr,
967 pub id: String,
968 pub creation_time: i64,
969 pub data: Vec<BybitWsAccountPosition>,
970}
971
972#[cfg(test)]
973mod tests {
974 use rstest::rstest;
975
976 use super::*;
977 use crate::common::testing::load_test_json;
978
979 #[rstest]
980 fn serialize_place_params_includes_order_iv_when_set() {
981 let params = BybitWsPlaceOrderParams {
982 category: BybitProductType::Option,
983 symbol: Ustr::from("BTC-30JUN25-100000-C"),
984 side: BybitOrderSide::Buy,
985 order_type: BybitOrderType::Limit,
986 qty: "0.1".to_string(),
987 is_leverage: None,
988 market_unit: None,
989 price: Some("500".to_string()),
990 time_in_force: Some(BybitTimeInForce::Gtc),
991 order_link_id: Some("test-1".to_string()),
992 reduce_only: None,
993 close_on_trigger: None,
994 trigger_price: None,
995 trigger_by: None,
996 trigger_direction: None,
997 tpsl_mode: None,
998 take_profit: None,
999 stop_loss: None,
1000 tp_trigger_by: None,
1001 sl_trigger_by: None,
1002 sl_trigger_price: None,
1003 tp_trigger_price: None,
1004 sl_order_type: None,
1005 tp_order_type: None,
1006 sl_limit_price: None,
1007 tp_limit_price: None,
1008 order_iv: Some("0.80".to_string()),
1009 mmp: Some(true),
1010 position_idx: None,
1011 };
1012
1013 let json = serde_json::to_string(¶ms).unwrap();
1014 assert!(json.contains("\"orderIv\":\"0.80\""));
1015 assert!(json.contains("\"mmp\":true"));
1016 }
1017
1018 #[rstest]
1019 fn serialize_place_params_omits_order_iv_when_none() {
1020 let params = BybitWsPlaceOrderParams {
1021 category: BybitProductType::Linear,
1022 symbol: Ustr::from("BTCUSDT"),
1023 side: BybitOrderSide::Buy,
1024 order_type: BybitOrderType::Limit,
1025 qty: "0.01".to_string(),
1026 is_leverage: None,
1027 market_unit: None,
1028 price: Some("50000".to_string()),
1029 time_in_force: Some(BybitTimeInForce::Gtc),
1030 order_link_id: None,
1031 reduce_only: None,
1032 close_on_trigger: None,
1033 trigger_price: None,
1034 trigger_by: None,
1035 trigger_direction: None,
1036 tpsl_mode: None,
1037 take_profit: None,
1038 stop_loss: None,
1039 tp_trigger_by: None,
1040 sl_trigger_by: None,
1041 sl_trigger_price: None,
1042 tp_trigger_price: None,
1043 sl_order_type: None,
1044 tp_order_type: None,
1045 sl_limit_price: None,
1046 tp_limit_price: None,
1047 order_iv: None,
1048 mmp: None,
1049 position_idx: None,
1050 };
1051
1052 let json = serde_json::to_string(¶ms).unwrap();
1053 assert!(!json.contains("orderIv"));
1054 assert!(!json.contains("mmp"));
1055 assert!(!json.contains("positionIdx"));
1056 }
1057
1058 #[rstest]
1059 #[case(BybitPositionIdx::BuyHedge, 1)]
1060 #[case(BybitPositionIdx::SellHedge, 2)]
1061 fn serialize_place_params_includes_position_idx_when_set(
1062 #[case] idx: BybitPositionIdx,
1063 #[case] expected: i32,
1064 ) {
1065 let params = BybitWsPlaceOrderParams {
1066 category: BybitProductType::Linear,
1067 symbol: Ustr::from("BTCUSDT"),
1068 side: BybitOrderSide::Buy,
1069 order_type: BybitOrderType::Limit,
1070 qty: "0.01".to_string(),
1071 is_leverage: None,
1072 market_unit: None,
1073 price: Some("50000".to_string()),
1074 time_in_force: Some(BybitTimeInForce::Gtc),
1075 order_link_id: None,
1076 reduce_only: None,
1077 close_on_trigger: None,
1078 trigger_price: None,
1079 trigger_by: None,
1080 trigger_direction: None,
1081 tpsl_mode: None,
1082 take_profit: None,
1083 stop_loss: None,
1084 tp_trigger_by: None,
1085 sl_trigger_by: None,
1086 sl_trigger_price: None,
1087 tp_trigger_price: None,
1088 sl_order_type: None,
1089 tp_order_type: None,
1090 sl_limit_price: None,
1091 tp_limit_price: None,
1092 order_iv: None,
1093 mmp: None,
1094 position_idx: Some(idx),
1095 };
1096
1097 let json = serde_json::to_string(¶ms).unwrap();
1098 assert!(json.contains(&format!("\"positionIdx\":{expected}")));
1099 }
1100
1101 #[rstest]
1102 #[case(None)]
1103 #[case(Some(BybitPositionIdx::OneWay))]
1104 #[case(Some(BybitPositionIdx::BuyHedge))]
1105 #[case(Some(BybitPositionIdx::SellHedge))]
1106 fn place_params_position_idx_roundtrip(#[case] idx: Option<BybitPositionIdx>) {
1107 let params = BybitWsPlaceOrderParams {
1108 category: BybitProductType::Linear,
1109 symbol: Ustr::from("BTCUSDT"),
1110 side: BybitOrderSide::Buy,
1111 order_type: BybitOrderType::Limit,
1112 qty: "0.01".to_string(),
1113 is_leverage: None,
1114 market_unit: None,
1115 price: Some("50000".to_string()),
1116 time_in_force: Some(BybitTimeInForce::Gtc),
1117 order_link_id: None,
1118 reduce_only: None,
1119 close_on_trigger: None,
1120 trigger_price: None,
1121 trigger_by: None,
1122 trigger_direction: None,
1123 tpsl_mode: None,
1124 take_profit: None,
1125 stop_loss: None,
1126 tp_trigger_by: None,
1127 sl_trigger_by: None,
1128 sl_trigger_price: None,
1129 tp_trigger_price: None,
1130 sl_order_type: None,
1131 tp_order_type: None,
1132 sl_limit_price: None,
1133 tp_limit_price: None,
1134 order_iv: None,
1135 mmp: None,
1136 position_idx: idx,
1137 };
1138
1139 let json = serde_json::to_string(¶ms).unwrap();
1140 let decoded: BybitWsPlaceOrderParams = serde_json::from_str(&json).unwrap();
1141 assert_eq!(decoded.position_idx, idx);
1142 }
1143
1144 #[rstest]
1145 fn serialize_amend_params_includes_order_iv_when_set() {
1146 let params = BybitWsAmendOrderParams {
1147 category: BybitProductType::Option,
1148 symbol: Ustr::from("BTC-30JUN25-100000-C"),
1149 order_id: None,
1150 order_link_id: Some("test-1".to_string()),
1151 qty: None,
1152 price: None,
1153 trigger_price: None,
1154 take_profit: None,
1155 stop_loss: None,
1156 tp_trigger_by: None,
1157 sl_trigger_by: None,
1158 order_iv: Some("0.90".to_string()),
1159 };
1160
1161 let json = serde_json::to_string(¶ms).unwrap();
1162 assert!(json.contains("\"orderIv\":\"0.90\""));
1163 }
1164
1165 #[rstest]
1166 fn deserialize_account_order_frame_uses_enums() {
1167 let json = load_test_json("ws_account_order.json");
1168 let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
1169 let order = &frame.data[0];
1170
1171 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1172 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1173 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1174 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1175 assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
1176 assert_eq!(order.side, BybitOrderSide::Buy);
1177 }
1178
1179 #[rstest]
1180 fn deserialize_ws_account_position_without_conditional_fields() {
1181 let json = r#"{
1185 "topic": "position",
1186 "id": "1",
1187 "creationTime": 1697673900000,
1188 "data": [{
1189 "category": "linear",
1190 "symbol": "LTCUSDT",
1191 "side": "",
1192 "size": "0",
1193 "positionIdx": 0,
1194 "tradeMode": 0,
1195 "positionValue": "0",
1196 "riskId": 1,
1197 "riskLimitValue": "150",
1198 "entryPrice": "",
1199 "markPrice": "70.00",
1200 "leverage": "10",
1201 "positionBalance": "0",
1202 "autoAddMargin": 0,
1203 "positionIM": "0",
1204 "positionIMByMp": "0",
1205 "positionMM": "0",
1206 "positionMMByMp": "0",
1207 "liqPrice": "",
1208 "bustPrice": "",
1209 "tpslMode": "Full",
1210 "takeProfit": "0",
1211 "stopLoss": "0",
1212 "trailingStop": "0",
1213 "unrealisedPnl": "0",
1214 "sessionAvgPrice": "0",
1215 "curRealisedPnl": "0",
1216 "cumRealisedPnl": "0",
1217 "positionStatus": "Normal",
1218 "adlRankIndicator": 0,
1219 "createdTime": "1676538056258",
1220 "updatedTime": "1697673600012"
1221 }]
1222 }"#;
1223
1224 let msg: BybitWsAccountPositionMsg = serde_json::from_str(json)
1225 .expect("Failed to parse WS account position with missing conditional fields");
1226 let position = &msg.data[0];
1227
1228 assert!(!position.is_reduce_only);
1229 assert_eq!(position.seq, -1);
1230 assert_eq!(position.mmr_sys_updated_time, "");
1231 assert_eq!(position.leverage_sys_updated_time, "");
1232 }
1233}