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