Skip to main content

bybit/http/
positions.rs

1use rust_decimal::{Decimal, serde::str_option::deserialize as option_decimal};
2use serde::{Deserialize, Serialize};
3use serde_aux::prelude::{
4    deserialize_number_from_string as number,
5    deserialize_option_number_from_string as option_number,
6};
7
8use crate::{
9    AdlRankIndicator, ExecType, OrderType, PositionIdx, PositionMode, PositionStatus, Side,
10    Timestamp, TpslMode, TradeMode, TriggerBy,
11    enums::{Category, StopOrderType},
12    serde::{decimal_or_zero, empty_string_as_none, int_to_bool, timestamp_or_zero},
13    ws::PositionMsg,
14};
15
16/// Query parameters of `GET /v5/position/list` ([`Client::get_position_info`](crate::http::Client::get_position_info)).
17#[derive(Clone, Debug, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct GetPositionInfoParams {
20    /// Product type
21    /// UTA2.0, UTA1.0: linear, inverse, option
22    /// Classic account: linear, inverse
23    pub category: Category,
24    /// Symbol name, like BTCUSDT, uppercase only
25    /// If symbol passed, it returns data regardless of having position or not.
26    /// If symbol=null and settleCoin specified, it returns position size greater than zero.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub symbol: Option<String>,
29    /// Base coin, uppercase only. option only. Return all option positions if not passed
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub base_coin: Option<String>,
32    /// Settle coin
33    /// linear: either symbol or settleCoin is required. symbol has a higher priority
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub settle_coin: Option<String>,
36    /// Limit for data size per page. [1, 200]. Default: 20
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub limit: Option<u64>,
39    /// Cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub cursor: Option<String>,
42}
43
44impl GetPositionInfoParams {
45    /// Create with the required fields; set the optional ones with the `with_*` methods.
46    pub fn new(category: Category) -> Self {
47        Self {
48            category,
49            symbol: None,
50            base_coin: None,
51            settle_coin: None,
52            limit: None,
53            cursor: None,
54        }
55    }
56
57    /// Set `symbol`: symbol name, like BTCUSDT, uppercase only If symbol passed, it returns data regardless of having position or not. If symbol=null and settleCoin specified, it returns position size greater than zero.
58    pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
59        self.symbol = Some(v.into());
60        self
61    }
62    /// Set `base_coin`: base coin, uppercase only. option only. Return all option positions if not passed.
63    pub fn with_base_coin(mut self, v: impl Into<String>) -> Self {
64        self.base_coin = Some(v.into());
65        self
66    }
67    /// Set `settle_coin`: settle coin linear: either symbol or settleCoin is required. symbol has a higher priority.
68    pub fn with_settle_coin(mut self, v: impl Into<String>) -> Self {
69        self.settle_coin = Some(v.into());
70        self
71    }
72    /// Set `limit`: limit for data size per page. [1, 200]. Default: 20.
73    pub fn with_limit(mut self, v: u64) -> Self {
74        self.limit = Some(v);
75        self
76    }
77    /// Set `cursor`: cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set.
78    pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
79        self.cursor = Some(v.into());
80        self
81    }
82}
83
84/// A position from `GET /v5/position/list`.
85///
86/// Fields checked against the Bybit docs (2026-10); the deprecated
87/// `tpslMode`, `bustPrice`, `positionBalance` and `tradeMode` are omitted.
88#[derive(Debug, Deserialize, PartialEq)]
89#[serde(rename_all = "camelCase")]
90pub struct Position {
91    /// Position idx, used to identify positions in different position modes
92    /// 0: One-Way Mode
93    /// 1: Buy side of both side mode
94    /// 2: Sell side of both side mode
95    pub position_idx: PositionIdx,
96    /// Risk tier ID
97    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
98    pub risk_id: i64,
99    /// Risk limit value
100    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
101    #[serde(default, deserialize_with = "option_decimal")]
102    pub risk_limit_value: Option<Decimal>,
103    /// Symbol name
104    pub symbol: String,
105    /// Position side. Buy: long, Sell: short
106    /// one-way mode: classic & UTA1.0(inverse), an empty position returns None.
107    /// UTA2.0(linear, inverse) & UTA1.0(linear): either one-way or hedge mode returns an empty string "" for an empty position.
108    #[serde(default, deserialize_with = "empty_string_as_none")]
109    pub side: Option<Side>,
110    /// Position size, always positive
111    pub size: Decimal,
112    /// Average entry price
113    /// For USDC Perp & Futures, it indicates average entry price, and it will not be changed with 8-hour session settlement
114    #[serde(deserialize_with = "decimal_or_zero")]
115    pub avg_price: Decimal,
116    /// Position value
117    #[serde(default, deserialize_with = "option_decimal")]
118    pub position_value: Option<Decimal>,
119    /// Whether to add margin automatically when using isolated margin mode
120    /// 0: false
121    /// 1: true
122    #[serde(deserialize_with = "int_to_bool")]
123    pub auto_add_margin: bool,
124    /// Position status. Normal, Liq, Adl
125    #[serde(default, deserialize_with = "empty_string_as_none")]
126    pub position_status: Option<PositionStatus>,
127    /// Position leverage
128    /// for portfolio margin mode, this field returns "", which means leverage rules are invalid
129    pub leverage: Decimal,
130    /// Mark price
131    #[serde(deserialize_with = "decimal_or_zero")]
132    pub mark_price: Decimal,
133    /// Position liquidation price
134    /// UTA2.0(isolated margin), UTA1.0(isolated margin), UTA1.0(inverse), Classic account:
135    /// it is the real price for isolated and cross positions, and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
136    /// UTA2.0(Cross margin), UTA1.0(Cross margin):
137    /// it is an estimated price for cross positions(because the unified mode controls the risk rate according to the account), and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
138    /// this field is empty for Portfolio Margin Mode, and no liquidation price will be provided
139    #[serde(default, deserialize_with = "option_decimal")]
140    pub liq_price: Option<Decimal>,
141    /// Initial margin
142    /// Classic & UTA1.0(inverse): ignore this field
143    /// UTA portfolio margin mode, it returns ""
144    #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
145    pub position_im: Option<Decimal>,
146    /// Initial margin calculated by mark price
147    /// Classic & UTA1.0(inverse) : ignore this field
148    /// UTA portfolio margin mode, it returns ""
149    #[serde(
150        rename = "positionIMByMp",
151        default,
152        deserialize_with = "option_decimal"
153    )]
154    pub position_im_by_mp: Option<Decimal>,
155    /// Maintenance margin
156    /// Classic & UTA1.0(inverse): ignore this field
157    /// UTA portfolio margin mode, it returns ""
158    #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
159    pub position_mm: Option<Decimal>,
160    /// Maintenance margin calculated by mark price
161    /// Classic & UTA1.0(inverse) : ignore this field
162    /// UTA portfolio margin mode, it returns ""
163    #[serde(
164        rename = "positionMMByMp",
165        default,
166        deserialize_with = "option_decimal"
167    )]
168    pub position_mm_by_mp: Option<Decimal>,
169    /// Take profit price
170    #[serde(default, deserialize_with = "option_decimal")]
171    pub take_profit: Option<Decimal>,
172    /// Stop loss price
173    #[serde(default, deserialize_with = "option_decimal")]
174    pub stop_loss: Option<Decimal>,
175    /// Trailing stop (The distance from market price)
176    #[serde(default, deserialize_with = "option_decimal")]
177    pub trailing_stop: Option<Decimal>,
178    /// USDC contract session avg price, it is the same figure as avg entry price shown in the web UI
179    #[serde(default, deserialize_with = "option_decimal")]
180    pub session_avg_price: Option<Decimal>,
181    /// Break even price (linear and inverse only).
182    #[serde(default, deserialize_with = "option_decimal")]
183    pub break_even_price: Option<Decimal>,
184    /// Net delta ratio; option delta is excluded from the calculation.
185    #[serde(default, deserialize_with = "option_decimal")]
186    pub net_delta_ratio: Option<Decimal>,
187    /// Position open timestamp (ms); `None` when not provided or 0.
188    #[serde(default, deserialize_with = "option_number")]
189    pub open_time: Option<Timestamp>,
190    /// Delta
191    #[serde(default, deserialize_with = "empty_string_as_none")]
192    pub delta: Option<String>,
193    /// Gamma
194    #[serde(default, deserialize_with = "empty_string_as_none")]
195    pub gamma: Option<String>,
196    /// Vega
197    #[serde(default, deserialize_with = "empty_string_as_none")]
198    pub vega: Option<String>,
199    /// Theta
200    #[serde(default, deserialize_with = "empty_string_as_none")]
201    pub theta: Option<String>,
202    /// Unrealised PnL
203    #[serde(default, deserialize_with = "option_decimal")]
204    pub unrealised_pnl: Option<Decimal>,
205    /// The realised PnL for the current holding position
206    #[serde(deserialize_with = "decimal_or_zero")]
207    pub cur_realised_pnl: Decimal,
208    /// Cumulative realised pnl
209    /// Futures & Perpetuals: it is the all time cumulative realised P&L
210    /// Option: always "", meaningless
211    #[serde(deserialize_with = "decimal_or_zero")]
212    pub cum_realised_pnl: Decimal,
213    /// Auto-deleverage rank indicator. What is Auto-Deleveraging?
214    pub adl_rank_indicator: AdlRankIndicator,
215    /// Timestamp of the first time a position was created on this symbol (ms)
216    #[serde(deserialize_with = "timestamp_or_zero")]
217    pub created_time: Timestamp,
218    /// Position updated timestamp (ms)
219    #[serde(deserialize_with = "timestamp_or_zero")]
220    pub updated_time: Timestamp,
221    /// Cross sequence, used to associate each fill and each position update
222    /// Different symbols may have the same seq, please use seq + symbol to check unique
223    /// Returns "-1" if the symbol has never been traded
224    /// Returns the seq updated by the last transaction when there are settings like leverage, risk limit
225    pub seq: i64,
226    /// Useful when Bybit lower the risk limit
227    /// true: Only allowed to reduce the position. You can consider a series of measures, e.g., lower the risk limit, decrease leverage or reduce the position, add margin, or cancel orders, after these operations, you can call confirm new risk limit endpoint to check if your position can be removed the reduceOnly mark
228    /// false: There is no restriction, and it means your position is under the risk when the risk limit is systematically adjusted
229    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
230    pub is_reduce_only: bool,
231    /// Useful when Bybit lower the risk limit
232    /// When isReduceOnly=true: the timestamp (ms) when the MMR will be forcibly adjusted by the system
233    /// When isReduceOnly=false: the timestamp when the MMR had been adjusted by system
234    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
235    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
236    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
237    #[serde(deserialize_with = "option_number")]
238    pub mmr_sys_updated_time: Option<Timestamp>,
239    /// Useful when Bybit lower the risk limit
240    /// When isReduceOnly=true: the timestamp (ms) when the leverage will be forcibly adjusted by the system
241    /// When isReduceOnly=false: the timestamp when the leverage had been adjusted by system
242    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
243    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
244    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
245    #[serde(deserialize_with = "option_number")]
246    pub leverage_sys_updated_time: Option<Timestamp>,
247}
248
249impl Position {
250    /// Replace the position with a `position` stream update (the stream
251    /// always sends the full position).
252    pub fn update(&mut self, msg: PositionMsg) {
253        *self = msg.into();
254    }
255}
256
257impl From<PositionMsg> for Position {
258    fn from(msg: PositionMsg) -> Self {
259        Self {
260            position_idx: msg.position_idx,
261            risk_id: msg.risk_id,
262            risk_limit_value: msg.risk_limit_value,
263            symbol: msg.symbol,
264            side: msg.side,
265            size: msg.size,
266            avg_price: msg.entry_price,
267            position_value: Some(msg.position_value),
268            auto_add_margin: msg.auto_add_margin,
269            position_status: Some(msg.position_status),
270            leverage: msg.leverage,
271            mark_price: msg.mark_price,
272            liq_price: msg.liq_price,
273            position_im: msg.position_im,
274            position_im_by_mp: msg.position_im_by_mp,
275            position_mm: msg.position_mm,
276            position_mm_by_mp: msg.position_mm_by_mp,
277            take_profit: Some(msg.take_profit),
278            stop_loss: Some(msg.stop_loss),
279            trailing_stop: Some(msg.trailing_stop),
280            session_avg_price: msg.session_avg_price,
281            // Not sent by the position stream.
282            break_even_price: None,
283            net_delta_ratio: None,
284            open_time: None,
285            delta: msg.delta,
286            gamma: msg.gamma,
287            vega: msg.vega,
288            theta: msg.theta,
289            unrealised_pnl: Some(msg.unrealised_pnl),
290            cur_realised_pnl: msg.cur_realised_pnl,
291            cum_realised_pnl: msg.cum_realised_pnl,
292            adl_rank_indicator: msg.adl_rank_indicator,
293            created_time: msg.created_time,
294            updated_time: msg.updated_time,
295            seq: msg.seq,
296            is_reduce_only: msg.is_reduce_only,
297            mmr_sys_updated_time: msg.mmr_sys_updated_time,
298            leverage_sys_updated_time: msg.leverage_sys_updated_time,
299        }
300    }
301}
302
303// ── Set Leverage ─────────────────────────────────────────────────────────────
304
305/// Request body of `POST /v5/position/set-leverage` ([`Client::set_leverage`](crate::http::Client::set_leverage)).
306#[derive(Debug, Serialize, Clone)]
307#[serde(rename_all = "camelCase")]
308pub struct SetLeverageRequest {
309    /// linear, inverse
310    pub category: Category,
311    /// Symbol name, like BTCUSDT, uppercase only.
312    pub symbol: String,
313    /// Under cross margin mode: 0 < leverage ≤ maxLeverage (must equal sellLeverage).
314    /// Under isolated margin mode: 0 < leverage ≤ maxLeverage.
315    pub buy_leverage: Decimal,
316    /// \[ 1, max leverage\].
317    pub sell_leverage: Decimal,
318}
319
320impl SetLeverageRequest {
321    /// Create with the required fields; set the optional ones with the `with_*` methods.
322    pub fn new(category: Category, symbol: impl Into<String>, leverage: Decimal) -> Self {
323        let symbol: String = symbol.into();
324        Self {
325            category,
326            symbol,
327            buy_leverage: leverage,
328            sell_leverage: leverage,
329        }
330    }
331
332    /// Different leverage for the buy and sell side (isolated margin only).
333    pub fn with_asymmetric(mut self, buy_leverage: Decimal, sell_leverage: Decimal) -> Self {
334        self.buy_leverage = buy_leverage;
335        self.sell_leverage = sell_leverage;
336        self
337    }
338}
339
340// ── Set Trading Stop ─────────────────────────────────────────────────────────
341
342/// Request body of `POST /v5/position/trading-stop` ([`Client::set_trading_stop`](crate::http::Client::set_trading_stop)).
343#[derive(Debug, Serialize, Clone)]
344#[serde(rename_all = "camelCase")]
345pub struct SetTradingStopRequest {
346    /// linear, inverse
347    pub category: Category,
348    /// Symbol name, like BTCUSDT, uppercase only.
349    pub symbol: String,
350    /// Required. 0: one-way, 1: hedge Buy side, 2: hedge Sell side
351    pub position_idx: PositionIdx,
352    /// Cannot be less than 0, 0 means cancel TP.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub take_profit: Option<Decimal>,
355    /// Cannot be less than 0, 0 means cancel SL.
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub stop_loss: Option<Decimal>,
358    /// Trailing stop distance from market price
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub trailing_stop: Option<Decimal>,
361    /// Take profit trigger price type.
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub tp_trigger_by: Option<TriggerBy>,
364    /// Stop loss trigger price type.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub sl_trigger_by: Option<TriggerBy>,
367    /// Activation price for trailing stop
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub active_price: Option<Decimal>,
370    /// Partial TP quantity
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub tp_size: Option<Decimal>,
373    /// Partial SL quantity
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub sl_size: Option<Decimal>,
376    /// Limit price for TP limit order
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub tp_limit_price: Option<Decimal>,
379    /// Limit price for SL limit order
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub sl_limit_price: Option<Decimal>,
382    /// The order type when take profit is triggered. Market (default), Limit For tpslMode=Full, it only supports tpOrderType="Market".
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub tp_order_type: Option<OrderType>,
385    /// The order type when stop loss is triggered. Market (default), Limit For tpslMode=Full, it only supports slOrderType="Market".
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub sl_order_type: Option<OrderType>,
388    /// TP/SL mode Full: entire position TP/SL, option supports "tpslMode"= Full only Partial: partial position TP/SL.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub tpsl_mode: Option<TpslMode>,
391}
392
393impl SetTradingStopRequest {
394    /// Create with the required fields; set the optional ones with the `with_*` methods.
395    pub fn new(category: Category, symbol: impl Into<String>, position_idx: PositionIdx) -> Self {
396        let symbol: String = symbol.into();
397        Self {
398            category,
399            symbol,
400            position_idx,
401            take_profit: None,
402            stop_loss: None,
403            trailing_stop: None,
404            tp_trigger_by: None,
405            sl_trigger_by: None,
406            active_price: None,
407            tp_size: None,
408            sl_size: None,
409            tp_limit_price: None,
410            sl_limit_price: None,
411            tp_order_type: None,
412            sl_order_type: None,
413            tpsl_mode: None,
414        }
415    }
416
417    /// Set `take_profit`: cannot be less than 0, 0 means cancel TP.
418    pub fn with_take_profit(mut self, v: Decimal) -> Self {
419        self.take_profit = Some(v);
420        self
421    }
422    /// Set `stop_loss`: cannot be less than 0, 0 means cancel SL.
423    pub fn with_stop_loss(mut self, v: Decimal) -> Self {
424        self.stop_loss = Some(v);
425        self
426    }
427    /// Set `trailing_stop`: trailing stop distance from market price.
428    pub fn with_trailing_stop(mut self, v: Decimal) -> Self {
429        self.trailing_stop = Some(v);
430        self
431    }
432    /// Set `tp_trigger_by`: take profit trigger price type.
433    pub fn with_tp_trigger_by(mut self, v: TriggerBy) -> Self {
434        self.tp_trigger_by = Some(v);
435        self
436    }
437    /// Set `sl_trigger_by`: stop loss trigger price type.
438    pub fn with_sl_trigger_by(mut self, v: TriggerBy) -> Self {
439        self.sl_trigger_by = Some(v);
440        self
441    }
442    /// Set `active_price`: activation price for trailing stop.
443    pub fn with_active_price(mut self, v: Decimal) -> Self {
444        self.active_price = Some(v);
445        self
446    }
447    /// Set `tp_size`: partial TP quantity.
448    pub fn with_tp_size(mut self, v: Decimal) -> Self {
449        self.tp_size = Some(v);
450        self
451    }
452    /// Set `sl_size`: partial SL quantity.
453    pub fn with_sl_size(mut self, v: Decimal) -> Self {
454        self.sl_size = Some(v);
455        self
456    }
457    /// Set `tp_limit_price`: limit price for TP limit order.
458    pub fn with_tp_limit_price(mut self, v: Decimal) -> Self {
459        self.tp_limit_price = Some(v);
460        self
461    }
462    /// Set `sl_limit_price`: limit price for SL limit order.
463    pub fn with_sl_limit_price(mut self, v: Decimal) -> Self {
464        self.sl_limit_price = Some(v);
465        self
466    }
467    /// Set `tp_order_type`: the order type when take profit is triggered. Market (default), Limit For tpslMode=Full, it only supports tpOrderType="Market".
468    pub fn with_tp_order_type(mut self, v: OrderType) -> Self {
469        self.tp_order_type = Some(v);
470        self
471    }
472    /// Set `sl_order_type`: the order type when stop loss is triggered. Market (default), Limit For tpslMode=Full, it only supports slOrderType="Market".
473    pub fn with_sl_order_type(mut self, v: OrderType) -> Self {
474        self.sl_order_type = Some(v);
475        self
476    }
477    /// Set `tpsl_mode`: tP/SL mode Full: entire position TP/SL, option supports "tpslMode"= Full only Partial: partial position TP/SL.
478    pub fn with_tpsl_mode(mut self, v: TpslMode) -> Self {
479        self.tpsl_mode = Some(v);
480        self
481    }
482}
483
484// ── Switch Cross / Isolated Margin ───────────────────────────────────────────
485
486/// Request body of `POST /v5/position/switch-isolated` ([`Client::switch_cross_isolated_margin`](crate::http::Client::switch_cross_isolated_margin)).
487#[derive(Debug, Serialize, Clone)]
488#[serde(rename_all = "camelCase")]
489pub struct SwitchCrossIsolatedMarginRequest {
490    /// linear, inverse
491    pub category: Category,
492    /// Symbol name.
493    pub symbol: String,
494    /// 0: cross margin, 1: isolated margin
495    pub trade_mode: TradeMode,
496    /// \[ 1, max leverage\] one-way mode: buyLeverage must be the same as sellLeverage Hedge mode: isolated margin: buyLeverage and sellLeverage can be different; cross margin: buyLeverage must be the same as sellLeverage.
497    pub buy_leverage: Decimal,
498    /// \[ 1, max leverage\].
499    pub sell_leverage: Decimal,
500}
501
502impl SwitchCrossIsolatedMarginRequest {
503    /// Switch to cross margin with the given leverage.
504    pub fn cross(category: Category, symbol: impl Into<String>, leverage: Decimal) -> Self {
505        Self {
506            category,
507            symbol: symbol.into(),
508            trade_mode: TradeMode::CrossMargin,
509            buy_leverage: leverage,
510            sell_leverage: leverage,
511        }
512    }
513
514    /// Switch to isolated margin with the given buy and sell leverage.
515    pub fn isolated(
516        category: Category,
517        symbol: impl Into<String>,
518        buy_leverage: Decimal,
519        sell_leverage: Decimal,
520    ) -> Self {
521        Self {
522            category,
523            symbol: symbol.into(),
524            trade_mode: TradeMode::IsolatedMargin,
525            buy_leverage,
526            sell_leverage,
527        }
528    }
529}
530
531// ── Switch Position Mode ─────────────────────────────────────────────────────
532
533/// Request body of `POST /v5/position/switch-mode` ([`Client::switch_position_mode`](crate::http::Client::switch_position_mode)).
534#[derive(Debug, Serialize, Clone)]
535#[serde(rename_all = "camelCase")]
536pub struct SwitchPositionModeRequest {
537    /// linear, inverse
538    pub category: Category,
539    /// Symbol name. Either symbol or coin is required
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub symbol: Option<String>,
542    /// Coin. Either symbol or coin is required
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub coin: Option<String>,
545    /// 0: Merged Single (one-way), 3: Both Sides (hedge)
546    pub mode: PositionMode,
547}
548
549impl SwitchPositionModeRequest {
550    /// Switch to one-way mode.
551    pub fn one_way(category: Category, symbol: impl Into<String>) -> Self {
552        Self {
553            category,
554            symbol: Some(symbol.into()),
555            coin: None,
556            mode: PositionMode::OneWay,
557        }
558    }
559
560    /// Switch to hedge mode (separate buy and sell positions).
561    pub fn hedge(category: Category, symbol: impl Into<String>) -> Self {
562        Self {
563            category,
564            symbol: Some(symbol.into()),
565            coin: None,
566            mode: PositionMode::Hedge,
567        }
568    }
569
570    /// Set `coin`: coin. Either symbol or coin is required.
571    pub fn with_coin(mut self, v: impl Into<String>) -> Self {
572        self.symbol = None;
573        self.coin = Some(v.into());
574        self
575    }
576}
577
578// ── Set Auto Add Margin ──────────────────────────────────────────────────────
579
580/// Request body of `POST /v5/position/set-auto-add-margin` ([`Client::set_auto_add_margin`](crate::http::Client::set_auto_add_margin)).
581#[derive(Debug, Serialize, Clone)]
582#[serde(rename_all = "camelCase")]
583pub struct SetAutoAddMarginRequest {
584    /// linear, inverse
585    pub category: Category,
586    /// Symbol name, like BTCUSDT, uppercase only.
587    pub symbol: String,
588    /// 0: false, 1: true
589    pub auto_add_margin: i32,
590    /// Used to identify positions in different position modes. For hedge mode position, this param is required 0: one-way mode 1: hedge-mode Buy side 2: hedge-mode Sell side.
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub position_idx: Option<PositionIdx>,
593}
594
595impl SetAutoAddMarginRequest {
596    /// Create with the required fields; set the optional ones with the `with_*` methods.
597    pub fn new(category: Category, symbol: impl Into<String>, enabled: bool) -> Self {
598        let symbol: String = symbol.into();
599        Self {
600            category,
601            symbol,
602            auto_add_margin: if enabled { 1 } else { 0 },
603            position_idx: None,
604        }
605    }
606
607    /// Set `position_idx`: used to identify positions in different position modes. For hedge mode position, this param is required 0: one-way mode 1: hedge-mode Buy side 2: hedge-mode Sell side.
608    pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
609        self.position_idx = Some(v);
610        self
611    }
612}
613
614// ── Set Risk Limit ───────────────────────────────────────────────────────────
615
616/// Request body of `POST /v5/position/set-risk-limit` ([`Client::set_risk_limit`](crate::http::Client::set_risk_limit)).
617#[derive(Debug, Serialize, Clone)]
618#[serde(rename_all = "camelCase")]
619pub struct SetRiskLimitRequest {
620    /// linear, inverse
621    pub category: Category,
622    /// Symbol name.
623    pub symbol: String,
624    /// Risk limit ID.
625    pub risk_id: i64,
626    /// Position index. Used to identify positions in different position modes.
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub position_idx: Option<PositionIdx>,
629}
630
631impl SetRiskLimitRequest {
632    /// Create with the required fields; set the optional ones with the `with_*` methods.
633    pub fn new(category: Category, symbol: impl Into<String>, risk_id: i64) -> Self {
634        let symbol: String = symbol.into();
635        Self {
636            category,
637            symbol,
638            risk_id,
639            position_idx: None,
640        }
641    }
642
643    /// Set `position_idx`: position index. Used to identify positions in different position modes.
644    pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
645        self.position_idx = Some(v);
646        self
647    }
648}
649
650/// Result of `POST /v5/position/set-risk-limit` ([`Client::set_risk_limit`](crate::http::Client::set_risk_limit)).
651#[derive(Debug, Deserialize, PartialEq)]
652#[serde(rename_all = "camelCase")]
653pub struct SetRiskLimitResponse {
654    /// Risk limit ID.
655    pub risk_id: i64,
656    /// Risk limit value.
657    pub risk_limit_value: String,
658    /// Product type.
659    pub category: String,
660    /// Message, if any.
661    #[serde(default, deserialize_with = "empty_string_as_none")]
662    pub message: Option<String>,
663}
664
665// ── Closed P&L ───────────────────────────────────────────────────────────────
666
667/// Query parameters of `GET /v5/position/closed-pnl` ([`Client::get_closed_pnl`](crate::http::Client::get_closed_pnl)).
668#[derive(Clone, Debug, Serialize)]
669#[serde(rename_all = "camelCase")]
670pub struct GetClosedPnlParams {
671    /// linear, inverse
672    pub category: Category,
673    /// Required for inverse; optional for linear
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub symbol: Option<String>,
676    /// The start timestamp (ms) startTime and endTime are not passed, return 7 days by default Only startTime is passed, return range between startTime and startTime+7 days Only endTime is passed, return range between endTime-7 days and endTime ...
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub start_time: Option<Timestamp>,
679    /// The end timestamp (ms).
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub end_time: Option<Timestamp>,
682    /// [1, 100]. Default: 50
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub limit: Option<i32>,
685    /// Cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set.
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub cursor: Option<String>,
688}
689
690impl GetClosedPnlParams {
691    /// Create with the required fields; set the optional ones with the `with_*` methods.
692    pub fn new(category: Category) -> Self {
693        Self {
694            category,
695            symbol: None,
696            start_time: None,
697            end_time: None,
698            limit: None,
699            cursor: None,
700        }
701    }
702
703    /// Set `symbol`: required for inverse; optional for linear.
704    pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
705        self.symbol = Some(v.into());
706        self
707    }
708    /// Set `start_time`: the start timestamp (ms) startTime and endTime are not passed, return 7 days by default Only startTime is passed, return range between startTime and startTime+7 days Only endTime is passed, return range between endTime-7 days and endTime ...
709    pub fn with_start_time(mut self, v: Timestamp) -> Self {
710        self.start_time = Some(v);
711        self
712    }
713    /// Set `end_time`: the end timestamp (ms).
714    pub fn with_end_time(mut self, v: Timestamp) -> Self {
715        self.end_time = Some(v);
716        self
717    }
718    /// Set `limit`: [1, 100]. Default: 50.
719    pub fn with_limit(mut self, v: i32) -> Self {
720        self.limit = Some(v);
721        self
722    }
723    /// Set `cursor`: cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set.
724    pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
725        self.cursor = Some(v.into());
726        self
727    }
728}
729
730/// Item of the list returned by `GET /v5/position/closed-pnl` ([`Client::get_closed_pnl`](crate::http::Client::get_closed_pnl)).
731#[derive(Debug, Deserialize, PartialEq)]
732#[serde(rename_all = "camelCase")]
733pub struct ClosedPnl {
734    /// Symbol name.
735    pub symbol: String,
736    /// Order ID.
737    pub order_id: String,
738    /// User customised order ID.
739    #[serde(default, deserialize_with = "empty_string_as_none")]
740    pub order_link_id: Option<String>,
741    /// Buy, Sell.
742    pub side: Side,
743    /// Order qty.
744    #[serde(deserialize_with = "number")]
745    pub qty: Decimal,
746    /// Order price.
747    #[serde(deserialize_with = "number")]
748    pub order_price: Decimal,
749    /// Order type. Market, Limit.
750    pub order_type: OrderType,
751    /// Exec type Trade, BustTrade SessionSettlePnL Settle, MovePosition ForwardSplitSettle, ReverseSplitSettle.
752    pub exec_type: ExecType,
753    /// Closed size.
754    #[serde(deserialize_with = "number")]
755    pub closed_size: Decimal,
756    /// Cumulated Position value.
757    pub cum_entry_value: Decimal,
758    /// Average entry price.
759    pub avg_entry_price: Decimal,
760    /// Cumulated exit position value.
761    pub cum_exit_value: Decimal,
762    /// Average exit price.
763    pub avg_exit_price: Decimal,
764    /// Closed PnL.
765    pub closed_pnl: Decimal,
766    /// The number of fills in a single order.
767    #[serde(deserialize_with = "number")]
768    pub fill_count: i64,
769    /// Leverage.
770    pub leverage: Decimal,
771    /// The created time (ms).
772    #[serde(deserialize_with = "number")]
773    pub created_time: Timestamp,
774    /// The updated time (ms).
775    #[serde(deserialize_with = "number")]
776    pub updated_time: Timestamp,
777}
778
779// ── Execution List ────────────────────────────────────────────────────────────
780
781/// Query parameters of `GET /v5/execution/list` ([`Client::get_execution_list`](crate::http::Client::get_execution_list)).
782#[derive(Clone, Debug, Serialize)]
783#[serde(rename_all = "camelCase")]
784pub struct GetExecutionListParams {
785    /// Product type linear, inverse, spot, option.
786    pub category: Category,
787    /// Symbol name, like BTCUSDT, uppercase only.
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub symbol: Option<String>,
790    /// Order ID.
791    #[serde(skip_serializing_if = "Option::is_none")]
792    pub order_id: Option<String>,
793    /// User customised order ID.
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub order_link_id: Option<String>,
796    /// Base coin, uppercase only. For type option, default value is BTC.
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub base_coin: Option<String>,
799    /// The start timestamp (ms) startTime and endTime are not passed, return 7 days by default Only startTime is passed, return range between startTime and startTime+7 days Only endTime is passed, return range between endTime-7 days and endTime ...
800    #[serde(skip_serializing_if = "Option::is_none")]
801    pub start_time: Option<Timestamp>,
802    /// The end timestamp (ms).
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub end_time: Option<Timestamp>,
805    /// Execution type.
806    #[serde(skip_serializing_if = "Option::is_none")]
807    pub exec_type: Option<ExecType>,
808    /// [1, 100]. Default: 50
809    #[serde(skip_serializing_if = "Option::is_none")]
810    pub limit: Option<i32>,
811    /// Cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set.
812    #[serde(skip_serializing_if = "Option::is_none")]
813    pub cursor: Option<String>,
814}
815
816impl GetExecutionListParams {
817    /// Create with the required fields; set the optional ones with the `with_*` methods.
818    pub fn new(category: Category) -> Self {
819        Self {
820            category,
821            symbol: None,
822            order_id: None,
823            order_link_id: None,
824            base_coin: None,
825            start_time: None,
826            end_time: None,
827            exec_type: None,
828            limit: None,
829            cursor: None,
830        }
831    }
832
833    /// Set `symbol`: symbol name, like BTCUSDT, uppercase only.
834    pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
835        self.symbol = Some(v.into());
836        self
837    }
838    /// Set `order_id`: order ID.
839    pub fn with_order_id(mut self, v: impl Into<String>) -> Self {
840        self.order_id = Some(v.into());
841        self
842    }
843    /// Set `order_link_id`: user customised order ID.
844    pub fn with_order_link_id(mut self, v: impl Into<String>) -> Self {
845        self.order_link_id = Some(v.into());
846        self
847    }
848    /// Set `base_coin`: base coin, uppercase only. For type option, default value is BTC.
849    pub fn with_base_coin(mut self, v: impl Into<String>) -> Self {
850        self.base_coin = Some(v.into());
851        self
852    }
853    /// Set `start_time`: the start timestamp (ms) startTime and endTime are not passed, return 7 days by default Only startTime is passed, return range between startTime and startTime+7 days Only endTime is passed, return range between endTime-7 days and endTime ...
854    pub fn with_start_time(mut self, v: Timestamp) -> Self {
855        self.start_time = Some(v);
856        self
857    }
858    /// Set `end_time`: the end timestamp (ms).
859    pub fn with_end_time(mut self, v: Timestamp) -> Self {
860        self.end_time = Some(v);
861        self
862    }
863    /// Set `exec_type`: execution type.
864    pub fn with_exec_type(mut self, v: ExecType) -> Self {
865        self.exec_type = Some(v);
866        self
867    }
868    /// Set `limit`: [1, 100]. Default: 50.
869    pub fn with_limit(mut self, v: i32) -> Self {
870        self.limit = Some(v);
871        self
872    }
873    /// Set `cursor`: cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set.
874    pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
875        self.cursor = Some(v.into());
876        self
877    }
878}
879
880/// Item of the list returned by `GET /v5/execution/list` ([`Client::get_execution_list`](crate::http::Client::get_execution_list)).
881#[derive(Debug, Deserialize, PartialEq)]
882#[serde(rename_all = "camelCase")]
883pub struct ExecutionEntry {
884    /// Symbol name.
885    pub symbol: String,
886    /// Order ID.
887    pub order_id: String,
888    /// User customized order ID.
889    #[serde(default, deserialize_with = "empty_string_as_none")]
890    pub order_link_id: Option<String>,
891    /// Side. Buy, Sell.
892    pub side: Side,
893    /// Order price.
894    pub order_price: Decimal,
895    /// Order qty.
896    pub order_qty: Decimal,
897    /// Order type. Market, Limit.
898    pub order_type: OrderType,
899    /// Stop order type. If the order is not stop order, it either returns UNKNOWN or "".
900    #[serde(default, deserialize_with = "empty_string_as_none")]
901    pub stop_order_type: Option<StopOrderType>,
902    /// Executed trading fee. You can get spot fee currency instruction here.
903    pub exec_fee: Decimal,
904    /// Execution ID.
905    pub exec_id: String,
906    /// Execution price.
907    pub exec_price: Decimal,
908    /// Execution qty.
909    pub exec_qty: Decimal,
910    /// Executed type.
911    pub exec_type: ExecType,
912    /// Executed order value.
913    pub exec_value: Decimal,
914    /// Executed timestamp (ms).
915    #[serde(deserialize_with = "number")]
916    pub exec_time: Timestamp,
917    /// Trading fee rate.
918    pub fee_rate: Decimal,
919    /// Implied volatility. Valid for option.
920    #[serde(default, deserialize_with = "option_decimal")]
921    pub trade_iv: Option<Decimal>,
922    /// Implied volatility of mark price. Valid for option.
923    #[serde(default, deserialize_with = "option_decimal")]
924    pub mark_iv: Option<Decimal>,
925    /// The mark price of the symbol when executing.
926    pub mark_price: Decimal,
927    /// The index price of the symbol when executing. Valid for option only.
928    #[serde(default, deserialize_with = "option_decimal")]
929    pub index_price: Option<Decimal>,
930    /// The underlying price of the symbol when executing. Valid for option.
931    #[serde(default, deserialize_with = "option_decimal")]
932    pub underlying_price: Option<Decimal>,
933    /// Paradigm block trade ID.
934    #[serde(default, deserialize_with = "empty_string_as_none")]
935    pub block_trade_id: Option<String>,
936    /// Closed position size.
937    pub closed_size: Decimal,
938    /// Cross sequence, used to associate each fill and each position update The seq will be the same when conclude multiple transactions at the same time Different symbols may have the same seq, please use seq + symbol to check unique.
939    pub seq: i64,
940    /// Is maker order. true: maker, false: taker.
941    pub is_maker: bool,
942}
943
944#[cfg(test)]
945mod tests {
946    use rust_decimal::dec;
947
948    use super::*;
949
950    #[test]
951    fn mode_constructors_accept_a_str_symbol() {
952        let req = SwitchCrossIsolatedMarginRequest::cross(Category::Linear, "BTCUSDT", dec!(10));
953        assert_eq!(
954            serde_json::to_value(&req).unwrap(),
955            serde_json::json!({
956                "category": "linear",
957                "symbol": "BTCUSDT",
958                "tradeMode": 0,
959                "buyLeverage": "10",
960                "sellLeverage": "10",
961            })
962        );
963        let req = SwitchCrossIsolatedMarginRequest::isolated(
964            Category::Linear,
965            "ETHUSDT",
966            dec!(5),
967            dec!(3),
968        );
969        assert_eq!(
970            (req.symbol.as_str(), req.trade_mode),
971            ("ETHUSDT", TradeMode::IsolatedMargin)
972        );
973
974        let req = SwitchPositionModeRequest::hedge(Category::Linear, "BTCUSDT");
975        assert_eq!(
976            serde_json::to_value(&req).unwrap(),
977            serde_json::json!({ "category": "linear", "symbol": "BTCUSDT", "mode": 3 })
978        );
979        let req = SwitchPositionModeRequest::one_way(Category::Inverse, String::from("BTCUSD"));
980        assert_eq!(req.symbol.as_deref(), Some("BTCUSD"));
981        assert_eq!(req.mode, PositionMode::OneWay);
982    }
983}