1use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::{
22 enums::{
23 BybitAccountType, BybitCancelType, BybitContractType, BybitExecType, BybitInnovationFlag,
24 BybitInstrumentStatus, BybitMarginTrading, BybitOptionType, BybitOrderSide,
25 BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide, BybitProductType,
26 BybitStopOrderType, BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection,
27 BybitTriggerType,
28 },
29 models::{
30 BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
31 LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
32 SpotPriceFilter,
33 },
34};
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct BybitServerTime {
43 pub time_second: String,
45 pub time_nano: String,
47}
48
49pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct BybitTickerSpot {
62 pub symbol: Ustr,
63 pub bid1_price: String,
64 pub bid1_size: String,
65 pub ask1_price: String,
66 pub ask1_size: String,
67 pub last_price: String,
68 pub prev_price24h: String,
69 pub price24h_pcnt: String,
70 pub high_price24h: String,
71 pub low_price24h: String,
72 pub turnover24h: String,
73 pub volume24h: String,
74 pub usd_index_price: String,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct BybitTickerLinear {
84 pub symbol: Ustr,
85 pub last_price: String,
86 pub index_price: String,
87 pub mark_price: String,
88 pub prev_price24h: String,
89 pub price24h_pcnt: String,
90 pub high_price24h: String,
91 pub low_price24h: String,
92 pub prev_price1h: String,
93 pub open_interest: String,
94 pub open_interest_value: String,
95 pub turnover24h: String,
96 pub volume24h: String,
97 pub funding_rate: String,
98 pub next_funding_time: String,
99 pub predicted_delivery_price: String,
100 pub basis_rate: String,
101 pub delivery_fee_rate: String,
102 pub delivery_time: String,
103 pub ask1_size: String,
104 pub bid1_price: String,
105 pub ask1_price: String,
106 pub bid1_size: String,
107 pub basis: String,
108}
109
110#[derive(Clone, Debug, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct BybitTickerOption {
117 pub symbol: Ustr,
118 pub bid1_price: String,
119 pub bid1_size: String,
120 pub bid1_iv: String,
121 pub ask1_price: String,
122 pub ask1_size: String,
123 pub ask1_iv: String,
124 pub last_price: String,
125 pub high_price24h: String,
126 pub low_price24h: String,
127 pub mark_price: String,
128 pub index_price: String,
129 pub mark_iv: String,
130 pub underlying_price: String,
131 pub open_interest: String,
132 pub turnover24h: String,
133 pub volume24h: String,
134 pub total_volume: String,
135 pub total_turnover: String,
136 pub delta: String,
137 pub gamma: String,
138 pub vega: String,
139 pub theta: String,
140 pub predicted_delivery_price: String,
141 pub change24h: String,
142}
143
144pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
149pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
154pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166#[cfg_attr(
167 feature = "python",
168 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
169)]
170pub struct BybitTickerData {
171 pub symbol: Ustr,
172 pub bid1_price: String,
173 pub bid1_size: String,
174 pub ask1_price: String,
175 pub ask1_size: String,
176 pub last_price: String,
177 pub high_price24h: String,
178 pub low_price24h: String,
179 pub turnover24h: String,
180 pub volume24h: String,
181}
182
183#[cfg(feature = "python")]
184#[pyo3::pymethods]
185impl BybitTickerData {
186 #[getter]
187 #[must_use]
188 pub fn symbol(&self) -> &str {
189 self.symbol.as_str()
190 }
191
192 #[getter]
193 #[must_use]
194 pub fn bid1_price(&self) -> &str {
195 &self.bid1_price
196 }
197
198 #[getter]
199 #[must_use]
200 pub fn bid1_size(&self) -> &str {
201 &self.bid1_size
202 }
203
204 #[getter]
205 #[must_use]
206 pub fn ask1_price(&self) -> &str {
207 &self.ask1_price
208 }
209
210 #[getter]
211 #[must_use]
212 pub fn ask1_size(&self) -> &str {
213 &self.ask1_size
214 }
215
216 #[getter]
217 #[must_use]
218 pub fn last_price(&self) -> &str {
219 &self.last_price
220 }
221
222 #[getter]
223 #[must_use]
224 pub fn high_price24h(&self) -> &str {
225 &self.high_price24h
226 }
227
228 #[getter]
229 #[must_use]
230 pub fn low_price24h(&self) -> &str {
231 &self.low_price24h
232 }
233
234 #[getter]
235 #[must_use]
236 pub fn turnover24h(&self) -> &str {
237 &self.turnover24h
238 }
239
240 #[getter]
241 #[must_use]
242 pub fn volume24h(&self) -> &str {
243 &self.volume24h
244 }
245}
246
247#[derive(Clone, Debug, Serialize)]
255pub struct BybitKline {
256 pub start: String,
257 pub open: String,
258 pub high: String,
259 pub low: String,
260 pub close: String,
261 pub volume: String,
262 pub turnover: String,
263}
264
265impl<'de> Deserialize<'de> for BybitKline {
266 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267 where
268 D: serde::Deserializer<'de>,
269 {
270 let arr: [String; 7] = Deserialize::deserialize(deserializer)?;
271 Ok(Self {
272 start: arr[0].clone(),
273 open: arr[1].clone(),
274 high: arr[2].clone(),
275 low: arr[3].clone(),
276 close: arr[4].clone(),
277 volume: arr[5].clone(),
278 turnover: arr[6].clone(),
279 })
280 }
281}
282
283#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct BybitKlineResult {
290 pub category: BybitProductType,
291 pub symbol: Ustr,
292 pub list: Vec<BybitKline>,
293}
294
295pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
300
301#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct BybitTrade {
308 pub exec_id: String,
309 pub symbol: Ustr,
310 pub price: String,
311 pub size: String,
312 pub side: BybitOrderSide,
313 pub time: String,
314 pub is_block_trade: bool,
315 #[serde(default)]
316 pub m_p: Option<String>,
317 #[serde(default)]
318 pub i_p: Option<String>,
319 #[serde(default)]
320 pub mlv: Option<String>,
321 #[serde(default)]
322 pub iv: Option<String>,
323}
324
325#[derive(Clone, Debug, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct BybitTradeResult {
332 pub category: BybitProductType,
333 pub list: Vec<BybitTrade>,
334}
335
336pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
341
342#[derive(Clone, Debug, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct BybitInstrumentSpot {
349 pub symbol: Ustr,
350 pub base_coin: Ustr,
351 pub quote_coin: Ustr,
352 pub innovation: BybitInnovationFlag,
353 pub status: BybitInstrumentStatus,
354 pub margin_trading: BybitMarginTrading,
355 pub lot_size_filter: SpotLotSizeFilter,
356 pub price_filter: SpotPriceFilter,
357}
358
359#[derive(Clone, Debug, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct BybitInstrumentLinear {
366 pub symbol: Ustr,
367 pub contract_type: BybitContractType,
368 pub status: BybitInstrumentStatus,
369 pub base_coin: Ustr,
370 pub quote_coin: Ustr,
371 pub launch_time: String,
372 pub delivery_time: String,
373 pub delivery_fee_rate: String,
374 pub price_scale: String,
375 pub leverage_filter: LeverageFilter,
376 pub price_filter: LinearPriceFilter,
377 pub lot_size_filter: LinearLotSizeFilter,
378 pub unified_margin_trade: bool,
379 pub funding_interval: i64,
380 pub settle_coin: Ustr,
381}
382
383#[derive(Clone, Debug, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct BybitInstrumentInverse {
390 pub symbol: Ustr,
391 pub contract_type: BybitContractType,
392 pub status: BybitInstrumentStatus,
393 pub base_coin: Ustr,
394 pub quote_coin: Ustr,
395 pub launch_time: String,
396 pub delivery_time: String,
397 pub delivery_fee_rate: String,
398 pub price_scale: String,
399 pub leverage_filter: LeverageFilter,
400 pub price_filter: LinearPriceFilter,
401 pub lot_size_filter: LinearLotSizeFilter,
402 pub unified_margin_trade: bool,
403 pub funding_interval: i64,
404 pub settle_coin: Ustr,
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct BybitInstrumentOption {
414 pub symbol: Ustr,
415 pub status: BybitInstrumentStatus,
416 pub base_coin: Ustr,
417 pub quote_coin: Ustr,
418 pub settle_coin: Ustr,
419 pub options_type: BybitOptionType,
420 pub launch_time: String,
421 pub delivery_time: String,
422 pub delivery_fee_rate: String,
423 pub price_filter: LinearPriceFilter,
424 pub lot_size_filter: OptionLotSizeFilter,
425}
426
427pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
432pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
437pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
442pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
447
448#[derive(Clone, Debug, Serialize, Deserialize)]
453#[serde(rename_all = "camelCase")]
454#[cfg_attr(
455 feature = "python",
456 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
457)]
458pub struct BybitFeeRate {
459 pub symbol: Ustr,
460 pub taker_fee_rate: String,
461 pub maker_fee_rate: String,
462 #[serde(default)]
463 pub base_coin: Option<Ustr>,
464}
465
466#[cfg(feature = "python")]
467#[pyo3::pymethods]
468impl BybitFeeRate {
469 #[getter]
470 #[must_use]
471 pub fn symbol(&self) -> &str {
472 self.symbol.as_str()
473 }
474
475 #[getter]
476 #[must_use]
477 pub fn taker_fee_rate(&self) -> &str {
478 &self.taker_fee_rate
479 }
480
481 #[getter]
482 #[must_use]
483 pub fn maker_fee_rate(&self) -> &str {
484 &self.maker_fee_rate
485 }
486
487 #[getter]
488 #[must_use]
489 pub fn base_coin(&self) -> Option<&str> {
490 self.base_coin.as_ref().map(|u| u.as_str())
491 }
492}
493
494pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
499
500#[derive(Clone, Debug, Serialize, Deserialize)]
505#[serde(rename_all = "camelCase")]
506pub struct BybitCoinBalance {
507 pub available_to_borrow: String,
508 pub bonus: String,
509 pub accrued_interest: String,
510 pub available_to_withdraw: String,
511 #[serde(default, rename = "totalOrderIM")]
512 pub total_order_im: Option<String>,
513 pub equity: String,
514 pub usd_value: String,
515 pub borrow_amount: String,
516 #[serde(default, rename = "totalPositionMM")]
517 pub total_position_mm: Option<String>,
518 #[serde(default, rename = "totalPositionIM")]
519 pub total_position_im: Option<String>,
520 pub wallet_balance: String,
521 pub unrealised_pnl: String,
522 pub cum_realised_pnl: String,
523 pub locked: String,
524 pub collateral_switch: bool,
525 pub margin_collateral: bool,
526 pub coin: Ustr,
527 #[serde(default)]
528 pub spot_hedging_qty: Option<String>,
529 #[serde(default)]
530 pub spot_borrow: Option<String>,
531}
532
533#[derive(Clone, Debug, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct BybitWalletBalance {
540 pub total_equity: String,
541 #[serde(rename = "accountIMRate")]
542 pub account_im_rate: String,
543 pub total_margin_balance: String,
544 pub total_initial_margin: String,
545 pub account_type: BybitAccountType,
546 pub total_available_balance: String,
547 #[serde(rename = "accountMMRate")]
548 pub account_mm_rate: String,
549 #[serde(rename = "totalPerpUPL")]
550 pub total_perp_upl: String,
551 pub total_wallet_balance: String,
552 #[serde(rename = "accountLTV")]
553 pub account_ltv: String,
554 pub total_maintenance_margin: String,
555 pub coin: Vec<BybitCoinBalance>,
556}
557
558pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
563
564#[derive(Clone, Debug, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct BybitOrder {
572 pub order_id: Ustr,
573 pub order_link_id: Ustr,
574 pub block_trade_id: Option<Ustr>,
575 pub symbol: Ustr,
576 pub price: String,
577 pub qty: String,
578 pub side: BybitOrderSide,
579 pub is_leverage: String,
580 pub position_idx: i32,
581 pub order_status: BybitOrderStatus,
582 pub cancel_type: BybitCancelType,
583 pub reject_reason: Ustr,
584 pub avg_price: Option<String>,
585 pub leaves_qty: String,
586 pub leaves_value: String,
587 pub cum_exec_qty: String,
588 pub cum_exec_value: String,
589 pub cum_exec_fee: String,
590 pub time_in_force: BybitTimeInForce,
591 pub order_type: BybitOrderType,
592 pub stop_order_type: BybitStopOrderType,
593 pub order_iv: Option<String>,
594 pub trigger_price: String,
595 pub take_profit: String,
596 pub stop_loss: String,
597 pub tp_trigger_by: BybitTriggerType,
598 pub sl_trigger_by: BybitTriggerType,
599 pub trigger_direction: BybitTriggerDirection,
600 pub trigger_by: BybitTriggerType,
601 pub last_price_on_created: String,
602 pub reduce_only: bool,
603 pub close_on_trigger: bool,
604 pub smp_type: Ustr,
605 pub smp_group: i32,
606 pub smp_order_id: Ustr,
607 pub tpsl_mode: Option<BybitTpSlMode>,
608 pub tp_limit_price: String,
609 pub sl_limit_price: String,
610 pub place_type: Ustr,
611 pub created_time: String,
612 pub updated_time: String,
613}
614
615pub type BybitOpenOrdersResponse = BybitListResponse<BybitOrder>;
620pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
625
626#[derive(Clone, Debug, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct BybitPlaceOrderResult {
633 pub order_id: Option<Ustr>,
634 pub order_link_id: Option<Ustr>,
635}
636
637pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
642
643#[derive(Clone, Debug, Serialize, Deserialize)]
648#[serde(rename_all = "camelCase")]
649pub struct BybitCancelOrderResult {
650 pub order_id: Option<Ustr>,
651 pub order_link_id: Option<Ustr>,
652}
653
654pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
659
660#[derive(Clone, Debug, Serialize, Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct BybitExecution {
667 pub symbol: Ustr,
668 pub order_id: Ustr,
669 pub order_link_id: Ustr,
670 pub side: BybitOrderSide,
671 pub order_price: String,
672 pub order_qty: String,
673 pub leaves_qty: String,
674 pub create_type: Option<String>,
675 pub order_type: BybitOrderType,
676 pub stop_order_type: Option<BybitStopOrderType>,
677 pub exec_fee: String,
678 pub exec_id: String,
679 pub exec_price: String,
680 pub exec_qty: String,
681 pub exec_type: BybitExecType,
682 pub exec_value: String,
683 pub exec_time: String,
684 pub fee_currency: Ustr,
685 pub is_maker: bool,
686 pub fee_rate: String,
687 pub trade_iv: String,
688 pub mark_iv: String,
689 pub mark_price: String,
690 pub index_price: String,
691 pub underlying_price: String,
692 pub block_trade_id: String,
693 pub closed_size: String,
694 pub seq: i64,
695}
696
697pub type BybitTradeHistoryResponse = BybitListResponse<BybitExecution>;
702
703#[derive(Clone, Debug, Serialize, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct BybitPosition {
710 pub position_idx: BybitPositionIdx,
711 pub risk_id: i32,
712 pub risk_limit_value: String,
713 pub symbol: Ustr,
714 pub side: BybitPositionSide,
715 pub size: String,
716 pub avg_price: String,
717 pub position_value: String,
718 pub trade_mode: i32,
719 pub position_status: String,
720 pub auto_add_margin: i32,
721 pub adl_rank_indicator: i32,
722 pub leverage: String,
723 pub position_balance: String,
724 pub mark_price: String,
725 pub liq_price: String,
726 pub bust_price: String,
727 #[serde(rename = "positionMM")]
728 pub position_mm: String,
729 #[serde(rename = "positionIM")]
730 pub position_im: String,
731 pub tpsl_mode: String,
732 pub take_profit: String,
733 pub stop_loss: String,
734 pub trailing_stop: String,
735 pub unrealised_pnl: String,
736 pub cur_realised_pnl: String,
737 pub cum_realised_pnl: String,
738 pub seq: i64,
739 pub is_reduce_only: bool,
740 pub mmr_sys_updated_time: String,
741 pub leverage_sys_updated_time: String,
742 pub created_time: String,
743 pub updated_time: String,
744}
745
746pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
751
752#[cfg(test)]
757mod tests {
758 use nautilus_core::UnixNanos;
759 use nautilus_model::identifiers::AccountId;
760 use rstest::rstest;
761
762 use super::*;
763 use crate::common::testing::load_test_json;
764
765 #[rstest]
766 fn deserialize_spot_instrument_uses_enums() {
767 let json = load_test_json("http_get_instruments_spot.json");
768 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
769 let instrument = &response.result.list[0];
770
771 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
772 assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
773 assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
774 }
775
776 #[rstest]
777 fn deserialize_linear_instrument_status() {
778 let json = load_test_json("http_get_instruments_linear.json");
779 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
780 let instrument = &response.result.list[0];
781
782 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
783 assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
784 }
785
786 #[rstest]
787 fn deserialize_order_response_maps_enums() {
788 let json = load_test_json("http_get_orders_history.json");
789 let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
790 let order = &response.result.list[0];
791
792 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
793 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
794 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
795 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
796 assert_eq!(order.order_type, BybitOrderType::Limit);
797 }
798
799 #[rstest]
800 fn deserialize_wallet_balance_without_optional_fields() {
801 let json = r#"{
802 "retCode": 0,
803 "retMsg": "OK",
804 "result": {
805 "list": [{
806 "totalEquity": "1000.00",
807 "accountIMRate": "0",
808 "totalMarginBalance": "1000.00",
809 "totalInitialMargin": "0",
810 "accountType": "UNIFIED",
811 "totalAvailableBalance": "1000.00",
812 "accountMMRate": "0",
813 "totalPerpUPL": "0",
814 "totalWalletBalance": "1000.00",
815 "accountLTV": "0",
816 "totalMaintenanceMargin": "0",
817 "coin": [{
818 "availableToBorrow": "0",
819 "bonus": "0",
820 "accruedInterest": "0",
821 "availableToWithdraw": "1000.00",
822 "equity": "1000.00",
823 "usdValue": "1000.00",
824 "borrowAmount": "0",
825 "totalPositionIM": "0",
826 "walletBalance": "1000.00",
827 "unrealisedPnl": "0",
828 "cumRealisedPnl": "0",
829 "locked": "0",
830 "collateralSwitch": true,
831 "marginCollateral": true,
832 "coin": "USDT"
833 }]
834 }]
835 }
836 }"#;
837
838 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
839 .expect("Failed to parse wallet balance without optional fields");
840
841 assert_eq!(response.ret_code, 0);
842 assert_eq!(response.result.list[0].coin[0].total_order_im, None);
843 assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
844 }
845
846 #[rstest]
847 fn deserialize_wallet_balance_from_docs() {
848 let json = include_str!("../../test_data/http_get_wallet_balance.json");
849
850 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
851 .expect("Failed to parse wallet balance from Bybit docs example");
852
853 assert_eq!(response.ret_code, 0);
854 assert_eq!(response.ret_msg, "OK");
855
856 let wallet = &response.result.list[0];
857 assert_eq!(wallet.total_equity, "3.31216591");
858 assert_eq!(wallet.account_im_rate, "0");
859 assert_eq!(wallet.account_mm_rate, "0");
860 assert_eq!(wallet.total_perp_upl, "0");
861 assert_eq!(wallet.account_ltv, "0");
862
863 let btc = &wallet.coin[0];
865 assert_eq!(btc.coin.as_str(), "BTC");
866 assert_eq!(btc.available_to_borrow, "3");
867 assert_eq!(btc.total_order_im, Some("0".to_string()));
868 assert_eq!(btc.total_position_mm, Some("0".to_string()));
869 assert_eq!(btc.total_position_im, Some("0".to_string()));
870
871 let usdt = &wallet.coin[1];
873 assert_eq!(usdt.coin.as_str(), "USDT");
874 assert_eq!(usdt.wallet_balance, "1000.50");
875 assert_eq!(usdt.total_order_im, None);
876 assert_eq!(usdt.total_position_mm, None);
877 assert_eq!(usdt.total_position_im, None);
878 assert_eq!(btc.spot_borrow, Some("0".to_string()));
879 assert_eq!(usdt.spot_borrow, Some("0".to_string()));
880 }
881
882 #[rstest]
883 fn test_parse_wallet_balance_with_spot_borrow() {
884 let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
885 let response: BybitWalletBalanceResponse =
886 serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
887
888 let wallet = &response.result.list[0];
889 let usdt = &wallet.coin[0];
890
891 assert_eq!(usdt.coin.as_str(), "USDT");
892 assert_eq!(usdt.wallet_balance, "1200.00");
893 assert_eq!(usdt.spot_borrow, Some("200.00".to_string()));
894 assert_eq!(usdt.borrow_amount, "200.00");
895
896 let account_id = crate::common::parse::parse_account_state(
898 wallet,
899 AccountId::new("BYBIT-001"),
900 UnixNanos::default(),
901 )
902 .expect("Failed to parse account state");
903
904 let balance = &account_id.balances[0];
905 assert_eq!(balance.total.as_f64(), 1000.0);
906 }
907}