bybit_rust_api/rest/position/
position_client.rs

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