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 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 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 pub async fn switch_margin_mode(
80 &self,
81 category: &str,
82 symbol: &str,
83 trade_mode: i32, 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 pub async fn switch_position_mode(
105 &self,
106 category: &str,
107 mode: i32, 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 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 pub async fn set_auto_add_margin(
203 &self,
204 category: &str,
205 symbol: &str,
206 auto_add_margin: i32, 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 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 pub async fn set_tpsl_mode(
267 &self,
268 category: &str,
269 symbol: &str,
270 tp_sl_mode: &str, ) -> 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 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 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 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 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 pub async fn update_margin(
401 &self,
402 category: &str,
403 symbol: &str,
404 margin: &str, 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 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; 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}