Skip to main content

nautilus_bybit/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data transfer objects for deserializing Bybit HTTP API payloads.
17
18use nautilus_core::{
19    serialization::deserialize_empty_string_as_none, string::secret::SecretString,
20};
21use rust_decimal::Decimal;
22use serde::{Deserialize, Serialize};
23use ustr::Ustr;
24
25use crate::common::{
26    enums::{
27        BybitAccountType, BybitApiKeyType, BybitCancelType, BybitContractType, BybitCreateType,
28        BybitExecType, BybitInnovationFlag, BybitInstrumentStatus, BybitMarginMode,
29        BybitMarginTrading, BybitOptionType, BybitOrderSide, BybitOrderStatus, BybitOrderType,
30        BybitPositionIdx, BybitPositionSide, BybitPositionStatus, BybitProductType,
31        BybitRepayStatus, BybitSmpType, BybitStopOrderType, BybitSymbolType, BybitTimeInForce,
32        BybitTpSlMode, BybitTriggerDirection, BybitTriggerType, BybitUnifiedMarginStatus,
33    },
34    models::{
35        BybitCursorList, BybitCursorListResponse, BybitListResponse, BybitResponse, LeverageFilter,
36        LinearLotSizeFilter, LinearPriceFilter, OptionLotSizeFilter, SpotLotSizeFilter,
37        SpotPriceFilter,
38    },
39    parse::{
40        bool_or_int, deserialize_decimal_or_zero, deserialize_i32_or_string,
41        deserialize_optional_decimal_or_zero, deserialize_optional_symbol_type,
42        deserialize_string_to_u8, masked_secret, on_off_bool,
43    },
44};
45
46/// Cursor-paginated list of orders for Python bindings.
47#[derive(Clone, Debug, Default, Serialize, Deserialize)]
48#[cfg_attr(
49    feature = "python",
50    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
51)]
52#[cfg_attr(
53    feature = "python",
54    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
55)]
56pub struct BybitOrderCursorList {
57    /// Collection of orders returned by the endpoint.
58    pub list: Vec<BybitOrder>,
59    /// Pagination cursor for the next page.
60    pub next_page_cursor: Option<String>,
61    /// Optional product category when the API includes it.
62    #[serde(default)]
63    pub category: Option<BybitProductType>,
64}
65
66impl From<BybitCursorList<BybitOrder>> for BybitOrderCursorList {
67    fn from(cursor_list: BybitCursorList<BybitOrder>) -> Self {
68        Self {
69            list: cursor_list.list,
70            next_page_cursor: cursor_list.next_page_cursor,
71            category: cursor_list.category,
72        }
73    }
74}
75
76#[cfg(feature = "python")]
77#[pyo3::pymethods]
78impl BybitOrderCursorList {
79    #[getter]
80    #[must_use]
81    pub fn list(&self) -> Vec<BybitOrder> {
82        self.list.clone()
83    }
84
85    #[getter]
86    #[must_use]
87    pub fn next_page_cursor(&self) -> Option<&str> {
88        self.next_page_cursor.as_deref()
89    }
90
91    #[getter]
92    #[must_use]
93    pub fn category(&self) -> Option<BybitProductType> {
94        self.category
95    }
96}
97
98/// Response payload returned by `GET /v5/market/time`.
99///
100/// # References
101/// - <https://bybit-exchange.github.io/docs/v5/market/time>
102#[derive(Clone, Debug, Serialize, Deserialize)]
103#[cfg_attr(
104    feature = "python",
105    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
106)]
107#[cfg_attr(
108    feature = "python",
109    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
110)]
111#[serde(rename_all = "camelCase")]
112pub struct BybitServerTime {
113    /// Server timestamp in seconds represented as string.
114    pub time_second: String,
115    /// Server timestamp in nanoseconds represented as string.
116    pub time_nano: String,
117}
118
119#[cfg(feature = "python")]
120#[pyo3::pymethods]
121impl BybitServerTime {
122    #[getter]
123    #[must_use]
124    pub fn time_second(&self) -> &str {
125        &self.time_second
126    }
127
128    #[getter]
129    #[must_use]
130    pub fn time_nano(&self) -> &str {
131        &self.time_nano
132    }
133}
134
135/// Type alias for the server time response envelope.
136///
137/// # References
138/// - <https://bybit-exchange.github.io/docs/v5/market/time>
139pub type BybitServerTimeResponse = BybitResponse<BybitServerTime>;
140
141/// Ticker payload for spot instruments.
142///
143/// # References
144/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
145#[derive(Clone, Debug, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct BybitTickerSpot {
148    pub symbol: Ustr,
149    pub bid1_price: String,
150    pub bid1_size: String,
151    pub ask1_price: String,
152    pub ask1_size: String,
153    pub last_price: String,
154    pub prev_price24h: String,
155    pub price24h_pcnt: String,
156    pub high_price24h: String,
157    pub low_price24h: String,
158    pub turnover24h: String,
159    pub volume24h: String,
160    #[serde(default)]
161    pub usd_index_price: String,
162}
163
164/// Ticker payload for linear and inverse perpetual/futures instruments.
165///
166/// # References
167/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
168#[derive(Clone, Debug, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct BybitTickerLinear {
171    pub symbol: Ustr,
172    pub last_price: String,
173    pub index_price: String,
174    pub mark_price: String,
175    pub prev_price24h: String,
176    pub price24h_pcnt: String,
177    pub high_price24h: String,
178    pub low_price24h: String,
179    pub prev_price1h: String,
180    pub open_interest: String,
181    pub open_interest_value: String,
182    pub turnover24h: String,
183    pub volume24h: String,
184    pub funding_rate: String,
185    pub next_funding_time: String,
186    pub predicted_delivery_price: String,
187    pub basis_rate: String,
188    pub delivery_fee_rate: String,
189    pub delivery_time: String,
190    pub ask1_size: String,
191    pub bid1_price: String,
192    pub ask1_price: String,
193    pub bid1_size: String,
194    pub basis: String,
195}
196
197/// Ticker payload for option instruments.
198///
199/// # References
200/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
201#[derive(Clone, Debug, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct BybitTickerOption {
204    pub symbol: Ustr,
205    pub bid1_price: String,
206    pub bid1_size: String,
207    pub bid1_iv: String,
208    pub ask1_price: String,
209    pub ask1_size: String,
210    pub ask1_iv: String,
211    pub last_price: String,
212    pub high_price24h: String,
213    pub low_price24h: String,
214    pub mark_price: String,
215    pub index_price: String,
216    pub mark_iv: String,
217    pub underlying_price: String,
218    pub open_interest: String,
219    pub turnover24h: String,
220    pub volume24h: String,
221    pub total_volume: String,
222    pub total_turnover: String,
223    pub delta: String,
224    pub gamma: String,
225    pub vega: String,
226    pub theta: String,
227    pub predicted_delivery_price: String,
228    pub change24h: String,
229}
230
231/// Response alias for spot ticker requests.
232///
233/// # References
234/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
235pub type BybitTickersSpotResponse = BybitListResponse<BybitTickerSpot>;
236/// Response alias for linear/inverse ticker requests.
237///
238/// # References
239/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
240pub type BybitTickersLinearResponse = BybitListResponse<BybitTickerLinear>;
241/// Response alias for option ticker requests.
242///
243/// # References
244/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
245pub type BybitTickersOptionResponse = BybitListResponse<BybitTickerOption>;
246
247/// Unified ticker data structure containing common fields across all product types.
248///
249/// This simplified ticker structure is designed to work across SPOT, LINEAR, and OPTION products,
250/// containing only the most commonly used fields.
251#[derive(Clone, Debug, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253#[cfg_attr(
254    feature = "python",
255    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
256)]
257#[cfg_attr(
258    feature = "python",
259    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
260)]
261pub struct BybitTickerData {
262    pub symbol: Ustr,
263    pub bid1_price: String,
264    pub bid1_size: String,
265    pub ask1_price: String,
266    pub ask1_size: String,
267    pub last_price: String,
268    pub high_price24h: String,
269    pub low_price24h: String,
270    pub turnover24h: String,
271    pub volume24h: String,
272    #[serde(default)]
273    pub open_interest: Option<String>,
274    #[serde(default)]
275    pub funding_rate: Option<String>,
276    #[serde(default)]
277    pub next_funding_time: Option<String>,
278    #[serde(default)]
279    pub mark_price: Option<String>,
280    #[serde(default)]
281    pub index_price: Option<String>,
282}
283
284#[cfg(feature = "python")]
285#[pyo3::pymethods]
286impl BybitTickerData {
287    #[getter]
288    #[must_use]
289    pub fn symbol(&self) -> &str {
290        self.symbol.as_str()
291    }
292
293    #[getter]
294    #[must_use]
295    pub fn bid1_price(&self) -> &str {
296        &self.bid1_price
297    }
298
299    #[getter]
300    #[must_use]
301    pub fn bid1_size(&self) -> &str {
302        &self.bid1_size
303    }
304
305    #[getter]
306    #[must_use]
307    pub fn ask1_price(&self) -> &str {
308        &self.ask1_price
309    }
310
311    #[getter]
312    #[must_use]
313    pub fn ask1_size(&self) -> &str {
314        &self.ask1_size
315    }
316
317    #[getter]
318    #[must_use]
319    pub fn last_price(&self) -> &str {
320        &self.last_price
321    }
322
323    #[getter]
324    #[must_use]
325    pub fn high_price24h(&self) -> &str {
326        &self.high_price24h
327    }
328
329    #[getter]
330    #[must_use]
331    pub fn low_price24h(&self) -> &str {
332        &self.low_price24h
333    }
334
335    #[getter]
336    #[must_use]
337    pub fn turnover24h(&self) -> &str {
338        &self.turnover24h
339    }
340
341    #[getter]
342    #[must_use]
343    pub fn volume24h(&self) -> &str {
344        &self.volume24h
345    }
346
347    #[getter]
348    #[must_use]
349    pub fn open_interest(&self) -> Option<&str> {
350        self.open_interest.as_deref()
351    }
352
353    #[getter]
354    #[must_use]
355    pub fn funding_rate(&self) -> Option<&str> {
356        self.funding_rate.as_deref()
357    }
358
359    #[getter]
360    #[must_use]
361    pub fn next_funding_time(&self) -> Option<&str> {
362        self.next_funding_time.as_deref()
363    }
364
365    #[getter]
366    #[must_use]
367    pub fn mark_price(&self) -> Option<&str> {
368        self.mark_price.as_deref()
369    }
370
371    #[getter]
372    #[must_use]
373    pub fn index_price(&self) -> Option<&str> {
374        self.index_price.as_deref()
375    }
376}
377
378impl From<BybitTickerSpot> for BybitTickerData {
379    fn from(ticker: BybitTickerSpot) -> Self {
380        Self {
381            symbol: ticker.symbol,
382            bid1_price: ticker.bid1_price,
383            bid1_size: ticker.bid1_size,
384            ask1_price: ticker.ask1_price,
385            ask1_size: ticker.ask1_size,
386            last_price: ticker.last_price,
387            high_price24h: ticker.high_price24h,
388            low_price24h: ticker.low_price24h,
389            turnover24h: ticker.turnover24h,
390            volume24h: ticker.volume24h,
391            open_interest: None,
392            funding_rate: None,
393            next_funding_time: None,
394            mark_price: None,
395            index_price: None,
396        }
397    }
398}
399
400impl From<BybitTickerLinear> for BybitTickerData {
401    fn from(ticker: BybitTickerLinear) -> Self {
402        Self {
403            symbol: ticker.symbol,
404            bid1_price: ticker.bid1_price,
405            bid1_size: ticker.bid1_size,
406            ask1_price: ticker.ask1_price,
407            ask1_size: ticker.ask1_size,
408            last_price: ticker.last_price,
409            high_price24h: ticker.high_price24h,
410            low_price24h: ticker.low_price24h,
411            turnover24h: ticker.turnover24h,
412            volume24h: ticker.volume24h,
413            open_interest: Some(ticker.open_interest),
414            funding_rate: Some(ticker.funding_rate),
415            next_funding_time: Some(ticker.next_funding_time),
416            mark_price: Some(ticker.mark_price),
417            index_price: Some(ticker.index_price),
418        }
419    }
420}
421
422impl From<BybitTickerOption> for BybitTickerData {
423    fn from(ticker: BybitTickerOption) -> Self {
424        Self {
425            symbol: ticker.symbol,
426            bid1_price: ticker.bid1_price,
427            bid1_size: ticker.bid1_size,
428            ask1_price: ticker.ask1_price,
429            ask1_size: ticker.ask1_size,
430            last_price: ticker.last_price,
431            high_price24h: ticker.high_price24h,
432            low_price24h: ticker.low_price24h,
433            turnover24h: ticker.turnover24h,
434            volume24h: ticker.volume24h,
435            open_interest: Some(ticker.open_interest),
436            funding_rate: None,
437            next_funding_time: None,
438            mark_price: Some(ticker.mark_price),
439            index_price: Some(ticker.index_price),
440        }
441    }
442}
443
444/// Kline/candlestick entry returned by `GET /v5/market/kline`.
445///
446/// Bybit returns klines as arrays with 7 elements:
447/// [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
448///
449/// # References
450/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
451#[derive(Clone, Debug, Serialize)]
452pub struct BybitKline {
453    pub start: String,
454    pub open: String,
455    pub high: String,
456    pub low: String,
457    pub close: String,
458    pub volume: String,
459    pub turnover: String,
460}
461
462impl<'de> Deserialize<'de> for BybitKline {
463    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
464    where
465        D: serde::Deserializer<'de>,
466    {
467        let [start, open, high, low, close, volume, turnover]: [String; 7] =
468            Deserialize::deserialize(deserializer)?;
469        Ok(Self {
470            start,
471            open,
472            high,
473            low,
474            close,
475            volume,
476            turnover,
477        })
478    }
479}
480
481/// Kline list result returned by Bybit.
482///
483/// # References
484/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
485#[derive(Clone, Debug, Serialize, Deserialize)]
486#[serde(rename_all = "camelCase")]
487pub struct BybitKlineResult {
488    pub category: BybitProductType,
489    pub symbol: Ustr,
490    pub list: Vec<BybitKline>,
491}
492
493/// Response alias for kline history requests.
494///
495/// # References
496/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
497pub type BybitKlinesResponse = BybitResponse<BybitKlineResult>;
498
499/// Trade entry returned by `GET /v5/market/recent-trade`.
500///
501/// # References
502/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
503#[derive(Clone, Debug, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct BybitTrade {
506    pub exec_id: String,
507    pub symbol: Ustr,
508    pub price: String,
509    pub size: String,
510    pub side: BybitOrderSide,
511    pub time: String,
512    pub is_block_trade: bool,
513    #[serde(default)]
514    pub m_p: Option<String>,
515    #[serde(default)]
516    pub i_p: Option<String>,
517    #[serde(default)]
518    pub mlv: Option<String>,
519    #[serde(default)]
520    pub iv: Option<String>,
521}
522
523/// Trade list result returned by Bybit.
524///
525/// # References
526/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
527#[derive(Clone, Debug, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub struct BybitTradeResult {
530    pub category: BybitProductType,
531    pub list: Vec<BybitTrade>,
532}
533
534/// Response alias for recent trades requests.
535///
536/// # References
537/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
538pub type BybitTradesResponse = BybitResponse<BybitTradeResult>;
539
540/// Funding entry returned by `GET /v5/market/funding/history`.
541///
542/// # References
543/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
544#[derive(Clone, Debug, Serialize, Deserialize)]
545#[serde(rename_all = "camelCase")]
546pub struct BybitFunding {
547    pub symbol: Ustr,
548    pub funding_rate: String,
549    pub funding_rate_timestamp: String,
550}
551
552/// Funding list result returned by Bybit.
553///
554/// # References
555/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
556#[derive(Clone, Debug, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct BybitFundingResult {
559    pub category: BybitProductType,
560    pub list: Vec<BybitFunding>,
561}
562
563/// Response alias for historical funding requests.
564///
565/// # References
566/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
567pub type BybitFundingResponse = BybitResponse<BybitFundingResult>;
568
569/// Orderbook result returned by Bybit.
570///
571/// # References
572/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
573#[derive(Clone, Debug, Serialize, Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct BybitOrderbookResult {
576    /// Symbol.
577    pub s: Ustr,
578    /// Bid levels represented as `[price, size]` string pairs.
579    pub b: Vec<[String; 2]>,
580    /// Ask levels represented as `[price, size]` string pairs.
581    pub a: Vec<[String; 2]>,
582    pub ts: i64,
583    /// Update identifier.
584    pub u: i64,
585    /// Cross sequence number.
586    pub seq: i64,
587    pub cts: i64,
588}
589
590/// Response alias for orderbook requests.
591///
592/// # References
593/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
594pub type BybitOrderbookResponse = BybitResponse<BybitOrderbookResult>;
595
596/// Instrument definition for spot symbols.
597///
598/// # References
599/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
600#[derive(Clone, Debug, Serialize, Deserialize)]
601#[serde(rename_all = "camelCase")]
602pub struct BybitInstrumentSpot {
603    pub symbol: Ustr,
604    pub base_coin: Ustr,
605    pub quote_coin: Ustr,
606    pub innovation: BybitInnovationFlag,
607    pub status: BybitInstrumentStatus,
608    pub margin_trading: BybitMarginTrading,
609    pub lot_size_filter: SpotLotSizeFilter,
610    pub price_filter: SpotPriceFilter,
611    #[serde(default)]
612    pub symbol_id: Option<i64>,
613    #[serde(default, deserialize_with = "deserialize_optional_symbol_type")]
614    pub symbol_type: Option<BybitSymbolType>,
615    #[serde(default)]
616    pub xstock_multiplier: Option<String>,
617}
618
619/// Instrument definition for linear contracts.
620///
621/// # References
622/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
623#[derive(Clone, Debug, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625pub struct BybitInstrumentLinear {
626    pub symbol: Ustr,
627    pub contract_type: BybitContractType,
628    pub status: BybitInstrumentStatus,
629    pub base_coin: Ustr,
630    pub quote_coin: Ustr,
631    pub launch_time: String,
632    pub delivery_time: String,
633    pub delivery_fee_rate: String,
634    pub price_scale: String,
635    pub leverage_filter: LeverageFilter,
636    pub price_filter: LinearPriceFilter,
637    pub lot_size_filter: LinearLotSizeFilter,
638    pub unified_margin_trade: bool,
639    pub funding_interval: i64,
640    pub settle_coin: Ustr,
641    #[serde(default)]
642    pub symbol_id: Option<i64>,
643    #[serde(default, deserialize_with = "deserialize_optional_symbol_type")]
644    pub symbol_type: Option<BybitSymbolType>,
645    /// Ticker of the underlying in its native market (TradFi only).
646    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
647    pub underlying_ticker: Option<String>,
648    /// Full name of the underlying asset.
649    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
650    pub full_name: Option<String>,
651    /// Market region of the underlying, which sets its trading calendar (TradFi only).
652    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
653    pub market_region: Option<String>,
654}
655
656/// Instrument definition for inverse contracts.
657///
658/// # References
659/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
660#[derive(Clone, Debug, Serialize, Deserialize)]
661#[serde(rename_all = "camelCase")]
662pub struct BybitInstrumentInverse {
663    pub symbol: Ustr,
664    pub contract_type: BybitContractType,
665    pub status: BybitInstrumentStatus,
666    pub base_coin: Ustr,
667    pub quote_coin: Ustr,
668    pub launch_time: String,
669    pub delivery_time: String,
670    pub delivery_fee_rate: String,
671    pub price_scale: String,
672    pub leverage_filter: LeverageFilter,
673    pub price_filter: LinearPriceFilter,
674    pub lot_size_filter: LinearLotSizeFilter,
675    pub unified_margin_trade: bool,
676    pub funding_interval: i64,
677    pub settle_coin: Ustr,
678    #[serde(default)]
679    pub symbol_id: Option<i64>,
680    #[serde(default, deserialize_with = "deserialize_optional_symbol_type")]
681    pub symbol_type: Option<BybitSymbolType>,
682}
683
684/// Instrument definition for option contracts.
685///
686/// # References
687/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
688#[derive(Clone, Debug, Serialize, Deserialize)]
689#[serde(rename_all = "camelCase")]
690pub struct BybitInstrumentOption {
691    pub symbol: Ustr,
692    pub status: BybitInstrumentStatus,
693    pub base_coin: Ustr,
694    pub quote_coin: Ustr,
695    pub settle_coin: Ustr,
696    pub options_type: BybitOptionType,
697    pub launch_time: String,
698    pub delivery_time: String,
699    pub delivery_fee_rate: String,
700    pub price_filter: LinearPriceFilter,
701    pub lot_size_filter: OptionLotSizeFilter,
702    #[serde(default)]
703    pub symbol_id: Option<i64>,
704}
705
706/// Response alias for instrument info requests that return spot instruments.
707///
708/// # References
709/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
710pub type BybitInstrumentSpotResponse = BybitCursorListResponse<BybitInstrumentSpot>;
711/// Response alias for instrument info requests that return linear contracts.
712///
713/// # References
714/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
715pub type BybitInstrumentLinearResponse = BybitCursorListResponse<BybitInstrumentLinear>;
716/// Response alias for instrument info requests that return inverse contracts.
717///
718/// # References
719/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
720pub type BybitInstrumentInverseResponse = BybitCursorListResponse<BybitInstrumentInverse>;
721/// Response alias for instrument info requests that return option contracts.
722///
723/// # References
724/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
725pub type BybitInstrumentOptionResponse = BybitCursorListResponse<BybitInstrumentOption>;
726
727/// Fee rate structure returned by `GET /v5/account/fee-rate`.
728///
729/// # References
730/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
731#[derive(Clone, Debug, Serialize, Deserialize)]
732#[serde(rename_all = "camelCase")]
733#[cfg_attr(
734    feature = "python",
735    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
736)]
737#[cfg_attr(
738    feature = "python",
739    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
740)]
741pub struct BybitFeeRate {
742    pub symbol: Ustr,
743    pub taker_fee_rate: String,
744    pub maker_fee_rate: String,
745    #[serde(default)]
746    pub base_coin: Option<Ustr>,
747}
748
749#[cfg(feature = "python")]
750#[pyo3::pymethods]
751impl BybitFeeRate {
752    #[getter]
753    #[must_use]
754    pub fn symbol(&self) -> &str {
755        self.symbol.as_str()
756    }
757
758    #[getter]
759    #[must_use]
760    pub fn taker_fee_rate(&self) -> &str {
761        &self.taker_fee_rate
762    }
763
764    #[getter]
765    #[must_use]
766    pub fn maker_fee_rate(&self) -> &str {
767        &self.maker_fee_rate
768    }
769
770    #[getter]
771    #[must_use]
772    pub fn base_coin(&self) -> Option<&str> {
773        self.base_coin.as_ref().map(|u| u.as_str())
774    }
775}
776
777/// Response alias for fee rate requests.
778///
779/// # References
780/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
781pub type BybitFeeRateResponse = BybitListResponse<BybitFeeRate>;
782
783/// Account balance snapshot coin entry.
784///
785/// # References
786/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
787#[derive(Clone, Debug, Serialize, Deserialize)]
788#[serde(rename_all = "camelCase")]
789pub struct BybitCoinBalance {
790    pub available_to_borrow: String,
791    pub bonus: String,
792    pub accrued_interest: String,
793    pub available_to_withdraw: String,
794    #[serde(default, rename = "totalOrderIM")]
795    pub total_order_im: Option<String>,
796    pub equity: String,
797    pub usd_value: String,
798    pub borrow_amount: String,
799    #[serde(default, rename = "totalPositionMM")]
800    pub total_position_mm: Option<String>,
801    #[serde(default, rename = "totalPositionIM")]
802    pub total_position_im: Option<String>,
803    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
804    pub wallet_balance: Decimal,
805    pub unrealised_pnl: String,
806    pub cum_realised_pnl: String,
807    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
808    pub locked: Decimal,
809    pub collateral_switch: bool,
810    pub margin_collateral: bool,
811    pub coin: Ustr,
812    #[serde(default)]
813    pub spot_hedging_qty: Option<String>,
814    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
815    pub spot_borrow: Decimal,
816}
817
818/// Wallet balance snapshot containing per-coin balances.
819///
820/// # References
821/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
822#[derive(Clone, Debug, Serialize, Deserialize)]
823#[serde(rename_all = "camelCase")]
824pub struct BybitWalletBalance {
825    pub total_equity: String,
826    #[serde(rename = "accountIMRate")]
827    pub account_im_rate: String,
828    pub total_margin_balance: String,
829    pub total_initial_margin: String,
830    pub account_type: BybitAccountType,
831    pub total_available_balance: String,
832    #[serde(rename = "accountMMRate")]
833    pub account_mm_rate: String,
834    #[serde(rename = "totalPerpUPL")]
835    pub total_perp_upl: String,
836    pub total_wallet_balance: String,
837    #[serde(rename = "accountLTV")]
838    pub account_ltv: String,
839    pub total_maintenance_margin: String,
840    pub coin: Vec<BybitCoinBalance>,
841}
842
843/// Response alias for wallet balance requests.
844///
845/// # References
846/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
847pub type BybitWalletBalanceResponse = BybitListResponse<BybitWalletBalance>;
848
849/// Account-level configuration returned by `GET /v5/account/info`.
850///
851/// # References
852/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
853#[derive(Clone, Debug, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct BybitAccountInfo {
856    pub unified_margin_status: BybitUnifiedMarginStatus,
857    pub margin_mode: BybitMarginMode,
858    pub is_master_trader: bool,
859    #[serde(with = "on_off_bool")]
860    pub spot_hedging_status: bool,
861    pub updated_time: String,
862    // `dcp_status`, `time_window`, and `smp_group` are absent from responses
863    // for accounts that predate the disconnection-protection feature.
864    #[serde(default, with = "on_off_bool")]
865    pub dcp_status: bool,
866    #[serde(default, deserialize_with = "deserialize_i32_or_string")]
867    pub time_window: i32,
868    #[serde(default, deserialize_with = "deserialize_i32_or_string")]
869    pub smp_group: i32,
870}
871
872/// Response alias for account info requests.
873///
874/// # References
875/// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
876pub type BybitAccountInfoResponse = BybitResponse<BybitAccountInfo>;
877
878/// Order representation as returned by order-related endpoints.
879///
880/// # References
881/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
882#[derive(Clone, Debug, Serialize, Deserialize)]
883#[cfg_attr(
884    feature = "python",
885    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
886)]
887#[cfg_attr(
888    feature = "python",
889    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
890)]
891#[serde(rename_all = "camelCase")]
892pub struct BybitOrder {
893    pub order_id: Ustr,
894    pub order_link_id: Ustr,
895    pub block_trade_id: Option<Ustr>,
896    pub symbol: Ustr,
897    pub price: String,
898    pub qty: String,
899    pub side: BybitOrderSide,
900    pub is_leverage: String,
901    #[serde(deserialize_with = "deserialize_i32_or_string")]
902    pub position_idx: i32,
903    pub order_status: BybitOrderStatus,
904    pub cancel_type: BybitCancelType,
905    pub reject_reason: Ustr,
906    pub avg_price: Option<String>,
907    pub leaves_qty: String,
908    pub leaves_value: String,
909    pub cum_exec_qty: String,
910    pub cum_exec_value: String,
911    pub cum_exec_fee: String,
912    pub time_in_force: BybitTimeInForce,
913    pub order_type: BybitOrderType,
914    pub stop_order_type: BybitStopOrderType,
915    pub order_iv: Option<String>,
916    pub trigger_price: String,
917    pub take_profit: String,
918    pub stop_loss: String,
919    pub tp_trigger_by: BybitTriggerType,
920    pub sl_trigger_by: BybitTriggerType,
921    pub trigger_direction: BybitTriggerDirection,
922    pub trigger_by: BybitTriggerType,
923    pub last_price_on_created: String,
924    pub reduce_only: bool,
925    pub close_on_trigger: bool,
926    pub smp_type: BybitSmpType,
927    #[serde(deserialize_with = "deserialize_i32_or_string")]
928    pub smp_group: i32,
929    pub smp_order_id: Ustr,
930    pub tpsl_mode: Option<BybitTpSlMode>,
931    #[serde(default)]
932    pub tp_limit_price: String,
933    #[serde(default)]
934    pub sl_limit_price: String,
935    pub place_type: Ustr,
936    pub created_time: String,
937    pub updated_time: String,
938}
939
940#[cfg(feature = "python")]
941#[pyo3::pymethods]
942impl BybitOrder {
943    #[getter]
944    #[must_use]
945    pub fn order_id(&self) -> &str {
946        self.order_id.as_str()
947    }
948
949    #[getter]
950    #[must_use]
951    pub fn order_link_id(&self) -> &str {
952        self.order_link_id.as_str()
953    }
954
955    #[getter]
956    #[must_use]
957    pub fn block_trade_id(&self) -> Option<&str> {
958        self.block_trade_id.as_ref().map(|s| s.as_str())
959    }
960
961    #[getter]
962    #[must_use]
963    pub fn symbol(&self) -> &str {
964        self.symbol.as_str()
965    }
966
967    #[getter]
968    #[must_use]
969    pub fn price(&self) -> &str {
970        &self.price
971    }
972
973    #[getter]
974    #[must_use]
975    pub fn qty(&self) -> &str {
976        &self.qty
977    }
978
979    #[getter]
980    #[must_use]
981    pub fn side(&self) -> BybitOrderSide {
982        self.side
983    }
984
985    #[getter]
986    #[must_use]
987    pub fn is_leverage(&self) -> &str {
988        &self.is_leverage
989    }
990
991    #[getter]
992    #[must_use]
993    pub fn position_idx(&self) -> i32 {
994        self.position_idx
995    }
996
997    #[getter]
998    #[must_use]
999    pub fn order_status(&self) -> BybitOrderStatus {
1000        self.order_status
1001    }
1002
1003    #[getter]
1004    #[must_use]
1005    pub fn cancel_type(&self) -> BybitCancelType {
1006        self.cancel_type
1007    }
1008
1009    #[getter]
1010    #[must_use]
1011    pub fn reject_reason(&self) -> &str {
1012        self.reject_reason.as_str()
1013    }
1014
1015    #[getter]
1016    #[must_use]
1017    pub fn avg_price(&self) -> Option<&str> {
1018        self.avg_price.as_deref()
1019    }
1020
1021    #[getter]
1022    #[must_use]
1023    pub fn leaves_qty(&self) -> &str {
1024        &self.leaves_qty
1025    }
1026
1027    #[getter]
1028    #[must_use]
1029    pub fn leaves_value(&self) -> &str {
1030        &self.leaves_value
1031    }
1032
1033    #[getter]
1034    #[must_use]
1035    pub fn cum_exec_qty(&self) -> &str {
1036        &self.cum_exec_qty
1037    }
1038
1039    #[getter]
1040    #[must_use]
1041    pub fn cum_exec_value(&self) -> &str {
1042        &self.cum_exec_value
1043    }
1044
1045    #[getter]
1046    #[must_use]
1047    pub fn cum_exec_fee(&self) -> &str {
1048        &self.cum_exec_fee
1049    }
1050
1051    #[getter]
1052    #[must_use]
1053    pub fn time_in_force(&self) -> BybitTimeInForce {
1054        self.time_in_force
1055    }
1056
1057    #[getter]
1058    #[must_use]
1059    pub fn order_type(&self) -> BybitOrderType {
1060        self.order_type
1061    }
1062
1063    #[getter]
1064    #[must_use]
1065    pub fn stop_order_type(&self) -> BybitStopOrderType {
1066        self.stop_order_type
1067    }
1068
1069    #[getter]
1070    #[must_use]
1071    pub fn order_iv(&self) -> Option<&str> {
1072        self.order_iv.as_deref()
1073    }
1074
1075    #[getter]
1076    #[must_use]
1077    pub fn trigger_price(&self) -> &str {
1078        &self.trigger_price
1079    }
1080
1081    #[getter]
1082    #[must_use]
1083    pub fn take_profit(&self) -> &str {
1084        &self.take_profit
1085    }
1086
1087    #[getter]
1088    #[must_use]
1089    pub fn stop_loss(&self) -> &str {
1090        &self.stop_loss
1091    }
1092
1093    #[getter]
1094    #[must_use]
1095    pub fn tp_trigger_by(&self) -> BybitTriggerType {
1096        self.tp_trigger_by
1097    }
1098
1099    #[getter]
1100    #[must_use]
1101    pub fn sl_trigger_by(&self) -> BybitTriggerType {
1102        self.sl_trigger_by
1103    }
1104
1105    #[getter]
1106    #[must_use]
1107    pub fn trigger_direction(&self) -> BybitTriggerDirection {
1108        self.trigger_direction
1109    }
1110
1111    #[getter]
1112    #[must_use]
1113    pub fn trigger_by(&self) -> BybitTriggerType {
1114        self.trigger_by
1115    }
1116
1117    #[getter]
1118    #[must_use]
1119    pub fn last_price_on_created(&self) -> &str {
1120        &self.last_price_on_created
1121    }
1122
1123    #[getter]
1124    #[must_use]
1125    pub fn reduce_only(&self) -> bool {
1126        self.reduce_only
1127    }
1128
1129    #[getter]
1130    #[must_use]
1131    pub fn close_on_trigger(&self) -> bool {
1132        self.close_on_trigger
1133    }
1134
1135    #[getter]
1136    #[must_use]
1137    #[expect(
1138        clippy::missing_panics_doc,
1139        reason = "serialization of a simple enum cannot fail"
1140    )]
1141    pub fn smp_type(&self) -> String {
1142        serde_json::to_string(&self.smp_type)
1143            .expect("Failed to serialize BybitSmpType")
1144            .trim_matches('"')
1145            .to_string()
1146    }
1147
1148    #[getter]
1149    #[must_use]
1150    pub fn smp_group(&self) -> i32 {
1151        self.smp_group
1152    }
1153
1154    #[getter]
1155    #[must_use]
1156    pub fn smp_order_id(&self) -> &str {
1157        self.smp_order_id.as_str()
1158    }
1159
1160    #[getter]
1161    #[must_use]
1162    pub fn tpsl_mode(&self) -> Option<BybitTpSlMode> {
1163        self.tpsl_mode
1164    }
1165
1166    #[getter]
1167    #[must_use]
1168    pub fn tp_limit_price(&self) -> &str {
1169        &self.tp_limit_price
1170    }
1171
1172    #[getter]
1173    #[must_use]
1174    pub fn sl_limit_price(&self) -> &str {
1175        &self.sl_limit_price
1176    }
1177
1178    #[getter]
1179    #[must_use]
1180    pub fn place_type(&self) -> &str {
1181        self.place_type.as_str()
1182    }
1183
1184    #[getter]
1185    #[must_use]
1186    pub fn created_time(&self) -> &str {
1187        &self.created_time
1188    }
1189
1190    #[getter]
1191    #[must_use]
1192    pub fn updated_time(&self) -> &str {
1193        &self.updated_time
1194    }
1195}
1196
1197/// Response alias for open order queries.
1198///
1199/// # References
1200/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1201pub type BybitOpenOrdersResponse = BybitCursorListResponse<BybitOrder>;
1202/// Response alias for order history queries with pagination.
1203///
1204/// # References
1205/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
1206pub type BybitOrderHistoryResponse = BybitCursorListResponse<BybitOrder>;
1207
1208/// Payload returned after placing a single order.
1209///
1210/// # References
1211/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1212#[derive(Clone, Debug, Serialize, Deserialize)]
1213#[serde(rename_all = "camelCase")]
1214pub struct BybitPlaceOrderResult {
1215    pub order_id: Option<Ustr>,
1216    pub order_link_id: Option<Ustr>,
1217}
1218
1219/// Response alias for order placement endpoints.
1220///
1221/// # References
1222/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1223pub type BybitPlaceOrderResponse = BybitResponse<BybitPlaceOrderResult>;
1224
1225/// Payload returned after cancelling a single order.
1226///
1227/// # References
1228/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1229#[derive(Clone, Debug, Serialize, Deserialize)]
1230#[serde(rename_all = "camelCase")]
1231pub struct BybitCancelOrderResult {
1232    pub order_id: Option<Ustr>,
1233    pub order_link_id: Option<Ustr>,
1234}
1235
1236/// Response alias for order cancellation endpoints.
1237///
1238/// # References
1239/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
1240pub type BybitCancelOrderResponse = BybitResponse<BybitCancelOrderResult>;
1241
1242/// Execution/Fill payload returned by `GET /v5/execution/list`.
1243///
1244/// # References
1245/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1246#[derive(Clone, Debug, Serialize, Deserialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct BybitExecution {
1249    pub symbol: Ustr,
1250    pub order_id: Ustr,
1251    pub order_link_id: Ustr,
1252    pub side: BybitOrderSide,
1253    pub order_price: String,
1254    pub order_qty: String,
1255    pub leaves_qty: String,
1256    pub create_type: Option<BybitCreateType>,
1257    pub order_type: BybitOrderType,
1258    pub stop_order_type: Option<BybitStopOrderType>,
1259    pub exec_fee: String,
1260    pub exec_id: String,
1261    pub exec_price: String,
1262    pub exec_qty: String,
1263    pub exec_type: BybitExecType,
1264    pub exec_value: String,
1265    pub exec_time: String,
1266    pub fee_currency: Ustr,
1267    pub is_maker: bool,
1268    pub fee_rate: String,
1269    pub trade_iv: String,
1270    pub mark_iv: String,
1271    pub mark_price: String,
1272    pub index_price: String,
1273    pub underlying_price: String,
1274    pub block_trade_id: String,
1275    pub closed_size: String,
1276    pub seq: i64,
1277}
1278
1279/// Response alias for trade history requests.
1280///
1281/// # References
1282/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1283pub type BybitTradeHistoryResponse = BybitCursorListResponse<BybitExecution>;
1284
1285/// Represents a position returned by the Bybit API.
1286///
1287/// # References
1288/// - <https://bybit-exchange.github.io/docs/v5/position>
1289#[derive(Clone, Debug, Serialize, Deserialize)]
1290#[serde(rename_all = "camelCase")]
1291pub struct BybitPosition {
1292    pub position_idx: BybitPositionIdx,
1293    #[serde(deserialize_with = "deserialize_i32_or_string")]
1294    pub risk_id: i32,
1295    pub risk_limit_value: String,
1296    pub symbol: Ustr,
1297    pub side: BybitPositionSide,
1298    pub size: String,
1299    pub avg_price: String,
1300    pub position_value: String,
1301    #[serde(deserialize_with = "deserialize_i32_or_string")]
1302    pub trade_mode: i32,
1303    pub position_status: BybitPositionStatus,
1304    #[serde(deserialize_with = "deserialize_i32_or_string")]
1305    pub auto_add_margin: i32,
1306    #[serde(deserialize_with = "deserialize_i32_or_string")]
1307    pub adl_rank_indicator: i32,
1308    pub leverage: String,
1309    pub position_balance: String,
1310    pub mark_price: String,
1311    pub liq_price: String,
1312    pub bust_price: String,
1313    #[serde(rename = "positionMM")]
1314    pub position_mm: String,
1315    #[serde(rename = "positionIM")]
1316    pub position_im: String,
1317    pub tpsl_mode: BybitTpSlMode,
1318    pub take_profit: String,
1319    pub stop_loss: String,
1320    pub trailing_stop: String,
1321    pub unrealised_pnl: String,
1322    pub cur_realised_pnl: String,
1323    pub cum_realised_pnl: String,
1324    #[serde(default = "default_position_seq")]
1325    pub seq: i64,
1326    #[serde(default)]
1327    pub is_reduce_only: bool,
1328    #[serde(default)]
1329    pub mmr_sys_updated_time: String,
1330    #[serde(default)]
1331    pub leverage_sys_updated_time: String,
1332    pub created_time: String,
1333    pub updated_time: String,
1334    #[serde(default)]
1335    pub open_time: i64,
1336}
1337
1338const fn default_position_seq() -> i64 {
1339    -1
1340}
1341
1342/// Response alias for position list requests.
1343///
1344/// # References
1345/// - <https://bybit-exchange.github.io/docs/v5/position>
1346pub type BybitPositionListResponse = BybitCursorListResponse<BybitPosition>;
1347
1348/// Reason detail for set margin mode failures.
1349///
1350/// # References
1351/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1352#[derive(Clone, Debug, Serialize, Deserialize)]
1353#[serde(rename_all = "camelCase")]
1354pub struct BybitSetMarginModeReason {
1355    pub reason_code: String,
1356    pub reason_msg: String,
1357}
1358
1359/// Result payload for set margin mode operation.
1360///
1361/// # References
1362/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1363#[derive(Clone, Debug, Serialize, Deserialize)]
1364#[serde(rename_all = "camelCase")]
1365pub struct BybitSetMarginModeResult {
1366    #[serde(default)]
1367    pub reasons: Vec<BybitSetMarginModeReason>,
1368}
1369
1370/// Response alias for set margin mode requests.
1371///
1372/// # References
1373/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1374pub type BybitSetMarginModeResponse = BybitResponse<BybitSetMarginModeResult>;
1375
1376/// Empty result for set leverage operation.
1377#[derive(Clone, Debug, Serialize, Deserialize)]
1378pub struct BybitSetLeverageResult {}
1379
1380/// Response alias for set leverage requests.
1381///
1382/// # References
1383/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1384pub type BybitSetLeverageResponse = BybitResponse<BybitSetLeverageResult>;
1385
1386/// Empty result for switch mode operation.
1387#[derive(Clone, Debug, Serialize, Deserialize)]
1388pub struct BybitSwitchModeResult {}
1389
1390/// Response alias for switch mode requests.
1391///
1392/// # References
1393/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1394pub type BybitSwitchModeResponse = BybitResponse<BybitSwitchModeResult>;
1395
1396/// Empty result for set trading stop operation.
1397#[derive(Clone, Debug, Serialize, Deserialize)]
1398pub struct BybitSetTradingStopResult {}
1399
1400/// Response alias for set trading stop requests.
1401///
1402/// # References
1403/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1404pub type BybitSetTradingStopResponse = BybitResponse<BybitSetTradingStopResult>;
1405
1406/// Result from manual borrow operation.
1407#[derive(Clone, Debug, Serialize, Deserialize)]
1408#[serde(rename_all = "camelCase")]
1409pub struct BybitBorrowResult {
1410    pub coin: Ustr,
1411    pub amount: String,
1412}
1413
1414/// Response alias for manual borrow requests.
1415///
1416/// # References
1417///
1418/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1419pub type BybitBorrowResponse = BybitResponse<BybitBorrowResult>;
1420
1421/// Result from no-convert repay operation.
1422#[derive(Clone, Debug, Serialize, Deserialize)]
1423#[serde(rename_all = "camelCase")]
1424pub struct BybitNoConvertRepayResult {
1425    pub result_status: BybitRepayStatus,
1426}
1427
1428/// Response alias for no-convert repay requests.
1429///
1430/// # References
1431///
1432/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1433pub type BybitNoConvertRepayResponse = BybitResponse<BybitNoConvertRepayResult>;
1434
1435/// Result from a manual repay (with conversion) operation.
1436#[derive(Clone, Debug, Serialize, Deserialize)]
1437#[serde(rename_all = "camelCase")]
1438pub struct BybitRepayResult {
1439    pub result_status: BybitRepayStatus,
1440}
1441
1442/// Response alias for manual repay requests.
1443///
1444/// # References
1445///
1446/// - <https://bybit-exchange.github.io/docs/v5/account/repay>
1447pub type BybitRepayResponse = BybitResponse<BybitRepayResult>;
1448
1449/// API key permissions.
1450#[derive(Clone, Debug, Serialize, Deserialize)]
1451#[cfg_attr(
1452    feature = "python",
1453    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
1454)]
1455#[cfg_attr(
1456    feature = "python",
1457    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1458)]
1459#[serde(rename_all = "PascalCase")]
1460pub struct BybitApiKeyPermissions {
1461    #[serde(default)]
1462    pub contract_trade: Vec<String>,
1463    #[serde(default)]
1464    pub spot: Vec<String>,
1465    #[serde(default)]
1466    pub wallet: Vec<String>,
1467    #[serde(default)]
1468    pub options: Vec<String>,
1469    #[serde(default)]
1470    pub derivatives: Vec<String>,
1471    #[serde(default)]
1472    pub exchange: Vec<String>,
1473    #[serde(default)]
1474    pub copy_trading: Vec<String>,
1475    #[serde(default)]
1476    pub block_trade: Vec<String>,
1477    // Bybit ships this key uppercase (`"NFT"`); the struct-level PascalCase
1478    // rule would otherwise serialize it as `"Nft"` and silently drop values.
1479    #[serde(rename = "NFT", default)]
1480    pub nft: Vec<String>,
1481    #[serde(default)]
1482    pub affiliate: Vec<String>,
1483    // Newer permission buckets. Master-account responses populate them, sub-key
1484    // responses typically omit or return empty arrays - both cases deserialize
1485    // to an empty `Vec` via `serde(default)`.
1486    #[serde(default)]
1487    pub earn: Vec<String>,
1488    // Bybit uses `"FiatP2P"` - PascalCase rename would emit `"FiatP2p"`.
1489    #[serde(rename = "FiatP2P", default)]
1490    pub fiat_p2p: Vec<String>,
1491    #[serde(default)]
1492    pub fiat_bybit_pay: Vec<String>,
1493    #[serde(default)]
1494    pub fiat_bit_pay: Vec<String>,
1495    #[serde(default)]
1496    pub fiat_global_pay: Vec<String>,
1497    #[serde(default)]
1498    pub fiat_convert_broker: Vec<String>,
1499    #[serde(default)]
1500    pub bit_card: Vec<String>,
1501    // Bybit uses `"ByXPost"` - PascalCase rename would emit `"ByxPost"`.
1502    #[serde(rename = "ByXPost", default)]
1503    pub byx_post: Vec<String>,
1504}
1505
1506/// Account details from API key info.
1507#[derive(Debug, Clone, Serialize, Deserialize)]
1508#[cfg_attr(
1509    feature = "python",
1510    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
1511)]
1512#[cfg_attr(
1513    feature = "python",
1514    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1515)]
1516#[serde(rename_all = "camelCase")]
1517pub struct BybitAccountDetails {
1518    pub id: String,
1519    pub note: String,
1520    pub api_key: SecretString,
1521    pub read_only: u8,
1522    pub secret: SecretString,
1523    #[serde(rename = "type")]
1524    pub key_type: u8,
1525    pub permissions: BybitApiKeyPermissions,
1526    pub ips: Vec<String>,
1527    #[serde(default)]
1528    pub user_id: Option<u64>,
1529    #[serde(default)]
1530    pub inviter_id: Option<u64>,
1531    pub vip_level: String,
1532    #[serde(deserialize_with = "deserialize_string_to_u8", default)]
1533    pub mkt_maker_level: u8,
1534    #[serde(default)]
1535    pub affiliate_id: Option<u64>,
1536    pub rsa_public_key: String,
1537    pub is_master: bool,
1538    pub parent_uid: String,
1539    pub uta: u8,
1540    pub kyc_level: String,
1541    pub kyc_region: String,
1542    #[serde(default)]
1543    pub unified: Option<i32>,
1544    #[serde(default)]
1545    pub deadline_day: i64,
1546    #[serde(default)]
1547    pub expired_at: Option<String>,
1548    pub created_at: String,
1549}
1550
1551#[cfg(feature = "python")]
1552#[pyo3::pymethods]
1553impl BybitAccountDetails {
1554    #[getter]
1555    #[must_use]
1556    pub fn id(&self) -> &str {
1557        &self.id
1558    }
1559
1560    #[getter]
1561    #[must_use]
1562    pub fn note(&self) -> &str {
1563        &self.note
1564    }
1565
1566    #[getter]
1567    #[must_use]
1568    pub fn api_key(&self) -> &str {
1569        self.api_key.expose_secret()
1570    }
1571
1572    #[getter]
1573    #[must_use]
1574    pub fn read_only(&self) -> u8 {
1575        self.read_only
1576    }
1577
1578    #[getter]
1579    #[must_use]
1580    pub fn key_type(&self) -> u8 {
1581        self.key_type
1582    }
1583
1584    #[getter]
1585    #[must_use]
1586    pub fn user_id(&self) -> Option<u64> {
1587        self.user_id
1588    }
1589
1590    #[getter]
1591    #[must_use]
1592    pub fn inviter_id(&self) -> Option<u64> {
1593        self.inviter_id
1594    }
1595
1596    #[getter]
1597    #[must_use]
1598    pub fn vip_level(&self) -> &str {
1599        &self.vip_level
1600    }
1601
1602    #[getter]
1603    #[must_use]
1604    pub fn mkt_maker_level(&self) -> u8 {
1605        self.mkt_maker_level
1606    }
1607
1608    #[getter]
1609    #[must_use]
1610    pub fn affiliate_id(&self) -> Option<u64> {
1611        self.affiliate_id
1612    }
1613
1614    #[getter]
1615    #[must_use]
1616    pub fn rsa_public_key(&self) -> &str {
1617        &self.rsa_public_key
1618    }
1619
1620    #[getter]
1621    #[must_use]
1622    pub fn is_master(&self) -> bool {
1623        self.is_master
1624    }
1625
1626    #[getter]
1627    #[must_use]
1628    pub fn parent_uid(&self) -> &str {
1629        &self.parent_uid
1630    }
1631
1632    #[getter]
1633    #[must_use]
1634    pub fn uta(&self) -> u8 {
1635        self.uta
1636    }
1637
1638    #[getter]
1639    #[must_use]
1640    pub fn kyc_level(&self) -> &str {
1641        &self.kyc_level
1642    }
1643
1644    #[getter]
1645    #[must_use]
1646    pub fn kyc_region(&self) -> &str {
1647        &self.kyc_region
1648    }
1649
1650    #[getter]
1651    #[must_use]
1652    pub fn deadline_day(&self) -> i64 {
1653        self.deadline_day
1654    }
1655
1656    #[getter]
1657    #[must_use]
1658    pub fn expired_at(&self) -> Option<&str> {
1659        self.expired_at.as_deref()
1660    }
1661
1662    #[getter]
1663    #[must_use]
1664    pub fn created_at(&self) -> &str {
1665        &self.created_at
1666    }
1667}
1668
1669/// Response alias for API key info requests.
1670///
1671/// # References
1672///
1673/// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1674pub type BybitAccountDetailsResponse = BybitResponse<BybitAccountDetails>;
1675
1676/// Basic information about a sub-account member.
1677///
1678/// `member_type`, `status`, and `account_mode` use raw integer codes whose valid
1679/// ranges differ per endpoint; values are kept as-is rather than mapped to Rust
1680/// enums, consistent with other venue-raw fields in this module.
1681///
1682/// # References
1683///
1684/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1685/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1686/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1687#[derive(Clone, Debug, Serialize, Deserialize)]
1688#[serde(rename_all = "camelCase")]
1689pub struct BybitSubMember {
1690    pub uid: String,
1691    pub username: String,
1692    pub member_type: i32,
1693    pub status: i32,
1694    pub account_mode: i32,
1695    #[serde(default)]
1696    pub remark: String,
1697}
1698
1699/// Result payload for `GET /v5/user/query-sub-members`.
1700#[derive(Clone, Debug, Serialize, Deserialize)]
1701#[serde(rename_all = "camelCase")]
1702pub struct BybitSubMembersResult {
1703    #[serde(default)]
1704    pub sub_members: Vec<BybitSubMember>,
1705}
1706
1707/// Response alias for the non-paginated sub-UID list.
1708///
1709/// # References
1710///
1711/// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
1712pub type BybitSubMembersResponse = BybitResponse<BybitSubMembersResult>;
1713
1714/// Result payload for cursor-paginated sub-account listings.
1715///
1716/// The inner array is named `subMembers` and the cursor field is `nextCursor`
1717/// (with `"0"` as the end-of-pages sentinel), so the standard
1718/// `BybitCursorListResponse<T>` (which expects `list` / `nextPageCursor`)
1719/// cannot be reused here. Callers treat `"0"` or an empty string as the
1720/// termination sentinel.
1721#[derive(Clone, Debug, Serialize, Deserialize)]
1722#[serde(rename_all = "camelCase")]
1723pub struct BybitSubMembersPagedResult {
1724    #[serde(default)]
1725    pub sub_members: Vec<BybitSubMember>,
1726    #[serde(default)]
1727    pub next_cursor: Option<String>,
1728}
1729
1730impl BybitSubMembersPagedResult {
1731    /// Returns the cursor to use for the next page, or `None` when the final
1732    /// page has been fetched.
1733    ///
1734    /// Bybit signals end-of-pages either by omitting the cursor or returning
1735    /// `"0"`/`""`; both cases collapse to `None` here so callers can treat any
1736    /// non-`None` return value as a live cursor.
1737    #[must_use]
1738    pub fn continuation_cursor(&self) -> Option<&str> {
1739        match self.next_cursor.as_deref() {
1740            None | Some("" | "0") => None,
1741            Some(cursor) => Some(cursor),
1742        }
1743    }
1744
1745    /// Returns `true` when the result has more pages to fetch.
1746    #[must_use]
1747    pub fn has_more_pages(&self) -> bool {
1748        self.continuation_cursor().is_some()
1749    }
1750}
1751
1752/// Response alias for paginated sub-UID list (`/v5/user/submembers`).
1753///
1754/// # References
1755///
1756/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
1757pub type BybitSubMembersPagedResponse = BybitResponse<BybitSubMembersPagedResult>;
1758
1759/// Response alias for the escrow (fund-custodial) sub-account list
1760/// (`/v5/user/escrow_sub_members`); shares the paginated sub-member shape.
1761///
1762/// # References
1763///
1764/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
1765pub type BybitEscrowSubMembersResponse = BybitResponse<BybitSubMembersPagedResult>;
1766
1767/// Information about a single sub-account API key.
1768///
1769/// Deliberately not shared with [`BybitAccountDetails`]: master-level fields
1770/// such as `is_master`, `parent_uid`, `uta`, and the KYC block are absent.
1771///
1772/// # References
1773///
1774/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1775#[derive(Debug, Clone, Serialize, Deserialize)]
1776#[serde(rename_all = "camelCase")]
1777pub struct BybitSubApiKeyInfo {
1778    pub id: String,
1779    #[serde(default)]
1780    pub ips: Vec<String>,
1781    pub api_key: SecretString,
1782    #[serde(default)]
1783    pub note: String,
1784    pub status: i32,
1785    #[serde(default)]
1786    pub expired_at: Option<String>,
1787    pub created_at: String,
1788    #[serde(rename = "type")]
1789    pub key_type: BybitApiKeyType,
1790    #[serde(with = "masked_secret")]
1791    pub secret: Option<SecretString>,
1792    #[serde(with = "bool_or_int")]
1793    pub read_only: bool,
1794    #[serde(default)]
1795    pub deadline_day: Option<i64>,
1796    #[serde(default)]
1797    pub flag: String,
1798    pub permissions: BybitApiKeyPermissions,
1799}
1800
1801/// Result payload for `GET /v5/user/sub-apikeys`.
1802///
1803/// The inner array field is named `result` (nested inside the outer
1804/// `retCode/retMsg/result` envelope) rather than the usual `list`, so the
1805/// standard `BybitCursorListResponse<T>` cannot be reused here.
1806#[derive(Clone, Debug, Serialize, Deserialize)]
1807#[serde(rename_all = "camelCase")]
1808pub struct BybitSubApiKeysResult {
1809    #[serde(rename = "result", default)]
1810    pub keys: Vec<BybitSubApiKeyInfo>,
1811    #[serde(default)]
1812    pub next_page_cursor: Option<String>,
1813}
1814
1815impl BybitSubApiKeysResult {
1816    /// Returns the cursor to use for the next page, or `None` when the final
1817    /// page has been fetched.
1818    ///
1819    /// The end-of-pages sentinel on this endpoint is an empty string rather
1820    /// than `"0"`; both that and a missing cursor collapse to `None`.
1821    #[must_use]
1822    pub fn continuation_cursor(&self) -> Option<&str> {
1823        match self.next_page_cursor.as_deref() {
1824            None | Some("") => None,
1825            Some(cursor) => Some(cursor),
1826        }
1827    }
1828
1829    /// Returns `true` when the result has more pages to fetch.
1830    #[must_use]
1831    pub fn has_more_pages(&self) -> bool {
1832        self.continuation_cursor().is_some()
1833    }
1834}
1835
1836/// Response alias for sub-account API keys list.
1837///
1838/// # References
1839///
1840/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1841pub type BybitSubApiKeysResponse = BybitResponse<BybitSubApiKeysResult>;
1842
1843/// Shared result payload for API-key update endpoints (sub or master).
1844///
1845/// `/v5/user/update-sub-api` and `/v5/user/update-api` return the same field
1846/// set; only the number of permission buckets populated inside `permissions`
1847/// differs. Because [`BybitApiKeyPermissions`] covers the superset of both,
1848/// the two endpoints reuse a single DTO.
1849#[derive(Debug, Clone, Serialize, Deserialize)]
1850#[serde(rename_all = "camelCase")]
1851pub struct BybitApiKeyUpdateResult {
1852    pub id: String,
1853    #[serde(default)]
1854    pub note: String,
1855    pub api_key: SecretString,
1856    #[serde(with = "bool_or_int")]
1857    pub read_only: bool,
1858    #[serde(with = "masked_secret")]
1859    pub secret: Option<SecretString>,
1860    pub permissions: BybitApiKeyPermissions,
1861    #[serde(default)]
1862    pub ips: Vec<String>,
1863}
1864
1865/// Response alias for `POST /v5/user/update-sub-api`.
1866///
1867/// # References
1868///
1869/// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
1870pub type BybitUpdateSubApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1871
1872/// Response alias for `POST /v5/user/update-api`.
1873///
1874/// # References
1875///
1876/// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
1877pub type BybitUpdateMasterApiResponse = BybitResponse<BybitApiKeyUpdateResult>;
1878
1879#[cfg(test)]
1880mod tests {
1881    use nautilus_core::UnixNanos;
1882    use nautilus_model::identifiers::AccountId;
1883    use rstest::rstest;
1884    use rust_decimal::Decimal;
1885    use rust_decimal_macros::dec;
1886
1887    use super::*;
1888    use crate::common::testing::load_test_json;
1889
1890    #[rstest]
1891    fn deserialize_spot_instrument_uses_enums() {
1892        let json = load_test_json("http_get_instruments_spot.json");
1893        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1894        let instrument = &response.result.list[0];
1895
1896        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1897        assert_eq!(instrument.innovation, BybitInnovationFlag::Standard);
1898        assert_eq!(instrument.margin_trading, BybitMarginTrading::UtaOnly);
1899    }
1900
1901    #[rstest]
1902    fn deserialize_linear_instrument_status() {
1903        let json = load_test_json("http_get_instruments_linear.json");
1904        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1905        let instrument = &response.result.list[0];
1906
1907        assert_eq!(instrument.status, BybitInstrumentStatus::Trading);
1908        assert_eq!(instrument.contract_type, BybitContractType::LinearPerpetual);
1909    }
1910
1911    #[rstest]
1912    fn deserialize_spot_instrument_with_xstock_fields() {
1913        let json = load_test_json("http_get_instruments_spot_xstocks.json");
1914        let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1915        let instrument = &response.result.list[0];
1916
1917        assert_eq!(instrument.symbol_id, Some(42));
1918        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Xstocks));
1919        assert_eq!(instrument.xstock_multiplier.as_deref(), Some("0.1"));
1920    }
1921
1922    #[rstest]
1923    fn deserialize_linear_instrument_with_symbol_type_and_id() {
1924        let json = load_test_json("http_get_instruments_linear_symbol_type.json");
1925        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1926        let instrument = &response.result.list[0];
1927
1928        assert_eq!(instrument.symbol_id, Some(7));
1929        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Stock));
1930        assert_eq!(instrument.underlying_ticker.as_deref(), Some("AMD"));
1931        assert_eq!(
1932            instrument.full_name.as_deref(),
1933            Some("Advanced Micro Devices")
1934        );
1935        assert_eq!(instrument.market_region.as_deref(), Some("US"));
1936    }
1937
1938    #[rstest]
1939    fn deserialize_linear_instrument_maps_empty_strings_to_none() {
1940        let json = load_test_json("http_get_instruments_linear_symbol_type.json")
1941            .replace(r#""symbolType": "stock""#, r#""symbolType": """#)
1942            .replace(r#""underlyingTicker": "AMD""#, r#""underlyingTicker": """#)
1943            .replace(
1944                r#""fullName": "Advanced Micro Devices""#,
1945                r#""fullName": """#,
1946            )
1947            .replace(r#""marketRegion": "US""#, r#""marketRegion": """#);
1948        let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1949        let instrument = &response.result.list[0];
1950
1951        assert_eq!(instrument.symbol_type, None);
1952        assert_eq!(instrument.underlying_ticker, None);
1953        assert_eq!(instrument.full_name, None);
1954        assert_eq!(instrument.market_region, None);
1955    }
1956
1957    #[derive(Deserialize)]
1958    struct SymbolTypeWrap {
1959        #[serde(rename = "symbolType")]
1960        t: BybitSymbolType,
1961    }
1962
1963    #[rstest]
1964    fn deserialize_symbol_type_falls_back_to_other_for_unknown() {
1965        let json = r#"{"symbolType": "newthing"}"#;
1966        let parsed: SymbolTypeWrap = serde_json::from_str(json).unwrap();
1967        assert_eq!(parsed.t, BybitSymbolType::Other);
1968    }
1969
1970    #[rstest]
1971    fn deserialize_account_info_response() {
1972        let json = load_test_json("http_get_account_info.json");
1973        let response: BybitAccountInfoResponse = serde_json::from_str(&json).unwrap();
1974
1975        assert_eq!(response.result.margin_mode, BybitMarginMode::RegularMargin);
1976        assert_eq!(
1977            response.result.unified_margin_status,
1978            BybitUnifiedMarginStatus::UnifiedTradingAccount10Pro
1979        );
1980        assert!(!response.result.is_master_trader);
1981        assert!(!response.result.spot_hedging_status);
1982        assert!(!response.result.dcp_status);
1983        assert_eq!(response.result.time_window, 10);
1984        assert_eq!(response.result.smp_group, 0);
1985    }
1986
1987    #[rstest]
1988    fn deserialize_account_info_without_deprecated_fields() {
1989        let json = r#"{
1990            "retCode": 0,
1991            "retMsg": "OK",
1992            "result": {
1993                "marginMode": "PORTFOLIO_MARGIN",
1994                "updatedTime": "1697078946000",
1995                "unifiedMarginStatus": 5,
1996                "isMasterTrader": true,
1997                "spotHedgingStatus": "ON"
1998            }
1999        }"#;
2000        let response: BybitAccountInfoResponse = serde_json::from_str(json).unwrap();
2001
2002        assert_eq!(
2003            response.result.margin_mode,
2004            BybitMarginMode::PortfolioMargin
2005        );
2006        assert_eq!(
2007            response.result.unified_margin_status,
2008            BybitUnifiedMarginStatus::UnifiedTradingAccount20
2009        );
2010        assert!(response.result.is_master_trader);
2011        assert!(response.result.spot_hedging_status);
2012        assert!(!response.result.dcp_status);
2013        assert_eq!(response.result.time_window, 0);
2014        assert_eq!(response.result.smp_group, 0);
2015    }
2016
2017    #[rstest]
2018    fn deserialize_account_info_accepts_string_time_window_and_smp_group() {
2019        let mut json: serde_json::Value =
2020            serde_json::from_str(&load_test_json("http_get_account_info.json")).unwrap();
2021        json["result"]["timeWindow"] = serde_json::Value::String("10".to_string());
2022        json["result"]["smpGroup"] = serde_json::Value::String("1234".to_string());
2023
2024        let response: BybitAccountInfoResponse = serde_json::from_value(json).unwrap();
2025
2026        assert_eq!(response.result.time_window, 10);
2027        assert_eq!(response.result.smp_group, 1234);
2028    }
2029
2030    #[rstest]
2031    fn deserialize_order_response_maps_enums() {
2032        let json = load_test_json("http_get_orders_history.json");
2033        let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
2034        let order = &response.result.list[0];
2035
2036        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
2037        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
2038        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
2039        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
2040        assert_eq!(order.order_type, BybitOrderType::Limit);
2041        assert_eq!(order.smp_type, BybitSmpType::None);
2042        assert_eq!(order.smp_group, 0);
2043    }
2044
2045    #[rstest]
2046    #[case::both_missing(true, true)]
2047    #[case::tp_missing(true, false)]
2048    #[case::sl_missing(false, true)]
2049    fn deserialize_option_order_response_without_tp_sl_limit_prices(
2050        #[case] omit_tp_limit_price: bool,
2051        #[case] omit_sl_limit_price: bool,
2052    ) {
2053        let mut json: serde_json::Value =
2054            serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2055        let order = json["result"]["list"][0].as_object_mut().unwrap();
2056        order.insert(
2057            "symbol".to_string(),
2058            serde_json::json!("BTC-25SEP26-86500-C-USDT"),
2059        );
2060        order.insert("orderIv".to_string(), serde_json::json!("0.45"));
2061        order.insert("tpLimitPrice".to_string(), serde_json::json!("31000"));
2062        order.insert("slLimitPrice".to_string(), serde_json::json!("29000"));
2063        if omit_tp_limit_price {
2064            order.remove("tpLimitPrice");
2065        }
2066
2067        if omit_sl_limit_price {
2068            order.remove("slLimitPrice");
2069        }
2070
2071        let mut expected = serde_json::Value::Object(order.clone());
2072        expected["tpLimitPrice"] =
2073            serde_json::json!(if omit_tp_limit_price { "" } else { "31000" });
2074        expected["slLimitPrice"] =
2075            serde_json::json!(if omit_sl_limit_price { "" } else { "29000" });
2076
2077        let response: BybitOrderHistoryResponse = serde_json::from_value(json).unwrap();
2078        let actual = serde_json::to_value(&response.result.list[0]).unwrap();
2079
2080        assert_eq!(response.result.list.len(), 1);
2081        assert_eq!(actual, expected);
2082    }
2083
2084    #[rstest]
2085    fn deserialize_order_response_accepts_string_smp_group() {
2086        let mut json: serde_json::Value =
2087            serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2088        json["result"]["list"][0]["smpGroup"] = serde_json::Value::String("123456789".to_string());
2089
2090        let response: BybitOrderHistoryResponse = serde_json::from_value(json).unwrap();
2091
2092        assert_eq!(response.result.list[0].smp_group, 123_456_789);
2093    }
2094
2095    #[rstest]
2096    fn deserialize_order_response_accepts_string_position_idx() {
2097        let mut json: serde_json::Value =
2098            serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2099        json["result"]["list"][0]["positionIdx"] = serde_json::Value::String("1".to_string());
2100
2101        let response: BybitOrderHistoryResponse = serde_json::from_value(json).unwrap();
2102
2103        assert_eq!(response.result.list[0].position_idx, 1);
2104    }
2105
2106    #[rstest]
2107    #[case::malformed(
2108        "invalid",
2109        "expected i32, received \"invalid\": invalid digit found in string"
2110    )]
2111    #[case::out_of_range(
2112        "2147483648",
2113        "expected i32, received \"2147483648\": number too large to fit in target type"
2114    )]
2115    fn deserialize_order_response_rejects_invalid_string_smp_group(
2116        #[case] value: &str,
2117        #[case] expected: &str,
2118    ) {
2119        let mut json: serde_json::Value =
2120            serde_json::from_str(&load_test_json("http_get_orders_history.json")).unwrap();
2121        json["result"]["list"][0]["smpGroup"] = serde_json::Value::String(value.to_string());
2122
2123        let result: Result<BybitOrderHistoryResponse, _> = serde_json::from_value(json);
2124
2125        assert_eq!(result.unwrap_err().to_string(), expected);
2126    }
2127
2128    #[rstest]
2129    fn deserialize_wallet_balance_without_optional_fields() {
2130        let json = r#"{
2131            "retCode": 0,
2132            "retMsg": "OK",
2133            "result": {
2134                "list": [{
2135                    "totalEquity": "1000.00",
2136                    "accountIMRate": "0",
2137                    "totalMarginBalance": "1000.00",
2138                    "totalInitialMargin": "0",
2139                    "accountType": "UNIFIED",
2140                    "totalAvailableBalance": "1000.00",
2141                    "accountMMRate": "0",
2142                    "totalPerpUPL": "0",
2143                    "totalWalletBalance": "1000.00",
2144                    "accountLTV": "0",
2145                    "totalMaintenanceMargin": "0",
2146                    "coin": [{
2147                        "availableToBorrow": "0",
2148                        "bonus": "0",
2149                        "accruedInterest": "0",
2150                        "availableToWithdraw": "1000.00",
2151                        "equity": "1000.00",
2152                        "usdValue": "1000.00",
2153                        "borrowAmount": "0",
2154                        "totalPositionIM": "0",
2155                        "walletBalance": "1000.00",
2156                        "unrealisedPnl": "0",
2157                        "cumRealisedPnl": "0",
2158                        "locked": "0",
2159                        "collateralSwitch": true,
2160                        "marginCollateral": true,
2161                        "coin": "USDT"
2162                    }]
2163                }]
2164            }
2165        }"#;
2166
2167        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2168            .expect("Failed to parse wallet balance without optional fields");
2169
2170        assert_eq!(response.ret_code, 0);
2171        assert_eq!(response.result.list[0].coin[0].total_order_im, None);
2172        assert_eq!(response.result.list[0].coin[0].total_position_mm, None);
2173    }
2174
2175    #[rstest]
2176    fn deserialize_wallet_balance_from_docs() {
2177        let json = include_str!("../../test_data/http_get_wallet_balance.json");
2178
2179        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2180            .expect("Failed to parse wallet balance from Bybit docs example");
2181
2182        assert_eq!(response.ret_code, 0);
2183        assert_eq!(response.ret_msg, "OK");
2184
2185        let wallet = &response.result.list[0];
2186        assert_eq!(wallet.total_equity, "3.31216591");
2187        assert_eq!(wallet.account_im_rate, "0");
2188        assert_eq!(wallet.account_mm_rate, "0");
2189        assert_eq!(wallet.total_perp_upl, "0");
2190        assert_eq!(wallet.account_ltv, "0");
2191
2192        // Check BTC coin
2193        let btc = &wallet.coin[0];
2194        assert_eq!(btc.coin, "BTC");
2195        assert_eq!(btc.available_to_borrow, "3");
2196        assert_eq!(btc.total_order_im, Some("0".to_string()));
2197        assert_eq!(btc.total_position_mm, Some("0".to_string()));
2198        assert_eq!(btc.total_position_im, Some("0".to_string()));
2199
2200        // Check USDT coin (without optional IM/MM fields)
2201        let usdt = &wallet.coin[1];
2202        assert_eq!(usdt.coin, "USDT");
2203        assert_eq!(usdt.wallet_balance, dec!(1000.50));
2204        assert_eq!(usdt.total_order_im, None);
2205        assert_eq!(usdt.total_position_mm, None);
2206        assert_eq!(usdt.total_position_im, None);
2207        assert_eq!(btc.spot_borrow, Decimal::ZERO);
2208        assert_eq!(usdt.spot_borrow, Decimal::ZERO);
2209    }
2210
2211    #[rstest]
2212    fn test_parse_wallet_balance_with_spot_borrow() {
2213        let json = include_str!("../../test_data/http_get_wallet_balance_with_spot_borrow.json");
2214        let response: BybitWalletBalanceResponse =
2215            serde_json::from_str(json).expect("Failed to parse wallet balance with spotBorrow");
2216
2217        let wallet = &response.result.list[0];
2218        let usdt = &wallet.coin[0];
2219
2220        assert_eq!(usdt.coin, "USDT");
2221        assert_eq!(usdt.wallet_balance, dec!(1200.00));
2222        assert_eq!(usdt.spot_borrow, dec!(200.00));
2223        assert_eq!(usdt.borrow_amount, "200.00");
2224
2225        // Verify calculation: actual_balance = walletBalance - spotBorrow = 1200 - 200 = 1000
2226        let account_id = crate::common::parse::parse_account_state(
2227            wallet,
2228            AccountId::new("BYBIT-001"),
2229            UnixNanos::default(),
2230        )
2231        .expect("Failed to parse account state");
2232
2233        let balance = &account_id.balances[0];
2234        assert_eq!(balance.total.as_f64(), 1000.0);
2235    }
2236
2237    #[rstest]
2238    fn test_parse_wallet_balance_spot_short() {
2239        let json = include_str!("../../test_data/http_get_wallet_balance_spot_short.json");
2240        let response: BybitWalletBalanceResponse = serde_json::from_str(json)
2241            .expect("Failed to parse wallet balance with SHORT SPOT position");
2242
2243        let wallet = &response.result.list[0];
2244        let eth = &wallet.coin[0];
2245
2246        assert_eq!(eth.coin, "ETH");
2247        assert_eq!(eth.wallet_balance, dec!(0));
2248        assert_eq!(eth.spot_borrow, dec!(0.06142));
2249        assert_eq!(eth.borrow_amount, "0.06142");
2250
2251        let account_state = crate::common::parse::parse_account_state(
2252            wallet,
2253            AccountId::new("BYBIT-001"),
2254            UnixNanos::default(),
2255        )
2256        .expect("Failed to parse account state");
2257
2258        let eth_balance = account_state
2259            .balances
2260            .iter()
2261            .find(|b| b.currency.code == "ETH")
2262            .expect("ETH balance not found");
2263
2264        // Negative balance represents SHORT position (borrowed ETH)
2265        assert_eq!(eth_balance.total.as_f64(), -0.06142);
2266    }
2267
2268    #[rstest]
2269    fn deserialize_borrow_response() {
2270        let json = r#"{
2271            "retCode": 0,
2272            "retMsg": "success",
2273            "result": {
2274                "coin": "BTC",
2275                "amount": "0.01"
2276            },
2277            "retExtInfo": {},
2278            "time": 1756197991955
2279        }"#;
2280
2281        let response: BybitBorrowResponse = serde_json::from_str(json).unwrap();
2282
2283        assert_eq!(response.ret_code, 0);
2284        assert_eq!(response.ret_msg, "success");
2285        assert_eq!(response.result.coin, "BTC");
2286        assert_eq!(response.result.amount, "0.01");
2287    }
2288
2289    #[rstest]
2290    fn deserialize_no_convert_repay_response() {
2291        let json = r#"{
2292            "retCode": 0,
2293            "retMsg": "OK",
2294            "result": {
2295                "resultStatus": "SU"
2296            },
2297            "retExtInfo": {},
2298            "time": 1234567890
2299        }"#;
2300
2301        let response: BybitNoConvertRepayResponse = serde_json::from_str(json).unwrap();
2302
2303        assert_eq!(response.ret_code, 0);
2304        assert_eq!(response.ret_msg, "OK");
2305        assert_eq!(response.result.result_status, BybitRepayStatus::Success);
2306    }
2307
2308    #[rstest]
2309    fn deserialize_repay_response() {
2310        let json = r#"{
2311            "retCode": 0,
2312            "retMsg": "success",
2313            "result": {
2314                "resultStatus": "P"
2315            },
2316            "retExtInfo": {},
2317            "time": 1756295680801
2318        }"#;
2319
2320        let response: BybitRepayResponse = serde_json::from_str(json).unwrap();
2321
2322        assert_eq!(response.ret_code, 0);
2323        assert_eq!(response.ret_msg, "success");
2324        assert_eq!(response.result.result_status, BybitRepayStatus::Processing);
2325    }
2326
2327    #[rstest]
2328    fn deserialize_position_without_conditional_fields() {
2329        // Bybit v5 docs mark `isReduceOnly`, `mmrSysUpdatedTime`, `leverageSysUpdatedTime`
2330        // and `seq` as conditional fields that may be absent, e.g. once a position has been
2331        // closed through the UI (see issue #3836).
2332        let json = r#"{
2333            "retCode": 0,
2334            "retMsg": "OK",
2335            "result": {
2336                "list": [{
2337                    "positionIdx": 0,
2338                    "riskId": 1,
2339                    "riskLimitValue": "150",
2340                    "symbol": "LTCUSDT",
2341                    "side": "",
2342                    "size": "0",
2343                    "avgPrice": "0",
2344                    "positionValue": "0",
2345                    "tradeMode": 0,
2346                    "positionStatus": "Normal",
2347                    "autoAddMargin": 0,
2348                    "adlRankIndicator": 0,
2349                    "leverage": "10",
2350                    "positionBalance": "0",
2351                    "markPrice": "70.00",
2352                    "liqPrice": "",
2353                    "bustPrice": "",
2354                    "positionMM": "0",
2355                    "positionIM": "0",
2356                    "tpslMode": "Full",
2357                    "takeProfit": "0",
2358                    "stopLoss": "0",
2359                    "trailingStop": "0",
2360                    "unrealisedPnl": "0",
2361                    "curRealisedPnl": "0",
2362                    "cumRealisedPnl": "0",
2363                    "createdTime": "1676538056258",
2364                    "updatedTime": "1697673600012"
2365                }],
2366                "nextPageCursor": "",
2367                "category": "linear"
2368            },
2369            "retExtInfo": {},
2370            "time": 1697673900000
2371        }"#;
2372
2373        let response: BybitPositionListResponse = serde_json::from_str(json)
2374            .expect("Failed to parse position list with missing conditional fields");
2375
2376        let position = &response.result.list[0];
2377        assert!(!position.is_reduce_only);
2378        assert_eq!(position.seq, -1);
2379        assert_eq!(position.mmr_sys_updated_time, "");
2380        assert_eq!(position.leverage_sys_updated_time, "");
2381        assert_eq!(position.open_time, 0);
2382    }
2383
2384    #[rstest]
2385    #[case(0_i64)]
2386    #[case(1_700_000_000_123_i64)]
2387    fn deserialize_position_with_open_time_integer(#[case] expected: i64) {
2388        // Bybit position info added `openTime` (integer, ms; default 0) effective 2026-04-21.
2389        let mut value: serde_json::Value =
2390            serde_json::from_str(&load_test_json("http_get_positions_with_open_time.json"))
2391                .unwrap();
2392        value["result"]["list"][0]["openTime"] = serde_json::json!(expected);
2393
2394        let response: BybitPositionListResponse = serde_json::from_value(value)
2395            .expect("Failed to parse position list with integer openTime");
2396
2397        assert_eq!(response.result.list[0].open_time, expected);
2398    }
2399
2400    #[rstest]
2401    fn deserialize_position_response_accepts_string_integer_fields() {
2402        let mut json: serde_json::Value =
2403            serde_json::from_str(&load_test_json("http_get_positions.json")).unwrap();
2404        let position = &mut json["result"]["list"][0];
2405        position["riskId"] = serde_json::Value::String("1234".to_string());
2406        position["tradeMode"] = serde_json::Value::String("1".to_string());
2407        position["autoAddMargin"] = serde_json::Value::String("2".to_string());
2408        position["adlRankIndicator"] = serde_json::Value::String("35".to_string());
2409
2410        let response: BybitPositionListResponse = serde_json::from_value(json).unwrap();
2411
2412        let position = &response.result.list[0];
2413        assert_eq!(position.risk_id, 1234);
2414        assert_eq!(position.trade_mode, 1);
2415        assert_eq!(position.auto_add_margin, 2);
2416        assert_eq!(position.adl_rank_indicator, 35);
2417    }
2418
2419    #[rstest]
2420    fn deserialize_inverse_instrument_with_symbol_type_and_id() {
2421        let json = load_test_json("http_get_instruments_inverse_symbol_type.json");
2422        let response: BybitInstrumentInverseResponse = serde_json::from_str(&json).unwrap();
2423        let instrument = &response.result.list[0];
2424
2425        assert_eq!(instrument.symbol_id, Some(11));
2426        assert_eq!(instrument.symbol_type, Some(BybitSymbolType::Commodity));
2427    }
2428
2429    #[rstest]
2430    fn deserialize_option_instrument_with_symbol_id() {
2431        let json = load_test_json("http_get_instruments_option_symbol_id.json");
2432        let response: BybitInstrumentOptionResponse = serde_json::from_str(&json).unwrap();
2433        let instrument = &response.result.list[0];
2434
2435        assert_eq!(instrument.symbol_id, Some(99));
2436    }
2437
2438    #[rstest]
2439    fn deserialize_sub_members_response() {
2440        let json = load_test_json("http_get_user_sub_members.json");
2441        let response: BybitSubMembersResponse =
2442            serde_json::from_str(&json).expect("parse sub members");
2443        assert_eq!(response.ret_code, 0);
2444        assert_eq!(response.result.sub_members.len(), 2);
2445        let first = &response.result.sub_members[0];
2446        assert_eq!(first.uid, "106314365");
2447        assert_eq!(first.username, "xxxx02");
2448        assert_eq!(first.member_type, 1);
2449        assert_eq!(first.status, 1);
2450        assert_eq!(first.account_mode, 5);
2451        assert_eq!(first.remark, "");
2452        let second = &response.result.sub_members[1];
2453        assert_eq!(second.uid, "106279879");
2454        assert_eq!(second.account_mode, 6);
2455    }
2456
2457    #[rstest]
2458    fn deserialize_sub_members_paged_response() {
2459        // The final-page sentinel is `"0"`; both `"0"` and `None` collapse to
2460        // `continuation_cursor() == None` through the method.
2461        let json = load_test_json("http_get_user_sub_members_paged.json");
2462        let response: BybitSubMembersPagedResponse =
2463            serde_json::from_str(&json).expect("parse paged sub members");
2464        assert_eq!(response.result.sub_members.len(), 2);
2465        assert_eq!(response.result.next_cursor.as_deref(), Some("0"));
2466        assert!(!response.result.has_more_pages());
2467        assert_eq!(response.result.continuation_cursor(), None);
2468    }
2469
2470    #[rstest]
2471    fn deserialize_escrow_sub_members_response_uses_same_shape() {
2472        // The escrow alias must decode into the same shape as the paginated
2473        // sub-member list; a non-`"0"` cursor indicates more pages to fetch.
2474        let json = load_test_json("http_get_user_escrow_sub_members.json");
2475        let response: BybitEscrowSubMembersResponse =
2476            serde_json::from_str(&json).expect("parse escrow sub members");
2477        assert_eq!(response.result.sub_members.len(), 2);
2478        assert_eq!(response.result.sub_members[0].member_type, 12);
2479        assert_eq!(response.result.sub_members[0].remark, "earn fund");
2480        assert_eq!(response.result.next_cursor.as_deref(), Some("344"));
2481        assert!(response.result.has_more_pages());
2482        assert_eq!(response.result.continuation_cursor(), Some("344"));
2483    }
2484
2485    #[rstest]
2486    fn deserialize_sub_api_keys_response() {
2487        // `readOnly` arrives as a bool here; the masked `"******"` secret
2488        // collapses to `None` through the `masked_secret` Serde adapter.
2489        let json = load_test_json("http_get_user_sub_apikeys.json");
2490        let response: BybitSubApiKeysResponse =
2491            serde_json::from_str(&json).expect("parse sub apikeys");
2492        assert_eq!(response.result.keys.len(), 1);
2493        let key = &response.result.keys[0];
2494        let debug = format!("{key:?}");
2495
2496        assert!(!key.read_only);
2497        assert_eq!(key.secret, None);
2498        assert_eq!(key.key_type, BybitApiKeyType::Hmac);
2499        assert_eq!(key.flag, "hmac");
2500        assert_eq!(key.deadline_day, Some(21));
2501        assert_eq!(key.permissions.contract_trade, vec!["Order", "Position"]);
2502        assert_eq!(key.permissions.spot, vec!["SpotTrade"]);
2503        assert!(key.permissions.earn.is_empty());
2504        assert_eq!(response.result.next_page_cursor.as_deref(), Some(""));
2505        assert!(!response.result.has_more_pages());
2506        assert!(debug.contains("api_key: <redacted>"));
2507        assert!(debug.contains("secret: None"));
2508        assert!(!debug.contains("XXXXXX"));
2509    }
2510
2511    #[rstest]
2512    fn deserialize_update_sub_api_response() {
2513        let json = load_test_json("http_post_user_update_sub_api.json");
2514        let response: BybitUpdateSubApiResponse =
2515            serde_json::from_str(&json).expect("parse update sub api");
2516        let debug = format!("{:?}", response.result);
2517
2518        assert!(!response.result.read_only);
2519        assert_eq!(response.result.secret, None);
2520        assert_eq!(response.result.ips, vec!["*"]);
2521        assert_eq!(response.result.permissions.spot, vec!["SpotTrade"]);
2522        assert_eq!(response.result.permissions.wallet, vec!["AccountTransfer"]);
2523        assert!(debug.contains("api_key: <redacted>"));
2524        assert!(debug.contains("secret: None"));
2525        assert!(!debug.contains("xxxxxx"));
2526    }
2527
2528    #[rstest]
2529    fn deserialize_update_master_api_response() {
2530        // Asserts on non-empty permission buckets so the test actually verifies
2531        // deserialisation (an empty `Vec` would be indistinguishable from a
2532        // `#[serde(default)]` fallback). In particular, `nft` exercises the
2533        // explicit `#[serde(rename = "NFT")]` attribute.
2534        let json = load_test_json("http_post_user_update_master_api.json");
2535        let response: BybitUpdateMasterApiResponse =
2536            serde_json::from_str(&json).expect("parse update master api");
2537        assert!(!response.result.read_only);
2538        assert_eq!(response.result.ips, vec!["*"]);
2539        let perms = &response.result.permissions;
2540        assert_eq!(perms.contract_trade, vec!["Order", "Position"]);
2541        assert_eq!(perms.copy_trading, vec!["CopyTrading"]);
2542        assert!(perms.earn.is_empty());
2543        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2544    }
2545
2546    #[rstest]
2547    fn deserialize_permissions_renamed_buckets_preserve_values() {
2548        // Regression guard for `#[serde(rename = ...)]` on permission keys
2549        // whose Bybit casing (`NFT`, `FiatP2P`, `ByXPost`) differs from
2550        // serde's `PascalCase` default (`Nft`, `FiatP2p`, `ByxPost`). Using
2551        // non-empty values ensures a rename regression causes a failure
2552        // rather than silently falling through to `serde(default)`.
2553        let json = r#"{
2554            "NFT": ["NFTQueryProductList"],
2555            "FiatP2P": ["P2PDeposit"],
2556            "ByXPost": ["PostContent"]
2557        }"#;
2558        let perms: BybitApiKeyPermissions =
2559            serde_json::from_str(json).expect("parse renamed buckets");
2560        assert_eq!(perms.nft, vec!["NFTQueryProductList"]);
2561        assert_eq!(perms.fiat_p2p, vec!["P2PDeposit"]);
2562        assert_eq!(perms.byx_post, vec!["PostContent"]);
2563    }
2564
2565    #[rstest]
2566    fn deserialize_account_details_response_with_current_docs_example() {
2567        let json = load_test_json("http_get_user_query_api.json");
2568        let response: BybitAccountDetailsResponse =
2569            serde_json::from_str(&json).expect("parse account details");
2570        let debug = format!("{:?}", response.result);
2571
2572        assert_eq!(
2573            response.result.permissions.fiat_global_pay,
2574            Vec::<String>::new()
2575        );
2576        assert_eq!(
2577            response.result.permissions.fiat_bit_pay,
2578            vec!["FaitPayOrder"]
2579        );
2580        assert_eq!(response.result.permissions.bit_card, vec!["BitCard"]);
2581        assert_eq!(response.result.permissions.byx_post, vec!["ByXPost"]);
2582        assert_eq!(response.result.unified, Some(0));
2583        assert!(debug.contains("api_key: <redacted>"));
2584        assert!(debug.contains("secret: <redacted>"));
2585        assert!(!debug.contains("api_key: \"XXXXXXXX\""));
2586    }
2587}