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 serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::{
22    enums::{
23        BybitAccountType, BybitCancelType, BybitContractType, BybitExecType, BybitInnovationFlag,
24        BybitInstrumentStatus, BybitMarginTrading, BybitOptionType, BybitOrderSide,
25        BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide, BybitProductType,
26        BybitStopOrderType, BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection,
27        BybitTriggerType,
28    },
29    models::{
30        BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
31        LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
32        SpotPriceFilter,
33    },
34};
35
36/// Response payload returned by `GET /v5/market/server-time`.
37///
38/// # References
39/// - <https://bybit-exchange.github.io/docs/v5/market/server-time>
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct BybitServerTime {
43    /// Server timestamp in seconds represented as string.
44    pub time_second: String,
45    /// Server timestamp in nanoseconds represented as string.
46    pub time_nano: String,
47}
48
49/// Type alias for the server time response envelope.
50///
51/// # References
52/// - <https://bybit-exchange.github.io/docs/v5/market/server-time>
53pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
54
55/// Ticker payload for spot instruments.
56///
57/// # References
58/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
59#[derive(Clone, Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct BybitTickerSpot {
62    pub symbol: Ustr,
63    pub bid1_price: String,
64    pub bid1_size: String,
65    pub ask1_price: String,
66    pub ask1_size: String,
67    pub last_price: String,
68    pub prev_price24h: String,
69    pub price24h_pcnt: String,
70    pub high_price24h: String,
71    pub low_price24h: String,
72    pub turnover24h: String,
73    pub volume24h: String,
74    pub usd_index_price: String,
75}
76
77/// Ticker payload for linear and inverse perpetual/futures instruments.
78///
79/// # References
80/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
81#[derive(Clone, Debug, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct BybitTickerLinear {
84    pub symbol: Ustr,
85    pub last_price: String,
86    pub index_price: String,
87    pub mark_price: String,
88    pub prev_price24h: String,
89    pub price24h_pcnt: String,
90    pub high_price24h: String,
91    pub low_price24h: String,
92    pub prev_price1h: String,
93    pub open_interest: String,
94    pub open_interest_value: String,
95    pub turnover24h: String,
96    pub volume24h: String,
97    pub funding_rate: String,
98    pub next_funding_time: String,
99    pub predicted_delivery_price: String,
100    pub basis_rate: String,
101    pub delivery_fee_rate: String,
102    pub delivery_time: String,
103    pub ask1_size: String,
104    pub bid1_price: String,
105    pub ask1_price: String,
106    pub bid1_size: String,
107    pub basis: String,
108}
109
110/// Ticker payload for option instruments.
111///
112/// # References
113/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
114#[derive(Clone, Debug, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct BybitTickerOption {
117    pub symbol: Ustr,
118    pub bid1_price: String,
119    pub bid1_size: String,
120    pub bid1_iv: String,
121    pub ask1_price: String,
122    pub ask1_size: String,
123    pub ask1_iv: String,
124    pub last_price: String,
125    pub high_price24h: String,
126    pub low_price24h: String,
127    pub mark_price: String,
128    pub index_price: String,
129    pub mark_iv: String,
130    pub underlying_price: String,
131    pub open_interest: String,
132    pub turnover24h: String,
133    pub volume24h: String,
134    pub total_volume: String,
135    pub total_turnover: String,
136    pub delta: String,
137    pub gamma: String,
138    pub vega: String,
139    pub theta: String,
140    pub predicted_delivery_price: String,
141    pub change24h: String,
142}
143
144/// Response alias for spot ticker requests.
145///
146/// # References
147/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
148pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
149/// Response alias for linear/inverse ticker requests.
150///
151/// # References
152/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
153pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
154/// Response alias for option ticker requests.
155///
156/// # References
157/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
158pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
159
160/// Unified ticker data structure containing common fields across all product types.
161///
162/// This simplified ticker structure is designed to work across SPOT, LINEAR, and OPTION products,
163/// containing only the most commonly used fields.
164#[derive(Clone, Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166#[cfg_attr(
167    feature = "python",
168    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
169)]
170pub struct BybitTickerData {
171    pub symbol: Ustr,
172    pub bid1_price: String,
173    pub bid1_size: String,
174    pub ask1_price: String,
175    pub ask1_size: String,
176    pub last_price: String,
177    pub high_price24h: String,
178    pub low_price24h: String,
179    pub turnover24h: String,
180    pub volume24h: String,
181}
182
183#[cfg(feature = "python")]
184#[pyo3::pymethods]
185impl BybitTickerData {
186    #[getter]
187    #[must_use]
188    pub fn symbol(&self) -> &str {
189        self.symbol.as_str()
190    }
191
192    #[getter]
193    #[must_use]
194    pub fn bid1_price(&self) -> &str {
195        &self.bid1_price
196    }
197
198    #[getter]
199    #[must_use]
200    pub fn bid1_size(&self) -> &str {
201        &self.bid1_size
202    }
203
204    #[getter]
205    #[must_use]
206    pub fn ask1_price(&self) -> &str {
207        &self.ask1_price
208    }
209
210    #[getter]
211    #[must_use]
212    pub fn ask1_size(&self) -> &str {
213        &self.ask1_size
214    }
215
216    #[getter]
217    #[must_use]
218    pub fn last_price(&self) -> &str {
219        &self.last_price
220    }
221
222    #[getter]
223    #[must_use]
224    pub fn high_price24h(&self) -> &str {
225        &self.high_price24h
226    }
227
228    #[getter]
229    #[must_use]
230    pub fn low_price24h(&self) -> &str {
231        &self.low_price24h
232    }
233
234    #[getter]
235    #[must_use]
236    pub fn turnover24h(&self) -> &str {
237        &self.turnover24h
238    }
239
240    #[getter]
241    #[must_use]
242    pub fn volume24h(&self) -> &str {
243        &self.volume24h
244    }
245}
246
247/// Kline/candlestick entry returned by `GET /v5/market/kline`.
248///
249/// Bybit returns klines as arrays with 7 elements:
250/// [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
251///
252/// # References
253/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
254#[derive(Clone, Debug, Serialize)]
255pub struct BybitKline {
256    pub start: String,
257    pub open: String,
258    pub high: String,
259    pub low: String,
260    pub close: String,
261    pub volume: String,
262    pub turnover: String,
263}
264
265impl<'de> Deserialize<'de> for BybitKline {
266    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267    where
268        D: serde::Deserializer<'de>,
269    {
270        let arr: [String; 7] = Deserialize::deserialize(deserializer)?;
271        Ok(Self {
272            start: arr[0].clone(),
273            open: arr[1].clone(),
274            high: arr[2].clone(),
275            low: arr[3].clone(),
276            close: arr[4].clone(),
277            volume: arr[5].clone(),
278            turnover: arr[6].clone(),
279        })
280    }
281}
282
283/// Kline list result returned by Bybit.
284///
285/// # References
286/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
287#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct BybitKlineResult {
290    pub category: BybitProductType,
291    pub symbol: Ustr,
292    pub list: Vec<BybitKline>,
293}
294
295/// Response alias for kline history requests.
296///
297/// # References
298/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
299pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
300
301/// Trade entry returned by `GET /v5/market/recent-trade`.
302///
303/// # References
304/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
305#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct BybitTrade {
308    pub exec_id: String,
309    pub symbol: Ustr,
310    pub price: String,
311    pub size: String,
312    pub side: BybitOrderSide,
313    pub time: String,
314    pub is_block_trade: bool,
315    #[serde(default)]
316    pub m_p: Option<String>,
317    #[serde(default)]
318    pub i_p: Option<String>,
319    #[serde(default)]
320    pub mlv: Option<String>,
321    #[serde(default)]
322    pub iv: Option<String>,
323}
324
325/// Trade list result returned by Bybit.
326///
327/// # References
328/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
329#[derive(Clone, Debug, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct BybitTradeResult {
332    pub category: BybitProductType,
333    pub list: Vec<BybitTrade>,
334}
335
336/// Response alias for recent trades requests.
337///
338/// # References
339/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
340pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
341
342/// Instrument definition for spot symbols.
343///
344/// # References
345/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
346#[derive(Clone, Debug, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct BybitInstrumentSpot {
349    pub symbol: Ustr,
350    pub base_coin: Ustr,
351    pub quote_coin: Ustr,
352    pub innovation: BybitInnovationFlag,
353    pub status: BybitInstrumentStatus,
354    pub margin_trading: BybitMarginTrading,
355    pub lot_size_filter: SpotLotSizeFilter,
356    pub price_filter: SpotPriceFilter,
357}
358
359/// Instrument definition for linear contracts.
360///
361/// # References
362/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
363#[derive(Clone, Debug, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct BybitInstrumentLinear {
366    pub symbol: Ustr,
367    pub contract_type: BybitContractType,
368    pub status: BybitInstrumentStatus,
369    pub base_coin: Ustr,
370    pub quote_coin: Ustr,
371    pub launch_time: String,
372    pub delivery_time: String,
373    pub delivery_fee_rate: String,
374    pub price_scale: String,
375    pub leverage_filter: LeverageFilter,
376    pub price_filter: LinearPriceFilter,
377    pub lot_size_filter: LinearLotSizeFilter,
378    pub unified_margin_trade: bool,
379    pub funding_interval: i64,
380    pub settle_coin: Ustr,
381}
382
383/// Instrument definition for inverse contracts.
384///
385/// # References
386/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
387#[derive(Clone, Debug, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct BybitInstrumentInverse {
390    pub symbol: Ustr,
391    pub contract_type: BybitContractType,
392    pub status: BybitInstrumentStatus,
393    pub base_coin: Ustr,
394    pub quote_coin: Ustr,
395    pub launch_time: String,
396    pub delivery_time: String,
397    pub delivery_fee_rate: String,
398    pub price_scale: String,
399    pub leverage_filter: LeverageFilter,
400    pub price_filter: LinearPriceFilter,
401    pub lot_size_filter: LinearLotSizeFilter,
402    pub unified_margin_trade: bool,
403    pub funding_interval: i64,
404    pub settle_coin: Ustr,
405}
406
407/// Instrument definition for option contracts.
408///
409/// # References
410/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
411#[derive(Clone, Debug, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct BybitInstrumentOption {
414    pub symbol: Ustr,
415    pub status: BybitInstrumentStatus,
416    pub base_coin: Ustr,
417    pub quote_coin: Ustr,
418    pub settle_coin: Ustr,
419    pub options_type: BybitOptionType,
420    pub launch_time: String,
421    pub delivery_time: String,
422    pub delivery_fee_rate: String,
423    pub price_filter: LinearPriceFilter,
424    pub lot_size_filter: OptionLotSizeFilter,
425}
426
427/// Response alias for instrument info requests that return spot instruments.
428///
429/// # References
430/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
431pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
432/// Response alias for instrument info requests that return linear contracts.
433///
434/// # References
435/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
436pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
437/// Response alias for instrument info requests that return inverse contracts.
438///
439/// # References
440/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
441pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
442/// Response alias for instrument info requests that return option contracts.
443///
444/// # References
445/// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
446pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
447
448/// Fee rate structure returned by `GET /v5/account/fee-rate`.
449///
450/// # References
451/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
452#[derive(Clone, Debug, Serialize, Deserialize)]
453#[serde(rename_all = "camelCase")]
454#[cfg_attr(
455    feature = "python",
456    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
457)]
458pub struct BybitFeeRate {
459    pub symbol: Ustr,
460    pub taker_fee_rate: String,
461    pub maker_fee_rate: String,
462    #[serde(default)]
463    pub base_coin: Option<Ustr>,
464}
465
466#[cfg(feature = "python")]
467#[pyo3::pymethods]
468impl BybitFeeRate {
469    #[getter]
470    #[must_use]
471    pub fn symbol(&self) -> &str {
472        self.symbol.as_str()
473    }
474
475    #[getter]
476    #[must_use]
477    pub fn taker_fee_rate(&self) -> &str {
478        &self.taker_fee_rate
479    }
480
481    #[getter]
482    #[must_use]
483    pub fn maker_fee_rate(&self) -> &str {
484        &self.maker_fee_rate
485    }
486
487    #[getter]
488    #[must_use]
489    pub fn base_coin(&self) -> Option<&str> {
490        self.base_coin.as_ref().map(|u| u.as_str())
491    }
492}
493
494/// Response alias for fee rate requests.
495///
496/// # References
497/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
498pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
499
500/// Account balance snapshot coin entry.
501///
502/// # References
503/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
504#[derive(Clone, Debug, Serialize, Deserialize)]
505#[serde(rename_all = "camelCase")]
506pub struct BybitCoinBalance {
507    pub available_to_borrow: String,
508    pub bonus: String,
509    pub accrued_interest: String,
510    pub available_to_withdraw: String,
511    #[serde(default, rename = "totalOrderIM")]
512    pub total_order_im: Option<String>,
513    pub equity: String,
514    pub usd_value: String,
515    pub borrow_amount: String,
516    #[serde(default, rename = "totalPositionMM")]
517    pub total_position_mm: Option<String>,
518    #[serde(default, rename = "totalPositionIM")]
519    pub total_position_im: Option<String>,
520    pub wallet_balance: String,
521    pub unrealised_pnl: String,
522    pub cum_realised_pnl: String,
523    pub locked: String,
524    pub collateral_switch: bool,
525    pub margin_collateral: bool,
526    pub coin: Ustr,
527    #[serde(default)]
528    pub spot_hedging_qty: Option<String>,
529    #[serde(default)]
530    pub spot_borrow: Option<String>,
531}
532
533/// Wallet balance snapshot containing per-coin balances.
534///
535/// # References
536/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
537#[derive(Clone, Debug, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct BybitWalletBalance {
540    pub total_equity: String,
541    #[serde(rename = "accountIMRate")]
542    pub account_im_rate: String,
543    pub total_margin_balance: String,
544    pub total_initial_margin: String,
545    pub account_type: BybitAccountType,
546    pub total_available_balance: String,
547    #[serde(rename = "accountMMRate")]
548    pub account_mm_rate: String,
549    #[serde(rename = "totalPerpUPL")]
550    pub total_perp_upl: String,
551    pub total_wallet_balance: String,
552    #[serde(rename = "accountLTV")]
553    pub account_ltv: String,
554    pub total_maintenance_margin: String,
555    pub coin: Vec<BybitCoinBalance>,
556}
557
558/// Response alias for wallet balance requests.
559///
560/// # References
561/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
562pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
563
564/// Order representation as returned by order-related endpoints.
565///
566/// # References
567/// - <https://bybit-exchange.github.io/docs/v5/order/realtime>
568/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
569#[derive(Clone, Debug, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct BybitOrder {
572    pub order_id: Ustr,
573    pub order_link_id: Ustr,
574    pub block_trade_id: Option<Ustr>,
575    pub symbol: Ustr,
576    pub price: String,
577    pub qty: String,
578    pub side: BybitOrderSide,
579    pub is_leverage: String,
580    pub position_idx: i32,
581    pub order_status: BybitOrderStatus,
582    pub cancel_type: BybitCancelType,
583    pub reject_reason: Ustr,
584    pub avg_price: Option<String>,
585    pub leaves_qty: String,
586    pub leaves_value: String,
587    pub cum_exec_qty: String,
588    pub cum_exec_value: String,
589    pub cum_exec_fee: String,
590    pub time_in_force: BybitTimeInForce,
591    pub order_type: BybitOrderType,
592    pub stop_order_type: BybitStopOrderType,
593    pub order_iv: Option<String>,
594    pub trigger_price: String,
595    pub take_profit: String,
596    pub stop_loss: String,
597    pub tp_trigger_by: BybitTriggerType,
598    pub sl_trigger_by: BybitTriggerType,
599    pub trigger_direction: BybitTriggerDirection,
600    pub trigger_by: BybitTriggerType,
601    pub last_price_on_created: String,
602    pub reduce_only: bool,
603    pub close_on_trigger: bool,
604    pub smp_type: Ustr,
605    pub smp_group: i32,
606    pub smp_order_id: Ustr,
607    pub tpsl_mode: Option<BybitTpSlMode>,
608    pub tp_limit_price: String,
609    pub sl_limit_price: String,
610    pub place_type: Ustr,
611    pub created_time: String,
612    pub updated_time: String,
613}
614
615/// Response alias for open order queries.
616///
617/// # References
618/// - <https://bybit-exchange.github.io/docs/v5/order/realtime>
619pub type BybitOpenOrdersResponse = BybitListResponse<BybitOrder>;
620/// Response alias for order history queries with pagination.
621///
622/// # References
623/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
624pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
625
626/// Payload returned after placing a single order.
627///
628/// # References
629/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
630#[derive(Clone, Debug, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct BybitPlaceOrderResult {
633    pub order_id: Option<Ustr>,
634    pub order_link_id: Option<Ustr>,
635}
636
637/// Response alias for order placement endpoints.
638///
639/// # References
640/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
641pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
642
643/// Payload returned after cancelling a single order.
644///
645/// # References
646/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
647#[derive(Clone, Debug, Serialize, Deserialize)]
648#[serde(rename_all = "camelCase")]
649pub struct BybitCancelOrderResult {
650    pub order_id: Option<Ustr>,
651    pub order_link_id: Option<Ustr>,
652}
653
654/// Response alias for order cancellation endpoints.
655///
656/// # References
657/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
658pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
659
660/// Execution/Fill payload returned by `GET /v5/execution/list`.
661///
662/// # References
663/// - <https://bybit-exchange.github.io/docs/v5/order/execution-list>
664#[derive(Clone, Debug, Serialize, Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct BybitExecution {
667    pub symbol: Ustr,
668    pub order_id: Ustr,
669    pub order_link_id: Ustr,
670    pub side: BybitOrderSide,
671    pub order_price: String,
672    pub order_qty: String,
673    pub leaves_qty: String,
674    pub create_type: Option<String>,
675    pub order_type: BybitOrderType,
676    pub stop_order_type: Option<BybitStopOrderType>,
677    pub exec_fee: String,
678    pub exec_id: String,
679    pub exec_price: String,
680    pub exec_qty: String,
681    pub exec_type: BybitExecType,
682    pub exec_value: String,
683    pub exec_time: String,
684    pub fee_currency: Ustr,
685    pub is_maker: bool,
686    pub fee_rate: String,
687    pub trade_iv: String,
688    pub mark_iv: String,
689    pub mark_price: String,
690    pub index_price: String,
691    pub underlying_price: String,
692    pub block_trade_id: String,
693    pub closed_size: String,
694    pub seq: i64,
695}
696
697/// Response alias for trade history requests.
698///
699/// # References
700/// - <https://bybit-exchange.github.io/docs/v5/order/execution-list>
701pub type BybitTradeHistoryResponse = BybitListResponse<BybitExecution>;
702
703/// Represents a position returned by the Bybit API.
704///
705/// # References
706/// - <https://bybit-exchange.github.io/docs/v5/position/position-info>
707#[derive(Clone, Debug, Serialize, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct BybitPosition {
710    pub position_idx: BybitPositionIdx,
711    pub risk_id: i32,
712    pub risk_limit_value: String,
713    pub symbol: Ustr,
714    pub side: BybitPositionSide,
715    pub size: String,
716    pub avg_price: String,
717    pub position_value: String,
718    pub trade_mode: i32,
719    pub position_status: String,
720    pub auto_add_margin: i32,
721    pub adl_rank_indicator: i32,
722    pub leverage: String,
723    pub position_balance: String,
724    pub mark_price: String,
725    pub liq_price: String,
726    pub bust_price: String,
727    #[serde(rename = "positionMM")]
728    pub position_mm: String,
729    #[serde(rename = "positionIM")]
730    pub position_im: String,
731    pub tpsl_mode: String,
732    pub take_profit: String,
733    pub stop_loss: String,
734    pub trailing_stop: String,
735    pub unrealised_pnl: String,
736    pub cur_realised_pnl: String,
737    pub cum_realised_pnl: String,
738    pub seq: i64,
739    pub is_reduce_only: bool,
740    pub mmr_sys_updated_time: String,
741    pub leverage_sys_updated_time: String,
742    pub created_time: String,
743    pub updated_time: String,
744}
745
746/// Response alias for position list requests.
747///
748/// # References
749/// - <https://bybit-exchange.github.io/docs/v5/position/position-info>
750pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
751
752////////////////////////////////////////////////////////////////////////////////
753// Tests
754////////////////////////////////////////////////////////////////////////////////
755
756#[cfg(test)]
757mod tests {
758    use nautilus_core::UnixNanos;
759    use nautilus_model::identifiers::AccountId;
760    use rstest::rstest;
761
762    use super::*;
763    use crate::common::testing::load_test_json;
764
765    #[rstest]
766    fn deserialize_spot_instrument_uses_enums() {
767        let json = load_test_json("http_get_instruments_spot.json");
768        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
769        let instrument = &response.result.list[0];
770
771        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
772        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
773        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
774    }
775
776    #[rstest]
777    fn deserialize_linear_instrument_status() {
778        let json = load_test_json("http_get_instruments_linear.json");
779        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
780        let instrument = &response.result.list[0];
781
782        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
783        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
784    }
785
786    #[rstest]
787    fn deserialize_order_response_maps_enums() {
788        let json = load_test_json("http_get_orders_history.json");
789        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
790        let order = &response.result.list[0];
791
792        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
793        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
794        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
795        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
796        assert_eq!(order.order_type, BybitOrderType::Limit);
797    }
798
799    #[rstest]
800    fn deserialize_wallet_balance_without_optional_fields() {
801        let json = r#"{
802            "retCode": 0,
803            "retMsg": "OK",
804            "result": {
805                "list": [{
806                    "totalEquity": "1000.00",
807                    "accountIMRate": "0",
808                    "totalMarginBalance": "1000.00",
809                    "totalInitialMargin": "0",
810                    "accountType": "UNIFIED",
811                    "totalAvailableBalance": "1000.00",
812                    "accountMMRate": "0",
813                    "totalPerpUPL": "0",
814                    "totalWalletBalance": "1000.00",
815                    "accountLTV": "0",
816                    "totalMaintenanceMargin": "0",
817                    "coin": [{
818                        "availableToBorrow": "0",
819                        "bonus": "0",
820                        "accruedInterest": "0",
821                        "availableToWithdraw": "1000.00",
822                        "equity": "1000.00",
823                        "usdValue": "1000.00",
824                        "borrowAmount": "0",
825                        "totalPositionIM": "0",
826                        "walletBalance": "1000.00",
827                        "unrealisedPnl": "0",
828                        "cumRealisedPnl": "0",
829                        "locked": "0",
830                        "collateralSwitch": true,
831                        "marginCollateral": true,
832                        "coin": "USDT"
833                    }]
834                }]
835            }
836        }"#;
837
838        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
839            .expect("Failed to parse wallet balance without optional fields");
840
841        assert_eq!(response.ret_code, 0);
842        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
843        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
844    }
845
846    #[rstest]
847    fn deserialize_wallet_balance_from_docs() {
848        let json = include_str!("../../test_data/http_get_wallet_balance.json");
849
850        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
851            .expect("Failed to parse wallet balance from Bybit docs example");
852
853        assert_eq!(response.ret_code, 0);
854        assert_eq!(response.ret_msg, "OK");
855
856        let wallet = &response.result.list[0];
857        assert_eq!(wallet.total_equity, "3.31216591");
858        assert_eq!(wallet.account_im_rate, "0");
859        assert_eq!(wallet.account_mm_rate, "0");
860        assert_eq!(wallet.total_perp_upl, "0");
861        assert_eq!(wallet.account_ltv, "0");
862
863        // Check BTC coin
864        let btc = &wallet.coin[0];
865        assert_eq!(btc.coin.as_str(), "BTC");
866        assert_eq!(btc.available_to_borrow, "3");
867        assert_eq!(btc.total_order_im, Some("0".to_string()));
868        assert_eq!(btc.total_position_mm, Some("0".to_string()));
869        assert_eq!(btc.total_position_im, Some("0".to_string()));
870
871        // Check USDT coin (without optional IM/MM fields)
872        let usdt = &wallet.coin[1];
873        assert_eq!(usdt.coin.as_str(), "USDT");
874        assert_eq!(usdt.wallet_balance, "1000.50");
875        assert_eq!(usdt.total_order_im, None);
876        assert_eq!(usdt.total_position_mm, None);
877        assert_eq!(usdt.total_position_im, None);
878        assert_eq!(btc.spot_borrow, Some("0".to_string()));
879        assert_eq!(usdt.spot_borrow, Some("0".to_string()));
880    }
881
882    #[rstest]
883    fn test_parse_wallet_balance_with_spot_borrow() {
884        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
885        let response: BybitWalletBalanceResponse =
886            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
887
888        let wallet = &response.result.list[0];
889        let usdt = &wallet.coin[0];
890
891        assert_eq!(usdt.coin.as_str(), "USDT");
892        assert_eq!(usdt.wallet_balance, "1200.00");
893        assert_eq!(usdt.spot_borrow, Some("200.00".to_string()));
894        assert_eq!(usdt.borrow_amount, "200.00");
895
896        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
897        let account_id = crate::common::parse::parse_account_state(
898            wallet,
899            AccountId::new("BYBIT-001"),
900            UnixNanos::default(),
901        )
902        .expect("Failed to parse account state");
903
904        let balance = &account_id.balances[0];
905        assert_eq!(balance.total.as_f64(), 1000.0);
906    }
907}