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