1use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use ustr::Ustr;
21
22use crate::{
23 common::{
24 enums::{
25 BybitBboSideType, BybitCancelType, BybitCreateType, BybitExecType, BybitMarketUnit,
26 BybitOrderSide, 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 AccountExecutionFast(BybitWsAccountExecutionFastMsg),
85 AccountWallet(BybitWsAccountWalletMsg),
87 AccountPosition(BybitWsAccountPositionMsg),
89 Unknown(Value),
91 Reconnected,
93}
94
95#[derive(Debug, Clone)]
97pub enum BybitWsMessage {
98 Auth(BybitWsAuthResponse),
100 OrderResponse(BybitWsOrderResponse),
102 Orderbook(BybitWsOrderbookDepthMsg),
104 Trade(BybitWsTradeMsg),
106 Kline(BybitWsKlineMsg),
108 TickerLinear(BybitWsTickerLinearMsg),
110 TickerOption(BybitWsTickerOptionMsg),
112 AccountOrder(BybitWsAccountOrderMsg),
114 AccountExecution(BybitWsAccountExecutionMsg),
116 AccountExecutionFast(BybitWsAccountExecutionFastMsg),
118 AccountWallet(BybitWsAccountWalletMsg),
120 AccountPosition(BybitWsAccountPositionMsg),
122 Error(BybitWebSocketError),
124 Reconnected,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
132#[cfg_attr(
133 feature = "python",
134 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
135)]
136pub struct BybitWebSocketError {
137 pub code: i64,
139 pub message: String,
141 #[serde(default)]
143 pub conn_id: Option<String>,
144 #[serde(default)]
146 pub topic: Option<String>,
147 #[serde(default)]
149 pub req_id: Option<String>,
150}
151
152impl BybitWebSocketError {
153 #[must_use]
155 pub fn new(code: i64, message: impl Into<String>) -> Self {
156 Self {
157 code,
158 message: message.into(),
159 conn_id: None,
160 topic: None,
161 req_id: None,
162 }
163 }
164
165 #[must_use]
167 pub fn from_response(response: &BybitWsResponse) -> Self {
168 let message = response.ret_msg.clone().unwrap_or_else(|| {
170 let mut parts = vec![];
171
172 if let Some(op) = &response.op {
173 parts.push(format!("op={op}"));
174 }
175
176 if let Some(topic) = &response.topic {
177 parts.push(format!("topic={topic}"));
178 }
179
180 if let Some(success) = response.success {
181 parts.push(format!("success={success}"));
182 }
183
184 if parts.is_empty() {
185 "Bybit websocket error (no error message provided)".to_string()
186 } else {
187 format!("Bybit websocket error: {}", parts.join(", "))
188 }
189 });
190
191 Self {
192 code: response.ret_code.unwrap_or_default(),
193 message,
194 conn_id: response.conn_id.clone(),
195 topic: response.topic.map(|t| t.to_string()),
196 req_id: response.req_id.clone(),
197 }
198 }
199
200 #[must_use]
202 pub fn from_message(message: impl Into<String>) -> Self {
203 Self::new(-1, message)
204 }
205}
206
207#[derive(Debug, Clone, Serialize)]
209#[serde(rename_all = "camelCase")]
210pub struct BybitWsRequest<T> {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub req_id: Option<String>,
214 pub op: BybitWsOrderRequestOp,
216 pub header: BybitWsHeader,
218 pub args: Vec<T>,
220}
221
222#[derive(Debug, Clone, Serialize)]
224#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
225pub struct BybitWsHeader {
226 pub x_bapi_timestamp: String,
228 #[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
230 pub referer: Option<String>,
231}
232
233impl BybitWsHeader {
234 #[must_use]
236 pub fn now() -> Self {
237 Self::with_referer(None)
238 }
239
240 #[must_use]
242 pub fn with_referer(referer: Option<String>) -> Self {
243 use nautilus_core::time::get_atomic_clock_realtime;
244 Self {
245 x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
246 referer,
247 }
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct BybitWsPlaceOrderParams {
255 pub category: BybitProductType,
256 pub symbol: Ustr,
257 pub side: BybitOrderSide,
258 pub order_type: BybitOrderType,
259 pub qty: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub is_leverage: Option<i32>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub market_unit: Option<BybitMarketUnit>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub price: Option<String>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub time_in_force: Option<BybitTimeInForce>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub order_link_id: Option<String>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub reduce_only: Option<bool>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub close_on_trigger: Option<bool>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub trigger_price: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub trigger_by: Option<BybitTriggerType>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub trigger_direction: Option<i32>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub tpsl_mode: Option<BybitTpSlMode>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub take_profit: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub stop_loss: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub tp_trigger_by: Option<BybitTriggerType>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub sl_trigger_by: Option<BybitTriggerType>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub sl_trigger_price: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub tp_trigger_price: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub sl_order_type: Option<BybitOrderType>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub tp_order_type: Option<BybitOrderType>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub sl_limit_price: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub tp_limit_price: Option<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub order_iv: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub mmp: Option<bool>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub position_idx: Option<BybitPositionIdx>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub bbo_side_type: Option<BybitBboSideType>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub bbo_level: Option<String>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct BybitWsAmendOrderParams {
318 pub category: BybitProductType,
319 pub symbol: Ustr,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub order_id: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub order_link_id: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub qty: Option<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub price: Option<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub trigger_price: Option<String>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub take_profit: Option<String>,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub stop_loss: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub tp_trigger_by: Option<BybitTriggerType>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub sl_trigger_by: Option<BybitTriggerType>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub order_iv: Option<String>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct BybitWsCancelOrderParams {
346 pub category: BybitProductType,
347 pub symbol: Ustr,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub order_id: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub order_link_id: Option<String>,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct BybitWsBatchCancelItem {
358 pub symbol: Ustr,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub order_id: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub order_link_id: Option<String>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct BybitWsBatchCancelOrderArgs {
368 pub category: BybitProductType,
369 pub request: Vec<BybitWsBatchCancelItem>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct BybitWsBatchPlaceItem {
376 pub symbol: Ustr,
377 pub side: BybitOrderSide,
378 pub order_type: BybitOrderType,
379 pub qty: String,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub is_leverage: Option<i32>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub market_unit: Option<BybitMarketUnit>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub price: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub time_in_force: Option<BybitTimeInForce>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub order_link_id: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub reduce_only: Option<bool>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub close_on_trigger: Option<bool>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub trigger_price: Option<String>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub trigger_by: Option<BybitTriggerType>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub trigger_direction: Option<i32>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub tpsl_mode: Option<BybitTpSlMode>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub take_profit: Option<String>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub stop_loss: Option<String>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub tp_trigger_by: Option<BybitTriggerType>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub sl_trigger_by: Option<BybitTriggerType>,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub sl_trigger_price: Option<String>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub tp_trigger_price: Option<String>,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub sl_order_type: Option<BybitOrderType>,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub tp_order_type: Option<BybitOrderType>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub sl_limit_price: Option<String>,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub tp_limit_price: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub order_iv: Option<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub mmp: Option<bool>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub position_idx: Option<BybitPositionIdx>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub bbo_side_type: Option<BybitBboSideType>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub bbo_level: Option<String>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct BybitWsBatchPlaceOrderArgs {
437 pub category: BybitProductType,
438 pub request: Vec<BybitWsBatchPlaceItem>,
439}
440
441#[derive(Clone, Debug, Serialize, Deserialize)]
443pub struct BybitWsSubscriptionMsg {
444 pub success: bool,
445 pub op: BybitWsOperation,
446 #[serde(default)]
447 pub conn_id: Option<String>,
448 #[serde(default)]
449 pub req_id: Option<String>,
450 #[serde(default)]
451 pub ret_msg: Option<String>,
452}
453
454#[derive(Clone, Debug, Serialize, Deserialize)]
456pub struct BybitWsResponse {
457 #[serde(default)]
458 pub op: Option<BybitWsOperation>,
459 #[serde(default)]
460 pub topic: Option<Ustr>,
461 #[serde(default)]
462 pub success: Option<bool>,
463 #[serde(default)]
464 pub conn_id: Option<String>,
465 #[serde(default)]
466 pub req_id: Option<String>,
467 #[serde(default)]
468 pub ret_code: Option<i64>,
469 #[serde(default)]
470 pub ret_msg: Option<String>,
471}
472
473#[derive(Clone, Debug, Serialize, Deserialize)]
475#[serde(rename_all = "camelCase")]
476pub struct BybitWsOrderResponse {
477 pub op: Ustr,
479 #[serde(default)]
481 pub conn_id: Option<String>,
482 pub ret_code: i64,
484 pub ret_msg: String,
486 #[serde(default)]
488 pub data: Value,
489 #[serde(default)]
491 pub req_id: Option<String>,
492 #[serde(default)]
494 pub header: Option<Value>,
495 #[serde(default)]
497 pub ret_ext_info: Option<Value>,
498}
499
500impl BybitWsOrderResponse {
501 #[must_use]
506 pub fn extract_batch_errors(&self) -> Vec<BybitBatchOrderError> {
507 self.ret_ext_info
508 .as_ref()
509 .and_then(|ext| ext.get("list"))
510 .and_then(|list| list.as_array())
511 .map(|arr| {
512 arr.iter()
513 .filter_map(|item| {
514 let code = item.get("code")?.as_i64()?;
515 let msg = item.get("msg")?.as_str()?.to_string();
516 Some(BybitBatchOrderError { code, msg })
517 })
518 .collect()
519 })
520 .unwrap_or_default()
521 }
522}
523
524#[derive(Clone, Debug)]
526pub struct BybitBatchOrderError {
527 pub code: i64,
529 pub msg: String,
531}
532
533#[derive(Clone, Debug, Serialize, Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct BybitWsAuthResponse {
537 pub op: BybitWsOperation,
538 #[serde(default)]
539 pub conn_id: Option<String>,
540 #[serde(default)]
541 pub ret_code: Option<i64>,
542 #[serde(default)]
543 pub ret_msg: Option<String>,
544 #[serde(default)]
545 pub success: Option<bool>,
546}
547
548#[derive(Clone, Debug, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct BybitWsKline {
552 pub start: i64,
553 pub end: i64,
554 pub interval: Ustr,
555 pub open: String,
556 pub close: String,
557 pub high: String,
558 pub low: String,
559 pub volume: String,
560 pub turnover: String,
561 pub confirm: bool,
562 pub timestamp: i64,
563}
564
565#[derive(Clone, Debug, Serialize, Deserialize)]
567#[serde(rename_all = "camelCase")]
568pub struct BybitWsKlineMsg {
569 pub topic: Ustr,
570 pub ts: i64,
571 #[serde(rename = "type")]
572 pub msg_type: Ustr,
573 pub data: Vec<BybitWsKline>,
574}
575
576#[derive(Clone, Debug, Serialize, Deserialize)]
578pub struct BybitWsOrderbookDepth {
579 pub s: Ustr,
581 pub b: Vec<Vec<String>>,
583 pub a: Vec<Vec<String>>,
585 pub u: i64,
587 pub seq: i64,
589}
590
591#[derive(Clone, Debug, Serialize, Deserialize)]
593#[serde(rename_all = "camelCase")]
594pub struct BybitWsOrderbookDepthMsg {
595 pub topic: Ustr,
596 #[serde(rename = "type")]
597 pub msg_type: Ustr,
598 pub ts: i64,
599 pub data: BybitWsOrderbookDepth,
600 #[serde(default)]
601 pub cts: Option<i64>,
602}
603
604#[derive(Clone, Debug, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct BybitWsTickerLinear {
608 pub symbol: Ustr,
609 #[serde(default)]
610 pub tick_direction: Option<String>,
611 #[serde(default)]
612 pub price24h_pcnt: Option<String>,
613 #[serde(default)]
614 pub last_price: Option<String>,
615 #[serde(default)]
616 pub prev_price24h: Option<String>,
617 #[serde(default)]
618 pub high_price24h: Option<String>,
619 #[serde(default)]
620 pub low_price24h: Option<String>,
621 #[serde(default)]
622 pub prev_price1h: Option<String>,
623 #[serde(default)]
624 pub mark_price: Option<String>,
625 #[serde(default)]
626 pub index_price: Option<String>,
627 #[serde(default)]
628 pub open_interest: Option<String>,
629 #[serde(default)]
630 pub open_interest_value: Option<String>,
631 #[serde(default)]
632 pub turnover24h: Option<String>,
633 #[serde(default)]
634 pub volume24h: Option<String>,
635 #[serde(default)]
636 pub next_funding_time: Option<String>,
637 #[serde(default)]
638 pub funding_rate: Option<String>,
639 #[serde(default)]
640 pub bid1_price: Option<String>,
641 #[serde(default)]
642 pub bid1_size: Option<String>,
643 #[serde(default)]
644 pub ask1_price: Option<String>,
645 #[serde(default)]
646 pub ask1_size: Option<String>,
647 #[serde(default)]
648 pub funding_interval_hour: Option<String>,
649}
650
651#[derive(Clone, Debug, Serialize, Deserialize)]
653#[serde(rename_all = "camelCase")]
654pub struct BybitWsTickerLinearMsg {
655 pub topic: Ustr,
656 #[serde(rename = "type")]
657 pub msg_type: Ustr,
658 pub ts: i64,
659 #[serde(default)]
660 pub cs: Option<i64>,
661 pub data: BybitWsTickerLinear,
662}
663
664#[derive(Clone, Debug, Serialize, Deserialize)]
666#[serde(rename_all = "camelCase")]
667pub struct BybitWsTickerOption {
668 pub symbol: Ustr,
669 pub bid_price: String,
670 pub bid_size: String,
671 pub bid_iv: String,
672 pub ask_price: String,
673 pub ask_size: String,
674 pub ask_iv: String,
675 pub last_price: String,
676 pub high_price24h: String,
677 pub low_price24h: String,
678 pub mark_price: String,
679 pub index_price: String,
680 pub mark_price_iv: String,
681 pub underlying_price: String,
682 pub open_interest: String,
683 pub turnover24h: String,
684 pub volume24h: String,
685 pub total_volume: String,
686 pub total_turnover: String,
687 pub delta: String,
688 pub gamma: String,
689 pub vega: String,
690 pub theta: String,
691 pub predicted_delivery_price: String,
692 pub change24h: String,
693}
694
695#[derive(Clone, Debug, Serialize, Deserialize)]
697#[serde(rename_all = "camelCase")]
698pub struct BybitWsTickerOptionMsg {
699 #[serde(default)]
700 pub id: Option<String>,
701 pub topic: Ustr,
702 #[serde(rename = "type")]
703 pub msg_type: Ustr,
704 pub ts: i64,
705 pub data: BybitWsTickerOption,
706}
707
708#[derive(Clone, Debug, Serialize, Deserialize)]
710pub struct BybitWsTrade {
711 #[serde(rename = "T")]
712 pub t: i64,
713 #[serde(rename = "s")]
714 pub s: Ustr,
715 #[serde(rename = "S")]
716 pub taker_side: BybitOrderSide,
717 #[serde(rename = "v")]
718 pub v: String,
719 #[serde(rename = "p")]
720 pub p: String,
721 #[serde(rename = "i")]
722 pub i: String,
723 #[serde(rename = "BT")]
724 pub bt: bool,
725 #[serde(rename = "L")]
726 #[serde(default)]
727 pub l: Option<String>,
728 #[serde(rename = "id")]
729 #[serde(default)]
730 pub id: Option<Ustr>,
731 #[serde(rename = "mP")]
732 #[serde(default)]
733 pub m_p: Option<String>,
734 #[serde(rename = "iP")]
735 #[serde(default)]
736 pub i_p: Option<String>,
737 #[serde(rename = "mIv")]
738 #[serde(default)]
739 pub m_iv: Option<String>,
740 #[serde(rename = "iv")]
741 #[serde(default)]
742 pub iv: Option<String>,
743}
744
745#[derive(Clone, Debug, Serialize, Deserialize)]
747#[serde(rename_all = "camelCase")]
748pub struct BybitWsTradeMsg {
749 pub topic: Ustr,
750 #[serde(rename = "type")]
751 pub msg_type: Ustr,
752 pub ts: i64,
753 pub data: Vec<BybitWsTrade>,
754}
755
756#[derive(Clone, Debug, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct BybitWsAccountOrder {
760 pub category: BybitProductType,
761 pub symbol: Ustr,
762 pub order_id: Ustr,
763 pub side: BybitOrderSide,
764 pub order_type: BybitOrderType,
765 pub cancel_type: BybitCancelType,
766 pub price: String,
767 pub qty: String,
768 pub order_iv: String,
769 pub time_in_force: BybitTimeInForce,
770 pub order_status: BybitOrderStatus,
771 pub order_link_id: Ustr,
772 pub last_price_on_created: Ustr,
773 pub reduce_only: bool,
774 pub leaves_qty: String,
775 pub leaves_value: String,
776 pub cum_exec_qty: String,
777 pub cum_exec_value: String,
778 pub avg_price: String,
779 pub block_trade_id: Ustr,
780 pub position_idx: i32,
781 pub cum_exec_fee: String,
782 pub created_time: String,
783 pub updated_time: String,
784 pub reject_reason: Ustr,
785 pub trigger_price: String,
786 pub take_profit: String,
787 pub stop_loss: String,
788 pub tp_trigger_by: BybitTriggerType,
789 pub sl_trigger_by: BybitTriggerType,
790 pub tp_limit_price: String,
791 pub sl_limit_price: String,
792 pub close_on_trigger: bool,
793 pub place_type: Ustr,
794 pub smp_type: BybitSmpType,
795 pub smp_group: i32,
796 pub smp_order_id: Ustr,
797 pub fee_currency: Ustr,
798 pub trigger_by: BybitTriggerType,
799 pub stop_order_type: BybitStopOrderType,
800 pub trigger_direction: BybitTriggerDirection,
801 #[serde(default)]
802 pub tpsl_mode: Option<BybitTpSlMode>,
803 #[serde(default)]
804 pub create_type: Option<BybitCreateType>,
805}
806
807#[derive(Clone, Debug, Serialize, Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct BybitWsAccountOrderMsg {
811 pub topic: Ustr,
812 pub id: String,
813 pub creation_time: i64,
814 pub data: Vec<BybitWsAccountOrder>,
815}
816
817#[derive(Clone, Debug, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct BybitWsAccountExecution {
821 pub category: BybitProductType,
822 pub symbol: Ustr,
823 pub exec_fee: String,
824 pub exec_id: String,
825 pub exec_price: String,
826 pub exec_qty: String,
827 pub exec_type: BybitExecType,
828 pub exec_value: String,
829 pub is_maker: bool,
830 pub fee_rate: String,
831 pub trade_iv: String,
832 pub mark_iv: String,
833 pub block_trade_id: Ustr,
834 pub mark_price: String,
835 pub index_price: String,
836 pub underlying_price: String,
837 pub leaves_qty: String,
838 pub order_id: Ustr,
839 pub order_link_id: Ustr,
840 pub order_price: String,
841 pub order_qty: String,
842 pub order_type: BybitOrderType,
843 pub side: BybitOrderSide,
844 pub exec_time: String,
845 pub is_leverage: String,
846 pub closed_size: String,
847 pub seq: i64,
848 pub stop_order_type: BybitStopOrderType,
849}
850
851#[derive(Clone, Debug, Serialize, Deserialize)]
853#[serde(rename_all = "camelCase")]
854pub struct BybitWsAccountExecutionMsg {
855 pub topic: Ustr,
856 pub id: String,
857 pub creation_time: i64,
858 pub data: Vec<BybitWsAccountExecution>,
859}
860
861#[derive(Clone, Debug, Serialize, Deserialize)]
873#[serde(rename_all = "camelCase")]
874pub struct BybitWsAccountExecutionFast {
875 pub category: BybitProductType,
876 pub symbol: Ustr,
877 pub exec_id: String,
878 pub exec_price: String,
879 pub exec_qty: String,
880 pub order_id: Ustr,
881 pub order_link_id: Ustr,
882 pub side: BybitOrderSide,
883 pub exec_time: String,
884 pub is_maker: bool,
885 #[serde(default = "default_ws_execution_fast_seq")]
886 pub seq: i64,
887}
888
889const fn default_ws_execution_fast_seq() -> i64 {
890 -1
891}
892
893#[derive(Clone, Debug, Serialize, Deserialize)]
898#[serde(rename_all = "camelCase")]
899pub struct BybitWsAccountExecutionFastMsg {
900 pub topic: Ustr,
901 #[serde(default)]
902 pub id: String,
903 pub creation_time: i64,
904 pub data: Vec<BybitWsAccountExecutionFast>,
905}
906
907#[derive(Clone, Debug, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct BybitWsAccountWalletCoin {
911 pub coin: Ustr,
912 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
913 pub wallet_balance: Decimal,
914 pub available_to_withdraw: String,
915 pub available_to_borrow: String,
916 pub accrued_interest: String,
917 #[serde(
918 default,
919 rename = "totalOrderIM",
920 deserialize_with = "deserialize_optional_decimal_or_zero"
921 )]
922 pub total_order_im: Decimal,
923 #[serde(
924 default,
925 rename = "totalPositionIM",
926 deserialize_with = "deserialize_optional_decimal_or_zero"
927 )]
928 pub total_position_im: Decimal,
929 #[serde(default, rename = "totalPositionMM")]
930 pub total_position_mm: Option<String>,
931 pub equity: String,
932 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
933 pub spot_borrow: Decimal,
934}
935
936#[derive(Clone, Debug, Serialize, Deserialize)]
938#[serde(rename_all = "camelCase")]
939pub struct BybitWsAccountWallet {
940 pub total_wallet_balance: String,
941 pub total_equity: String,
942 pub total_available_balance: String,
943 pub total_margin_balance: String,
944 pub total_initial_margin: String,
945 pub total_maintenance_margin: String,
946 #[serde(rename = "accountIMRate")]
947 pub account_im_rate: String,
948 #[serde(rename = "accountMMRate")]
949 pub account_mm_rate: String,
950 #[serde(rename = "accountLTV")]
951 pub account_ltv: String,
952 pub coin: Vec<BybitWsAccountWalletCoin>,
953}
954
955#[derive(Clone, Debug, Serialize, Deserialize)]
957#[serde(rename_all = "camelCase")]
958pub struct BybitWsAccountWalletMsg {
959 pub topic: Ustr,
960 pub id: String,
961 pub creation_time: i64,
962 pub data: Vec<BybitWsAccountWallet>,
963}
964
965#[derive(Clone, Debug, Serialize, Deserialize)]
967#[serde(rename_all = "camelCase")]
968pub struct BybitWsAccountPosition {
969 pub category: BybitProductType,
970 pub symbol: Ustr,
971 pub side: BybitPositionSide,
972 pub size: String,
973 pub position_idx: i32,
974 pub trade_mode: i32,
975 pub position_value: String,
976 pub risk_id: i64,
977 pub risk_limit_value: String,
978 #[serde(deserialize_with = "deserialize_optional_decimal_str")]
979 pub entry_price: Option<Decimal>,
980 pub mark_price: String,
981 pub leverage: String,
982 pub position_balance: String,
983 pub auto_add_margin: i32,
984 #[serde(rename = "positionIM")]
985 pub position_im: String,
986 #[serde(rename = "positionIMByMp")]
987 pub position_im_by_mp: String,
988 #[serde(rename = "positionMM")]
989 pub position_mm: String,
990 #[serde(rename = "positionMMByMp")]
991 pub position_mm_by_mp: String,
992 pub liq_price: String,
993 pub bust_price: String,
994 pub tpsl_mode: BybitTpSlMode,
995 pub take_profit: String,
996 pub stop_loss: String,
997 pub trailing_stop: String,
998 pub unrealised_pnl: String,
999 pub session_avg_price: String,
1000 pub cur_realised_pnl: String,
1001 pub cum_realised_pnl: String,
1002 pub position_status: BybitPositionStatus,
1003 pub adl_rank_indicator: i32,
1004 pub created_time: String,
1005 pub updated_time: String,
1006 #[serde(default = "default_ws_position_seq")]
1007 pub seq: i64,
1008 #[serde(default)]
1009 pub is_reduce_only: bool,
1010 #[serde(default)]
1011 pub mmr_sys_updated_time: String,
1012 #[serde(default)]
1013 pub leverage_sys_updated_time: String,
1014 #[serde(default)]
1015 pub open_time: i64,
1016}
1017
1018const fn default_ws_position_seq() -> i64 {
1019 -1
1020}
1021
1022#[derive(Clone, Debug, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct BybitWsAccountPositionMsg {
1026 pub topic: Ustr,
1027 pub id: String,
1028 pub creation_time: i64,
1029 pub data: Vec<BybitWsAccountPosition>,
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use rstest::rstest;
1035
1036 use super::*;
1037 use crate::common::testing::load_test_json;
1038
1039 #[rstest]
1040 fn deserialize_account_execution_fast_msg() {
1041 let json = load_test_json("ws_account_execution_fast.json");
1044 let msg: BybitWsAccountExecutionFastMsg = serde_json::from_str(&json).unwrap();
1045
1046 assert_eq!(msg.id, "");
1047 assert_eq!(msg.creation_time, 1_716_800_399_338);
1048 assert_eq!(msg.data.len(), 1);
1049 let exec = &msg.data[0];
1050 assert_eq!(exec.category, BybitProductType::Linear);
1051 assert_eq!(exec.symbol, Ustr::from("ICPUSDT"));
1052 assert_eq!(exec.exec_id, "3510f361-0add-5c7b-a2e7-9679810944fc");
1053 assert_eq!(exec.exec_price, "12.015");
1054 assert_eq!(exec.exec_qty, "3000");
1055 assert_eq!(
1056 exec.order_id,
1057 Ustr::from("443d63fa-b4c3-4297-b7b1-23bca88b04dc")
1058 );
1059 assert_eq!(exec.order_link_id, Ustr::from("test-order-link-001"));
1060 assert_eq!(exec.side, BybitOrderSide::Sell);
1061 assert!(!exec.is_maker);
1062 assert_eq!(exec.exec_time, "1716800399334");
1063 assert_eq!(exec.seq, 34_771_365_464);
1064 }
1065
1066 #[rstest]
1067 fn deserialize_account_execution_fast_msg_accepts_envelope_id() {
1068 let json = load_test_json("ws_account_execution_fast_envelope_id.json");
1070 let msg: BybitWsAccountExecutionFastMsg = serde_json::from_str(&json).unwrap();
1071 assert_eq!(msg.id, "fast-1");
1072 assert!(msg.data.is_empty());
1073 }
1074
1075 #[rstest]
1076 fn deserialize_account_position_with_open_time() {
1077 let json = load_test_json("ws_account_position_with_open_time.json");
1078 let position: BybitWsAccountPosition = serde_json::from_str(&json).unwrap();
1079 assert_eq!(position.open_time, 1_700_000_000_123);
1080 }
1081
1082 #[rstest]
1083 fn serialize_place_params_includes_order_iv_when_set() {
1084 let params = BybitWsPlaceOrderParams {
1085 category: BybitProductType::Option,
1086 symbol: Ustr::from("BTC-30JUN25-100000-C"),
1087 side: BybitOrderSide::Buy,
1088 order_type: BybitOrderType::Limit,
1089 qty: "0.1".to_string(),
1090 is_leverage: None,
1091 market_unit: None,
1092 price: Some("500".to_string()),
1093 time_in_force: Some(BybitTimeInForce::Gtc),
1094 order_link_id: Some("test-1".to_string()),
1095 reduce_only: None,
1096 close_on_trigger: None,
1097 trigger_price: None,
1098 trigger_by: None,
1099 trigger_direction: None,
1100 tpsl_mode: None,
1101 take_profit: None,
1102 stop_loss: None,
1103 tp_trigger_by: None,
1104 sl_trigger_by: None,
1105 sl_trigger_price: None,
1106 tp_trigger_price: None,
1107 sl_order_type: None,
1108 tp_order_type: None,
1109 sl_limit_price: None,
1110 tp_limit_price: None,
1111 order_iv: Some("0.80".to_string()),
1112 mmp: Some(true),
1113 position_idx: None,
1114 bbo_side_type: None,
1115 bbo_level: None,
1116 };
1117
1118 let json = serde_json::to_string(¶ms).unwrap();
1119 assert!(json.contains("\"orderIv\":\"0.80\""));
1120 assert!(json.contains("\"mmp\":true"));
1121 }
1122
1123 #[rstest]
1124 fn serialize_place_params_omits_order_iv_when_none() {
1125 let params = BybitWsPlaceOrderParams {
1126 category: BybitProductType::Linear,
1127 symbol: Ustr::from("BTCUSDT"),
1128 side: BybitOrderSide::Buy,
1129 order_type: BybitOrderType::Limit,
1130 qty: "0.01".to_string(),
1131 is_leverage: None,
1132 market_unit: None,
1133 price: Some("50000".to_string()),
1134 time_in_force: Some(BybitTimeInForce::Gtc),
1135 order_link_id: None,
1136 reduce_only: None,
1137 close_on_trigger: None,
1138 trigger_price: None,
1139 trigger_by: None,
1140 trigger_direction: None,
1141 tpsl_mode: None,
1142 take_profit: None,
1143 stop_loss: None,
1144 tp_trigger_by: None,
1145 sl_trigger_by: None,
1146 sl_trigger_price: None,
1147 tp_trigger_price: None,
1148 sl_order_type: None,
1149 tp_order_type: None,
1150 sl_limit_price: None,
1151 tp_limit_price: None,
1152 order_iv: None,
1153 mmp: None,
1154 position_idx: None,
1155 bbo_side_type: None,
1156 bbo_level: None,
1157 };
1158
1159 let json = serde_json::to_string(¶ms).unwrap();
1160 assert!(!json.contains("orderIv"));
1161 assert!(!json.contains("mmp"));
1162 assert!(!json.contains("positionIdx"));
1163 }
1164
1165 #[rstest]
1166 fn serialize_place_params_includes_bbo_when_set() {
1167 let params = BybitWsPlaceOrderParams {
1168 category: BybitProductType::Linear,
1169 symbol: Ustr::from("BTCUSDT"),
1170 side: BybitOrderSide::Buy,
1171 order_type: BybitOrderType::Limit,
1172 qty: "0.01".to_string(),
1173 is_leverage: None,
1174 market_unit: None,
1175 price: None,
1176 time_in_force: Some(BybitTimeInForce::Gtc),
1177 order_link_id: None,
1178 reduce_only: None,
1179 close_on_trigger: None,
1180 trigger_price: None,
1181 trigger_by: None,
1182 trigger_direction: None,
1183 tpsl_mode: None,
1184 take_profit: None,
1185 stop_loss: None,
1186 tp_trigger_by: None,
1187 sl_trigger_by: None,
1188 sl_trigger_price: None,
1189 tp_trigger_price: None,
1190 sl_order_type: None,
1191 tp_order_type: None,
1192 sl_limit_price: None,
1193 tp_limit_price: None,
1194 order_iv: None,
1195 mmp: None,
1196 position_idx: None,
1197 bbo_side_type: Some(BybitBboSideType::Queue),
1198 bbo_level: Some("2".to_string()),
1199 };
1200
1201 let json = serde_json::to_string(¶ms).unwrap();
1202 assert!(json.contains("\"bboSideType\":\"Queue\""));
1203 assert!(json.contains("\"bboLevel\":\"2\""));
1204 assert!(!json.contains("\"price\""));
1205 }
1206
1207 #[rstest]
1208 #[case(BybitPositionIdx::BuyHedge, 1)]
1209 #[case(BybitPositionIdx::SellHedge, 2)]
1210 fn serialize_place_params_includes_position_idx_when_set(
1211 #[case] idx: BybitPositionIdx,
1212 #[case] expected: i32,
1213 ) {
1214 let params = BybitWsPlaceOrderParams {
1215 category: BybitProductType::Linear,
1216 symbol: Ustr::from("BTCUSDT"),
1217 side: BybitOrderSide::Buy,
1218 order_type: BybitOrderType::Limit,
1219 qty: "0.01".to_string(),
1220 is_leverage: None,
1221 market_unit: None,
1222 price: Some("50000".to_string()),
1223 time_in_force: Some(BybitTimeInForce::Gtc),
1224 order_link_id: None,
1225 reduce_only: None,
1226 close_on_trigger: None,
1227 trigger_price: None,
1228 trigger_by: None,
1229 trigger_direction: None,
1230 tpsl_mode: None,
1231 take_profit: None,
1232 stop_loss: None,
1233 tp_trigger_by: None,
1234 sl_trigger_by: None,
1235 sl_trigger_price: None,
1236 tp_trigger_price: None,
1237 sl_order_type: None,
1238 tp_order_type: None,
1239 sl_limit_price: None,
1240 tp_limit_price: None,
1241 order_iv: None,
1242 mmp: None,
1243 position_idx: Some(idx),
1244 bbo_side_type: None,
1245 bbo_level: None,
1246 };
1247
1248 let json = serde_json::to_string(¶ms).unwrap();
1249 assert!(json.contains(&format!("\"positionIdx\":{expected}")));
1250 }
1251
1252 #[rstest]
1253 #[case(None)]
1254 #[case(Some(BybitPositionIdx::OneWay))]
1255 #[case(Some(BybitPositionIdx::BuyHedge))]
1256 #[case(Some(BybitPositionIdx::SellHedge))]
1257 fn place_params_position_idx_roundtrip(#[case] idx: Option<BybitPositionIdx>) {
1258 let params = BybitWsPlaceOrderParams {
1259 category: BybitProductType::Linear,
1260 symbol: Ustr::from("BTCUSDT"),
1261 side: BybitOrderSide::Buy,
1262 order_type: BybitOrderType::Limit,
1263 qty: "0.01".to_string(),
1264 is_leverage: None,
1265 market_unit: None,
1266 price: Some("50000".to_string()),
1267 time_in_force: Some(BybitTimeInForce::Gtc),
1268 order_link_id: None,
1269 reduce_only: None,
1270 close_on_trigger: None,
1271 trigger_price: None,
1272 trigger_by: None,
1273 trigger_direction: None,
1274 tpsl_mode: None,
1275 take_profit: None,
1276 stop_loss: None,
1277 tp_trigger_by: None,
1278 sl_trigger_by: None,
1279 sl_trigger_price: None,
1280 tp_trigger_price: None,
1281 sl_order_type: None,
1282 tp_order_type: None,
1283 sl_limit_price: None,
1284 tp_limit_price: None,
1285 order_iv: None,
1286 mmp: None,
1287 position_idx: idx,
1288 bbo_side_type: None,
1289 bbo_level: None,
1290 };
1291
1292 let json = serde_json::to_string(¶ms).unwrap();
1293 let decoded: BybitWsPlaceOrderParams = serde_json::from_str(&json).unwrap();
1294 assert_eq!(decoded.position_idx, idx);
1295 }
1296
1297 #[rstest]
1298 fn serialize_amend_params_includes_order_iv_when_set() {
1299 let params = BybitWsAmendOrderParams {
1300 category: BybitProductType::Option,
1301 symbol: Ustr::from("BTC-30JUN25-100000-C"),
1302 order_id: None,
1303 order_link_id: Some("test-1".to_string()),
1304 qty: None,
1305 price: None,
1306 trigger_price: None,
1307 take_profit: None,
1308 stop_loss: None,
1309 tp_trigger_by: None,
1310 sl_trigger_by: None,
1311 order_iv: Some("0.90".to_string()),
1312 };
1313
1314 let json = serde_json::to_string(¶ms).unwrap();
1315 assert!(json.contains("\"orderIv\":\"0.90\""));
1316 }
1317
1318 #[rstest]
1319 fn deserialize_account_order_frame_uses_enums() {
1320 let json = load_test_json("ws_account_order.json");
1321 let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
1322 let order = &frame.data[0];
1323
1324 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1325 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1326 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1327 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1328 assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
1329 assert_eq!(order.side, BybitOrderSide::Buy);
1330 }
1331
1332 #[rstest]
1333 fn deserialize_ws_account_position_without_conditional_fields() {
1334 let json = r#"{
1338 "topic": "position",
1339 "id": "1",
1340 "creationTime": 1697673900000,
1341 "data": [{
1342 "category": "linear",
1343 "symbol": "LTCUSDT",
1344 "side": "",
1345 "size": "0",
1346 "positionIdx": 0,
1347 "tradeMode": 0,
1348 "positionValue": "0",
1349 "riskId": 1,
1350 "riskLimitValue": "150",
1351 "entryPrice": "",
1352 "markPrice": "70.00",
1353 "leverage": "10",
1354 "positionBalance": "0",
1355 "autoAddMargin": 0,
1356 "positionIM": "0",
1357 "positionIMByMp": "0",
1358 "positionMM": "0",
1359 "positionMMByMp": "0",
1360 "liqPrice": "",
1361 "bustPrice": "",
1362 "tpslMode": "Full",
1363 "takeProfit": "0",
1364 "stopLoss": "0",
1365 "trailingStop": "0",
1366 "unrealisedPnl": "0",
1367 "sessionAvgPrice": "0",
1368 "curRealisedPnl": "0",
1369 "cumRealisedPnl": "0",
1370 "positionStatus": "Normal",
1371 "adlRankIndicator": 0,
1372 "createdTime": "1676538056258",
1373 "updatedTime": "1697673600012"
1374 }]
1375 }"#;
1376
1377 let msg: BybitWsAccountPositionMsg = serde_json::from_str(json)
1378 .expect("Failed to parse WS account position with missing conditional fields");
1379 let position = &msg.data[0];
1380
1381 assert!(!position.is_reduce_only);
1382 assert_eq!(position.seq, -1);
1383 assert_eq!(position.mmr_sys_updated_time, "");
1384 assert_eq!(position.leverage_sys_updated_time, "");
1385 }
1386}