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, BybitCancelType, BybitContractType, BybitExecType, BybitInnovationFlag,
25        BybitInstrumentStatus, BybitMarginTrading, BybitOptionType, BybitOrderSide,
26        BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide, BybitProductType,
27        BybitStopOrderType, BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection,
28        BybitTriggerType,
29    },
30    models::{
31        BybitCursorList, BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
32        LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
33        SpotPriceFilter,
34    },
35    parse::{
36        deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero, deserialize_string_to_u8,
37    },
38};
39
40/// Cursor-paginated list of orders for Python bindings.
41#[derive(Clone, Debug, Default, Serialize, Deserialize)]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
45)]
46pub struct BybitOrderCursorList {
47    /// Collection of orders returned by the endpoint.
48    pub list: Vec<BybitOrder>,
49    /// Pagination cursor for the next page.
50    pub next_page_cursor: Option<String>,
51    /// Optional product category when the API includes it.
52    #[serde(default)]
53    pub category: Option<BybitProductType>,
54}
55
56impl From<BybitCursorList<BybitOrder>> for BybitOrderCursorList {
57    fn from(cursor_list: BybitCursorList<BybitOrder>) -> Self {
58        Self {
59            list: cursor_list.list,
60            next_page_cursor: cursor_list.next_page_cursor,
61            category: cursor_list.category,
62        }
63    }
64}
65
66#[cfg(feature = "python")]
67#[pyo3::pymethods]
68impl BybitOrderCursorList {
69    #[getter]
70    #[must_use]
71    pub fn list(&self) -> Vec<BybitOrder> {
72        self.list.clone()
73    }
74
75    #[getter]
76    #[must_use]
77    pub fn next_page_cursor(&self) -> Option<&str> {
78        self.next_page_cursor.as_deref()
79    }
80
81    #[getter]
82    #[must_use]
83    pub fn category(&self) -> Option<BybitProductType> {
84        self.category
85    }
86}
87
88/// Response payload returned by `GET /v5/market/time`.
89///
90/// # References
91/// - <https://bybit-exchange.github.io/docs/v5/market/time>
92#[derive(Clone, Debug, Serialize, Deserialize)]
93#[cfg_attr(
94    feature = "python",
95    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
96)]
97#[serde(rename_all = "camelCase")]
98pub struct BybitServerTime {
99    /// Server timestamp in seconds represented as string.
100    pub time_second: String,
101    /// Server timestamp in nanoseconds represented as string.
102    pub time_nano: String,
103}
104
105#[cfg(feature = "python")]
106#[pyo3::pymethods]
107impl BybitServerTime {
108    #[getter]
109    #[must_use]
110    pub fn time_second(&self) -> &str {
111        &self.time_second
112    }
113
114    #[getter]
115    #[must_use]
116    pub fn time_nano(&self) -> &str {
117        &self.time_nano
118    }
119}
120
121/// Type alias for the server time response envelope.
122///
123/// # References
124/// - <https://bybit-exchange.github.io/docs/v5/market/time>
125pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
126
127/// Ticker payload for spot instruments.
128///
129/// # References
130/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
131#[derive(Clone, Debug, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct BybitTickerSpot {
134    pub symbol: Ustr,
135    pub bid1_price: String,
136    pub bid1_size: String,
137    pub ask1_price: String,
138    pub ask1_size: String,
139    pub last_price: String,
140    pub prev_price24h: String,
141    pub price24h_pcnt: String,
142    pub high_price24h: String,
143    pub low_price24h: String,
144    pub turnover24h: String,
145    pub volume24h: String,
146    #[serde(default)]
147    pub usd_index_price: String,
148}
149
150/// Ticker payload for linear and inverse perpetual/futures instruments.
151///
152/// # References
153/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
154#[derive(Clone, Debug, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct BybitTickerLinear {
157    pub symbol: Ustr,
158    pub last_price: String,
159    pub index_price: String,
160    pub mark_price: String,
161    pub prev_price24h: String,
162    pub price24h_pcnt: String,
163    pub high_price24h: String,
164    pub low_price24h: String,
165    pub prev_price1h: String,
166    pub open_interest: String,
167    pub open_interest_value: String,
168    pub turnover24h: String,
169    pub volume24h: String,
170    pub funding_rate: String,
171    pub next_funding_time: String,
172    pub predicted_delivery_price: String,
173    pub basis_rate: String,
174    pub delivery_fee_rate: String,
175    pub delivery_time: String,
176    pub ask1_size: String,
177    pub bid1_price: String,
178    pub ask1_price: String,
179    pub bid1_size: String,
180    pub basis: String,
181}
182
183/// Ticker payload for option instruments.
184///
185/// # References
186/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
187#[derive(Clone, Debug, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct BybitTickerOption {
190    pub symbol: Ustr,
191    pub bid1_price: String,
192    pub bid1_size: String,
193    pub bid1_iv: String,
194    pub ask1_price: String,
195    pub ask1_size: String,
196    pub ask1_iv: String,
197    pub last_price: String,
198    pub high_price24h: String,
199    pub low_price24h: String,
200    pub mark_price: String,
201    pub index_price: String,
202    pub mark_iv: String,
203    pub underlying_price: String,
204    pub open_interest: String,
205    pub turnover24h: String,
206    pub volume24h: String,
207    pub total_volume: String,
208    pub total_turnover: String,
209    pub delta: String,
210    pub gamma: String,
211    pub vega: String,
212    pub theta: String,
213    pub predicted_delivery_price: String,
214    pub change24h: String,
215}
216
217/// Response alias for spot ticker requests.
218///
219/// # References
220/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
221pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
222/// Response alias for linear/inverse ticker requests.
223///
224/// # References
225/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
226pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
227/// Response alias for option ticker requests.
228///
229/// # References
230/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
231pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
232
233/// Unified ticker data structure containing common fields across all product types.
234///
235/// This simplified ticker structure is designed to work across SPOT, LINEAR, and OPTION products,
236/// containing only the most commonly used fields.
237#[derive(Clone, Debug, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239#[cfg_attr(
240    feature = "python",
241    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
242)]
243pub struct BybitTickerData {
244    pub symbol: Ustr,
245    pub bid1_price: String,
246    pub bid1_size: String,
247    pub ask1_price: String,
248    pub ask1_size: String,
249    pub last_price: String,
250    pub high_price24h: String,
251    pub low_price24h: String,
252    pub turnover24h: String,
253    pub volume24h: String,
254    #[serde(default)]
255    pub open_interest: Option<String>,
256    #[serde(default)]
257    pub funding_rate: Option<String>,
258    #[serde(default)]
259    pub next_funding_time: Option<String>,
260    #[serde(default)]
261    pub mark_price: Option<String>,
262    #[serde(default)]
263    pub index_price: Option<String>,
264}
265
266#[cfg(feature = "python")]
267#[pyo3::pymethods]
268impl BybitTickerData {
269    #[getter]
270    #[must_use]
271    pub fn symbol(&self) -> &str {
272        self.symbol.as_str()
273    }
274
275    #[getter]
276    #[must_use]
277    pub fn bid1_price(&self) -> &str {
278        &self.bid1_price
279    }
280
281    #[getter]
282    #[must_use]
283    pub fn bid1_size(&self) -> &str {
284        &self.bid1_size
285    }
286
287    #[getter]
288    #[must_use]
289    pub fn ask1_price(&self) -> &str {
290        &self.ask1_price
291    }
292
293    #[getter]
294    #[must_use]
295    pub fn ask1_size(&self) -> &str {
296        &self.ask1_size
297    }
298
299    #[getter]
300    #[must_use]
301    pub fn last_price(&self) -> &str {
302        &self.last_price
303    }
304
305    #[getter]
306    #[must_use]
307    pub fn high_price24h(&self) -> &str {
308        &self.high_price24h
309    }
310
311    #[getter]
312    #[must_use]
313    pub fn low_price24h(&self) -> &str {
314        &self.low_price24h
315    }
316
317    #[getter]
318    #[must_use]
319    pub fn turnover24h(&self) -> &str {
320        &self.turnover24h
321    }
322
323    #[getter]
324    #[must_use]
325    pub fn volume24h(&self) -> &str {
326        &self.volume24h
327    }
328
329    #[getter]
330    #[must_use]
331    pub fn open_interest(&self) -> Option<&str> {
332        self.open_interest.as_deref()
333    }
334
335    #[getter]
336    #[must_use]
337    pub fn funding_rate(&self) -> Option<&str> {
338        self.funding_rate.as_deref()
339    }
340
341    #[getter]
342    #[must_use]
343    pub fn next_funding_time(&self) -> Option<&str> {
344        self.next_funding_time.as_deref()
345    }
346
347    #[getter]
348    #[must_use]
349    pub fn mark_price(&self) -> Option<&str> {
350        self.mark_price.as_deref()
351    }
352
353    #[getter]
354    #[must_use]
355    pub fn index_price(&self) -> Option<&str> {
356        self.index_price.as_deref()
357    }
358}
359
360impl From<BybitTickerSpot> for BybitTickerData {
361    fn from(ticker: BybitTickerSpot) -> Self {
362        Self {
363            symbol: ticker.symbol,
364            bid1_price: ticker.bid1_price,
365            bid1_size: ticker.bid1_size,
366            ask1_price: ticker.ask1_price,
367            ask1_size: ticker.ask1_size,
368            last_price: ticker.last_price,
369            high_price24h: ticker.high_price24h,
370            low_price24h: ticker.low_price24h,
371            turnover24h: ticker.turnover24h,
372            volume24h: ticker.volume24h,
373            open_interest: None,
374            funding_rate: None,
375            next_funding_time: None,
376            mark_price: None,
377            index_price: None,
378        }
379    }
380}
381
382impl From<BybitTickerLinear> for BybitTickerData {
383    fn from(ticker: BybitTickerLinear) -> Self {
384        Self {
385            symbol: ticker.symbol,
386            bid1_price: ticker.bid1_price,
387            bid1_size: ticker.bid1_size,
388            ask1_price: ticker.ask1_price,
389            ask1_size: ticker.ask1_size,
390            last_price: ticker.last_price,
391            high_price24h: ticker.high_price24h,
392            low_price24h: ticker.low_price24h,
393            turnover24h: ticker.turnover24h,
394            volume24h: ticker.volume24h,
395            open_interest: Some(ticker.open_interest),
396            funding_rate: Some(ticker.funding_rate),
397            next_funding_time: Some(ticker.next_funding_time),
398            mark_price: Some(ticker.mark_price),
399            index_price: Some(ticker.index_price),
400        }
401    }
402}
403
404impl From<BybitTickerOption> for BybitTickerData {
405    fn from(ticker: BybitTickerOption) -> Self {
406        Self {
407            symbol: ticker.symbol,
408            bid1_price: ticker.bid1_price,
409            bid1_size: ticker.bid1_size,
410            ask1_price: ticker.ask1_price,
411            ask1_size: ticker.ask1_size,
412            last_price: ticker.last_price,
413            high_price24h: ticker.high_price24h,
414            low_price24h: ticker.low_price24h,
415            turnover24h: ticker.turnover24h,
416            volume24h: ticker.volume24h,
417            open_interest: Some(ticker.open_interest),
418            funding_rate: None,
419            next_funding_time: None,
420            mark_price: Some(ticker.mark_price),
421            index_price: Some(ticker.index_price),
422        }
423    }
424}
425
426/// Kline/candlestick entry returned by `GET /v5/market/kline`.
427///
428/// Bybit returns klines as arrays with 7 elements:
429/// [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
430///
431/// # References
432/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
433#[derive(Clone, Debug, Serialize)]
434pub struct BybitKline {
435    pub start: String,
436    pub open: String,
437    pub high: String,
438    pub low: String,
439    pub close: String,
440    pub volume: String,
441    pub turnover: String,
442}
443
444impl<'de> Deserialize<'de> for BybitKline {
445    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
446    where
447        D: serde::Deserializer<'de>,
448    {
449        let [start, open, high, low, close, volume, turnover]: [String; 7] =
450            Deserialize::deserialize(deserializer)?;
451        Ok(Self {
452            start,
453            open,
454            high,
455            low,
456            close,
457            volume,
458            turnover,
459        })
460    }
461}
462
463/// Kline list result returned by Bybit.
464///
465/// # References
466/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
467#[derive(Clone, Debug, Serialize, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub struct BybitKlineResult {
470    pub category: BybitProductType,
471    pub symbol: Ustr,
472    pub list: Vec<BybitKline>,
473}
474
475/// Response alias for kline history requests.
476///
477/// # References
478/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
479pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
480
481/// Trade entry returned by `GET /v5/market/recent-trade`.
482///
483/// # References
484/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
485#[derive(Clone, Debug, Serialize, Deserialize)]
486#[serde(rename_all = "camelCase")]
487pub struct BybitTrade {
488    pub exec_id: String,
489    pub symbol: Ustr,
490    pub price: String,
491    pub size: String,
492    pub side: BybitOrderSide,
493    pub time: String,
494    pub is_block_trade: bool,
495    #[serde(default)]
496    pub m_p: Option<String>,
497    #[serde(default)]
498    pub i_p: Option<String>,
499    #[serde(default)]
500    pub mlv: Option<String>,
501    #[serde(default)]
502    pub iv: Option<String>,
503}
504
505/// Trade list result returned by Bybit.
506///
507/// # References
508/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
509#[derive(Clone, Debug, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct BybitTradeResult {
512    pub category: BybitProductType,
513    pub list: Vec<BybitTrade>,
514}
515
516/// Response alias for recent trades requests.
517///
518/// # References
519/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
520pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
521
522/// Funding entry returned by `GET /v5/market/funding/history`.
523///
524/// # References
525/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
526#[derive(Clone, Debug, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct BybitFunding {
529    pub symbol: Ustr,
530    pub funding_rate: String,
531    pub funding_rate_timestamp: String,
532}
533
534/// Funding list result returned by Bybit.
535///
536/// # References
537/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
538#[derive(Clone, Debug, Serialize, Deserialize)]
539#[serde(rename_all = "camelCase")]
540pub struct BybitFundingResult {
541    pub category: BybitProductType,
542    pub list: Vec<BybitFunding>,
543}
544
545/// Response alias for historical funding requests.
546///
547/// # References
548/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
549pub type BybitFundingResponse = BybitResponse<BybitFundingResult>;
550
551/// Orderbook result returned by Bybit.
552///
553/// # References
554/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
555#[derive(Clone, Debug, Serialize, Deserialize)]
556#[serde(rename_all = "camelCase")]
557pub struct BybitOrderbookResult {
558    /// Symbol.
559    pub s: Ustr,
560    /// Bid levels represented as `[price, size]` string pairs.
561    pub b: Vec<[String; 2]>,
562    /// Ask levels represented as `[price, size]` string pairs.
563    pub a: Vec<[String; 2]>,
564    pub ts: i64,
565    /// Update identifier.
566    pub u: i64,
567    /// Cross sequence number.
568    pub seq: i64,
569    pub cts: i64,
570}
571
572/// Response alias for orderbook requests.
573///
574/// # References
575/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
576pub type BybitOrderbookResponse = BybitResponse<BybitOrderbookResult>;
577
578/// Instrument definition for spot symbols.
579///
580/// # References
581/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
582#[derive(Clone, Debug, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct BybitInstrumentSpot {
585    pub symbol: Ustr,
586    pub base_coin: Ustr,
587    pub quote_coin: Ustr,
588    pub innovation: BybitInnovationFlag,
589    pub status: BybitInstrumentStatus,
590    pub margin_trading: BybitMarginTrading,
591    pub lot_size_filter: SpotLotSizeFilter,
592    pub price_filter: SpotPriceFilter,
593}
594
595/// Instrument definition for linear contracts.
596///
597/// # References
598/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
599#[derive(Clone, Debug, Serialize, Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct BybitInstrumentLinear {
602    pub symbol: Ustr,
603    pub contract_type: BybitContractType,
604    pub status: BybitInstrumentStatus,
605    pub base_coin: Ustr,
606    pub quote_coin: Ustr,
607    pub launch_time: String,
608    pub delivery_time: String,
609    pub delivery_fee_rate: String,
610    pub price_scale: String,
611    pub leverage_filter: LeverageFilter,
612    pub price_filter: LinearPriceFilter,
613    pub lot_size_filter: LinearLotSizeFilter,
614    pub unified_margin_trade: bool,
615    pub funding_interval: i64,
616    pub settle_coin: Ustr,
617}
618
619/// Instrument definition for inverse contracts.
620///
621/// # References
622/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
623#[derive(Clone, Debug, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625pub struct BybitInstrumentInverse {
626    pub symbol: Ustr,
627    pub contract_type: BybitContractType,
628    pub status: BybitInstrumentStatus,
629    pub base_coin: Ustr,
630    pub quote_coin: Ustr,
631    pub launch_time: String,
632    pub delivery_time: String,
633    pub delivery_fee_rate: String,
634    pub price_scale: String,
635    pub leverage_filter: LeverageFilter,
636    pub price_filter: LinearPriceFilter,
637    pub lot_size_filter: LinearLotSizeFilter,
638    pub unified_margin_trade: bool,
639    pub funding_interval: i64,
640    pub settle_coin: Ustr,
641}
642
643/// Instrument definition for option 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 BybitInstrumentOption {
650    pub symbol: Ustr,
651    pub status: BybitInstrumentStatus,
652    pub base_coin: Ustr,
653    pub quote_coin: Ustr,
654    pub settle_coin: Ustr,
655    pub options_type: BybitOptionType,
656    pub launch_time: String,
657    pub delivery_time: String,
658    pub delivery_fee_rate: String,
659    pub price_filter: LinearPriceFilter,
660    pub lot_size_filter: OptionLotSizeFilter,
661}
662
663/// Response alias for instrument info requests that return spot instruments.
664///
665/// # References
666/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
667pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
668/// Response alias for instrument info requests that return linear contracts.
669///
670/// # References
671/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
672pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
673/// Response alias for instrument info requests that return inverse contracts.
674///
675/// # References
676/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
677pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
678/// Response alias for instrument info requests that return option contracts.
679///
680/// # References
681/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
682pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
683
684/// Fee rate structure returned by `GET /v5/account/fee-rate`.
685///
686/// # References
687/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
688#[derive(Clone, Debug, Serialize, Deserialize)]
689#[serde(rename_all = "camelCase")]
690#[cfg_attr(
691    feature = "python",
692    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
693)]
694pub struct BybitFeeRate {
695    pub symbol: Ustr,
696    pub taker_fee_rate: String,
697    pub maker_fee_rate: String,
698    #[serde(default)]
699    pub base_coin: Option<Ustr>,
700}
701
702#[cfg(feature = "python")]
703#[pyo3::pymethods]
704impl BybitFeeRate {
705    #[getter]
706    #[must_use]
707    pub fn symbol(&self) -> &str {
708        self.symbol.as_str()
709    }
710
711    #[getter]
712    #[must_use]
713    pub fn taker_fee_rate(&self) -> &str {
714        &self.taker_fee_rate
715    }
716
717    #[getter]
718    #[must_use]
719    pub fn maker_fee_rate(&self) -> &str {
720        &self.maker_fee_rate
721    }
722
723    #[getter]
724    #[must_use]
725    pub fn base_coin(&self) -> Option<&str> {
726        self.base_coin.as_ref().map(|u| u.as_str())
727    }
728}
729
730/// Response alias for fee rate requests.
731///
732/// # References
733/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
734pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
735
736/// Account balance snapshot coin entry.
737///
738/// # References
739/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
740#[derive(Clone, Debug, Serialize, Deserialize)]
741#[serde(rename_all = "camelCase")]
742pub struct BybitCoinBalance {
743    pub available_to_borrow: String,
744    pub bonus: String,
745    pub accrued_interest: String,
746    pub available_to_withdraw: String,
747    #[serde(default, rename = "totalOrderIM")]
748    pub total_order_im: Option<String>,
749    pub equity: String,
750    pub usd_value: String,
751    pub borrow_amount: String,
752    #[serde(default, rename = "totalPositionMM")]
753    pub total_position_mm: Option<String>,
754    #[serde(default, rename = "totalPositionIM")]
755    pub total_position_im: Option<String>,
756    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
757    pub wallet_balance: Decimal,
758    pub unrealised_pnl: String,
759    pub cum_realised_pnl: String,
760    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
761    pub locked: Decimal,
762    pub collateral_switch: bool,
763    pub margin_collateral: bool,
764    pub coin: Ustr,
765    #[serde(default)]
766    pub spot_hedging_qty: Option<String>,
767    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
768    pub spot_borrow: Decimal,
769}
770
771/// Wallet balance snapshot containing per-coin balances.
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 BybitWalletBalance {
778    pub total_equity: String,
779    #[serde(rename = "accountIMRate")]
780    pub account_im_rate: String,
781    pub total_margin_balance: String,
782    pub total_initial_margin: String,
783    pub account_type: BybitAccountType,
784    pub total_available_balance: String,
785    #[serde(rename = "accountMMRate")]
786    pub account_mm_rate: String,
787    #[serde(rename = "totalPerpUPL")]
788    pub total_perp_upl: String,
789    pub total_wallet_balance: String,
790    #[serde(rename = "accountLTV")]
791    pub account_ltv: String,
792    pub total_maintenance_margin: String,
793    pub coin: Vec<BybitCoinBalance>,
794}
795
796/// Response alias for wallet balance requests.
797///
798/// # References
799/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
800pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
801
802/// Order representation as returned by order-related endpoints.
803///
804/// # References
805/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
806#[derive(Clone, Debug, Serialize, Deserialize)]
807#[cfg_attr(
808    feature = "python",
809    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
810)]
811#[serde(rename_all = "camelCase")]
812pub struct BybitOrder {
813    pub order_id: Ustr,
814    pub order_link_id: Ustr,
815    pub block_trade_id: Option<Ustr>,
816    pub symbol: Ustr,
817    pub price: String,
818    pub qty: String,
819    pub side: BybitOrderSide,
820    pub is_leverage: String,
821    pub position_idx: i32,
822    pub order_status: BybitOrderStatus,
823    pub cancel_type: BybitCancelType,
824    pub reject_reason: Ustr,
825    pub avg_price: Option<String>,
826    pub leaves_qty: String,
827    pub leaves_value: String,
828    pub cum_exec_qty: String,
829    pub cum_exec_value: String,
830    pub cum_exec_fee: String,
831    pub time_in_force: BybitTimeInForce,
832    pub order_type: BybitOrderType,
833    pub stop_order_type: BybitStopOrderType,
834    pub order_iv: Option<String>,
835    pub trigger_price: String,
836    pub take_profit: String,
837    pub stop_loss: String,
838    pub tp_trigger_by: BybitTriggerType,
839    pub sl_trigger_by: BybitTriggerType,
840    pub trigger_direction: BybitTriggerDirection,
841    pub trigger_by: BybitTriggerType,
842    pub last_price_on_created: String,
843    pub reduce_only: bool,
844    pub close_on_trigger: bool,
845    pub smp_type: Ustr,
846    pub smp_group: i32,
847    pub smp_order_id: Ustr,
848    pub tpsl_mode: Option<BybitTpSlMode>,
849    pub tp_limit_price: String,
850    pub sl_limit_price: String,
851    pub place_type: Ustr,
852    pub created_time: String,
853    pub updated_time: String,
854}
855
856#[cfg(feature = "python")]
857#[pyo3::pymethods]
858impl BybitOrder {
859    #[getter]
860    #[must_use]
861    pub fn order_id(&self) -> &str {
862        self.order_id.as_str()
863    }
864
865    #[getter]
866    #[must_use]
867    pub fn order_link_id(&self) -> &str {
868        self.order_link_id.as_str()
869    }
870
871    #[getter]
872    #[must_use]
873    pub fn block_trade_id(&self) -> Option<&str> {
874        self.block_trade_id.as_ref().map(|s| s.as_str())
875    }
876
877    #[getter]
878    #[must_use]
879    pub fn symbol(&self) -> &str {
880        self.symbol.as_str()
881    }
882
883    #[getter]
884    #[must_use]
885    pub fn price(&self) -> &str {
886        &self.price
887    }
888
889    #[getter]
890    #[must_use]
891    pub fn qty(&self) -> &str {
892        &self.qty
893    }
894
895    #[getter]
896    #[must_use]
897    pub fn side(&self) -> BybitOrderSide {
898        self.side
899    }
900
901    #[getter]
902    #[must_use]
903    pub fn is_leverage(&self) -> &str {
904        &self.is_leverage
905    }
906
907    #[getter]
908    #[must_use]
909    pub fn position_idx(&self) -> i32 {
910        self.position_idx
911    }
912
913    #[getter]
914    #[must_use]
915    pub fn order_status(&self) -> BybitOrderStatus {
916        self.order_status
917    }
918
919    #[getter]
920    #[must_use]
921    pub fn cancel_type(&self) -> BybitCancelType {
922        self.cancel_type
923    }
924
925    #[getter]
926    #[must_use]
927    pub fn reject_reason(&self) -> &str {
928        self.reject_reason.as_str()
929    }
930
931    #[getter]
932    #[must_use]
933    pub fn avg_price(&self) -> Option<&str> {
934        self.avg_price.as_deref()
935    }
936
937    #[getter]
938    #[must_use]
939    pub fn leaves_qty(&self) -> &str {
940        &self.leaves_qty
941    }
942
943    #[getter]
944    #[must_use]
945    pub fn leaves_value(&self) -> &str {
946        &self.leaves_value
947    }
948
949    #[getter]
950    #[must_use]
951    pub fn cum_exec_qty(&self) -> &str {
952        &self.cum_exec_qty
953    }
954
955    #[getter]
956    #[must_use]
957    pub fn cum_exec_value(&self) -> &str {
958        &self.cum_exec_value
959    }
960
961    #[getter]
962    #[must_use]
963    pub fn cum_exec_fee(&self) -> &str {
964        &self.cum_exec_fee
965    }
966
967    #[getter]
968    #[must_use]
969    pub fn time_in_force(&self) -> BybitTimeInForce {
970        self.time_in_force
971    }
972
973    #[getter]
974    #[must_use]
975    pub fn order_type(&self) -> BybitOrderType {
976        self.order_type
977    }
978
979    #[getter]
980    #[must_use]
981    pub fn stop_order_type(&self) -> BybitStopOrderType {
982        self.stop_order_type
983    }
984
985    #[getter]
986    #[must_use]
987    pub fn order_iv(&self) -> Option<&str> {
988        self.order_iv.as_deref()
989    }
990
991    #[getter]
992    #[must_use]
993    pub fn trigger_price(&self) -> &str {
994        &self.trigger_price
995    }
996
997    #[getter]
998    #[must_use]
999    pub fn take_profit(&self) -> &str {
1000        &self.take_profit
1001    }
1002
1003    #[getter]
1004    #[must_use]
1005    pub fn stop_loss(&self) -> &str {
1006        &self.stop_loss
1007    }
1008
1009    #[getter]
1010    #[must_use]
1011    pub fn tp_trigger_by(&self) -> BybitTriggerType {
1012        self.tp_trigger_by
1013    }
1014
1015    #[getter]
1016    #[must_use]
1017    pub fn sl_trigger_by(&self) -> BybitTriggerType {
1018        self.sl_trigger_by
1019    }
1020
1021    #[getter]
1022    #[must_use]
1023    pub fn trigger_direction(&self) -> BybitTriggerDirection {
1024        self.trigger_direction
1025    }
1026
1027    #[getter]
1028    #[must_use]
1029    pub fn trigger_by(&self) -> BybitTriggerType {
1030        self.trigger_by
1031    }
1032
1033    #[getter]
1034    #[must_use]
1035    pub fn last_price_on_created(&self) -> &str {
1036        &self.last_price_on_created
1037    }
1038
1039    #[getter]
1040    #[must_use]
1041    pub fn reduce_only(&self) -> bool {
1042        self.reduce_only
1043    }
1044
1045    #[getter]
1046    #[must_use]
1047    pub fn close_on_trigger(&self) -> bool {
1048        self.close_on_trigger
1049    }
1050
1051    #[getter]
1052    #[must_use]
1053    pub fn smp_type(&self) -> &str {
1054        self.smp_type.as_str()
1055    }
1056
1057    #[getter]
1058    #[must_use]
1059    pub fn smp_group(&self) -> i32 {
1060        self.smp_group
1061    }
1062
1063    #[getter]
1064    #[must_use]
1065    pub fn smp_order_id(&self) -> &str {
1066        self.smp_order_id.as_str()
1067    }
1068
1069    #[getter]
1070    #[must_use]
1071    pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1072        self.tpsl_mode
1073    }
1074
1075    #[getter]
1076    #[must_use]
1077    pub fn tp_limit_price(&self) -> &str {
1078        &self.tp_limit_price
1079    }
1080
1081    #[getter]
1082    #[must_use]
1083    pub fn sl_limit_price(&self) -> &str {
1084        &self.sl_limit_price
1085    }
1086
1087    #[getter]
1088    #[must_use]
1089    pub fn place_type(&self) -> &str {
1090        self.place_type.as_str()
1091    }
1092
1093    #[getter]
1094    #[must_use]
1095    pub fn created_time(&self) -> &str {
1096        &self.created_time
1097    }
1098
1099    #[getter]
1100    #[must_use]
1101    pub fn updated_time(&self) -> &str {
1102        &self.updated_time
1103    }
1104}
1105
1106/// Response alias for open order queries.
1107///
1108/// # References
1109/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1110pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1111/// Response alias for order history queries with pagination.
1112///
1113/// # References
1114/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1115pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1116
1117/// Payload returned after placing a single order.
1118///
1119/// # References
1120/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1121#[derive(Clone, Debug, Serialize, Deserialize)]
1122#[serde(rename_all = "camelCase")]
1123pub struct BybitPlaceOrderResult {
1124    pub order_id: Option<Ustr>,
1125    pub order_link_id: Option<Ustr>,
1126}
1127
1128/// Response alias for order placement endpoints.
1129///
1130/// # References
1131/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1132pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1133
1134/// Payload returned after cancelling a single order.
1135///
1136/// # References
1137/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1138#[derive(Clone, Debug, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct BybitCancelOrderResult {
1141    pub order_id: Option<Ustr>,
1142    pub order_link_id: Option<Ustr>,
1143}
1144
1145/// Response alias for order cancellation endpoints.
1146///
1147/// # References
1148/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1149pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1150
1151/// Execution/Fill payload returned by `GET /v5/execution/list`.
1152///
1153/// # References
1154/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1155#[derive(Clone, Debug, Serialize, Deserialize)]
1156#[serde(rename_all = "camelCase")]
1157pub struct BybitExecution {
1158    pub symbol: Ustr,
1159    pub order_id: Ustr,
1160    pub order_link_id: Ustr,
1161    pub side: BybitOrderSide,
1162    pub order_price: String,
1163    pub order_qty: String,
1164    pub leaves_qty: String,
1165    pub create_type: Option<String>,
1166    pub order_type: BybitOrderType,
1167    pub stop_order_type: Option<BybitStopOrderType>,
1168    pub exec_fee: String,
1169    pub exec_id: String,
1170    pub exec_price: String,
1171    pub exec_qty: String,
1172    pub exec_type: BybitExecType,
1173    pub exec_value: String,
1174    pub exec_time: String,
1175    pub fee_currency: Ustr,
1176    pub is_maker: bool,
1177    pub fee_rate: String,
1178    pub trade_iv: String,
1179    pub mark_iv: String,
1180    pub mark_price: String,
1181    pub index_price: String,
1182    pub underlying_price: String,
1183    pub block_trade_id: String,
1184    pub closed_size: String,
1185    pub seq: i64,
1186}
1187
1188/// Response alias for trade history requests.
1189///
1190/// # References
1191/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1192pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1193
1194/// Represents a position returned by the Bybit API.
1195///
1196/// # References
1197/// - <https://bybit-exchange.github.io/docs/v5/position>
1198#[derive(Clone, Debug, Serialize, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct BybitPosition {
1201    pub position_idx: BybitPositionIdx,
1202    pub risk_id: i32,
1203    pub risk_limit_value: String,
1204    pub symbol: Ustr,
1205    pub side: BybitPositionSide,
1206    pub size: String,
1207    pub avg_price: String,
1208    pub position_value: String,
1209    pub trade_mode: i32,
1210    pub position_status: String,
1211    pub auto_add_margin: i32,
1212    pub adl_rank_indicator: i32,
1213    pub leverage: String,
1214    pub position_balance: String,
1215    pub mark_price: String,
1216    pub liq_price: String,
1217    pub bust_price: String,
1218    #[serde(rename = "positionMM")]
1219    pub position_mm: String,
1220    #[serde(rename = "positionIM")]
1221    pub position_im: String,
1222    pub tpsl_mode: String,
1223    pub take_profit: String,
1224    pub stop_loss: String,
1225    pub trailing_stop: String,
1226    pub unrealised_pnl: String,
1227    pub cur_realised_pnl: String,
1228    pub cum_realised_pnl: String,
1229    pub seq: i64,
1230    pub is_reduce_only: bool,
1231    pub mmr_sys_updated_time: String,
1232    pub leverage_sys_updated_time: String,
1233    pub created_time: String,
1234    pub updated_time: String,
1235}
1236
1237/// Response alias for position list requests.
1238///
1239/// # References
1240/// - <https://bybit-exchange.github.io/docs/v5/position>
1241pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1242
1243/// Reason detail for set margin mode failures.
1244///
1245/// # References
1246/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1247#[derive(Clone, Debug, Serialize, Deserialize)]
1248#[serde(rename_all = "camelCase")]
1249pub struct BybitSetMarginModeReason {
1250    pub reason_code: String,
1251    pub reason_msg: String,
1252}
1253
1254/// Result payload for set margin mode operation.
1255///
1256/// # References
1257/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1258#[derive(Clone, Debug, Serialize, Deserialize)]
1259#[serde(rename_all = "camelCase")]
1260pub struct BybitSetMarginModeResult {
1261    #[serde(default)]
1262    pub reasons: Vec<BybitSetMarginModeReason>,
1263}
1264
1265/// Response alias for set margin mode requests.
1266///
1267/// # References
1268/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1269pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1270
1271/// Empty result for set leverage operation.
1272#[derive(Clone, Debug, Serialize, Deserialize)]
1273pub struct BybitSetLeverageResult {}
1274
1275/// Response alias for set leverage requests.
1276///
1277/// # References
1278/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1279pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1280
1281/// Empty result for switch mode operation.
1282#[derive(Clone, Debug, Serialize, Deserialize)]
1283pub struct BybitSwitchModeResult {}
1284
1285/// Response alias for switch mode requests.
1286///
1287/// # References
1288/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1289pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1290
1291/// Empty result for set trading stop operation.
1292#[derive(Clone, Debug, Serialize, Deserialize)]
1293pub struct BybitSetTradingStopResult {}
1294
1295/// Response alias for set trading stop requests.
1296///
1297/// # References
1298/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1299pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1300
1301/// Result from manual borrow operation.
1302#[derive(Clone, Debug, Serialize, Deserialize)]
1303#[serde(rename_all = "camelCase")]
1304pub struct BybitBorrowResult {
1305    pub coin: String,
1306    pub amount: String,
1307}
1308
1309/// Response alias for manual borrow requests.
1310///
1311/// # References
1312///
1313/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1314pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1315
1316/// Result from no-convert repay operation.
1317#[derive(Clone, Debug, Serialize, Deserialize)]
1318#[serde(rename_all = "camelCase")]
1319pub struct BybitNoConvertRepayResult {
1320    pub result_status: String,
1321}
1322
1323/// Response alias for no-convert repay requests.
1324///
1325/// # References
1326///
1327/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1328pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1329
1330/// API key permissions.
1331#[derive(Clone, Debug, Serialize, Deserialize)]
1332#[cfg_attr(
1333    feature = "python",
1334    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
1335)]
1336#[serde(rename_all = "PascalCase")]
1337pub struct BybitApiKeyPermissions {
1338    #[serde(default)]
1339    pub contract_trade: Vec<String>,
1340    #[serde(default)]
1341    pub spot: Vec<String>,
1342    #[serde(default)]
1343    pub wallet: Vec<String>,
1344    #[serde(default)]
1345    pub options: Vec<String>,
1346    #[serde(default)]
1347    pub derivatives: Vec<String>,
1348    #[serde(default)]
1349    pub exchange: Vec<String>,
1350    #[serde(default)]
1351    pub copy_trading: Vec<String>,
1352    #[serde(default)]
1353    pub block_trade: Vec<String>,
1354    #[serde(default)]
1355    pub nft: Vec<String>,
1356    #[serde(default)]
1357    pub affiliate: Vec<String>,
1358}
1359
1360/// Account details from API key info.
1361#[derive(Clone, Debug, Serialize, Deserialize)]
1362#[cfg_attr(
1363    feature = "python",
1364    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
1365)]
1366#[serde(rename_all = "camelCase")]
1367pub struct BybitAccountDetails {
1368    pub id: String,
1369    pub note: String,
1370    pub api_key: String,
1371    pub read_only: u8,
1372    pub secret: String,
1373    #[serde(rename = "type")]
1374    pub key_type: u8,
1375    pub permissions: BybitApiKeyPermissions,
1376    pub ips: Vec<String>,
1377    #[serde(default)]
1378    pub user_id: Option<u64>,
1379    #[serde(default)]
1380    pub inviter_id: Option<u64>,
1381    pub vip_level: String,
1382    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1383    pub mkt_maker_level: u8,
1384    #[serde(default)]
1385    pub affiliate_id: Option<u64>,
1386    pub rsa_public_key: String,
1387    pub is_master: bool,
1388    pub parent_uid: String,
1389    pub uta: u8,
1390    pub kyc_level: String,
1391    pub kyc_region: String,
1392    #[serde(default)]
1393    pub deadline_day: i64,
1394    #[serde(default)]
1395    pub expired_at: Option<String>,
1396    pub created_at: String,
1397}
1398
1399#[cfg(feature = "python")]
1400#[pyo3::pymethods]
1401impl BybitAccountDetails {
1402    #[getter]
1403    #[must_use]
1404    pub fn id(&self) -> &str {
1405        &self.id
1406    }
1407
1408    #[getter]
1409    #[must_use]
1410    pub fn note(&self) -> &str {
1411        &self.note
1412    }
1413
1414    #[getter]
1415    #[must_use]
1416    pub fn api_key(&self) -> &str {
1417        &self.api_key
1418    }
1419
1420    #[getter]
1421    #[must_use]
1422    pub fn read_only(&self) -> u8 {
1423        self.read_only
1424    }
1425
1426    #[getter]
1427    #[must_use]
1428    pub fn key_type(&self) -> u8 {
1429        self.key_type
1430    }
1431
1432    #[getter]
1433    #[must_use]
1434    pub fn user_id(&self) -> Option<u64> {
1435        self.user_id
1436    }
1437
1438    #[getter]
1439    #[must_use]
1440    pub fn inviter_id(&self) -> Option<u64> {
1441        self.inviter_id
1442    }
1443
1444    #[getter]
1445    #[must_use]
1446    pub fn vip_level(&self) -> &str {
1447        &self.vip_level
1448    }
1449
1450    #[getter]
1451    #[must_use]
1452    pub fn mkt_maker_level(&self) -> u8 {
1453        self.mkt_maker_level
1454    }
1455
1456    #[getter]
1457    #[must_use]
1458    pub fn affiliate_id(&self) -> Option<u64> {
1459        self.affiliate_id
1460    }
1461
1462    #[getter]
1463    #[must_use]
1464    pub fn rsa_public_key(&self) -> &str {
1465        &self.rsa_public_key
1466    }
1467
1468    #[getter]
1469    #[must_use]
1470    pub fn is_master(&self) -> bool {
1471        self.is_master
1472    }
1473
1474    #[getter]
1475    #[must_use]
1476    pub fn parent_uid(&self) -> &str {
1477        &self.parent_uid
1478    }
1479
1480    #[getter]
1481    #[must_use]
1482    pub fn uta(&self) -> u8 {
1483        self.uta
1484    }
1485
1486    #[getter]
1487    #[must_use]
1488    pub fn kyc_level(&self) -> &str {
1489        &self.kyc_level
1490    }
1491
1492    #[getter]
1493    #[must_use]
1494    pub fn kyc_region(&self) -> &str {
1495        &self.kyc_region
1496    }
1497
1498    #[getter]
1499    #[must_use]
1500    pub fn deadline_day(&self) -> i64 {
1501        self.deadline_day
1502    }
1503
1504    #[getter]
1505    #[must_use]
1506    pub fn expired_at(&self) -> Option<&str> {
1507        self.expired_at.as_deref()
1508    }
1509
1510    #[getter]
1511    #[must_use]
1512    pub fn created_at(&self) -> &str {
1513        &self.created_at
1514    }
1515}
1516
1517/// Response alias for API key info requests.
1518///
1519/// # References
1520///
1521/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1522pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1523
1524#[cfg(test)]
1525mod tests {
1526    use nautilus_core::UnixNanos;
1527    use nautilus_model::identifiers::AccountId;
1528    use rstest::rstest;
1529    use rust_decimal::Decimal;
1530    use rust_decimal_macros::dec;
1531
1532    use super::*;
1533    use crate::common::testing::load_test_json;
1534
1535    #[rstest]
1536    fn deserialize_spot_instrument_uses_enums() {
1537        let json = load_test_json("http_get_instruments_spot.json");
1538        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1539        let instrument = &response.result.list[0];
1540
1541        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1542        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1543        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1544    }
1545
1546    #[rstest]
1547    fn deserialize_linear_instrument_status() {
1548        let json = load_test_json("http_get_instruments_linear.json");
1549        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1550        let instrument = &response.result.list[0];
1551
1552        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1553        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1554    }
1555
1556    #[rstest]
1557    fn deserialize_order_response_maps_enums() {
1558        let json = load_test_json("http_get_orders_history.json");
1559        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1560        let order = &response.result.list[0];
1561
1562        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1563        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1564        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1565        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1566        assert_eq!(order.order_type, BybitOrderType::Limit);
1567    }
1568
1569    #[rstest]
1570    fn deserialize_wallet_balance_without_optional_fields() {
1571        let json = r#"{
1572            "retCode": 0,
1573            "retMsg": "OK",
1574            "result": {
1575                "list": [{
1576                    "totalEquity": "1000.00",
1577                    "accountIMRate": "0",
1578                    "totalMarginBalance": "1000.00",
1579                    "totalInitialMargin": "0",
1580                    "accountType": "UNIFIED",
1581                    "totalAvailableBalance": "1000.00",
1582                    "accountMMRate": "0",
1583                    "totalPerpUPL": "0",
1584                    "totalWalletBalance": "1000.00",
1585                    "accountLTV": "0",
1586                    "totalMaintenanceMargin": "0",
1587                    "coin": [{
1588                        "availableToBorrow": "0",
1589                        "bonus": "0",
1590                        "accruedInterest": "0",
1591                        "availableToWithdraw": "1000.00",
1592                        "equity": "1000.00",
1593                        "usdValue": "1000.00",
1594                        "borrowAmount": "0",
1595                        "totalPositionIM": "0",
1596                        "walletBalance": "1000.00",
1597                        "unrealisedPnl": "0",
1598                        "cumRealisedPnl": "0",
1599                        "locked": "0",
1600                        "collateralSwitch": true,
1601                        "marginCollateral": true,
1602                        "coin": "USDT"
1603                    }]
1604                }]
1605            }
1606        }"#;
1607
1608        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1609            .expect("Failed to parse wallet balance without optional fields");
1610
1611        assert_eq!(response.ret_code, 0);
1612        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
1613        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
1614    }
1615
1616    #[rstest]
1617    fn deserialize_wallet_balance_from_docs() {
1618        let json = include_str!("../../test_data/http_get_wallet_balance.json");
1619
1620        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1621            .expect("Failed to parse wallet balance from Bybit docs example");
1622
1623        assert_eq!(response.ret_code, 0);
1624        assert_eq!(response.ret_msg, "OK");
1625
1626        let wallet = &response.result.list[0];
1627        assert_eq!(wallet.total_equity, "3.31216591");
1628        assert_eq!(wallet.account_im_rate, "0");
1629        assert_eq!(wallet.account_mm_rate, "0");
1630        assert_eq!(wallet.total_perp_upl, "0");
1631        assert_eq!(wallet.account_ltv, "0");
1632
1633        // Check BTC coin
1634        let btc = &wallet.coin[0];
1635        assert_eq!(btc.coin.as_str(), "BTC");
1636        assert_eq!(btc.available_to_borrow, "3");
1637        assert_eq!(btc.total_order_im, Some("0".to_string()));
1638        assert_eq!(btc.total_position_mm, Some("0".to_string()));
1639        assert_eq!(btc.total_position_im, Some("0".to_string()));
1640
1641        // Check USDT coin (without optional IM/MM fields)
1642        let usdt = &wallet.coin[1];
1643        assert_eq!(usdt.coin.as_str(), "USDT");
1644        assert_eq!(usdt.wallet_balance, dec!(1000.50));
1645        assert_eq!(usdt.total_order_im, None);
1646        assert_eq!(usdt.total_position_mm, None);
1647        assert_eq!(usdt.total_position_im, None);
1648        assert_eq!(btc.spot_borrow, Decimal::ZERO);
1649        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
1650    }
1651
1652    #[rstest]
1653    fn test_parse_wallet_balance_with_spot_borrow() {
1654        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
1655        let response: BybitWalletBalanceResponse =
1656            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
1657
1658        let wallet = &response.result.list[0];
1659        let usdt = &wallet.coin[0];
1660
1661        assert_eq!(usdt.coin.as_str(), "USDT");
1662        assert_eq!(usdt.wallet_balance, dec!(1200.00));
1663        assert_eq!(usdt.spot_borrow, dec!(200.00));
1664        assert_eq!(usdt.borrow_amount, "200.00");
1665
1666        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
1667        let account_id = crate::common::parse::parse_account_state(
1668            wallet,
1669            AccountId::new("BYBIT-001"),
1670            UnixNanos::default(),
1671        )
1672        .expect("Failed to parse account state");
1673
1674        let balance = &account_id.balances[0];
1675        assert_eq!(balance.total.as_f64(), 1000.0);
1676    }
1677
1678    #[rstest]
1679    fn test_parse_wallet_balance_spot_short() {
1680        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
1681        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1682            .expect("Failed to parse wallet balance with SHORT SPOT position");
1683
1684        let wallet = &response.result.list[0];
1685        let eth = &wallet.coin[0];
1686
1687        assert_eq!(eth.coin.as_str(), "ETH");
1688        assert_eq!(eth.wallet_balance, dec!(0));
1689        assert_eq!(eth.spot_borrow, dec!(0.06142));
1690        assert_eq!(eth.borrow_amount, "0.06142");
1691
1692        let account_state = crate::common::parse::parse_account_state(
1693            wallet,
1694            AccountId::new("BYBIT-001"),
1695            UnixNanos::default(),
1696        )
1697        .expect("Failed to parse account state");
1698
1699        let eth_balance = account_state
1700            .balances
1701            .iter()
1702            .find(|b| b.currency.code.as_str() == "ETH")
1703            .expect("ETH balance not found");
1704
1705        // Negative balance represents SHORT position (borrowed ETH)
1706        assert_eq!(eth_balance.total.as_f64(), -0.06142);
1707    }
1708
1709    #[rstest]
1710    fn deserialize_borrow_response() {
1711        let json = r#"{
1712            "retCode": 0,
1713            "retMsg": "success",
1714            "result": {
1715                "coin": "BTC",
1716                "amount": "0.01"
1717            },
1718            "retExtInfo": {},
1719            "time": 1756197991955
1720        }"#;
1721
1722        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
1723
1724        assert_eq!(response.ret_code, 0);
1725        assert_eq!(response.ret_msg, "success");
1726        assert_eq!(response.result.coin, "BTC");
1727        assert_eq!(response.result.amount, "0.01");
1728    }
1729
1730    #[rstest]
1731    fn deserialize_no_convert_repay_response() {
1732        let json = r#"{
1733            "retCode": 0,
1734            "retMsg": "OK",
1735            "result": {
1736                "resultStatus": "SU"
1737            },
1738            "retExtInfo": {},
1739            "time": 1234567890
1740        }"#;
1741
1742        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
1743
1744        assert_eq!(response.ret_code, 0);
1745        assert_eq!(response.ret_msg, "OK");
1746        assert_eq!(response.result.result_status, "SU");
1747    }
1748}