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