nautilus_bybit/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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.adapters")
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.adapters")
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/// Instrument definition for spot symbols.
523///
524/// # References
525/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
526#[derive(Clone, Debug, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct BybitInstrumentSpot {
529    pub symbol: Ustr,
530    pub base_coin: Ustr,
531    pub quote_coin: Ustr,
532    pub innovation: BybitInnovationFlag,
533    pub status: BybitInstrumentStatus,
534    pub margin_trading: BybitMarginTrading,
535    pub lot_size_filter: SpotLotSizeFilter,
536    pub price_filter: SpotPriceFilter,
537}
538
539/// Instrument definition for linear contracts.
540///
541/// # References
542/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
543#[derive(Clone, Debug, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct BybitInstrumentLinear {
546    pub symbol: Ustr,
547    pub contract_type: BybitContractType,
548    pub status: BybitInstrumentStatus,
549    pub base_coin: Ustr,
550    pub quote_coin: Ustr,
551    pub launch_time: String,
552    pub delivery_time: String,
553    pub delivery_fee_rate: String,
554    pub price_scale: String,
555    pub leverage_filter: LeverageFilter,
556    pub price_filter: LinearPriceFilter,
557    pub lot_size_filter: LinearLotSizeFilter,
558    pub unified_margin_trade: bool,
559    pub funding_interval: i64,
560    pub settle_coin: Ustr,
561}
562
563/// Instrument definition for inverse contracts.
564///
565/// # References
566/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
567#[derive(Clone, Debug, Serialize, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub struct BybitInstrumentInverse {
570    pub symbol: Ustr,
571    pub contract_type: BybitContractType,
572    pub status: BybitInstrumentStatus,
573    pub base_coin: Ustr,
574    pub quote_coin: Ustr,
575    pub launch_time: String,
576    pub delivery_time: String,
577    pub delivery_fee_rate: String,
578    pub price_scale: String,
579    pub leverage_filter: LeverageFilter,
580    pub price_filter: LinearPriceFilter,
581    pub lot_size_filter: LinearLotSizeFilter,
582    pub unified_margin_trade: bool,
583    pub funding_interval: i64,
584    pub settle_coin: Ustr,
585}
586
587/// Instrument definition for option contracts.
588///
589/// # References
590/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
591#[derive(Clone, Debug, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct BybitInstrumentOption {
594    pub symbol: Ustr,
595    pub status: BybitInstrumentStatus,
596    pub base_coin: Ustr,
597    pub quote_coin: Ustr,
598    pub settle_coin: Ustr,
599    pub options_type: BybitOptionType,
600    pub launch_time: String,
601    pub delivery_time: String,
602    pub delivery_fee_rate: String,
603    pub price_filter: LinearPriceFilter,
604    pub lot_size_filter: OptionLotSizeFilter,
605}
606
607/// Response alias for instrument info requests that return spot instruments.
608///
609/// # References
610/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
611pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
612/// Response alias for instrument info requests that return linear contracts.
613///
614/// # References
615/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
616pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
617/// Response alias for instrument info requests that return inverse contracts.
618///
619/// # References
620/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
621pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
622/// Response alias for instrument info requests that return option contracts.
623///
624/// # References
625/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
626pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
627
628/// Fee rate structure returned by `GET /v5/account/fee-rate`.
629///
630/// # References
631/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
632#[derive(Clone, Debug, Serialize, Deserialize)]
633#[serde(rename_all = "camelCase")]
634#[cfg_attr(
635    feature = "python",
636    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
637)]
638pub struct BybitFeeRate {
639    pub symbol: Ustr,
640    pub taker_fee_rate: String,
641    pub maker_fee_rate: String,
642    #[serde(default)]
643    pub base_coin: Option<Ustr>,
644}
645
646#[cfg(feature = "python")]
647#[pyo3::pymethods]
648impl BybitFeeRate {
649    #[getter]
650    #[must_use]
651    pub fn symbol(&self) -> &str {
652        self.symbol.as_str()
653    }
654
655    #[getter]
656    #[must_use]
657    pub fn taker_fee_rate(&self) -> &str {
658        &self.taker_fee_rate
659    }
660
661    #[getter]
662    #[must_use]
663    pub fn maker_fee_rate(&self) -> &str {
664        &self.maker_fee_rate
665    }
666
667    #[getter]
668    #[must_use]
669    pub fn base_coin(&self) -> Option<&str> {
670        self.base_coin.as_ref().map(|u| u.as_str())
671    }
672}
673
674/// Response alias for fee rate requests.
675///
676/// # References
677/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
678pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
679
680/// Account balance snapshot coin entry.
681///
682/// # References
683/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
684#[derive(Clone, Debug, Serialize, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct BybitCoinBalance {
687    pub available_to_borrow: String,
688    pub bonus: String,
689    pub accrued_interest: String,
690    pub available_to_withdraw: String,
691    #[serde(default, rename = "totalOrderIM")]
692    pub total_order_im: Option<String>,
693    pub equity: String,
694    pub usd_value: String,
695    pub borrow_amount: String,
696    #[serde(default, rename = "totalPositionMM")]
697    pub total_position_mm: Option<String>,
698    #[serde(default, rename = "totalPositionIM")]
699    pub total_position_im: Option<String>,
700    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
701    pub wallet_balance: Decimal,
702    pub unrealised_pnl: String,
703    pub cum_realised_pnl: String,
704    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
705    pub locked: Decimal,
706    pub collateral_switch: bool,
707    pub margin_collateral: bool,
708    pub coin: Ustr,
709    #[serde(default)]
710    pub spot_hedging_qty: Option<String>,
711    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
712    pub spot_borrow: Decimal,
713}
714
715/// Wallet balance snapshot containing per-coin balances.
716///
717/// # References
718/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
719#[derive(Clone, Debug, Serialize, Deserialize)]
720#[serde(rename_all = "camelCase")]
721pub struct BybitWalletBalance {
722    pub total_equity: String,
723    #[serde(rename = "accountIMRate")]
724    pub account_im_rate: String,
725    pub total_margin_balance: String,
726    pub total_initial_margin: String,
727    pub account_type: BybitAccountType,
728    pub total_available_balance: String,
729    #[serde(rename = "accountMMRate")]
730    pub account_mm_rate: String,
731    #[serde(rename = "totalPerpUPL")]
732    pub total_perp_upl: String,
733    pub total_wallet_balance: String,
734    #[serde(rename = "accountLTV")]
735    pub account_ltv: String,
736    pub total_maintenance_margin: String,
737    pub coin: Vec<BybitCoinBalance>,
738}
739
740/// Response alias for wallet balance requests.
741///
742/// # References
743/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
744pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
745
746/// Order representation as returned by order-related endpoints.
747///
748/// # References
749/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
750#[derive(Clone, Debug, Serialize, Deserialize)]
751#[cfg_attr(
752    feature = "python",
753    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
754)]
755#[serde(rename_all = "camelCase")]
756pub struct BybitOrder {
757    pub order_id: Ustr,
758    pub order_link_id: Ustr,
759    pub block_trade_id: Option<Ustr>,
760    pub symbol: Ustr,
761    pub price: String,
762    pub qty: String,
763    pub side: BybitOrderSide,
764    pub is_leverage: String,
765    pub position_idx: i32,
766    pub order_status: BybitOrderStatus,
767    pub cancel_type: BybitCancelType,
768    pub reject_reason: Ustr,
769    pub avg_price: Option<String>,
770    pub leaves_qty: String,
771    pub leaves_value: String,
772    pub cum_exec_qty: String,
773    pub cum_exec_value: String,
774    pub cum_exec_fee: String,
775    pub time_in_force: BybitTimeInForce,
776    pub order_type: BybitOrderType,
777    pub stop_order_type: BybitStopOrderType,
778    pub order_iv: Option<String>,
779    pub trigger_price: String,
780    pub take_profit: String,
781    pub stop_loss: String,
782    pub tp_trigger_by: BybitTriggerType,
783    pub sl_trigger_by: BybitTriggerType,
784    pub trigger_direction: BybitTriggerDirection,
785    pub trigger_by: BybitTriggerType,
786    pub last_price_on_created: String,
787    pub reduce_only: bool,
788    pub close_on_trigger: bool,
789    pub smp_type: Ustr,
790    pub smp_group: i32,
791    pub smp_order_id: Ustr,
792    pub tpsl_mode: Option<BybitTpSlMode>,
793    pub tp_limit_price: String,
794    pub sl_limit_price: String,
795    pub place_type: Ustr,
796    pub created_time: String,
797    pub updated_time: String,
798}
799
800#[cfg(feature = "python")]
801#[pyo3::pymethods]
802impl BybitOrder {
803    #[getter]
804    #[must_use]
805    pub fn order_id(&self) -> &str {
806        self.order_id.as_str()
807    }
808
809    #[getter]
810    #[must_use]
811    pub fn order_link_id(&self) -> &str {
812        self.order_link_id.as_str()
813    }
814
815    #[getter]
816    #[must_use]
817    pub fn block_trade_id(&self) -> Option<&str> {
818        self.block_trade_id.as_ref().map(|s| s.as_str())
819    }
820
821    #[getter]
822    #[must_use]
823    pub fn symbol(&self) -> &str {
824        self.symbol.as_str()
825    }
826
827    #[getter]
828    #[must_use]
829    pub fn price(&self) -> &str {
830        &self.price
831    }
832
833    #[getter]
834    #[must_use]
835    pub fn qty(&self) -> &str {
836        &self.qty
837    }
838
839    #[getter]
840    #[must_use]
841    pub fn side(&self) -> BybitOrderSide {
842        self.side
843    }
844
845    #[getter]
846    #[must_use]
847    pub fn is_leverage(&self) -> &str {
848        &self.is_leverage
849    }
850
851    #[getter]
852    #[must_use]
853    pub fn position_idx(&self) -> i32 {
854        self.position_idx
855    }
856
857    #[getter]
858    #[must_use]
859    pub fn order_status(&self) -> BybitOrderStatus {
860        self.order_status
861    }
862
863    #[getter]
864    #[must_use]
865    pub fn cancel_type(&self) -> BybitCancelType {
866        self.cancel_type
867    }
868
869    #[getter]
870    #[must_use]
871    pub fn reject_reason(&self) -> &str {
872        self.reject_reason.as_str()
873    }
874
875    #[getter]
876    #[must_use]
877    pub fn avg_price(&self) -> Option<&str> {
878        self.avg_price.as_deref()
879    }
880
881    #[getter]
882    #[must_use]
883    pub fn leaves_qty(&self) -> &str {
884        &self.leaves_qty
885    }
886
887    #[getter]
888    #[must_use]
889    pub fn leaves_value(&self) -> &str {
890        &self.leaves_value
891    }
892
893    #[getter]
894    #[must_use]
895    pub fn cum_exec_qty(&self) -> &str {
896        &self.cum_exec_qty
897    }
898
899    #[getter]
900    #[must_use]
901    pub fn cum_exec_value(&self) -> &str {
902        &self.cum_exec_value
903    }
904
905    #[getter]
906    #[must_use]
907    pub fn cum_exec_fee(&self) -> &str {
908        &self.cum_exec_fee
909    }
910
911    #[getter]
912    #[must_use]
913    pub fn time_in_force(&self) -> BybitTimeInForce {
914        self.time_in_force
915    }
916
917    #[getter]
918    #[must_use]
919    pub fn order_type(&self) -> BybitOrderType {
920        self.order_type
921    }
922
923    #[getter]
924    #[must_use]
925    pub fn stop_order_type(&self) -> BybitStopOrderType {
926        self.stop_order_type
927    }
928
929    #[getter]
930    #[must_use]
931    pub fn order_iv(&self) -> Option<&str> {
932        self.order_iv.as_deref()
933    }
934
935    #[getter]
936    #[must_use]
937    pub fn trigger_price(&self) -> &str {
938        &self.trigger_price
939    }
940
941    #[getter]
942    #[must_use]
943    pub fn take_profit(&self) -> &str {
944        &self.take_profit
945    }
946
947    #[getter]
948    #[must_use]
949    pub fn stop_loss(&self) -> &str {
950        &self.stop_loss
951    }
952
953    #[getter]
954    #[must_use]
955    pub fn tp_trigger_by(&self) -> BybitTriggerType {
956        self.tp_trigger_by
957    }
958
959    #[getter]
960    #[must_use]
961    pub fn sl_trigger_by(&self) -> BybitTriggerType {
962        self.sl_trigger_by
963    }
964
965    #[getter]
966    #[must_use]
967    pub fn trigger_direction(&self) -> BybitTriggerDirection {
968        self.trigger_direction
969    }
970
971    #[getter]
972    #[must_use]
973    pub fn trigger_by(&self) -> BybitTriggerType {
974        self.trigger_by
975    }
976
977    #[getter]
978    #[must_use]
979    pub fn last_price_on_created(&self) -> &str {
980        &self.last_price_on_created
981    }
982
983    #[getter]
984    #[must_use]
985    pub fn reduce_only(&self) -> bool {
986        self.reduce_only
987    }
988
989    #[getter]
990    #[must_use]
991    pub fn close_on_trigger(&self) -> bool {
992        self.close_on_trigger
993    }
994
995    #[getter]
996    #[must_use]
997    pub fn smp_type(&self) -> &str {
998        self.smp_type.as_str()
999    }
1000
1001    #[getter]
1002    #[must_use]
1003    pub fn smp_group(&self) -> i32 {
1004        self.smp_group
1005    }
1006
1007    #[getter]
1008    #[must_use]
1009    pub fn smp_order_id(&self) -> &str {
1010        self.smp_order_id.as_str()
1011    }
1012
1013    #[getter]
1014    #[must_use]
1015    pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1016        self.tpsl_mode
1017    }
1018
1019    #[getter]
1020    #[must_use]
1021    pub fn tp_limit_price(&self) -> &str {
1022        &self.tp_limit_price
1023    }
1024
1025    #[getter]
1026    #[must_use]
1027    pub fn sl_limit_price(&self) -> &str {
1028        &self.sl_limit_price
1029    }
1030
1031    #[getter]
1032    #[must_use]
1033    pub fn place_type(&self) -> &str {
1034        self.place_type.as_str()
1035    }
1036
1037    #[getter]
1038    #[must_use]
1039    pub fn created_time(&self) -> &str {
1040        &self.created_time
1041    }
1042
1043    #[getter]
1044    #[must_use]
1045    pub fn updated_time(&self) -> &str {
1046        &self.updated_time
1047    }
1048}
1049
1050/// Response alias for open order queries.
1051///
1052/// # References
1053/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1054pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1055/// Response alias for order history queries with pagination.
1056///
1057/// # References
1058/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1059pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1060
1061/// Payload returned after placing a single order.
1062///
1063/// # References
1064/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1065#[derive(Clone, Debug, Serialize, Deserialize)]
1066#[serde(rename_all = "camelCase")]
1067pub struct BybitPlaceOrderResult {
1068    pub order_id: Option<Ustr>,
1069    pub order_link_id: Option<Ustr>,
1070}
1071
1072/// Response alias for order placement endpoints.
1073///
1074/// # References
1075/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1076pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1077
1078/// Payload returned after cancelling a single order.
1079///
1080/// # References
1081/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1082#[derive(Clone, Debug, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase")]
1084pub struct BybitCancelOrderResult {
1085    pub order_id: Option<Ustr>,
1086    pub order_link_id: Option<Ustr>,
1087}
1088
1089/// Response alias for order cancellation endpoints.
1090///
1091/// # References
1092/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1093pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1094
1095/// Execution/Fill payload returned by `GET /v5/execution/list`.
1096///
1097/// # References
1098/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1099#[derive(Clone, Debug, Serialize, Deserialize)]
1100#[serde(rename_all = "camelCase")]
1101pub struct BybitExecution {
1102    pub symbol: Ustr,
1103    pub order_id: Ustr,
1104    pub order_link_id: Ustr,
1105    pub side: BybitOrderSide,
1106    pub order_price: String,
1107    pub order_qty: String,
1108    pub leaves_qty: String,
1109    pub create_type: Option<String>,
1110    pub order_type: BybitOrderType,
1111    pub stop_order_type: Option<BybitStopOrderType>,
1112    pub exec_fee: String,
1113    pub exec_id: String,
1114    pub exec_price: String,
1115    pub exec_qty: String,
1116    pub exec_type: BybitExecType,
1117    pub exec_value: String,
1118    pub exec_time: String,
1119    pub fee_currency: Ustr,
1120    pub is_maker: bool,
1121    pub fee_rate: String,
1122    pub trade_iv: String,
1123    pub mark_iv: String,
1124    pub mark_price: String,
1125    pub index_price: String,
1126    pub underlying_price: String,
1127    pub block_trade_id: String,
1128    pub closed_size: String,
1129    pub seq: i64,
1130}
1131
1132/// Response alias for trade history requests.
1133///
1134/// # References
1135/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1136pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1137
1138/// Represents a position returned by the Bybit API.
1139///
1140/// # References
1141/// - <https://bybit-exchange.github.io/docs/v5/position>
1142#[derive(Clone, Debug, Serialize, Deserialize)]
1143#[serde(rename_all = "camelCase")]
1144pub struct BybitPosition {
1145    pub position_idx: BybitPositionIdx,
1146    pub risk_id: i32,
1147    pub risk_limit_value: String,
1148    pub symbol: Ustr,
1149    pub side: BybitPositionSide,
1150    pub size: String,
1151    pub avg_price: String,
1152    pub position_value: String,
1153    pub trade_mode: i32,
1154    pub position_status: String,
1155    pub auto_add_margin: i32,
1156    pub adl_rank_indicator: i32,
1157    pub leverage: String,
1158    pub position_balance: String,
1159    pub mark_price: String,
1160    pub liq_price: String,
1161    pub bust_price: String,
1162    #[serde(rename = "positionMM")]
1163    pub position_mm: String,
1164    #[serde(rename = "positionIM")]
1165    pub position_im: String,
1166    pub tpsl_mode: String,
1167    pub take_profit: String,
1168    pub stop_loss: String,
1169    pub trailing_stop: String,
1170    pub unrealised_pnl: String,
1171    pub cur_realised_pnl: String,
1172    pub cum_realised_pnl: String,
1173    pub seq: i64,
1174    pub is_reduce_only: bool,
1175    pub mmr_sys_updated_time: String,
1176    pub leverage_sys_updated_time: String,
1177    pub created_time: String,
1178    pub updated_time: String,
1179}
1180
1181/// Response alias for position list requests.
1182///
1183/// # References
1184/// - <https://bybit-exchange.github.io/docs/v5/position>
1185pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1186
1187/// Reason detail for set margin mode failures.
1188///
1189/// # References
1190/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1191#[derive(Clone, Debug, Serialize, Deserialize)]
1192#[serde(rename_all = "camelCase")]
1193pub struct BybitSetMarginModeReason {
1194    pub reason_code: String,
1195    pub reason_msg: String,
1196}
1197
1198/// Result payload for set margin mode operation.
1199///
1200/// # References
1201/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1202#[derive(Clone, Debug, Serialize, Deserialize)]
1203#[serde(rename_all = "camelCase")]
1204pub struct BybitSetMarginModeResult {
1205    #[serde(default)]
1206    pub reasons: Vec<BybitSetMarginModeReason>,
1207}
1208
1209/// Response alias for set margin mode requests.
1210///
1211/// # References
1212/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1213pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1214
1215/// Empty result for set leverage operation.
1216#[derive(Clone, Debug, Serialize, Deserialize)]
1217pub struct BybitSetLeverageResult {}
1218
1219/// Response alias for set leverage requests.
1220///
1221/// # References
1222/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1223pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1224
1225/// Empty result for switch mode operation.
1226#[derive(Clone, Debug, Serialize, Deserialize)]
1227pub struct BybitSwitchModeResult {}
1228
1229/// Response alias for switch mode requests.
1230///
1231/// # References
1232/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1233pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1234
1235/// Empty result for set trading stop operation.
1236#[derive(Clone, Debug, Serialize, Deserialize)]
1237pub struct BybitSetTradingStopResult {}
1238
1239/// Response alias for set trading stop requests.
1240///
1241/// # References
1242/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1243pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1244
1245/// Result from manual borrow operation.
1246#[derive(Clone, Debug, Serialize, Deserialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct BybitBorrowResult {
1249    pub coin: String,
1250    pub amount: String,
1251}
1252
1253/// Response alias for manual borrow requests.
1254///
1255/// # References
1256///
1257/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1258pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1259
1260/// Result from no-convert repay operation.
1261#[derive(Clone, Debug, Serialize, Deserialize)]
1262#[serde(rename_all = "camelCase")]
1263pub struct BybitNoConvertRepayResult {
1264    pub result_status: String,
1265}
1266
1267/// Response alias for no-convert repay requests.
1268///
1269/// # References
1270///
1271/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1272pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1273
1274/// API key permissions.
1275#[derive(Clone, Debug, Serialize, Deserialize)]
1276#[cfg_attr(
1277    feature = "python",
1278    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
1279)]
1280#[serde(rename_all = "PascalCase")]
1281pub struct BybitApiKeyPermissions {
1282    #[serde(default)]
1283    pub contract_trade: Vec<String>,
1284    #[serde(default)]
1285    pub spot: Vec<String>,
1286    #[serde(default)]
1287    pub wallet: Vec<String>,
1288    #[serde(default)]
1289    pub options: Vec<String>,
1290    #[serde(default)]
1291    pub derivatives: Vec<String>,
1292    #[serde(default)]
1293    pub exchange: Vec<String>,
1294    #[serde(default)]
1295    pub copy_trading: Vec<String>,
1296    #[serde(default)]
1297    pub block_trade: Vec<String>,
1298    #[serde(default)]
1299    pub nft: Vec<String>,
1300    #[serde(default)]
1301    pub affiliate: Vec<String>,
1302}
1303
1304/// Account details from API key info.
1305#[derive(Clone, Debug, Serialize, Deserialize)]
1306#[cfg_attr(
1307    feature = "python",
1308    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
1309)]
1310#[serde(rename_all = "camelCase")]
1311pub struct BybitAccountDetails {
1312    pub id: String,
1313    pub note: String,
1314    pub api_key: String,
1315    pub read_only: u8,
1316    pub secret: String,
1317    #[serde(rename = "type")]
1318    pub key_type: u8,
1319    pub permissions: BybitApiKeyPermissions,
1320    pub ips: Vec<String>,
1321    #[serde(default)]
1322    pub user_id: Option<u64>,
1323    #[serde(default)]
1324    pub inviter_id: Option<u64>,
1325    pub vip_level: String,
1326    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1327    pub mkt_maker_level: u8,
1328    #[serde(default)]
1329    pub affiliate_id: Option<u64>,
1330    pub rsa_public_key: String,
1331    pub is_master: bool,
1332    pub parent_uid: String,
1333    pub uta: u8,
1334    pub kyc_level: String,
1335    pub kyc_region: String,
1336    #[serde(default)]
1337    pub deadline_day: i64,
1338    #[serde(default)]
1339    pub expired_at: Option<String>,
1340    pub created_at: String,
1341}
1342
1343#[cfg(feature = "python")]
1344#[pyo3::pymethods]
1345impl BybitAccountDetails {
1346    #[getter]
1347    #[must_use]
1348    pub fn id(&self) -> &str {
1349        &self.id
1350    }
1351
1352    #[getter]
1353    #[must_use]
1354    pub fn note(&self) -> &str {
1355        &self.note
1356    }
1357
1358    #[getter]
1359    #[must_use]
1360    pub fn api_key(&self) -> &str {
1361        &self.api_key
1362    }
1363
1364    #[getter]
1365    #[must_use]
1366    pub fn read_only(&self) -> u8 {
1367        self.read_only
1368    }
1369
1370    #[getter]
1371    #[must_use]
1372    pub fn key_type(&self) -> u8 {
1373        self.key_type
1374    }
1375
1376    #[getter]
1377    #[must_use]
1378    pub fn user_id(&self) -> Option<u64> {
1379        self.user_id
1380    }
1381
1382    #[getter]
1383    #[must_use]
1384    pub fn inviter_id(&self) -> Option<u64> {
1385        self.inviter_id
1386    }
1387
1388    #[getter]
1389    #[must_use]
1390    pub fn vip_level(&self) -> &str {
1391        &self.vip_level
1392    }
1393
1394    #[getter]
1395    #[must_use]
1396    pub fn mkt_maker_level(&self) -> u8 {
1397        self.mkt_maker_level
1398    }
1399
1400    #[getter]
1401    #[must_use]
1402    pub fn affiliate_id(&self) -> Option<u64> {
1403        self.affiliate_id
1404    }
1405
1406    #[getter]
1407    #[must_use]
1408    pub fn rsa_public_key(&self) -> &str {
1409        &self.rsa_public_key
1410    }
1411
1412    #[getter]
1413    #[must_use]
1414    pub fn is_master(&self) -> bool {
1415        self.is_master
1416    }
1417
1418    #[getter]
1419    #[must_use]
1420    pub fn parent_uid(&self) -> &str {
1421        &self.parent_uid
1422    }
1423
1424    #[getter]
1425    #[must_use]
1426    pub fn uta(&self) -> u8 {
1427        self.uta
1428    }
1429
1430    #[getter]
1431    #[must_use]
1432    pub fn kyc_level(&self) -> &str {
1433        &self.kyc_level
1434    }
1435
1436    #[getter]
1437    #[must_use]
1438    pub fn kyc_region(&self) -> &str {
1439        &self.kyc_region
1440    }
1441
1442    #[getter]
1443    #[must_use]
1444    pub fn deadline_day(&self) -> i64 {
1445        self.deadline_day
1446    }
1447
1448    #[getter]
1449    #[must_use]
1450    pub fn expired_at(&self) -> Option<&str> {
1451        self.expired_at.as_deref()
1452    }
1453
1454    #[getter]
1455    #[must_use]
1456    pub fn created_at(&self) -> &str {
1457        &self.created_at
1458    }
1459}
1460
1461/// Response alias for API key info requests.
1462///
1463/// # References
1464///
1465/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1466pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1467
1468#[cfg(test)]
1469mod tests {
1470    use nautilus_core::UnixNanos;
1471    use nautilus_model::identifiers::AccountId;
1472    use rstest::rstest;
1473    use rust_decimal::Decimal;
1474    use rust_decimal_macros::dec;
1475
1476    use super::*;
1477    use crate::common::testing::load_test_json;
1478
1479    #[rstest]
1480    fn deserialize_spot_instrument_uses_enums() {
1481        let json = load_test_json("http_get_instruments_spot.json");
1482        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1483        let instrument = &response.result.list[0];
1484
1485        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1486        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1487        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1488    }
1489
1490    #[rstest]
1491    fn deserialize_linear_instrument_status() {
1492        let json = load_test_json("http_get_instruments_linear.json");
1493        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1494        let instrument = &response.result.list[0];
1495
1496        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1497        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1498    }
1499
1500    #[rstest]
1501    fn deserialize_order_response_maps_enums() {
1502        let json = load_test_json("http_get_orders_history.json");
1503        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1504        let order = &response.result.list[0];
1505
1506        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1507        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1508        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1509        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1510        assert_eq!(order.order_type, BybitOrderType::Limit);
1511    }
1512
1513    #[rstest]
1514    fn deserialize_wallet_balance_without_optional_fields() {
1515        let json = r#"{
1516            "retCode": 0,
1517            "retMsg": "OK",
1518            "result": {
1519                "list": [{
1520                    "totalEquity": "1000.00",
1521                    "accountIMRate": "0",
1522                    "totalMarginBalance": "1000.00",
1523                    "totalInitialMargin": "0",
1524                    "accountType": "UNIFIED",
1525                    "totalAvailableBalance": "1000.00",
1526                    "accountMMRate": "0",
1527                    "totalPerpUPL": "0",
1528                    "totalWalletBalance": "1000.00",
1529                    "accountLTV": "0",
1530                    "totalMaintenanceMargin": "0",
1531                    "coin": [{
1532                        "availableToBorrow": "0",
1533                        "bonus": "0",
1534                        "accruedInterest": "0",
1535                        "availableToWithdraw": "1000.00",
1536                        "equity": "1000.00",
1537                        "usdValue": "1000.00",
1538                        "borrowAmount": "0",
1539                        "totalPositionIM": "0",
1540                        "walletBalance": "1000.00",
1541                        "unrealisedPnl": "0",
1542                        "cumRealisedPnl": "0",
1543                        "locked": "0",
1544                        "collateralSwitch": true,
1545                        "marginCollateral": true,
1546                        "coin": "USDT"
1547                    }]
1548                }]
1549            }
1550        }"#;
1551
1552        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1553            .expect("Failed to parse wallet balance without optional fields");
1554
1555        assert_eq!(response.ret_code, 0);
1556        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
1557        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
1558    }
1559
1560    #[rstest]
1561    fn deserialize_wallet_balance_from_docs() {
1562        let json = include_str!("../../test_data/http_get_wallet_balance.json");
1563
1564        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1565            .expect("Failed to parse wallet balance from Bybit docs example");
1566
1567        assert_eq!(response.ret_code, 0);
1568        assert_eq!(response.ret_msg, "OK");
1569
1570        let wallet = &response.result.list[0];
1571        assert_eq!(wallet.total_equity, "3.31216591");
1572        assert_eq!(wallet.account_im_rate, "0");
1573        assert_eq!(wallet.account_mm_rate, "0");
1574        assert_eq!(wallet.total_perp_upl, "0");
1575        assert_eq!(wallet.account_ltv, "0");
1576
1577        // Check BTC coin
1578        let btc = &wallet.coin[0];
1579        assert_eq!(btc.coin.as_str(), "BTC");
1580        assert_eq!(btc.available_to_borrow, "3");
1581        assert_eq!(btc.total_order_im, Some("0".to_string()));
1582        assert_eq!(btc.total_position_mm, Some("0".to_string()));
1583        assert_eq!(btc.total_position_im, Some("0".to_string()));
1584
1585        // Check USDT coin (without optional IM/MM fields)
1586        let usdt = &wallet.coin[1];
1587        assert_eq!(usdt.coin.as_str(), "USDT");
1588        assert_eq!(usdt.wallet_balance, dec!(1000.50));
1589        assert_eq!(usdt.total_order_im, None);
1590        assert_eq!(usdt.total_position_mm, None);
1591        assert_eq!(usdt.total_position_im, None);
1592        assert_eq!(btc.spot_borrow, Decimal::ZERO);
1593        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
1594    }
1595
1596    #[rstest]
1597    fn test_parse_wallet_balance_with_spot_borrow() {
1598        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
1599        let response: BybitWalletBalanceResponse =
1600            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
1601
1602        let wallet = &response.result.list[0];
1603        let usdt = &wallet.coin[0];
1604
1605        assert_eq!(usdt.coin.as_str(), "USDT");
1606        assert_eq!(usdt.wallet_balance, dec!(1200.00));
1607        assert_eq!(usdt.spot_borrow, dec!(200.00));
1608        assert_eq!(usdt.borrow_amount, "200.00");
1609
1610        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
1611        let account_id = crate::common::parse::parse_account_state(
1612            wallet,
1613            AccountId::new("BYBIT-001"),
1614            UnixNanos::default(),
1615        )
1616        .expect("Failed to parse account state");
1617
1618        let balance = &account_id.balances[0];
1619        assert_eq!(balance.total.as_f64(), 1000.0);
1620    }
1621
1622    #[rstest]
1623    fn test_parse_wallet_balance_spot_short() {
1624        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
1625        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1626            .expect("Failed to parse wallet balance with SHORT SPOT position");
1627
1628        let wallet = &response.result.list[0];
1629        let eth = &wallet.coin[0];
1630
1631        assert_eq!(eth.coin.as_str(), "ETH");
1632        assert_eq!(eth.wallet_balance, dec!(0));
1633        assert_eq!(eth.spot_borrow, dec!(0.06142));
1634        assert_eq!(eth.borrow_amount, "0.06142");
1635
1636        let account_state = crate::common::parse::parse_account_state(
1637            wallet,
1638            AccountId::new("BYBIT-001"),
1639            UnixNanos::default(),
1640        )
1641        .expect("Failed to parse account state");
1642
1643        let eth_balance = account_state
1644            .balances
1645            .iter()
1646            .find(|b| b.currency.code.as_str() == "ETH")
1647            .expect("ETH balance not found");
1648
1649        // Negative balance represents SHORT position (borrowed ETH)
1650        assert_eq!(eth_balance.total.as_f64(), -0.06142);
1651    }
1652
1653    #[rstest]
1654    fn deserialize_borrow_response() {
1655        let json = r#"{
1656            "retCode": 0,
1657            "retMsg": "success",
1658            "result": {
1659                "coin": "BTC",
1660                "amount": "0.01"
1661            },
1662            "retExtInfo": {},
1663            "time": 1756197991955
1664        }"#;
1665
1666        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
1667
1668        assert_eq!(response.ret_code, 0);
1669        assert_eq!(response.ret_msg, "success");
1670        assert_eq!(response.result.coin, "BTC");
1671        assert_eq!(response.result.amount, "0.01");
1672    }
1673
1674    #[rstest]
1675    fn deserialize_no_convert_repay_response() {
1676        let json = r#"{
1677            "retCode": 0,
1678            "retMsg": "OK",
1679            "result": {
1680                "resultStatus": "SU"
1681            },
1682            "retExtInfo": {},
1683            "time": 1234567890
1684        }"#;
1685
1686        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
1687
1688        assert_eq!(response.ret_code, 0);
1689        assert_eq!(response.ret_msg, "OK");
1690        assert_eq!(response.result.result_status, "SU");
1691    }
1692}