Skip to main content

bybit/http/
orders.rs

1use rust_decimal::{Decimal, serde::str_option::deserialize as option_decimal};
2use serde::{Deserialize, Serialize};
3use serde_aux::prelude::deserialize_number_from_string as number;
4
5use super::common::List;
6use crate::{
7    CancelType, CreateType, OcoTriggerBy, OrderStatus, OrderType, PlaceType, PositionIdx,
8    RejectReason, Side, SmpType, TimeInForce, Timestamp, TpslMode, TriggerBy, TriggerDirection,
9    enums::{Category, StopOrderType},
10    serde::{empty_string_as_none, string_to_option_bool},
11    ws::OrderMsg,
12};
13
14#[derive(Clone, Debug, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct GetOpenClosedOrdersParams {
17    /// Product type
18    /// UTA2.0, UTA1.0: linear, inverse, spot, option
19    /// classic account: linear, inverse, spot
20    pub category: Category,
21    /// Symbol name, like BTCUSDT, uppercase only. For linear, either symbol, baseCoin, settleCoin is required
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub symbol: Option<String>,
24    /// Base coin, uppercase only
25    /// Supports linear, inverse & option
26    /// option: it returns all option open orders by default
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub base_coin: Option<String>,
29    /// Settle coin, uppercase only
30    /// linear: either symbol, baseCoin or settleCoin is required
31    /// spot: not supported
32    /// option: USDT or USDC
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub settle_coin: Option<String>,
35    /// Order ID
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub order_id: Option<String>,
38    /// User customized order ID
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub order_link_id: Option<String>,
41    /// 0(default): UTA2.0, UTA1.0, classic account query open status orders (e.g., New, PartiallyFilled) only
42    /// 1: UTA2.0, UTA1.0(except inverse)
43    /// 2: UTA1.0(inverse), classic account
44    /// Query a maximum of recent 500 closed status records are kept under each account each category (e.g., Cancelled, Rejected, Filled orders).
45    /// If the Bybit service is restarted due to an update, this part of the data will be cleared and accumulated again, but the order records will still be queried in order history
46    /// openOnly param will be ignored when query by orderId or orderLinkId
47    /// Classic spot: not supported
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub open_only: Option<i32>,
50    /// Order: active order,
51    /// StopOrder: conditional order for Futures and Spot,
52    /// tpslOrder: spot TP/SL order,
53    /// OcoOrder: Spot oco order,
54    /// BidirectionalTpslOrder: Spot bidirectional TPSL order
55    /// - classic account spot: return Order active order by default
56    /// - Others: all kinds of orders by default
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub order_filter: Option<OrderFilter>,
59    /// Limit for data size per page. [1, 50]. Default: 20
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub limit: Option<i32>,
62    /// Cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub cursor: Option<String>,
65}
66
67impl GetOpenClosedOrdersParams {
68    pub fn new(category: Category) -> Self {
69        Self {
70            category,
71            symbol: None,
72            base_coin: None,
73            settle_coin: None,
74            order_id: None,
75            order_link_id: None,
76            open_only: None,
77            order_filter: None,
78            limit: None,
79            cursor: None,
80        }
81    }
82
83    pub fn with_symbol(mut self, v: String) -> Self {
84        self.symbol = Some(v);
85        self
86    }
87    pub fn with_base_coin(mut self, v: String) -> Self {
88        self.base_coin = Some(v);
89        self
90    }
91    pub fn with_settle_coin(mut self, v: String) -> Self {
92        self.settle_coin = Some(v);
93        self
94    }
95    pub fn with_order_id(mut self, v: String) -> Self {
96        self.order_id = Some(v);
97        self
98    }
99    pub fn with_order_link_id(mut self, v: String) -> Self {
100        self.order_link_id = Some(v);
101        self
102    }
103    pub fn with_open_only(mut self, v: i32) -> Self {
104        self.open_only = Some(v);
105        self
106    }
107    pub fn with_order_filter(mut self, v: OrderFilter) -> Self {
108        self.order_filter = Some(v);
109        self
110    }
111    pub fn with_limit(mut self, v: i32) -> Self {
112        self.limit = Some(v);
113        self
114    }
115    pub fn with_cursor(mut self, v: String) -> Self {
116        self.cursor = Some(v);
117        self
118    }
119}
120
121#[derive(Debug, Clone, Serialize)]
122pub enum OrderFilter {
123    /// active order,
124    Order,
125    /// conditional order for Futures and Spot,
126    StopOrder,
127    /// spot TP/SL order,
128    #[serde(rename = "camelCase")]
129    TpslOrder,
130    /// Spot oco order,
131    OcoOrder,
132    /// Spot bidirectional TPSL order
133    /// - classic account spot: return Order active order by default
134    /// - Others: all kinds of orders by default
135    BidirectionalTpslOrder,
136}
137
138#[derive(Debug, Deserialize, PartialEq, Clone)]
139#[serde(rename_all = "camelCase")]
140pub struct Order {
141    /// Order ID
142    pub order_id: String,
143    /// User customized order ID
144    #[serde(default, deserialize_with = "empty_string_as_none")]
145    pub order_link_id: Option<String>,
146    /// Paradigm block trade ID
147    #[serde(default, deserialize_with = "empty_string_as_none")]
148    pub block_trade_id: Option<String>,
149    /// Symbol name
150    pub symbol: String,
151    /// Order price
152    pub price: Decimal,
153    /// Order qty
154    pub qty: Decimal,
155    /// Side. Buy,Sell
156    pub side: Side,
157    /// Whether to borrow. Unified spot only. 0: false, 1: true. Classic spot is not supported, always 0
158    #[serde(default, deserialize_with = "string_to_option_bool")]
159    pub is_leverage: Option<bool>,
160    /// Position index. Used to identify positions in different position modes.
161    pub position_idx: PositionIdx,
162    /// Order status
163    pub order_status: OrderStatus,
164    /// Order create type
165    /// Only for category=linear or inverse
166    /// Spot, Option do not have this key
167    #[serde(default, deserialize_with = "empty_string_as_none")]
168    pub create_type: Option<CreateType>,
169    /// Cancel type
170    pub cancel_type: CancelType,
171    /// Reject reason. Classic spot is not supported
172    pub reject_reason: RejectReason,
173    /// Average filled price
174    /// UTA: returns "" for those orders without avg price
175    /// classic account: returns "0" for those orders without avg price, and also for those orders have partially filled but cancelled at the end
176    #[serde(default, deserialize_with = "option_decimal")]
177    pub avg_price: Option<Decimal>,
178    /// The remaining qty not executed. Classic spot is not supported
179    pub leaves_qty: Decimal,
180    /// The estimated value not executed. Classic spot is not supported
181    pub leaves_value: Decimal,
182    /// Cumulative executed order qty
183    pub cum_exec_qty: Decimal,
184    /// Cumulative executed order value. Classic spot is not supported
185    pub cum_exec_value: Decimal,
186    /// Cumulative executed trading fee. Classic spot is not supported
187    pub cum_exec_fee: Decimal,
188    /// Time in force
189    pub time_in_force: TimeInForce,
190    /// Order type. Market,Limit. For TP/SL order, it means the order type after triggered
191    pub order_type: OrderType,
192    /// Stop order type
193    #[serde(default, deserialize_with = "empty_string_as_none")]
194    pub stop_order_type: Option<StopOrderType>,
195    /// Implied volatility
196    #[serde(default, deserialize_with = "option_decimal")]
197    pub order_iv: Option<Decimal>,
198    /// The unit for qty when create Spot market orders for UTA account. baseCoin, quoteCoin
199    #[serde(default, deserialize_with = "empty_string_as_none")]
200    pub market_unit: Option<String>,
201    /// Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price
202    #[serde(default, deserialize_with = "option_decimal")]
203    pub trigger_price: Option<Decimal>,
204    /// Take profit price
205    #[serde(default, deserialize_with = "option_decimal")]
206    pub take_profit: Option<Decimal>,
207    /// Stop loss price
208    #[serde(default, deserialize_with = "option_decimal")]
209    pub stop_loss: Option<Decimal>,
210    /// TP/SL mode, Full: entire position for TP/SL. Partial: partial position tp/sl. Spot does not have this field, and Option returns always ""
211    #[serde(default, deserialize_with = "empty_string_as_none")]
212    pub tpsl_mode: Option<TpslMode>,
213    /// The trigger type of Spot OCO order.OcoTriggerByUnknown, OcoTriggerByTp, OcoTriggerByBySl. Classic spot is not supported
214    #[serde(default, deserialize_with = "empty_string_as_none")]
215    pub oco_trigger_by: Option<OcoTriggerBy>,
216    /// The limit order price when take profit price is triggered
217    #[serde(default, deserialize_with = "option_decimal")]
218    pub tp_limit_price: Option<Decimal>,
219    /// The limit order price when stop loss price is triggered
220    #[serde(default, deserialize_with = "option_decimal")]
221    pub sl_limit_price: Option<Decimal>,
222    /// The price type to trigger take profit
223    #[serde(default, deserialize_with = "empty_string_as_none")]
224    pub tp_trigger_by: Option<TriggerBy>,
225    /// The price type to trigger stop loss
226    #[serde(default, deserialize_with = "empty_string_as_none")]
227    pub sl_trigger_by: Option<TriggerBy>,
228    /// Trigger direction. 1: rise, 2: fall
229    pub trigger_direction: TriggerDirection,
230    /// The price type of trigger price
231    #[serde(default, deserialize_with = "empty_string_as_none")]
232    pub trigger_by: Option<TriggerBy>,
233    /// Last price when place the order, Spot is not applicable
234    #[serde(default, deserialize_with = "option_decimal")]
235    pub last_price_on_created: Option<Decimal>,
236    /// Last price when place the order, Spot has this field only
237    #[serde(default, deserialize_with = "option_decimal")]
238    pub base_price: Option<Decimal>,
239    /// Reduce only. true means reduce position size
240    pub reduce_only: bool,
241    /// Close on trigger. What is a close on trigger order?
242    pub close_on_trigger: bool,
243    /// Place type, option used. iv, price
244    #[serde(default, deserialize_with = "empty_string_as_none")]
245    pub place_type: Option<PlaceType>,
246    /// SMP execution type
247    pub smp_type: SmpType,
248    /// Smp group ID. If the UID has no group, it is 0 by default
249    pub smp_group: i64,
250    /// The counterparty's orderID which triggers this SMP execution
251    #[serde(default, deserialize_with = "empty_string_as_none")]
252    pub smp_order_id: Option<String>,
253    /// Order created timestamp (ms)
254    #[serde(deserialize_with = "number")]
255    pub created_time: Timestamp,
256    /// Order updated timestamp (ms)
257    #[serde(deserialize_with = "number")]
258    pub updated_time: Timestamp,
259}
260
261impl Order {
262    pub fn is_open_status(&self) -> bool {
263        self.order_status.is_open()
264    }
265    pub fn is_closed_status(&self) -> bool {
266        self.order_status.is_closed()
267    }
268    pub fn update(&mut self, msg: OrderMsg) {
269        self.order_id = msg.order_id;
270        self.order_link_id = msg.order_link_id;
271        self.block_trade_id = msg.block_trade_id;
272        self.symbol = msg.symbol;
273        self.price = msg.price;
274        self.qty = msg.qty;
275        self.side = msg.side;
276        self.is_leverage = msg.is_leverage;
277        self.position_idx = msg.position_idx;
278        self.order_status = msg.order_status;
279        self.create_type = msg.create_type;
280        self.cancel_type = msg.cancel_type;
281        self.reject_reason = msg.reject_reason;
282        self.avg_price = msg.avg_price;
283        if let Some(leaves_qty) = msg.leaves_qty {
284            self.leaves_qty = leaves_qty;
285        }
286        if let Some(leaves_value) = msg.leaves_value {
287            self.leaves_value = leaves_value;
288        }
289        self.cum_exec_qty = msg.cum_exec_qty;
290        self.cum_exec_value = msg.cum_exec_value;
291        self.cum_exec_fee = msg.cum_exec_fee;
292        self.time_in_force = msg.time_in_force;
293        self.order_type = msg.order_type;
294        self.stop_order_type = msg.stop_order_type;
295        self.order_iv = msg.order_iv;
296        self.market_unit = msg.market_unit;
297        self.trigger_price = msg.trigger_price;
298        self.take_profit = msg.take_profit;
299        self.stop_loss = msg.stop_loss;
300        self.tpsl_mode = msg.tpsl_mode;
301        self.oco_trigger_by = msg.oco_trigger_by;
302        self.tp_limit_price = msg.tp_limit_price;
303        self.sl_limit_price = msg.sl_limit_price;
304        self.tp_trigger_by = msg.tp_trigger_by;
305        self.sl_trigger_by = msg.sl_trigger_by;
306        self.trigger_direction = msg.trigger_direction;
307        self.trigger_by = msg.trigger_by;
308        self.last_price_on_created = msg.last_price_on_created;
309        // TODO: self.base_price
310        self.reduce_only = msg.reduce_only;
311        self.close_on_trigger = msg.close_on_trigger;
312        self.place_type = msg.place_type;
313        self.smp_type = msg.smp_type;
314        self.smp_group = msg.smp_group;
315        self.smp_order_id = msg.smp_order_id;
316        self.created_time = msg.created_time;
317        self.updated_time = msg.updated_time;
318    }
319}
320
321/// Build an [`Order`] from its first `order` stream message (an order that
322/// was not in a REST snapshot).
323///
324/// `base_price` is not sent by the stream and stays `None`. When the stream
325/// omits `leavesQty`/`leavesValue`, they are derived as `qty - cumExecQty`
326/// and `price * leavesQty`.
327impl From<OrderMsg> for Order {
328    fn from(msg: OrderMsg) -> Self {
329        let leaves_qty = msg.leaves_qty.unwrap_or(msg.qty - msg.cum_exec_qty);
330        let leaves_value = msg.leaves_value.unwrap_or(msg.price * leaves_qty);
331        Self {
332            order_id: msg.order_id,
333            order_link_id: msg.order_link_id,
334            block_trade_id: msg.block_trade_id,
335            symbol: msg.symbol,
336            price: msg.price,
337            qty: msg.qty,
338            side: msg.side,
339            is_leverage: msg.is_leverage,
340            position_idx: msg.position_idx,
341            order_status: msg.order_status,
342            create_type: msg.create_type,
343            cancel_type: msg.cancel_type,
344            reject_reason: msg.reject_reason,
345            avg_price: msg.avg_price,
346            leaves_qty,
347            leaves_value,
348            cum_exec_qty: msg.cum_exec_qty,
349            cum_exec_value: msg.cum_exec_value,
350            cum_exec_fee: msg.cum_exec_fee,
351            time_in_force: msg.time_in_force,
352            order_type: msg.order_type,
353            stop_order_type: msg.stop_order_type,
354            order_iv: msg.order_iv,
355            market_unit: msg.market_unit,
356            trigger_price: msg.trigger_price,
357            take_profit: msg.take_profit,
358            stop_loss: msg.stop_loss,
359            tpsl_mode: msg.tpsl_mode,
360            oco_trigger_by: msg.oco_trigger_by,
361            tp_limit_price: msg.tp_limit_price,
362            sl_limit_price: msg.sl_limit_price,
363            tp_trigger_by: msg.tp_trigger_by,
364            sl_trigger_by: msg.sl_trigger_by,
365            trigger_direction: msg.trigger_direction,
366            trigger_by: msg.trigger_by,
367            last_price_on_created: msg.last_price_on_created,
368            base_price: None,
369            reduce_only: msg.reduce_only,
370            close_on_trigger: msg.close_on_trigger,
371            place_type: msg.place_type,
372            smp_type: msg.smp_type,
373            smp_group: msg.smp_group,
374            smp_order_id: msg.smp_order_id,
375            created_time: msg.created_time,
376            updated_time: msg.updated_time,
377        }
378    }
379}
380
381#[derive(Debug, Serialize, Clone)]
382#[serde(rename_all = "camelCase")]
383pub struct PlaceOrderRequest {
384    /// Product type
385    /// UTA2.0, UTA1.0: linear, inverse, spot, option
386    /// classic account: linear, inverse, spot
387    pub category: Category,
388    /// Symbol name, like BTCUSDT, uppercase only
389    pub symbol: String,
390    // Whether to borrow. Unified account Spot trading only.
391    /// 0(default): false, spot trading
392    /// 1: true, margin trading, make sure you turn on margin trading, and set the relevant currency as collateral
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub is_leverage: Option<i64>,
395    /// Buy, Sell
396    pub side: Side,
397    /// Market, Limit
398    pub order_type: OrderType,
399    /// Order quantity
400    /// UTA account
401    /// Spot: Market Buy order by value by default, you can set marketUnit field to choose order by value or qty for market orders
402    /// Perps, Futures & Option: always order by qty
403    /// classic account
404    /// Spot: Market Buy order by value by default
405    /// Perps, Futures: always order by qty
406    /// Perps & Futures: if you pass qty="0" and specify reduceOnly=true&closeOnTrigger=true, you can close the position up to maxMktOrderQty or maxOrderQty shown on Get Instruments Info of current symbol
407    pub qty: Decimal,
408    /// Select the unit for qty when create Spot market orders for UTA account
409    /// baseCoin: for example, buy BTCUSDT, then "qty" unit is BTC
410    /// quoteCoin: for example, sell BTCUSDT, then "qty" unit is USDT
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub market_unit: Option<String>,
413    /// Slippage tolerance Type for market order, TickSize, Percent
414    /// take profit, stoploss, conditional orders are not supported
415    /// TickSize:
416    /// the highest price of Buy order = ask1 + slippageTolerance x tickSize;
417    /// the lowest price of Sell order = bid1 - slippageTolerance x tickSize
418    /// Percent:
419    /// the highest price of Buy order = ask1 x (1 + slippageTolerance x 0.01);
420    /// the lowest price of Sell order = bid1 x (1 - slippageTolerance x 0.01)
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub slippage_tolerance_type: Option<Decimal>,
423    /// Slippage tolerance value
424    /// TickSize: range is [1, 10000], integer only
425    /// Percent: range is [0.01, 10], up to 2 decimals
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub slippage_tolerance: Option<Decimal>,
428    /// Order price
429    /// Market order will ignore this field
430    /// Please check the min price and price precision from instrument info endpoint
431    /// If you have position, price needs to be better than liquidation price
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub price: Option<Decimal>,
434    /// Conditional order param. Used to identify the expected direction of the conditional order.
435    /// 1: triggered when market price rises to triggerPrice
436    /// 2: triggered when market price falls to triggerPrice
437    /// Valid for linear & inverse
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub trigger_direction: Option<TriggerDirection>,
440    /// If it is not passed, Order by default.
441    /// Order
442    /// tpslOrder: Spot TP/SL order, the assets are occupied even before the order is triggered
443    /// StopOrder: Spot conditional order, the assets will not be occupied until the price of the underlying asset reaches the trigger price, and the required assets will be occupied after the Conditional order is triggered
444    /// Valid for spot only
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub order_filter: Option<String>,
447    /// For Perps & Futures, it is the conditional order trigger price. If you expect the price to rise to trigger your conditional order, make sure:
448    /// triggerPrice > market price
449    /// Else, triggerPrice < market price
450    /// For spot, it is the TP/SL and Conditional order trigger price
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub trigger_price: Option<Decimal>,
453    /// Trigger price type, Conditional order param for Perps & Futures.
454    /// LastPrice
455    /// IndexPrice
456    /// MarkPrice
457    /// Valid for linear & inverse
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub trigger_by: Option<TriggerBy>,
460    /// Implied volatility. option only. Pass the real value, e.g for 10%, 0.1 should be passed. orderIv has a higher priority when price is passed as well
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub order_iv: Option<Decimal>,
463    /// Time in force
464    /// Market order will always use IOC
465    /// If not passed, GTC is used by default
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub time_in_force: Option<TimeInForce>,
468    /// Used to identify positions in different position modes. Under hedge-mode, this param is required
469    /// 0: one-way mode
470    /// 1: hedge-mode Buy side
471    /// 2: hedge-mode Sell side
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub position_idx: Option<PositionIdx>,
474    /// User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported.
475    /// Futures & Perps: orderLinkId rules:
476    /// optional param
477    /// always unique
478    /// option orderLinkId rules:
479    /// required param
480    /// always unique
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub order_link_id: Option<String>,
483    /// Take profit price
484    /// UTA: Spot Limit order supports take profit, stop loss or limit take profit, limit stop loss when creating an order
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub take_profit: Option<Decimal>,
487    /// Stop loss price
488    /// UTA: Spot Limit order supports take profit, stop loss or limit take profit, limit stop loss when creating an order
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub stop_loss: Option<Decimal>,
491    /// The price type to trigger take profit. MarkPrice, IndexPrice, default: LastPrice. Valid for linear & inverse
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub tp_trigger_by: Option<TriggerBy>,
494    /// The price type to trigger stop loss. MarkPrice, IndexPrice, default: LastPrice. Valid for linear & inverse
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub sl_trigger_by: Option<TriggerBy>,
497    /// What is a reduce-only order? true means your position can only reduce in size if this order is triggered.
498    /// You must specify it as true when you are about to close/reduce the position
499    /// When reduceOnly is true, take profit/stop loss cannot be set
500    /// Valid for linear, inverse & option
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub reduce_only: Option<bool>,
503    /// What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin.
504    /// Valid for linear & inverse
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub close_on_trigger: Option<bool>,
507    /// Smp execution type. What is SMP?
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub smp_type: Option<SmpType>,
510    /// Market maker protection. option only. true means set the order as a market maker protection order. What is mmp?
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub mmp: Option<bool>,
513    /// TP/SL mode
514    /// Full: entire position for TP/SL. Then, tpOrderType or slOrderType must be Market
515    /// Partial: partial position tp/sl (as there is no size option, so it will create tp/sl orders with the qty you actually fill). Limit TP/SL order are supported. Note: When create limit tp/sl, tpslMode is required and it must be Partial
516    /// Valid for linear & inverse
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub tpsl_mode: Option<TpslMode>,
519    /// The limit order price when take profit price is triggered
520    /// linear & inverse: only works when tpslMode=Partial and tpOrderType=Limit
521    /// Spot(UTA): it is required when the order has takeProfit and "tpOrderType"=Limit
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub tp_limit_price: Option<Decimal>,
524    /// The limit order price when stop loss price is triggered
525    /// linear & inverse: only works when tpslMode=Partial and slOrderType=Limit
526    /// Spot(UTA): it is required when the order has stopLoss and "slOrderType"=Limit
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub sl_limit_price: Option<Decimal>,
529    /// The order type when take profit is triggered
530    /// linear & inverse: Market(default), Limit. For tpslMode=Full, it only supports tpOrderType=Market
531    /// Spot(UTA):
532    /// Market: when you set "takeProfit",
533    /// Limit: when you set "takeProfit" and "tpLimitPrice"
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub tp_order_type: Option<OrderType>,
536    /// The order type when stop loss is triggered
537    /// linear & inverse: Market(default), Limit. For tpslMode=Full, it only supports slOrderType=Market
538    /// Spot(UTA):
539    /// Market: when you set "stopLoss",
540    /// Limit: when you set "stopLoss" and "slLimitPrice"
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub sl_order_type: Option<OrderType>,
543    /// Queue: use the order price on the orderbook in the same direction as the side
544    /// Counterparty: use the order price on the orderbook in the opposite direction as the side
545    /// Valid for linear & inverse
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub bbo_side_type: Option<String>,
548    /// 1,2,3,4,5 Valid for linear & inverse
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub bbo_level: Option<String>,
551}
552
553impl PlaceOrderRequest {
554    pub fn new(
555        category: Category,
556        symbol: String,
557        side: Side,
558        order_type: OrderType,
559        qty: Decimal,
560    ) -> Self {
561        Self {
562            category,
563            symbol,
564            is_leverage: None,
565            side,
566            order_type,
567            qty,
568            market_unit: None,
569            slippage_tolerance_type: None,
570            slippage_tolerance: None,
571            price: None,
572            trigger_direction: None,
573            order_filter: None,
574            trigger_price: None,
575            trigger_by: None,
576            order_iv: None,
577            time_in_force: None,
578            position_idx: None,
579            order_link_id: None,
580            take_profit: None,
581            stop_loss: None,
582            tp_trigger_by: None,
583            sl_trigger_by: None,
584            reduce_only: None,
585            close_on_trigger: None,
586            smp_type: None,
587            mmp: None,
588            tpsl_mode: None,
589            tp_limit_price: None,
590            sl_limit_price: None,
591            tp_order_type: None,
592            sl_order_type: None,
593            bbo_side_type: None,
594            bbo_level: None,
595        }
596    }
597
598    pub fn with_is_leverage(mut self, v: i64) -> Self {
599        self.is_leverage = Some(v);
600        self
601    }
602    pub fn with_market_unit(mut self, v: String) -> Self {
603        self.market_unit = Some(v);
604        self
605    }
606    pub fn with_slippage_tolerance_type(mut self, v: Decimal) -> Self {
607        self.slippage_tolerance_type = Some(v);
608        self
609    }
610    pub fn with_slippage_tolerance(mut self, v: Decimal) -> Self {
611        self.slippage_tolerance = Some(v);
612        self
613    }
614    pub fn with_price(mut self, v: Decimal) -> Self {
615        self.price = Some(v);
616        self
617    }
618    pub fn with_trigger_direction(mut self, v: TriggerDirection) -> Self {
619        self.trigger_direction = Some(v);
620        self
621    }
622    pub fn with_order_filter(mut self, v: String) -> Self {
623        self.order_filter = Some(v);
624        self
625    }
626    pub fn with_trigger_price(mut self, v: Decimal) -> Self {
627        self.trigger_price = Some(v);
628        self
629    }
630    pub fn with_trigger_by(mut self, v: TriggerBy) -> Self {
631        self.trigger_by = Some(v);
632        self
633    }
634    pub fn with_order_iv(mut self, v: Decimal) -> Self {
635        self.order_iv = Some(v);
636        self
637    }
638    pub fn with_time_in_force(mut self, v: TimeInForce) -> Self {
639        self.time_in_force = Some(v);
640        self
641    }
642    pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
643        self.position_idx = Some(v);
644        self
645    }
646    pub fn with_order_link_id(mut self, v: String) -> Self {
647        self.order_link_id = Some(v);
648        self
649    }
650    pub fn with_take_profit(mut self, v: Decimal) -> Self {
651        self.take_profit = Some(v);
652        self
653    }
654    pub fn with_stop_loss(mut self, v: Decimal) -> Self {
655        self.stop_loss = Some(v);
656        self
657    }
658    pub fn with_tp_trigger_by(mut self, v: TriggerBy) -> Self {
659        self.tp_trigger_by = Some(v);
660        self
661    }
662    pub fn with_sl_trigger_by(mut self, v: TriggerBy) -> Self {
663        self.sl_trigger_by = Some(v);
664        self
665    }
666    pub fn with_reduce_only(mut self, v: bool) -> Self {
667        self.reduce_only = Some(v);
668        self
669    }
670    pub fn with_close_on_trigger(mut self, v: bool) -> Self {
671        self.close_on_trigger = Some(v);
672        self
673    }
674    pub fn with_smp_type(mut self, v: SmpType) -> Self {
675        self.smp_type = Some(v);
676        self
677    }
678    pub fn with_mmp(mut self, v: bool) -> Self {
679        self.mmp = Some(v);
680        self
681    }
682    pub fn with_tpsl_mode(mut self, v: TpslMode) -> Self {
683        self.tpsl_mode = Some(v);
684        self
685    }
686    pub fn with_tp_limit_price(mut self, v: Decimal) -> Self {
687        self.tp_limit_price = Some(v);
688        self
689    }
690    pub fn with_sl_limit_price(mut self, v: Decimal) -> Self {
691        self.sl_limit_price = Some(v);
692        self
693    }
694    pub fn with_tp_order_type(mut self, v: OrderType) -> Self {
695        self.tp_order_type = Some(v);
696        self
697    }
698    pub fn with_sl_order_type(mut self, v: OrderType) -> Self {
699        self.sl_order_type = Some(v);
700        self
701    }
702    pub fn with_bbo_side_type(mut self, v: String) -> Self {
703        self.bbo_side_type = Some(v);
704        self
705    }
706    pub fn with_bbo_level(mut self, v: String) -> Self {
707        self.bbo_level = Some(v);
708        self
709    }
710}
711
712#[derive(Debug, Deserialize, PartialEq)]
713#[serde(rename_all = "camelCase")]
714pub struct PlaceOrderResponse {
715    /// Order ID
716    pub order_id: String,
717    /// User customised order ID
718    pub order_link_id: String,
719}
720
721#[derive(Debug, Serialize, Clone)]
722#[serde(rename_all = "camelCase")]
723pub struct AmendOrderRequest {
724    pub category: Category,
725    pub symbol: String,
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub order_id: Option<String>,
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub order_link_id: Option<String>,
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub qty: Option<Decimal>,
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub price: Option<Decimal>,
734}
735
736impl AmendOrderRequest {
737    pub fn new(category: Category, symbol: String) -> Self {
738        Self {
739            category,
740            symbol,
741            order_id: None,
742            order_link_id: None,
743            qty: None,
744            price: None,
745        }
746    }
747
748    pub fn with_order_id(mut self, v: String) -> Self {
749        self.order_id = Some(v);
750        self
751    }
752    pub fn with_order_link_id(mut self, v: String) -> Self {
753        self.order_link_id = Some(v);
754        self
755    }
756    pub fn with_qty(mut self, v: Decimal) -> Self {
757        self.qty = Some(v);
758        self
759    }
760    pub fn with_price(mut self, v: Decimal) -> Self {
761        self.price = Some(v);
762        self
763    }
764}
765
766#[derive(Debug, Deserialize, PartialEq)]
767#[serde(rename_all = "camelCase")]
768pub struct AmendOrderResponse {
769    /// Order ID
770    pub order_id: String,
771    #[serde(default, deserialize_with = "empty_string_as_none")]
772    /// User customised order ID
773    pub order_link_id: Option<String>,
774}
775
776#[derive(Debug, Serialize, Clone)]
777#[serde(rename_all = "camelCase")]
778pub struct CancelOrderRequest {
779    /// Product type. linear, inverse, spot, option
780    pub category: Category,
781    /// Symbol name, like BTCUSDT, uppercase only
782    pub symbol: String,
783    /// Order ID. Either orderId or orderLinkId is required
784    #[serde(skip_serializing_if = "Option::is_none")]
785    pub order_id: Option<String>,
786    /// User customised order ID. Either orderId or orderLinkId is required
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub order_link_id: Option<String>,
789    /// Spot trading only
790    /// Order, tpslOrder, StopOrder
791    /// If not passed, Order by default
792    #[serde(skip_serializing_if = "Option::is_none")]
793    pub order_filter: Option<OrderFilter>,
794}
795
796impl CancelOrderRequest {
797    pub fn new(category: Category, symbol: String) -> Self {
798        Self {
799            category,
800            symbol,
801            order_id: None,
802            order_link_id: None,
803            order_filter: None,
804        }
805    }
806
807    pub fn with_order_id(mut self, v: String) -> Self {
808        self.order_id = Some(v);
809        self
810    }
811    pub fn with_order_link_id(mut self, v: String) -> Self {
812        self.order_link_id = Some(v);
813        self
814    }
815    pub fn with_order_filter(mut self, v: OrderFilter) -> Self {
816        self.order_filter = Some(v);
817        self
818    }
819}
820
821#[derive(Debug, Deserialize, PartialEq)]
822#[serde(rename_all = "camelCase")]
823pub struct CancelOrderResponse {
824    /// Order ID
825    pub order_id: String,
826    /// User customised order ID
827    #[serde(default, deserialize_with = "empty_string_as_none")]
828    pub order_link_id: Option<String>,
829}
830
831// ── Cancel All Orders ────────────────────────────────────────────────────────
832
833#[derive(Debug, Serialize, Clone)]
834#[serde(rename_all = "camelCase")]
835pub struct CancelAllOrdersRequest {
836    pub category: Category,
837    #[serde(skip_serializing_if = "Option::is_none")]
838    pub symbol: Option<String>,
839    #[serde(skip_serializing_if = "Option::is_none")]
840    pub base_coin: Option<String>,
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub settle_coin: Option<String>,
843    #[serde(skip_serializing_if = "Option::is_none")]
844    pub order_filter: Option<OrderFilter>,
845    /// For futures only: TakeProfit, StopLoss, TrailingStop
846    #[serde(skip_serializing_if = "Option::is_none")]
847    pub stop_order_type: Option<StopOrderType>,
848}
849
850impl CancelAllOrdersRequest {
851    pub fn new(category: Category) -> Self {
852        Self {
853            category,
854            symbol: None,
855            base_coin: None,
856            settle_coin: None,
857            order_filter: None,
858            stop_order_type: None,
859        }
860    }
861
862    pub fn with_symbol(mut self, v: String) -> Self {
863        self.symbol = Some(v);
864        self
865    }
866    pub fn with_base_coin(mut self, v: String) -> Self {
867        self.base_coin = Some(v);
868        self
869    }
870    pub fn with_settle_coin(mut self, v: String) -> Self {
871        self.settle_coin = Some(v);
872        self
873    }
874    pub fn with_order_filter(mut self, v: OrderFilter) -> Self {
875        self.order_filter = Some(v);
876        self
877    }
878    pub fn with_stop_order_type(mut self, v: StopOrderType) -> Self {
879        self.stop_order_type = Some(v);
880        self
881    }
882}
883
884/// Response for cancel-all: a flat list of cancelled order IDs.
885pub type CancelAllOrdersResponse = List<CancelOrderResponse>;
886
887// ── Order History ────────────────────────────────────────────────────────────
888
889#[derive(Clone, Debug, Serialize)]
890#[serde(rename_all = "camelCase")]
891pub struct GetOrderHistoryParams {
892    pub category: Category,
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub symbol: Option<String>,
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub base_coin: Option<String>,
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub settle_coin: Option<String>,
899    #[serde(skip_serializing_if = "Option::is_none")]
900    pub order_id: Option<String>,
901    #[serde(skip_serializing_if = "Option::is_none")]
902    pub order_link_id: Option<String>,
903    #[serde(skip_serializing_if = "Option::is_none")]
904    pub order_filter: Option<OrderFilter>,
905    #[serde(skip_serializing_if = "Option::is_none")]
906    pub order_status: Option<OrderStatus>,
907    /// Start timestamp (ms)
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub start_time: Option<Timestamp>,
910    /// End timestamp (ms)
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub end_time: Option<Timestamp>,
913    /// [1, 50]. Default: 20
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub limit: Option<i32>,
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub cursor: Option<String>,
918}
919
920impl GetOrderHistoryParams {
921    pub fn new(category: Category) -> Self {
922        Self {
923            category,
924            symbol: None,
925            base_coin: None,
926            settle_coin: None,
927            order_id: None,
928            order_link_id: None,
929            order_filter: None,
930            order_status: None,
931            start_time: None,
932            end_time: None,
933            limit: None,
934            cursor: None,
935        }
936    }
937
938    pub fn with_symbol(mut self, v: String) -> Self {
939        self.symbol = Some(v);
940        self
941    }
942    pub fn with_base_coin(mut self, v: String) -> Self {
943        self.base_coin = Some(v);
944        self
945    }
946    pub fn with_settle_coin(mut self, v: String) -> Self {
947        self.settle_coin = Some(v);
948        self
949    }
950    pub fn with_order_id(mut self, v: String) -> Self {
951        self.order_id = Some(v);
952        self
953    }
954    pub fn with_order_link_id(mut self, v: String) -> Self {
955        self.order_link_id = Some(v);
956        self
957    }
958    pub fn with_order_filter(mut self, v: OrderFilter) -> Self {
959        self.order_filter = Some(v);
960        self
961    }
962    pub fn with_order_status(mut self, v: OrderStatus) -> Self {
963        self.order_status = Some(v);
964        self
965    }
966    pub fn with_start_time(mut self, v: Timestamp) -> Self {
967        self.start_time = Some(v);
968        self
969    }
970    pub fn with_end_time(mut self, v: Timestamp) -> Self {
971        self.end_time = Some(v);
972        self
973    }
974    pub fn with_limit(mut self, v: i32) -> Self {
975        self.limit = Some(v);
976        self
977    }
978    pub fn with_cursor(mut self, v: String) -> Self {
979        self.cursor = Some(v);
980        self
981    }
982}
983
984// ── Batch Orders ─────────────────────────────────────────────────────────────
985
986#[derive(Debug, Serialize, Clone)]
987#[serde(rename_all = "camelCase")]
988pub struct PlaceOrderBatchRequest {
989    pub category: Category,
990    pub request: Vec<PlaceOrderRequest>,
991}
992
993impl PlaceOrderBatchRequest {
994    pub fn new(category: Category, request: Vec<PlaceOrderRequest>) -> Self {
995        Self { category, request }
996    }
997}
998
999#[derive(Debug, Serialize, Clone)]
1000#[serde(rename_all = "camelCase")]
1001pub struct AmendOrderBatchRequest {
1002    pub category: Category,
1003    pub request: Vec<AmendOrderRequest>,
1004}
1005
1006impl AmendOrderBatchRequest {
1007    pub fn new(category: Category, request: Vec<AmendOrderRequest>) -> Self {
1008        Self { category, request }
1009    }
1010}
1011
1012#[derive(Debug, Serialize, Clone)]
1013#[serde(rename_all = "camelCase")]
1014pub struct CancelOrderBatchRequest {
1015    pub category: Category,
1016    pub request: Vec<CancelOrderRequest>,
1017}
1018
1019impl CancelOrderBatchRequest {
1020    pub fn new(category: Category, request: Vec<CancelOrderRequest>) -> Self {
1021        Self { category, request }
1022    }
1023}
1024
1025#[derive(Debug, Deserialize, PartialEq)]
1026#[serde(rename_all = "camelCase")]
1027pub struct PlaceOrderBatchResult {
1028    pub category: Category,
1029    pub symbol: String,
1030    pub order_id: String,
1031    #[serde(default, deserialize_with = "empty_string_as_none")]
1032    pub order_link_id: Option<String>,
1033    /// Order creation timestamp (ms). Note: Bybit uses the non-standard key "createAt".
1034    #[serde(rename = "createAt", default)]
1035    pub create_at: Option<Timestamp>,
1036}
1037
1038#[derive(Debug, Deserialize, PartialEq)]
1039#[serde(rename_all = "camelCase")]
1040pub struct AmendOrderBatchResult {
1041    pub category: Category,
1042    pub symbol: String,
1043    pub order_id: String,
1044    #[serde(default, deserialize_with = "empty_string_as_none")]
1045    pub order_link_id: Option<String>,
1046}
1047
1048#[derive(Debug, Deserialize, PartialEq)]
1049#[serde(rename_all = "camelCase")]
1050pub struct CancelOrderBatchResult {
1051    pub category: Category,
1052    pub symbol: String,
1053    pub order_id: String,
1054    #[serde(default, deserialize_with = "empty_string_as_none")]
1055    pub order_link_id: Option<String>,
1056}
1057
1058// --- Spot Borrow Check ---
1059
1060/// Query params for [`Client::get_spot_borrow_check`](crate::http::Client::get_spot_borrow_check).
1061///
1062/// Covers: spot only
1063#[derive(Debug, Serialize, Clone)]
1064pub struct GetSpotBorrowCheckParams {
1065    pub category: Category,
1066    pub symbol: String,
1067    pub side: Side,
1068}
1069
1070impl GetSpotBorrowCheckParams {
1071    pub fn new(category: Category, symbol: impl Into<String>, side: Side) -> Self {
1072        Self {
1073            category,
1074            symbol: symbol.into(),
1075            side,
1076        }
1077    }
1078}
1079
1080/// Response for [`Client::get_spot_borrow_check`](crate::http::Client::get_spot_borrow_check).
1081#[derive(Debug, Deserialize, PartialEq)]
1082#[serde(rename_all = "camelCase")]
1083pub struct SpotBorrowCheck {
1084    pub symbol: String,
1085    pub side: Side,
1086    /// Maximum tradeable quantity with leverage (in base coin).
1087    pub max_trade_qty: Decimal,
1088    /// Maximum tradeable amount with leverage (in quote coin).
1089    pub max_trade_amount: Decimal,
1090    /// Maximum tradeable quantity without leverage (in base coin).
1091    pub spot_max_trade_qty: Decimal,
1092    /// Maximum tradeable amount without leverage (in quote coin).
1093    pub spot_max_trade_amount: Decimal,
1094    /// The coin to borrow.
1095    pub borrow_coin: String,
1096}