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}
528
529#[derive(Clone, Debug, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct BybitWalletBalance {
536 pub total_equity: String,
537 #[serde(rename = "accountIMRate")]
538 pub account_im_rate: String,
539 pub total_margin_balance: String,
540 pub total_initial_margin: String,
541 pub account_type: BybitAccountType,
542 pub total_available_balance: String,
543 #[serde(rename = "accountMMRate")]
544 pub account_mm_rate: String,
545 #[serde(rename = "totalPerpUPL")]
546 pub total_perp_upl: String,
547 pub total_wallet_balance: String,
548 #[serde(rename = "accountLTV")]
549 pub account_ltv: String,
550 pub total_maintenance_margin: String,
551 pub coin: Vec<BybitCoinBalance>,
552}
553
554pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
559
560#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct BybitOrder {
568 pub order_id: Ustr,
569 pub order_link_id: Ustr,
570 pub block_trade_id: Option<Ustr>,
571 pub symbol: Ustr,
572 pub price: String,
573 pub qty: String,
574 pub side: BybitOrderSide,
575 pub is_leverage: String,
576 pub position_idx: i32,
577 pub order_status: BybitOrderStatus,
578 pub cancel_type: BybitCancelType,
579 pub reject_reason: Ustr,
580 pub avg_price: Option<String>,
581 pub leaves_qty: String,
582 pub leaves_value: String,
583 pub cum_exec_qty: String,
584 pub cum_exec_value: String,
585 pub cum_exec_fee: String,
586 pub time_in_force: BybitTimeInForce,
587 pub order_type: BybitOrderType,
588 pub stop_order_type: BybitStopOrderType,
589 pub order_iv: Option<String>,
590 pub trigger_price: String,
591 pub take_profit: String,
592 pub stop_loss: String,
593 pub tp_trigger_by: BybitTriggerType,
594 pub sl_trigger_by: BybitTriggerType,
595 pub trigger_direction: BybitTriggerDirection,
596 pub trigger_by: BybitTriggerType,
597 pub last_price_on_created: String,
598 pub reduce_only: bool,
599 pub close_on_trigger: bool,
600 pub smp_type: Ustr,
601 pub smp_group: i32,
602 pub smp_order_id: Ustr,
603 pub tpsl_mode: Option<BybitTpSlMode>,
604 pub tp_limit_price: String,
605 pub sl_limit_price: String,
606 pub place_type: Ustr,
607 pub created_time: String,
608 pub updated_time: String,
609}
610
611pub type BybitOpenOrdersResponse = BybitListResponse<BybitOrder>;
616pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
621
622#[derive(Clone, Debug, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628pub struct BybitPlaceOrderResult {
629 pub order_id: Option<Ustr>,
630 pub order_link_id: Option<Ustr>,
631}
632
633pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
638
639#[derive(Clone, Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct BybitCancelOrderResult {
646 pub order_id: Option<Ustr>,
647 pub order_link_id: Option<Ustr>,
648}
649
650pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
655
656#[derive(Clone, Debug, Serialize, Deserialize)]
661#[serde(rename_all = "camelCase")]
662pub struct BybitExecution {
663 pub symbol: Ustr,
664 pub order_id: Ustr,
665 pub order_link_id: Ustr,
666 pub side: BybitOrderSide,
667 pub order_price: String,
668 pub order_qty: String,
669 pub leaves_qty: String,
670 pub create_type: Option<String>,
671 pub order_type: BybitOrderType,
672 pub stop_order_type: Option<BybitStopOrderType>,
673 pub exec_fee: String,
674 pub exec_id: String,
675 pub exec_price: String,
676 pub exec_qty: String,
677 pub exec_type: BybitExecType,
678 pub exec_value: String,
679 pub exec_time: String,
680 pub fee_currency: Ustr,
681 pub is_maker: bool,
682 pub fee_rate: String,
683 pub trade_iv: String,
684 pub mark_iv: String,
685 pub mark_price: String,
686 pub index_price: String,
687 pub underlying_price: String,
688 pub block_trade_id: String,
689 pub closed_size: String,
690 pub seq: i64,
691}
692
693pub type BybitTradeHistoryResponse = BybitListResponse<BybitExecution>;
698
699#[derive(Clone, Debug, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705pub struct BybitPosition {
706 pub position_idx: BybitPositionIdx,
707 pub risk_id: i32,
708 pub risk_limit_value: String,
709 pub symbol: Ustr,
710 pub side: BybitPositionSide,
711 pub size: String,
712 pub avg_price: String,
713 pub position_value: String,
714 pub trade_mode: i32,
715 pub position_status: String,
716 pub auto_add_margin: i32,
717 pub adl_rank_indicator: i32,
718 pub leverage: String,
719 pub position_balance: String,
720 pub mark_price: String,
721 pub liq_price: String,
722 pub bust_price: String,
723 #[serde(rename = "positionMM")]
724 pub position_mm: String,
725 #[serde(rename = "positionIM")]
726 pub position_im: String,
727 pub tpsl_mode: String,
728 pub take_profit: String,
729 pub stop_loss: String,
730 pub trailing_stop: String,
731 pub unrealised_pnl: String,
732 pub cur_realised_pnl: String,
733 pub cum_realised_pnl: String,
734 pub seq: i64,
735 pub is_reduce_only: bool,
736 pub mmr_sys_updated_time: String,
737 pub leverage_sys_updated_time: String,
738 pub created_time: String,
739 pub updated_time: String,
740}
741
742pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
747
748#[cfg(test)]
753mod tests {
754 use rstest::rstest;
755
756 use super::*;
757 use crate::common::testing::load_test_json;
758
759 #[rstest]
760 fn deserialize_spot_instrument_uses_enums() {
761 let json = load_test_json("http_get_instruments_spot.json");
762 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
763 let instrument = &response.result.list[0];
764
765 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
766 assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
767 assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
768 }
769
770 #[rstest]
771 fn deserialize_linear_instrument_status() {
772 let json = load_test_json("http_get_instruments_linear.json");
773 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
774 let instrument = &response.result.list[0];
775
776 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
777 assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
778 }
779
780 #[rstest]
781 fn deserialize_order_response_maps_enums() {
782 let json = load_test_json("http_get_orders_history.json");
783 let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
784 let order = &response.result.list[0];
785
786 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
787 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
788 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
789 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
790 assert_eq!(order.order_type, BybitOrderType::Limit);
791 }
792
793 #[rstest]
794 fn deserialize_wallet_balance_without_optional_fields() {
795 let json = r#"{
796 "retCode": 0,
797 "retMsg": "OK",
798 "result": {
799 "list": [{
800 "totalEquity": "1000.00",
801 "accountIMRate": "0",
802 "totalMarginBalance": "1000.00",
803 "totalInitialMargin": "0",
804 "accountType": "UNIFIED",
805 "totalAvailableBalance": "1000.00",
806 "accountMMRate": "0",
807 "totalPerpUPL": "0",
808 "totalWalletBalance": "1000.00",
809 "accountLTV": "0",
810 "totalMaintenanceMargin": "0",
811 "coin": [{
812 "availableToBorrow": "0",
813 "bonus": "0",
814 "accruedInterest": "0",
815 "availableToWithdraw": "1000.00",
816 "equity": "1000.00",
817 "usdValue": "1000.00",
818 "borrowAmount": "0",
819 "totalPositionIM": "0",
820 "walletBalance": "1000.00",
821 "unrealisedPnl": "0",
822 "cumRealisedPnl": "0",
823 "locked": "0",
824 "collateralSwitch": true,
825 "marginCollateral": true,
826 "coin": "USDT"
827 }]
828 }]
829 }
830 }"#;
831
832 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
833 .expect("Failed to parse wallet balance without optional fields");
834
835 assert_eq!(response.ret_code, 0);
836 assert_eq!(response.result.list[0].coin[0].total_order_im, None);
837 assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
838 }
839
840 #[rstest]
841 fn deserialize_wallet_balance_from_docs() {
842 let json = include_str!("../../test_data/http_get_wallet_balance.json");
843
844 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
845 .expect("Failed to parse wallet balance from Bybit docs example");
846
847 assert_eq!(response.ret_code, 0);
848 assert_eq!(response.ret_msg, "OK");
849
850 let wallet = &response.result.list[0];
851 assert_eq!(wallet.total_equity, "3.31216591");
852 assert_eq!(wallet.account_im_rate, "0");
853 assert_eq!(wallet.account_mm_rate, "0");
854 assert_eq!(wallet.total_perp_upl, "0");
855 assert_eq!(wallet.account_ltv, "0");
856
857 let btc = &wallet.coin[0];
859 assert_eq!(btc.coin.as_str(), "BTC");
860 assert_eq!(btc.available_to_borrow, "3");
861 assert_eq!(btc.total_order_im, Some("0".to_string()));
862 assert_eq!(btc.total_position_mm, Some("0".to_string()));
863 assert_eq!(btc.total_position_im, Some("0".to_string()));
864
865 let usdt = &wallet.coin[1];
867 assert_eq!(usdt.coin.as_str(), "USDT");
868 assert_eq!(usdt.wallet_balance, "1000.50");
869 assert_eq!(usdt.total_order_im, None);
870 assert_eq!(usdt.total_position_mm, None);
871 assert_eq!(usdt.total_position_im, None);
872 }
873}