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