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}
528
529/// Wallet balance snapshot containing per-coin balances.
530///
531/// # References
532/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
533#[derive(Clone, Debug, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct BybitWalletBalance {
536    pub total_equity: String,
537    #[serde(rename = "accountIMRate")]
538    pub account_im_rate: String,
539    pub total_margin_balance: String,
540    pub total_initial_margin: String,
541    pub account_type: BybitAccountType,
542    pub total_available_balance: String,
543    #[serde(rename = "accountMMRate")]
544    pub account_mm_rate: String,
545    #[serde(rename = "totalPerpUPL")]
546    pub total_perp_upl: String,
547    pub total_wallet_balance: String,
548    #[serde(rename = "accountLTV")]
549    pub account_ltv: String,
550    pub total_maintenance_margin: String,
551    pub coin: Vec<BybitCoinBalance>,
552}
553
554/// Response alias for wallet balance requests.
555///
556/// # References
557/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
558pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
559
560/// Order representation as returned by order-related endpoints.
561///
562/// # References
563/// - <https://bybit-exchange.github.io/docs/v5/order/realtime>
564/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
565#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct BybitOrder {
568    pub order_id: Ustr,
569    pub order_link_id: Ustr,
570    pub block_trade_id: Option<Ustr>,
571    pub symbol: Ustr,
572    pub price: String,
573    pub qty: String,
574    pub side: BybitOrderSide,
575    pub is_leverage: String,
576    pub position_idx: i32,
577    pub order_status: BybitOrderStatus,
578    pub cancel_type: BybitCancelType,
579    pub reject_reason: Ustr,
580    pub avg_price: Option<String>,
581    pub leaves_qty: String,
582    pub leaves_value: String,
583    pub cum_exec_qty: String,
584    pub cum_exec_value: String,
585    pub cum_exec_fee: String,
586    pub time_in_force: BybitTimeInForce,
587    pub order_type: BybitOrderType,
588    pub stop_order_type: BybitStopOrderType,
589    pub order_iv: Option<String>,
590    pub trigger_price: String,
591    pub take_profit: String,
592    pub stop_loss: String,
593    pub tp_trigger_by: BybitTriggerType,
594    pub sl_trigger_by: BybitTriggerType,
595    pub trigger_direction: BybitTriggerDirection,
596    pub trigger_by: BybitTriggerType,
597    pub last_price_on_created: String,
598    pub reduce_only: bool,
599    pub close_on_trigger: bool,
600    pub smp_type: Ustr,
601    pub smp_group: i32,
602    pub smp_order_id: Ustr,
603    pub tpsl_mode: Option<BybitTpSlMode>,
604    pub tp_limit_price: String,
605    pub sl_limit_price: String,
606    pub place_type: Ustr,
607    pub created_time: String,
608    pub updated_time: String,
609}
610
611/// Response alias for open order queries.
612///
613/// # References
614/// - <https://bybit-exchange.github.io/docs/v5/order/realtime>
615pub type BybitOpenOrdersResponse = BybitListResponse<BybitOrder>;
616/// Response alias for order history queries with pagination.
617///
618/// # References
619/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
620pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
621
622/// Payload returned after placing a single order.
623///
624/// # References
625/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
626#[derive(Clone, Debug, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628pub struct BybitPlaceOrderResult {
629    pub order_id: Option<Ustr>,
630    pub order_link_id: Option<Ustr>,
631}
632
633/// Response alias for order placement endpoints.
634///
635/// # References
636/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
637pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
638
639/// Payload returned after cancelling a single order.
640///
641/// # References
642/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
643#[derive(Clone, Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct BybitCancelOrderResult {
646    pub order_id: Option<Ustr>,
647    pub order_link_id: Option<Ustr>,
648}
649
650/// Response alias for order cancellation endpoints.
651///
652/// # References
653/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
654pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
655
656/// Execution/Fill payload returned by `GET /v5/execution/list`.
657///
658/// # References
659/// - <https://bybit-exchange.github.io/docs/v5/order/execution-list>
660#[derive(Clone, Debug, Serialize, Deserialize)]
661#[serde(rename_all = "camelCase")]
662pub struct BybitExecution {
663    pub symbol: Ustr,
664    pub order_id: Ustr,
665    pub order_link_id: Ustr,
666    pub side: BybitOrderSide,
667    pub order_price: String,
668    pub order_qty: String,
669    pub leaves_qty: String,
670    pub create_type: Option<String>,
671    pub order_type: BybitOrderType,
672    pub stop_order_type: Option<BybitStopOrderType>,
673    pub exec_fee: String,
674    pub exec_id: String,
675    pub exec_price: String,
676    pub exec_qty: String,
677    pub exec_type: BybitExecType,
678    pub exec_value: String,
679    pub exec_time: String,
680    pub fee_currency: Ustr,
681    pub is_maker: bool,
682    pub fee_rate: String,
683    pub trade_iv: String,
684    pub mark_iv: String,
685    pub mark_price: String,
686    pub index_price: String,
687    pub underlying_price: String,
688    pub block_trade_id: String,
689    pub closed_size: String,
690    pub seq: i64,
691}
692
693/// Response alias for trade history requests.
694///
695/// # References
696/// - <https://bybit-exchange.github.io/docs/v5/order/execution-list>
697pub type BybitTradeHistoryResponse = BybitListResponse<BybitExecution>;
698
699/// Represents a position returned by the Bybit API.
700///
701/// # References
702/// - <https://bybit-exchange.github.io/docs/v5/position/position-info>
703#[derive(Clone, Debug, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705pub struct BybitPosition {
706    pub position_idx: BybitPositionIdx,
707    pub risk_id: i32,
708    pub risk_limit_value: String,
709    pub symbol: Ustr,
710    pub side: BybitPositionSide,
711    pub size: String,
712    pub avg_price: String,
713    pub position_value: String,
714    pub trade_mode: i32,
715    pub position_status: String,
716    pub auto_add_margin: i32,
717    pub adl_rank_indicator: i32,
718    pub leverage: String,
719    pub position_balance: String,
720    pub mark_price: String,
721    pub liq_price: String,
722    pub bust_price: String,
723    #[serde(rename = "positionMM")]
724    pub position_mm: String,
725    #[serde(rename = "positionIM")]
726    pub position_im: String,
727    pub tpsl_mode: String,
728    pub take_profit: String,
729    pub stop_loss: String,
730    pub trailing_stop: String,
731    pub unrealised_pnl: String,
732    pub cur_realised_pnl: String,
733    pub cum_realised_pnl: String,
734    pub seq: i64,
735    pub is_reduce_only: bool,
736    pub mmr_sys_updated_time: String,
737    pub leverage_sys_updated_time: String,
738    pub created_time: String,
739    pub updated_time: String,
740}
741
742/// Response alias for position list requests.
743///
744/// # References
745/// - <https://bybit-exchange.github.io/docs/v5/position/position-info>
746pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
747
748////////////////////////////////////////////////////////////////////////////////
749// Tests
750////////////////////////////////////////////////////////////////////////////////
751
752#[cfg(test)]
753mod tests {
754    use rstest::rstest;
755
756    use super::*;
757    use crate::common::testing::load_test_json;
758
759    #[rstest]
760    fn deserialize_spot_instrument_uses_enums() {
761        let json = load_test_json("http_get_instruments_spot.json");
762        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
763        let instrument = &response.result.list[0];
764
765        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
766        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
767        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
768    }
769
770    #[rstest]
771    fn deserialize_linear_instrument_status() {
772        let json = load_test_json("http_get_instruments_linear.json");
773        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
774        let instrument = &response.result.list[0];
775
776        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
777        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
778    }
779
780    #[rstest]
781    fn deserialize_order_response_maps_enums() {
782        let json = load_test_json("http_get_orders_history.json");
783        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
784        let order = &response.result.list[0];
785
786        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
787        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
788        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
789        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
790        assert_eq!(order.order_type, BybitOrderType::Limit);
791    }
792
793    #[rstest]
794    fn deserialize_wallet_balance_without_optional_fields() {
795        let json = r#"{
796            "retCode": 0,
797            "retMsg": "OK",
798            "result": {
799                "list": [{
800                    "totalEquity": "1000.00",
801                    "accountIMRate": "0",
802                    "totalMarginBalance": "1000.00",
803                    "totalInitialMargin": "0",
804                    "accountType": "UNIFIED",
805                    "totalAvailableBalance": "1000.00",
806                    "accountMMRate": "0",
807                    "totalPerpUPL": "0",
808                    "totalWalletBalance": "1000.00",
809                    "accountLTV": "0",
810                    "totalMaintenanceMargin": "0",
811                    "coin": [{
812                        "availableToBorrow": "0",
813                        "bonus": "0",
814                        "accruedInterest": "0",
815                        "availableToWithdraw": "1000.00",
816                        "equity": "1000.00",
817                        "usdValue": "1000.00",
818                        "borrowAmount": "0",
819                        "totalPositionIM": "0",
820                        "walletBalance": "1000.00",
821                        "unrealisedPnl": "0",
822                        "cumRealisedPnl": "0",
823                        "locked": "0",
824                        "collateralSwitch": true,
825                        "marginCollateral": true,
826                        "coin": "USDT"
827                    }]
828                }]
829            }
830        }"#;
831
832        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
833            .expect("Failed to parse wallet balance without optional fields");
834
835        assert_eq!(response.ret_code, 0);
836        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
837        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
838    }
839
840    #[rstest]
841    fn deserialize_wallet_balance_from_docs() {
842        let json = include_str!("../../test_data/http_get_wallet_balance.json");
843
844        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
845            .expect("Failed to parse wallet balance from Bybit docs example");
846
847        assert_eq!(response.ret_code, 0);
848        assert_eq!(response.ret_msg, "OK");
849
850        let wallet = &response.result.list[0];
851        assert_eq!(wallet.total_equity, "3.31216591");
852        assert_eq!(wallet.account_im_rate, "0");
853        assert_eq!(wallet.account_mm_rate, "0");
854        assert_eq!(wallet.total_perp_upl, "0");
855        assert_eq!(wallet.account_ltv, "0");
856
857        // Check BTC coin
858        let btc = &wallet.coin[0];
859        assert_eq!(btc.coin.as_str(), "BTC");
860        assert_eq!(btc.available_to_borrow, "3");
861        assert_eq!(btc.total_order_im, Some("0".to_string()));
862        assert_eq!(btc.total_position_mm, Some("0".to_string()));
863        assert_eq!(btc.total_position_im, Some("0".to_string()));
864
865        // Check USDT coin (without optional IM/MM fields)
866        let usdt = &wallet.coin[1];
867        assert_eq!(usdt.coin.as_str(), "USDT");
868        assert_eq!(usdt.wallet_balance, "1000.50");
869        assert_eq!(usdt.total_order_im, None);
870        assert_eq!(usdt.total_position_mm, None);
871        assert_eq!(usdt.total_position_im, None);
872    }
873}