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