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