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