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        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/// Response payload returned by `GET /v5/market/time`.
41///
42/// # References
43/// - <https://bybit-exchange.github.io/docs/v5/market/time>
44#[derive(Clone, Debug, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct BybitServerTime {
47    /// Server timestamp in seconds represented as string.
48    pub time_second: String,
49    /// Server timestamp in nanoseconds represented as string.
50    pub time_nano: String,
51}
52
53/// Type alias for the server time response envelope.
54///
55/// # References
56/// - <https://bybit-exchange.github.io/docs/v5/market/time>
57pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
58
59/// Ticker payload for spot instruments.
60///
61/// # References
62/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
63#[derive(Clone, Debug, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct BybitTickerSpot {
66    pub symbol: Ustr,
67    pub bid1_price: String,
68    pub bid1_size: String,
69    pub ask1_price: String,
70    pub ask1_size: String,
71    pub last_price: String,
72    pub prev_price24h: String,
73    pub price24h_pcnt: String,
74    pub high_price24h: String,
75    pub low_price24h: String,
76    pub turnover24h: String,
77    pub volume24h: String,
78    pub usd_index_price: String,
79}
80
81/// Ticker payload for linear and inverse perpetual/futures instruments.
82///
83/// # References
84/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
85#[derive(Clone, Debug, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct BybitTickerLinear {
88    pub symbol: Ustr,
89    pub last_price: String,
90    pub index_price: String,
91    pub mark_price: String,
92    pub prev_price24h: String,
93    pub price24h_pcnt: String,
94    pub high_price24h: String,
95    pub low_price24h: String,
96    pub prev_price1h: String,
97    pub open_interest: String,
98    pub open_interest_value: String,
99    pub turnover24h: String,
100    pub volume24h: String,
101    pub funding_rate: String,
102    pub next_funding_time: String,
103    pub predicted_delivery_price: String,
104    pub basis_rate: String,
105    pub delivery_fee_rate: String,
106    pub delivery_time: String,
107    pub ask1_size: String,
108    pub bid1_price: String,
109    pub ask1_price: String,
110    pub bid1_size: String,
111    pub basis: String,
112}
113
114/// Ticker payload for option instruments.
115///
116/// # References
117/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
118#[derive(Clone, Debug, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct BybitTickerOption {
121    pub symbol: Ustr,
122    pub bid1_price: String,
123    pub bid1_size: String,
124    pub bid1_iv: String,
125    pub ask1_price: String,
126    pub ask1_size: String,
127    pub ask1_iv: String,
128    pub last_price: String,
129    pub high_price24h: String,
130    pub low_price24h: String,
131    pub mark_price: String,
132    pub index_price: String,
133    pub mark_iv: String,
134    pub underlying_price: String,
135    pub open_interest: String,
136    pub turnover24h: String,
137    pub volume24h: String,
138    pub total_volume: String,
139    pub total_turnover: String,
140    pub delta: String,
141    pub gamma: String,
142    pub vega: String,
143    pub theta: String,
144    pub predicted_delivery_price: String,
145    pub change24h: String,
146}
147
148/// Response alias for spot ticker requests.
149///
150/// # References
151/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
152pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
153/// Response alias for linear/inverse ticker requests.
154///
155/// # References
156/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
157pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
158/// Response alias for option ticker requests.
159///
160/// # References
161/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
162pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
163
164/// Unified ticker data structure containing common fields across all product types.
165///
166/// This simplified ticker structure is designed to work across SPOT, LINEAR, and OPTION products,
167/// containing only the most commonly used fields.
168#[derive(Clone, Debug, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170#[cfg_attr(
171    feature = "python",
172    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
173)]
174pub struct BybitTickerData {
175    pub symbol: Ustr,
176    pub bid1_price: String,
177    pub bid1_size: String,
178    pub ask1_price: String,
179    pub ask1_size: String,
180    pub last_price: String,
181    pub high_price24h: String,
182    pub low_price24h: String,
183    pub turnover24h: String,
184    pub volume24h: String,
185}
186
187#[cfg(feature = "python")]
188#[pyo3::pymethods]
189impl BybitTickerData {
190    #[getter]
191    #[must_use]
192    pub fn symbol(&self) -> &str {
193        self.symbol.as_str()
194    }
195
196    #[getter]
197    #[must_use]
198    pub fn bid1_price(&self) -> &str {
199        &self.bid1_price
200    }
201
202    #[getter]
203    #[must_use]
204    pub fn bid1_size(&self) -> &str {
205        &self.bid1_size
206    }
207
208    #[getter]
209    #[must_use]
210    pub fn ask1_price(&self) -> &str {
211        &self.ask1_price
212    }
213
214    #[getter]
215    #[must_use]
216    pub fn ask1_size(&self) -> &str {
217        &self.ask1_size
218    }
219
220    #[getter]
221    #[must_use]
222    pub fn last_price(&self) -> &str {
223        &self.last_price
224    }
225
226    #[getter]
227    #[must_use]
228    pub fn high_price24h(&self) -> &str {
229        &self.high_price24h
230    }
231
232    #[getter]
233    #[must_use]
234    pub fn low_price24h(&self) -> &str {
235        &self.low_price24h
236    }
237
238    #[getter]
239    #[must_use]
240    pub fn turnover24h(&self) -> &str {
241        &self.turnover24h
242    }
243
244    #[getter]
245    #[must_use]
246    pub fn volume24h(&self) -> &str {
247        &self.volume24h
248    }
249}
250
251/// Kline/candlestick entry returned by `GET /v5/market/kline`.
252///
253/// Bybit returns klines as arrays with 7 elements:
254/// [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
255///
256/// # References
257/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
258#[derive(Clone, Debug, Serialize)]
259pub struct BybitKline {
260    pub start: String,
261    pub open: String,
262    pub high: String,
263    pub low: String,
264    pub close: String,
265    pub volume: String,
266    pub turnover: String,
267}
268
269impl<'de> Deserialize<'de> for BybitKline {
270    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
271    where
272        D: serde::Deserializer<'de>,
273    {
274        let arr: [String; 7] = Deserialize::deserialize(deserializer)?;
275        Ok(Self {
276            start: arr[0].clone(),
277            open: arr[1].clone(),
278            high: arr[2].clone(),
279            low: arr[3].clone(),
280            close: arr[4].clone(),
281            volume: arr[5].clone(),
282            turnover: arr[6].clone(),
283        })
284    }
285}
286
287/// Kline list result returned by Bybit.
288///
289/// # References
290/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
291#[derive(Clone, Debug, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct BybitKlineResult {
294    pub category: BybitProductType,
295    pub symbol: Ustr,
296    pub list: Vec<BybitKline>,
297}
298
299/// Response alias for kline history requests.
300///
301/// # References
302/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
303pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
304
305/// Trade entry returned by `GET /v5/market/recent-trade`.
306///
307/// # References
308/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
309#[derive(Clone, Debug, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct BybitTrade {
312    pub exec_id: String,
313    pub symbol: Ustr,
314    pub price: String,
315    pub size: String,
316    pub side: BybitOrderSide,
317    pub time: String,
318    pub is_block_trade: bool,
319    #[serde(default)]
320    pub m_p: Option<String>,
321    #[serde(default)]
322    pub i_p: Option<String>,
323    #[serde(default)]
324    pub mlv: Option<String>,
325    #[serde(default)]
326    pub iv: Option<String>,
327}
328
329/// Trade list result returned by Bybit.
330///
331/// # References
332/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
333#[derive(Clone, Debug, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct BybitTradeResult {
336    pub category: BybitProductType,
337    pub list: Vec<BybitTrade>,
338}
339
340/// Response alias for recent trades requests.
341///
342/// # References
343/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
344pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
345
346/// Instrument definition for spot symbols.
347///
348/// # References
349/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
350#[derive(Clone, Debug, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct BybitInstrumentSpot {
353    pub symbol: Ustr,
354    pub base_coin: Ustr,
355    pub quote_coin: Ustr,
356    pub innovation: BybitInnovationFlag,
357    pub status: BybitInstrumentStatus,
358    pub margin_trading: BybitMarginTrading,
359    pub lot_size_filter: SpotLotSizeFilter,
360    pub price_filter: SpotPriceFilter,
361}
362
363/// Instrument definition for linear contracts.
364///
365/// # References
366/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
367#[derive(Clone, Debug, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct BybitInstrumentLinear {
370    pub symbol: Ustr,
371    pub contract_type: BybitContractType,
372    pub status: BybitInstrumentStatus,
373    pub base_coin: Ustr,
374    pub quote_coin: Ustr,
375    pub launch_time: String,
376    pub delivery_time: String,
377    pub delivery_fee_rate: String,
378    pub price_scale: String,
379    pub leverage_filter: LeverageFilter,
380    pub price_filter: LinearPriceFilter,
381    pub lot_size_filter: LinearLotSizeFilter,
382    pub unified_margin_trade: bool,
383    pub funding_interval: i64,
384    pub settle_coin: Ustr,
385}
386
387/// Instrument definition for inverse contracts.
388///
389/// # References
390/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
391#[derive(Clone, Debug, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct BybitInstrumentInverse {
394    pub symbol: Ustr,
395    pub contract_type: BybitContractType,
396    pub status: BybitInstrumentStatus,
397    pub base_coin: Ustr,
398    pub quote_coin: Ustr,
399    pub launch_time: String,
400    pub delivery_time: String,
401    pub delivery_fee_rate: String,
402    pub price_scale: String,
403    pub leverage_filter: LeverageFilter,
404    pub price_filter: LinearPriceFilter,
405    pub lot_size_filter: LinearLotSizeFilter,
406    pub unified_margin_trade: bool,
407    pub funding_interval: i64,
408    pub settle_coin: Ustr,
409}
410
411/// Instrument definition for option contracts.
412///
413/// # References
414/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
415#[derive(Clone, Debug, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct BybitInstrumentOption {
418    pub symbol: Ustr,
419    pub status: BybitInstrumentStatus,
420    pub base_coin: Ustr,
421    pub quote_coin: Ustr,
422    pub settle_coin: Ustr,
423    pub options_type: BybitOptionType,
424    pub launch_time: String,
425    pub delivery_time: String,
426    pub delivery_fee_rate: String,
427    pub price_filter: LinearPriceFilter,
428    pub lot_size_filter: OptionLotSizeFilter,
429}
430
431/// Response alias for instrument info requests that return spot instruments.
432///
433/// # References
434/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
435pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
436/// Response alias for instrument info requests that return linear contracts.
437///
438/// # References
439/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
440pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
441/// Response alias for instrument info requests that return inverse contracts.
442///
443/// # References
444/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
445pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
446/// Response alias for instrument info requests that return option contracts.
447///
448/// # References
449/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
450pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
451
452/// Fee rate structure returned by `GET /v5/account/fee-rate`.
453///
454/// # References
455/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
456#[derive(Clone, Debug, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458#[cfg_attr(
459    feature = "python",
460    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
461)]
462pub struct BybitFeeRate {
463    pub symbol: Ustr,
464    pub taker_fee_rate: String,
465    pub maker_fee_rate: String,
466    #[serde(default)]
467    pub base_coin: Option<Ustr>,
468}
469
470#[cfg(feature = "python")]
471#[pyo3::pymethods]
472impl BybitFeeRate {
473    #[getter]
474    #[must_use]
475    pub fn symbol(&self) -> &str {
476        self.symbol.as_str()
477    }
478
479    #[getter]
480    #[must_use]
481    pub fn taker_fee_rate(&self) -> &str {
482        &self.taker_fee_rate
483    }
484
485    #[getter]
486    #[must_use]
487    pub fn maker_fee_rate(&self) -> &str {
488        &self.maker_fee_rate
489    }
490
491    #[getter]
492    #[must_use]
493    pub fn base_coin(&self) -> Option<&str> {
494        self.base_coin.as_ref().map(|u| u.as_str())
495    }
496}
497
498/// Response alias for fee rate requests.
499///
500/// # References
501/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
502pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
503
504/// Account balance snapshot coin entry.
505///
506/// # References
507/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
508#[derive(Clone, Debug, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct BybitCoinBalance {
511    pub available_to_borrow: String,
512    pub bonus: String,
513    pub accrued_interest: String,
514    pub available_to_withdraw: String,
515    #[serde(default, rename = "totalOrderIM")]
516    pub total_order_im: Option<String>,
517    pub equity: String,
518    pub usd_value: String,
519    pub borrow_amount: String,
520    #[serde(default, rename = "totalPositionMM")]
521    pub total_position_mm: Option<String>,
522    #[serde(default, rename = "totalPositionIM")]
523    pub total_position_im: Option<String>,
524    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
525    pub wallet_balance: Decimal,
526    pub unrealised_pnl: String,
527    pub cum_realised_pnl: String,
528    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
529    pub locked: Decimal,
530    pub collateral_switch: bool,
531    pub margin_collateral: bool,
532    pub coin: Ustr,
533    #[serde(default)]
534    pub spot_hedging_qty: Option<String>,
535    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
536    pub spot_borrow: Decimal,
537}
538
539/// Wallet balance snapshot containing per-coin balances.
540///
541/// # References
542/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
543#[derive(Clone, Debug, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct BybitWalletBalance {
546    pub total_equity: String,
547    #[serde(rename = "accountIMRate")]
548    pub account_im_rate: String,
549    pub total_margin_balance: String,
550    pub total_initial_margin: String,
551    pub account_type: BybitAccountType,
552    pub total_available_balance: String,
553    #[serde(rename = "accountMMRate")]
554    pub account_mm_rate: String,
555    #[serde(rename = "totalPerpUPL")]
556    pub total_perp_upl: String,
557    pub total_wallet_balance: String,
558    #[serde(rename = "accountLTV")]
559    pub account_ltv: String,
560    pub total_maintenance_margin: String,
561    pub coin: Vec<BybitCoinBalance>,
562}
563
564/// Response alias for wallet balance requests.
565///
566/// # References
567/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
568pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
569
570/// Order representation as returned by order-related endpoints.
571///
572/// # References
573/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
574#[derive(Clone, Debug, Serialize, Deserialize)]
575#[serde(rename_all = "camelCase")]
576pub struct BybitOrder {
577    pub order_id: Ustr,
578    pub order_link_id: Ustr,
579    pub block_trade_id: Option<Ustr>,
580    pub symbol: Ustr,
581    pub price: String,
582    pub qty: String,
583    pub side: BybitOrderSide,
584    pub is_leverage: String,
585    pub position_idx: i32,
586    pub order_status: BybitOrderStatus,
587    pub cancel_type: BybitCancelType,
588    pub reject_reason: Ustr,
589    pub avg_price: Option<String>,
590    pub leaves_qty: String,
591    pub leaves_value: String,
592    pub cum_exec_qty: String,
593    pub cum_exec_value: String,
594    pub cum_exec_fee: String,
595    pub time_in_force: BybitTimeInForce,
596    pub order_type: BybitOrderType,
597    pub stop_order_type: BybitStopOrderType,
598    pub order_iv: Option<String>,
599    pub trigger_price: String,
600    pub take_profit: String,
601    pub stop_loss: String,
602    pub tp_trigger_by: BybitTriggerType,
603    pub sl_trigger_by: BybitTriggerType,
604    pub trigger_direction: BybitTriggerDirection,
605    pub trigger_by: BybitTriggerType,
606    pub last_price_on_created: String,
607    pub reduce_only: bool,
608    pub close_on_trigger: bool,
609    pub smp_type: Ustr,
610    pub smp_group: i32,
611    pub smp_order_id: Ustr,
612    pub tpsl_mode: Option<BybitTpSlMode>,
613    pub tp_limit_price: String,
614    pub sl_limit_price: String,
615    pub place_type: Ustr,
616    pub created_time: String,
617    pub updated_time: String,
618}
619
620/// Response alias for open order queries.
621///
622/// # References
623/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
624pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
625/// Response alias for order history queries with pagination.
626///
627/// # References
628/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
629pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
630
631/// Payload returned after placing a single order.
632///
633/// # References
634/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
635#[derive(Clone, Debug, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637pub struct BybitPlaceOrderResult {
638    pub order_id: Option<Ustr>,
639    pub order_link_id: Option<Ustr>,
640}
641
642/// Response alias for order placement endpoints.
643///
644/// # References
645/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
646pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
647
648/// Payload returned after cancelling a single order.
649///
650/// # References
651/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
652#[derive(Clone, Debug, Serialize, Deserialize)]
653#[serde(rename_all = "camelCase")]
654pub struct BybitCancelOrderResult {
655    pub order_id: Option<Ustr>,
656    pub order_link_id: Option<Ustr>,
657}
658
659/// Response alias for order cancellation endpoints.
660///
661/// # References
662/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
663pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
664
665/// Execution/Fill payload returned by `GET /v5/execution/list`.
666///
667/// # References
668/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
669#[derive(Clone, Debug, Serialize, Deserialize)]
670#[serde(rename_all = "camelCase")]
671pub struct BybitExecution {
672    pub symbol: Ustr,
673    pub order_id: Ustr,
674    pub order_link_id: Ustr,
675    pub side: BybitOrderSide,
676    pub order_price: String,
677    pub order_qty: String,
678    pub leaves_qty: String,
679    pub create_type: Option<String>,
680    pub order_type: BybitOrderType,
681    pub stop_order_type: Option<BybitStopOrderType>,
682    pub exec_fee: String,
683    pub exec_id: String,
684    pub exec_price: String,
685    pub exec_qty: String,
686    pub exec_type: BybitExecType,
687    pub exec_value: String,
688    pub exec_time: String,
689    pub fee_currency: Ustr,
690    pub is_maker: bool,
691    pub fee_rate: String,
692    pub trade_iv: String,
693    pub mark_iv: String,
694    pub mark_price: String,
695    pub index_price: String,
696    pub underlying_price: String,
697    pub block_trade_id: String,
698    pub closed_size: String,
699    pub seq: i64,
700}
701
702/// Response alias for trade history requests.
703///
704/// # References
705/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
706pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
707
708/// Represents a position returned by the Bybit API.
709///
710/// # References
711/// - <https://bybit-exchange.github.io/docs/v5/position>
712#[derive(Clone, Debug, Serialize, Deserialize)]
713#[serde(rename_all = "camelCase")]
714pub struct BybitPosition {
715    pub position_idx: BybitPositionIdx,
716    pub risk_id: i32,
717    pub risk_limit_value: String,
718    pub symbol: Ustr,
719    pub side: BybitPositionSide,
720    pub size: String,
721    pub avg_price: String,
722    pub position_value: String,
723    pub trade_mode: i32,
724    pub position_status: String,
725    pub auto_add_margin: i32,
726    pub adl_rank_indicator: i32,
727    pub leverage: String,
728    pub position_balance: String,
729    pub mark_price: String,
730    pub liq_price: String,
731    pub bust_price: String,
732    #[serde(rename = "positionMM")]
733    pub position_mm: String,
734    #[serde(rename = "positionIM")]
735    pub position_im: String,
736    pub tpsl_mode: String,
737    pub take_profit: String,
738    pub stop_loss: String,
739    pub trailing_stop: String,
740    pub unrealised_pnl: String,
741    pub cur_realised_pnl: String,
742    pub cum_realised_pnl: String,
743    pub seq: i64,
744    pub is_reduce_only: bool,
745    pub mmr_sys_updated_time: String,
746    pub leverage_sys_updated_time: String,
747    pub created_time: String,
748    pub updated_time: String,
749}
750
751/// Response alias for position list requests.
752///
753/// # References
754/// - <https://bybit-exchange.github.io/docs/v5/position>
755pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
756
757/// Reason detail for set margin mode failures.
758///
759/// # References
760/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
761#[derive(Clone, Debug, Serialize, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct BybitSetMarginModeReason {
764    pub reason_code: String,
765    pub reason_msg: String,
766}
767
768/// Result payload for set margin mode operation.
769///
770/// # References
771/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
772#[derive(Clone, Debug, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct BybitSetMarginModeResult {
775    #[serde(default)]
776    pub reasons: Vec<BybitSetMarginModeReason>,
777}
778
779/// Response alias for set margin mode requests.
780///
781/// # References
782/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
783pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
784
785/// Empty result for set leverage operation.
786#[derive(Clone, Debug, Serialize, Deserialize)]
787pub struct BybitSetLeverageResult {}
788
789/// Response alias for set leverage requests.
790///
791/// # References
792/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
793pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
794
795/// Empty result for switch mode operation.
796#[derive(Clone, Debug, Serialize, Deserialize)]
797pub struct BybitSwitchModeResult {}
798
799/// Response alias for switch mode requests.
800///
801/// # References
802/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
803pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
804
805/// Empty result for set trading stop operation.
806#[derive(Clone, Debug, Serialize, Deserialize)]
807pub struct BybitSetTradingStopResult {}
808
809/// Response alias for set trading stop requests.
810///
811/// # References
812/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
813pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
814
815/// Result from manual borrow operation.
816#[derive(Clone, Debug, Serialize, Deserialize)]
817#[serde(rename_all = "camelCase")]
818pub struct BybitBorrowResult {
819    pub coin: String,
820    pub amount: String,
821}
822
823/// Response alias for manual borrow requests.
824///
825/// # References
826///
827/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
828pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
829
830/// Result from no-convert repay operation.
831#[derive(Clone, Debug, Serialize, Deserialize)]
832#[serde(rename_all = "camelCase")]
833pub struct BybitNoConvertRepayResult {
834    pub result_status: String,
835}
836
837/// Response alias for no-convert repay requests.
838///
839/// # References
840///
841/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
842pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
843
844/// API key permissions.
845#[derive(Clone, Debug, Serialize, Deserialize)]
846#[cfg_attr(
847    feature = "python",
848    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
849)]
850#[serde(rename_all = "PascalCase")]
851pub struct BybitApiKeyPermissions {
852    #[serde(default)]
853    pub contract_trade: Vec<String>,
854    #[serde(default)]
855    pub spot: Vec<String>,
856    #[serde(default)]
857    pub wallet: Vec<String>,
858    #[serde(default)]
859    pub options: Vec<String>,
860    #[serde(default)]
861    pub derivatives: Vec<String>,
862    #[serde(default)]
863    pub exchange: Vec<String>,
864    #[serde(default)]
865    pub copy_trading: Vec<String>,
866    #[serde(default)]
867    pub block_trade: Vec<String>,
868    #[serde(default)]
869    pub nft: Vec<String>,
870    #[serde(default)]
871    pub affiliate: Vec<String>,
872}
873
874/// Account details from API key info.
875#[derive(Clone, Debug, Serialize, Deserialize)]
876#[cfg_attr(
877    feature = "python",
878    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
879)]
880#[serde(rename_all = "camelCase")]
881pub struct BybitAccountDetails {
882    pub id: String,
883    pub note: String,
884    pub api_key: String,
885    pub read_only: u8,
886    pub secret: String,
887    #[serde(rename = "type")]
888    pub key_type: u8,
889    pub permissions: BybitApiKeyPermissions,
890    pub ips: Vec<String>,
891    #[serde(default)]
892    pub user_id: Option<u64>,
893    #[serde(default)]
894    pub inviter_id: Option<u64>,
895    pub vip_level: String,
896    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
897    pub mkt_maker_level: u8,
898    #[serde(default)]
899    pub affiliate_id: Option<u64>,
900    pub rsa_public_key: String,
901    pub is_master: bool,
902    pub parent_uid: String,
903    pub uta: u8,
904    pub kyc_level: String,
905    pub kyc_region: String,
906    #[serde(default)]
907    pub deadline_day: i64,
908    #[serde(default)]
909    pub expired_at: Option<String>,
910    pub created_at: String,
911}
912
913#[cfg(feature = "python")]
914#[pyo3::pymethods]
915impl BybitAccountDetails {
916    #[getter]
917    #[must_use]
918    pub fn id(&self) -> &str {
919        &self.id
920    }
921
922    #[getter]
923    #[must_use]
924    pub fn note(&self) -> &str {
925        &self.note
926    }
927
928    #[getter]
929    #[must_use]
930    pub fn api_key(&self) -> &str {
931        &self.api_key
932    }
933
934    #[getter]
935    #[must_use]
936    pub fn read_only(&self) -> u8 {
937        self.read_only
938    }
939
940    #[getter]
941    #[must_use]
942    pub fn key_type(&self) -> u8 {
943        self.key_type
944    }
945
946    #[getter]
947    #[must_use]
948    pub fn user_id(&self) -> Option<u64> {
949        self.user_id
950    }
951
952    #[getter]
953    #[must_use]
954    pub fn inviter_id(&self) -> Option<u64> {
955        self.inviter_id
956    }
957
958    #[getter]
959    #[must_use]
960    pub fn vip_level(&self) -> &str {
961        &self.vip_level
962    }
963
964    #[getter]
965    #[must_use]
966    pub fn mkt_maker_level(&self) -> u8 {
967        self.mkt_maker_level
968    }
969
970    #[getter]
971    #[must_use]
972    pub fn affiliate_id(&self) -> Option<u64> {
973        self.affiliate_id
974    }
975
976    #[getter]
977    #[must_use]
978    pub fn rsa_public_key(&self) -> &str {
979        &self.rsa_public_key
980    }
981
982    #[getter]
983    #[must_use]
984    pub fn is_master(&self) -> bool {
985        self.is_master
986    }
987
988    #[getter]
989    #[must_use]
990    pub fn parent_uid(&self) -> &str {
991        &self.parent_uid
992    }
993
994    #[getter]
995    #[must_use]
996    pub fn uta(&self) -> u8 {
997        self.uta
998    }
999
1000    #[getter]
1001    #[must_use]
1002    pub fn kyc_level(&self) -> &str {
1003        &self.kyc_level
1004    }
1005
1006    #[getter]
1007    #[must_use]
1008    pub fn kyc_region(&self) -> &str {
1009        &self.kyc_region
1010    }
1011
1012    #[getter]
1013    #[must_use]
1014    pub fn deadline_day(&self) -> i64 {
1015        self.deadline_day
1016    }
1017
1018    #[getter]
1019    #[must_use]
1020    pub fn expired_at(&self) -> Option<&str> {
1021        self.expired_at.as_deref()
1022    }
1023
1024    #[getter]
1025    #[must_use]
1026    pub fn created_at(&self) -> &str {
1027        &self.created_at
1028    }
1029}
1030
1031/// Response alias for API key info requests.
1032///
1033/// # References
1034///
1035/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1036pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1037
1038////////////////////////////////////////////////////////////////////////////////
1039// Tests
1040////////////////////////////////////////////////////////////////////////////////
1041
1042#[cfg(test)]
1043mod tests {
1044    use nautilus_core::UnixNanos;
1045    use nautilus_model::identifiers::AccountId;
1046    use rstest::rstest;
1047    use rust_decimal::Decimal;
1048    use rust_decimal_macros::dec;
1049
1050    use super::*;
1051    use crate::common::testing::load_test_json;
1052
1053    #[rstest]
1054    fn deserialize_spot_instrument_uses_enums() {
1055        let json = load_test_json("http_get_instruments_spot.json");
1056        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1057        let instrument = &response.result.list[0];
1058
1059        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1060        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1061        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1062    }
1063
1064    #[rstest]
1065    fn deserialize_linear_instrument_status() {
1066        let json = load_test_json("http_get_instruments_linear.json");
1067        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1068        let instrument = &response.result.list[0];
1069
1070        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1071        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1072    }
1073
1074    #[rstest]
1075    fn deserialize_order_response_maps_enums() {
1076        let json = load_test_json("http_get_orders_history.json");
1077        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1078        let order = &response.result.list[0];
1079
1080        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
1081        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
1082        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
1083        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
1084        assert_eq!(order.order_type, BybitOrderType::Limit);
1085    }
1086
1087    #[rstest]
1088    fn deserialize_wallet_balance_without_optional_fields() {
1089        let json = r#"{
1090            "retCode": 0,
1091            "retMsg": "OK",
1092            "result": {
1093                "list": [{
1094                    "totalEquity": "1000.00",
1095                    "accountIMRate": "0",
1096                    "totalMarginBalance": "1000.00",
1097                    "totalInitialMargin": "0",
1098                    "accountType": "UNIFIED",
1099                    "totalAvailableBalance": "1000.00",
1100                    "accountMMRate": "0",
1101                    "totalPerpUPL": "0",
1102                    "totalWalletBalance": "1000.00",
1103                    "accountLTV": "0",
1104                    "totalMaintenanceMargin": "0",
1105                    "coin": [{
1106                        "availableToBorrow": "0",
1107                        "bonus": "0",
1108                        "accruedInterest": "0",
1109                        "availableToWithdraw": "1000.00",
1110                        "equity": "1000.00",
1111                        "usdValue": "1000.00",
1112                        "borrowAmount": "0",
1113                        "totalPositionIM": "0",
1114                        "walletBalance": "1000.00",
1115                        "unrealisedPnl": "0",
1116                        "cumRealisedPnl": "0",
1117                        "locked": "0",
1118                        "collateralSwitch": true,
1119                        "marginCollateral": true,
1120                        "coin": "USDT"
1121                    }]
1122                }]
1123            }
1124        }"#;
1125
1126        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1127            .expect("Failed to parse wallet balance without optional fields");
1128
1129        assert_eq!(response.ret_code, 0);
1130        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
1131        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
1132    }
1133
1134    #[rstest]
1135    fn deserialize_wallet_balance_from_docs() {
1136        let json = include_str!("../../test_data/http_get_wallet_balance.json");
1137
1138        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1139            .expect("Failed to parse wallet balance from Bybit docs example");
1140
1141        assert_eq!(response.ret_code, 0);
1142        assert_eq!(response.ret_msg, "OK");
1143
1144        let wallet = &response.result.list[0];
1145        assert_eq!(wallet.total_equity, "3.31216591");
1146        assert_eq!(wallet.account_im_rate, "0");
1147        assert_eq!(wallet.account_mm_rate, "0");
1148        assert_eq!(wallet.total_perp_upl, "0");
1149        assert_eq!(wallet.account_ltv, "0");
1150
1151        // Check BTC coin
1152        let btc = &wallet.coin[0];
1153        assert_eq!(btc.coin.as_str(), "BTC");
1154        assert_eq!(btc.available_to_borrow, "3");
1155        assert_eq!(btc.total_order_im, Some("0".to_string()));
1156        assert_eq!(btc.total_position_mm, Some("0".to_string()));
1157        assert_eq!(btc.total_position_im, Some("0".to_string()));
1158
1159        // Check USDT coin (without optional IM/MM fields)
1160        let usdt = &wallet.coin[1];
1161        assert_eq!(usdt.coin.as_str(), "USDT");
1162        assert_eq!(usdt.wallet_balance, dec!(1000.50));
1163        assert_eq!(usdt.total_order_im, None);
1164        assert_eq!(usdt.total_position_mm, None);
1165        assert_eq!(usdt.total_position_im, None);
1166        assert_eq!(btc.spot_borrow, Decimal::ZERO);
1167        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
1168    }
1169
1170    #[rstest]
1171    fn test_parse_wallet_balance_with_spot_borrow() {
1172        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
1173        let response: BybitWalletBalanceResponse =
1174            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
1175
1176        let wallet = &response.result.list[0];
1177        let usdt = &wallet.coin[0];
1178
1179        assert_eq!(usdt.coin.as_str(), "USDT");
1180        assert_eq!(usdt.wallet_balance, dec!(1200.00));
1181        assert_eq!(usdt.spot_borrow, dec!(200.00));
1182        assert_eq!(usdt.borrow_amount, "200.00");
1183
1184        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
1185        let account_id = crate::common::parse::parse_account_state(
1186            wallet,
1187            AccountId::new("BYBIT-001"),
1188            UnixNanos::default(),
1189        )
1190        .expect("Failed to parse account state");
1191
1192        let balance = &account_id.balances[0];
1193        assert_eq!(balance.total.as_f64(), 1000.0);
1194    }
1195
1196    #[rstest]
1197    fn test_parse_wallet_balance_spot_short() {
1198        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
1199        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
1200            .expect("Failed to parse wallet balance with SHORT SPOT position");
1201
1202        let wallet = &response.result.list[0];
1203        let eth = &wallet.coin[0];
1204
1205        assert_eq!(eth.coin.as_str(), "ETH");
1206        assert_eq!(eth.wallet_balance, dec!(0));
1207        assert_eq!(eth.spot_borrow, dec!(0.06142));
1208        assert_eq!(eth.borrow_amount, "0.06142");
1209
1210        let account_state = crate::common::parse::parse_account_state(
1211            wallet,
1212            AccountId::new("BYBIT-001"),
1213            UnixNanos::default(),
1214        )
1215        .expect("Failed to parse account state");
1216
1217        let eth_balance = account_state
1218            .balances
1219            .iter()
1220            .find(|b| b.currency.code.as_str() == "ETH")
1221            .expect("ETH balance not found");
1222
1223        // Negative balance represents SHORT position (borrowed ETH)
1224        assert_eq!(eth_balance.total.as_f64(), -0.06142);
1225    }
1226
1227    #[rstest]
1228    fn deserialize_borrow_response() {
1229        let json = r#"{
1230            "retCode": 0,
1231            "retMsg": "success",
1232            "result": {
1233                "coin": "BTC",
1234                "amount": "0.01"
1235            },
1236            "retExtInfo": {},
1237            "time": 1756197991955
1238        }"#;
1239
1240        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
1241
1242        assert_eq!(response.ret_code, 0);
1243        assert_eq!(response.ret_msg, "success");
1244        assert_eq!(response.result.coin, "BTC");
1245        assert_eq!(response.result.amount, "0.01");
1246    }
1247
1248    #[rstest]
1249    fn deserialize_no_convert_repay_response() {
1250        let json = r#"{
1251            "retCode": 0,
1252            "retMsg": "OK",
1253            "result": {
1254                "resultStatus": "SU"
1255            },
1256            "retExtInfo": {},
1257            "time": 1234567890
1258        }"#;
1259
1260        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
1261
1262        assert_eq!(response.ret_code, 0);
1263        assert_eq!(response.ret_msg, "OK");
1264        assert_eq!(response.result.result_status, "SU");
1265    }
1266}