bybit_rust_api/rest/position/
position_client.rs

1use crate::rest::client::{RestClient, SecType, ServerResponse};
2use crate::rest::BybitResult as Result;
3use serde_json::json;
4
5#[derive(Clone)]
6pub struct PositionClient {
7    client: RestClient,
8}
9
10impl PositionClient {
11    pub fn new(client: RestClient) -> Self {
12        PositionClient { client }
13    }
14
15    /// Get position info
16    ///
17    /// API: GET /v5/position/list
18    /// https://bybit-exchange.github.io/docs/v5/position
19    pub async fn get_position_info(
20        &self,
21        category: &str,
22        symbol: Option<&str>,
23        base_coin: Option<&str>,
24        settle_coin: Option<&str>,
25        limit: Option<i32>,
26        cursor: Option<&str>,
27    ) -> Result<ServerResponse<serde_json::Value>> {
28        let endpoint = "v5/position/list";
29        let mut params = json!({
30            "category": category,
31        });
32
33        if let Some(symbol) = symbol {
34            params["symbol"] = json!(symbol);
35        }
36        if let Some(base_coin) = base_coin {
37            params["baseCoin"] = json!(base_coin);
38        }
39        if let Some(settle_coin) = settle_coin {
40            params["settleCoin"] = json!(settle_coin);
41        }
42        if let Some(limit) = limit {
43            params["limit"] = json!(limit);
44        }
45        if let Some(cursor) = cursor {
46            params["cursor"] = json!(cursor);
47        }
48
49        let response = self.client.get(endpoint, params, SecType::Signed).await?;
50        Ok(response)
51    }
52
53    /// Set leverage
54    ///
55    /// API: POST /v5/position/set-leverage
56    /// https://bybit-exchange.github.io/docs/v5/position/set-leverage
57    pub async fn set_leverage(
58        &self,
59        category: &str,
60        symbol: &str,
61        buy_leverage: &str,
62        sell_leverage: &str,
63    ) -> Result<ServerResponse<serde_json::Value>> {
64        let endpoint = "v5/position/set-leverage";
65        let body = json!({
66            "category": category,
67            "symbol": symbol,
68            "buyLeverage": buy_leverage,
69            "sellLeverage": sell_leverage,
70        });
71
72        let response = self.client.post(endpoint, body, SecType::Signed).await?;
73        Ok(response)
74    }
75
76    /// Switch between cross/isolated margin
77    ///
78    /// API: POST /v5/position/switch-isolated
79    /// https://bybit-exchange.github.io/docs/v5/position/switch-isolated
80    pub async fn switch_margin_mode(
81        &self,
82        category: &str,
83        symbol: &str,
84        trade_mode: i32, // 0: cross margin, 1: isolated margin
85        buy_leverage: &str,
86        sell_leverage: &str,
87    ) -> Result<ServerResponse<serde_json::Value>> {
88        let endpoint = "v5/position/switch-isolated";
89        let body = json!({
90            "category": category,
91            "symbol": symbol,
92            "tradeMode": trade_mode,
93            "buyLeverage": buy_leverage,
94            "sellLeverage": sell_leverage,
95        });
96
97        let response = self.client.post(endpoint, body, SecType::Signed).await?;
98        Ok(response)
99    }
100
101    /// Switch position mode
102    ///
103    /// API: POST /v5/position/switch-mode
104    /// https://bybit-exchange.github.io/docs/v5/position/switch-mode
105    pub async fn switch_position_mode(
106        &self,
107        category: &str,
108        mode: i32, // 0: Merged Single, 3: Both Sides
109        symbol: Option<&str>,
110        coin: Option<&str>,
111    ) -> Result<ServerResponse<serde_json::Value>> {
112        let endpoint = "v5/position/switch-mode";
113        let mut body = json!({
114            "category": category,
115            "mode": mode,
116        });
117
118        if let Some(symbol) = symbol {
119            body["symbol"] = json!(symbol);
120        }
121        if let Some(coin) = coin {
122            body["coin"] = json!(coin);
123        }
124
125        let response = self.client.post(endpoint, body, SecType::Signed).await?;
126        Ok(response)
127    }
128
129    /// Set Trading Stop (Take profit/Stop loss)
130    ///
131    /// API: POST /v5/position/trading-stop
132    /// https://bybit-exchange.github.io/docs/v5/position/trading-stop
133    pub async fn set_trading_stop(
134        &self,
135        category: &str,
136        symbol: &str,
137        position_idx: i32,
138        take_profit: Option<&str>,
139        stop_loss: Option<&str>,
140        trailing_stop: Option<&str>,
141        tp_trigger_by: Option<&str>,
142        sl_trigger_by: Option<&str>,
143        active_price: Option<&str>,
144        tp_size: Option<&str>,
145        sl_size: Option<&str>,
146        tp_limit_price: Option<&str>,
147        sl_limit_price: Option<&str>,
148        tp_order_type: Option<&str>,
149        sl_order_type: Option<&str>,
150    ) -> Result<ServerResponse<serde_json::Value>> {
151        let endpoint = "v5/position/trading-stop";
152        let mut body = json!({
153            "category": category,
154            "symbol": symbol,
155            "positionIdx": position_idx,
156        });
157
158        if let Some(take_profit) = take_profit {
159            body["takeProfit"] = json!(take_profit);
160        }
161        if let Some(stop_loss) = stop_loss {
162            body["stopLoss"] = json!(stop_loss);
163        }
164        if let Some(trailing_stop) = trailing_stop {
165            body["trailingStop"] = json!(trailing_stop);
166        }
167        if let Some(tp_trigger_by) = tp_trigger_by {
168            body["tpTriggerBy"] = json!(tp_trigger_by);
169        }
170        if let Some(sl_trigger_by) = sl_trigger_by {
171            body["slTriggerBy"] = json!(sl_trigger_by);
172        }
173        if let Some(active_price) = active_price {
174            body["activePrice"] = json!(active_price);
175        }
176        if let Some(tp_size) = tp_size {
177            body["tpSize"] = json!(tp_size);
178        }
179        if let Some(sl_size) = sl_size {
180            body["slSize"] = json!(sl_size);
181        }
182        if let Some(tp_limit_price) = tp_limit_price {
183            body["tpLimitPrice"] = json!(tp_limit_price);
184        }
185        if let Some(sl_limit_price) = sl_limit_price {
186            body["slLimitPrice"] = json!(sl_limit_price);
187        }
188        if let Some(tp_order_type) = tp_order_type {
189            body["tpOrderType"] = json!(tp_order_type);
190        }
191        if let Some(sl_order_type) = sl_order_type {
192            body["slOrderType"] = json!(sl_order_type);
193        }
194
195        let response = self.client.post(endpoint, body, SecType::Signed).await?;
196        Ok(response)
197    }
198
199    /// Set auto add margin
200    ///
201    /// API: POST /v5/position/set-auto-add-margin
202    /// https://bybit-exchange.github.io/docs/v5/position/set-auto-add-margin
203    pub async fn set_auto_add_margin(
204        &self,
205        category: &str,
206        symbol: &str,
207        auto_add_margin: i32, // 0: off, 1: on
208        position_idx: Option<i32>,
209    ) -> Result<ServerResponse<serde_json::Value>> {
210        let endpoint = "v5/position/set-auto-add-margin";
211        let mut body = json!({
212            "category": category,
213            "symbol": symbol,
214            "autoAddMargin": auto_add_margin,
215        });
216
217        if let Some(position_idx) = position_idx {
218            body["positionIdx"] = json!(position_idx);
219        }
220
221        let response = self.client.post(endpoint, body, SecType::Signed).await?;
222        Ok(response)
223    }
224
225    /// Get closed PnL
226    ///
227    /// API: GET /v5/position/closed-pnl
228    /// https://bybit-exchange.github.io/docs/v5/position/closed-pnl
229    pub async fn get_closed_pnl(
230        &self,
231        category: &str,
232        symbol: Option<&str>,
233        start_time: Option<i64>,
234        end_time: Option<i64>,
235        limit: Option<i32>,
236        cursor: Option<&str>,
237    ) -> Result<ServerResponse<serde_json::Value>> {
238        let endpoint = "v5/position/closed-pnl";
239        let mut params = json!({
240            "category": category,
241        });
242
243        if let Some(symbol) = symbol {
244            params["symbol"] = json!(symbol);
245        }
246        if let Some(start_time) = start_time {
247            params["startTime"] = json!(start_time);
248        }
249        if let Some(end_time) = end_time {
250            params["endTime"] = json!(end_time);
251        }
252        if let Some(limit) = limit {
253            params["limit"] = json!(limit);
254        }
255        if let Some(cursor) = cursor {
256            params["cursor"] = json!(cursor);
257        }
258
259        let response = self.client.get(endpoint, params, SecType::Signed).await?;
260        Ok(response)
261    }
262
263    /// Set TP/SL mode
264    ///
265    /// API: POST /v5/position/set-tpsl-mode
266    /// https://bybit-exchange.github.io/docs/v5/position/set-tpsl-mode
267    pub async fn set_tpsl_mode(
268        &self,
269        category: &str,
270        symbol: &str,
271        tp_sl_mode: &str, // Full: entire position TP/SL, Partial: partial position TP/SL
272    ) -> Result<ServerResponse<serde_json::Value>> {
273        let endpoint = "v5/position/set-tpsl-mode";
274        let body = json!({
275            "category": category,
276            "symbol": symbol,
277            "tpSlMode": tp_sl_mode,
278        });
279
280        let response = self.client.post(endpoint, body, SecType::Signed).await?;
281        Ok(response)
282    }
283
284    /// Set risk limit
285    ///
286    /// API: POST /v5/position/set-risk-limit
287    /// https://bybit-exchange.github.io/docs/v5/position/set-risk-limit
288    pub async fn set_risk_limit(
289        &self,
290        category: &str,
291        symbol: &str,
292        risk_id: i32,
293        position_idx: Option<i32>,
294    ) -> Result<ServerResponse<serde_json::Value>> {
295        let endpoint = "v5/position/set-risk-limit";
296        let mut body = json!({
297            "category": category,
298            "symbol": symbol,
299            "riskId": risk_id,
300        });
301
302        if let Some(position_idx) = position_idx {
303            body["positionIdx"] = json!(position_idx);
304        }
305
306        let response = self.client.post(endpoint, body, SecType::Signed).await?;
307        Ok(response)
308    }
309
310    /// Move positions
311    ///
312    /// API: POST /v5/position/move-positions
313    /// https://bybit-exchange.github.io/docs/v5/position/move-positions
314    pub async fn move_positions(
315        &self,
316        from_uid: &str,
317        to_uid: &str,
318        list: Vec<serde_json::Value>,
319    ) -> Result<ServerResponse<serde_json::Value>> {
320        let endpoint = "v5/position/move-positions";
321        let body = json!({
322            "fromUid": from_uid,
323            "toUid": to_uid,
324            "list": list,
325        });
326
327        let response = self.client.post(endpoint, body, SecType::Signed).await?;
328        Ok(response)
329    }
330
331    /// Get move position history
332    ///
333    /// API: GET /v5/position/move-history
334    /// https://bybit-exchange.github.io/docs/v5/position/move-history
335    pub async fn get_move_position_history(
336        &self,
337        category: Option<&str>,
338        symbol: Option<&str>,
339        start_time: Option<i64>,
340        end_time: Option<i64>,
341        status: Option<&str>,
342        block_trade_id: Option<&str>,
343        limit: Option<i32>,
344        cursor: Option<&str>,
345    ) -> Result<ServerResponse<serde_json::Value>> {
346        let endpoint = "v5/position/move-history";
347        let mut params = json!({});
348
349        if let Some(category) = category {
350            params["category"] = json!(category);
351        }
352        if let Some(symbol) = symbol {
353            params["symbol"] = json!(symbol);
354        }
355        if let Some(start_time) = start_time {
356            params["startTime"] = json!(start_time);
357        }
358        if let Some(end_time) = end_time {
359            params["endTime"] = json!(end_time);
360        }
361        if let Some(status) = status {
362            params["status"] = json!(status);
363        }
364        if let Some(block_trade_id) = block_trade_id {
365            params["blockTradeId"] = json!(block_trade_id);
366        }
367        if let Some(limit) = limit {
368            params["limit"] = json!(limit);
369        }
370        if let Some(cursor) = cursor {
371            params["cursor"] = json!(cursor);
372        }
373
374        let response = self.client.get(endpoint, params, SecType::Signed).await?;
375        Ok(response)
376    }
377
378    /// Confirm new risk limit
379    ///
380    /// API: POST /v5/position/confirm-pending-mmr
381    /// https://bybit-exchange.github.io/docs/v5/position/confirm-pending-mmr
382    pub async fn confirm_new_risk_limit(
383        &self,
384        category: &str,
385        symbol: &str,
386    ) -> Result<ServerResponse<serde_json::Value>> {
387        let endpoint = "v5/position/confirm-pending-mmr";
388        let body = json!({
389            "category": category,
390            "symbol": symbol,
391        });
392
393        let response = self.client.post(endpoint, body, SecType::Signed).await?;
394        Ok(response)
395    }
396
397    /// Add/Reduce margin
398    ///
399    /// API: POST /v5/position/add-margin
400    /// https://bybit-exchange.github.io/docs/v5/position/manual-add-margin
401    pub async fn update_margin(
402        &self,
403        category: &str,
404        symbol: &str,
405        margin: &str, // positive for add, negative for reduce
406        position_idx: Option<i32>,
407    ) -> Result<ServerResponse<serde_json::Value>> {
408        let endpoint = "v5/position/add-margin";
409        let mut body = json!({
410            "category": category,
411            "symbol": symbol,
412            "margin": margin,
413        });
414
415        if let Some(position_idx) = position_idx {
416            body["positionIdx"] = json!(position_idx);
417        }
418
419        let response = self.client.post(endpoint, body, SecType::Signed).await?;
420        Ok(response)
421    }
422
423    /// Get execution
424    ///
425    /// API: GET /v5/execution/list
426    /// https://bybit-exchange.github.io/docs/v5/position/execution
427    pub async fn get_execution(
428        &self,
429        category: &str,
430        symbol: Option<&str>,
431        order_id: Option<&str>,
432        order_link_id: Option<&str>,
433        base_coin: Option<&str>,
434        start_time: Option<i64>,
435        end_time: Option<i64>,
436        exec_type: Option<&str>,
437        limit: Option<i32>,
438        cursor: Option<&str>,
439    ) -> Result<ServerResponse<serde_json::Value>> {
440        let endpoint = "v5/execution/list";
441        let mut params = json!({
442            "category": category,
443        });
444
445        if let Some(symbol) = symbol {
446            params["symbol"] = json!(symbol);
447        }
448        if let Some(order_id) = order_id {
449            params["orderId"] = json!(order_id);
450        }
451        if let Some(order_link_id) = order_link_id {
452            params["orderLinkId"] = json!(order_link_id);
453        }
454        if let Some(base_coin) = base_coin {
455            params["baseCoin"] = json!(base_coin);
456        }
457        if let Some(start_time) = start_time {
458            params["startTime"] = json!(start_time);
459        }
460        if let Some(end_time) = end_time {
461            params["endTime"] = json!(end_time);
462        }
463        if let Some(exec_type) = exec_type {
464            params["execType"] = json!(exec_type);
465        }
466        if let Some(limit) = limit {
467            params["limit"] = json!(limit);
468        }
469        if let Some(cursor) = cursor {
470            params["cursor"] = json!(cursor);
471        }
472
473        let response = self.client.get(endpoint, params, SecType::Signed).await?;
474        Ok(response)
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::rest::ApiKeyPair;
482
483    fn create_test_client() -> PositionClient {
484        let api_key_pair = ApiKeyPair::new(
485            "test".to_string(),
486            "test_key".to_string(),
487            "test_secret".to_string(),
488        );
489        let rest_client =
490            RestClient::new(api_key_pair, "https://api-testnet.bybit.com".to_string());
491        PositionClient::new(rest_client)
492    }
493
494    #[test]
495    fn test_position_client_creation() {
496        let _client = create_test_client();
497    }
498
499    #[tokio::test]
500    async fn test_position_info_params() {
501        let category = "linear";
502        let symbol = Some("BTCUSDT");
503        let base_coin: Option<&str> = None;
504        let settle_coin: Option<&str> = None;
505        let limit = Some(50);
506        let cursor: Option<&str> = None;
507
508        assert_eq!(category, "linear");
509        assert_eq!(symbol, Some("BTCUSDT"));
510        assert!(base_coin.is_none());
511        assert!(settle_coin.is_none());
512        assert_eq!(limit, Some(50));
513        assert!(cursor.is_none());
514    }
515
516    #[tokio::test]
517    async fn test_set_leverage_params() {
518        let category = "linear";
519        let symbol = "BTCUSDT";
520        let buy_leverage = "10";
521        let sell_leverage = "10";
522
523        assert_eq!(category, "linear");
524        assert_eq!(symbol, "BTCUSDT");
525        assert_eq!(buy_leverage, "10");
526        assert_eq!(sell_leverage, "10");
527    }
528
529    #[tokio::test]
530    async fn test_switch_margin_mode_params() {
531        let category = "linear";
532        let symbol = "BTCUSDT";
533        let trade_mode = 1; // isolated margin
534        let buy_leverage = "5";
535        let sell_leverage = "5";
536
537        assert_eq!(category, "linear");
538        assert_eq!(symbol, "BTCUSDT");
539        assert_eq!(trade_mode, 1);
540        assert_eq!(buy_leverage, "5");
541        assert_eq!(sell_leverage, "5");
542    }
543
544    #[tokio::test]
545    async fn test_set_trading_stop_params() {
546        let category = "linear";
547        let symbol = "BTCUSDT";
548        let position_idx = 0;
549        let take_profit = Some("50000");
550        let stop_loss = Some("40000");
551        let trailing_stop: Option<&str> = None;
552
553        assert_eq!(category, "linear");
554        assert_eq!(symbol, "BTCUSDT");
555        assert_eq!(position_idx, 0);
556        assert_eq!(take_profit, Some("50000"));
557        assert_eq!(stop_loss, Some("40000"));
558        assert!(trailing_stop.is_none());
559    }
560
561    #[tokio::test]
562    async fn test_closed_pnl_params() {
563        let category = "linear";
564        let symbol = Some("BTCUSDT");
565        let start_time = Some(1234567890i64);
566        let end_time = Some(1234567899i64);
567        let limit = Some(100);
568        let cursor: Option<&str> = None;
569
570        assert_eq!(category, "linear");
571        assert_eq!(symbol, Some("BTCUSDT"));
572        assert_eq!(start_time, Some(1234567890));
573        assert_eq!(end_time, Some(1234567899));
574        assert_eq!(limit, Some(100));
575        assert!(cursor.is_none());
576    }
577}