Skip to main content

nautilus_bybit/http/
query.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//! Builder types for Bybit REST query parameters and filters.
17
18use derive_builder::Builder;
19#[cfg(test)]
20use nautilus_core::string::secret::REDACTED;
21use nautilus_core::string::secret::SecretString;
22use serde::{Deserialize, Serialize};
23use zeroize::Zeroize;
24
25use crate::common::{
26    enums::{
27        BybitAccountType, BybitBboSideType, BybitExecType, BybitInstrumentStatus,
28        BybitKlineInterval, BybitMarginMode, BybitMarketUnit, BybitOpenOnly, BybitOptionType,
29        BybitOrderFilter, BybitOrderSide, BybitOrderSmpType, BybitOrderStatus, BybitOrderType,
30        BybitPositionIdx, BybitPositionMode, BybitProductType, BybitStopOrderType,
31        BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection, BybitTriggerType,
32    },
33    parse::opt_bool_as_int,
34};
35
36/// Query parameters for `GET /v5/market/instruments-info`.
37///
38/// # References
39/// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
40#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
41#[serde(rename_all = "camelCase")]
42#[builder(default)]
43#[builder(setter(into))]
44pub struct BybitInstrumentsInfoParams {
45    pub category: BybitProductType,
46    #[builder(setter(strip_option))]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub symbol: Option<String>,
49    #[builder(setter(strip_option))]
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub status: Option<BybitInstrumentStatus>,
52    #[builder(setter(strip_option))]
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub base_coin: Option<String>,
55    #[builder(setter(strip_option))]
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub limit: Option<u32>,
58    #[builder(setter(strip_option))]
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub cursor: Option<String>,
61}
62
63/// Query parameters for `GET /v5/market/tickers`.
64///
65/// # References
66/// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
67#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
68#[serde(rename_all = "camelCase")]
69#[builder(default)]
70#[builder(setter(into))]
71pub struct BybitTickersParams {
72    pub category: BybitProductType,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    #[builder(setter(strip_option))]
75    pub symbol: Option<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    #[builder(setter(strip_option))]
78    pub base_coin: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    #[builder(setter(strip_option))]
81    pub exp_date: Option<String>,
82}
83
84/// Query parameters for `GET /v5/market/kline`.
85///
86/// # References
87/// - <https://bybit-exchange.github.io/docs/v5/market/kline>
88#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
89#[serde(rename_all = "camelCase")]
90#[builder(setter(into, strip_option), default)]
91pub struct BybitKlinesParams {
92    pub category: BybitProductType,
93    pub symbol: String,
94    pub interval: BybitKlineInterval,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub start: Option<i64>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub end: Option<i64>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub limit: Option<u32>,
101}
102
103impl Default for BybitKlinesParams {
104    fn default() -> Self {
105        Self {
106            category: BybitProductType::Linear,
107            symbol: String::new(),
108            interval: BybitKlineInterval::Minute1,
109            start: None,
110            end: None,
111            limit: None,
112        }
113    }
114}
115
116/// Query parameters for `GET /v5/market/recent-trade`.
117///
118/// # References
119/// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
120#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
121#[serde(rename_all = "camelCase")]
122#[builder(setter(into, strip_option), default)]
123pub struct BybitTradesParams {
124    pub category: BybitProductType,
125    pub symbol: String,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub base_coin: Option<String>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub option_type: Option<BybitOptionType>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub limit: Option<u32>,
132}
133
134impl Default for BybitTradesParams {
135    fn default() -> Self {
136        Self {
137            category: BybitProductType::Linear,
138            symbol: String::new(),
139            base_coin: None,
140            option_type: None,
141            limit: None,
142        }
143    }
144}
145
146/// Query parameters for `GET /v5/market/funding/history`.
147///
148/// # References
149/// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
150#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
151#[serde(rename_all = "camelCase")]
152#[builder(setter(into, strip_option), default)]
153pub struct BybitFundingParams {
154    pub category: BybitProductType,
155    pub symbol: String,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub start_time: Option<i64>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub end_time: Option<i64>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub limit: Option<u32>,
162}
163
164impl Default for BybitFundingParams {
165    fn default() -> Self {
166        Self {
167            category: BybitProductType::Linear,
168            symbol: String::new(),
169            start_time: None,
170            end_time: None,
171            limit: None,
172        }
173    }
174}
175
176/// Query parameters for `GET /v5/market/orderbook`.
177///
178/// # References
179/// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
180#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
181#[builder(setter(into, strip_option), default)]
182pub struct BybitOrderbookParams {
183    pub category: BybitProductType,
184    pub symbol: String,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub limit: Option<u32>,
187}
188
189impl Default for BybitOrderbookParams {
190    fn default() -> Self {
191        Self {
192            category: BybitProductType::Linear,
193            symbol: String::new(),
194            limit: None,
195        }
196    }
197}
198
199/// Query parameters for `GET /v5/asset/coin/query-info`.
200///
201/// # References
202/// - <https://bybit-exchange.github.io/docs/v5/asset/coin-info>
203#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
204#[serde(rename_all = "camelCase")]
205#[builder(default)]
206#[builder(setter(into))]
207pub struct BybitCoinInfoParams {
208    #[serde(skip_serializing_if = "Option::is_none")]
209    #[builder(setter(strip_option))]
210    pub coin: Option<String>,
211}
212
213/// Query parameters for `GET /v5/account/fee-rate`.
214///
215/// # References
216/// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
217#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
218#[serde(rename_all = "camelCase")]
219#[builder(default)]
220#[builder(setter(into))]
221pub struct BybitFeeRateParams {
222    pub category: BybitProductType,
223    #[builder(setter(strip_option))]
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub symbol: Option<String>,
226    #[builder(setter(strip_option))]
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub base_coin: Option<String>,
229}
230
231/// Query parameters for `GET /v5/account/wallet-balance`.
232///
233/// # References
234/// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
235#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
236#[serde(rename_all = "camelCase")]
237pub struct BybitWalletBalanceParams {
238    pub account_type: BybitAccountType,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    #[builder(setter(strip_option))]
241    pub coin: Option<String>,
242}
243
244/// Query parameters for `GET /v5/position/list`.
245///
246/// # References
247/// - <https://bybit-exchange.github.io/docs/v5/position>
248#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
249#[serde(rename_all = "camelCase")]
250#[builder(default)]
251#[builder(setter(into))]
252pub struct BybitPositionListParams {
253    pub category: BybitProductType,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    #[builder(setter(strip_option))]
256    pub symbol: Option<String>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    #[builder(setter(strip_option))]
259    pub base_coin: Option<String>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    #[builder(setter(strip_option))]
262    pub settle_coin: Option<String>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    #[builder(setter(strip_option))]
265    pub limit: Option<u32>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    #[builder(setter(strip_option))]
268    pub cursor: Option<String>,
269}
270
271/// Body parameters for `POST /v5/account/set-margin-mode`.
272///
273/// # References
274/// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
275#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
276#[serde(rename_all = "camelCase")]
277pub struct BybitSetMarginModeParams {
278    pub set_margin_mode: BybitMarginMode,
279}
280
281/// Body parameters for `POST /v5/position/set-leverage`.
282///
283/// # References
284/// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
285#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
286#[serde(rename_all = "camelCase")]
287pub struct BybitSetLeverageParams {
288    pub category: BybitProductType,
289    pub symbol: String,
290    pub buy_leverage: String,
291    pub sell_leverage: String,
292}
293
294/// Body parameters for `POST /v5/position/switch-mode`.
295///
296/// # References
297/// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
298#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
299#[serde(rename_all = "camelCase")]
300pub struct BybitSwitchModeParams {
301    pub category: BybitProductType,
302    #[builder(default)]
303    #[serde(skip_serializing_if = "Option::is_none")]
304    #[builder(setter(strip_option))]
305    pub symbol: Option<String>,
306    #[builder(default)]
307    #[serde(skip_serializing_if = "Option::is_none")]
308    #[builder(setter(strip_option))]
309    pub coin: Option<String>,
310    pub mode: BybitPositionMode,
311}
312
313/// Body parameters for `POST /v5/position/trading-stop`.
314///
315/// # References
316/// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
317#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
318#[serde(rename_all = "camelCase")]
319pub struct BybitSetTradingStopParams {
320    pub category: BybitProductType,
321    pub symbol: String,
322    pub position_idx: BybitPositionIdx,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub take_profit: Option<String>,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub stop_loss: Option<String>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub trailing_stop: Option<String>,
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub tp_trigger_by: Option<BybitTriggerType>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub sl_trigger_by: Option<BybitTriggerType>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub active_price: Option<String>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub tpsl_mode: Option<BybitTpSlMode>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub tp_size: Option<String>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub sl_size: Option<String>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub tp_limit_price: Option<String>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub sl_limit_price: Option<String>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub tp_order_type: Option<BybitOrderType>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub sl_order_type: Option<BybitOrderType>,
349}
350
351/// Body parameters for `POST /v5/account/borrow`.
352///
353/// # References
354///
355/// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
356#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
357#[serde(rename_all = "camelCase")]
358pub struct BybitBorrowParams {
359    pub coin: String,
360    pub amount: String,
361}
362
363/// Body parameters for `POST /v5/account/no-convert-repay`.
364///
365/// # References
366///
367/// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
368#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
369#[serde(rename_all = "camelCase")]
370pub struct BybitNoConvertRepayParams {
371    pub coin: String,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    #[builder(setter(strip_option), default)]
374    pub amount: Option<String>,
375}
376
377/// Body parameters for `POST /v5/account/repay`.
378///
379/// # References
380///
381/// - <https://bybit-exchange.github.io/docs/v5/account/repay>
382#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
383#[serde(rename_all = "camelCase")]
384pub struct BybitRepayParams {
385    #[serde(skip_serializing_if = "Option::is_none")]
386    #[builder(setter(strip_option), default)]
387    pub coin: Option<String>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    #[builder(setter(strip_option), default)]
390    pub amount: Option<String>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    #[builder(setter(strip_option), default)]
393    pub repayment_type: Option<String>,
394}
395
396/// Order entry payload for `POST /v5/order/create-batch`.
397///
398/// # References
399/// - <https://bybit-exchange.github.io/docs/v5/order/batch-place>
400#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
401#[serde(rename_all = "camelCase")]
402pub struct BybitBatchPlaceOrderEntry {
403    pub symbol: String,
404    #[builder(default)]
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub is_leverage: Option<i32>,
407    pub side: BybitOrderSide,
408    pub order_type: BybitOrderType,
409    pub qty: String,
410    #[builder(default)]
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub price: Option<String>,
413    #[builder(default)]
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub market_unit: Option<BybitMarketUnit>,
416    #[builder(default)]
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub trigger_direction: Option<BybitTriggerDirection>,
419    #[builder(default)]
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub order_filter: Option<BybitOrderFilter>,
422    #[builder(default)]
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub trigger_price: Option<String>,
425    #[builder(default)]
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub trigger_by: Option<BybitTriggerType>,
428    #[builder(default)]
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub order_iv: Option<String>,
431    #[builder(default)]
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub time_in_force: Option<BybitTimeInForce>,
434    #[builder(default)]
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub position_idx: Option<BybitPositionIdx>,
437    #[builder(default)]
438    #[serde(skip_serializing_if = "Option::is_none")]
439    #[builder(setter(strip_option))]
440    pub order_link_id: Option<String>,
441    #[builder(default)]
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub take_profit: Option<String>,
444    #[builder(default)]
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub stop_loss: Option<String>,
447    #[builder(default)]
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub tp_trigger_by: Option<BybitTriggerType>,
450    #[builder(default)]
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub sl_trigger_by: Option<BybitTriggerType>,
453    #[builder(default)]
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub reduce_only: Option<bool>,
456    #[builder(default)]
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub close_on_trigger: Option<bool>,
459    #[builder(default)]
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub smp_type: Option<BybitOrderSmpType>,
462    #[builder(default)]
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub mmp: Option<bool>,
465    #[builder(default)]
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub tpsl_mode: Option<BybitTpSlMode>,
468    #[builder(default)]
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub tp_limit_price: Option<String>,
471    #[builder(default)]
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub sl_limit_price: Option<String>,
474    #[builder(default)]
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub tp_order_type: Option<BybitOrderType>,
477    #[builder(default)]
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub sl_order_type: Option<BybitOrderType>,
480    #[builder(default)]
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub bbo_side_type: Option<BybitBboSideType>,
483    #[builder(default)]
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub bbo_level: Option<String>,
486}
487
488/// Body parameters for `POST /v5/order/create-batch`.
489///
490/// # References
491/// - <https://bybit-exchange.github.io/docs/v5/order/batch-place>
492#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
493#[serde(rename_all = "camelCase")]
494pub struct BybitBatchPlaceOrderParams {
495    pub category: BybitProductType,
496    pub request: Vec<BybitBatchPlaceOrderEntry>,
497}
498
499/// Native TP/SL and option-specific fields that map onto the `POST /v5/order/create` entry.
500///
501/// Bundled to keep the `submit_order` signature manageable, and to give the demo HTTP path
502/// access to the same fields the mainnet WS path supports via
503/// [`crate::websocket::messages::BybitWsPlaceOrderParams`]. All fields are optional; populated
504/// fields are written onto the entry builder as-is. `tpsl_mode` defaults to `Full` upstream when
505/// only `take_profit` / `stop_loss` are set without an explicit mode.
506///
507/// `tp_trigger_price` / `sl_trigger_price` are intentionally absent: the create-order entry does
508/// not carry them (the mainnet WS Trade API does, via separate fields).
509#[derive(Debug, Clone, Default)]
510pub struct BybitNativeTpSlParams {
511    pub take_profit: Option<String>,
512    pub stop_loss: Option<String>,
513    pub tp_trigger_by: Option<BybitTriggerType>,
514    pub sl_trigger_by: Option<BybitTriggerType>,
515    pub tp_order_type: Option<BybitOrderType>,
516    pub sl_order_type: Option<BybitOrderType>,
517    pub tp_limit_price: Option<String>,
518    pub sl_limit_price: Option<String>,
519    pub tpsl_mode: Option<BybitTpSlMode>,
520    pub close_on_trigger: Option<bool>,
521    pub order_iv: Option<String>,
522    pub mmp: Option<bool>,
523}
524
525impl BybitNativeTpSlParams {
526    /// Returns `true` if no TP/SL or option-specific field is set.
527    #[must_use]
528    pub fn is_empty(&self) -> bool {
529        self.take_profit.is_none()
530            && self.stop_loss.is_none()
531            && self.tp_trigger_by.is_none()
532            && self.sl_trigger_by.is_none()
533            && self.tp_order_type.is_none()
534            && self.sl_order_type.is_none()
535            && self.tp_limit_price.is_none()
536            && self.sl_limit_price.is_none()
537            && self.tpsl_mode.is_none()
538            && self.close_on_trigger.is_none()
539            && self.order_iv.is_none()
540            && self.mmp.is_none()
541    }
542}
543
544/// Body parameters for `POST /v5/order/create`.
545///
546/// # References
547/// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
548#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
549#[serde(rename_all = "camelCase")]
550pub struct BybitPlaceOrderParams {
551    #[serde(flatten)]
552    pub order: BybitBatchPlaceOrderEntry,
553    pub category: BybitProductType,
554    #[builder(default)]
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub slippage_tolerance_type: Option<String>,
557    #[builder(default)]
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub slippage_tolerance: Option<String>,
560}
561
562/// Amend entry for `POST /v5/order/amend-batch`.
563///
564/// # References
565/// - <https://bybit-exchange.github.io/docs/v5/order/batch-amend>
566#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
567#[serde(rename_all = "camelCase")]
568pub struct BybitBatchAmendOrderEntry {
569    pub symbol: String,
570    #[builder(default)]
571    #[serde(skip_serializing_if = "Option::is_none")]
572    #[builder(setter(strip_option))]
573    pub order_id: Option<String>,
574    #[builder(default)]
575    #[serde(skip_serializing_if = "Option::is_none")]
576    #[builder(setter(strip_option))]
577    pub order_link_id: Option<String>,
578    #[builder(default)]
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub order_iv: Option<String>,
581    #[builder(default)]
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub trigger_price: Option<String>,
584    #[builder(default)]
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub qty: Option<String>,
587    #[builder(default)]
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub price: Option<String>,
590    #[builder(default)]
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub tpsl_mode: Option<BybitTpSlMode>,
593    #[builder(default)]
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub take_profit: Option<String>,
596    #[builder(default)]
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub stop_loss: Option<String>,
599    #[builder(default)]
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub tp_trigger_by: Option<BybitTriggerType>,
602    #[builder(default)]
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub sl_trigger_by: Option<BybitTriggerType>,
605    #[builder(default)]
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub trigger_by: Option<BybitTriggerType>,
608    #[builder(default)]
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub tp_limit_price: Option<String>,
611    #[builder(default)]
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub sl_limit_price: Option<String>,
614}
615
616/// Body parameters for `POST /v5/order/amend-batch`.
617///
618/// # References
619/// - <https://bybit-exchange.github.io/docs/v5/order/batch-amend>
620#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
621#[serde(rename_all = "camelCase")]
622pub struct BybitBatchAmendOrderParams {
623    pub category: BybitProductType,
624    pub request: Vec<BybitBatchAmendOrderEntry>,
625}
626
627/// Body parameters for `POST /v5/order/amend`.
628///
629/// # References
630/// - <https://bybit-exchange.github.io/docs/v5/order/amend-order>
631#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
632#[serde(rename_all = "camelCase")]
633pub struct BybitAmendOrderParams {
634    #[serde(flatten)]
635    pub order: BybitBatchAmendOrderEntry,
636    pub category: BybitProductType,
637}
638
639/// Cancel entry for `POST /v5/order/cancel-batch`.
640///
641/// # References
642/// - <https://bybit-exchange.github.io/docs/v5/order/batch-cancel>
643#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
644#[serde(rename_all = "camelCase")]
645pub struct BybitBatchCancelOrderEntry {
646    pub symbol: String,
647    #[builder(default)]
648    #[serde(skip_serializing_if = "Option::is_none")]
649    #[builder(setter(strip_option))]
650    pub order_id: Option<String>,
651    #[builder(default)]
652    #[serde(skip_serializing_if = "Option::is_none")]
653    #[builder(setter(strip_option))]
654    pub order_link_id: Option<String>,
655    #[builder(default)]
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub order_filter: Option<BybitOrderFilter>,
658}
659
660/// Body parameters for `POST /v5/order/cancel-batch`.
661///
662/// # References
663/// - <https://bybit-exchange.github.io/docs/v5/order/batch-cancel>
664#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
665#[serde(rename_all = "camelCase")]
666pub struct BybitBatchCancelOrderParams {
667    pub category: BybitProductType,
668    pub request: Vec<BybitBatchCancelOrderEntry>,
669}
670
671/// Body parameters for `POST /v5/order/cancel`.
672///
673/// # References
674/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-order>
675#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
676#[serde(rename_all = "camelCase")]
677pub struct BybitCancelOrderParams {
678    #[serde(flatten)]
679    pub order: BybitBatchCancelOrderEntry,
680    pub category: BybitProductType,
681}
682
683/// Body parameters for `POST /v5/order/cancel-all`.
684///
685/// # References
686/// - <https://bybit-exchange.github.io/docs/v5/order/cancel-all>
687#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
688#[serde(rename_all = "camelCase")]
689#[builder(default)]
690#[builder(setter(into))]
691pub struct BybitCancelAllOrdersParams {
692    pub category: BybitProductType,
693    #[serde(skip_serializing_if = "Option::is_none")]
694    #[builder(setter(strip_option))]
695    pub symbol: Option<String>,
696    #[serde(skip_serializing_if = "Option::is_none")]
697    #[builder(setter(strip_option))]
698    pub base_coin: Option<String>,
699    #[serde(skip_serializing_if = "Option::is_none")]
700    #[builder(setter(strip_option))]
701    pub settle_coin: Option<String>,
702    #[serde(skip_serializing_if = "Option::is_none")]
703    #[builder(setter(strip_option))]
704    pub order_filter: Option<BybitOrderFilter>,
705    #[serde(skip_serializing_if = "Option::is_none")]
706    #[builder(setter(strip_option))]
707    pub stop_order_type: Option<BybitStopOrderType>,
708}
709
710/// Query parameters for `GET /v5/order/realtime`.
711///
712/// # References
713/// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
714#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
715#[serde(rename_all = "camelCase")]
716#[builder(default)]
717#[builder(setter(into))]
718pub struct BybitOpenOrdersParams {
719    pub category: BybitProductType,
720    #[serde(skip_serializing_if = "Option::is_none")]
721    #[builder(setter(strip_option))]
722    pub symbol: Option<String>,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    #[builder(setter(strip_option))]
725    pub base_coin: Option<String>,
726    #[serde(skip_serializing_if = "Option::is_none")]
727    #[builder(setter(strip_option))]
728    pub settle_coin: Option<String>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    #[builder(setter(strip_option))]
731    pub order_id: Option<String>,
732    #[serde(skip_serializing_if = "Option::is_none")]
733    #[builder(setter(strip_option))]
734    pub order_link_id: Option<String>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    #[builder(setter(strip_option))]
737    pub open_only: Option<BybitOpenOnly>,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    #[builder(setter(strip_option))]
740    pub order_filter: Option<BybitOrderFilter>,
741    #[serde(skip_serializing_if = "Option::is_none")]
742    #[builder(setter(strip_option))]
743    pub limit: Option<u32>,
744    #[serde(skip_serializing_if = "Option::is_none")]
745    #[builder(setter(strip_option))]
746    pub cursor: Option<String>,
747}
748
749/// Query parameters for `GET /v5/order/history`.
750///
751/// # References
752/// - <https://bybit-exchange.github.io/docs/v5/order/order-list>
753#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
754#[serde(rename_all = "camelCase")]
755#[builder(default)]
756#[builder(setter(into))]
757pub struct BybitOrderHistoryParams {
758    pub category: BybitProductType,
759    #[serde(skip_serializing_if = "Option::is_none")]
760    #[builder(setter(strip_option))]
761    pub symbol: Option<String>,
762    #[serde(skip_serializing_if = "Option::is_none")]
763    #[builder(setter(strip_option))]
764    pub base_coin: Option<String>,
765    #[serde(skip_serializing_if = "Option::is_none")]
766    #[builder(setter(strip_option))]
767    pub settle_coin: Option<String>,
768    #[serde(skip_serializing_if = "Option::is_none")]
769    #[builder(setter(strip_option))]
770    pub order_id: Option<String>,
771    #[serde(skip_serializing_if = "Option::is_none")]
772    #[builder(setter(strip_option))]
773    pub order_link_id: Option<String>,
774    #[serde(skip_serializing_if = "Option::is_none")]
775    #[builder(setter(strip_option))]
776    pub open_only: Option<BybitOpenOnly>,
777    #[serde(skip_serializing_if = "Option::is_none")]
778    #[builder(setter(strip_option))]
779    pub order_filter: Option<BybitOrderFilter>,
780    #[serde(skip_serializing_if = "Option::is_none")]
781    #[builder(setter(strip_option))]
782    pub order_status: Option<BybitOrderStatus>,
783    #[serde(skip_serializing_if = "Option::is_none")]
784    #[builder(setter(strip_option))]
785    pub start_time: Option<i64>,
786    #[serde(skip_serializing_if = "Option::is_none")]
787    #[serde(rename = "endTime")]
788    #[builder(setter(strip_option))]
789    pub end_time: Option<i64>,
790    #[serde(skip_serializing_if = "Option::is_none")]
791    #[builder(setter(strip_option))]
792    pub limit: Option<u32>,
793    #[serde(skip_serializing_if = "Option::is_none")]
794    #[builder(setter(strip_option))]
795    pub cursor: Option<String>,
796}
797
798/// Query parameters for `GET /v5/execution/list`.
799///
800/// # References
801/// - <https://bybit-exchange.github.io/docs/v5/order/execution>
802#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
803#[serde(rename_all = "camelCase")]
804#[builder(default)]
805#[builder(setter(into))]
806pub struct BybitTradeHistoryParams {
807    pub category: BybitProductType,
808    #[serde(skip_serializing_if = "Option::is_none")]
809    #[builder(setter(strip_option))]
810    pub symbol: Option<String>,
811    #[serde(skip_serializing_if = "Option::is_none")]
812    #[builder(setter(strip_option))]
813    pub base_coin: Option<String>,
814    #[serde(skip_serializing_if = "Option::is_none")]
815    #[builder(setter(strip_option))]
816    pub order_id: Option<String>,
817    #[serde(skip_serializing_if = "Option::is_none")]
818    #[builder(setter(strip_option))]
819    pub order_link_id: Option<String>,
820    #[serde(skip_serializing_if = "Option::is_none")]
821    #[builder(setter(strip_option))]
822    pub start_time: Option<i64>,
823    #[serde(skip_serializing_if = "Option::is_none")]
824    #[serde(rename = "endTime")]
825    #[builder(setter(strip_option))]
826    pub end_time: Option<i64>,
827    #[serde(skip_serializing_if = "Option::is_none")]
828    #[builder(setter(strip_option))]
829    pub exec_type: Option<BybitExecType>,
830    #[serde(skip_serializing_if = "Option::is_none")]
831    #[builder(setter(strip_option))]
832    pub limit: Option<u32>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    #[builder(setter(strip_option))]
835    pub cursor: Option<String>,
836}
837
838/// Request-side permission patch for API-key update endpoints (`PascalCase`).
839///
840/// Kept separate from the response-side
841/// [`crate::http::models::BybitApiKeyPermissions`]: every field here is
842/// `Option<Vec<String>>` with `skip_serializing_if`, so an unset bucket is
843/// omitted from the request body entirely rather than being serialized as an
844/// explicit empty array (which the venue treats as "clear all permissions").
845///
846/// The field set is the superset of the master and sub-account permission
847/// tables. Buckets that only appear for master-account keys (`FiatP2P`,
848/// `FiatBybitPay`, `FiatBitPay`, `FiatConvertBroker`, `BitCard`, `ByXPost`)
849/// must not be set when calling `POST /v5/user/update-sub-api`; callers are
850/// expected to only populate the buckets documented for the endpoint being
851/// invoked.
852///
853/// # References
854/// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
855/// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
856#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
857#[serde(rename_all = "PascalCase")]
858#[builder(default)]
859#[builder(setter(into, strip_option))]
860pub struct BybitApiKeyPermissionUpdate {
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub contract_trade: Option<Vec<String>>,
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub spot: Option<Vec<String>>,
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub wallet: Option<Vec<String>>,
867    #[serde(skip_serializing_if = "Option::is_none")]
868    pub options: Option<Vec<String>>,
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub derivatives: Option<Vec<String>>,
871    #[serde(skip_serializing_if = "Option::is_none")]
872    pub exchange: Option<Vec<String>>,
873    #[serde(skip_serializing_if = "Option::is_none")]
874    pub earn: Option<Vec<String>>,
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub affiliate: Option<Vec<String>>,
877    #[serde(skip_serializing_if = "Option::is_none")]
878    pub block_trade: Option<Vec<String>>,
879    #[serde(skip_serializing_if = "Option::is_none")]
880    pub copy_trading: Option<Vec<String>>,
881    // Bybit ships this key uppercase (`"NFT"`); the struct-level PascalCase
882    // rule would otherwise emit `"Nft"` and the venue would ignore the field.
883    #[serde(rename = "NFT", skip_serializing_if = "Option::is_none")]
884    pub nft: Option<Vec<String>>,
885    // Bybit uses `"FiatP2P"` - PascalCase rename would emit `"FiatP2p"`.
886    #[serde(rename = "FiatP2P", skip_serializing_if = "Option::is_none")]
887    pub fiat_p2p: Option<Vec<String>>,
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub fiat_bybit_pay: Option<Vec<String>>,
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub fiat_bit_pay: Option<Vec<String>>,
892    #[serde(skip_serializing_if = "Option::is_none")]
893    pub fiat_convert_broker: Option<Vec<String>>,
894    #[serde(skip_serializing_if = "Option::is_none")]
895    pub bit_card: Option<Vec<String>>,
896    // Bybit uses `"ByXPost"` - PascalCase rename would emit `"ByxPost"`.
897    #[serde(rename = "ByXPost", skip_serializing_if = "Option::is_none")]
898    pub byx_post: Option<Vec<String>>,
899}
900
901/// Body parameters for `POST /v5/user/update-sub-api`.
902///
903/// `api_key` is only required when a master key is editing a sub-account key;
904/// it is omitted when a sub key edits itself.
905///
906/// # References
907/// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
908#[derive(Debug, Clone, Deserialize, Serialize, Default, Builder)]
909#[serde(rename_all = "camelCase")]
910#[builder(setter(into))]
911pub struct BybitUpdateSubApiParams {
912    #[builder(default)]
913    #[serde(skip_serializing_if = "Option::is_none")]
914    #[builder(setter(strip_option))]
915    pub api_key: Option<SecretString>,
916    // Bybit accepts `readOnly` as a 0/1 integer on the wire; the builder takes
917    // a `bool` and `opt_bool_as_int` serializes it to match.
918    #[builder(default)]
919    #[serde(skip_serializing_if = "Option::is_none", with = "opt_bool_as_int")]
920    #[builder(setter(strip_option))]
921    pub read_only: Option<bool>,
922    #[builder(default)]
923    #[serde(skip_serializing_if = "Option::is_none")]
924    #[builder(setter(strip_option))]
925    pub ips: Option<String>,
926    #[builder(default)]
927    #[serde(skip_serializing_if = "Option::is_none")]
928    #[builder(setter(strip_option))]
929    pub permissions: Option<BybitApiKeyPermissionUpdate>,
930}
931
932impl Zeroize for BybitUpdateSubApiParams {
933    fn zeroize(&mut self) {
934        self.api_key.zeroize();
935    }
936}
937
938/// Body parameters for `POST /v5/user/update-api`.
939///
940/// Can only modify the caller's own master key.
941///
942/// The official request parameter table lists only `readOnly` and
943/// `permissions`; `ips` appears in the response but is not a request field,
944/// so it is deliberately excluded here.
945///
946/// # References
947/// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
948#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
949#[serde(rename_all = "camelCase")]
950#[builder(default)]
951#[builder(setter(into))]
952pub struct BybitUpdateMasterApiParams {
953    #[serde(skip_serializing_if = "Option::is_none", with = "opt_bool_as_int")]
954    #[builder(setter(strip_option))]
955    pub read_only: Option<bool>,
956    #[serde(skip_serializing_if = "Option::is_none")]
957    #[builder(setter(strip_option))]
958    pub permissions: Option<BybitApiKeyPermissionUpdate>,
959}
960
961/// Query parameters for cursor-paginated sub-account listings.
962///
963/// Shared by `GET /v5/user/submembers` and `GET /v5/user/escrow_sub_members`,
964/// which take the same pagination shape (`pageSize` up to 100 plus
965/// `nextCursor`). Bybit documents `pageSize` as a string, but the URL encoder
966/// serializes any numeric value as text anyway, so `u32` is used on the Rust
967/// side for compile-time type safety.
968///
969/// # References
970/// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
971/// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
972#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
973#[serde(rename_all = "camelCase")]
974#[builder(default)]
975#[builder(setter(into))]
976pub struct BybitSubMembersPageParams {
977    #[serde(skip_serializing_if = "Option::is_none")]
978    #[builder(setter(strip_option))]
979    pub page_size: Option<u32>,
980    #[serde(skip_serializing_if = "Option::is_none")]
981    #[builder(setter(strip_option))]
982    pub next_cursor: Option<String>,
983}
984
985/// Query parameters for `GET /v5/user/sub-apikeys`.
986///
987/// `sub_member_id` is required, so this struct intentionally does not carry
988/// `#[builder(default)]`; the generated builder returns `Err` when the field
989/// is missing, matching the convention used by `BybitBorrowParams`.
990///
991/// # References
992/// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
993#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
994#[serde(rename_all = "camelCase")]
995#[builder(setter(into))]
996pub struct BybitSubApiKeysParams {
997    pub sub_member_id: String,
998    #[serde(skip_serializing_if = "Option::is_none")]
999    #[builder(setter(strip_option), default)]
1000    pub limit: Option<u32>,
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    #[builder(setter(strip_option), default)]
1003    pub cursor: Option<String>,
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use rstest::rstest;
1009    use zeroize::Zeroize;
1010
1011    use super::*;
1012
1013    #[rstest]
1014    fn test_update_sub_api_preserves_wire_value_and_redacts_debug() {
1015        let mut params = BybitUpdateSubApiParamsBuilder::default()
1016            .api_key("sub-api-key-secret".to_string())
1017            .read_only(true)
1018            .build()
1019            .unwrap();
1020
1021        let value = serde_json::to_value(&params).unwrap();
1022        let debug = format!("{params:?}");
1023
1024        assert_eq!(value["apiKey"], "sub-api-key-secret");
1025        assert_eq!(value["readOnly"], 1);
1026        assert!(debug.contains(REDACTED));
1027        assert!(!debug.contains("sub-api-key-secret"));
1028
1029        params.zeroize();
1030        assert!(params.api_key.is_none());
1031    }
1032
1033    #[rstest]
1034    fn test_amend_entry_builds_with_only_order_id() {
1035        let entry = BybitBatchAmendOrderEntryBuilder::default()
1036            .symbol("ETHUSDT".to_string())
1037            .order_id("12345".to_string())
1038            .price(Some("1600.0".to_string()))
1039            .build()
1040            .expect("amend entry should build with only order_id");
1041
1042        assert_eq!(entry.order_id.as_deref(), Some("12345"));
1043        assert_eq!(entry.order_link_id, None);
1044    }
1045
1046    #[rstest]
1047    fn test_amend_entry_builds_with_only_order_link_id() {
1048        let entry = BybitBatchAmendOrderEntryBuilder::default()
1049            .symbol("ETHUSDT".to_string())
1050            .order_link_id("link-1".to_string())
1051            .qty(Some("0.02".to_string()))
1052            .build()
1053            .expect("amend entry should build with only order_link_id");
1054
1055        assert_eq!(entry.order_link_id.as_deref(), Some("link-1"));
1056        assert_eq!(entry.order_id, None);
1057    }
1058
1059    #[rstest]
1060    fn test_amend_entry_builds_with_no_identifiers() {
1061        let entry = BybitBatchAmendOrderEntryBuilder::default()
1062            .symbol("ETHUSDT".to_string())
1063            .price(Some("1600.0".to_string()))
1064            .build()
1065            .expect("amend entry optional identifiers default to None");
1066
1067        assert_eq!(entry.order_id, None);
1068        assert_eq!(entry.order_link_id, None);
1069    }
1070
1071    #[rstest]
1072    fn test_cancel_entry_builds_with_only_order_id() {
1073        let entry = BybitBatchCancelOrderEntryBuilder::default()
1074            .symbol("ETHUSDT".to_string())
1075            .order_id("12345".to_string())
1076            .build()
1077            .expect("cancel entry should build with only order_id");
1078
1079        assert_eq!(entry.order_id.as_deref(), Some("12345"));
1080        assert_eq!(entry.order_link_id, None);
1081    }
1082
1083    #[rstest]
1084    fn test_cancel_entry_builds_with_only_order_link_id() {
1085        let entry = BybitBatchCancelOrderEntryBuilder::default()
1086            .symbol("ETHUSDT".to_string())
1087            .order_link_id("link-1".to_string())
1088            .build()
1089            .expect("cancel entry should build with only order_link_id");
1090
1091        assert_eq!(entry.order_link_id.as_deref(), Some("link-1"));
1092        assert_eq!(entry.order_id, None);
1093    }
1094}