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::{empty_string_as_none, int_to_bool},
13    ws::PositionMsg,
14};
15
16use super::account::WalletCoin;
17
18#[derive(Clone, Debug, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct GetPositionInfoParams {
21    /// Product type
22    /// UTA2.0, UTA1.0: linear, inverse, option
23    /// Classic account: linear, inverse
24    pub category: Category,
25    /// Symbol name, like BTCUSDT, uppercase only
26    /// If symbol passed, it returns data regardless of having position or not.
27    /// If symbol=null and settleCoin specified, it returns position size greater than zero.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub symbol: Option<String>,
30    /// Base coin, uppercase only. option only. Return all option positions if not passed
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub base_coin: Option<String>,
33    /// Settle coin
34    /// linear: either symbol or settleCoin is required. symbol has a higher priority
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub settle_coin: Option<String>,
37    /// Limit for data size per page. [1, 200]. Default: 20
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub limit: Option<u64>,
40    /// Cursor. Use the nextPageCursor token from the response to retrieve the next page of the result set
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub cursor: Option<String>,
43}
44
45impl GetPositionInfoParams {
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    pub fn with_symbol(mut self, v: String) -> Self {
58        self.symbol = Some(v);
59        self
60    }
61    pub fn with_base_coin(mut self, v: String) -> Self {
62        self.base_coin = Some(v);
63        self
64    }
65    pub fn with_settle_coin(mut self, v: String) -> Self {
66        self.settle_coin = Some(v);
67        self
68    }
69    pub fn with_limit(mut self, v: u64) -> Self {
70        self.limit = Some(v);
71        self
72    }
73    pub fn with_cursor(mut self, v: String) -> Self {
74        self.cursor = Some(v);
75        self
76    }
77}
78
79// TODO: check fields
80#[derive(Debug, Deserialize, PartialEq)]
81#[serde(rename_all = "camelCase")]
82pub struct Position {
83    /// Position idx, used to identify positions in different position modes
84    /// 0: One-Way Mode
85    /// 1: Buy side of both side mode
86    /// 2: Sell side of both side mode
87    pub position_idx: PositionIdx,
88    /// Risk tier ID
89    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
90    pub risk_id: i64,
91    /// Risk limit value
92    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
93    #[serde(default, deserialize_with = "option_decimal")]
94    pub risk_limit_value: Option<Decimal>,
95    /// Symbol name
96    pub symbol: String,
97    /// Position side. Buy: long, Sell: short
98    /// one-way mode: classic & UTA1.0(inverse), an empty position returns None.
99    /// UTA2.0(linear, inverse) & UTA1.0(linear): either one-way or hedge mode returns an empty string "" for an empty position.
100    #[serde(default, deserialize_with = "empty_string_as_none")]
101    pub side: Option<Side>,
102    /// Position size, always positive
103    pub size: Decimal,
104    /// Average entry price
105    /// For USDC Perp & Futures, it indicates average entry price, and it will not be changed with 8-hour session settlement
106    pub avg_price: Decimal,
107    /// Position value
108    #[serde(default, deserialize_with = "option_decimal")]
109    pub position_value: Option<Decimal>,
110    /// Whether to add margin automatically when using isolated margin mode
111    /// 0: false
112    /// 1: true
113    #[serde(deserialize_with = "int_to_bool")]
114    pub auto_add_margin: bool,
115    /// Position status. Normal, Liq, Adl
116    pub position_status: PositionStatus,
117    /// Position leverage
118    /// for portfolio margin mode, this field returns "", which means leverage rules are invalid
119    pub leverage: Decimal,
120    /// Mark price
121    pub mark_price: Decimal,
122    /// Position liquidation price
123    /// UTA2.0(isolated margin), UTA1.0(isolated margin), UTA1.0(inverse), Classic account:
124    /// it is the real price for isolated and cross positions, and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
125    /// UTA2.0(Cross margin), UTA1.0(Cross margin):
126    /// 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
127    /// this field is empty for Portfolio Margin Mode, and no liquidation price will be provided
128    #[serde(default, deserialize_with = "option_decimal")]
129    pub liq_price: Option<Decimal>,
130    /// Initial margin
131    /// Classic & UTA1.0(inverse): ignore this field
132    /// UTA portfolio margin mode, it returns ""
133    #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
134    pub position_im: Option<Decimal>,
135    /// Initial margin calculated by mark price
136    /// Classic & UTA1.0(inverse) : ignore this field
137    /// UTA portfolio margin mode, it returns ""
138    #[serde(
139        rename = "positionIMByMp",
140        default,
141        deserialize_with = "option_decimal"
142    )]
143    pub position_im_by_mp: Option<Decimal>,
144    /// Maintenance margin
145    /// Classic & UTA1.0(inverse): ignore this field
146    /// UTA portfolio margin mode, it returns ""
147    #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
148    pub position_mm: Option<Decimal>,
149    /// Maintenance margin calculated by mark price
150    /// Classic & UTA1.0(inverse) : ignore this field
151    /// UTA portfolio margin mode, it returns ""
152    #[serde(
153        rename = "positionMMByMp",
154        default,
155        deserialize_with = "option_decimal"
156    )]
157    pub position_mm_by_mp: Option<Decimal>,
158    /// Take profit price
159    #[serde(default, deserialize_with = "option_decimal")]
160    pub take_profit: Option<Decimal>,
161    /// Stop loss price
162    #[serde(default, deserialize_with = "option_decimal")]
163    pub stop_loss: Option<Decimal>,
164    /// Trailing stop (The distance from market price)
165    #[serde(default, deserialize_with = "option_decimal")]
166    pub trailing_stop: Option<Decimal>,
167    /// USDC contract session avg price, it is the same figure as avg entry price shown in the web UI
168    #[serde(default, deserialize_with = "option_decimal")]
169    pub session_avg_price: Option<Decimal>,
170    /// Delta
171    #[serde(default, deserialize_with = "empty_string_as_none")]
172    pub delta: Option<String>,
173    /// Gamma
174    #[serde(default, deserialize_with = "empty_string_as_none")]
175    pub gamma: Option<String>,
176    /// Vega
177    #[serde(default, deserialize_with = "empty_string_as_none")]
178    pub vega: Option<String>,
179    /// Theta
180    #[serde(default, deserialize_with = "empty_string_as_none")]
181    pub theta: Option<String>,
182    /// Unrealised PnL
183    #[serde(default, deserialize_with = "option_decimal")]
184    pub unrealised_pnl: Option<Decimal>,
185    /// The realised PnL for the current holding position
186    pub cur_realised_pnl: Decimal,
187    /// Cumulative realised pnl
188    /// Futures & Perpetuals: it is the all time cumulative realised P&L
189    /// Option: always "", meaningless
190    pub cum_realised_pnl: Decimal,
191    /// Auto-deleverage rank indicator. What is Auto-Deleveraging?
192    pub adl_rank_indicator: AdlRankIndicator,
193    /// Timestamp of the first time a position was created on this symbol (ms)
194    #[serde(deserialize_with = "number")]
195    pub created_time: Timestamp,
196    /// Position updated timestamp (ms)
197    #[serde(deserialize_with = "number")]
198    pub updated_time: Timestamp,
199    /// Cross sequence, used to associate each fill and each position update
200    /// Different symbols may have the same seq, please use seq + symbol to check unique
201    /// Returns "-1" if the symbol has never been traded
202    /// Returns the seq updated by the last transaction when there are settings like leverage, risk limit
203    pub seq: i64,
204    /// Useful when Bybit lower the risk limit
205    /// 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
206    /// false: There is no restriction, and it means your position is under the risk when the risk limit is systematically adjusted
207    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
208    pub is_reduce_only: bool,
209    /// Useful when Bybit lower the risk limit
210    /// When isReduceOnly=true: the timestamp (ms) when the MMR will be forcibly adjusted by the system
211    /// When isReduceOnly=false: the timestamp when the MMR had been adjusted by system
212    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
213    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
214    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
215    #[serde(deserialize_with = "option_number")]
216    pub mmr_sys_updated_time: Option<Timestamp>,
217    /// Useful when Bybit lower the risk limit
218    /// When isReduceOnly=true: the timestamp (ms) when the leverage will be forcibly adjusted by the system
219    /// When isReduceOnly=false: the timestamp when the leverage had been adjusted by system
220    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
221    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
222    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
223    #[serde(deserialize_with = "option_number")]
224    pub leverage_sys_updated_time: Option<Timestamp>,
225}
226
227impl Position {
228    pub fn update(&mut self, msg: PositionMsg) {
229        self.position_idx = msg.position_idx;
230        self.risk_id = msg.risk_id;
231        self.risk_limit_value = msg.risk_limit_value;
232        self.symbol = msg.symbol;
233        self.side = msg.side;
234        self.size = msg.size;
235        self.avg_price = msg.entry_price;
236        self.position_value = Some(msg.position_value);
237        self.auto_add_margin = msg.auto_add_margin;
238        self.position_status = msg.position_status;
239        self.leverage = msg.leverage;
240        self.mark_price = msg.mark_price;
241        self.liq_price = msg.liq_price;
242        // INFO: self.position_im updated in self.update_with_a_wallet_coin
243        // INFO: self.position_mm updated in self.update_with_a_wallet_coin
244        self.take_profit = Some(msg.take_profit);
245        self.stop_loss = Some(msg.stop_loss);
246        self.trailing_stop = Some(msg.trailing_stop);
247        // self.trailing_stop = msg.trailing_stop;
248        self.session_avg_price = msg.session_avg_price;
249        self.delta = msg.delta;
250        self.gamma = msg.gamma;
251        self.vega = msg.vega;
252        self.theta = msg.theta;
253        // INFO: self.unrealised_pnl updated in self.update_with_a_wallet_coin
254        self.cur_realised_pnl = msg.cur_realised_pnl;
255        self.cum_realised_pnl = msg.cum_realised_pnl;
256        self.adl_rank_indicator = msg.adl_rank_indicator;
257        self.created_time = msg.created_time;
258        self.updated_time = msg.updated_time;
259        self.seq = msg.seq;
260        self.is_reduce_only = msg.is_reduce_only;
261        self.mmr_sys_updated_time = msg.mmr_sys_updated_time;
262        self.leverage_sys_updated_time = msg.leverage_sys_updated_time;
263    }
264
265    pub fn update_with_a_wallet_coin(&mut self, msg: &WalletCoin) {
266        self.position_mm = msg.total_position_im;
267        self.position_im = msg.total_position_mm;
268        self.unrealised_pnl = Some(msg.unrealised_pnl);
269    }
270}
271
272// ── Set Leverage ─────────────────────────────────────────────────────────────
273
274#[derive(Debug, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct SetLeverageRequest {
277    /// linear, inverse
278    pub category: Category,
279    pub symbol: String,
280    /// Under cross margin mode: 0 < leverage ≤ maxLeverage (must equal sellLeverage).
281    /// Under isolated margin mode: 0 < leverage ≤ maxLeverage.
282    pub buy_leverage: Decimal,
283    pub sell_leverage: Decimal,
284}
285
286impl SetLeverageRequest {
287    pub fn new(category: Category, symbol: String, leverage: Decimal) -> Self {
288        Self {
289            category,
290            symbol,
291            buy_leverage: leverage,
292            sell_leverage: leverage,
293        }
294    }
295
296    pub fn with_asymmetric(mut self, buy_leverage: Decimal, sell_leverage: Decimal) -> Self {
297        self.buy_leverage = buy_leverage;
298        self.sell_leverage = sell_leverage;
299        self
300    }
301}
302
303// ── Set Trading Stop ─────────────────────────────────────────────────────────
304
305#[derive(Debug, Serialize)]
306#[serde(rename_all = "camelCase")]
307pub struct SetTradingStopRequest {
308    /// linear, inverse
309    pub category: Category,
310    pub symbol: String,
311    /// Required. 0: one-way, 1: hedge Buy side, 2: hedge Sell side
312    pub position_idx: PositionIdx,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub take_profit: Option<Decimal>,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub stop_loss: Option<Decimal>,
317    /// Trailing stop distance from market price
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub trailing_stop: Option<Decimal>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub tp_trigger_by: Option<TriggerBy>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub sl_trigger_by: Option<TriggerBy>,
324    /// Activation price for trailing stop
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub active_price: Option<Decimal>,
327    /// Partial TP quantity
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub tp_size: Option<Decimal>,
330    /// Partial SL quantity
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub sl_size: Option<Decimal>,
333    /// Limit price for TP limit order
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub tp_limit_price: Option<Decimal>,
336    /// Limit price for SL limit order
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub sl_limit_price: Option<Decimal>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub tp_order_type: Option<OrderType>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub sl_order_type: Option<OrderType>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub tpsl_mode: Option<TpslMode>,
345}
346
347impl SetTradingStopRequest {
348    pub fn new(category: Category, symbol: String, position_idx: PositionIdx) -> Self {
349        Self {
350            category,
351            symbol,
352            position_idx,
353            take_profit: None,
354            stop_loss: None,
355            trailing_stop: None,
356            tp_trigger_by: None,
357            sl_trigger_by: None,
358            active_price: None,
359            tp_size: None,
360            sl_size: None,
361            tp_limit_price: None,
362            sl_limit_price: None,
363            tp_order_type: None,
364            sl_order_type: None,
365            tpsl_mode: None,
366        }
367    }
368
369    pub fn with_take_profit(mut self, v: Decimal) -> Self {
370        self.take_profit = Some(v);
371        self
372    }
373    pub fn with_stop_loss(mut self, v: Decimal) -> Self {
374        self.stop_loss = Some(v);
375        self
376    }
377    pub fn with_trailing_stop(mut self, v: Decimal) -> Self {
378        self.trailing_stop = Some(v);
379        self
380    }
381    pub fn with_tp_trigger_by(mut self, v: TriggerBy) -> Self {
382        self.tp_trigger_by = Some(v);
383        self
384    }
385    pub fn with_sl_trigger_by(mut self, v: TriggerBy) -> Self {
386        self.sl_trigger_by = Some(v);
387        self
388    }
389    pub fn with_active_price(mut self, v: Decimal) -> Self {
390        self.active_price = Some(v);
391        self
392    }
393    pub fn with_tp_size(mut self, v: Decimal) -> Self {
394        self.tp_size = Some(v);
395        self
396    }
397    pub fn with_sl_size(mut self, v: Decimal) -> Self {
398        self.sl_size = Some(v);
399        self
400    }
401    pub fn with_tp_limit_price(mut self, v: Decimal) -> Self {
402        self.tp_limit_price = Some(v);
403        self
404    }
405    pub fn with_sl_limit_price(mut self, v: Decimal) -> Self {
406        self.sl_limit_price = Some(v);
407        self
408    }
409    pub fn with_tp_order_type(mut self, v: OrderType) -> Self {
410        self.tp_order_type = Some(v);
411        self
412    }
413    pub fn with_sl_order_type(mut self, v: OrderType) -> Self {
414        self.sl_order_type = Some(v);
415        self
416    }
417    pub fn with_tpsl_mode(mut self, v: TpslMode) -> Self {
418        self.tpsl_mode = Some(v);
419        self
420    }
421}
422
423// ── Switch Cross / Isolated Margin ───────────────────────────────────────────
424
425#[derive(Debug, Serialize)]
426#[serde(rename_all = "camelCase")]
427pub struct SwitchCrossIsolatedMarginRequest {
428    /// linear, inverse
429    pub category: Category,
430    pub symbol: String,
431    /// 0: cross margin, 1: isolated margin
432    pub trade_mode: TradeMode,
433    pub buy_leverage: Decimal,
434    pub sell_leverage: Decimal,
435}
436
437impl SwitchCrossIsolatedMarginRequest {
438    pub fn cross(category: Category, symbol: String, leverage: Decimal) -> Self {
439        Self {
440            category,
441            symbol,
442            trade_mode: TradeMode::CrossMargin,
443            buy_leverage: leverage,
444            sell_leverage: leverage,
445        }
446    }
447
448    pub fn isolated(
449        category: Category,
450        symbol: String,
451        buy_leverage: Decimal,
452        sell_leverage: Decimal,
453    ) -> Self {
454        Self {
455            category,
456            symbol,
457            trade_mode: TradeMode::IsolatedMargin,
458            buy_leverage,
459            sell_leverage,
460        }
461    }
462}
463
464// ── Switch Position Mode ─────────────────────────────────────────────────────
465
466#[derive(Debug, Serialize)]
467#[serde(rename_all = "camelCase")]
468pub struct SwitchPositionModeRequest {
469    /// linear, inverse
470    pub category: Category,
471    /// Symbol name. Either symbol or coin is required
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub symbol: Option<String>,
474    /// Coin. Either symbol or coin is required
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub coin: Option<String>,
477    /// 0: Merged Single (one-way), 3: Both Sides (hedge)
478    pub mode: PositionMode,
479}
480
481impl SwitchPositionModeRequest {
482    pub fn one_way(category: Category, symbol: String) -> Self {
483        Self {
484            category,
485            symbol: Some(symbol),
486            coin: None,
487            mode: PositionMode::OneWay,
488        }
489    }
490
491    pub fn hedge(category: Category, symbol: String) -> Self {
492        Self {
493            category,
494            symbol: Some(symbol),
495            coin: None,
496            mode: PositionMode::Hedge,
497        }
498    }
499
500    pub fn with_coin(mut self, v: String) -> Self {
501        self.symbol = None;
502        self.coin = Some(v);
503        self
504    }
505}
506
507// ── Set Auto Add Margin ──────────────────────────────────────────────────────
508
509#[derive(Debug, Serialize)]
510#[serde(rename_all = "camelCase")]
511pub struct SetAutoAddMarginRequest {
512    /// linear, inverse
513    pub category: Category,
514    pub symbol: String,
515    /// 0: false, 1: true
516    pub auto_add_margin: i32,
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub position_idx: Option<PositionIdx>,
519}
520
521impl SetAutoAddMarginRequest {
522    pub fn new(category: Category, symbol: String, enabled: bool) -> Self {
523        Self {
524            category,
525            symbol,
526            auto_add_margin: if enabled { 1 } else { 0 },
527            position_idx: None,
528        }
529    }
530
531    pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
532        self.position_idx = Some(v);
533        self
534    }
535}
536
537// ── Set Risk Limit ───────────────────────────────────────────────────────────
538
539#[derive(Debug, Serialize)]
540#[serde(rename_all = "camelCase")]
541pub struct SetRiskLimitRequest {
542    /// linear, inverse
543    pub category: Category,
544    pub symbol: String,
545    pub risk_id: i64,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub position_idx: Option<PositionIdx>,
548}
549
550impl SetRiskLimitRequest {
551    pub fn new(category: Category, symbol: String, risk_id: i64) -> Self {
552        Self {
553            category,
554            symbol,
555            risk_id,
556            position_idx: None,
557        }
558    }
559
560    pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
561        self.position_idx = Some(v);
562        self
563    }
564}
565
566#[derive(Debug, Deserialize, PartialEq)]
567#[serde(rename_all = "camelCase")]
568pub struct SetRiskLimitResponse {
569    pub risk_id: i64,
570    pub risk_limit_value: String,
571    pub category: String,
572    #[serde(default, deserialize_with = "empty_string_as_none")]
573    pub message: Option<String>,
574}
575
576// ── Closed P&L ───────────────────────────────────────────────────────────────
577
578#[derive(Clone, Debug, Serialize)]
579#[serde(rename_all = "camelCase")]
580pub struct GetClosedPnlParams {
581    /// linear, inverse
582    pub category: Category,
583    /// Required for inverse; optional for linear
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub symbol: Option<String>,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub start_time: Option<Timestamp>,
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub end_time: Option<Timestamp>,
590    /// [1, 100]. Default: 50
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub limit: Option<i32>,
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub cursor: Option<String>,
595}
596
597impl GetClosedPnlParams {
598    pub fn new(category: Category) -> Self {
599        Self {
600            category,
601            symbol: None,
602            start_time: None,
603            end_time: None,
604            limit: None,
605            cursor: None,
606        }
607    }
608
609    pub fn with_symbol(mut self, v: String) -> Self {
610        self.symbol = Some(v);
611        self
612    }
613    pub fn with_start_time(mut self, v: Timestamp) -> Self {
614        self.start_time = Some(v);
615        self
616    }
617    pub fn with_end_time(mut self, v: Timestamp) -> Self {
618        self.end_time = Some(v);
619        self
620    }
621    pub fn with_limit(mut self, v: i32) -> Self {
622        self.limit = Some(v);
623        self
624    }
625    pub fn with_cursor(mut self, v: String) -> Self {
626        self.cursor = Some(v);
627        self
628    }
629}
630
631#[derive(Debug, Deserialize, PartialEq)]
632#[serde(rename_all = "camelCase")]
633pub struct ClosedPnl {
634    pub symbol: String,
635    pub order_id: String,
636    #[serde(default, deserialize_with = "empty_string_as_none")]
637    pub order_link_id: Option<String>,
638    pub side: Side,
639    #[serde(deserialize_with = "number")]
640    pub qty: Decimal,
641    #[serde(deserialize_with = "number")]
642    pub order_price: Decimal,
643    pub order_type: OrderType,
644    pub exec_type: ExecType,
645    #[serde(deserialize_with = "number")]
646    pub closed_size: Decimal,
647    pub cum_entry_value: Decimal,
648    pub avg_entry_price: Decimal,
649    pub cum_exit_value: Decimal,
650    pub avg_exit_price: Decimal,
651    pub closed_pnl: Decimal,
652    #[serde(deserialize_with = "number")]
653    pub fill_count: i64,
654    pub leverage: Decimal,
655    #[serde(deserialize_with = "number")]
656    pub created_time: Timestamp,
657    #[serde(deserialize_with = "number")]
658    pub updated_time: Timestamp,
659}
660
661// ── Execution List ────────────────────────────────────────────────────────────
662
663#[derive(Clone, Debug, Serialize)]
664#[serde(rename_all = "camelCase")]
665pub struct GetExecutionListParams {
666    pub category: Category,
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub symbol: Option<String>,
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub order_id: Option<String>,
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub order_link_id: Option<String>,
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub base_coin: Option<String>,
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub start_time: Option<Timestamp>,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub end_time: Option<Timestamp>,
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub exec_type: Option<ExecType>,
681    /// [1, 100]. Default: 50
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub limit: Option<i32>,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub cursor: Option<String>,
686}
687
688impl GetExecutionListParams {
689    pub fn new(category: Category) -> Self {
690        Self {
691            category,
692            symbol: None,
693            order_id: None,
694            order_link_id: None,
695            base_coin: None,
696            start_time: None,
697            end_time: None,
698            exec_type: None,
699            limit: None,
700            cursor: None,
701        }
702    }
703
704    pub fn with_symbol(mut self, v: String) -> Self {
705        self.symbol = Some(v);
706        self
707    }
708    pub fn with_order_id(mut self, v: String) -> Self {
709        self.order_id = Some(v);
710        self
711    }
712    pub fn with_order_link_id(mut self, v: String) -> Self {
713        self.order_link_id = Some(v);
714        self
715    }
716    pub fn with_base_coin(mut self, v: String) -> Self {
717        self.base_coin = Some(v);
718        self
719    }
720    pub fn with_start_time(mut self, v: Timestamp) -> Self {
721        self.start_time = Some(v);
722        self
723    }
724    pub fn with_end_time(mut self, v: Timestamp) -> Self {
725        self.end_time = Some(v);
726        self
727    }
728    pub fn with_exec_type(mut self, v: ExecType) -> Self {
729        self.exec_type = Some(v);
730        self
731    }
732    pub fn with_limit(mut self, v: i32) -> Self {
733        self.limit = Some(v);
734        self
735    }
736    pub fn with_cursor(mut self, v: String) -> Self {
737        self.cursor = Some(v);
738        self
739    }
740}
741
742#[derive(Debug, Deserialize, PartialEq)]
743#[serde(rename_all = "camelCase")]
744pub struct ExecutionEntry {
745    pub symbol: String,
746    pub order_id: String,
747    #[serde(default, deserialize_with = "empty_string_as_none")]
748    pub order_link_id: Option<String>,
749    pub side: Side,
750    pub order_price: Decimal,
751    pub order_qty: Decimal,
752    pub order_type: OrderType,
753    #[serde(default, deserialize_with = "empty_string_as_none")]
754    pub stop_order_type: Option<StopOrderType>,
755    pub exec_fee: Decimal,
756    pub exec_id: String,
757    pub exec_price: Decimal,
758    pub exec_qty: Decimal,
759    pub exec_type: ExecType,
760    pub exec_value: Decimal,
761    #[serde(deserialize_with = "number")]
762    pub exec_time: Timestamp,
763    pub fee_rate: Decimal,
764    #[serde(default, deserialize_with = "option_decimal")]
765    pub trade_iv: Option<Decimal>,
766    #[serde(default, deserialize_with = "option_decimal")]
767    pub mark_iv: Option<Decimal>,
768    pub mark_price: Decimal,
769    #[serde(default, deserialize_with = "option_decimal")]
770    pub index_price: Option<Decimal>,
771    #[serde(default, deserialize_with = "option_decimal")]
772    pub underlying_price: Option<Decimal>,
773    #[serde(default, deserialize_with = "empty_string_as_none")]
774    pub block_trade_id: Option<String>,
775    pub closed_size: Decimal,
776    pub seq: i64,
777    pub is_maker: bool,
778}