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