1use nautilus_core::string::secret::SecretString;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use ustr::Ustr;
22
23use crate::common::{
24 enums::{
25 BybitAccountType, BybitApiKeyType, BybitCancelType, BybitContractType, BybitCreateType,
26 BybitExecType, BybitInnovationFlag, BybitInstrumentStatus, BybitMarginMode,
27 BybitMarginTrading, BybitOptionType, BybitOrderSide, BybitOrderStatus, BybitOrderType,
28 BybitPositionIdx, BybitPositionSide, BybitPositionStatus, BybitProductType,
29 BybitRepayStatus, BybitSmpType, BybitStopOrderType, BybitSymbolType, BybitTimeInForce,
30 BybitTpSlMode, BybitTriggerDirection, BybitTriggerType, BybitUnifiedMarginStatus,
31 },
32 models::{
33 BybitCursorList, BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
34 LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
35 SpotPriceFilter,
36 },
37 parse::{
38 bool_or_int, deserialize_decimal_or_zero, deserialize_i32_or_string,
39 deserialize_optional_decimal_or_zero, deserialize_string_to_u8, masked_secret, on_off_bool,
40 },
41};
42
43#[derive(Clone, Debug, Default, Serialize, Deserialize)]
45#[cfg_attr(
46 feature = "python",
47 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
48)]
49#[cfg_attr(
50 feature = "python",
51 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
52)]
53pub struct BybitOrderCursorList {
54 pub list: Vec<BybitOrder>,
56 pub next_page_cursor: Option<String>,
58 #[serde(default)]
60 pub category: Option<BybitProductType>,
61}
62
63impl From<BybitCursorList<BybitOrder>> for BybitOrderCursorList {
64 fn from(cursor_list: BybitCursorList<BybitOrder>) -> Self {
65 Self {
66 list: cursor_list.list,
67 next_page_cursor: cursor_list.next_page_cursor,
68 category: cursor_list.category,
69 }
70 }
71}
72
73#[cfg(feature = "python")]
74#[pyo3::pymethods]
75impl BybitOrderCursorList {
76 #[getter]
77 #[must_use]
78 pub fn list(&self) -> Vec<BybitOrder> {
79 self.list.clone()
80 }
81
82 #[getter]
83 #[must_use]
84 pub fn next_page_cursor(&self) -> Option<&str> {
85 self.next_page_cursor.as_deref()
86 }
87
88 #[getter]
89 #[must_use]
90 pub fn category(&self) -> Option<BybitProductType> {
91 self.category
92 }
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
100#[cfg_attr(
101 feature = "python",
102 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
103)]
104#[cfg_attr(
105 feature = "python",
106 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
107)]
108#[serde(rename_all = "camelCase")]
109pub struct BybitServerTime {
110 pub time_second: String,
112 pub time_nano: String,
114}
115
116#[cfg(feature = "python")]
117#[pyo3::pymethods]
118impl BybitServerTime {
119 #[getter]
120 #[must_use]
121 pub fn time_second(&self) -> &str {
122 &self.time_second
123 }
124
125 #[getter]
126 #[must_use]
127 pub fn time_nano(&self) -> &str {
128 &self.time_nano
129 }
130}
131
132pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct BybitTickerSpot {
145 pub symbol: Ustr,
146 pub bid1_price: String,
147 pub bid1_size: String,
148 pub ask1_price: String,
149 pub ask1_size: String,
150 pub last_price: String,
151 pub prev_price24h: String,
152 pub price24h_pcnt: String,
153 pub high_price24h: String,
154 pub low_price24h: String,
155 pub turnover24h: String,
156 pub volume24h: String,
157 #[serde(default)]
158 pub usd_index_price: String,
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct BybitTickerLinear {
168 pub symbol: Ustr,
169 pub last_price: String,
170 pub index_price: String,
171 pub mark_price: String,
172 pub prev_price24h: String,
173 pub price24h_pcnt: String,
174 pub high_price24h: String,
175 pub low_price24h: String,
176 pub prev_price1h: String,
177 pub open_interest: String,
178 pub open_interest_value: String,
179 pub turnover24h: String,
180 pub volume24h: String,
181 pub funding_rate: String,
182 pub next_funding_time: String,
183 pub predicted_delivery_price: String,
184 pub basis_rate: String,
185 pub delivery_fee_rate: String,
186 pub delivery_time: String,
187 pub ask1_size: String,
188 pub bid1_price: String,
189 pub ask1_price: String,
190 pub bid1_size: String,
191 pub basis: String,
192}
193
194#[derive(Clone, Debug, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct BybitTickerOption {
201 pub symbol: Ustr,
202 pub bid1_price: String,
203 pub bid1_size: String,
204 pub bid1_iv: String,
205 pub ask1_price: String,
206 pub ask1_size: String,
207 pub ask1_iv: String,
208 pub last_price: String,
209 pub high_price24h: String,
210 pub low_price24h: String,
211 pub mark_price: String,
212 pub index_price: String,
213 pub mark_iv: String,
214 pub underlying_price: String,
215 pub open_interest: String,
216 pub turnover24h: String,
217 pub volume24h: String,
218 pub total_volume: String,
219 pub total_turnover: String,
220 pub delta: String,
221 pub gamma: String,
222 pub vega: String,
223 pub theta: String,
224 pub predicted_delivery_price: String,
225 pub change24h: String,
226}
227
228pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
233pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
238pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
243
244#[derive(Clone, Debug, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250#[cfg_attr(
251 feature = "python",
252 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
253)]
254#[cfg_attr(
255 feature = "python",
256 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
257)]
258pub struct BybitTickerData {
259 pub symbol: Ustr,
260 pub bid1_price: String,
261 pub bid1_size: String,
262 pub ask1_price: String,
263 pub ask1_size: String,
264 pub last_price: String,
265 pub high_price24h: String,
266 pub low_price24h: String,
267 pub turnover24h: String,
268 pub volume24h: String,
269 #[serde(default)]
270 pub open_interest: Option<String>,
271 #[serde(default)]
272 pub funding_rate: Option<String>,
273 #[serde(default)]
274 pub next_funding_time: Option<String>,
275 #[serde(default)]
276 pub mark_price: Option<String>,
277 #[serde(default)]
278 pub index_price: Option<String>,
279}
280
281#[cfg(feature = "python")]
282#[pyo3::pymethods]
283impl BybitTickerData {
284 #[getter]
285 #[must_use]
286 pub fn symbol(&self) -> &str {
287 self.symbol.as_str()
288 }
289
290 #[getter]
291 #[must_use]
292 pub fn bid1_price(&self) -> &str {
293 &self.bid1_price
294 }
295
296 #[getter]
297 #[must_use]
298 pub fn bid1_size(&self) -> &str {
299 &self.bid1_size
300 }
301
302 #[getter]
303 #[must_use]
304 pub fn ask1_price(&self) -> &str {
305 &self.ask1_price
306 }
307
308 #[getter]
309 #[must_use]
310 pub fn ask1_size(&self) -> &str {
311 &self.ask1_size
312 }
313
314 #[getter]
315 #[must_use]
316 pub fn last_price(&self) -> &str {
317 &self.last_price
318 }
319
320 #[getter]
321 #[must_use]
322 pub fn high_price24h(&self) -> &str {
323 &self.high_price24h
324 }
325
326 #[getter]
327 #[must_use]
328 pub fn low_price24h(&self) -> &str {
329 &self.low_price24h
330 }
331
332 #[getter]
333 #[must_use]
334 pub fn turnover24h(&self) -> &str {
335 &self.turnover24h
336 }
337
338 #[getter]
339 #[must_use]
340 pub fn volume24h(&self) -> &str {
341 &self.volume24h
342 }
343
344 #[getter]
345 #[must_use]
346 pub fn open_interest(&self) -> Option<&str> {
347 self.open_interest.as_deref()
348 }
349
350 #[getter]
351 #[must_use]
352 pub fn funding_rate(&self) -> Option<&str> {
353 self.funding_rate.as_deref()
354 }
355
356 #[getter]
357 #[must_use]
358 pub fn next_funding_time(&self) -> Option<&str> {
359 self.next_funding_time.as_deref()
360 }
361
362 #[getter]
363 #[must_use]
364 pub fn mark_price(&self) -> Option<&str> {
365 self.mark_price.as_deref()
366 }
367
368 #[getter]
369 #[must_use]
370 pub fn index_price(&self) -> Option<&str> {
371 self.index_price.as_deref()
372 }
373}
374
375impl From<BybitTickerSpot> for BybitTickerData {
376 fn from(ticker: BybitTickerSpot) -> Self {
377 Self {
378 symbol: ticker.symbol,
379 bid1_price: ticker.bid1_price,
380 bid1_size: ticker.bid1_size,
381 ask1_price: ticker.ask1_price,
382 ask1_size: ticker.ask1_size,
383 last_price: ticker.last_price,
384 high_price24h: ticker.high_price24h,
385 low_price24h: ticker.low_price24h,
386 turnover24h: ticker.turnover24h,
387 volume24h: ticker.volume24h,
388 open_interest: None,
389 funding_rate: None,
390 next_funding_time: None,
391 mark_price: None,
392 index_price: None,
393 }
394 }
395}
396
397impl From<BybitTickerLinear> for BybitTickerData {
398 fn from(ticker: BybitTickerLinear) -> Self {
399 Self {
400 symbol: ticker.symbol,
401 bid1_price: ticker.bid1_price,
402 bid1_size: ticker.bid1_size,
403 ask1_price: ticker.ask1_price,
404 ask1_size: ticker.ask1_size,
405 last_price: ticker.last_price,
406 high_price24h: ticker.high_price24h,
407 low_price24h: ticker.low_price24h,
408 turnover24h: ticker.turnover24h,
409 volume24h: ticker.volume24h,
410 open_interest: Some(ticker.open_interest),
411 funding_rate: Some(ticker.funding_rate),
412 next_funding_time: Some(ticker.next_funding_time),
413 mark_price: Some(ticker.mark_price),
414 index_price: Some(ticker.index_price),
415 }
416 }
417}
418
419impl From<BybitTickerOption> for BybitTickerData {
420 fn from(ticker: BybitTickerOption) -> Self {
421 Self {
422 symbol: ticker.symbol,
423 bid1_price: ticker.bid1_price,
424 bid1_size: ticker.bid1_size,
425 ask1_price: ticker.ask1_price,
426 ask1_size: ticker.ask1_size,
427 last_price: ticker.last_price,
428 high_price24h: ticker.high_price24h,
429 low_price24h: ticker.low_price24h,
430 turnover24h: ticker.turnover24h,
431 volume24h: ticker.volume24h,
432 open_interest: Some(ticker.open_interest),
433 funding_rate: None,
434 next_funding_time: None,
435 mark_price: Some(ticker.mark_price),
436 index_price: Some(ticker.index_price),
437 }
438 }
439}
440
441#[derive(Clone, Debug, Serialize)]
449pub struct BybitKline {
450 pub start: String,
451 pub open: String,
452 pub high: String,
453 pub low: String,
454 pub close: String,
455 pub volume: String,
456 pub turnover: String,
457}
458
459impl<'de> Deserialize<'de> for BybitKline {
460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461 where
462 D: serde::Deserializer<'de>,
463 {
464 let [start, open, high, low, close, volume, turnover]: [String; 7] =
465 Deserialize::deserialize(deserializer)?;
466 Ok(Self {
467 start,
468 open,
469 high,
470 low,
471 close,
472 volume,
473 turnover,
474 })
475 }
476}
477
478#[derive(Clone, Debug, Serialize, Deserialize)]
483#[serde(rename_all = "camelCase")]
484pub struct BybitKlineResult {
485 pub category: BybitProductType,
486 pub symbol: Ustr,
487 pub list: Vec<BybitKline>,
488}
489
490pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct BybitTrade {
503 pub exec_id: String,
504 pub symbol: Ustr,
505 pub price: String,
506 pub size: String,
507 pub side: BybitOrderSide,
508 pub time: String,
509 pub is_block_trade: bool,
510 #[serde(default)]
511 pub m_p: Option<String>,
512 #[serde(default)]
513 pub i_p: Option<String>,
514 #[serde(default)]
515 pub mlv: Option<String>,
516 #[serde(default)]
517 pub iv: Option<String>,
518}
519
520#[derive(Clone, Debug, Serialize, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct BybitTradeResult {
527 pub category: BybitProductType,
528 pub list: Vec<BybitTrade>,
529}
530
531pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
536
537#[derive(Clone, Debug, Serialize, Deserialize)]
542#[serde(rename_all = "camelCase")]
543pub struct BybitFunding {
544 pub symbol: Ustr,
545 pub funding_rate: String,
546 pub funding_rate_timestamp: String,
547}
548
549#[derive(Clone, Debug, Serialize, Deserialize)]
554#[serde(rename_all = "camelCase")]
555pub struct BybitFundingResult {
556 pub category: BybitProductType,
557 pub list: Vec<BybitFunding>,
558}
559
560pub type BybitFundingResponse = BybitResponse<BybitFundingResult>;
565
566#[derive(Clone, Debug, Serialize, Deserialize)]
571#[serde(rename_all = "camelCase")]
572pub struct BybitOrderbookResult {
573 pub s: Ustr,
575 pub b: Vec<[String; 2]>,
577 pub a: Vec<[String; 2]>,
579 pub ts: i64,
580 pub u: i64,
582 pub seq: i64,
584 pub cts: i64,
585}
586
587pub type BybitOrderbookResponse = BybitResponse<BybitOrderbookResult>;
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
598#[serde(rename_all = "camelCase")]
599pub struct BybitInstrumentSpot {
600 pub symbol: Ustr,
601 pub base_coin: Ustr,
602 pub quote_coin: Ustr,
603 pub innovation: BybitInnovationFlag,
604 pub status: BybitInstrumentStatus,
605 pub margin_trading: BybitMarginTrading,
606 pub lot_size_filter: SpotLotSizeFilter,
607 pub price_filter: SpotPriceFilter,
608 #[serde(default)]
609 pub symbol_id: Option<i64>,
610 #[serde(default)]
611 pub symbol_type: Option<BybitSymbolType>,
612 #[serde(default)]
613 pub xstock_multiplier: Option<String>,
614}
615
616#[derive(Clone, Debug, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct BybitInstrumentLinear {
623 pub symbol: Ustr,
624 pub contract_type: BybitContractType,
625 pub status: BybitInstrumentStatus,
626 pub base_coin: Ustr,
627 pub quote_coin: Ustr,
628 pub launch_time: String,
629 pub delivery_time: String,
630 pub delivery_fee_rate: String,
631 pub price_scale: String,
632 pub leverage_filter: LeverageFilter,
633 pub price_filter: LinearPriceFilter,
634 pub lot_size_filter: LinearLotSizeFilter,
635 pub unified_margin_trade: bool,
636 pub funding_interval: i64,
637 pub settle_coin: Ustr,
638 #[serde(default)]
639 pub symbol_id: Option<i64>,
640 #[serde(default)]
641 pub symbol_type: Option<BybitSymbolType>,
642}
643
644#[derive(Clone, Debug, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct BybitInstrumentInverse {
651 pub symbol: Ustr,
652 pub contract_type: BybitContractType,
653 pub status: BybitInstrumentStatus,
654 pub base_coin: Ustr,
655 pub quote_coin: Ustr,
656 pub launch_time: String,
657 pub delivery_time: String,
658 pub delivery_fee_rate: String,
659 pub price_scale: String,
660 pub leverage_filter: LeverageFilter,
661 pub price_filter: LinearPriceFilter,
662 pub lot_size_filter: LinearLotSizeFilter,
663 pub unified_margin_trade: bool,
664 pub funding_interval: i64,
665 pub settle_coin: Ustr,
666 #[serde(default)]
667 pub symbol_id: Option<i64>,
668 #[serde(default)]
669 pub symbol_type: Option<BybitSymbolType>,
670}
671
672#[derive(Clone, Debug, Serialize, Deserialize)]
677#[serde(rename_all = "camelCase")]
678pub struct BybitInstrumentOption {
679 pub symbol: Ustr,
680 pub status: BybitInstrumentStatus,
681 pub base_coin: Ustr,
682 pub quote_coin: Ustr,
683 pub settle_coin: Ustr,
684 pub options_type: BybitOptionType,
685 pub launch_time: String,
686 pub delivery_time: String,
687 pub delivery_fee_rate: String,
688 pub price_filter: LinearPriceFilter,
689 pub lot_size_filter: OptionLotSizeFilter,
690 #[serde(default)]
691 pub symbol_id: Option<i64>,
692}
693
694pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
699pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
704pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
709pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
714
715#[derive(Clone, Debug, Serialize, Deserialize)]
720#[serde(rename_all = "camelCase")]
721#[cfg_attr(
722 feature = "python",
723 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
724)]
725#[cfg_attr(
726 feature = "python",
727 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
728)]
729pub struct BybitFeeRate {
730 pub symbol: Ustr,
731 pub taker_fee_rate: String,
732 pub maker_fee_rate: String,
733 #[serde(default)]
734 pub base_coin: Option<Ustr>,
735}
736
737#[cfg(feature = "python")]
738#[pyo3::pymethods]
739impl BybitFeeRate {
740 #[getter]
741 #[must_use]
742 pub fn symbol(&self) -> &str {
743 self.symbol.as_str()
744 }
745
746 #[getter]
747 #[must_use]
748 pub fn taker_fee_rate(&self) -> &str {
749 &self.taker_fee_rate
750 }
751
752 #[getter]
753 #[must_use]
754 pub fn maker_fee_rate(&self) -> &str {
755 &self.maker_fee_rate
756 }
757
758 #[getter]
759 #[must_use]
760 pub fn base_coin(&self) -> Option<&str> {
761 self.base_coin.as_ref().map(|u| u.as_str())
762 }
763}
764
765pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
770
771#[derive(Clone, Debug, Serialize, Deserialize)]
776#[serde(rename_all = "camelCase")]
777pub struct BybitCoinBalance {
778 pub available_to_borrow: String,
779 pub bonus: String,
780 pub accrued_interest: String,
781 pub available_to_withdraw: String,
782 #[serde(default, rename = "totalOrderIM")]
783 pub total_order_im: Option<String>,
784 pub equity: String,
785 pub usd_value: String,
786 pub borrow_amount: String,
787 #[serde(default, rename = "totalPositionMM")]
788 pub total_position_mm: Option<String>,
789 #[serde(default, rename = "totalPositionIM")]
790 pub total_position_im: Option<String>,
791 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
792 pub wallet_balance: Decimal,
793 pub unrealised_pnl: String,
794 pub cum_realised_pnl: String,
795 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
796 pub locked: Decimal,
797 pub collateral_switch: bool,
798 pub margin_collateral: bool,
799 pub coin: Ustr,
800 #[serde(default)]
801 pub spot_hedging_qty: Option<String>,
802 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
803 pub spot_borrow: Decimal,
804}
805
806#[derive(Clone, Debug, Serialize, Deserialize)]
811#[serde(rename_all = "camelCase")]
812pub struct BybitWalletBalance {
813 pub total_equity: String,
814 #[serde(rename = "accountIMRate")]
815 pub account_im_rate: String,
816 pub total_margin_balance: String,
817 pub total_initial_margin: String,
818 pub account_type: BybitAccountType,
819 pub total_available_balance: String,
820 #[serde(rename = "accountMMRate")]
821 pub account_mm_rate: String,
822 #[serde(rename = "totalPerpUPL")]
823 pub total_perp_upl: String,
824 pub total_wallet_balance: String,
825 #[serde(rename = "accountLTV")]
826 pub account_ltv: String,
827 pub total_maintenance_margin: String,
828 pub coin: Vec<BybitCoinBalance>,
829}
830
831pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
836
837#[derive(Clone, Debug, Serialize, Deserialize)]
842#[serde(rename_all = "camelCase")]
843pub struct BybitAccountInfo {
844 pub unified_margin_status: BybitUnifiedMarginStatus,
845 pub margin_mode: BybitMarginMode,
846 pub is_master_trader: bool,
847 #[serde(with = "on_off_bool")]
848 pub spot_hedging_status: bool,
849 pub updated_time: String,
850 #[serde(default, with = "on_off_bool")]
853 pub dcp_status: bool,
854 #[serde(default, deserialize_with = "deserialize_i32_or_string")]
855 pub time_window: i32,
856 #[serde(default, deserialize_with = "deserialize_i32_or_string")]
857 pub smp_group: i32,
858}
859
860pub type BybitAccountInfoResponse = BybitResponse<BybitAccountInfo>;
865
866#[derive(Clone, Debug, Serialize, Deserialize)]
871#[cfg_attr(
872 feature = "python",
873 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
874)]
875#[cfg_attr(
876 feature = "python",
877 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
878)]
879#[serde(rename_all = "camelCase")]
880pub struct BybitOrder {
881 pub order_id: Ustr,
882 pub order_link_id: Ustr,
883 pub block_trade_id: Option<Ustr>,
884 pub symbol: Ustr,
885 pub price: String,
886 pub qty: String,
887 pub side: BybitOrderSide,
888 pub is_leverage: String,
889 #[serde(deserialize_with = "deserialize_i32_or_string")]
890 pub position_idx: i32,
891 pub order_status: BybitOrderStatus,
892 pub cancel_type: BybitCancelType,
893 pub reject_reason: Ustr,
894 pub avg_price: Option<String>,
895 pub leaves_qty: String,
896 pub leaves_value: String,
897 pub cum_exec_qty: String,
898 pub cum_exec_value: String,
899 pub cum_exec_fee: String,
900 pub time_in_force: BybitTimeInForce,
901 pub order_type: BybitOrderType,
902 pub stop_order_type: BybitStopOrderType,
903 pub order_iv: Option<String>,
904 pub trigger_price: String,
905 pub take_profit: String,
906 pub stop_loss: String,
907 pub tp_trigger_by: BybitTriggerType,
908 pub sl_trigger_by: BybitTriggerType,
909 pub trigger_direction: BybitTriggerDirection,
910 pub trigger_by: BybitTriggerType,
911 pub last_price_on_created: String,
912 pub reduce_only: bool,
913 pub close_on_trigger: bool,
914 pub smp_type: BybitSmpType,
915 #[serde(deserialize_with = "deserialize_i32_or_string")]
916 pub smp_group: i32,
917 pub smp_order_id: Ustr,
918 pub tpsl_mode: Option<BybitTpSlMode>,
919 pub tp_limit_price: String,
920 pub sl_limit_price: String,
921 pub place_type: Ustr,
922 pub created_time: String,
923 pub updated_time: String,
924}
925
926#[cfg(feature = "python")]
927#[pyo3::pymethods]
928impl BybitOrder {
929 #[getter]
930 #[must_use]
931 pub fn order_id(&self) -> &str {
932 self.order_id.as_str()
933 }
934
935 #[getter]
936 #[must_use]
937 pub fn order_link_id(&self) -> &str {
938 self.order_link_id.as_str()
939 }
940
941 #[getter]
942 #[must_use]
943 pub fn block_trade_id(&self) -> Option<&str> {
944 self.block_trade_id.as_ref().map(|s| s.as_str())
945 }
946
947 #[getter]
948 #[must_use]
949 pub fn symbol(&self) -> &str {
950 self.symbol.as_str()
951 }
952
953 #[getter]
954 #[must_use]
955 pub fn price(&self) -> &str {
956 &self.price
957 }
958
959 #[getter]
960 #[must_use]
961 pub fn qty(&self) -> &str {
962 &self.qty
963 }
964
965 #[getter]
966 #[must_use]
967 pub fn side(&self) -> BybitOrderSide {
968 self.side
969 }
970
971 #[getter]
972 #[must_use]
973 pub fn is_leverage(&self) -> &str {
974 &self.is_leverage
975 }
976
977 #[getter]
978 #[must_use]
979 pub fn position_idx(&self) -> i32 {
980 self.position_idx
981 }
982
983 #[getter]
984 #[must_use]
985 pub fn order_status(&self) -> BybitOrderStatus {
986 self.order_status
987 }
988
989 #[getter]
990 #[must_use]
991 pub fn cancel_type(&self) -> BybitCancelType {
992 self.cancel_type
993 }
994
995 #[getter]
996 #[must_use]
997 pub fn reject_reason(&self) -> &str {
998 self.reject_reason.as_str()
999 }
1000
1001 #[getter]
1002 #[must_use]
1003 pub fn avg_price(&self) -> Option<&str> {
1004 self.avg_price.as_deref()
1005 }
1006
1007 #[getter]
1008 #[must_use]
1009 pub fn leaves_qty(&self) -> &str {
1010 &self.leaves_qty
1011 }
1012
1013 #[getter]
1014 #[must_use]
1015 pub fn leaves_value(&self) -> &str {
1016 &self.leaves_value
1017 }
1018
1019 #[getter]
1020 #[must_use]
1021 pub fn cum_exec_qty(&self) -> &str {
1022 &self.cum_exec_qty
1023 }
1024
1025 #[getter]
1026 #[must_use]
1027 pub fn cum_exec_value(&self) -> &str {
1028 &self.cum_exec_value
1029 }
1030
1031 #[getter]
1032 #[must_use]
1033 pub fn cum_exec_fee(&self) -> &str {
1034 &self.cum_exec_fee
1035 }
1036
1037 #[getter]
1038 #[must_use]
1039 pub fn time_in_force(&self) -> BybitTimeInForce {
1040 self.time_in_force
1041 }
1042
1043 #[getter]
1044 #[must_use]
1045 pub fn order_type(&self) -> BybitOrderType {
1046 self.order_type
1047 }
1048
1049 #[getter]
1050 #[must_use]
1051 pub fn stop_order_type(&self) -> BybitStopOrderType {
1052 self.stop_order_type
1053 }
1054
1055 #[getter]
1056 #[must_use]
1057 pub fn order_iv(&self) -> Option<&str> {
1058 self.order_iv.as_deref()
1059 }
1060
1061 #[getter]
1062 #[must_use]
1063 pub fn trigger_price(&self) -> &str {
1064 &self.trigger_price
1065 }
1066
1067 #[getter]
1068 #[must_use]
1069 pub fn take_profit(&self) -> &str {
1070 &self.take_profit
1071 }
1072
1073 #[getter]
1074 #[must_use]
1075 pub fn stop_loss(&self) -> &str {
1076 &self.stop_loss
1077 }
1078
1079 #[getter]
1080 #[must_use]
1081 pub fn tp_trigger_by(&self) -> BybitTriggerType {
1082 self.tp_trigger_by
1083 }
1084
1085 #[getter]
1086 #[must_use]
1087 pub fn sl_trigger_by(&self) -> BybitTriggerType {
1088 self.sl_trigger_by
1089 }
1090
1091 #[getter]
1092 #[must_use]
1093 pub fn trigger_direction(&self) -> BybitTriggerDirection {
1094 self.trigger_direction
1095 }
1096
1097 #[getter]
1098 #[must_use]
1099 pub fn trigger_by(&self) -> BybitTriggerType {
1100 self.trigger_by
1101 }
1102
1103 #[getter]
1104 #[must_use]
1105 pub fn last_price_on_created(&self) -> &str {
1106 &self.last_price_on_created
1107 }
1108
1109 #[getter]
1110 #[must_use]
1111 pub fn reduce_only(&self) -> bool {
1112 self.reduce_only
1113 }
1114
1115 #[getter]
1116 #[must_use]
1117 pub fn close_on_trigger(&self) -> bool {
1118 self.close_on_trigger
1119 }
1120
1121 #[getter]
1122 #[must_use]
1123 #[expect(
1124 clippy::missing_panics_doc,
1125 reason = "serialization of a simple enum cannot fail"
1126 )]
1127 pub fn smp_type(&self) -> String {
1128 serde_json::to_string(&self.smp_type)
1129 .expect("Failed to serialize BybitSmpType")
1130 .trim_matches('"')
1131 .to_string()
1132 }
1133
1134 #[getter]
1135 #[must_use]
1136 pub fn smp_group(&self) -> i32 {
1137 self.smp_group
1138 }
1139
1140 #[getter]
1141 #[must_use]
1142 pub fn smp_order_id(&self) -> &str {
1143 self.smp_order_id.as_str()
1144 }
1145
1146 #[getter]
1147 #[must_use]
1148 pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1149 self.tpsl_mode
1150 }
1151
1152 #[getter]
1153 #[must_use]
1154 pub fn tp_limit_price(&self) -> &str {
1155 &self.tp_limit_price
1156 }
1157
1158 #[getter]
1159 #[must_use]
1160 pub fn sl_limit_price(&self) -> &str {
1161 &self.sl_limit_price
1162 }
1163
1164 #[getter]
1165 #[must_use]
1166 pub fn place_type(&self) -> &str {
1167 self.place_type.as_str()
1168 }
1169
1170 #[getter]
1171 #[must_use]
1172 pub fn created_time(&self) -> &str {
1173 &self.created_time
1174 }
1175
1176 #[getter]
1177 #[must_use]
1178 pub fn updated_time(&self) -> &str {
1179 &self.updated_time
1180 }
1181}
1182
1183pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1188pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1193
1194#[derive(Clone, Debug, Serialize, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct BybitPlaceOrderResult {
1201 pub order_id: Option<Ustr>,
1202 pub order_link_id: Option<Ustr>,
1203}
1204
1205pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1210
1211#[derive(Clone, Debug, Serialize, Deserialize)]
1216#[serde(rename_all = "camelCase")]
1217pub struct BybitCancelOrderResult {
1218 pub order_id: Option<Ustr>,
1219 pub order_link_id: Option<Ustr>,
1220}
1221
1222pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1227
1228#[derive(Clone, Debug, Serialize, Deserialize)]
1233#[serde(rename_all = "camelCase")]
1234pub struct BybitExecution {
1235 pub symbol: Ustr,
1236 pub order_id: Ustr,
1237 pub order_link_id: Ustr,
1238 pub side: BybitOrderSide,
1239 pub order_price: String,
1240 pub order_qty: String,
1241 pub leaves_qty: String,
1242 pub create_type: Option<BybitCreateType>,
1243 pub order_type: BybitOrderType,
1244 pub stop_order_type: Option<BybitStopOrderType>,
1245 pub exec_fee: String,
1246 pub exec_id: String,
1247 pub exec_price: String,
1248 pub exec_qty: String,
1249 pub exec_type: BybitExecType,
1250 pub exec_value: String,
1251 pub exec_time: String,
1252 pub fee_currency: Ustr,
1253 pub is_maker: bool,
1254 pub fee_rate: String,
1255 pub trade_iv: String,
1256 pub mark_iv: String,
1257 pub mark_price: String,
1258 pub index_price: String,
1259 pub underlying_price: String,
1260 pub block_trade_id: String,
1261 pub closed_size: String,
1262 pub seq: i64,
1263}
1264
1265pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1270
1271#[derive(Clone, Debug, Serialize, Deserialize)]
1276#[serde(rename_all = "camelCase")]
1277pub struct BybitPosition {
1278 pub position_idx: BybitPositionIdx,
1279 #[serde(deserialize_with = "deserialize_i32_or_string")]
1280 pub risk_id: i32,
1281 pub risk_limit_value: String,
1282 pub symbol: Ustr,
1283 pub side: BybitPositionSide,
1284 pub size: String,
1285 pub avg_price: String,
1286 pub position_value: String,
1287 #[serde(deserialize_with = "deserialize_i32_or_string")]
1288 pub trade_mode: i32,
1289 pub position_status: BybitPositionStatus,
1290 #[serde(deserialize_with = "deserialize_i32_or_string")]
1291 pub auto_add_margin: i32,
1292 #[serde(deserialize_with = "deserialize_i32_or_string")]
1293 pub adl_rank_indicator: i32,
1294 pub leverage: String,
1295 pub position_balance: String,
1296 pub mark_price: String,
1297 pub liq_price: String,
1298 pub bust_price: String,
1299 #[serde(rename = "positionMM")]
1300 pub position_mm: String,
1301 #[serde(rename = "positionIM")]
1302 pub position_im: String,
1303 pub tpsl_mode: BybitTpSlMode,
1304 pub take_profit: String,
1305 pub stop_loss: String,
1306 pub trailing_stop: String,
1307 pub unrealised_pnl: String,
1308 pub cur_realised_pnl: String,
1309 pub cum_realised_pnl: String,
1310 #[serde(default = "default_position_seq")]
1311 pub seq: i64,
1312 #[serde(default)]
1313 pub is_reduce_only: bool,
1314 #[serde(default)]
1315 pub mmr_sys_updated_time: String,
1316 #[serde(default)]
1317 pub leverage_sys_updated_time: String,
1318 pub created_time: String,
1319 pub updated_time: String,
1320 #[serde(default)]
1321 pub open_time: i64,
1322}
1323
1324const fn default_position_seq() -> i64 {
1325 -1
1326}
1327
1328pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1333
1334#[derive(Clone, Debug, Serialize, Deserialize)]
1339#[serde(rename_all = "camelCase")]
1340pub struct BybitSetMarginModeReason {
1341 pub reason_code: String,
1342 pub reason_msg: String,
1343}
1344
1345#[derive(Clone, Debug, Serialize, Deserialize)]
1350#[serde(rename_all = "camelCase")]
1351pub struct BybitSetMarginModeResult {
1352 #[serde(default)]
1353 pub reasons: Vec<BybitSetMarginModeReason>,
1354}
1355
1356pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1361
1362#[derive(Clone, Debug, Serialize, Deserialize)]
1364pub struct BybitSetLeverageResult {}
1365
1366pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1371
1372#[derive(Clone, Debug, Serialize, Deserialize)]
1374pub struct BybitSwitchModeResult {}
1375
1376pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1381
1382#[derive(Clone, Debug, Serialize, Deserialize)]
1384pub struct BybitSetTradingStopResult {}
1385
1386pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1391
1392#[derive(Clone, Debug, Serialize, Deserialize)]
1394#[serde(rename_all = "camelCase")]
1395pub struct BybitBorrowResult {
1396 pub coin: Ustr,
1397 pub amount: String,
1398}
1399
1400pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1406
1407#[derive(Clone, Debug, Serialize, Deserialize)]
1409#[serde(rename_all = "camelCase")]
1410pub struct BybitNoConvertRepayResult {
1411 pub result_status: BybitRepayStatus,
1412}
1413
1414pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1420
1421#[derive(Clone, Debug, Serialize, Deserialize)]
1423#[serde(rename_all = "camelCase")]
1424pub struct BybitRepayResult {
1425 pub result_status: BybitRepayStatus,
1426}
1427
1428pub type BybitRepayResponse = BybitResponse<BybitRepayResult>;
1434
1435#[derive(Clone, Debug, Serialize, Deserialize)]
1437#[cfg_attr(
1438 feature = "python",
1439 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
1440)]
1441#[cfg_attr(
1442 feature = "python",
1443 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1444)]
1445#[serde(rename_all = "PascalCase")]
1446pub struct BybitApiKeyPermissions {
1447 #[serde(default)]
1448 pub contract_trade: Vec<String>,
1449 #[serde(default)]
1450 pub spot: Vec<String>,
1451 #[serde(default)]
1452 pub wallet: Vec<String>,
1453 #[serde(default)]
1454 pub options: Vec<String>,
1455 #[serde(default)]
1456 pub derivatives: Vec<String>,
1457 #[serde(default)]
1458 pub exchange: Vec<String>,
1459 #[serde(default)]
1460 pub copy_trading: Vec<String>,
1461 #[serde(default)]
1462 pub block_trade: Vec<String>,
1463 #[serde(rename = "NFT", default)]
1466 pub nft: Vec<String>,
1467 #[serde(default)]
1468 pub affiliate: Vec<String>,
1469 #[serde(default)]
1473 pub earn: Vec<String>,
1474 #[serde(rename = "FiatP2P", default)]
1476 pub fiat_p2p: Vec<String>,
1477 #[serde(default)]
1478 pub fiat_bybit_pay: Vec<String>,
1479 #[serde(default)]
1480 pub fiat_bit_pay: Vec<String>,
1481 #[serde(default)]
1482 pub fiat_global_pay: Vec<String>,
1483 #[serde(default)]
1484 pub fiat_convert_broker: Vec<String>,
1485 #[serde(default)]
1486 pub bit_card: Vec<String>,
1487 #[serde(rename = "ByXPost", default)]
1489 pub byx_post: Vec<String>,
1490}
1491
1492#[derive(Debug, Clone, Serialize, Deserialize)]
1494#[cfg_attr(
1495 feature = "python",
1496 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
1497)]
1498#[cfg_attr(
1499 feature = "python",
1500 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1501)]
1502#[serde(rename_all = "camelCase")]
1503pub struct BybitAccountDetails {
1504 pub id: String,
1505 pub note: String,
1506 pub api_key: SecretString,
1507 pub read_only: u8,
1508 pub secret: SecretString,
1509 #[serde(rename = "type")]
1510 pub key_type: u8,
1511 pub permissions: BybitApiKeyPermissions,
1512 pub ips: Vec<String>,
1513 #[serde(default)]
1514 pub user_id: Option<u64>,
1515 #[serde(default)]
1516 pub inviter_id: Option<u64>,
1517 pub vip_level: String,
1518 #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1519 pub mkt_maker_level: u8,
1520 #[serde(default)]
1521 pub affiliate_id: Option<u64>,
1522 pub rsa_public_key: String,
1523 pub is_master: bool,
1524 pub parent_uid: String,
1525 pub uta: u8,
1526 pub kyc_level: String,
1527 pub kyc_region: String,
1528 #[serde(default)]
1529 pub unified: Option<i32>,
1530 #[serde(default)]
1531 pub deadline_day: i64,
1532 #[serde(default)]
1533 pub expired_at: Option<String>,
1534 pub created_at: String,
1535}
1536
1537#[cfg(feature = "python")]
1538#[pyo3::pymethods]
1539impl BybitAccountDetails {
1540 #[getter]
1541 #[must_use]
1542 pub fn id(&self) -> &str {
1543 &self.id
1544 }
1545
1546 #[getter]
1547 #[must_use]
1548 pub fn note(&self) -> &str {
1549 &self.note
1550 }
1551
1552 #[getter]
1553 #[must_use]
1554 pub fn api_key(&self) -> &str {
1555 self.api_key.expose_secret()
1556 }
1557
1558 #[getter]
1559 #[must_use]
1560 pub fn read_only(&self) -> u8 {
1561 self.read_only
1562 }
1563
1564 #[getter]
1565 #[must_use]
1566 pub fn key_type(&self) -> u8 {
1567 self.key_type
1568 }
1569
1570 #[getter]
1571 #[must_use]
1572 pub fn user_id(&self) -> Option<u64> {
1573 self.user_id
1574 }
1575
1576 #[getter]
1577 #[must_use]
1578 pub fn inviter_id(&self) -> Option<u64> {
1579 self.inviter_id
1580 }
1581
1582 #[getter]
1583 #[must_use]
1584 pub fn vip_level(&self) -> &str {
1585 &self.vip_level
1586 }
1587
1588 #[getter]
1589 #[must_use]
1590 pub fn mkt_maker_level(&self) -> u8 {
1591 self.mkt_maker_level
1592 }
1593
1594 #[getter]
1595 #[must_use]
1596 pub fn affiliate_id(&self) -> Option<u64> {
1597 self.affiliate_id
1598 }
1599
1600 #[getter]
1601 #[must_use]
1602 pub fn rsa_public_key(&self) -> &str {
1603 &self.rsa_public_key
1604 }
1605
1606 #[getter]
1607 #[must_use]
1608 pub fn is_master(&self) -> bool {
1609 self.is_master
1610 }
1611
1612 #[getter]
1613 #[must_use]
1614 pub fn parent_uid(&self) -> &str {
1615 &self.parent_uid
1616 }
1617
1618 #[getter]
1619 #[must_use]
1620 pub fn uta(&self) -> u8 {
1621 self.uta
1622 }
1623
1624 #[getter]
1625 #[must_use]
1626 pub fn kyc_level(&self) -> &str {
1627 &self.kyc_level
1628 }
1629
1630 #[getter]
1631 #[must_use]
1632 pub fn kyc_region(&self) -> &str {
1633 &self.kyc_region
1634 }
1635
1636 #[getter]
1637 #[must_use]
1638 pub fn deadline_day(&self) -> i64 {
1639 self.deadline_day
1640 }
1641
1642 #[getter]
1643 #[must_use]
1644 pub fn expired_at(&self) -> Option<&str> {
1645 self.expired_at.as_deref()
1646 }
1647
1648 #[getter]
1649 #[must_use]
1650 pub fn created_at(&self) -> &str {
1651 &self.created_at
1652 }
1653}
1654
1655pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1661
1662#[derive(Clone, Debug, Serialize, Deserialize)]
1674#[serde(rename_all = "camelCase")]
1675pub struct BybitSubMember {
1676 pub uid: String,
1677 pub username: String,
1678 pub member_type: i32,
1679 pub status: i32,
1680 pub account_mode: i32,
1681 #[serde(default)]
1682 pub remark: String,
1683}
1684
1685#[derive(Clone, Debug, Serialize, Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688pub struct BybitSubMembersResult {
1689 #[serde(default)]
1690 pub sub_members: Vec<BybitSubMember>,
1691}
1692
1693pub type BybitSubMembersResponse = BybitResponse<BybitSubMembersResult>;
1699
1700#[derive(Clone, Debug, Serialize, Deserialize)]
1708#[serde(rename_all = "camelCase")]
1709pub struct BybitSubMembersPagedResult {
1710 #[serde(default)]
1711 pub sub_members: Vec<BybitSubMember>,
1712 #[serde(default)]
1713 pub next_cursor: Option<String>,
1714}
1715
1716impl BybitSubMembersPagedResult {
1717 #[must_use]
1724 pub fn continuation_cursor(&self) -> Option<&str> {
1725 match self.next_cursor.as_deref() {
1726 None | Some("" | "0") => None,
1727 Some(cursor) => Some(cursor),
1728 }
1729 }
1730
1731 #[must_use]
1733 pub fn has_more_pages(&self) -> bool {
1734 self.continuation_cursor().is_some()
1735 }
1736}
1737
1738pub type BybitSubMembersPagedResponse = BybitResponse<BybitSubMembersPagedResult>;
1744
1745pub type BybitEscrowSubMembersResponse = BybitResponse<BybitSubMembersPagedResult>;
1752
1753#[derive(Debug, Clone, Serialize, Deserialize)]
1762#[serde(rename_all = "camelCase")]
1763pub struct BybitSubApiKeyInfo {
1764 pub id: String,
1765 #[serde(default)]
1766 pub ips: Vec<String>,
1767 pub api_key: SecretString,
1768 #[serde(default)]
1769 pub note: String,
1770 pub status: i32,
1771 #[serde(default)]
1772 pub expired_at: Option<String>,
1773 pub created_at: String,
1774 #[serde(rename = "type")]
1775 pub key_type: BybitApiKeyType,
1776 #[serde(with = "masked_secret")]
1777 pub secret: Option<SecretString>,
1778 #[serde(with = "bool_or_int")]
1779 pub read_only: bool,
1780 #[serde(default)]
1781 pub deadline_day: Option<i64>,
1782 #[serde(default)]
1783 pub flag: String,
1784 pub permissions: BybitApiKeyPermissions,
1785}
1786
1787#[derive(Clone, Debug, Serialize, Deserialize)]
1793#[serde(rename_all = "camelCase")]
1794pub struct BybitSubApiKeysResult {
1795 #[serde(rename = "result", default)]
1796 pub keys: Vec<BybitSubApiKeyInfo>,
1797 #[serde(default)]
1798 pub next_page_cursor: Option<String>,
1799}
1800
1801impl BybitSubApiKeysResult {
1802 #[must_use]
1808 pub fn continuation_cursor(&self) -> Option<&str> {
1809 match self.next_page_cursor.as_deref() {
1810 None | Some("") => None,
1811 Some(cursor) => Some(cursor),
1812 }
1813 }
1814
1815 #[must_use]
1817 pub fn has_more_pages(&self) -> bool {
1818 self.continuation_cursor().is_some()
1819 }
1820}
1821
1822pub type BybitSubApiKeysResponse = BybitResponse<BybitSubApiKeysResult>;
1828
1829#[derive(Debug, Clone, Serialize, Deserialize)]
1836#[serde(rename_all = "camelCase")]
1837pub struct BybitApiKeyUpdateResult {
1838 pub id: String,
1839 #[serde(default)]
1840 pub note: String,
1841 pub api_key: SecretString,
1842 #[serde(with = "bool_or_int")]
1843 pub read_only: bool,
1844 #[serde(with = "masked_secret")]
1845 pub secret: Option<SecretString>,
1846 pub permissions: BybitApiKeyPermissions,
1847 #[serde(default)]
1848 pub ips: Vec<String>,
1849}
1850
1851pub type BybitUpdateSubApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1857
1858pub type BybitUpdateMasterApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1864
1865#[cfg(test)]
1866mod tests {
1867 use nautilus_core::UnixNanos;
1868 use nautilus_model::identifiers::AccountId;
1869 use rstest::rstest;
1870 use rust_decimal::Decimal;
1871 use rust_decimal_macros::dec;
1872
1873 use super::*;
1874 use crate::common::testing::load_test_json;
1875
1876 #[rstest]
1877 fn deserialize_spot_instrument_uses_enums() {
1878 let json = load_test_json("http_get_instruments_spot.json");
1879 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1880 let instrument = &response.result.list[0];
1881
1882 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1883 assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1884 assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1885 }
1886
1887 #[rstest]
1888 fn deserialize_linear_instrument_status() {
1889 let json = load_test_json("http_get_instruments_linear.json");
1890 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1891 let instrument = &response.result.list[0];
1892
1893 assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1894 assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1895 }
1896
1897 #[rstest]
1898 fn deserialize_spot_instrument_with_xstock_fields() {
1899 let json = load_test_json("http_get_instruments_spot_xstocks.json");
1900 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1901 let instrument = &response.result.list[0];
1902
1903 assert_eq!(instrument.symbol_id, Some(42));
1904 assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Xstocks));
1905 assert_eq!(instrument.xstock_multiplier.as_deref(), Some("0.1"));
1906 }
1907
1908 #[rstest]
1909 fn deserialize_linear_instrument_with_symbol_type_and_id() {
1910 let json = load_test_json("http_get_instruments_linear_symbol_type.json");
1911 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1912 let instrument = &response.result.list[0];
1913
1914 assert_eq!(instrument.symbol_id, Some(7));
1915 assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Stock));
1916 }
1917
1918 #[derive(Deserialize)]
1919 struct SymbolTypeWrap {
1920 #[serde(rename = "symbolType")]
1921 t: BybitSymbolType,
1922 }
1923
1924 #[rstest]
1925 fn deserialize_symbol_type_falls_back_to_other_for_unknown() {
1926 let json = r#"{"symbolType": "newthing"}"#;
1927 let parsed: SymbolTypeWrap = serde_json::from_str(json).unwrap();
1928 assert_eq!(parsed.t, BybitSymbolType::Other);
1929 }
1930
1931 #[rstest]
1932 fn deserialize_account_info_response() {
1933 let json = load_test_json("http_get_account_info.json");
1934 let response: BybitAccountInfoResponse = serde_json::from_str(&json).unwrap();
1935
1936 assert_eq!(response.result.margin_mode, BybitMarginMode::RegularMargin);
1937 assert_eq!(
1938 response.result.unified_margin_status,
1939 BybitUnifiedMarginStatus::UnifiedTradingAccount10Pro
1940 );
1941 assert!(!response.result.is_master_trader);
1942 assert!(!response.result.spot_hedging_status);
1943 assert!(!response.result.dcp_status);
1944 assert_eq!(response.result.time_window, 10);
1945 assert_eq!(response.result.smp_group, 0);
1946 }
1947
1948 #[rstest]
1949 fn deserialize_account_info_without_deprecated_fields() {
1950 let json = r#"{
1951 "retCode": 0,
1952 "retMsg": "OK",
1953 "result": {
1954 "marginMode": "PORTFOLIO_MARGIN",
1955 "updatedTime": "1697078946000",
1956 "unifiedMarginStatus": 5,
1957 "isMasterTrader": true,
1958 "spotHedgingStatus": "ON"
1959 }
1960 }"#;
1961 let response: BybitAccountInfoResponse = serde_json::from_str(json).unwrap();
1962
1963 assert_eq!(
1964 response.result.margin_mode,
1965 BybitMarginMode::PortfolioMargin
1966 );
1967 assert_eq!(
1968 response.result.unified_margin_status,
1969 BybitUnifiedMarginStatus::UnifiedTradingAccount20
1970 );
1971 assert!(response.result.is_master_trader);
1972 assert!(response.result.spot_hedging_status);
1973 assert!(!response.result.dcp_status);
1974 assert_eq!(response.result.time_window, 0);
1975 assert_eq!(response.result.smp_group, 0);
1976 }
1977
1978 #[rstest]
1979 fn deserialize_account_info_accepts_string_time_window_and_smp_group() {
1980 let mut json: serde_json::Value =
1981 serde_json::from_str(&load_test_json("http_get_account_info.json")).unwrap();
1982 json["result"]["timeWindow"] = serde_json::Value::String("10".to_string());
1983 json["result"]["smpGroup"] = serde_json::Value::String("1234".to_string());
1984
1985 let response: BybitAccountInfoResponse = serde_json::from_value(json).unwrap();
1986
1987 assert_eq!(response.result.time_window, 10);
1988 assert_eq!(response.result.smp_group, 1234);
1989 }
1990
1991 #[rstest]
1992 fn deserialize_order_response_maps_enums() {
1993 let json = load_test_json("http_get_orders_history.json");
1994 let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1995 let order = &response.result.list[0];
1996
1997 assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1998 assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1999 assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
2000 assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
2001 assert_eq!(order.order_type, BybitOrderType::Limit);
2002 assert_eq!(order.smp_type, BybitSmpType::None);
2003 assert_eq!(order.smp_group, 0);
2004 }
2005
2006 #[rstest]
2007 fn deserialize_order_response_accepts_string_smp_group() {
2008 let mut json: serde_json::Value =
2009 serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2010 json["result"]["list"][0]["smpGroup"] = serde_json::Value::String("123456789".to_string());
2011
2012 let response: BybitOrderHistoryResponse = serde_json::from_value(json).unwrap();
2013
2014 assert_eq!(response.result.list[0].smp_group, 123_456_789);
2015 }
2016
2017 #[rstest]
2018 fn deserialize_order_response_accepts_string_position_idx() {
2019 let mut json: serde_json::Value =
2020 serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2021 json["result"]["list"][0]["positionIdx"] = serde_json::Value::String("1".to_string());
2022
2023 let response: BybitOrderHistoryResponse = serde_json::from_value(json).unwrap();
2024
2025 assert_eq!(response.result.list[0].position_idx, 1);
2026 }
2027
2028 #[rstest]
2029 #[case::malformed(
2030 "invalid",
2031 "expected i32, received \"invalid\": invalid digit found in string"
2032 )]
2033 #[case::out_of_range(
2034 "2147483648",
2035 "expected i32, received \"2147483648\": number too large to fit in target type"
2036 )]
2037 fn deserialize_order_response_rejects_invalid_string_smp_group(
2038 #[case] value: &str,
2039 #[case] expected: &str,
2040 ) {
2041 let mut json: serde_json::Value =
2042 serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2043 json["result"]["list"][0]["smpGroup"] = serde_json::Value::String(value.to_string());
2044
2045 let result: Result<BybitOrderHistoryResponse, _> = serde_json::from_value(json);
2046
2047 assert_eq!(result.unwrap_err().to_string(), expected);
2048 }
2049
2050 #[rstest]
2051 fn deserialize_wallet_balance_without_optional_fields() {
2052 let json = r#"{
2053 "retCode": 0,
2054 "retMsg": "OK",
2055 "result": {
2056 "list": [{
2057 "totalEquity": "1000.00",
2058 "accountIMRate": "0",
2059 "totalMarginBalance": "1000.00",
2060 "totalInitialMargin": "0",
2061 "accountType": "UNIFIED",
2062 "totalAvailableBalance": "1000.00",
2063 "accountMMRate": "0",
2064 "totalPerpUPL": "0",
2065 "totalWalletBalance": "1000.00",
2066 "accountLTV": "0",
2067 "totalMaintenanceMargin": "0",
2068 "coin": [{
2069 "availableToBorrow": "0",
2070 "bonus": "0",
2071 "accruedInterest": "0",
2072 "availableToWithdraw": "1000.00",
2073 "equity": "1000.00",
2074 "usdValue": "1000.00",
2075 "borrowAmount": "0",
2076 "totalPositionIM": "0",
2077 "walletBalance": "1000.00",
2078 "unrealisedPnl": "0",
2079 "cumRealisedPnl": "0",
2080 "locked": "0",
2081 "collateralSwitch": true,
2082 "marginCollateral": true,
2083 "coin": "USDT"
2084 }]
2085 }]
2086 }
2087 }"#;
2088
2089 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2090 .expect("Failed to parse wallet balance without optional fields");
2091
2092 assert_eq!(response.ret_code, 0);
2093 assert_eq!(response.result.list[0].coin[0].total_order_im, None);
2094 assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
2095 }
2096
2097 #[rstest]
2098 fn deserialize_wallet_balance_from_docs() {
2099 let json = include_str!("../../test_data/http_get_wallet_balance.json");
2100
2101 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2102 .expect("Failed to parse wallet balance from Bybit docs example");
2103
2104 assert_eq!(response.ret_code, 0);
2105 assert_eq!(response.ret_msg, "OK");
2106
2107 let wallet = &response.result.list[0];
2108 assert_eq!(wallet.total_equity, "3.31216591");
2109 assert_eq!(wallet.account_im_rate, "0");
2110 assert_eq!(wallet.account_mm_rate, "0");
2111 assert_eq!(wallet.total_perp_upl, "0");
2112 assert_eq!(wallet.account_ltv, "0");
2113
2114 let btc = &wallet.coin[0];
2116 assert_eq!(btc.coin, "BTC");
2117 assert_eq!(btc.available_to_borrow, "3");
2118 assert_eq!(btc.total_order_im, Some("0".to_string()));
2119 assert_eq!(btc.total_position_mm, Some("0".to_string()));
2120 assert_eq!(btc.total_position_im, Some("0".to_string()));
2121
2122 let usdt = &wallet.coin[1];
2124 assert_eq!(usdt.coin, "USDT");
2125 assert_eq!(usdt.wallet_balance, dec!(1000.50));
2126 assert_eq!(usdt.total_order_im, None);
2127 assert_eq!(usdt.total_position_mm, None);
2128 assert_eq!(usdt.total_position_im, None);
2129 assert_eq!(btc.spot_borrow, Decimal::ZERO);
2130 assert_eq!(usdt.spot_borrow, Decimal::ZERO);
2131 }
2132
2133 #[rstest]
2134 fn test_parse_wallet_balance_with_spot_borrow() {
2135 let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
2136 let response: BybitWalletBalanceResponse =
2137 serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
2138
2139 let wallet = &response.result.list[0];
2140 let usdt = &wallet.coin[0];
2141
2142 assert_eq!(usdt.coin, "USDT");
2143 assert_eq!(usdt.wallet_balance, dec!(1200.00));
2144 assert_eq!(usdt.spot_borrow, dec!(200.00));
2145 assert_eq!(usdt.borrow_amount, "200.00");
2146
2147 let account_id = crate::common::parse::parse_account_state(
2149 wallet,
2150 AccountId::new("BYBIT-001"),
2151 UnixNanos::default(),
2152 )
2153 .expect("Failed to parse account state");
2154
2155 let balance = &account_id.balances[0];
2156 assert_eq!(balance.total.as_f64(), 1000.0);
2157 }
2158
2159 #[rstest]
2160 fn test_parse_wallet_balance_spot_short() {
2161 let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
2162 let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2163 .expect("Failed to parse wallet balance with SHORT SPOT position");
2164
2165 let wallet = &response.result.list[0];
2166 let eth = &wallet.coin[0];
2167
2168 assert_eq!(eth.coin, "ETH");
2169 assert_eq!(eth.wallet_balance, dec!(0));
2170 assert_eq!(eth.spot_borrow, dec!(0.06142));
2171 assert_eq!(eth.borrow_amount, "0.06142");
2172
2173 let account_state = crate::common::parse::parse_account_state(
2174 wallet,
2175 AccountId::new("BYBIT-001"),
2176 UnixNanos::default(),
2177 )
2178 .expect("Failed to parse account state");
2179
2180 let eth_balance = account_state
2181 .balances
2182 .iter()
2183 .find(|b| b.currency.code == "ETH")
2184 .expect("ETH balance not found");
2185
2186 assert_eq!(eth_balance.total.as_f64(), -0.06142);
2188 }
2189
2190 #[rstest]
2191 fn deserialize_borrow_response() {
2192 let json = r#"{
2193 "retCode": 0,
2194 "retMsg": "success",
2195 "result": {
2196 "coin": "BTC",
2197 "amount": "0.01"
2198 },
2199 "retExtInfo": {},
2200 "time": 1756197991955
2201 }"#;
2202
2203 let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
2204
2205 assert_eq!(response.ret_code, 0);
2206 assert_eq!(response.ret_msg, "success");
2207 assert_eq!(response.result.coin, "BTC");
2208 assert_eq!(response.result.amount, "0.01");
2209 }
2210
2211 #[rstest]
2212 fn deserialize_no_convert_repay_response() {
2213 let json = r#"{
2214 "retCode": 0,
2215 "retMsg": "OK",
2216 "result": {
2217 "resultStatus": "SU"
2218 },
2219 "retExtInfo": {},
2220 "time": 1234567890
2221 }"#;
2222
2223 let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
2224
2225 assert_eq!(response.ret_code, 0);
2226 assert_eq!(response.ret_msg, "OK");
2227 assert_eq!(response.result.result_status, BybitRepayStatus::Success);
2228 }
2229
2230 #[rstest]
2231 fn deserialize_repay_response() {
2232 let json = r#"{
2233 "retCode": 0,
2234 "retMsg": "success",
2235 "result": {
2236 "resultStatus": "P"
2237 },
2238 "retExtInfo": {},
2239 "time": 1756295680801
2240 }"#;
2241
2242 let response: BybitRepayResponse = serde_json::from_str(json).unwrap();
2243
2244 assert_eq!(response.ret_code, 0);
2245 assert_eq!(response.ret_msg, "success");
2246 assert_eq!(response.result.result_status, BybitRepayStatus::Processing);
2247 }
2248
2249 #[rstest]
2250 fn deserialize_position_without_conditional_fields() {
2251 let json = r#"{
2255 "retCode": 0,
2256 "retMsg": "OK",
2257 "result": {
2258 "list": [{
2259 "positionIdx": 0,
2260 "riskId": 1,
2261 "riskLimitValue": "150",
2262 "symbol": "LTCUSDT",
2263 "side": "",
2264 "size": "0",
2265 "avgPrice": "0",
2266 "positionValue": "0",
2267 "tradeMode": 0,
2268 "positionStatus": "Normal",
2269 "autoAddMargin": 0,
2270 "adlRankIndicator": 0,
2271 "leverage": "10",
2272 "positionBalance": "0",
2273 "markPrice": "70.00",
2274 "liqPrice": "",
2275 "bustPrice": "",
2276 "positionMM": "0",
2277 "positionIM": "0",
2278 "tpslMode": "Full",
2279 "takeProfit": "0",
2280 "stopLoss": "0",
2281 "trailingStop": "0",
2282 "unrealisedPnl": "0",
2283 "curRealisedPnl": "0",
2284 "cumRealisedPnl": "0",
2285 "createdTime": "1676538056258",
2286 "updatedTime": "1697673600012"
2287 }],
2288 "nextPageCursor": "",
2289 "category": "linear"
2290 },
2291 "retExtInfo": {},
2292 "time": 1697673900000
2293 }"#;
2294
2295 let response: BybitPositionListResponse = serde_json::from_str(json)
2296 .expect("Failed to parse position list with missing conditional fields");
2297
2298 let position = &response.result.list[0];
2299 assert!(!position.is_reduce_only);
2300 assert_eq!(position.seq, -1);
2301 assert_eq!(position.mmr_sys_updated_time, "");
2302 assert_eq!(position.leverage_sys_updated_time, "");
2303 assert_eq!(position.open_time, 0);
2304 }
2305
2306 #[rstest]
2307 #[case(0_i64)]
2308 #[case(1_700_000_000_123_i64)]
2309 fn deserialize_position_with_open_time_integer(#[case] expected: i64) {
2310 let mut value: serde_json::Value =
2312 serde_json::from_str(&load_test_json("http_get_positions_with_open_time.json"))
2313 .unwrap();
2314 value["result"]["list"][0]["openTime"] = serde_json::json!(expected);
2315
2316 let response: BybitPositionListResponse = serde_json::from_value(value)
2317 .expect("Failed to parse position list with integer openTime");
2318
2319 assert_eq!(response.result.list[0].open_time, expected);
2320 }
2321
2322 #[rstest]
2323 fn deserialize_position_response_accepts_string_integer_fields() {
2324 let mut json: serde_json::Value =
2325 serde_json::from_str(&load_test_json("http_get_positions.json")).unwrap();
2326 let position = &mut json["result"]["list"][0];
2327 position["riskId"] = serde_json::Value::String("1234".to_string());
2328 position["tradeMode"] = serde_json::Value::String("1".to_string());
2329 position["autoAddMargin"] = serde_json::Value::String("2".to_string());
2330 position["adlRankIndicator"] = serde_json::Value::String("35".to_string());
2331
2332 let response: BybitPositionListResponse = serde_json::from_value(json).unwrap();
2333
2334 let position = &response.result.list[0];
2335 assert_eq!(position.risk_id, 1234);
2336 assert_eq!(position.trade_mode, 1);
2337 assert_eq!(position.auto_add_margin, 2);
2338 assert_eq!(position.adl_rank_indicator, 35);
2339 }
2340
2341 #[rstest]
2342 fn deserialize_inverse_instrument_with_symbol_type_and_id() {
2343 let json = load_test_json("http_get_instruments_inverse_symbol_type.json");
2344 let response: BybitInstrumentInverseResponse = serde_json::from_str(&json).unwrap();
2345 let instrument = &response.result.list[0];
2346
2347 assert_eq!(instrument.symbol_id, Some(11));
2348 assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Commodity));
2349 }
2350
2351 #[rstest]
2352 fn deserialize_option_instrument_with_symbol_id() {
2353 let json = load_test_json("http_get_instruments_option_symbol_id.json");
2354 let response: BybitInstrumentOptionResponse = serde_json::from_str(&json).unwrap();
2355 let instrument = &response.result.list[0];
2356
2357 assert_eq!(instrument.symbol_id, Some(99));
2358 }
2359
2360 #[rstest]
2361 fn deserialize_sub_members_response() {
2362 let json = load_test_json("http_get_user_sub_members.json");
2363 let response: BybitSubMembersResponse =
2364 serde_json::from_str(&json).expect("parse sub members");
2365 assert_eq!(response.ret_code, 0);
2366 assert_eq!(response.result.sub_members.len(), 2);
2367 let first = &response.result.sub_members[0];
2368 assert_eq!(first.uid, "106314365");
2369 assert_eq!(first.username, "xxxx02");
2370 assert_eq!(first.member_type, 1);
2371 assert_eq!(first.status, 1);
2372 assert_eq!(first.account_mode, 5);
2373 assert_eq!(first.remark, "");
2374 let second = &response.result.sub_members[1];
2375 assert_eq!(second.uid, "106279879");
2376 assert_eq!(second.account_mode, 6);
2377 }
2378
2379 #[rstest]
2380 fn deserialize_sub_members_paged_response() {
2381 let json = load_test_json("http_get_user_sub_members_paged.json");
2384 let response: BybitSubMembersPagedResponse =
2385 serde_json::from_str(&json).expect("parse paged sub members");
2386 assert_eq!(response.result.sub_members.len(), 2);
2387 assert_eq!(response.result.next_cursor.as_deref(), Some("0"));
2388 assert!(!response.result.has_more_pages());
2389 assert_eq!(response.result.continuation_cursor(), None);
2390 }
2391
2392 #[rstest]
2393 fn deserialize_escrow_sub_members_response_uses_same_shape() {
2394 let json = load_test_json("http_get_user_escrow_sub_members.json");
2397 let response: BybitEscrowSubMembersResponse =
2398 serde_json::from_str(&json).expect("parse escrow sub members");
2399 assert_eq!(response.result.sub_members.len(), 2);
2400 assert_eq!(response.result.sub_members[0].member_type, 12);
2401 assert_eq!(response.result.sub_members[0].remark, "earn fund");
2402 assert_eq!(response.result.next_cursor.as_deref(), Some("344"));
2403 assert!(response.result.has_more_pages());
2404 assert_eq!(response.result.continuation_cursor(), Some("344"));
2405 }
2406
2407 #[rstest]
2408 fn deserialize_sub_api_keys_response() {
2409 let json = load_test_json("http_get_user_sub_apikeys.json");
2412 let response: BybitSubApiKeysResponse =
2413 serde_json::from_str(&json).expect("parse sub apikeys");
2414 assert_eq!(response.result.keys.len(), 1);
2415 let key = &response.result.keys[0];
2416 let debug = format!("{key:?}");
2417
2418 assert!(!key.read_only);
2419 assert_eq!(key.secret, None);
2420 assert_eq!(key.key_type, BybitApiKeyType::Hmac);
2421 assert_eq!(key.flag, "hmac");
2422 assert_eq!(key.deadline_day, Some(21));
2423 assert_eq!(key.permissions.contract_trade, vec!["Order", "Position"]);
2424 assert_eq!(key.permissions.spot, vec!["SpotTrade"]);
2425 assert!(key.permissions.earn.is_empty());
2426 assert_eq!(response.result.next_page_cursor.as_deref(), Some(""));
2427 assert!(!response.result.has_more_pages());
2428 assert!(debug.contains("api_key: <redacted>"));
2429 assert!(debug.contains("secret: None"));
2430 assert!(!debug.contains("XXXXXX"));
2431 }
2432
2433 #[rstest]
2434 fn deserialize_update_sub_api_response() {
2435 let json = load_test_json("http_post_user_update_sub_api.json");
2436 let response: BybitUpdateSubApiResponse =
2437 serde_json::from_str(&json).expect("parse update sub api");
2438 let debug = format!("{:?}", response.result);
2439
2440 assert!(!response.result.read_only);
2441 assert_eq!(response.result.secret, None);
2442 assert_eq!(response.result.ips, vec!["*"]);
2443 assert_eq!(response.result.permissions.spot, vec!["SpotTrade"]);
2444 assert_eq!(response.result.permissions.wallet, vec!["AccountTransfer"]);
2445 assert!(debug.contains("api_key: <redacted>"));
2446 assert!(debug.contains("secret: None"));
2447 assert!(!debug.contains("xxxxxx"));
2448 }
2449
2450 #[rstest]
2451 fn deserialize_update_master_api_response() {
2452 let json = load_test_json("http_post_user_update_master_api.json");
2457 let response: BybitUpdateMasterApiResponse =
2458 serde_json::from_str(&json).expect("parse update master api");
2459 assert!(!response.result.read_only);
2460 assert_eq!(response.result.ips, vec!["*"]);
2461 let perms = &response.result.permissions;
2462 assert_eq!(perms.contract_trade, vec!["Order", "Position"]);
2463 assert_eq!(perms.copy_trading, vec!["CopyTrading"]);
2464 assert!(perms.earn.is_empty());
2465 assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2466 }
2467
2468 #[rstest]
2469 fn deserialize_permissions_renamed_buckets_preserve_values() {
2470 let json = r#"{
2476 "NFT": ["NFTQueryProductList"],
2477 "FiatP2P": ["P2PDeposit"],
2478 "ByXPost": ["PostContent"]
2479 }"#;
2480 let perms: BybitApiKeyPermissions =
2481 serde_json::from_str(json).expect("parse renamed buckets");
2482 assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2483 assert_eq!(perms.fiat_p2p, vec!["P2PDeposit"]);
2484 assert_eq!(perms.byx_post, vec!["PostContent"]);
2485 }
2486
2487 #[rstest]
2488 fn deserialize_account_details_response_with_current_docs_example() {
2489 let json = load_test_json("http_get_user_query_api.json");
2490 let response: BybitAccountDetailsResponse =
2491 serde_json::from_str(&json).expect("parse account details");
2492 let debug = format!("{:?}", response.result);
2493
2494 assert_eq!(
2495 response.result.permissions.fiat_global_pay,
2496 Vec::<String>::new()
2497 );
2498 assert_eq!(
2499 response.result.permissions.fiat_bit_pay,
2500 vec!["FaitPayOrder"]
2501 );
2502 assert_eq!(response.result.permissions.bit_card, vec!["BitCard"]);
2503 assert_eq!(response.result.permissions.byx_post, vec!["ByXPost"]);
2504 assert_eq!(response.result.unified, Some(0));
2505 assert!(debug.contains("api_key: <redacted>"));
2506 assert!(debug.contains("secret: <redacted>"));
2507 assert!(!debug.contains("api_key: \"XXXXXXXX\""));
2508 }
2509}