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, BybitSymbolType, BybitTimeInForce, BybitTpSlMode,
29        BybitTriggerDirection, 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.adapters.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.adapters.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.adapters.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    #[serde(default)]
608    pub symbol_id: Option<i64>,
609    #[serde(default)]
610    pub symbol_type: Option<BybitSymbolType>,
611    #[serde(default)]
612    pub xstock_multiplier: Option<String>,
613}
614
615/// Instrument definition for linear contracts.
616///
617/// # References
618/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
619#[derive(Clone, Debug, Serialize, Deserialize)]
620#[serde(rename_all = "camelCase")]
621pub struct BybitInstrumentLinear {
622    pub symbol: Ustr,
623    pub contract_type: BybitContractType,
624    pub status: BybitInstrumentStatus,
625    pub base_coin: Ustr,
626    pub quote_coin: Ustr,
627    pub launch_time: String,
628    pub delivery_time: String,
629    pub delivery_fee_rate: String,
630    pub price_scale: String,
631    pub leverage_filter: LeverageFilter,
632    pub price_filter: LinearPriceFilter,
633    pub lot_size_filter: LinearLotSizeFilter,
634    pub unified_margin_trade: bool,
635    pub funding_interval: i64,
636    pub settle_coin: Ustr,
637    #[serde(default)]
638    pub symbol_id: Option<i64>,
639    #[serde(default)]
640    pub symbol_type: Option<BybitSymbolType>,
641}
642
643/// Instrument definition for inverse contracts.
644///
645/// # References
646/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
647#[derive(Clone, Debug, Serialize, Deserialize)]
648#[serde(rename_all = "camelCase")]
649pub struct BybitInstrumentInverse {
650    pub symbol: Ustr,
651    pub contract_type: BybitContractType,
652    pub status: BybitInstrumentStatus,
653    pub base_coin: Ustr,
654    pub quote_coin: Ustr,
655    pub launch_time: String,
656    pub delivery_time: String,
657    pub delivery_fee_rate: String,
658    pub price_scale: String,
659    pub leverage_filter: LeverageFilter,
660    pub price_filter: LinearPriceFilter,
661    pub lot_size_filter: LinearLotSizeFilter,
662    pub unified_margin_trade: bool,
663    pub funding_interval: i64,
664    pub settle_coin: Ustr,
665    #[serde(default)]
666    pub symbol_id: Option<i64>,
667    #[serde(default)]
668    pub symbol_type: Option<BybitSymbolType>,
669}
670
671/// Instrument definition for option contracts.
672///
673/// # References
674/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
675#[derive(Clone, Debug, Serialize, Deserialize)]
676#[serde(rename_all = "camelCase")]
677pub struct BybitInstrumentOption {
678    pub symbol: Ustr,
679    pub status: BybitInstrumentStatus,
680    pub base_coin: Ustr,
681    pub quote_coin: Ustr,
682    pub settle_coin: Ustr,
683    pub options_type: BybitOptionType,
684    pub launch_time: String,
685    pub delivery_time: String,
686    pub delivery_fee_rate: String,
687    pub price_filter: LinearPriceFilter,
688    pub lot_size_filter: OptionLotSizeFilter,
689    #[serde(default)]
690    pub symbol_id: Option<i64>,
691}
692
693/// Response alias for instrument info requests that return spot instruments.
694///
695/// # References
696/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
697pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
698/// Response alias for instrument info requests that return linear contracts.
699///
700/// # References
701/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
702pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
703/// Response alias for instrument info requests that return inverse contracts.
704///
705/// # References
706/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
707pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
708/// Response alias for instrument info requests that return option contracts.
709///
710/// # References
711/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
712pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
713
714/// Fee rate structure returned by `GET /v5/account/fee-rate`.
715///
716/// # References
717/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
718#[derive(Clone, Debug, Serialize, Deserialize)]
719#[serde(rename_all = "camelCase")]
720#[cfg_attr(
721    feature = "python",
722    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
723)]
724#[cfg_attr(
725    feature = "python",
726    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
727)]
728pub struct BybitFeeRate {
729    pub symbol: Ustr,
730    pub taker_fee_rate: String,
731    pub maker_fee_rate: String,
732    #[serde(default)]
733    pub base_coin: Option<Ustr>,
734}
735
736#[cfg(feature = "python")]
737#[pyo3::pymethods]
738impl BybitFeeRate {
739    #[getter]
740    #[must_use]
741    pub fn symbol(&self) -> &str {
742        self.symbol.as_str()
743    }
744
745    #[getter]
746    #[must_use]
747    pub fn taker_fee_rate(&self) -> &str {
748        &self.taker_fee_rate
749    }
750
751    #[getter]
752    #[must_use]
753    pub fn maker_fee_rate(&self) -> &str {
754        &self.maker_fee_rate
755    }
756
757    #[getter]
758    #[must_use]
759    pub fn base_coin(&self) -> Option<&str> {
760        self.base_coin.as_ref().map(|u| u.as_str())
761    }
762}
763
764/// Response alias for fee rate requests.
765///
766/// # References
767/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
768pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
769
770/// Account balance snapshot coin entry.
771///
772/// # References
773/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
774#[derive(Clone, Debug, Serialize, Deserialize)]
775#[serde(rename_all = "camelCase")]
776pub struct BybitCoinBalance {
777    pub available_to_borrow: String,
778    pub bonus: String,
779    pub accrued_interest: String,
780    pub available_to_withdraw: String,
781    #[serde(default, rename = "totalOrderIM")]
782    pub total_order_im: Option<String>,
783    pub equity: String,
784    pub usd_value: String,
785    pub borrow_amount: String,
786    #[serde(default, rename = "totalPositionMM")]
787    pub total_position_mm: Option<String>,
788    #[serde(default, rename = "totalPositionIM")]
789    pub total_position_im: Option<String>,
790    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
791    pub wallet_balance: Decimal,
792    pub unrealised_pnl: String,
793    pub cum_realised_pnl: String,
794    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
795    pub locked: Decimal,
796    pub collateral_switch: bool,
797    pub margin_collateral: bool,
798    pub coin: Ustr,
799    #[serde(default)]
800    pub spot_hedging_qty: Option<String>,
801    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
802    pub spot_borrow: Decimal,
803}
804
805/// Wallet balance snapshot containing per-coin balances.
806///
807/// # References
808/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
809#[derive(Clone, Debug, Serialize, Deserialize)]
810#[serde(rename_all = "camelCase")]
811pub struct BybitWalletBalance {
812    pub total_equity: String,
813    #[serde(rename = "accountIMRate")]
814    pub account_im_rate: String,
815    pub total_margin_balance: String,
816    pub total_initial_margin: String,
817    pub account_type: BybitAccountType,
818    pub total_available_balance: String,
819    #[serde(rename = "accountMMRate")]
820    pub account_mm_rate: String,
821    #[serde(rename = "totalPerpUPL")]
822    pub total_perp_upl: String,
823    pub total_wallet_balance: String,
824    #[serde(rename = "accountLTV")]
825    pub account_ltv: String,
826    pub total_maintenance_margin: String,
827    pub coin: Vec<BybitCoinBalance>,
828}
829
830/// Response alias for wallet balance requests.
831///
832/// # References
833/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
834pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
835
836/// Account-level configuration returned by `GET /v5/account/info`.
837///
838/// # References
839/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
840#[derive(Clone, Debug, Serialize, Deserialize)]
841#[serde(rename_all = "camelCase")]
842pub struct BybitAccountInfo {
843    pub unified_margin_status: BybitUnifiedMarginStatus,
844    pub margin_mode: BybitMarginMode,
845    pub is_master_trader: bool,
846    #[serde(with = "on_off_bool")]
847    pub spot_hedging_status: bool,
848    pub updated_time: String,
849    // `dcp_status`, `time_window`, and `smp_group` are absent from responses
850    // for accounts that predate the disconnection-protection feature.
851    #[serde(default, with = "on_off_bool")]
852    pub dcp_status: bool,
853    #[serde(default)]
854    pub time_window: i32,
855    #[serde(default)]
856    pub smp_group: i32,
857}
858
859/// Response alias for account info requests.
860///
861/// # References
862/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
863pub type BybitAccountInfoResponse = BybitResponse<BybitAccountInfo>;
864
865/// Order representation as returned by order-related endpoints.
866///
867/// # References
868/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
869#[derive(Clone, Debug, Serialize, Deserialize)]
870#[cfg_attr(
871    feature = "python",
872    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
873)]
874#[cfg_attr(
875    feature = "python",
876    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
877)]
878#[serde(rename_all = "camelCase")]
879pub struct BybitOrder {
880    pub order_id: Ustr,
881    pub order_link_id: Ustr,
882    pub block_trade_id: Option<Ustr>,
883    pub symbol: Ustr,
884    pub price: String,
885    pub qty: String,
886    pub side: BybitOrderSide,
887    pub is_leverage: String,
888    pub position_idx: i32,
889    pub order_status: BybitOrderStatus,
890    pub cancel_type: BybitCancelType,
891    pub reject_reason: Ustr,
892    pub avg_price: Option<String>,
893    pub leaves_qty: String,
894    pub leaves_value: String,
895    pub cum_exec_qty: String,
896    pub cum_exec_value: String,
897    pub cum_exec_fee: String,
898    pub time_in_force: BybitTimeInForce,
899    pub order_type: BybitOrderType,
900    pub stop_order_type: BybitStopOrderType,
901    pub order_iv: Option<String>,
902    pub trigger_price: String,
903    pub take_profit: String,
904    pub stop_loss: String,
905    pub tp_trigger_by: BybitTriggerType,
906    pub sl_trigger_by: BybitTriggerType,
907    pub trigger_direction: BybitTriggerDirection,
908    pub trigger_by: BybitTriggerType,
909    pub last_price_on_created: String,
910    pub reduce_only: bool,
911    pub close_on_trigger: bool,
912    pub smp_type: BybitSmpType,
913    pub smp_group: i32,
914    pub smp_order_id: Ustr,
915    pub tpsl_mode: Option<BybitTpSlMode>,
916    pub tp_limit_price: String,
917    pub sl_limit_price: String,
918    pub place_type: Ustr,
919    pub created_time: String,
920    pub updated_time: String,
921}
922
923#[cfg(feature = "python")]
924#[pyo3::pymethods]
925impl BybitOrder {
926    #[getter]
927    #[must_use]
928    pub fn order_id(&self) -> &str {
929        self.order_id.as_str()
930    }
931
932    #[getter]
933    #[must_use]
934    pub fn order_link_id(&self) -> &str {
935        self.order_link_id.as_str()
936    }
937
938    #[getter]
939    #[must_use]
940    pub fn block_trade_id(&self) -> Option<&str> {
941        self.block_trade_id.as_ref().map(|s| s.as_str())
942    }
943
944    #[getter]
945    #[must_use]
946    pub fn symbol(&self) -> &str {
947        self.symbol.as_str()
948    }
949
950    #[getter]
951    #[must_use]
952    pub fn price(&self) -> &str {
953        &self.price
954    }
955
956    #[getter]
957    #[must_use]
958    pub fn qty(&self) -> &str {
959        &self.qty
960    }
961
962    #[getter]
963    #[must_use]
964    pub fn side(&self) -> BybitOrderSide {
965        self.side
966    }
967
968    #[getter]
969    #[must_use]
970    pub fn is_leverage(&self) -> &str {
971        &self.is_leverage
972    }
973
974    #[getter]
975    #[must_use]
976    pub fn position_idx(&self) -> i32 {
977        self.position_idx
978    }
979
980    #[getter]
981    #[must_use]
982    pub fn order_status(&self) -> BybitOrderStatus {
983        self.order_status
984    }
985
986    #[getter]
987    #[must_use]
988    pub fn cancel_type(&self) -> BybitCancelType {
989        self.cancel_type
990    }
991
992    #[getter]
993    #[must_use]
994    pub fn reject_reason(&self) -> &str {
995        self.reject_reason.as_str()
996    }
997
998    #[getter]
999    #[must_use]
1000    pub fn avg_price(&self) -> Option<&str> {
1001        self.avg_price.as_deref()
1002    }
1003
1004    #[getter]
1005    #[must_use]
1006    pub fn leaves_qty(&self) -> &str {
1007        &self.leaves_qty
1008    }
1009
1010    #[getter]
1011    #[must_use]
1012    pub fn leaves_value(&self) -> &str {
1013        &self.leaves_value
1014    }
1015
1016    #[getter]
1017    #[must_use]
1018    pub fn cum_exec_qty(&self) -> &str {
1019        &self.cum_exec_qty
1020    }
1021
1022    #[getter]
1023    #[must_use]
1024    pub fn cum_exec_value(&self) -> &str {
1025        &self.cum_exec_value
1026    }
1027
1028    #[getter]
1029    #[must_use]
1030    pub fn cum_exec_fee(&self) -> &str {
1031        &self.cum_exec_fee
1032    }
1033
1034    #[getter]
1035    #[must_use]
1036    pub fn time_in_force(&self) -> BybitTimeInForce {
1037        self.time_in_force
1038    }
1039
1040    #[getter]
1041    #[must_use]
1042    pub fn order_type(&self) -> BybitOrderType {
1043        self.order_type
1044    }
1045
1046    #[getter]
1047    #[must_use]
1048    pub fn stop_order_type(&self) -> BybitStopOrderType {
1049        self.stop_order_type
1050    }
1051
1052    #[getter]
1053    #[must_use]
1054    pub fn order_iv(&self) -> Option<&str> {
1055        self.order_iv.as_deref()
1056    }
1057
1058    #[getter]
1059    #[must_use]
1060    pub fn trigger_price(&self) -> &str {
1061        &self.trigger_price
1062    }
1063
1064    #[getter]
1065    #[must_use]
1066    pub fn take_profit(&self) -> &str {
1067        &self.take_profit
1068    }
1069
1070    #[getter]
1071    #[must_use]
1072    pub fn stop_loss(&self) -> &str {
1073        &self.stop_loss
1074    }
1075
1076    #[getter]
1077    #[must_use]
1078    pub fn tp_trigger_by(&self) -> BybitTriggerType {
1079        self.tp_trigger_by
1080    }
1081
1082    #[getter]
1083    #[must_use]
1084    pub fn sl_trigger_by(&self) -> BybitTriggerType {
1085        self.sl_trigger_by
1086    }
1087
1088    #[getter]
1089    #[must_use]
1090    pub fn trigger_direction(&self) -> BybitTriggerDirection {
1091        self.trigger_direction
1092    }
1093
1094    #[getter]
1095    #[must_use]
1096    pub fn trigger_by(&self) -> BybitTriggerType {
1097        self.trigger_by
1098    }
1099
1100    #[getter]
1101    #[must_use]
1102    pub fn last_price_on_created(&self) -> &str {
1103        &self.last_price_on_created
1104    }
1105
1106    #[getter]
1107    #[must_use]
1108    pub fn reduce_only(&self) -> bool {
1109        self.reduce_only
1110    }
1111
1112    #[getter]
1113    #[must_use]
1114    pub fn close_on_trigger(&self) -> bool {
1115        self.close_on_trigger
1116    }
1117
1118    #[getter]
1119    #[must_use]
1120    #[expect(
1121        clippy::missing_panics_doc,
1122        reason = "serialization of a simple enum cannot fail"
1123    )]
1124    pub fn smp_type(&self) -> String {
1125        serde_json::to_string(&self.smp_type)
1126            .expect("Failed to serialize BybitSmpType")
1127            .trim_matches('"')
1128            .to_string()
1129    }
1130
1131    #[getter]
1132    #[must_use]
1133    pub fn smp_group(&self) -> i32 {
1134        self.smp_group
1135    }
1136
1137    #[getter]
1138    #[must_use]
1139    pub fn smp_order_id(&self) -> &str {
1140        self.smp_order_id.as_str()
1141    }
1142
1143    #[getter]
1144    #[must_use]
1145    pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1146        self.tpsl_mode
1147    }
1148
1149    #[getter]
1150    #[must_use]
1151    pub fn tp_limit_price(&self) -> &str {
1152        &self.tp_limit_price
1153    }
1154
1155    #[getter]
1156    #[must_use]
1157    pub fn sl_limit_price(&self) -> &str {
1158        &self.sl_limit_price
1159    }
1160
1161    #[getter]
1162    #[must_use]
1163    pub fn place_type(&self) -> &str {
1164        self.place_type.as_str()
1165    }
1166
1167    #[getter]
1168    #[must_use]
1169    pub fn created_time(&self) -> &str {
1170        &self.created_time
1171    }
1172
1173    #[getter]
1174    #[must_use]
1175    pub fn updated_time(&self) -> &str {
1176        &self.updated_time
1177    }
1178}
1179
1180/// Response alias for open order queries.
1181///
1182/// # References
1183/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1184pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1185/// Response alias for order history queries with pagination.
1186///
1187/// # References
1188/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1189pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1190
1191/// Payload returned after placing a single order.
1192///
1193/// # References
1194/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1195#[derive(Clone, Debug, Serialize, Deserialize)]
1196#[serde(rename_all = "camelCase")]
1197pub struct BybitPlaceOrderResult {
1198    pub order_id: Option<Ustr>,
1199    pub order_link_id: Option<Ustr>,
1200}
1201
1202/// Response alias for order placement endpoints.
1203///
1204/// # References
1205/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1206pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1207
1208/// Payload returned after cancelling a single order.
1209///
1210/// # References
1211/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1212#[derive(Clone, Debug, Serialize, Deserialize)]
1213#[serde(rename_all = "camelCase")]
1214pub struct BybitCancelOrderResult {
1215    pub order_id: Option<Ustr>,
1216    pub order_link_id: Option<Ustr>,
1217}
1218
1219/// Response alias for order cancellation endpoints.
1220///
1221/// # References
1222/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1223pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1224
1225/// Execution/Fill payload returned by `GET /v5/execution/list`.
1226///
1227/// # References
1228/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1229#[derive(Clone, Debug, Serialize, Deserialize)]
1230#[serde(rename_all = "camelCase")]
1231pub struct BybitExecution {
1232    pub symbol: Ustr,
1233    pub order_id: Ustr,
1234    pub order_link_id: Ustr,
1235    pub side: BybitOrderSide,
1236    pub order_price: String,
1237    pub order_qty: String,
1238    pub leaves_qty: String,
1239    pub create_type: Option<BybitCreateType>,
1240    pub order_type: BybitOrderType,
1241    pub stop_order_type: Option<BybitStopOrderType>,
1242    pub exec_fee: String,
1243    pub exec_id: String,
1244    pub exec_price: String,
1245    pub exec_qty: String,
1246    pub exec_type: BybitExecType,
1247    pub exec_value: String,
1248    pub exec_time: String,
1249    pub fee_currency: Ustr,
1250    pub is_maker: bool,
1251    pub fee_rate: String,
1252    pub trade_iv: String,
1253    pub mark_iv: String,
1254    pub mark_price: String,
1255    pub index_price: String,
1256    pub underlying_price: String,
1257    pub block_trade_id: String,
1258    pub closed_size: String,
1259    pub seq: i64,
1260}
1261
1262/// Response alias for trade history requests.
1263///
1264/// # References
1265/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1266pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1267
1268/// Represents a position returned by the Bybit API.
1269///
1270/// # References
1271/// - <https://bybit-exchange.github.io/docs/v5/position>
1272#[derive(Clone, Debug, Serialize, Deserialize)]
1273#[serde(rename_all = "camelCase")]
1274pub struct BybitPosition {
1275    pub position_idx: BybitPositionIdx,
1276    pub risk_id: i32,
1277    pub risk_limit_value: String,
1278    pub symbol: Ustr,
1279    pub side: BybitPositionSide,
1280    pub size: String,
1281    pub avg_price: String,
1282    pub position_value: String,
1283    pub trade_mode: i32,
1284    pub position_status: BybitPositionStatus,
1285    pub auto_add_margin: i32,
1286    pub adl_rank_indicator: i32,
1287    pub leverage: String,
1288    pub position_balance: String,
1289    pub mark_price: String,
1290    pub liq_price: String,
1291    pub bust_price: String,
1292    #[serde(rename = "positionMM")]
1293    pub position_mm: String,
1294    #[serde(rename = "positionIM")]
1295    pub position_im: String,
1296    pub tpsl_mode: BybitTpSlMode,
1297    pub take_profit: String,
1298    pub stop_loss: String,
1299    pub trailing_stop: String,
1300    pub unrealised_pnl: String,
1301    pub cur_realised_pnl: String,
1302    pub cum_realised_pnl: String,
1303    #[serde(default = "default_position_seq")]
1304    pub seq: i64,
1305    #[serde(default)]
1306    pub is_reduce_only: bool,
1307    #[serde(default)]
1308    pub mmr_sys_updated_time: String,
1309    #[serde(default)]
1310    pub leverage_sys_updated_time: String,
1311    pub created_time: String,
1312    pub updated_time: String,
1313    #[serde(default)]
1314    pub open_time: i64,
1315}
1316
1317const fn default_position_seq() -> i64 {
1318    -1
1319}
1320
1321/// Response alias for position list requests.
1322///
1323/// # References
1324/// - <https://bybit-exchange.github.io/docs/v5/position>
1325pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1326
1327/// Reason detail for set margin mode failures.
1328///
1329/// # References
1330/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1331#[derive(Clone, Debug, Serialize, Deserialize)]
1332#[serde(rename_all = "camelCase")]
1333pub struct BybitSetMarginModeReason {
1334    pub reason_code: String,
1335    pub reason_msg: String,
1336}
1337
1338/// Result payload for set margin mode operation.
1339///
1340/// # References
1341/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1342#[derive(Clone, Debug, Serialize, Deserialize)]
1343#[serde(rename_all = "camelCase")]
1344pub struct BybitSetMarginModeResult {
1345    #[serde(default)]
1346    pub reasons: Vec<BybitSetMarginModeReason>,
1347}
1348
1349/// Response alias for set margin mode requests.
1350///
1351/// # References
1352/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1353pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1354
1355/// Empty result for set leverage operation.
1356#[derive(Clone, Debug, Serialize, Deserialize)]
1357pub struct BybitSetLeverageResult {}
1358
1359/// Response alias for set leverage requests.
1360///
1361/// # References
1362/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1363pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1364
1365/// Empty result for switch mode operation.
1366#[derive(Clone, Debug, Serialize, Deserialize)]
1367pub struct BybitSwitchModeResult {}
1368
1369/// Response alias for switch mode requests.
1370///
1371/// # References
1372/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1373pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1374
1375/// Empty result for set trading stop operation.
1376#[derive(Clone, Debug, Serialize, Deserialize)]
1377pub struct BybitSetTradingStopResult {}
1378
1379/// Response alias for set trading stop requests.
1380///
1381/// # References
1382/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1383pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1384
1385/// Result from manual borrow operation.
1386#[derive(Clone, Debug, Serialize, Deserialize)]
1387#[serde(rename_all = "camelCase")]
1388pub struct BybitBorrowResult {
1389    pub coin: Ustr,
1390    pub amount: String,
1391}
1392
1393/// Response alias for manual borrow requests.
1394///
1395/// # References
1396///
1397/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1398pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1399
1400/// Result from no-convert repay operation.
1401#[derive(Clone, Debug, Serialize, Deserialize)]
1402#[serde(rename_all = "camelCase")]
1403pub struct BybitNoConvertRepayResult {
1404    pub result_status: String,
1405}
1406
1407/// Response alias for no-convert repay requests.
1408///
1409/// # References
1410///
1411/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1412pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1413
1414/// API key permissions.
1415#[derive(Clone, Debug, Serialize, Deserialize)]
1416#[cfg_attr(
1417    feature = "python",
1418    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1419)]
1420#[cfg_attr(
1421    feature = "python",
1422    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1423)]
1424#[serde(rename_all = "PascalCase")]
1425pub struct BybitApiKeyPermissions {
1426    #[serde(default)]
1427    pub contract_trade: Vec<String>,
1428    #[serde(default)]
1429    pub spot: Vec<String>,
1430    #[serde(default)]
1431    pub wallet: Vec<String>,
1432    #[serde(default)]
1433    pub options: Vec<String>,
1434    #[serde(default)]
1435    pub derivatives: Vec<String>,
1436    #[serde(default)]
1437    pub exchange: Vec<String>,
1438    #[serde(default)]
1439    pub copy_trading: Vec<String>,
1440    #[serde(default)]
1441    pub block_trade: Vec<String>,
1442    // Bybit ships this key uppercase (`"NFT"`); the struct-level PascalCase
1443    // rule would otherwise serialize it as `"Nft"` and silently drop values.
1444    #[serde(rename = "NFT", default)]
1445    pub nft: Vec<String>,
1446    #[serde(default)]
1447    pub affiliate: Vec<String>,
1448    // Newer permission buckets. Master-account responses populate them, sub-key
1449    // responses typically omit or return empty arrays — both cases deserialize
1450    // to an empty `Vec` via `serde(default)`.
1451    #[serde(default)]
1452    pub earn: Vec<String>,
1453    // Bybit uses `"FiatP2P"` — PascalCase rename would emit `"FiatP2p"`.
1454    #[serde(rename = "FiatP2P", default)]
1455    pub fiat_p2p: Vec<String>,
1456    #[serde(default)]
1457    pub fiat_bybit_pay: Vec<String>,
1458    #[serde(default)]
1459    pub fiat_bit_pay: Vec<String>,
1460    #[serde(default)]
1461    pub fiat_global_pay: Vec<String>,
1462    #[serde(default)]
1463    pub fiat_convert_broker: Vec<String>,
1464    #[serde(default)]
1465    pub bit_card: Vec<String>,
1466    // Bybit uses `"ByXPost"` — PascalCase rename would emit `"ByxPost"`.
1467    #[serde(rename = "ByXPost", default)]
1468    pub byx_post: Vec<String>,
1469}
1470
1471/// Account details from API key info.
1472#[derive(Clone, Debug, Serialize, Deserialize)]
1473#[cfg_attr(
1474    feature = "python",
1475    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1476)]
1477#[cfg_attr(
1478    feature = "python",
1479    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1480)]
1481#[serde(rename_all = "camelCase")]
1482pub struct BybitAccountDetails {
1483    pub id: String,
1484    pub note: String,
1485    pub api_key: String,
1486    pub read_only: u8,
1487    pub secret: String,
1488    #[serde(rename = "type")]
1489    pub key_type: u8,
1490    pub permissions: BybitApiKeyPermissions,
1491    pub ips: Vec<String>,
1492    #[serde(default)]
1493    pub user_id: Option<u64>,
1494    #[serde(default)]
1495    pub inviter_id: Option<u64>,
1496    pub vip_level: String,
1497    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1498    pub mkt_maker_level: u8,
1499    #[serde(default)]
1500    pub affiliate_id: Option<u64>,
1501    pub rsa_public_key: String,
1502    pub is_master: bool,
1503    pub parent_uid: String,
1504    pub uta: u8,
1505    pub kyc_level: String,
1506    pub kyc_region: String,
1507    #[serde(default)]
1508    pub unified: Option<i32>,
1509    #[serde(default)]
1510    pub deadline_day: i64,
1511    #[serde(default)]
1512    pub expired_at: Option<String>,
1513    pub created_at: String,
1514}
1515
1516#[cfg(feature = "python")]
1517#[pyo3::pymethods]
1518impl BybitAccountDetails {
1519    #[getter]
1520    #[must_use]
1521    pub fn id(&self) -> &str {
1522        &self.id
1523    }
1524
1525    #[getter]
1526    #[must_use]
1527    pub fn note(&self) -> &str {
1528        &self.note
1529    }
1530
1531    #[getter]
1532    #[must_use]
1533    pub fn api_key(&self) -> &str {
1534        &self.api_key
1535    }
1536
1537    #[getter]
1538    #[must_use]
1539    pub fn read_only(&self) -> u8 {
1540        self.read_only
1541    }
1542
1543    #[getter]
1544    #[must_use]
1545    pub fn key_type(&self) -> u8 {
1546        self.key_type
1547    }
1548
1549    #[getter]
1550    #[must_use]
1551    pub fn user_id(&self) -> Option<u64> {
1552        self.user_id
1553    }
1554
1555    #[getter]
1556    #[must_use]
1557    pub fn inviter_id(&self) -> Option<u64> {
1558        self.inviter_id
1559    }
1560
1561    #[getter]
1562    #[must_use]
1563    pub fn vip_level(&self) -> &str {
1564        &self.vip_level
1565    }
1566
1567    #[getter]
1568    #[must_use]
1569    pub fn mkt_maker_level(&self) -> u8 {
1570        self.mkt_maker_level
1571    }
1572
1573    #[getter]
1574    #[must_use]
1575    pub fn affiliate_id(&self) -> Option<u64> {
1576        self.affiliate_id
1577    }
1578
1579    #[getter]
1580    #[must_use]
1581    pub fn rsa_public_key(&self) -> &str {
1582        &self.rsa_public_key
1583    }
1584
1585    #[getter]
1586    #[must_use]
1587    pub fn is_master(&self) -> bool {
1588        self.is_master
1589    }
1590
1591    #[getter]
1592    #[must_use]
1593    pub fn parent_uid(&self) -> &str {
1594        &self.parent_uid
1595    }
1596
1597    #[getter]
1598    #[must_use]
1599    pub fn uta(&self) -> u8 {
1600        self.uta
1601    }
1602
1603    #[getter]
1604    #[must_use]
1605    pub fn kyc_level(&self) -> &str {
1606        &self.kyc_level
1607    }
1608
1609    #[getter]
1610    #[must_use]
1611    pub fn kyc_region(&self) -> &str {
1612        &self.kyc_region
1613    }
1614
1615    #[getter]
1616    #[must_use]
1617    pub fn deadline_day(&self) -> i64 {
1618        self.deadline_day
1619    }
1620
1621    #[getter]
1622    #[must_use]
1623    pub fn expired_at(&self) -> Option<&str> {
1624        self.expired_at.as_deref()
1625    }
1626
1627    #[getter]
1628    #[must_use]
1629    pub fn created_at(&self) -> &str {
1630        &self.created_at
1631    }
1632}
1633
1634/// Response alias for API key info requests.
1635///
1636/// # References
1637///
1638/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1639pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1640
1641/// Basic information about a sub-account member.
1642///
1643/// `member_type`, `status`, and `account_mode` use raw integer codes whose valid
1644/// ranges differ per endpoint; values are kept as-is rather than mapped to Rust
1645/// enums, consistent with other venue-raw fields in this module.
1646///
1647/// # References
1648///
1649/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1650/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1651/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1652#[derive(Clone, Debug, Serialize, Deserialize)]
1653#[serde(rename_all = "camelCase")]
1654pub struct BybitSubMember {
1655    pub uid: String,
1656    pub username: String,
1657    pub member_type: i32,
1658    pub status: i32,
1659    pub account_mode: i32,
1660    #[serde(default)]
1661    pub remark: String,
1662}
1663
1664/// Result payload for `GET /v5/user/query-sub-members`.
1665#[derive(Clone, Debug, Serialize, Deserialize)]
1666#[serde(rename_all = "camelCase")]
1667pub struct BybitSubMembersResult {
1668    #[serde(default)]
1669    pub sub_members: Vec<BybitSubMember>,
1670}
1671
1672/// Response alias for the non-paginated sub-UID list.
1673///
1674/// # References
1675///
1676/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1677pub type BybitSubMembersResponse = BybitResponse<BybitSubMembersResult>;
1678
1679/// Result payload for cursor-paginated sub-account listings.
1680///
1681/// The inner array is named `subMembers` and the cursor field is `nextCursor`
1682/// (with `"0"` as the end-of-pages sentinel), so the standard
1683/// `BybitCursorListResponse<T>` (which expects `list` / `nextPageCursor`)
1684/// cannot be reused here. Callers treat `"0"` or an empty string as the
1685/// termination sentinel.
1686#[derive(Clone, Debug, Serialize, Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688pub struct BybitSubMembersPagedResult {
1689    #[serde(default)]
1690    pub sub_members: Vec<BybitSubMember>,
1691    #[serde(default)]
1692    pub next_cursor: Option<String>,
1693}
1694
1695impl BybitSubMembersPagedResult {
1696    /// Returns the cursor to use for the next page, or `None` when the final
1697    /// page has been fetched.
1698    ///
1699    /// Bybit signals end-of-pages either by omitting the cursor or returning
1700    /// `"0"`/`""`; both cases collapse to `None` here so callers can treat any
1701    /// non-`None` return value as a live cursor.
1702    #[must_use]
1703    pub fn continuation_cursor(&self) -> Option<&str> {
1704        match self.next_cursor.as_deref() {
1705            None | Some("" | "0") => None,
1706            Some(cursor) => Some(cursor),
1707        }
1708    }
1709
1710    /// Returns `true` when the result has more pages to fetch.
1711    #[must_use]
1712    pub fn has_more_pages(&self) -> bool {
1713        self.continuation_cursor().is_some()
1714    }
1715}
1716
1717/// Response alias for paginated sub-UID list (`/v5/user/submembers`).
1718///
1719/// # References
1720///
1721/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1722pub type BybitSubMembersPagedResponse = BybitResponse<BybitSubMembersPagedResult>;
1723
1724/// Response alias for the escrow (fund-custodial) sub-account list
1725/// (`/v5/user/escrow_sub_members`); shares the paginated sub-member shape.
1726///
1727/// # References
1728///
1729/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1730pub type BybitEscrowSubMembersResponse = BybitResponse<BybitSubMembersPagedResult>;
1731
1732/// Information about a single sub-account API key.
1733///
1734/// Deliberately not shared with [`BybitAccountDetails`]: master-level fields
1735/// such as `is_master`, `parent_uid`, `uta`, and the KYC block are absent.
1736///
1737/// # References
1738///
1739/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1740#[derive(Clone, Debug, Serialize, Deserialize)]
1741#[serde(rename_all = "camelCase")]
1742pub struct BybitSubApiKeyInfo {
1743    pub id: String,
1744    #[serde(default)]
1745    pub ips: Vec<String>,
1746    pub api_key: String,
1747    #[serde(default)]
1748    pub note: String,
1749    pub status: i32,
1750    #[serde(default)]
1751    pub expired_at: Option<String>,
1752    pub created_at: String,
1753    #[serde(rename = "type")]
1754    pub key_type: BybitApiKeyType,
1755    #[serde(with = "masked_secret")]
1756    pub secret: Option<String>,
1757    #[serde(with = "bool_or_int")]
1758    pub read_only: bool,
1759    #[serde(default)]
1760    pub deadline_day: Option<i64>,
1761    #[serde(default)]
1762    pub flag: String,
1763    pub permissions: BybitApiKeyPermissions,
1764}
1765
1766/// Result payload for `GET /v5/user/sub-apikeys`.
1767///
1768/// The inner array field is named `result` (nested inside the outer
1769/// `retCode/retMsg/result` envelope) rather than the usual `list`, so the
1770/// standard `BybitCursorListResponse<T>` cannot be reused here.
1771#[derive(Clone, Debug, Serialize, Deserialize)]
1772#[serde(rename_all = "camelCase")]
1773pub struct BybitSubApiKeysResult {
1774    #[serde(rename = "result", default)]
1775    pub keys: Vec<BybitSubApiKeyInfo>,
1776    #[serde(default)]
1777    pub next_page_cursor: Option<String>,
1778}
1779
1780impl BybitSubApiKeysResult {
1781    /// Returns the cursor to use for the next page, or `None` when the final
1782    /// page has been fetched.
1783    ///
1784    /// The end-of-pages sentinel on this endpoint is an empty string rather
1785    /// than `"0"`; both that and a missing cursor collapse to `None`.
1786    #[must_use]
1787    pub fn continuation_cursor(&self) -> Option<&str> {
1788        match self.next_page_cursor.as_deref() {
1789            None | Some("") => None,
1790            Some(cursor) => Some(cursor),
1791        }
1792    }
1793
1794    /// Returns `true` when the result has more pages to fetch.
1795    #[must_use]
1796    pub fn has_more_pages(&self) -> bool {
1797        self.continuation_cursor().is_some()
1798    }
1799}
1800
1801/// Response alias for sub-account API keys list.
1802///
1803/// # References
1804///
1805/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1806pub type BybitSubApiKeysResponse = BybitResponse<BybitSubApiKeysResult>;
1807
1808/// Shared result payload for API-key update endpoints (sub or master).
1809///
1810/// `/v5/user/update-sub-api` and `/v5/user/update-api` return the same field
1811/// set; only the number of permission buckets populated inside `permissions`
1812/// differs. Because [`BybitApiKeyPermissions`] covers the superset of both,
1813/// the two endpoints reuse a single DTO.
1814#[derive(Clone, Debug, Serialize, Deserialize)]
1815#[serde(rename_all = "camelCase")]
1816pub struct BybitApiKeyUpdateResult {
1817    pub id: String,
1818    #[serde(default)]
1819    pub note: String,
1820    pub api_key: String,
1821    #[serde(with = "bool_or_int")]
1822    pub read_only: bool,
1823    #[serde(with = "masked_secret")]
1824    pub secret: Option<String>,
1825    pub permissions: BybitApiKeyPermissions,
1826    #[serde(default)]
1827    pub ips: Vec<String>,
1828}
1829
1830/// Response alias for `POST /v5/user/update-sub-api`.
1831///
1832/// # References
1833///
1834/// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
1835pub type BybitUpdateSubApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1836
1837/// Response alias for `POST /v5/user/update-api`.
1838///
1839/// # References
1840///
1841/// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
1842pub type BybitUpdateMasterApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1843
1844#[cfg(test)]
1845mod tests {
1846    use nautilus_core::UnixNanos;
1847    use nautilus_model::identifiers::AccountId;
1848    use rstest::rstest;
1849    use rust_decimal::Decimal;
1850    use rust_decimal_macros::dec;
1851
1852    use super::*;
1853    use crate::common::testing::load_test_json;
1854
1855    #[rstest]
1856    fn deserialize_spot_instrument_uses_enums() {
1857        let json = load_test_json("http_get_instruments_spot.json");
1858        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1859        let instrument = &response.result.list[0];
1860
1861        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1862        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1863        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1864    }
1865
1866    #[rstest]
1867    fn deserialize_linear_instrument_status() {
1868        let json = load_test_json("http_get_instruments_linear.json");
1869        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1870        let instrument = &response.result.list[0];
1871
1872        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1873        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1874    }
1875
1876    #[rstest]
1877    fn deserialize_spot_instrument_with_xstock_fields() {
1878        let json = load_test_json("http_get_instruments_spot_xstocks.json");
1879        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1880        let instrument = &response.result.list[0];
1881
1882        assert_eq!(instrument.symbol_id, Some(42));
1883        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Xstocks));
1884        assert_eq!(instrument.xstock_multiplier.as_deref(), Some("0.1"));
1885    }
1886
1887    #[rstest]
1888    fn deserialize_linear_instrument_with_symbol_type_and_id() {
1889        let json = load_test_json("http_get_instruments_linear_symbol_type.json");
1890        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1891        let instrument = &response.result.list[0];
1892
1893        assert_eq!(instrument.symbol_id, Some(7));
1894        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Stock));
1895    }
1896
1897    #[derive(Deserialize)]
1898    struct SymbolTypeWrap {
1899        #[serde(rename = "symbolType")]
1900        t: BybitSymbolType,
1901    }
1902
1903    #[rstest]
1904    fn deserialize_symbol_type_falls_back_to_other_for_unknown() {
1905        let json = r#"{"symbolType": "newthing"}"#;
1906        let parsed: SymbolTypeWrap = serde_json::from_str(json).unwrap();
1907        assert_eq!(parsed.t, BybitSymbolType::Other);
1908    }
1909
1910    #[rstest]
1911    fn deserialize_account_info_response() {
1912        let json = load_test_json("http_get_account_info.json");
1913        let response: BybitAccountInfoResponse = serde_json::from_str(&json).unwrap();
1914
1915        assert_eq!(response.result.margin_mode, BybitMarginMode::RegularMargin);
1916        assert_eq!(
1917            response.result.unified_margin_status,
1918            BybitUnifiedMarginStatus::UnifiedTradingAccount10Pro
1919        );
1920        assert!(!response.result.is_master_trader);
1921        assert!(!response.result.spot_hedging_status);
1922        assert!(!response.result.dcp_status);
1923        assert_eq!(response.result.time_window, 10);
1924        assert_eq!(response.result.smp_group, 0);
1925    }
1926
1927    #[rstest]
1928    fn deserialize_account_info_without_deprecated_fields() {
1929        let json = r#"{
1930            "retCode": 0,
1931            "retMsg": "OK",
1932            "result": {
1933                "marginMode": "PORTFOLIO_MARGIN",
1934                "updatedTime": "1697078946000",
1935                "unifiedMarginStatus": 5,
1936                "isMasterTrader": true,
1937                "spotHedgingStatus": "ON"
1938            }
1939        }"#;
1940        let response: BybitAccountInfoResponse = serde_json::from_str(json).unwrap();
1941
1942        assert_eq!(
1943            response.result.margin_mode,
1944            BybitMarginMode::PortfolioMargin
1945        );
1946        assert_eq!(
1947            response.result.unified_margin_status,
1948            BybitUnifiedMarginStatus::UnifiedTradingAccount20
1949        );
1950        assert!(response.result.is_master_trader);
1951        assert!(response.result.spot_hedging_status);
1952        assert!(!response.result.dcp_status);
1953        assert_eq!(response.result.time_window, 0);
1954        assert_eq!(response.result.smp_group, 0);
1955    }
1956
1957    #[rstest]
1958    fn deserialize_order_response_maps_enums() {
1959        let json = load_test_json("http_get_orders_history.json");
1960        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1961        let order = &response.result.list[0];
1962
1963        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1964        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1965        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1966        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1967        assert_eq!(order.order_type, BybitOrderType::Limit);
1968        assert_eq!(order.smp_type, BybitSmpType::None);
1969    }
1970
1971    #[rstest]
1972    fn deserialize_wallet_balance_without_optional_fields() {
1973        let json = r#"{
1974            "retCode": 0,
1975            "retMsg": "OK",
1976            "result": {
1977                "list": [{
1978                    "totalEquity": "1000.00",
1979                    "accountIMRate": "0",
1980                    "totalMarginBalance": "1000.00",
1981                    "totalInitialMargin": "0",
1982                    "accountType": "UNIFIED",
1983                    "totalAvailableBalance": "1000.00",
1984                    "accountMMRate": "0",
1985                    "totalPerpUPL": "0",
1986                    "totalWalletBalance": "1000.00",
1987                    "accountLTV": "0",
1988                    "totalMaintenanceMargin": "0",
1989                    "coin": [{
1990                        "availableToBorrow": "0",
1991                        "bonus": "0",
1992                        "accruedInterest": "0",
1993                        "availableToWithdraw": "1000.00",
1994                        "equity": "1000.00",
1995                        "usdValue": "1000.00",
1996                        "borrowAmount": "0",
1997                        "totalPositionIM": "0",
1998                        "walletBalance": "1000.00",
1999                        "unrealisedPnl": "0",
2000                        "cumRealisedPnl": "0",
2001                        "locked": "0",
2002                        "collateralSwitch": true,
2003                        "marginCollateral": true,
2004                        "coin": "USDT"
2005                    }]
2006                }]
2007            }
2008        }"#;
2009
2010        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2011            .expect("Failed to parse wallet balance without optional fields");
2012
2013        assert_eq!(response.ret_code, 0);
2014        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
2015        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
2016    }
2017
2018    #[rstest]
2019    fn deserialize_wallet_balance_from_docs() {
2020        let json = include_str!("../../test_data/http_get_wallet_balance.json");
2021
2022        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2023            .expect("Failed to parse wallet balance from Bybit docs example");
2024
2025        assert_eq!(response.ret_code, 0);
2026        assert_eq!(response.ret_msg, "OK");
2027
2028        let wallet = &response.result.list[0];
2029        assert_eq!(wallet.total_equity, "3.31216591");
2030        assert_eq!(wallet.account_im_rate, "0");
2031        assert_eq!(wallet.account_mm_rate, "0");
2032        assert_eq!(wallet.total_perp_upl, "0");
2033        assert_eq!(wallet.account_ltv, "0");
2034
2035        // Check BTC coin
2036        let btc = &wallet.coin[0];
2037        assert_eq!(btc.coin.as_str(), "BTC");
2038        assert_eq!(btc.available_to_borrow, "3");
2039        assert_eq!(btc.total_order_im, Some("0".to_string()));
2040        assert_eq!(btc.total_position_mm, Some("0".to_string()));
2041        assert_eq!(btc.total_position_im, Some("0".to_string()));
2042
2043        // Check USDT coin (without optional IM/MM fields)
2044        let usdt = &wallet.coin[1];
2045        assert_eq!(usdt.coin.as_str(), "USDT");
2046        assert_eq!(usdt.wallet_balance, dec!(1000.50));
2047        assert_eq!(usdt.total_order_im, None);
2048        assert_eq!(usdt.total_position_mm, None);
2049        assert_eq!(usdt.total_position_im, None);
2050        assert_eq!(btc.spot_borrow, Decimal::ZERO);
2051        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
2052    }
2053
2054    #[rstest]
2055    fn test_parse_wallet_balance_with_spot_borrow() {
2056        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
2057        let response: BybitWalletBalanceResponse =
2058            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
2059
2060        let wallet = &response.result.list[0];
2061        let usdt = &wallet.coin[0];
2062
2063        assert_eq!(usdt.coin.as_str(), "USDT");
2064        assert_eq!(usdt.wallet_balance, dec!(1200.00));
2065        assert_eq!(usdt.spot_borrow, dec!(200.00));
2066        assert_eq!(usdt.borrow_amount, "200.00");
2067
2068        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
2069        let account_id = crate::common::parse::parse_account_state(
2070            wallet,
2071            AccountId::new("BYBIT-001"),
2072            UnixNanos::default(),
2073        )
2074        .expect("Failed to parse account state");
2075
2076        let balance = &account_id.balances[0];
2077        assert_eq!(balance.total.as_f64(), 1000.0);
2078    }
2079
2080    #[rstest]
2081    fn test_parse_wallet_balance_spot_short() {
2082        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
2083        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2084            .expect("Failed to parse wallet balance with SHORT SPOT position");
2085
2086        let wallet = &response.result.list[0];
2087        let eth = &wallet.coin[0];
2088
2089        assert_eq!(eth.coin.as_str(), "ETH");
2090        assert_eq!(eth.wallet_balance, dec!(0));
2091        assert_eq!(eth.spot_borrow, dec!(0.06142));
2092        assert_eq!(eth.borrow_amount, "0.06142");
2093
2094        let account_state = crate::common::parse::parse_account_state(
2095            wallet,
2096            AccountId::new("BYBIT-001"),
2097            UnixNanos::default(),
2098        )
2099        .expect("Failed to parse account state");
2100
2101        let eth_balance = account_state
2102            .balances
2103            .iter()
2104            .find(|b| b.currency.code.as_str() == "ETH")
2105            .expect("ETH balance not found");
2106
2107        // Negative balance represents SHORT position (borrowed ETH)
2108        assert_eq!(eth_balance.total.as_f64(), -0.06142);
2109    }
2110
2111    #[rstest]
2112    fn deserialize_borrow_response() {
2113        let json = r#"{
2114            "retCode": 0,
2115            "retMsg": "success",
2116            "result": {
2117                "coin": "BTC",
2118                "amount": "0.01"
2119            },
2120            "retExtInfo": {},
2121            "time": 1756197991955
2122        }"#;
2123
2124        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
2125
2126        assert_eq!(response.ret_code, 0);
2127        assert_eq!(response.ret_msg, "success");
2128        assert_eq!(response.result.coin, "BTC");
2129        assert_eq!(response.result.amount, "0.01");
2130    }
2131
2132    #[rstest]
2133    fn deserialize_no_convert_repay_response() {
2134        let json = r#"{
2135            "retCode": 0,
2136            "retMsg": "OK",
2137            "result": {
2138                "resultStatus": "SU"
2139            },
2140            "retExtInfo": {},
2141            "time": 1234567890
2142        }"#;
2143
2144        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
2145
2146        assert_eq!(response.ret_code, 0);
2147        assert_eq!(response.ret_msg, "OK");
2148        assert_eq!(response.result.result_status, "SU");
2149    }
2150
2151    #[rstest]
2152    fn deserialize_position_without_conditional_fields() {
2153        // Bybit v5 docs mark `isReduceOnly`, `mmrSysUpdatedTime`, `leverageSysUpdatedTime`
2154        // and `seq` as conditional fields that may be absent, e.g. once a position has been
2155        // closed through the UI (see issue #3836).
2156        let json = r#"{
2157            "retCode": 0,
2158            "retMsg": "OK",
2159            "result": {
2160                "list": [{
2161                    "positionIdx": 0,
2162                    "riskId": 1,
2163                    "riskLimitValue": "150",
2164                    "symbol": "LTCUSDT",
2165                    "side": "",
2166                    "size": "0",
2167                    "avgPrice": "0",
2168                    "positionValue": "0",
2169                    "tradeMode": 0,
2170                    "positionStatus": "Normal",
2171                    "autoAddMargin": 0,
2172                    "adlRankIndicator": 0,
2173                    "leverage": "10",
2174                    "positionBalance": "0",
2175                    "markPrice": "70.00",
2176                    "liqPrice": "",
2177                    "bustPrice": "",
2178                    "positionMM": "0",
2179                    "positionIM": "0",
2180                    "tpslMode": "Full",
2181                    "takeProfit": "0",
2182                    "stopLoss": "0",
2183                    "trailingStop": "0",
2184                    "unrealisedPnl": "0",
2185                    "curRealisedPnl": "0",
2186                    "cumRealisedPnl": "0",
2187                    "createdTime": "1676538056258",
2188                    "updatedTime": "1697673600012"
2189                }],
2190                "nextPageCursor": "",
2191                "category": "linear"
2192            },
2193            "retExtInfo": {},
2194            "time": 1697673900000
2195        }"#;
2196
2197        let response: BybitPositionListResponse = serde_json::from_str(json)
2198            .expect("Failed to parse position list with missing conditional fields");
2199
2200        let position = &response.result.list[0];
2201        assert!(!position.is_reduce_only);
2202        assert_eq!(position.seq, -1);
2203        assert_eq!(position.mmr_sys_updated_time, "");
2204        assert_eq!(position.leverage_sys_updated_time, "");
2205        assert_eq!(position.open_time, 0);
2206    }
2207
2208    #[rstest]
2209    #[case(0_i64)]
2210    #[case(1_700_000_000_123_i64)]
2211    fn deserialize_position_with_open_time_integer(#[case] expected: i64) {
2212        // Bybit position info added `openTime` (integer, ms; default 0) effective 2026-04-21.
2213        let mut value: serde_json::Value =
2214            serde_json::from_str(&load_test_json("http_get_positions_with_open_time.json"))
2215                .unwrap();
2216        value["result"]["list"][0]["openTime"] = serde_json::json!(expected);
2217
2218        let response: BybitPositionListResponse = serde_json::from_value(value)
2219            .expect("Failed to parse position list with integer openTime");
2220
2221        assert_eq!(response.result.list[0].open_time, expected);
2222    }
2223
2224    #[rstest]
2225    fn deserialize_inverse_instrument_with_symbol_type_and_id() {
2226        let json = load_test_json("http_get_instruments_inverse_symbol_type.json");
2227        let response: BybitInstrumentInverseResponse = serde_json::from_str(&json).unwrap();
2228        let instrument = &response.result.list[0];
2229
2230        assert_eq!(instrument.symbol_id, Some(11));
2231        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Commodity));
2232    }
2233
2234    #[rstest]
2235    fn deserialize_option_instrument_with_symbol_id() {
2236        let json = load_test_json("http_get_instruments_option_symbol_id.json");
2237        let response: BybitInstrumentOptionResponse = serde_json::from_str(&json).unwrap();
2238        let instrument = &response.result.list[0];
2239
2240        assert_eq!(instrument.symbol_id, Some(99));
2241    }
2242
2243    #[rstest]
2244    fn deserialize_sub_members_response() {
2245        let json = load_test_json("http_get_user_sub_members.json");
2246        let response: BybitSubMembersResponse =
2247            serde_json::from_str(&json).expect("parse sub members");
2248        assert_eq!(response.ret_code, 0);
2249        assert_eq!(response.result.sub_members.len(), 2);
2250        let first = &response.result.sub_members[0];
2251        assert_eq!(first.uid, "106314365");
2252        assert_eq!(first.username, "xxxx02");
2253        assert_eq!(first.member_type, 1);
2254        assert_eq!(first.status, 1);
2255        assert_eq!(first.account_mode, 5);
2256        assert_eq!(first.remark, "");
2257        let second = &response.result.sub_members[1];
2258        assert_eq!(second.uid, "106279879");
2259        assert_eq!(second.account_mode, 6);
2260    }
2261
2262    #[rstest]
2263    fn deserialize_sub_members_paged_response() {
2264        // The final-page sentinel is `"0"`; both `"0"` and `None` collapse to
2265        // `continuation_cursor() == None` via the helper.
2266        let json = load_test_json("http_get_user_sub_members_paged.json");
2267        let response: BybitSubMembersPagedResponse =
2268            serde_json::from_str(&json).expect("parse paged sub members");
2269        assert_eq!(response.result.sub_members.len(), 2);
2270        assert_eq!(response.result.next_cursor.as_deref(), Some("0"));
2271        assert!(!response.result.has_more_pages());
2272        assert_eq!(response.result.continuation_cursor(), None);
2273    }
2274
2275    #[rstest]
2276    fn deserialize_escrow_sub_members_response_uses_same_shape() {
2277        // The escrow alias must decode into the same shape as the paginated
2278        // sub-member list; a non-`"0"` cursor indicates more pages to fetch.
2279        let json = load_test_json("http_get_user_escrow_sub_members.json");
2280        let response: BybitEscrowSubMembersResponse =
2281            serde_json::from_str(&json).expect("parse escrow sub members");
2282        assert_eq!(response.result.sub_members.len(), 2);
2283        assert_eq!(response.result.sub_members[0].member_type, 12);
2284        assert_eq!(response.result.sub_members[0].remark, "earn fund");
2285        assert_eq!(response.result.next_cursor.as_deref(), Some("344"));
2286        assert!(response.result.has_more_pages());
2287        assert_eq!(response.result.continuation_cursor(), Some("344"));
2288    }
2289
2290    #[rstest]
2291    fn deserialize_sub_api_keys_response() {
2292        // `readOnly` arrives as a bool here; the masked `"******"` secret
2293        // collapses to `None` through the `masked_secret` helper.
2294        let json = load_test_json("http_get_user_sub_apikeys.json");
2295        let response: BybitSubApiKeysResponse =
2296            serde_json::from_str(&json).expect("parse sub apikeys");
2297        assert_eq!(response.result.keys.len(), 1);
2298        let key = &response.result.keys[0];
2299        assert!(!key.read_only);
2300        assert_eq!(key.secret, None);
2301        assert_eq!(key.key_type, BybitApiKeyType::Hmac);
2302        assert_eq!(key.flag, "hmac");
2303        assert_eq!(key.deadline_day, Some(21));
2304        assert_eq!(key.permissions.contract_trade, vec!["Order", "Position"]);
2305        assert_eq!(key.permissions.spot, vec!["SpotTrade"]);
2306        assert!(key.permissions.earn.is_empty());
2307        assert_eq!(response.result.next_page_cursor.as_deref(), Some(""));
2308        assert!(!response.result.has_more_pages());
2309    }
2310
2311    #[rstest]
2312    fn deserialize_update_sub_api_response() {
2313        let json = load_test_json("http_post_user_update_sub_api.json");
2314        let response: BybitUpdateSubApiResponse =
2315            serde_json::from_str(&json).expect("parse update sub api");
2316        assert!(!response.result.read_only);
2317        assert_eq!(response.result.secret, None);
2318        assert_eq!(response.result.ips, vec!["*"]);
2319        assert_eq!(response.result.permissions.spot, vec!["SpotTrade"]);
2320        assert_eq!(response.result.permissions.wallet, vec!["AccountTransfer"]);
2321    }
2322
2323    #[rstest]
2324    fn deserialize_update_master_api_response() {
2325        // Asserts on non-empty permission buckets so the test actually verifies
2326        // deserialisation (an empty `Vec` would be indistinguishable from a
2327        // `#[serde(default)]` fallback). In particular, `nft` exercises the
2328        // explicit `#[serde(rename = "NFT")]` attribute.
2329        let json = load_test_json("http_post_user_update_master_api.json");
2330        let response: BybitUpdateMasterApiResponse =
2331            serde_json::from_str(&json).expect("parse update master api");
2332        assert!(!response.result.read_only);
2333        assert_eq!(response.result.ips, vec!["*"]);
2334        let perms = &response.result.permissions;
2335        assert_eq!(perms.contract_trade, vec!["Order", "Position"]);
2336        assert_eq!(perms.copy_trading, vec!["CopyTrading"]);
2337        assert!(perms.earn.is_empty());
2338        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2339    }
2340
2341    #[rstest]
2342    fn deserialize_permissions_renamed_buckets_preserve_values() {
2343        // Regression guard for `#[serde(rename = ...)]` on permission keys
2344        // whose Bybit casing (`NFT`, `FiatP2P`, `ByXPost`) differs from
2345        // serde's `PascalCase` default (`Nft`, `FiatP2p`, `ByxPost`). Using
2346        // non-empty values ensures a rename regression causes a failure
2347        // rather than silently falling through to `serde(default)`.
2348        let json = r#"{
2349            "NFT": ["NFTQueryProductList"],
2350            "FiatP2P": ["P2PDeposit"],
2351            "ByXPost": ["PostContent"]
2352        }"#;
2353        let perms: BybitApiKeyPermissions =
2354            serde_json::from_str(json).expect("parse renamed buckets");
2355        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2356        assert_eq!(perms.fiat_p2p, vec!["P2PDeposit"]);
2357        assert_eq!(perms.byx_post, vec!["PostContent"]);
2358    }
2359
2360    #[rstest]
2361    fn deserialize_account_details_response_with_current_docs_example() {
2362        let json = load_test_json("http_get_user_query_api.json");
2363        let response: BybitAccountDetailsResponse =
2364            serde_json::from_str(&json).expect("parse account details");
2365
2366        assert_eq!(
2367            response.result.permissions.fiat_global_pay,
2368            Vec::<String>::new()
2369        );
2370        assert_eq!(
2371            response.result.permissions.fiat_bit_pay,
2372            vec!["FaitPayOrder"]
2373        );
2374        assert_eq!(response.result.permissions.bit_card, vec!["BitCard"]);
2375        assert_eq!(response.result.permissions.byx_post, vec!["ByXPost"]);
2376        assert_eq!(response.result.unified, Some(0));
2377    }
2378}