Skip to main content

nautilus_bybit/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data transfer objects for deserializing Bybit HTTP API payloads.
17
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::{
23    enums::{
24        BybitAccountType, BybitApiKeyType, BybitCancelType, BybitContractType, BybitCreateType,
25        BybitExecType, BybitInnovationFlag, BybitInstrumentStatus, BybitMarginMode,
26        BybitMarginTrading, BybitOptionType, BybitOrderSide, BybitOrderStatus, BybitOrderType,
27        BybitPositionIdx, BybitPositionSide, BybitPositionStatus, BybitProductType, BybitSmpType,
28        BybitStopOrderType, BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection,
29        BybitTriggerType, BybitUnifiedMarginStatus,
30    },
31    models::{
32        BybitCursorList, BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
33        LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
34        SpotPriceFilter,
35    },
36    parse::{
37        bool_or_int, deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero,
38        deserialize_string_to_u8, masked_secret, on_off_bool,
39    },
40};
41
42/// Cursor-paginated list of orders for Python bindings.
43#[derive(Clone, Debug, Default, Serialize, Deserialize)]
44#[cfg_attr(
45    feature = "python",
46    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
47)]
48#[cfg_attr(
49    feature = "python",
50    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
51)]
52pub struct BybitOrderCursorList {
53    /// Collection of orders returned by the endpoint.
54    pub list: Vec<BybitOrder>,
55    /// Pagination cursor for the next page.
56    pub next_page_cursor: Option<String>,
57    /// Optional product category when the API includes it.
58    #[serde(default)]
59    pub category: Option<BybitProductType>,
60}
61
62impl From<BybitCursorList<BybitOrder>> for BybitOrderCursorList {
63    fn from(cursor_list: BybitCursorList<BybitOrder>) -> Self {
64        Self {
65            list: cursor_list.list,
66            next_page_cursor: cursor_list.next_page_cursor,
67            category: cursor_list.category,
68        }
69    }
70}
71
72#[cfg(feature = "python")]
73#[pyo3::pymethods]
74impl BybitOrderCursorList {
75    #[getter]
76    #[must_use]
77    pub fn list(&self) -> Vec<BybitOrder> {
78        self.list.clone()
79    }
80
81    #[getter]
82    #[must_use]
83    pub fn next_page_cursor(&self) -> Option<&str> {
84        self.next_page_cursor.as_deref()
85    }
86
87    #[getter]
88    #[must_use]
89    pub fn category(&self) -> Option<BybitProductType> {
90        self.category
91    }
92}
93
94/// Response payload returned by `GET /v5/market/time`.
95///
96/// # References
97/// - <https://bybit-exchange.github.io/docs/v5/market/time>
98#[derive(Clone, Debug, Serialize, Deserialize)]
99#[cfg_attr(
100    feature = "python",
101    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
102)]
103#[cfg_attr(
104    feature = "python",
105    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
106)]
107#[serde(rename_all = "camelCase")]
108pub struct BybitServerTime {
109    /// Server timestamp in seconds represented as string.
110    pub time_second: String,
111    /// Server timestamp in nanoseconds represented as string.
112    pub time_nano: String,
113}
114
115#[cfg(feature = "python")]
116#[pyo3::pymethods]
117impl BybitServerTime {
118    #[getter]
119    #[must_use]
120    pub fn time_second(&self) -> &str {
121        &self.time_second
122    }
123
124    #[getter]
125    #[must_use]
126    pub fn time_nano(&self) -> &str {
127        &self.time_nano
128    }
129}
130
131/// Type alias for the server time response envelope.
132///
133/// # References
134/// - <https://bybit-exchange.github.io/docs/v5/market/time>
135pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
136
137/// Ticker payload for spot instruments.
138///
139/// # References
140/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
141#[derive(Clone, Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct BybitTickerSpot {
144    pub symbol: Ustr,
145    pub bid1_price: String,
146    pub bid1_size: String,
147    pub ask1_price: String,
148    pub ask1_size: String,
149    pub last_price: String,
150    pub prev_price24h: String,
151    pub price24h_pcnt: String,
152    pub high_price24h: String,
153    pub low_price24h: String,
154    pub turnover24h: String,
155    pub volume24h: String,
156    #[serde(default)]
157    pub usd_index_price: String,
158}
159
160/// Ticker payload for linear and inverse perpetual/futures instruments.
161///
162/// # References
163/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
164#[derive(Clone, Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct BybitTickerLinear {
167    pub symbol: Ustr,
168    pub last_price: String,
169    pub index_price: String,
170    pub mark_price: String,
171    pub prev_price24h: String,
172    pub price24h_pcnt: String,
173    pub high_price24h: String,
174    pub low_price24h: String,
175    pub prev_price1h: String,
176    pub open_interest: String,
177    pub open_interest_value: String,
178    pub turnover24h: String,
179    pub volume24h: String,
180    pub funding_rate: String,
181    pub next_funding_time: String,
182    pub predicted_delivery_price: String,
183    pub basis_rate: String,
184    pub delivery_fee_rate: String,
185    pub delivery_time: String,
186    pub ask1_size: String,
187    pub bid1_price: String,
188    pub ask1_price: String,
189    pub bid1_size: String,
190    pub basis: String,
191}
192
193/// Ticker payload for option instruments.
194///
195/// # References
196/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
197#[derive(Clone, Debug, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct BybitTickerOption {
200    pub symbol: Ustr,
201    pub bid1_price: String,
202    pub bid1_size: String,
203    pub bid1_iv: String,
204    pub ask1_price: String,
205    pub ask1_size: String,
206    pub ask1_iv: String,
207    pub last_price: String,
208    pub high_price24h: String,
209    pub low_price24h: String,
210    pub mark_price: String,
211    pub index_price: String,
212    pub mark_iv: String,
213    pub underlying_price: String,
214    pub open_interest: String,
215    pub turnover24h: String,
216    pub volume24h: String,
217    pub total_volume: String,
218    pub total_turnover: String,
219    pub delta: String,
220    pub gamma: String,
221    pub vega: String,
222    pub theta: String,
223    pub predicted_delivery_price: String,
224    pub change24h: String,
225}
226
227/// Response alias for spot ticker requests.
228///
229/// # References
230/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
231pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
232/// Response alias for linear/inverse ticker requests.
233///
234/// # References
235/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
236pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
237/// Response alias for option ticker requests.
238///
239/// # References
240/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
241pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
242
243/// Unified ticker data structure containing common fields across all product types.
244///
245/// This simplified ticker structure is designed to work across SPOT, LINEAR, and OPTION products,
246/// containing only the most commonly used fields.
247#[derive(Clone, Debug, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249#[cfg_attr(
250    feature = "python",
251    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
252)]
253#[cfg_attr(
254    feature = "python",
255    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
256)]
257pub struct BybitTickerData {
258    pub symbol: Ustr,
259    pub bid1_price: String,
260    pub bid1_size: String,
261    pub ask1_price: String,
262    pub ask1_size: String,
263    pub last_price: String,
264    pub high_price24h: String,
265    pub low_price24h: String,
266    pub turnover24h: String,
267    pub volume24h: String,
268    #[serde(default)]
269    pub open_interest: Option<String>,
270    #[serde(default)]
271    pub funding_rate: Option<String>,
272    #[serde(default)]
273    pub next_funding_time: Option<String>,
274    #[serde(default)]
275    pub mark_price: Option<String>,
276    #[serde(default)]
277    pub index_price: Option<String>,
278}
279
280#[cfg(feature = "python")]
281#[pyo3::pymethods]
282impl BybitTickerData {
283    #[getter]
284    #[must_use]
285    pub fn symbol(&self) -> &str {
286        self.symbol.as_str()
287    }
288
289    #[getter]
290    #[must_use]
291    pub fn bid1_price(&self) -> &str {
292        &self.bid1_price
293    }
294
295    #[getter]
296    #[must_use]
297    pub fn bid1_size(&self) -> &str {
298        &self.bid1_size
299    }
300
301    #[getter]
302    #[must_use]
303    pub fn ask1_price(&self) -> &str {
304        &self.ask1_price
305    }
306
307    #[getter]
308    #[must_use]
309    pub fn ask1_size(&self) -> &str {
310        &self.ask1_size
311    }
312
313    #[getter]
314    #[must_use]
315    pub fn last_price(&self) -> &str {
316        &self.last_price
317    }
318
319    #[getter]
320    #[must_use]
321    pub fn high_price24h(&self) -> &str {
322        &self.high_price24h
323    }
324
325    #[getter]
326    #[must_use]
327    pub fn low_price24h(&self) -> &str {
328        &self.low_price24h
329    }
330
331    #[getter]
332    #[must_use]
333    pub fn turnover24h(&self) -> &str {
334        &self.turnover24h
335    }
336
337    #[getter]
338    #[must_use]
339    pub fn volume24h(&self) -> &str {
340        &self.volume24h
341    }
342
343    #[getter]
344    #[must_use]
345    pub fn open_interest(&self) -> Option<&str> {
346        self.open_interest.as_deref()
347    }
348
349    #[getter]
350    #[must_use]
351    pub fn funding_rate(&self) -> Option<&str> {
352        self.funding_rate.as_deref()
353    }
354
355    #[getter]
356    #[must_use]
357    pub fn next_funding_time(&self) -> Option<&str> {
358        self.next_funding_time.as_deref()
359    }
360
361    #[getter]
362    #[must_use]
363    pub fn mark_price(&self) -> Option<&str> {
364        self.mark_price.as_deref()
365    }
366
367    #[getter]
368    #[must_use]
369    pub fn index_price(&self) -> Option<&str> {
370        self.index_price.as_deref()
371    }
372}
373
374impl From<BybitTickerSpot> for BybitTickerData {
375    fn from(ticker: BybitTickerSpot) -> Self {
376        Self {
377            symbol: ticker.symbol,
378            bid1_price: ticker.bid1_price,
379            bid1_size: ticker.bid1_size,
380            ask1_price: ticker.ask1_price,
381            ask1_size: ticker.ask1_size,
382            last_price: ticker.last_price,
383            high_price24h: ticker.high_price24h,
384            low_price24h: ticker.low_price24h,
385            turnover24h: ticker.turnover24h,
386            volume24h: ticker.volume24h,
387            open_interest: None,
388            funding_rate: None,
389            next_funding_time: None,
390            mark_price: None,
391            index_price: None,
392        }
393    }
394}
395
396impl From<BybitTickerLinear> for BybitTickerData {
397    fn from(ticker: BybitTickerLinear) -> Self {
398        Self {
399            symbol: ticker.symbol,
400            bid1_price: ticker.bid1_price,
401            bid1_size: ticker.bid1_size,
402            ask1_price: ticker.ask1_price,
403            ask1_size: ticker.ask1_size,
404            last_price: ticker.last_price,
405            high_price24h: ticker.high_price24h,
406            low_price24h: ticker.low_price24h,
407            turnover24h: ticker.turnover24h,
408            volume24h: ticker.volume24h,
409            open_interest: Some(ticker.open_interest),
410            funding_rate: Some(ticker.funding_rate),
411            next_funding_time: Some(ticker.next_funding_time),
412            mark_price: Some(ticker.mark_price),
413            index_price: Some(ticker.index_price),
414        }
415    }
416}
417
418impl From<BybitTickerOption> for BybitTickerData {
419    fn from(ticker: BybitTickerOption) -> Self {
420        Self {
421            symbol: ticker.symbol,
422            bid1_price: ticker.bid1_price,
423            bid1_size: ticker.bid1_size,
424            ask1_price: ticker.ask1_price,
425            ask1_size: ticker.ask1_size,
426            last_price: ticker.last_price,
427            high_price24h: ticker.high_price24h,
428            low_price24h: ticker.low_price24h,
429            turnover24h: ticker.turnover24h,
430            volume24h: ticker.volume24h,
431            open_interest: Some(ticker.open_interest),
432            funding_rate: None,
433            next_funding_time: None,
434            mark_price: Some(ticker.mark_price),
435            index_price: Some(ticker.index_price),
436        }
437    }
438}
439
440/// Kline/candlestick entry returned by `GET /v5/market/kline`.
441///
442/// Bybit returns klines as arrays with 7 elements:
443/// [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
444///
445/// # References
446/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
447#[derive(Clone, Debug, Serialize)]
448pub struct BybitKline {
449    pub start: String,
450    pub open: String,
451    pub high: String,
452    pub low: String,
453    pub close: String,
454    pub volume: String,
455    pub turnover: String,
456}
457
458impl<'de> Deserialize<'de> for BybitKline {
459    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
460    where
461        D: serde::Deserializer<'de>,
462    {
463        let [start, open, high, low, close, volume, turnover]: [String; 7] =
464            Deserialize::deserialize(deserializer)?;
465        Ok(Self {
466            start,
467            open,
468            high,
469            low,
470            close,
471            volume,
472            turnover,
473        })
474    }
475}
476
477/// Kline list result returned by Bybit.
478///
479/// # References
480/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
481#[derive(Clone, Debug, Serialize, Deserialize)]
482#[serde(rename_all = "camelCase")]
483pub struct BybitKlineResult {
484    pub category: BybitProductType,
485    pub symbol: Ustr,
486    pub list: Vec<BybitKline>,
487}
488
489/// Response alias for kline history requests.
490///
491/// # References
492/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
493pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
494
495/// Trade entry returned by `GET /v5/market/recent-trade`.
496///
497/// # References
498/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
499#[derive(Clone, Debug, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct BybitTrade {
502    pub exec_id: String,
503    pub symbol: Ustr,
504    pub price: String,
505    pub size: String,
506    pub side: BybitOrderSide,
507    pub time: String,
508    pub is_block_trade: bool,
509    #[serde(default)]
510    pub m_p: Option<String>,
511    #[serde(default)]
512    pub i_p: Option<String>,
513    #[serde(default)]
514    pub mlv: Option<String>,
515    #[serde(default)]
516    pub iv: Option<String>,
517}
518
519/// Trade list result returned by Bybit.
520///
521/// # References
522/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
523#[derive(Clone, Debug, Serialize, Deserialize)]
524#[serde(rename_all = "camelCase")]
525pub struct BybitTradeResult {
526    pub category: BybitProductType,
527    pub list: Vec<BybitTrade>,
528}
529
530/// Response alias for recent trades requests.
531///
532/// # References
533/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
534pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
535
536/// Funding entry returned by `GET /v5/market/funding/history`.
537///
538/// # References
539/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
540#[derive(Clone, Debug, Serialize, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct BybitFunding {
543    pub symbol: Ustr,
544    pub funding_rate: String,
545    pub funding_rate_timestamp: String,
546}
547
548/// Funding list result returned by Bybit.
549///
550/// # References
551/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
552#[derive(Clone, Debug, Serialize, Deserialize)]
553#[serde(rename_all = "camelCase")]
554pub struct BybitFundingResult {
555    pub category: BybitProductType,
556    pub list: Vec<BybitFunding>,
557}
558
559/// Response alias for historical funding requests.
560///
561/// # References
562/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
563pub type BybitFundingResponse = BybitResponse<BybitFundingResult>;
564
565/// Orderbook result returned by Bybit.
566///
567/// # References
568/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
569#[derive(Clone, Debug, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct BybitOrderbookResult {
572    /// Symbol.
573    pub s: Ustr,
574    /// Bid levels represented as `[price, size]` string pairs.
575    pub b: Vec<[String; 2]>,
576    /// Ask levels represented as `[price, size]` string pairs.
577    pub a: Vec<[String; 2]>,
578    pub ts: i64,
579    /// Update identifier.
580    pub u: i64,
581    /// Cross sequence number.
582    pub seq: i64,
583    pub cts: i64,
584}
585
586/// Response alias for orderbook requests.
587///
588/// # References
589/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
590pub type BybitOrderbookResponse = BybitResponse<BybitOrderbookResult>;
591
592/// Instrument definition for spot symbols.
593///
594/// # References
595/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
596#[derive(Clone, Debug, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct BybitInstrumentSpot {
599    pub symbol: Ustr,
600    pub base_coin: Ustr,
601    pub quote_coin: Ustr,
602    pub innovation: BybitInnovationFlag,
603    pub status: BybitInstrumentStatus,
604    pub margin_trading: BybitMarginTrading,
605    pub lot_size_filter: SpotLotSizeFilter,
606    pub price_filter: SpotPriceFilter,
607}
608
609/// Instrument definition for linear contracts.
610///
611/// # References
612/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
613#[derive(Clone, Debug, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub struct BybitInstrumentLinear {
616    pub symbol: Ustr,
617    pub contract_type: BybitContractType,
618    pub status: BybitInstrumentStatus,
619    pub base_coin: Ustr,
620    pub quote_coin: Ustr,
621    pub launch_time: String,
622    pub delivery_time: String,
623    pub delivery_fee_rate: String,
624    pub price_scale: String,
625    pub leverage_filter: LeverageFilter,
626    pub price_filter: LinearPriceFilter,
627    pub lot_size_filter: LinearLotSizeFilter,
628    pub unified_margin_trade: bool,
629    pub funding_interval: i64,
630    pub settle_coin: Ustr,
631}
632
633/// Instrument definition for inverse contracts.
634///
635/// # References
636/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
637#[derive(Clone, Debug, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct BybitInstrumentInverse {
640    pub symbol: Ustr,
641    pub contract_type: BybitContractType,
642    pub status: BybitInstrumentStatus,
643    pub base_coin: Ustr,
644    pub quote_coin: Ustr,
645    pub launch_time: String,
646    pub delivery_time: String,
647    pub delivery_fee_rate: String,
648    pub price_scale: String,
649    pub leverage_filter: LeverageFilter,
650    pub price_filter: LinearPriceFilter,
651    pub lot_size_filter: LinearLotSizeFilter,
652    pub unified_margin_trade: bool,
653    pub funding_interval: i64,
654    pub settle_coin: Ustr,
655}
656
657/// Instrument definition for option contracts.
658///
659/// # References
660/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
661#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct BybitInstrumentOption {
664    pub symbol: Ustr,
665    pub status: BybitInstrumentStatus,
666    pub base_coin: Ustr,
667    pub quote_coin: Ustr,
668    pub settle_coin: Ustr,
669    pub options_type: BybitOptionType,
670    pub launch_time: String,
671    pub delivery_time: String,
672    pub delivery_fee_rate: String,
673    pub price_filter: LinearPriceFilter,
674    pub lot_size_filter: OptionLotSizeFilter,
675}
676
677/// Response alias for instrument info requests that return spot instruments.
678///
679/// # References
680/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
681pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
682/// Response alias for instrument info requests that return linear contracts.
683///
684/// # References
685/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
686pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
687/// Response alias for instrument info requests that return inverse contracts.
688///
689/// # References
690/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
691pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
692/// Response alias for instrument info requests that return option contracts.
693///
694/// # References
695/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
696pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
697
698/// Fee rate structure returned by `GET /v5/account/fee-rate`.
699///
700/// # References
701/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
702#[derive(Clone, Debug, Serialize, Deserialize)]
703#[serde(rename_all = "camelCase")]
704#[cfg_attr(
705    feature = "python",
706    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
707)]
708#[cfg_attr(
709    feature = "python",
710    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
711)]
712pub struct BybitFeeRate {
713    pub symbol: Ustr,
714    pub taker_fee_rate: String,
715    pub maker_fee_rate: String,
716    #[serde(default)]
717    pub base_coin: Option<Ustr>,
718}
719
720#[cfg(feature = "python")]
721#[pyo3::pymethods]
722impl BybitFeeRate {
723    #[getter]
724    #[must_use]
725    pub fn symbol(&self) -> &str {
726        self.symbol.as_str()
727    }
728
729    #[getter]
730    #[must_use]
731    pub fn taker_fee_rate(&self) -> &str {
732        &self.taker_fee_rate
733    }
734
735    #[getter]
736    #[must_use]
737    pub fn maker_fee_rate(&self) -> &str {
738        &self.maker_fee_rate
739    }
740
741    #[getter]
742    #[must_use]
743    pub fn base_coin(&self) -> Option<&str> {
744        self.base_coin.as_ref().map(|u| u.as_str())
745    }
746}
747
748/// Response alias for fee rate requests.
749///
750/// # References
751/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
752pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
753
754/// Account balance snapshot coin entry.
755///
756/// # References
757/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
758#[derive(Clone, Debug, Serialize, Deserialize)]
759#[serde(rename_all = "camelCase")]
760pub struct BybitCoinBalance {
761    pub available_to_borrow: String,
762    pub bonus: String,
763    pub accrued_interest: String,
764    pub available_to_withdraw: String,
765    #[serde(default, rename = "totalOrderIM")]
766    pub total_order_im: Option<String>,
767    pub equity: String,
768    pub usd_value: String,
769    pub borrow_amount: String,
770    #[serde(default, rename = "totalPositionMM")]
771    pub total_position_mm: Option<String>,
772    #[serde(default, rename = "totalPositionIM")]
773    pub total_position_im: Option<String>,
774    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
775    pub wallet_balance: Decimal,
776    pub unrealised_pnl: String,
777    pub cum_realised_pnl: String,
778    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
779    pub locked: Decimal,
780    pub collateral_switch: bool,
781    pub margin_collateral: bool,
782    pub coin: Ustr,
783    #[serde(default)]
784    pub spot_hedging_qty: Option<String>,
785    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
786    pub spot_borrow: Decimal,
787}
788
789/// Wallet balance snapshot containing per-coin balances.
790///
791/// # References
792/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
793#[derive(Clone, Debug, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795pub struct BybitWalletBalance {
796    pub total_equity: String,
797    #[serde(rename = "accountIMRate")]
798    pub account_im_rate: String,
799    pub total_margin_balance: String,
800    pub total_initial_margin: String,
801    pub account_type: BybitAccountType,
802    pub total_available_balance: String,
803    #[serde(rename = "accountMMRate")]
804    pub account_mm_rate: String,
805    #[serde(rename = "totalPerpUPL")]
806    pub total_perp_upl: String,
807    pub total_wallet_balance: String,
808    #[serde(rename = "accountLTV")]
809    pub account_ltv: String,
810    pub total_maintenance_margin: String,
811    pub coin: Vec<BybitCoinBalance>,
812}
813
814/// Response alias for wallet balance requests.
815///
816/// # References
817/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
818pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
819
820/// Account-level configuration returned by `GET /v5/account/info`.
821///
822/// # References
823/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
824#[derive(Clone, Debug, Serialize, Deserialize)]
825#[serde(rename_all = "camelCase")]
826pub struct BybitAccountInfo {
827    pub unified_margin_status: BybitUnifiedMarginStatus,
828    pub margin_mode: BybitMarginMode,
829    pub is_master_trader: bool,
830    #[serde(with = "on_off_bool")]
831    pub spot_hedging_status: bool,
832    pub updated_time: String,
833    // `dcp_status`, `time_window`, and `smp_group` are absent from responses
834    // for accounts that predate the disconnection-protection feature.
835    #[serde(default, with = "on_off_bool")]
836    pub dcp_status: bool,
837    #[serde(default)]
838    pub time_window: i32,
839    #[serde(default)]
840    pub smp_group: i32,
841}
842
843/// Response alias for account info requests.
844///
845/// # References
846/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
847pub type BybitAccountInfoResponse = BybitResponse<BybitAccountInfo>;
848
849/// Order representation as returned by order-related endpoints.
850///
851/// # References
852/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
853#[derive(Clone, Debug, Serialize, Deserialize)]
854#[cfg_attr(
855    feature = "python",
856    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
857)]
858#[cfg_attr(
859    feature = "python",
860    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
861)]
862#[serde(rename_all = "camelCase")]
863pub struct BybitOrder {
864    pub order_id: Ustr,
865    pub order_link_id: Ustr,
866    pub block_trade_id: Option<Ustr>,
867    pub symbol: Ustr,
868    pub price: String,
869    pub qty: String,
870    pub side: BybitOrderSide,
871    pub is_leverage: String,
872    pub position_idx: i32,
873    pub order_status: BybitOrderStatus,
874    pub cancel_type: BybitCancelType,
875    pub reject_reason: Ustr,
876    pub avg_price: Option<String>,
877    pub leaves_qty: String,
878    pub leaves_value: String,
879    pub cum_exec_qty: String,
880    pub cum_exec_value: String,
881    pub cum_exec_fee: String,
882    pub time_in_force: BybitTimeInForce,
883    pub order_type: BybitOrderType,
884    pub stop_order_type: BybitStopOrderType,
885    pub order_iv: Option<String>,
886    pub trigger_price: String,
887    pub take_profit: String,
888    pub stop_loss: String,
889    pub tp_trigger_by: BybitTriggerType,
890    pub sl_trigger_by: BybitTriggerType,
891    pub trigger_direction: BybitTriggerDirection,
892    pub trigger_by: BybitTriggerType,
893    pub last_price_on_created: String,
894    pub reduce_only: bool,
895    pub close_on_trigger: bool,
896    pub smp_type: BybitSmpType,
897    pub smp_group: i32,
898    pub smp_order_id: Ustr,
899    pub tpsl_mode: Option<BybitTpSlMode>,
900    pub tp_limit_price: String,
901    pub sl_limit_price: String,
902    pub place_type: Ustr,
903    pub created_time: String,
904    pub updated_time: String,
905}
906
907#[cfg(feature = "python")]
908#[pyo3::pymethods]
909impl BybitOrder {
910    #[getter]
911    #[must_use]
912    pub fn order_id(&self) -> &str {
913        self.order_id.as_str()
914    }
915
916    #[getter]
917    #[must_use]
918    pub fn order_link_id(&self) -> &str {
919        self.order_link_id.as_str()
920    }
921
922    #[getter]
923    #[must_use]
924    pub fn block_trade_id(&self) -> Option<&str> {
925        self.block_trade_id.as_ref().map(|s| s.as_str())
926    }
927
928    #[getter]
929    #[must_use]
930    pub fn symbol(&self) -> &str {
931        self.symbol.as_str()
932    }
933
934    #[getter]
935    #[must_use]
936    pub fn price(&self) -> &str {
937        &self.price
938    }
939
940    #[getter]
941    #[must_use]
942    pub fn qty(&self) -> &str {
943        &self.qty
944    }
945
946    #[getter]
947    #[must_use]
948    pub fn side(&self) -> BybitOrderSide {
949        self.side
950    }
951
952    #[getter]
953    #[must_use]
954    pub fn is_leverage(&self) -> &str {
955        &self.is_leverage
956    }
957
958    #[getter]
959    #[must_use]
960    pub fn position_idx(&self) -> i32 {
961        self.position_idx
962    }
963
964    #[getter]
965    #[must_use]
966    pub fn order_status(&self) -> BybitOrderStatus {
967        self.order_status
968    }
969
970    #[getter]
971    #[must_use]
972    pub fn cancel_type(&self) -> BybitCancelType {
973        self.cancel_type
974    }
975
976    #[getter]
977    #[must_use]
978    pub fn reject_reason(&self) -> &str {
979        self.reject_reason.as_str()
980    }
981
982    #[getter]
983    #[must_use]
984    pub fn avg_price(&self) -> Option<&str> {
985        self.avg_price.as_deref()
986    }
987
988    #[getter]
989    #[must_use]
990    pub fn leaves_qty(&self) -> &str {
991        &self.leaves_qty
992    }
993
994    #[getter]
995    #[must_use]
996    pub fn leaves_value(&self) -> &str {
997        &self.leaves_value
998    }
999
1000    #[getter]
1001    #[must_use]
1002    pub fn cum_exec_qty(&self) -> &str {
1003        &self.cum_exec_qty
1004    }
1005
1006    #[getter]
1007    #[must_use]
1008    pub fn cum_exec_value(&self) -> &str {
1009        &self.cum_exec_value
1010    }
1011
1012    #[getter]
1013    #[must_use]
1014    pub fn cum_exec_fee(&self) -> &str {
1015        &self.cum_exec_fee
1016    }
1017
1018    #[getter]
1019    #[must_use]
1020    pub fn time_in_force(&self) -> BybitTimeInForce {
1021        self.time_in_force
1022    }
1023
1024    #[getter]
1025    #[must_use]
1026    pub fn order_type(&self) -> BybitOrderType {
1027        self.order_type
1028    }
1029
1030    #[getter]
1031    #[must_use]
1032    pub fn stop_order_type(&self) -> BybitStopOrderType {
1033        self.stop_order_type
1034    }
1035
1036    #[getter]
1037    #[must_use]
1038    pub fn order_iv(&self) -> Option<&str> {
1039        self.order_iv.as_deref()
1040    }
1041
1042    #[getter]
1043    #[must_use]
1044    pub fn trigger_price(&self) -> &str {
1045        &self.trigger_price
1046    }
1047
1048    #[getter]
1049    #[must_use]
1050    pub fn take_profit(&self) -> &str {
1051        &self.take_profit
1052    }
1053
1054    #[getter]
1055    #[must_use]
1056    pub fn stop_loss(&self) -> &str {
1057        &self.stop_loss
1058    }
1059
1060    #[getter]
1061    #[must_use]
1062    pub fn tp_trigger_by(&self) -> BybitTriggerType {
1063        self.tp_trigger_by
1064    }
1065
1066    #[getter]
1067    #[must_use]
1068    pub fn sl_trigger_by(&self) -> BybitTriggerType {
1069        self.sl_trigger_by
1070    }
1071
1072    #[getter]
1073    #[must_use]
1074    pub fn trigger_direction(&self) -> BybitTriggerDirection {
1075        self.trigger_direction
1076    }
1077
1078    #[getter]
1079    #[must_use]
1080    pub fn trigger_by(&self) -> BybitTriggerType {
1081        self.trigger_by
1082    }
1083
1084    #[getter]
1085    #[must_use]
1086    pub fn last_price_on_created(&self) -> &str {
1087        &self.last_price_on_created
1088    }
1089
1090    #[getter]
1091    #[must_use]
1092    pub fn reduce_only(&self) -> bool {
1093        self.reduce_only
1094    }
1095
1096    #[getter]
1097    #[must_use]
1098    pub fn close_on_trigger(&self) -> bool {
1099        self.close_on_trigger
1100    }
1101
1102    #[getter]
1103    #[must_use]
1104    #[expect(
1105        clippy::missing_panics_doc,
1106        reason = "serialization of a simple enum cannot fail"
1107    )]
1108    pub fn smp_type(&self) -> String {
1109        serde_json::to_string(&self.smp_type)
1110            .expect("Failed to serialize BybitSmpType")
1111            .trim_matches('"')
1112            .to_string()
1113    }
1114
1115    #[getter]
1116    #[must_use]
1117    pub fn smp_group(&self) -> i32 {
1118        self.smp_group
1119    }
1120
1121    #[getter]
1122    #[must_use]
1123    pub fn smp_order_id(&self) -> &str {
1124        self.smp_order_id.as_str()
1125    }
1126
1127    #[getter]
1128    #[must_use]
1129    pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1130        self.tpsl_mode
1131    }
1132
1133    #[getter]
1134    #[must_use]
1135    pub fn tp_limit_price(&self) -> &str {
1136        &self.tp_limit_price
1137    }
1138
1139    #[getter]
1140    #[must_use]
1141    pub fn sl_limit_price(&self) -> &str {
1142        &self.sl_limit_price
1143    }
1144
1145    #[getter]
1146    #[must_use]
1147    pub fn place_type(&self) -> &str {
1148        self.place_type.as_str()
1149    }
1150
1151    #[getter]
1152    #[must_use]
1153    pub fn created_time(&self) -> &str {
1154        &self.created_time
1155    }
1156
1157    #[getter]
1158    #[must_use]
1159    pub fn updated_time(&self) -> &str {
1160        &self.updated_time
1161    }
1162}
1163
1164/// Response alias for open order queries.
1165///
1166/// # References
1167/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1168pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1169/// Response alias for order history queries with pagination.
1170///
1171/// # References
1172/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1173pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1174
1175/// Payload returned after placing a single order.
1176///
1177/// # References
1178/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1179#[derive(Clone, Debug, Serialize, Deserialize)]
1180#[serde(rename_all = "camelCase")]
1181pub struct BybitPlaceOrderResult {
1182    pub order_id: Option<Ustr>,
1183    pub order_link_id: Option<Ustr>,
1184}
1185
1186/// Response alias for order placement endpoints.
1187///
1188/// # References
1189/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1190pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1191
1192/// Payload returned after cancelling a single order.
1193///
1194/// # References
1195/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1196#[derive(Clone, Debug, Serialize, Deserialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct BybitCancelOrderResult {
1199    pub order_id: Option<Ustr>,
1200    pub order_link_id: Option<Ustr>,
1201}
1202
1203/// Response alias for order cancellation endpoints.
1204///
1205/// # References
1206/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1207pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1208
1209/// Execution/Fill payload returned by `GET /v5/execution/list`.
1210///
1211/// # References
1212/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1213#[derive(Clone, Debug, Serialize, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub struct BybitExecution {
1216    pub symbol: Ustr,
1217    pub order_id: Ustr,
1218    pub order_link_id: Ustr,
1219    pub side: BybitOrderSide,
1220    pub order_price: String,
1221    pub order_qty: String,
1222    pub leaves_qty: String,
1223    pub create_type: Option<BybitCreateType>,
1224    pub order_type: BybitOrderType,
1225    pub stop_order_type: Option<BybitStopOrderType>,
1226    pub exec_fee: String,
1227    pub exec_id: String,
1228    pub exec_price: String,
1229    pub exec_qty: String,
1230    pub exec_type: BybitExecType,
1231    pub exec_value: String,
1232    pub exec_time: String,
1233    pub fee_currency: Ustr,
1234    pub is_maker: bool,
1235    pub fee_rate: String,
1236    pub trade_iv: String,
1237    pub mark_iv: String,
1238    pub mark_price: String,
1239    pub index_price: String,
1240    pub underlying_price: String,
1241    pub block_trade_id: String,
1242    pub closed_size: String,
1243    pub seq: i64,
1244}
1245
1246/// Response alias for trade history requests.
1247///
1248/// # References
1249/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1250pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1251
1252/// Represents a position returned by the Bybit API.
1253///
1254/// # References
1255/// - <https://bybit-exchange.github.io/docs/v5/position>
1256#[derive(Clone, Debug, Serialize, Deserialize)]
1257#[serde(rename_all = "camelCase")]
1258pub struct BybitPosition {
1259    pub position_idx: BybitPositionIdx,
1260    pub risk_id: i32,
1261    pub risk_limit_value: String,
1262    pub symbol: Ustr,
1263    pub side: BybitPositionSide,
1264    pub size: String,
1265    pub avg_price: String,
1266    pub position_value: String,
1267    pub trade_mode: i32,
1268    pub position_status: BybitPositionStatus,
1269    pub auto_add_margin: i32,
1270    pub adl_rank_indicator: i32,
1271    pub leverage: String,
1272    pub position_balance: String,
1273    pub mark_price: String,
1274    pub liq_price: String,
1275    pub bust_price: String,
1276    #[serde(rename = "positionMM")]
1277    pub position_mm: String,
1278    #[serde(rename = "positionIM")]
1279    pub position_im: String,
1280    pub tpsl_mode: BybitTpSlMode,
1281    pub take_profit: String,
1282    pub stop_loss: String,
1283    pub trailing_stop: String,
1284    pub unrealised_pnl: String,
1285    pub cur_realised_pnl: String,
1286    pub cum_realised_pnl: String,
1287    #[serde(default = "default_position_seq")]
1288    pub seq: i64,
1289    #[serde(default)]
1290    pub is_reduce_only: bool,
1291    #[serde(default)]
1292    pub mmr_sys_updated_time: String,
1293    #[serde(default)]
1294    pub leverage_sys_updated_time: String,
1295    pub created_time: String,
1296    pub updated_time: String,
1297}
1298
1299const fn default_position_seq() -> i64 {
1300    -1
1301}
1302
1303/// Response alias for position list requests.
1304///
1305/// # References
1306/// - <https://bybit-exchange.github.io/docs/v5/position>
1307pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1308
1309/// Reason detail for set margin mode failures.
1310///
1311/// # References
1312/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1313#[derive(Clone, Debug, Serialize, Deserialize)]
1314#[serde(rename_all = "camelCase")]
1315pub struct BybitSetMarginModeReason {
1316    pub reason_code: String,
1317    pub reason_msg: String,
1318}
1319
1320/// Result payload for set margin mode operation.
1321///
1322/// # References
1323/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1324#[derive(Clone, Debug, Serialize, Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326pub struct BybitSetMarginModeResult {
1327    #[serde(default)]
1328    pub reasons: Vec<BybitSetMarginModeReason>,
1329}
1330
1331/// Response alias for set margin mode requests.
1332///
1333/// # References
1334/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1335pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1336
1337/// Empty result for set leverage operation.
1338#[derive(Clone, Debug, Serialize, Deserialize)]
1339pub struct BybitSetLeverageResult {}
1340
1341/// Response alias for set leverage requests.
1342///
1343/// # References
1344/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1345pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1346
1347/// Empty result for switch mode operation.
1348#[derive(Clone, Debug, Serialize, Deserialize)]
1349pub struct BybitSwitchModeResult {}
1350
1351/// Response alias for switch mode requests.
1352///
1353/// # References
1354/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1355pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1356
1357/// Empty result for set trading stop operation.
1358#[derive(Clone, Debug, Serialize, Deserialize)]
1359pub struct BybitSetTradingStopResult {}
1360
1361/// Response alias for set trading stop requests.
1362///
1363/// # References
1364/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1365pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1366
1367/// Result from manual borrow operation.
1368#[derive(Clone, Debug, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct BybitBorrowResult {
1371    pub coin: Ustr,
1372    pub amount: String,
1373}
1374
1375/// Response alias for manual borrow requests.
1376///
1377/// # References
1378///
1379/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1380pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1381
1382/// Result from no-convert repay operation.
1383#[derive(Clone, Debug, Serialize, Deserialize)]
1384#[serde(rename_all = "camelCase")]
1385pub struct BybitNoConvertRepayResult {
1386    pub result_status: String,
1387}
1388
1389/// Response alias for no-convert repay requests.
1390///
1391/// # References
1392///
1393/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1394pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1395
1396/// API key permissions.
1397#[derive(Clone, Debug, Serialize, Deserialize)]
1398#[cfg_attr(
1399    feature = "python",
1400    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1401)]
1402#[cfg_attr(
1403    feature = "python",
1404    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1405)]
1406#[serde(rename_all = "PascalCase")]
1407pub struct BybitApiKeyPermissions {
1408    #[serde(default)]
1409    pub contract_trade: Vec<String>,
1410    #[serde(default)]
1411    pub spot: Vec<String>,
1412    #[serde(default)]
1413    pub wallet: Vec<String>,
1414    #[serde(default)]
1415    pub options: Vec<String>,
1416    #[serde(default)]
1417    pub derivatives: Vec<String>,
1418    #[serde(default)]
1419    pub exchange: Vec<String>,
1420    #[serde(default)]
1421    pub copy_trading: Vec<String>,
1422    #[serde(default)]
1423    pub block_trade: Vec<String>,
1424    // Bybit ships this key uppercase (`"NFT"`); the struct-level PascalCase
1425    // rule would otherwise serialize it as `"Nft"` and silently drop values.
1426    #[serde(rename = "NFT", default)]
1427    pub nft: Vec<String>,
1428    #[serde(default)]
1429    pub affiliate: Vec<String>,
1430    // Newer permission buckets. Master-account responses populate them, sub-key
1431    // responses typically omit or return empty arrays — both cases deserialize
1432    // to an empty `Vec` via `serde(default)`.
1433    #[serde(default)]
1434    pub earn: Vec<String>,
1435    // Bybit uses `"FiatP2P"` — PascalCase rename would emit `"FiatP2p"`.
1436    #[serde(rename = "FiatP2P", default)]
1437    pub fiat_p2p: Vec<String>,
1438    #[serde(default)]
1439    pub fiat_bybit_pay: Vec<String>,
1440    #[serde(default)]
1441    pub fiat_bit_pay: Vec<String>,
1442    #[serde(default)]
1443    pub fiat_global_pay: Vec<String>,
1444    #[serde(default)]
1445    pub fiat_convert_broker: Vec<String>,
1446    #[serde(default)]
1447    pub bit_card: Vec<String>,
1448    // Bybit uses `"ByXPost"` — PascalCase rename would emit `"ByxPost"`.
1449    #[serde(rename = "ByXPost", default)]
1450    pub byx_post: Vec<String>,
1451}
1452
1453/// Account details from API key info.
1454#[derive(Clone, Debug, Serialize, Deserialize)]
1455#[cfg_attr(
1456    feature = "python",
1457    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1458)]
1459#[cfg_attr(
1460    feature = "python",
1461    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1462)]
1463#[serde(rename_all = "camelCase")]
1464pub struct BybitAccountDetails {
1465    pub id: String,
1466    pub note: String,
1467    pub api_key: String,
1468    pub read_only: u8,
1469    pub secret: String,
1470    #[serde(rename = "type")]
1471    pub key_type: u8,
1472    pub permissions: BybitApiKeyPermissions,
1473    pub ips: Vec<String>,
1474    #[serde(default)]
1475    pub user_id: Option<u64>,
1476    #[serde(default)]
1477    pub inviter_id: Option<u64>,
1478    pub vip_level: String,
1479    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1480    pub mkt_maker_level: u8,
1481    #[serde(default)]
1482    pub affiliate_id: Option<u64>,
1483    pub rsa_public_key: String,
1484    pub is_master: bool,
1485    pub parent_uid: String,
1486    pub uta: u8,
1487    pub kyc_level: String,
1488    pub kyc_region: String,
1489    #[serde(default)]
1490    pub unified: Option<i32>,
1491    #[serde(default)]
1492    pub deadline_day: i64,
1493    #[serde(default)]
1494    pub expired_at: Option<String>,
1495    pub created_at: String,
1496}
1497
1498#[cfg(feature = "python")]
1499#[pyo3::pymethods]
1500impl BybitAccountDetails {
1501    #[getter]
1502    #[must_use]
1503    pub fn id(&self) -> &str {
1504        &self.id
1505    }
1506
1507    #[getter]
1508    #[must_use]
1509    pub fn note(&self) -> &str {
1510        &self.note
1511    }
1512
1513    #[getter]
1514    #[must_use]
1515    pub fn api_key(&self) -> &str {
1516        &self.api_key
1517    }
1518
1519    #[getter]
1520    #[must_use]
1521    pub fn read_only(&self) -> u8 {
1522        self.read_only
1523    }
1524
1525    #[getter]
1526    #[must_use]
1527    pub fn key_type(&self) -> u8 {
1528        self.key_type
1529    }
1530
1531    #[getter]
1532    #[must_use]
1533    pub fn user_id(&self) -> Option<u64> {
1534        self.user_id
1535    }
1536
1537    #[getter]
1538    #[must_use]
1539    pub fn inviter_id(&self) -> Option<u64> {
1540        self.inviter_id
1541    }
1542
1543    #[getter]
1544    #[must_use]
1545    pub fn vip_level(&self) -> &str {
1546        &self.vip_level
1547    }
1548
1549    #[getter]
1550    #[must_use]
1551    pub fn mkt_maker_level(&self) -> u8 {
1552        self.mkt_maker_level
1553    }
1554
1555    #[getter]
1556    #[must_use]
1557    pub fn affiliate_id(&self) -> Option<u64> {
1558        self.affiliate_id
1559    }
1560
1561    #[getter]
1562    #[must_use]
1563    pub fn rsa_public_key(&self) -> &str {
1564        &self.rsa_public_key
1565    }
1566
1567    #[getter]
1568    #[must_use]
1569    pub fn is_master(&self) -> bool {
1570        self.is_master
1571    }
1572
1573    #[getter]
1574    #[must_use]
1575    pub fn parent_uid(&self) -> &str {
1576        &self.parent_uid
1577    }
1578
1579    #[getter]
1580    #[must_use]
1581    pub fn uta(&self) -> u8 {
1582        self.uta
1583    }
1584
1585    #[getter]
1586    #[must_use]
1587    pub fn kyc_level(&self) -> &str {
1588        &self.kyc_level
1589    }
1590
1591    #[getter]
1592    #[must_use]
1593    pub fn kyc_region(&self) -> &str {
1594        &self.kyc_region
1595    }
1596
1597    #[getter]
1598    #[must_use]
1599    pub fn deadline_day(&self) -> i64 {
1600        self.deadline_day
1601    }
1602
1603    #[getter]
1604    #[must_use]
1605    pub fn expired_at(&self) -> Option<&str> {
1606        self.expired_at.as_deref()
1607    }
1608
1609    #[getter]
1610    #[must_use]
1611    pub fn created_at(&self) -> &str {
1612        &self.created_at
1613    }
1614}
1615
1616/// Response alias for API key info requests.
1617///
1618/// # References
1619///
1620/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1621pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1622
1623/// Basic information about a sub-account member.
1624///
1625/// `member_type`, `status`, and `account_mode` use raw integer codes whose valid
1626/// ranges differ per endpoint; values are kept as-is rather than mapped to Rust
1627/// enums, consistent with other venue-raw fields in this module.
1628///
1629/// # References
1630///
1631/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1632/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1633/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1634#[derive(Clone, Debug, Serialize, Deserialize)]
1635#[serde(rename_all = "camelCase")]
1636pub struct BybitSubMember {
1637    pub uid: String,
1638    pub username: String,
1639    pub member_type: i32,
1640    pub status: i32,
1641    pub account_mode: i32,
1642    #[serde(default)]
1643    pub remark: String,
1644}
1645
1646/// Result payload for `GET /v5/user/query-sub-members`.
1647#[derive(Clone, Debug, Serialize, Deserialize)]
1648#[serde(rename_all = "camelCase")]
1649pub struct BybitSubMembersResult {
1650    #[serde(default)]
1651    pub sub_members: Vec<BybitSubMember>,
1652}
1653
1654/// Response alias for the non-paginated sub-UID list.
1655///
1656/// # References
1657///
1658/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1659pub type BybitSubMembersResponse = BybitResponse<BybitSubMembersResult>;
1660
1661/// Result payload for cursor-paginated sub-account listings.
1662///
1663/// The inner array is named `subMembers` and the cursor field is `nextCursor`
1664/// (with `"0"` as the end-of-pages sentinel), so the standard
1665/// `BybitCursorListResponse<T>` (which expects `list` / `nextPageCursor`)
1666/// cannot be reused here. Callers treat `"0"` or an empty string as the
1667/// termination sentinel.
1668#[derive(Clone, Debug, Serialize, Deserialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct BybitSubMembersPagedResult {
1671    #[serde(default)]
1672    pub sub_members: Vec<BybitSubMember>,
1673    #[serde(default)]
1674    pub next_cursor: Option<String>,
1675}
1676
1677impl BybitSubMembersPagedResult {
1678    /// Returns the cursor to use for the next page, or `None` when the final
1679    /// page has been fetched.
1680    ///
1681    /// Bybit signals end-of-pages either by omitting the cursor or returning
1682    /// `"0"`/`""`; both cases collapse to `None` here so callers can treat any
1683    /// non-`None` return value as a live cursor.
1684    #[must_use]
1685    pub fn continuation_cursor(&self) -> Option<&str> {
1686        match self.next_cursor.as_deref() {
1687            None | Some("" | "0") => None,
1688            Some(cursor) => Some(cursor),
1689        }
1690    }
1691
1692    /// Returns `true` when the result has more pages to fetch.
1693    #[must_use]
1694    pub fn has_more_pages(&self) -> bool {
1695        self.continuation_cursor().is_some()
1696    }
1697}
1698
1699/// Response alias for paginated sub-UID list (`/v5/user/submembers`).
1700///
1701/// # References
1702///
1703/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1704pub type BybitSubMembersPagedResponse = BybitResponse<BybitSubMembersPagedResult>;
1705
1706/// Response alias for the escrow (fund-custodial) sub-account list
1707/// (`/v5/user/escrow_sub_members`); shares the paginated sub-member shape.
1708///
1709/// # References
1710///
1711/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1712pub type BybitEscrowSubMembersResponse = BybitResponse<BybitSubMembersPagedResult>;
1713
1714/// Information about a single sub-account API key.
1715///
1716/// Deliberately not shared with [`BybitAccountDetails`]: master-level fields
1717/// such as `is_master`, `parent_uid`, `uta`, and the KYC block are absent.
1718///
1719/// # References
1720///
1721/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1722#[derive(Clone, Debug, Serialize, Deserialize)]
1723#[serde(rename_all = "camelCase")]
1724pub struct BybitSubApiKeyInfo {
1725    pub id: String,
1726    #[serde(default)]
1727    pub ips: Vec<String>,
1728    pub api_key: String,
1729    #[serde(default)]
1730    pub note: String,
1731    pub status: i32,
1732    #[serde(default)]
1733    pub expired_at: Option<String>,
1734    pub created_at: String,
1735    #[serde(rename = "type")]
1736    pub key_type: BybitApiKeyType,
1737    #[serde(with = "masked_secret")]
1738    pub secret: Option<String>,
1739    #[serde(with = "bool_or_int")]
1740    pub read_only: bool,
1741    #[serde(default)]
1742    pub deadline_day: Option<i64>,
1743    #[serde(default)]
1744    pub flag: String,
1745    pub permissions: BybitApiKeyPermissions,
1746}
1747
1748/// Result payload for `GET /v5/user/sub-apikeys`.
1749///
1750/// The inner array field is named `result` (nested inside the outer
1751/// `retCode/retMsg/result` envelope) rather than the usual `list`, so the
1752/// standard `BybitCursorListResponse<T>` cannot be reused here.
1753#[derive(Clone, Debug, Serialize, Deserialize)]
1754#[serde(rename_all = "camelCase")]
1755pub struct BybitSubApiKeysResult {
1756    #[serde(rename = "result", default)]
1757    pub keys: Vec<BybitSubApiKeyInfo>,
1758    #[serde(default)]
1759    pub next_page_cursor: Option<String>,
1760}
1761
1762impl BybitSubApiKeysResult {
1763    /// Returns the cursor to use for the next page, or `None` when the final
1764    /// page has been fetched.
1765    ///
1766    /// The end-of-pages sentinel on this endpoint is an empty string rather
1767    /// than `"0"`; both that and a missing cursor collapse to `None`.
1768    #[must_use]
1769    pub fn continuation_cursor(&self) -> Option<&str> {
1770        match self.next_page_cursor.as_deref() {
1771            None | Some("") => None,
1772            Some(cursor) => Some(cursor),
1773        }
1774    }
1775
1776    /// Returns `true` when the result has more pages to fetch.
1777    #[must_use]
1778    pub fn has_more_pages(&self) -> bool {
1779        self.continuation_cursor().is_some()
1780    }
1781}
1782
1783/// Response alias for sub-account API keys list.
1784///
1785/// # References
1786///
1787/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1788pub type BybitSubApiKeysResponse = BybitResponse<BybitSubApiKeysResult>;
1789
1790/// Shared result payload for API-key update endpoints (sub or master).
1791///
1792/// `/v5/user/update-sub-api` and `/v5/user/update-api` return the same field
1793/// set; only the number of permission buckets populated inside `permissions`
1794/// differs. Because [`BybitApiKeyPermissions`] covers the superset of both,
1795/// the two endpoints reuse a single DTO.
1796#[derive(Clone, Debug, Serialize, Deserialize)]
1797#[serde(rename_all = "camelCase")]
1798pub struct BybitApiKeyUpdateResult {
1799    pub id: String,
1800    #[serde(default)]
1801    pub note: String,
1802    pub api_key: String,
1803    #[serde(with = "bool_or_int")]
1804    pub read_only: bool,
1805    #[serde(with = "masked_secret")]
1806    pub secret: Option<String>,
1807    pub permissions: BybitApiKeyPermissions,
1808    #[serde(default)]
1809    pub ips: Vec<String>,
1810}
1811
1812/// Response alias for `POST /v5/user/update-sub-api`.
1813///
1814/// # References
1815///
1816/// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
1817pub type BybitUpdateSubApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1818
1819/// Response alias for `POST /v5/user/update-api`.
1820///
1821/// # References
1822///
1823/// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
1824pub type BybitUpdateMasterApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1825
1826#[cfg(test)]
1827mod tests {
1828    use nautilus_core::UnixNanos;
1829    use nautilus_model::identifiers::AccountId;
1830    use rstest::rstest;
1831    use rust_decimal::Decimal;
1832    use rust_decimal_macros::dec;
1833
1834    use super::*;
1835    use crate::common::testing::load_test_json;
1836
1837    #[rstest]
1838    fn deserialize_spot_instrument_uses_enums() {
1839        let json = load_test_json("http_get_instruments_spot.json");
1840        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1841        let instrument = &response.result.list[0];
1842
1843        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1844        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1845        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1846    }
1847
1848    #[rstest]
1849    fn deserialize_linear_instrument_status() {
1850        let json = load_test_json("http_get_instruments_linear.json");
1851        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1852        let instrument = &response.result.list[0];
1853
1854        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1855        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1856    }
1857
1858    #[rstest]
1859    fn deserialize_account_info_response() {
1860        let json = load_test_json("http_get_account_info.json");
1861        let response: BybitAccountInfoResponse = serde_json::from_str(&json).unwrap();
1862
1863        assert_eq!(response.result.margin_mode, BybitMarginMode::RegularMargin);
1864        assert_eq!(
1865            response.result.unified_margin_status,
1866            BybitUnifiedMarginStatus::UnifiedTradingAccount10Pro
1867        );
1868        assert!(!response.result.is_master_trader);
1869        assert!(!response.result.spot_hedging_status);
1870        assert!(!response.result.dcp_status);
1871        assert_eq!(response.result.time_window, 10);
1872        assert_eq!(response.result.smp_group, 0);
1873    }
1874
1875    #[rstest]
1876    fn deserialize_account_info_without_deprecated_fields() {
1877        let json = r#"{
1878            "retCode": 0,
1879            "retMsg": "OK",
1880            "result": {
1881                "marginMode": "PORTFOLIO_MARGIN",
1882                "updatedTime": "1697078946000",
1883                "unifiedMarginStatus": 5,
1884                "isMasterTrader": true,
1885                "spotHedgingStatus": "ON"
1886            }
1887        }"#;
1888        let response: BybitAccountInfoResponse = serde_json::from_str(json).unwrap();
1889
1890        assert_eq!(
1891            response.result.margin_mode,
1892            BybitMarginMode::PortfolioMargin
1893        );
1894        assert_eq!(
1895            response.result.unified_margin_status,
1896            BybitUnifiedMarginStatus::UnifiedTradingAccount20
1897        );
1898        assert!(response.result.is_master_trader);
1899        assert!(response.result.spot_hedging_status);
1900        assert!(!response.result.dcp_status);
1901        assert_eq!(response.result.time_window, 0);
1902        assert_eq!(response.result.smp_group, 0);
1903    }
1904
1905    #[rstest]
1906    fn deserialize_order_response_maps_enums() {
1907        let json = load_test_json("http_get_orders_history.json");
1908        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1909        let order = &response.result.list[0];
1910
1911        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1912        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1913        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1914        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1915        assert_eq!(order.order_type, BybitOrderType::Limit);
1916        assert_eq!(order.smp_type, BybitSmpType::None);
1917    }
1918
1919    #[rstest]
1920    fn deserialize_wallet_balance_without_optional_fields() {
1921        let json = r#"{
1922            "retCode": 0,
1923            "retMsg": "OK",
1924            "result": {
1925                "list": [{
1926                    "totalEquity": "1000.00",
1927                    "accountIMRate": "0",
1928                    "totalMarginBalance": "1000.00",
1929                    "totalInitialMargin": "0",
1930                    "accountType": "UNIFIED",
1931                    "totalAvailableBalance": "1000.00",
1932                    "accountMMRate": "0",
1933                    "totalPerpUPL": "0",
1934                    "totalWalletBalance": "1000.00",
1935                    "accountLTV": "0",
1936                    "totalMaintenanceMargin": "0",
1937                    "coin": [{
1938                        "availableToBorrow": "0",
1939                        "bonus": "0",
1940                        "accruedInterest": "0",
1941                        "availableToWithdraw": "1000.00",
1942                        "equity": "1000.00",
1943                        "usdValue": "1000.00",
1944                        "borrowAmount": "0",
1945                        "totalPositionIM": "0",
1946                        "walletBalance": "1000.00",
1947                        "unrealisedPnl": "0",
1948                        "cumRealisedPnl": "0",
1949                        "locked": "0",
1950                        "collateralSwitch": true,
1951                        "marginCollateral": true,
1952                        "coin": "USDT"
1953                    }]
1954                }]
1955            }
1956        }"#;
1957
1958        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1959            .expect("Failed to parse wallet balance without optional fields");
1960
1961        assert_eq!(response.ret_code, 0);
1962        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
1963        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
1964    }
1965
1966    #[rstest]
1967    fn deserialize_wallet_balance_from_docs() {
1968        let json = include_str!("../../test_data/http_get_wallet_balance.json");
1969
1970        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1971            .expect("Failed to parse wallet balance from Bybit docs example");
1972
1973        assert_eq!(response.ret_code, 0);
1974        assert_eq!(response.ret_msg, "OK");
1975
1976        let wallet = &response.result.list[0];
1977        assert_eq!(wallet.total_equity, "3.31216591");
1978        assert_eq!(wallet.account_im_rate, "0");
1979        assert_eq!(wallet.account_mm_rate, "0");
1980        assert_eq!(wallet.total_perp_upl, "0");
1981        assert_eq!(wallet.account_ltv, "0");
1982
1983        // Check BTC coin
1984        let btc = &wallet.coin[0];
1985        assert_eq!(btc.coin.as_str(), "BTC");
1986        assert_eq!(btc.available_to_borrow, "3");
1987        assert_eq!(btc.total_order_im, Some("0".to_string()));
1988        assert_eq!(btc.total_position_mm, Some("0".to_string()));
1989        assert_eq!(btc.total_position_im, Some("0".to_string()));
1990
1991        // Check USDT coin (without optional IM/MM fields)
1992        let usdt = &wallet.coin[1];
1993        assert_eq!(usdt.coin.as_str(), "USDT");
1994        assert_eq!(usdt.wallet_balance, dec!(1000.50));
1995        assert_eq!(usdt.total_order_im, None);
1996        assert_eq!(usdt.total_position_mm, None);
1997        assert_eq!(usdt.total_position_im, None);
1998        assert_eq!(btc.spot_borrow, Decimal::ZERO);
1999        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
2000    }
2001
2002    #[rstest]
2003    fn test_parse_wallet_balance_with_spot_borrow() {
2004        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
2005        let response: BybitWalletBalanceResponse =
2006            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
2007
2008        let wallet = &response.result.list[0];
2009        let usdt = &wallet.coin[0];
2010
2011        assert_eq!(usdt.coin.as_str(), "USDT");
2012        assert_eq!(usdt.wallet_balance, dec!(1200.00));
2013        assert_eq!(usdt.spot_borrow, dec!(200.00));
2014        assert_eq!(usdt.borrow_amount, "200.00");
2015
2016        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
2017        let account_id = crate::common::parse::parse_account_state(
2018            wallet,
2019            AccountId::new("BYBIT-001"),
2020            UnixNanos::default(),
2021        )
2022        .expect("Failed to parse account state");
2023
2024        let balance = &account_id.balances[0];
2025        assert_eq!(balance.total.as_f64(), 1000.0);
2026    }
2027
2028    #[rstest]
2029    fn test_parse_wallet_balance_spot_short() {
2030        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
2031        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2032            .expect("Failed to parse wallet balance with SHORT SPOT position");
2033
2034        let wallet = &response.result.list[0];
2035        let eth = &wallet.coin[0];
2036
2037        assert_eq!(eth.coin.as_str(), "ETH");
2038        assert_eq!(eth.wallet_balance, dec!(0));
2039        assert_eq!(eth.spot_borrow, dec!(0.06142));
2040        assert_eq!(eth.borrow_amount, "0.06142");
2041
2042        let account_state = crate::common::parse::parse_account_state(
2043            wallet,
2044            AccountId::new("BYBIT-001"),
2045            UnixNanos::default(),
2046        )
2047        .expect("Failed to parse account state");
2048
2049        let eth_balance = account_state
2050            .balances
2051            .iter()
2052            .find(|b| b.currency.code.as_str() == "ETH")
2053            .expect("ETH balance not found");
2054
2055        // Negative balance represents SHORT position (borrowed ETH)
2056        assert_eq!(eth_balance.total.as_f64(), -0.06142);
2057    }
2058
2059    #[rstest]
2060    fn deserialize_borrow_response() {
2061        let json = r#"{
2062            "retCode": 0,
2063            "retMsg": "success",
2064            "result": {
2065                "coin": "BTC",
2066                "amount": "0.01"
2067            },
2068            "retExtInfo": {},
2069            "time": 1756197991955
2070        }"#;
2071
2072        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
2073
2074        assert_eq!(response.ret_code, 0);
2075        assert_eq!(response.ret_msg, "success");
2076        assert_eq!(response.result.coin, "BTC");
2077        assert_eq!(response.result.amount, "0.01");
2078    }
2079
2080    #[rstest]
2081    fn deserialize_no_convert_repay_response() {
2082        let json = r#"{
2083            "retCode": 0,
2084            "retMsg": "OK",
2085            "result": {
2086                "resultStatus": "SU"
2087            },
2088            "retExtInfo": {},
2089            "time": 1234567890
2090        }"#;
2091
2092        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
2093
2094        assert_eq!(response.ret_code, 0);
2095        assert_eq!(response.ret_msg, "OK");
2096        assert_eq!(response.result.result_status, "SU");
2097    }
2098
2099    #[rstest]
2100    fn deserialize_position_without_conditional_fields() {
2101        // Bybit v5 docs mark `isReduceOnly`, `mmrSysUpdatedTime`, `leverageSysUpdatedTime`
2102        // and `seq` as conditional fields that may be absent, e.g. once a position has been
2103        // closed through the UI (see issue #3836).
2104        let json = r#"{
2105            "retCode": 0,
2106            "retMsg": "OK",
2107            "result": {
2108                "list": [{
2109                    "positionIdx": 0,
2110                    "riskId": 1,
2111                    "riskLimitValue": "150",
2112                    "symbol": "LTCUSDT",
2113                    "side": "",
2114                    "size": "0",
2115                    "avgPrice": "0",
2116                    "positionValue": "0",
2117                    "tradeMode": 0,
2118                    "positionStatus": "Normal",
2119                    "autoAddMargin": 0,
2120                    "adlRankIndicator": 0,
2121                    "leverage": "10",
2122                    "positionBalance": "0",
2123                    "markPrice": "70.00",
2124                    "liqPrice": "",
2125                    "bustPrice": "",
2126                    "positionMM": "0",
2127                    "positionIM": "0",
2128                    "tpslMode": "Full",
2129                    "takeProfit": "0",
2130                    "stopLoss": "0",
2131                    "trailingStop": "0",
2132                    "unrealisedPnl": "0",
2133                    "curRealisedPnl": "0",
2134                    "cumRealisedPnl": "0",
2135                    "createdTime": "1676538056258",
2136                    "updatedTime": "1697673600012"
2137                }],
2138                "nextPageCursor": "",
2139                "category": "linear"
2140            },
2141            "retExtInfo": {},
2142            "time": 1697673900000
2143        }"#;
2144
2145        let response: BybitPositionListResponse = serde_json::from_str(json)
2146            .expect("Failed to parse position list with missing conditional fields");
2147
2148        let position = &response.result.list[0];
2149        assert!(!position.is_reduce_only);
2150        assert_eq!(position.seq, -1);
2151        assert_eq!(position.mmr_sys_updated_time, "");
2152        assert_eq!(position.leverage_sys_updated_time, "");
2153    }
2154
2155    #[rstest]
2156    fn deserialize_sub_members_response() {
2157        let json = load_test_json("http_get_user_sub_members.json");
2158        let response: BybitSubMembersResponse =
2159            serde_json::from_str(&json).expect("parse sub members");
2160        assert_eq!(response.ret_code, 0);
2161        assert_eq!(response.result.sub_members.len(), 2);
2162        let first = &response.result.sub_members[0];
2163        assert_eq!(first.uid, "106314365");
2164        assert_eq!(first.username, "xxxx02");
2165        assert_eq!(first.member_type, 1);
2166        assert_eq!(first.status, 1);
2167        assert_eq!(first.account_mode, 5);
2168        assert_eq!(first.remark, "");
2169        let second = &response.result.sub_members[1];
2170        assert_eq!(second.uid, "106279879");
2171        assert_eq!(second.account_mode, 6);
2172    }
2173
2174    #[rstest]
2175    fn deserialize_sub_members_paged_response() {
2176        // The final-page sentinel is `"0"`; both `"0"` and `None` collapse to
2177        // `continuation_cursor() == None` via the helper.
2178        let json = load_test_json("http_get_user_sub_members_paged.json");
2179        let response: BybitSubMembersPagedResponse =
2180            serde_json::from_str(&json).expect("parse paged sub members");
2181        assert_eq!(response.result.sub_members.len(), 2);
2182        assert_eq!(response.result.next_cursor.as_deref(), Some("0"));
2183        assert!(!response.result.has_more_pages());
2184        assert_eq!(response.result.continuation_cursor(), None);
2185    }
2186
2187    #[rstest]
2188    fn deserialize_escrow_sub_members_response_uses_same_shape() {
2189        // The escrow alias must decode into the same shape as the paginated
2190        // sub-member list; a non-`"0"` cursor indicates more pages to fetch.
2191        let json = load_test_json("http_get_user_escrow_sub_members.json");
2192        let response: BybitEscrowSubMembersResponse =
2193            serde_json::from_str(&json).expect("parse escrow sub members");
2194        assert_eq!(response.result.sub_members.len(), 2);
2195        assert_eq!(response.result.sub_members[0].member_type, 12);
2196        assert_eq!(response.result.sub_members[0].remark, "earn fund");
2197        assert_eq!(response.result.next_cursor.as_deref(), Some("344"));
2198        assert!(response.result.has_more_pages());
2199        assert_eq!(response.result.continuation_cursor(), Some("344"));
2200    }
2201
2202    #[rstest]
2203    fn deserialize_sub_api_keys_response() {
2204        // `readOnly` arrives as a bool here; the masked `"******"` secret
2205        // collapses to `None` through the `masked_secret` helper.
2206        let json = load_test_json("http_get_user_sub_apikeys.json");
2207        let response: BybitSubApiKeysResponse =
2208            serde_json::from_str(&json).expect("parse sub apikeys");
2209        assert_eq!(response.result.keys.len(), 1);
2210        let key = &response.result.keys[0];
2211        assert!(!key.read_only);
2212        assert_eq!(key.secret, None);
2213        assert_eq!(key.key_type, BybitApiKeyType::Hmac);
2214        assert_eq!(key.flag, "hmac");
2215        assert_eq!(key.deadline_day, Some(21));
2216        assert_eq!(key.permissions.contract_trade, vec!["Order", "Position"]);
2217        assert_eq!(key.permissions.spot, vec!["SpotTrade"]);
2218        assert!(key.permissions.earn.is_empty());
2219        assert_eq!(response.result.next_page_cursor.as_deref(), Some(""));
2220        assert!(!response.result.has_more_pages());
2221    }
2222
2223    #[rstest]
2224    fn deserialize_update_sub_api_response() {
2225        let json = load_test_json("http_post_user_update_sub_api.json");
2226        let response: BybitUpdateSubApiResponse =
2227            serde_json::from_str(&json).expect("parse update sub api");
2228        assert!(!response.result.read_only);
2229        assert_eq!(response.result.secret, None);
2230        assert_eq!(response.result.ips, vec!["*"]);
2231        assert_eq!(response.result.permissions.spot, vec!["SpotTrade"]);
2232        assert_eq!(response.result.permissions.wallet, vec!["AccountTransfer"]);
2233    }
2234
2235    #[rstest]
2236    fn deserialize_update_master_api_response() {
2237        // Asserts on non-empty permission buckets so the test actually verifies
2238        // deserialisation (an empty `Vec` would be indistinguishable from a
2239        // `#[serde(default)]` fallback). In particular, `nft` exercises the
2240        // explicit `#[serde(rename = "NFT")]` attribute.
2241        let json = load_test_json("http_post_user_update_master_api.json");
2242        let response: BybitUpdateMasterApiResponse =
2243            serde_json::from_str(&json).expect("parse update master api");
2244        assert!(!response.result.read_only);
2245        assert_eq!(response.result.ips, vec!["*"]);
2246        let perms = &response.result.permissions;
2247        assert_eq!(perms.contract_trade, vec!["Order", "Position"]);
2248        assert_eq!(perms.copy_trading, vec!["CopyTrading"]);
2249        assert!(perms.earn.is_empty());
2250        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2251    }
2252
2253    #[rstest]
2254    fn deserialize_permissions_renamed_buckets_preserve_values() {
2255        // Regression guard for `#[serde(rename = ...)]` on permission keys
2256        // whose Bybit casing (`NFT`, `FiatP2P`, `ByXPost`) differs from
2257        // serde's `PascalCase` default (`Nft`, `FiatP2p`, `ByxPost`). Using
2258        // non-empty values ensures a rename regression causes a failure
2259        // rather than silently falling through to `serde(default)`.
2260        let json = r#"{
2261            "NFT": ["NFTQueryProductList"],
2262            "FiatP2P": ["P2PDeposit"],
2263            "ByXPost": ["PostContent"]
2264        }"#;
2265        let perms: BybitApiKeyPermissions =
2266            serde_json::from_str(json).expect("parse renamed buckets");
2267        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2268        assert_eq!(perms.fiat_p2p, vec!["P2PDeposit"]);
2269        assert_eq!(perms.byx_post, vec!["PostContent"]);
2270    }
2271
2272    #[rstest]
2273    fn deserialize_account_details_response_with_current_docs_example() {
2274        let json = load_test_json("http_get_user_query_api.json");
2275        let response: BybitAccountDetailsResponse =
2276            serde_json::from_str(&json).expect("parse account details");
2277
2278        assert_eq!(
2279            response.result.permissions.fiat_global_pay,
2280            Vec::<String>::new()
2281        );
2282        assert_eq!(
2283            response.result.permissions.fiat_bit_pay,
2284            vec!["FaitPayOrder"]
2285        );
2286        assert_eq!(response.result.permissions.bit_card, vec!["BitCard"]);
2287        assert_eq!(response.result.permissions.byx_post, vec!["ByXPost"]);
2288        assert_eq!(response.result.unified, Some(0));
2289    }
2290}