1use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::{
23 enums::{
24 BybitAccountType, BybitApiKeyType, BybitCancelType, BybitContractType, BybitCreateType,
25 BybitExecType, BybitInnovationFlag, BybitInstrumentStatus, BybitMarginMode,
26 BybitMarginTrading, BybitOptionType, BybitOrderSide, BybitOrderStatus, BybitOrderType,
27 BybitPositionIdx, BybitPositionSide, BybitPositionStatus, BybitProductType, BybitSmpType,
28 BybitStopOrderType, BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection,
29 BybitTriggerType, BybitUnifiedMarginStatus,
30 },
31 models::{
32 BybitCursorList, BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
33 LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
34 SpotPriceFilter,
35 },
36 parse::{
37 bool_or_int, deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero,
38 deserialize_string_to_u8, masked_secret, on_off_bool,
39 },
40};
41
42#[derive(Clone, Debug, Default, Serialize, Deserialize)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
51)]
52pub struct BybitOrderCursorList {
53 pub list: Vec<BybitOrder>,
55 pub next_page_cursor: Option<String>,
57 #[serde(default)]
59 pub category: Option<BybitProductType>,
60}
61
62impl From<BybitCursorList<BybitOrder>> for BybitOrderCursorList {
63 fn from(cursor_list: BybitCursorList<BybitOrder>) -> Self {
64 Self {
65 list: cursor_list.list,
66 next_page_cursor: cursor_list.next_page_cursor,
67 category: cursor_list.category,
68 }
69 }
70}
71
72#[cfg(feature = "python")]
73#[pyo3::pymethods]
74impl BybitOrderCursorList {
75 #[getter]
76 #[must_use]
77 pub fn list(&self) -> Vec<BybitOrder> {
78 self.list.clone()
79 }
80
81 #[getter]
82 #[must_use]
83 pub fn next_page_cursor(&self) -> Option<&str> {
84 self.next_page_cursor.as_deref()
85 }
86
87 #[getter]
88 #[must_use]
89 pub fn category(&self) -> Option<BybitProductType> {
90 self.category
91 }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
99#[cfg_attr(
100 feature = "python",
101 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
102)]
103#[cfg_attr(
104 feature = "python",
105 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
106)]
107#[serde(rename_all = "camelCase")]
108pub struct BybitServerTime {
109 pub time_second: String,
111 pub time_nano: String,
113}
114
115#[cfg(feature = "python")]
116#[pyo3::pymethods]
117impl BybitServerTime {
118 #[getter]
119 #[must_use]
120 pub fn time_second(&self) -> &str {
121 &self.time_second
122 }
123
124 #[getter]
125 #[must_use]
126 pub fn time_nano(&self) -> &str {
127 &self.time_nano
128 }
129}
130
131pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
136
137#[derive(Clone, Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct BybitTickerSpot {
144 pub symbol: Ustr,
145 pub bid1_price: String,
146 pub bid1_size: String,
147 pub ask1_price: String,
148 pub ask1_size: String,
149 pub last_price: String,
150 pub prev_price24h: String,
151 pub price24h_pcnt: String,
152 pub high_price24h: String,
153 pub low_price24h: String,
154 pub turnover24h: String,
155 pub volume24h: String,
156 #[serde(default)]
157 pub usd_index_price: String,
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct BybitTickerLinear {
167 pub symbol: Ustr,
168 pub last_price: String,
169 pub index_price: String,
170 pub mark_price: String,
171 pub prev_price24h: String,
172 pub price24h_pcnt: String,
173 pub high_price24h: String,
174 pub low_price24h: String,
175 pub prev_price1h: String,
176 pub open_interest: String,
177 pub open_interest_value: String,
178 pub turnover24h: String,
179 pub volume24h: String,
180 pub funding_rate: String,
181 pub next_funding_time: String,
182 pub predicted_delivery_price: String,
183 pub basis_rate: String,
184 pub delivery_fee_rate: String,
185 pub delivery_time: String,
186 pub ask1_size: String,
187 pub bid1_price: String,
188 pub ask1_price: String,
189 pub bid1_size: String,
190 pub basis: String,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct BybitTickerOption {
200 pub symbol: Ustr,
201 pub bid1_price: String,
202 pub bid1_size: String,
203 pub bid1_iv: String,
204 pub ask1_price: String,
205 pub ask1_size: String,
206 pub ask1_iv: String,
207 pub last_price: String,
208 pub high_price24h: String,
209 pub low_price24h: String,
210 pub mark_price: String,
211 pub index_price: String,
212 pub mark_iv: String,
213 pub underlying_price: String,
214 pub open_interest: String,
215 pub turnover24h: String,
216 pub volume24h: String,
217 pub total_volume: String,
218 pub total_turnover: String,
219 pub delta: String,
220 pub gamma: String,
221 pub vega: String,
222 pub theta: String,
223 pub predicted_delivery_price: String,
224 pub change24h: String,
225}
226
227pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
232pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
237pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
242
243#[derive(Clone, Debug, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249#[cfg_attr(
250 feature = "python",
251 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
252)]
253#[cfg_attr(
254 feature = "python",
255 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
256)]
257pub struct BybitTickerData {
258 pub symbol: Ustr,
259 pub bid1_price: String,
260 pub bid1_size: String,
261 pub ask1_price: String,
262 pub ask1_size: String,
263 pub last_price: String,
264 pub high_price24h: String,
265 pub low_price24h: String,
266 pub turnover24h: String,
267 pub volume24h: String,
268 #[serde(default)]
269 pub open_interest: Option<String>,
270 #[serde(default)]
271 pub funding_rate: Option<String>,
272 #[serde(default)]
273 pub next_funding_time: Option<String>,
274 #[serde(default)]
275 pub mark_price: Option<String>,
276 #[serde(default)]
277 pub index_price: Option<String>,
278}
279
280#[cfg(feature = "python")]
281#[pyo3::pymethods]
282impl BybitTickerData {
283 #[getter]
284 #[must_use]
285 pub fn symbol(&self) -> &str {
286 self.symbol.as_str()
287 }
288
289 #[getter]
290 #[must_use]
291 pub fn bid1_price(&self) -> &str {
292 &self.bid1_price
293 }
294
295 #[getter]
296 #[must_use]
297 pub fn bid1_size(&self) -> &str {
298 &self.bid1_size
299 }
300
301 #[getter]
302 #[must_use]
303 pub fn ask1_price(&self) -> &str {
304 &self.ask1_price
305 }
306
307 #[getter]
308 #[must_use]
309 pub fn ask1_size(&self) -> &str {
310 &self.ask1_size
311 }
312
313 #[getter]
314 #[must_use]
315 pub fn last_price(&self) -> &str {
316 &self.last_price
317 }
318
319 #[getter]
320 #[must_use]
321 pub fn high_price24h(&self) -> &str {
322 &self.high_price24h
323 }
324
325 #[getter]
326 #[must_use]
327 pub fn low_price24h(&self) -> &str {
328 &self.low_price24h
329 }
330
331 #[getter]
332 #[must_use]
333 pub fn turnover24h(&self) -> &str {
334 &self.turnover24h
335 }
336
337 #[getter]
338 #[must_use]
339 pub fn volume24h(&self) -> &str {
340 &self.volume24h
341 }
342
343 #[getter]
344 #[must_use]
345 pub fn open_interest(&self) -> Option<&str> {
346 self.open_interest.as_deref()
347 }
348
349 #[getter]
350 #[must_use]
351 pub fn funding_rate(&self) -> Option<&str> {
352 self.funding_rate.as_deref()
353 }
354
355 #[getter]
356 #[must_use]
357 pub fn next_funding_time(&self) -> Option<&str> {
358 self.next_funding_time.as_deref()
359 }
360
361 #[getter]
362 #[must_use]
363 pub fn mark_price(&self) -> Option<&str> {
364 self.mark_price.as_deref()
365 }
366
367 #[getter]
368 #[must_use]
369 pub fn index_price(&self) -> Option<&str> {
370 self.index_price.as_deref()
371 }
372}
373
374impl From<BybitTickerSpot> for BybitTickerData {
375 fn from(ticker: BybitTickerSpot) -> Self {
376 Self {
377 symbol: ticker.symbol,
378 bid1_price: ticker.bid1_price,
379 bid1_size: ticker.bid1_size,
380 ask1_price: ticker.ask1_price,
381 ask1_size: ticker.ask1_size,
382 last_price: ticker.last_price,
383 high_price24h: ticker.high_price24h,
384 low_price24h: ticker.low_price24h,
385 turnover24h: ticker.turnover24h,
386 volume24h: ticker.volume24h,
387 open_interest: None,
388 funding_rate: None,
389 next_funding_time: None,
390 mark_price: None,
391 index_price: None,
392 }
393 }
394}
395
396impl From<BybitTickerLinear> for BybitTickerData {
397 fn from(ticker: BybitTickerLinear) -> Self {
398 Self {
399 symbol: ticker.symbol,
400 bid1_price: ticker.bid1_price,
401 bid1_size: ticker.bid1_size,
402 ask1_price: ticker.ask1_price,
403 ask1_size: ticker.ask1_size,
404 last_price: ticker.last_price,
405 high_price24h: ticker.high_price24h,
406 low_price24h: ticker.low_price24h,
407 turnover24h: ticker.turnover24h,
408 volume24h: ticker.volume24h,
409 open_interest: Some(ticker.open_interest),
410 funding_rate: Some(ticker.funding_rate),
411 next_funding_time: Some(ticker.next_funding_time),
412 mark_price: Some(ticker.mark_price),
413 index_price: Some(ticker.index_price),
414 }
415 }
416}
417
418impl From<BybitTickerOption> for BybitTickerData {
419 fn from(ticker: BybitTickerOption) -> Self {
420 Self {
421 symbol: ticker.symbol,
422 bid1_price: ticker.bid1_price,
423 bid1_size: ticker.bid1_size,
424 ask1_price: ticker.ask1_price,
425 ask1_size: ticker.ask1_size,
426 last_price: ticker.last_price,
427 high_price24h: ticker.high_price24h,
428 low_price24h: ticker.low_price24h,
429 turnover24h: ticker.turnover24h,
430 volume24h: ticker.volume24h,
431 open_interest: Some(ticker.open_interest),
432 funding_rate: None,
433 next_funding_time: None,
434 mark_price: Some(ticker.mark_price),
435 index_price: Some(ticker.index_price),
436 }
437 }
438}
439
440#[derive(Clone, Debug, Serialize)]
448pub struct BybitKline {
449 pub start: String,
450 pub open: String,
451 pub high: String,
452 pub low: String,
453 pub close: String,
454 pub volume: String,
455 pub turnover: String,
456}
457
458impl<'de> Deserialize<'de> for BybitKline {
459 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
460 where
461 D: serde::Deserializer<'de>,
462 {
463 let [start, open, high, low, close, volume, turnover]: [String; 7] =
464 Deserialize::deserialize(deserializer)?;
465 Ok(Self {
466 start,
467 open,
468 high,
469 low,
470 close,
471 volume,
472 turnover,
473 })
474 }
475}
476
477#[derive(Clone, Debug, Serialize, Deserialize)]
482#[serde(rename_all = "camelCase")]
483pub struct BybitKlineResult {
484 pub category: BybitProductType,
485 pub symbol: Ustr,
486 pub list: Vec<BybitKline>,
487}
488
489pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
494
495#[derive(Clone, Debug, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct BybitTrade {
502 pub exec_id: String,
503 pub symbol: Ustr,
504 pub price: String,
505 pub size: String,
506 pub side: BybitOrderSide,
507 pub time: String,
508 pub is_block_trade: bool,
509 #[serde(default)]
510 pub m_p: Option<String>,
511 #[serde(default)]
512 pub i_p: Option<String>,
513 #[serde(default)]
514 pub mlv: Option<String>,
515 #[serde(default)]
516 pub iv: Option<String>,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize)]
524#[serde(rename_all = "camelCase")]
525pub struct BybitTradeResult {
526 pub category: BybitProductType,
527 pub list: Vec<BybitTrade>,
528}
529
530pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
535
536#[derive(Clone, Debug, Serialize, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct BybitFunding {
543 pub symbol: Ustr,
544 pub funding_rate: String,
545 pub funding_rate_timestamp: String,
546}
547
548#[derive(Clone, Debug, Serialize, Deserialize)]
553#[serde(rename_all = "camelCase")]
554pub struct BybitFundingResult {
555 pub category: BybitProductType,
556 pub list: Vec<BybitFunding>,
557}
558
559pub type BybitFundingResponse = BybitResponse<BybitFundingResult>;
564
565#[derive(Clone, Debug, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct BybitOrderbookResult {
572 pub s: Ustr,
574 pub b: Vec<[String; 2]>,
576 pub a: Vec<[String; 2]>,
578 pub ts: i64,
579 pub u: i64,
581 pub seq: i64,
583 pub cts: i64,
584}
585
586pub type BybitOrderbookResponse = BybitResponse<BybitOrderbookResult>;
591
592#[derive(Clone, Debug, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct BybitInstrumentSpot {
599 pub symbol: Ustr,
600 pub base_coin: Ustr,
601 pub quote_coin: Ustr,
602 pub innovation: BybitInnovationFlag,
603 pub status: BybitInstrumentStatus,
604 pub margin_trading: BybitMarginTrading,
605 pub lot_size_filter: SpotLotSizeFilter,
606 pub price_filter: SpotPriceFilter,
607}
608
609#[derive(Clone, Debug, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub struct BybitInstrumentLinear {
616 pub symbol: Ustr,
617 pub contract_type: BybitContractType,
618 pub status: BybitInstrumentStatus,
619 pub base_coin: Ustr,
620 pub quote_coin: Ustr,
621 pub launch_time: String,
622 pub delivery_time: String,
623 pub delivery_fee_rate: String,
624 pub price_scale: String,
625 pub leverage_filter: LeverageFilter,
626 pub price_filter: LinearPriceFilter,
627 pub lot_size_filter: LinearLotSizeFilter,
628 pub unified_margin_trade: bool,
629 pub funding_interval: i64,
630 pub settle_coin: Ustr,
631}
632
633#[derive(Clone, Debug, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct BybitInstrumentInverse {
640 pub symbol: Ustr,
641 pub contract_type: BybitContractType,
642 pub status: BybitInstrumentStatus,
643 pub base_coin: Ustr,
644 pub quote_coin: Ustr,
645 pub launch_time: String,
646 pub delivery_time: String,
647 pub delivery_fee_rate: String,
648 pub price_scale: String,
649 pub leverage_filter: LeverageFilter,
650 pub price_filter: LinearPriceFilter,
651 pub lot_size_filter: LinearLotSizeFilter,
652 pub unified_margin_trade: bool,
653 pub funding_interval: i64,
654 pub settle_coin: Ustr,
655}
656
657#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct BybitInstrumentOption {
664 pub symbol: Ustr,
665 pub status: BybitInstrumentStatus,
666 pub base_coin: Ustr,
667 pub quote_coin: Ustr,
668 pub settle_coin: Ustr,
669 pub options_type: BybitOptionType,
670 pub launch_time: String,
671 pub delivery_time: String,
672 pub delivery_fee_rate: String,
673 pub price_filter: LinearPriceFilter,
674 pub lot_size_filter: OptionLotSizeFilter,
675}
676
677pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
682pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
687pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
692pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
697
698#[derive(Clone, Debug, Serialize, Deserialize)]
703#[serde(rename_all = "camelCase")]
704#[cfg_attr(
705 feature = "python",
706 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
707)]
708#[cfg_attr(
709 feature = "python",
710 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
711)]
712pub struct BybitFeeRate {
713 pub symbol: Ustr,
714 pub taker_fee_rate: String,
715 pub maker_fee_rate: String,
716 #[serde(default)]
717 pub base_coin: Option<Ustr>,
718}
719
720#[cfg(feature = "python")]
721#[pyo3::pymethods]
722impl BybitFeeRate {
723 #[getter]
724 #[must_use]
725 pub fn symbol(&self) -> &str {
726 self.symbol.as_str()
727 }
728
729 #[getter]
730 #[must_use]
731 pub fn taker_fee_rate(&self) -> &str {
732 &self.taker_fee_rate
733 }
734
735 #[getter]
736 #[must_use]
737 pub fn maker_fee_rate(&self) -> &str {
738 &self.maker_fee_rate
739 }
740
741 #[getter]
742 #[must_use]
743 pub fn base_coin(&self) -> Option<&str> {
744 self.base_coin.as_ref().map(|u| u.as_str())
745 }
746}
747
748pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
753
754#[derive(Clone, Debug, Serialize, Deserialize)]
759#[serde(rename_all = "camelCase")]
760pub struct BybitCoinBalance {
761 pub available_to_borrow: String,
762 pub bonus: String,
763 pub accrued_interest: String,
764 pub available_to_withdraw: String,
765 #[serde(default, rename = "totalOrderIM")]
766 pub total_order_im: Option<String>,
767 pub equity: String,
768 pub usd_value: String,
769 pub borrow_amount: String,
770 #[serde(default, rename = "totalPositionMM")]
771 pub total_position_mm: Option<String>,
772 #[serde(default, rename = "totalPositionIM")]
773 pub total_position_im: Option<String>,
774 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
775 pub wallet_balance: Decimal,
776 pub unrealised_pnl: String,
777 pub cum_realised_pnl: String,
778 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
779 pub locked: Decimal,
780 pub collateral_switch: bool,
781 pub margin_collateral: bool,
782 pub coin: Ustr,
783 #[serde(default)]
784 pub spot_hedging_qty: Option<String>,
785 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
786 pub spot_borrow: Decimal,
787}
788
789#[derive(Clone, Debug, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795pub struct BybitWalletBalance {
796 pub total_equity: String,
797 #[serde(rename = "accountIMRate")]
798 pub account_im_rate: String,
799 pub total_margin_balance: String,
800 pub total_initial_margin: String,
801 pub account_type: BybitAccountType,
802 pub total_available_balance: String,
803 #[serde(rename = "accountMMRate")]
804 pub account_mm_rate: String,
805 #[serde(rename = "totalPerpUPL")]
806 pub total_perp_upl: String,
807 pub total_wallet_balance: String,
808 #[serde(rename = "accountLTV")]
809 pub account_ltv: String,
810 pub total_maintenance_margin: String,
811 pub coin: Vec<BybitCoinBalance>,
812}
813
814pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
819
820#[derive(Clone, Debug, Serialize, Deserialize)]
825#[serde(rename_all = "camelCase")]
826pub struct BybitAccountInfo {
827 pub unified_margin_status: BybitUnifiedMarginStatus,
828 pub margin_mode: BybitMarginMode,
829 pub is_master_trader: bool,
830 #[serde(with = "on_off_bool")]
831 pub spot_hedging_status: bool,
832 pub updated_time: String,
833 #[serde(default, with = "on_off_bool")]
836 pub dcp_status: bool,
837 #[serde(default)]
838 pub time_window: i32,
839 #[serde(default)]
840 pub smp_group: i32,
841}
842
843pub type BybitAccountInfoResponse = BybitResponse<BybitAccountInfo>;
848
849#[derive(Clone, Debug, Serialize, Deserialize)]
854#[cfg_attr(
855 feature = "python",
856 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
857)]
858#[cfg_attr(
859 feature = "python",
860 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
861)]
862#[serde(rename_all = "camelCase")]
863pub struct BybitOrder {
864 pub order_id: Ustr,
865 pub order_link_id: Ustr,
866 pub block_trade_id: Option<Ustr>,
867 pub symbol: Ustr,
868 pub price: String,
869 pub qty: String,
870 pub side: BybitOrderSide,
871 pub is_leverage: String,
872 pub position_idx: i32,
873 pub order_status: BybitOrderStatus,
874 pub cancel_type: BybitCancelType,
875 pub reject_reason: Ustr,
876 pub avg_price: Option<String>,
877 pub leaves_qty: String,
878 pub leaves_value: String,
879 pub cum_exec_qty: String,
880 pub cum_exec_value: String,
881 pub cum_exec_fee: String,
882 pub time_in_force: BybitTimeInForce,
883 pub order_type: BybitOrderType,
884 pub stop_order_type: BybitStopOrderType,
885 pub order_iv: Option<String>,
886 pub trigger_price: String,
887 pub take_profit: String,
888 pub stop_loss: String,
889 pub tp_trigger_by: BybitTriggerType,
890 pub sl_trigger_by: BybitTriggerType,
891 pub trigger_direction: BybitTriggerDirection,
892 pub trigger_by: BybitTriggerType,
893 pub last_price_on_created: String,
894 pub reduce_only: bool,
895 pub close_on_trigger: bool,
896 pub smp_type: BybitSmpType,
897 pub smp_group: i32,
898 pub smp_order_id: Ustr,
899 pub tpsl_mode: Option<BybitTpSlMode>,
900 pub tp_limit_price: String,
901 pub sl_limit_price: String,
902 pub place_type: Ustr,
903 pub created_time: String,
904 pub updated_time: String,
905}
906
907#[cfg(feature = "python")]
908#[pyo3::pymethods]
909impl BybitOrder {
910 #[getter]
911 #[must_use]
912 pub fn order_id(&self) -> &str {
913 self.order_id.as_str()
914 }
915
916 #[getter]
917 #[must_use]
918 pub fn order_link_id(&self) -> &str {
919 self.order_link_id.as_str()
920 }
921
922 #[getter]
923 #[must_use]
924 pub fn block_trade_id(&self) -> Option<&str> {
925 self.block_trade_id.as_ref().map(|s| s.as_str())
926 }
927
928 #[getter]
929 #[must_use]
930 pub fn symbol(&self) -> &str {
931 self.symbol.as_str()
932 }
933
934 #[getter]
935 #[must_use]
936 pub fn price(&self) -> &str {
937 &self.price
938 }
939
940 #[getter]
941 #[must_use]
942 pub fn qty(&self) -> &str {
943 &self.qty
944 }
945
946 #[getter]
947 #[must_use]
948 pub fn side(&self) -> BybitOrderSide {
949 self.side
950 }
951
952 #[getter]
953 #[must_use]
954 pub fn is_leverage(&self) -> &str {
955 &self.is_leverage
956 }
957
958 #[getter]
959 #[must_use]
960 pub fn position_idx(&self) -> i32 {
961 self.position_idx
962 }
963
964 #[getter]
965 #[must_use]
966 pub fn order_status(&self) -> BybitOrderStatus {
967 self.order_status
968 }
969
970 #[getter]
971 #[must_use]
972 pub fn cancel_type(&self) -> BybitCancelType {
973 self.cancel_type
974 }
975
976 #[getter]
977 #[must_use]
978 pub fn reject_reason(&self) -> &str {
979 self.reject_reason.as_str()
980 }
981
982 #[getter]
983 #[must_use]
984 pub fn avg_price(&self) -> Option<&str> {
985 self.avg_price.as_deref()
986 }
987
988 #[getter]
989 #[must_use]
990 pub fn leaves_qty(&self) -> &str {
991 &self.leaves_qty
992 }
993
994 #[getter]
995 #[must_use]
996 pub fn leaves_value(&self) -> &str {
997 &self.leaves_value
998 }
999
1000 #[getter]
1001 #[must_use]
1002 pub fn cum_exec_qty(&self) -> &str {
1003 &self.cum_exec_qty
1004 }
1005
1006 #[getter]
1007 #[must_use]
1008 pub fn cum_exec_value(&self) -> &str {
1009 &self.cum_exec_value
1010 }
1011
1012 #[getter]
1013 #[must_use]
1014 pub fn cum_exec_fee(&self) -> &str {
1015 &self.cum_exec_fee
1016 }
1017
1018 #[getter]
1019 #[must_use]
1020 pub fn time_in_force(&self) -> BybitTimeInForce {
1021 self.time_in_force
1022 }
1023
1024 #[getter]
1025 #[must_use]
1026 pub fn order_type(&self) -> BybitOrderType {
1027 self.order_type
1028 }
1029
1030 #[getter]
1031 #[must_use]
1032 pub fn stop_order_type(&self) -> BybitStopOrderType {
1033 self.stop_order_type
1034 }
1035
1036 #[getter]
1037 #[must_use]
1038 pub fn order_iv(&self) -> Option<&str> {
1039 self.order_iv.as_deref()
1040 }
1041
1042 #[getter]
1043 #[must_use]
1044 pub fn trigger_price(&self) -> &str {
1045 &self.trigger_price
1046 }
1047
1048 #[getter]
1049 #[must_use]
1050 pub fn take_profit(&self) -> &str {
1051 &self.take_profit
1052 }
1053
1054 #[getter]
1055 #[must_use]
1056 pub fn stop_loss(&self) -> &str {
1057 &self.stop_loss
1058 }
1059
1060 #[getter]
1061 #[must_use]
1062 pub fn tp_trigger_by(&self) -> BybitTriggerType {
1063 self.tp_trigger_by
1064 }
1065
1066 #[getter]
1067 #[must_use]
1068 pub fn sl_trigger_by(&self) -> BybitTriggerType {
1069 self.sl_trigger_by
1070 }
1071
1072 #[getter]
1073 #[must_use]
1074 pub fn trigger_direction(&self) -> BybitTriggerDirection {
1075 self.trigger_direction
1076 }
1077
1078 #[getter]
1079 #[must_use]
1080 pub fn trigger_by(&self) -> BybitTriggerType {
1081 self.trigger_by
1082 }
1083
1084 #[getter]
1085 #[must_use]
1086 pub fn last_price_on_created(&self) -> &str {
1087 &self.last_price_on_created
1088 }
1089
1090 #[getter]
1091 #[must_use]
1092 pub fn reduce_only(&self) -> bool {
1093 self.reduce_only
1094 }
1095
1096 #[getter]
1097 #[must_use]
1098 pub fn close_on_trigger(&self) -> bool {
1099 self.close_on_trigger
1100 }
1101
1102 #[getter]
1103 #[must_use]
1104 #[expect(
1105 clippy::missing_panics_doc,
1106 reason = "serialization of a simple enum cannot fail"
1107 )]
1108 pub fn smp_type(&self) -> String {
1109 serde_json::to_string(&self.smp_type)
1110 .expect("Failed to serialize BybitSmpType")
1111 .trim_matches('"')
1112 .to_string()
1113 }
1114
1115 #[getter]
1116 #[must_use]
1117 pub fn smp_group(&self) -> i32 {
1118 self.smp_group
1119 }
1120
1121 #[getter]
1122 #[must_use]
1123 pub fn smp_order_id(&self) -> &str {
1124 self.smp_order_id.as_str()
1125 }
1126
1127 #[getter]
1128 #[must_use]
1129 pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1130 self.tpsl_mode
1131 }
1132
1133 #[getter]
1134 #[must_use]
1135 pub fn tp_limit_price(&self) -> &str {
1136 &self.tp_limit_price
1137 }
1138
1139 #[getter]
1140 #[must_use]
1141 pub fn sl_limit_price(&self) -> &str {
1142 &self.sl_limit_price
1143 }
1144
1145 #[getter]
1146 #[must_use]
1147 pub fn place_type(&self) -> &str {
1148 self.place_type.as_str()
1149 }
1150
1151 #[getter]
1152 #[must_use]
1153 pub fn created_time(&self) -> &str {
1154 &self.created_time
1155 }
1156
1157 #[getter]
1158 #[must_use]
1159 pub fn updated_time(&self) -> &str {
1160 &self.updated_time
1161 }
1162}
1163
1164pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1169pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1174
1175#[derive(Clone, Debug, Serialize, Deserialize)]
1180#[serde(rename_all = "camelCase")]
1181pub struct BybitPlaceOrderResult {
1182 pub order_id: Option<Ustr>,
1183 pub order_link_id: Option<Ustr>,
1184}
1185
1186pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1191
1192#[derive(Clone, Debug, Serialize, Deserialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct BybitCancelOrderResult {
1199 pub order_id: Option<Ustr>,
1200 pub order_link_id: Option<Ustr>,
1201}
1202
1203pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1208
1209#[derive(Clone, Debug, Serialize, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub struct BybitExecution {
1216 pub symbol: Ustr,
1217 pub order_id: Ustr,
1218 pub order_link_id: Ustr,
1219 pub side: BybitOrderSide,
1220 pub order_price: String,
1221 pub order_qty: String,
1222 pub leaves_qty: String,
1223 pub create_type: Option<BybitCreateType>,
1224 pub order_type: BybitOrderType,
1225 pub stop_order_type: Option<BybitStopOrderType>,
1226 pub exec_fee: String,
1227 pub exec_id: String,
1228 pub exec_price: String,
1229 pub exec_qty: String,
1230 pub exec_type: BybitExecType,
1231 pub exec_value: String,
1232 pub exec_time: String,
1233 pub fee_currency: Ustr,
1234 pub is_maker: bool,
1235 pub fee_rate: String,
1236 pub trade_iv: String,
1237 pub mark_iv: String,
1238 pub mark_price: String,
1239 pub index_price: String,
1240 pub underlying_price: String,
1241 pub block_trade_id: String,
1242 pub closed_size: String,
1243 pub seq: i64,
1244}
1245
1246pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1251
1252#[derive(Clone, Debug, Serialize, Deserialize)]
1257#[serde(rename_all = "camelCase")]
1258pub struct BybitPosition {
1259 pub position_idx: BybitPositionIdx,
1260 pub risk_id: i32,
1261 pub risk_limit_value: String,
1262 pub symbol: Ustr,
1263 pub side: BybitPositionSide,
1264 pub size: String,
1265 pub avg_price: String,
1266 pub position_value: String,
1267 pub trade_mode: i32,
1268 pub position_status: BybitPositionStatus,
1269 pub auto_add_margin: i32,
1270 pub adl_rank_indicator: i32,
1271 pub leverage: String,
1272 pub position_balance: String,
1273 pub mark_price: String,
1274 pub liq_price: String,
1275 pub bust_price: String,
1276 #[serde(rename = "positionMM")]
1277 pub position_mm: String,
1278 #[serde(rename = "positionIM")]
1279 pub position_im: String,
1280 pub tpsl_mode: BybitTpSlMode,
1281 pub take_profit: String,
1282 pub stop_loss: String,
1283 pub trailing_stop: String,
1284 pub unrealised_pnl: String,
1285 pub cur_realised_pnl: String,
1286 pub cum_realised_pnl: String,
1287 #[serde(default = "default_position_seq")]
1288 pub seq: i64,
1289 #[serde(default)]
1290 pub is_reduce_only: bool,
1291 #[serde(default)]
1292 pub mmr_sys_updated_time: String,
1293 #[serde(default)]
1294 pub leverage_sys_updated_time: String,
1295 pub created_time: String,
1296 pub updated_time: String,
1297}
1298
1299const fn default_position_seq() -> i64 {
1300 -1
1301}
1302
1303pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1308
1309#[derive(Clone, Debug, Serialize, Deserialize)]
1314#[serde(rename_all = "camelCase")]
1315pub struct BybitSetMarginModeReason {
1316 pub reason_code: String,
1317 pub reason_msg: String,
1318}
1319
1320#[derive(Clone, Debug, Serialize, Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326pub struct BybitSetMarginModeResult {
1327 #[serde(default)]
1328 pub reasons: Vec<BybitSetMarginModeReason>,
1329}
1330
1331pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1336
1337#[derive(Clone, Debug, Serialize, Deserialize)]
1339pub struct BybitSetLeverageResult {}
1340
1341pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1346
1347#[derive(Clone, Debug, Serialize, Deserialize)]
1349pub struct BybitSwitchModeResult {}
1350
1351pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1356
1357#[derive(Clone, Debug, Serialize, Deserialize)]
1359pub struct BybitSetTradingStopResult {}
1360
1361pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1366
1367#[derive(Clone, Debug, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct BybitBorrowResult {
1371 pub coin: Ustr,
1372 pub amount: String,
1373}
1374
1375pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1381
1382#[derive(Clone, Debug, Serialize, Deserialize)]
1384#[serde(rename_all = "camelCase")]
1385pub struct BybitNoConvertRepayResult {
1386 pub result_status: String,
1387}
1388
1389pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1395
1396#[derive(Clone, Debug, Serialize, Deserialize)]
1398#[cfg_attr(
1399 feature = "python",
1400 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1401)]
1402#[cfg_attr(
1403 feature = "python",
1404 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1405)]
1406#[serde(rename_all = "PascalCase")]
1407pub struct BybitApiKeyPermissions {
1408 #[serde(default)]
1409 pub contract_trade: Vec<String>,
1410 #[serde(default)]
1411 pub spot: Vec<String>,
1412 #[serde(default)]
1413 pub wallet: Vec<String>,
1414 #[serde(default)]
1415 pub options: Vec<String>,
1416 #[serde(default)]
1417 pub derivatives: Vec<String>,
1418 #[serde(default)]
1419 pub exchange: Vec<String>,
1420 #[serde(default)]
1421 pub copy_trading: Vec<String>,
1422 #[serde(default)]
1423 pub block_trade: Vec<String>,
1424 #[serde(rename = "NFT", default)]
1427 pub nft: Vec<String>,
1428 #[serde(default)]
1429 pub affiliate: Vec<String>,
1430 #[serde(default)]
1434 pub earn: Vec<String>,
1435 #[serde(rename = "FiatP2P", default)]
1437 pub fiat_p2p: Vec<String>,
1438 #[serde(default)]
1439 pub fiat_bybit_pay: Vec<String>,
1440 #[serde(default)]
1441 pub fiat_bit_pay: Vec<String>,
1442 #[serde(default)]
1443 pub fiat_global_pay: Vec<String>,
1444 #[serde(default)]
1445 pub fiat_convert_broker: Vec<String>,
1446 #[serde(default)]
1447 pub bit_card: Vec<String>,
1448 #[serde(rename = "ByXPost", default)]
1450 pub byx_post: Vec<String>,
1451}
1452
1453#[derive(Clone, Debug, Serialize, Deserialize)]
1455#[cfg_attr(
1456 feature = "python",
1457 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1458)]
1459#[cfg_attr(
1460 feature = "python",
1461 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1462)]
1463#[serde(rename_all = "camelCase")]
1464pub struct BybitAccountDetails {
1465 pub id: String,
1466 pub note: String,
1467 pub api_key: String,
1468 pub read_only: u8,
1469 pub secret: String,
1470 #[serde(rename = "type")]
1471 pub key_type: u8,
1472 pub permissions: BybitApiKeyPermissions,
1473 pub ips: Vec<String>,
1474 #[serde(default)]
1475 pub user_id: Option<u64>,
1476 #[serde(default)]
1477 pub inviter_id: Option<u64>,
1478 pub vip_level: String,
1479 #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1480 pub mkt_maker_level: u8,
1481 #[serde(default)]
1482 pub affiliate_id: Option<u64>,
1483 pub rsa_public_key: String,
1484 pub is_master: bool,
1485 pub parent_uid: String,
1486 pub uta: u8,
1487 pub kyc_level: String,
1488 pub kyc_region: String,
1489 #[serde(default)]
1490 pub unified: Option<i32>,
1491 #[serde(default)]
1492 pub deadline_day: i64,
1493 #[serde(default)]
1494 pub expired_at: Option<String>,
1495 pub created_at: String,
1496}
1497
1498#[cfg(feature = "python")]
1499#[pyo3::pymethods]
1500impl BybitAccountDetails {
1501 #[getter]
1502 #[must_use]
1503 pub fn id(&self) -> &str {
1504 &self.id
1505 }
1506
1507 #[getter]
1508 #[must_use]
1509 pub fn note(&self) -> &str {
1510 &self.note
1511 }
1512
1513 #[getter]
1514 #[must_use]
1515 pub fn api_key(&self) -> &str {
1516 &self.api_key
1517 }
1518
1519 #[getter]
1520 #[must_use]
1521 pub fn read_only(&self) -> u8 {
1522 self.read_only
1523 }
1524
1525 #[getter]
1526 #[must_use]
1527 pub fn key_type(&self) -> u8 {
1528 self.key_type
1529 }
1530
1531 #[getter]
1532 #[must_use]
1533 pub fn user_id(&self) -> Option<u64> {
1534 self.user_id
1535 }
1536
1537 #[getter]
1538 #[must_use]
1539 pub fn inviter_id(&self) -> Option<u64> {
1540 self.inviter_id
1541 }
1542
1543 #[getter]
1544 #[must_use]
1545 pub fn vip_level(&self) -> &str {
1546 &self.vip_level
1547 }
1548
1549 #[getter]
1550 #[must_use]
1551 pub fn mkt_maker_level(&self) -> u8 {
1552 self.mkt_maker_level
1553 }
1554
1555 #[getter]
1556 #[must_use]
1557 pub fn affiliate_id(&self) -> Option<u64> {
1558 self.affiliate_id
1559 }
1560
1561 #[getter]
1562 #[must_use]
1563 pub fn rsa_public_key(&self) -> &str {
1564 &self.rsa_public_key
1565 }
1566
1567 #[getter]
1568 #[must_use]
1569 pub fn is_master(&self) -> bool {
1570 self.is_master
1571 }
1572
1573 #[getter]
1574 #[must_use]
1575 pub fn parent_uid(&self) -> &str {
1576 &self.parent_uid
1577 }
1578
1579 #[getter]
1580 #[must_use]
1581 pub fn uta(&self) -> u8 {
1582 self.uta
1583 }
1584
1585 #[getter]
1586 #[must_use]
1587 pub fn kyc_level(&self) -> &str {
1588 &self.kyc_level
1589 }
1590
1591 #[getter]
1592 #[must_use]
1593 pub fn kyc_region(&self) -> &str {
1594 &self.kyc_region
1595 }
1596
1597 #[getter]
1598 #[must_use]
1599 pub fn deadline_day(&self) -> i64 {
1600 self.deadline_day
1601 }
1602
1603 #[getter]
1604 #[must_use]
1605 pub fn expired_at(&self) -> Option<&str> {
1606 self.expired_at.as_deref()
1607 }
1608
1609 #[getter]
1610 #[must_use]
1611 pub fn created_at(&self) -> &str {
1612 &self.created_at
1613 }
1614}
1615
1616pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1622
1623#[derive(Clone, Debug, Serialize, Deserialize)]
1635#[serde(rename_all = "camelCase")]
1636pub struct BybitSubMember {
1637 pub uid: String,
1638 pub username: String,
1639 pub member_type: i32,
1640 pub status: i32,
1641 pub account_mode: i32,
1642 #[serde(default)]
1643 pub remark: String,
1644}
1645
1646#[derive(Clone, Debug, Serialize, Deserialize)]
1648#[serde(rename_all = "camelCase")]
1649pub struct BybitSubMembersResult {
1650 #[serde(default)]
1651 pub sub_members: Vec<BybitSubMember>,
1652}
1653
1654pub type BybitSubMembersResponse = BybitResponse<BybitSubMembersResult>;
1660
1661#[derive(Clone, Debug, Serialize, Deserialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct BybitSubMembersPagedResult {
1671 #[serde(default)]
1672 pub sub_members: Vec<BybitSubMember>,
1673 #[serde(default)]
1674 pub next_cursor: Option<String>,
1675}
1676
1677impl BybitSubMembersPagedResult {
1678 #[must_use]
1685 pub fn continuation_cursor(&self) -> Option<&str> {
1686 match self.next_cursor.as_deref() {
1687 None | Some("" | "0") => None,
1688 Some(cursor) => Some(cursor),
1689 }
1690 }
1691
1692 #[must_use]
1694 pub fn has_more_pages(&self) -> bool {
1695 self.continuation_cursor().is_some()
1696 }
1697}
1698
1699pub type BybitSubMembersPagedResponse = BybitResponse<BybitSubMembersPagedResult>;
1705
1706pub type BybitEscrowSubMembersResponse = BybitResponse<BybitSubMembersPagedResult>;
1713
1714#[derive(Clone, Debug, Serialize, Deserialize)]
1723#[serde(rename_all = "camelCase")]
1724pub struct BybitSubApiKeyInfo {
1725 pub id: String,
1726 #[serde(default)]
1727 pub ips: Vec<String>,
1728 pub api_key: String,
1729 #[serde(default)]
1730 pub note: String,
1731 pub status: i32,
1732 #[serde(default)]
1733 pub expired_at: Option<String>,
1734 pub created_at: String,
1735 #[serde(rename = "type")]
1736 pub key_type: BybitApiKeyType,
1737 #[serde(with = "masked_secret")]
1738 pub secret: Option<String>,
1739 #[serde(with = "bool_or_int")]
1740 pub read_only: bool,
1741 #[serde(default)]
1742 pub deadline_day: Option<i64>,
1743 #[serde(default)]
1744 pub flag: String,
1745 pub permissions: BybitApiKeyPermissions,
1746}
1747
1748#[derive(Clone, Debug, Serialize, Deserialize)]
1754#[serde(rename_all = "camelCase")]
1755pub struct BybitSubApiKeysResult {
1756 #[serde(rename = "result", default)]
1757 pub keys: Vec<BybitSubApiKeyInfo>,
1758 #[serde(default)]
1759 pub next_page_cursor: Option<String>,
1760}
1761
1762impl BybitSubApiKeysResult {
1763 #[must_use]
1769 pub fn continuation_cursor(&self) -> Option<&str> {
1770 match self.next_page_cursor.as_deref() {
1771 None | Some("") => None,
1772 Some(cursor) => Some(cursor),
1773 }
1774 }
1775
1776 #[must_use]
1778 pub fn has_more_pages(&self) -> bool {
1779 self.continuation_cursor().is_some()
1780 }
1781}
1782
1783pub type BybitSubApiKeysResponse = BybitResponse<BybitSubApiKeysResult>;
1789
1790#[derive(Clone, Debug, Serialize, Deserialize)]
1797#[serde(rename_all = "camelCase")]
1798pub struct BybitApiKeyUpdateResult {
1799 pub id: String,
1800 #[serde(default)]
1801 pub note: String,
1802 pub api_key: String,
1803 #[serde(with = "bool_or_int")]
1804 pub read_only: bool,
1805 #[serde(with = "masked_secret")]
1806 pub secret: Option<String>,
1807 pub permissions: BybitApiKeyPermissions,
1808 #[serde(default)]
1809 pub ips: Vec<String>,
1810}
1811
1812pub type BybitUpdateSubApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1818
1819pub type BybitUpdateMasterApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1825
1826#[cfg(test)]
1827mod tests {
1828 use nautilus_core::UnixNanos;
1829 use nautilus_model::identifiers::AccountId;
1830 use rstest::rstest;
1831 use rust_decimal::Decimal;
1832 use rust_decimal_macros::dec;
1833
1834 use super::*;
1835 use crate::common::testing::load_test_json;
1836
1837 #[rstest]
1838 fn deserialize_spot_instrument_uses_enums() {
1839 let json = load_test_json("http_get_instruments_spot.json");
1840 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1841 let instrument = &response.result.list[0];
1842
1843 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1844 assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1845 assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1846 }
1847
1848 #[rstest]
1849 fn deserialize_linear_instrument_status() {
1850 let json = load_test_json("http_get_instruments_linear.json");
1851 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1852 let instrument = &response.result.list[0];
1853
1854 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1855 assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1856 }
1857
1858 #[rstest]
1859 fn deserialize_account_info_response() {
1860 let json = load_test_json("http_get_account_info.json");
1861 let response: BybitAccountInfoResponse = serde_json::from_str(&json).unwrap();
1862
1863 assert_eq!(response.result.margin_mode, BybitMarginMode::RegularMargin);
1864 assert_eq!(
1865 response.result.unified_margin_status,
1866 BybitUnifiedMarginStatus::UnifiedTradingAccount10Pro
1867 );
1868 assert!(!response.result.is_master_trader);
1869 assert!(!response.result.spot_hedging_status);
1870 assert!(!response.result.dcp_status);
1871 assert_eq!(response.result.time_window, 10);
1872 assert_eq!(response.result.smp_group, 0);
1873 }
1874
1875 #[rstest]
1876 fn deserialize_account_info_without_deprecated_fields() {
1877 let json = r#"{
1878 "retCode": 0,
1879 "retMsg": "OK",
1880 "result": {
1881 "marginMode": "PORTFOLIO_MARGIN",
1882 "updatedTime": "1697078946000",
1883 "unifiedMarginStatus": 5,
1884 "isMasterTrader": true,
1885 "spotHedgingStatus": "ON"
1886 }
1887 }"#;
1888 let response: BybitAccountInfoResponse = serde_json::from_str(json).unwrap();
1889
1890 assert_eq!(
1891 response.result.margin_mode,
1892 BybitMarginMode::PortfolioMargin
1893 );
1894 assert_eq!(
1895 response.result.unified_margin_status,
1896 BybitUnifiedMarginStatus::UnifiedTradingAccount20
1897 );
1898 assert!(response.result.is_master_trader);
1899 assert!(response.result.spot_hedging_status);
1900 assert!(!response.result.dcp_status);
1901 assert_eq!(response.result.time_window, 0);
1902 assert_eq!(response.result.smp_group, 0);
1903 }
1904
1905 #[rstest]
1906 fn deserialize_order_response_maps_enums() {
1907 let json = load_test_json("http_get_orders_history.json");
1908 let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1909 let order = &response.result.list[0];
1910
1911 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1912 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1913 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1914 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1915 assert_eq!(order.order_type, BybitOrderType::Limit);
1916 assert_eq!(order.smp_type, BybitSmpType::None);
1917 }
1918
1919 #[rstest]
1920 fn deserialize_wallet_balance_without_optional_fields() {
1921 let json = r#"{
1922 "retCode": 0,
1923 "retMsg": "OK",
1924 "result": {
1925 "list": [{
1926 "totalEquity": "1000.00",
1927 "accountIMRate": "0",
1928 "totalMarginBalance": "1000.00",
1929 "totalInitialMargin": "0",
1930 "accountType": "UNIFIED",
1931 "totalAvailableBalance": "1000.00",
1932 "accountMMRate": "0",
1933 "totalPerpUPL": "0",
1934 "totalWalletBalance": "1000.00",
1935 "accountLTV": "0",
1936 "totalMaintenanceMargin": "0",
1937 "coin": [{
1938 "availableToBorrow": "0",
1939 "bonus": "0",
1940 "accruedInterest": "0",
1941 "availableToWithdraw": "1000.00",
1942 "equity": "1000.00",
1943 "usdValue": "1000.00",
1944 "borrowAmount": "0",
1945 "totalPositionIM": "0",
1946 "walletBalance": "1000.00",
1947 "unrealisedPnl": "0",
1948 "cumRealisedPnl": "0",
1949 "locked": "0",
1950 "collateralSwitch": true,
1951 "marginCollateral": true,
1952 "coin": "USDT"
1953 }]
1954 }]
1955 }
1956 }"#;
1957
1958 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1959 .expect("Failed to parse wallet balance without optional fields");
1960
1961 assert_eq!(response.ret_code, 0);
1962 assert_eq!(response.result.list[0].coin[0].total_order_im, None);
1963 assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
1964 }
1965
1966 #[rstest]
1967 fn deserialize_wallet_balance_from_docs() {
1968 let json = include_str!("../../test_data/http_get_wallet_balance.json");
1969
1970 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1971 .expect("Failed to parse wallet balance from Bybit docs example");
1972
1973 assert_eq!(response.ret_code, 0);
1974 assert_eq!(response.ret_msg, "OK");
1975
1976 let wallet = &response.result.list[0];
1977 assert_eq!(wallet.total_equity, "3.31216591");
1978 assert_eq!(wallet.account_im_rate, "0");
1979 assert_eq!(wallet.account_mm_rate, "0");
1980 assert_eq!(wallet.total_perp_upl, "0");
1981 assert_eq!(wallet.account_ltv, "0");
1982
1983 let btc = &wallet.coin[0];
1985 assert_eq!(btc.coin.as_str(), "BTC");
1986 assert_eq!(btc.available_to_borrow, "3");
1987 assert_eq!(btc.total_order_im, Some("0".to_string()));
1988 assert_eq!(btc.total_position_mm, Some("0".to_string()));
1989 assert_eq!(btc.total_position_im, Some("0".to_string()));
1990
1991 let usdt = &wallet.coin[1];
1993 assert_eq!(usdt.coin.as_str(), "USDT");
1994 assert_eq!(usdt.wallet_balance, dec!(1000.50));
1995 assert_eq!(usdt.total_order_im, None);
1996 assert_eq!(usdt.total_position_mm, None);
1997 assert_eq!(usdt.total_position_im, None);
1998 assert_eq!(btc.spot_borrow, Decimal::ZERO);
1999 assert_eq!(usdt.spot_borrow, Decimal::ZERO);
2000 }
2001
2002 #[rstest]
2003 fn test_parse_wallet_balance_with_spot_borrow() {
2004 let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
2005 let response: BybitWalletBalanceResponse =
2006 serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
2007
2008 let wallet = &response.result.list[0];
2009 let usdt = &wallet.coin[0];
2010
2011 assert_eq!(usdt.coin.as_str(), "USDT");
2012 assert_eq!(usdt.wallet_balance, dec!(1200.00));
2013 assert_eq!(usdt.spot_borrow, dec!(200.00));
2014 assert_eq!(usdt.borrow_amount, "200.00");
2015
2016 let account_id = crate::common::parse::parse_account_state(
2018 wallet,
2019 AccountId::new("BYBIT-001"),
2020 UnixNanos::default(),
2021 )
2022 .expect("Failed to parse account state");
2023
2024 let balance = &account_id.balances[0];
2025 assert_eq!(balance.total.as_f64(), 1000.0);
2026 }
2027
2028 #[rstest]
2029 fn test_parse_wallet_balance_spot_short() {
2030 let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
2031 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2032 .expect("Failed to parse wallet balance with SHORT SPOT position");
2033
2034 let wallet = &response.result.list[0];
2035 let eth = &wallet.coin[0];
2036
2037 assert_eq!(eth.coin.as_str(), "ETH");
2038 assert_eq!(eth.wallet_balance, dec!(0));
2039 assert_eq!(eth.spot_borrow, dec!(0.06142));
2040 assert_eq!(eth.borrow_amount, "0.06142");
2041
2042 let account_state = crate::common::parse::parse_account_state(
2043 wallet,
2044 AccountId::new("BYBIT-001"),
2045 UnixNanos::default(),
2046 )
2047 .expect("Failed to parse account state");
2048
2049 let eth_balance = account_state
2050 .balances
2051 .iter()
2052 .find(|b| b.currency.code.as_str() == "ETH")
2053 .expect("ETH balance not found");
2054
2055 assert_eq!(eth_balance.total.as_f64(), -0.06142);
2057 }
2058
2059 #[rstest]
2060 fn deserialize_borrow_response() {
2061 let json = r#"{
2062 "retCode": 0,
2063 "retMsg": "success",
2064 "result": {
2065 "coin": "BTC",
2066 "amount": "0.01"
2067 },
2068 "retExtInfo": {},
2069 "time": 1756197991955
2070 }"#;
2071
2072 let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
2073
2074 assert_eq!(response.ret_code, 0);
2075 assert_eq!(response.ret_msg, "success");
2076 assert_eq!(response.result.coin, "BTC");
2077 assert_eq!(response.result.amount, "0.01");
2078 }
2079
2080 #[rstest]
2081 fn deserialize_no_convert_repay_response() {
2082 let json = r#"{
2083 "retCode": 0,
2084 "retMsg": "OK",
2085 "result": {
2086 "resultStatus": "SU"
2087 },
2088 "retExtInfo": {},
2089 "time": 1234567890
2090 }"#;
2091
2092 let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
2093
2094 assert_eq!(response.ret_code, 0);
2095 assert_eq!(response.ret_msg, "OK");
2096 assert_eq!(response.result.result_status, "SU");
2097 }
2098
2099 #[rstest]
2100 fn deserialize_position_without_conditional_fields() {
2101 let json = r#"{
2105 "retCode": 0,
2106 "retMsg": "OK",
2107 "result": {
2108 "list": [{
2109 "positionIdx": 0,
2110 "riskId": 1,
2111 "riskLimitValue": "150",
2112 "symbol": "LTCUSDT",
2113 "side": "",
2114 "size": "0",
2115 "avgPrice": "0",
2116 "positionValue": "0",
2117 "tradeMode": 0,
2118 "positionStatus": "Normal",
2119 "autoAddMargin": 0,
2120 "adlRankIndicator": 0,
2121 "leverage": "10",
2122 "positionBalance": "0",
2123 "markPrice": "70.00",
2124 "liqPrice": "",
2125 "bustPrice": "",
2126 "positionMM": "0",
2127 "positionIM": "0",
2128 "tpslMode": "Full",
2129 "takeProfit": "0",
2130 "stopLoss": "0",
2131 "trailingStop": "0",
2132 "unrealisedPnl": "0",
2133 "curRealisedPnl": "0",
2134 "cumRealisedPnl": "0",
2135 "createdTime": "1676538056258",
2136 "updatedTime": "1697673600012"
2137 }],
2138 "nextPageCursor": "",
2139 "category": "linear"
2140 },
2141 "retExtInfo": {},
2142 "time": 1697673900000
2143 }"#;
2144
2145 let response: BybitPositionListResponse = serde_json::from_str(json)
2146 .expect("Failed to parse position list with missing conditional fields");
2147
2148 let position = &response.result.list[0];
2149 assert!(!position.is_reduce_only);
2150 assert_eq!(position.seq, -1);
2151 assert_eq!(position.mmr_sys_updated_time, "");
2152 assert_eq!(position.leverage_sys_updated_time, "");
2153 }
2154
2155 #[rstest]
2156 fn deserialize_sub_members_response() {
2157 let json = load_test_json("http_get_user_sub_members.json");
2158 let response: BybitSubMembersResponse =
2159 serde_json::from_str(&json).expect("parse sub members");
2160 assert_eq!(response.ret_code, 0);
2161 assert_eq!(response.result.sub_members.len(), 2);
2162 let first = &response.result.sub_members[0];
2163 assert_eq!(first.uid, "106314365");
2164 assert_eq!(first.username, "xxxx02");
2165 assert_eq!(first.member_type, 1);
2166 assert_eq!(first.status, 1);
2167 assert_eq!(first.account_mode, 5);
2168 assert_eq!(first.remark, "");
2169 let second = &response.result.sub_members[1];
2170 assert_eq!(second.uid, "106279879");
2171 assert_eq!(second.account_mode, 6);
2172 }
2173
2174 #[rstest]
2175 fn deserialize_sub_members_paged_response() {
2176 let json = load_test_json("http_get_user_sub_members_paged.json");
2179 let response: BybitSubMembersPagedResponse =
2180 serde_json::from_str(&json).expect("parse paged sub members");
2181 assert_eq!(response.result.sub_members.len(), 2);
2182 assert_eq!(response.result.next_cursor.as_deref(), Some("0"));
2183 assert!(!response.result.has_more_pages());
2184 assert_eq!(response.result.continuation_cursor(), None);
2185 }
2186
2187 #[rstest]
2188 fn deserialize_escrow_sub_members_response_uses_same_shape() {
2189 let json = load_test_json("http_get_user_escrow_sub_members.json");
2192 let response: BybitEscrowSubMembersResponse =
2193 serde_json::from_str(&json).expect("parse escrow sub members");
2194 assert_eq!(response.result.sub_members.len(), 2);
2195 assert_eq!(response.result.sub_members[0].member_type, 12);
2196 assert_eq!(response.result.sub_members[0].remark, "earn fund");
2197 assert_eq!(response.result.next_cursor.as_deref(), Some("344"));
2198 assert!(response.result.has_more_pages());
2199 assert_eq!(response.result.continuation_cursor(), Some("344"));
2200 }
2201
2202 #[rstest]
2203 fn deserialize_sub_api_keys_response() {
2204 let json = load_test_json("http_get_user_sub_apikeys.json");
2207 let response: BybitSubApiKeysResponse =
2208 serde_json::from_str(&json).expect("parse sub apikeys");
2209 assert_eq!(response.result.keys.len(), 1);
2210 let key = &response.result.keys[0];
2211 assert!(!key.read_only);
2212 assert_eq!(key.secret, None);
2213 assert_eq!(key.key_type, BybitApiKeyType::Hmac);
2214 assert_eq!(key.flag, "hmac");
2215 assert_eq!(key.deadline_day, Some(21));
2216 assert_eq!(key.permissions.contract_trade, vec!["Order", "Position"]);
2217 assert_eq!(key.permissions.spot, vec!["SpotTrade"]);
2218 assert!(key.permissions.earn.is_empty());
2219 assert_eq!(response.result.next_page_cursor.as_deref(), Some(""));
2220 assert!(!response.result.has_more_pages());
2221 }
2222
2223 #[rstest]
2224 fn deserialize_update_sub_api_response() {
2225 let json = load_test_json("http_post_user_update_sub_api.json");
2226 let response: BybitUpdateSubApiResponse =
2227 serde_json::from_str(&json).expect("parse update sub api");
2228 assert!(!response.result.read_only);
2229 assert_eq!(response.result.secret, None);
2230 assert_eq!(response.result.ips, vec!["*"]);
2231 assert_eq!(response.result.permissions.spot, vec!["SpotTrade"]);
2232 assert_eq!(response.result.permissions.wallet, vec!["AccountTransfer"]);
2233 }
2234
2235 #[rstest]
2236 fn deserialize_update_master_api_response() {
2237 let json = load_test_json("http_post_user_update_master_api.json");
2242 let response: BybitUpdateMasterApiResponse =
2243 serde_json::from_str(&json).expect("parse update master api");
2244 assert!(!response.result.read_only);
2245 assert_eq!(response.result.ips, vec!["*"]);
2246 let perms = &response.result.permissions;
2247 assert_eq!(perms.contract_trade, vec!["Order", "Position"]);
2248 assert_eq!(perms.copy_trading, vec!["CopyTrading"]);
2249 assert!(perms.earn.is_empty());
2250 assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2251 }
2252
2253 #[rstest]
2254 fn deserialize_permissions_renamed_buckets_preserve_values() {
2255 let json = r#"{
2261 "NFT": ["NFTQueryProductList"],
2262 "FiatP2P": ["P2PDeposit"],
2263 "ByXPost": ["PostContent"]
2264 }"#;
2265 let perms: BybitApiKeyPermissions =
2266 serde_json::from_str(json).expect("parse renamed buckets");
2267 assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2268 assert_eq!(perms.fiat_p2p, vec!["P2PDeposit"]);
2269 assert_eq!(perms.byx_post, vec!["PostContent"]);
2270 }
2271
2272 #[rstest]
2273 fn deserialize_account_details_response_with_current_docs_example() {
2274 let json = load_test_json("http_get_user_query_api.json");
2275 let response: BybitAccountDetailsResponse =
2276 serde_json::from_str(&json).expect("parse account details");
2277
2278 assert_eq!(
2279 response.result.permissions.fiat_global_pay,
2280 Vec::<String>::new()
2281 );
2282 assert_eq!(
2283 response.result.permissions.fiat_bit_pay,
2284 vec!["FaitPayOrder"]
2285 );
2286 assert_eq!(response.result.permissions.bit_card, vec!["BitCard"]);
2287 assert_eq!(response.result.permissions.byx_post, vec!["ByXPost"]);
2288 assert_eq!(response.result.unified, Some(0));
2289 }
2290}