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}