bybit/
position.rs

1use std::collections::BTreeMap;
2
3use serde_json::{json, Value};
4
5use crate::api::{Position, API};
6use crate::client::Client;
7use crate::errors::BybitError;
8use crate::model::{
9    AddMarginRequest, AddMarginResponse, AddReduceMarginRequest, AddReduceMarginResponse,
10    ChangeMarginRequest, ChangeMarginResponse, ClosedPnlRequest,
11    ClosedPnlResponse, InfoResponse, LeverageRequest, LeverageResponse,
12    MarginModeRequest, MarginModeResponse, MoveHistoryRequest, MoveHistoryResponse,
13    MovePositionRequest, MovePositionResponse, PositionRequest, SetRiskLimit, SetRiskLimitResponse, TradingStopRequest,
14    TradingStopResponse,
15};
16use crate::util::{build_json_request, build_request, date_to_milliseconds};
17
18#[derive(Clone)]
19pub struct PositionManager {
20    pub client: Client,
21    pub recv_window: u16,
22}
23
24impl PositionManager {
25    /// Asynchronously retrieves information about a position based on the provided request.
26    ///
27    /// # Arguments
28    ///
29    /// * `req` - The position request containing the category, symbol, base coin, settle coin, and limit.
30    ///
31    /// # Returns
32    ///
33    /// A `Result` containing a vector of `PositionInfo` if the operation is successful, or an error if it fails.
34    ///
35    /// # Example
36    ///
37    /// ```
38    /// use crate::model::{PositionRequest, Category};
39    /// use crate::errors::Result;
40    /// use crate::api::PositionInfo;
41    /// use my_module::PositionManager;
42    ///
43    /// #[tokio::main]
44    /// async fn main() -> Result<()> {
45    ///     let position_manager = PositionManager::new();
46    ///     let request = PositionRequest::new(Category::Linear, Some("symbol"), Some("base_coin"), Some("settle_coin"), Some(10));
47    ///     let position_info = position_manager.get_info(request).await?;
48    ///     Ok(())
49    /// }
50    /// ```
51    pub async fn get_info<'b>(&self, req: PositionRequest<'_>) -> Result<InfoResponse, BybitError> {
52        let mut parameters: BTreeMap<String, String> = BTreeMap::new();
53        parameters.insert("category".into(), req.category.as_str().into());
54        if let Some(v) = req.symbol {
55            parameters.insert("symbol".into(), v.into());
56        }
57        if let Some(v) = req.base_coin {
58            parameters.insert("baseCoin".into(), v.into());
59        }
60        if let Some(v) = req.settle_coin {
61            parameters.insert("settleCoin".into(), v.into());
62        }
63        if let Some(v) = req.limit {
64            parameters.insert("limit".into(), v.to_string());
65        }
66        let request = build_request(&parameters);
67        let response: InfoResponse = self
68            .client
69            .get_signed(
70                API::Position(Position::Information),
71                self.recv_window.into(),
72                Some(request),
73            )
74            .await?;
75        Ok(response)
76    }
77
78    // Sets the leverage for a given symbol.
79    ///
80    /// # Arguments
81    ///
82    /// * `req` - The leverage request containing category, symbol, and leverage.
83    ///
84    /// # Returns
85    ///
86    /// A result containing the leverage response.
87    pub async fn set_leverage<'b>(
88        &self,
89        req: LeverageRequest<'_>,
90    ) -> Result<LeverageResponse, BybitError> {
91        let mut parameters: BTreeMap<String, String> = BTreeMap::new();
92        parameters.insert("category".into(), req.category.as_str().into());
93        parameters.insert("symbol".into(), req.symbol.into());
94        parameters.insert("buyLeverage".into(), req.leverage.to_string());
95        parameters.insert("sellLeverage".into(), req.leverage.to_string());
96        let request = build_json_request(&parameters);
97        let response: LeverageResponse = self
98            .client
99            .post_signed(
100                API::Position(Position::SetLeverage),
101                self.recv_window.into(),
102                Some(request),
103            )
104            .await?;
105        Ok(response)
106    }
107
108    /// Set the margin mode.
109    ///
110    /// # Arguments
111    ///
112    /// * `req` - The ChangeMarginRequest containing the necessary information.
113    ///
114    /// # Returns
115    ///
116    /// * Result<ChangeMarginResponse> - The result of setting the margin mode.
117    pub async fn set_margin_mode<'b>(
118        &self,
119        req: ChangeMarginRequest<'_>,
120    ) -> Result<ChangeMarginResponse, BybitError> {
121        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
122        parameters.insert("category".into(), req.category.as_str().into());
123        parameters.insert("symbol".into(), req.symbol.into());
124        parameters.insert("tradeMode".into(), req.trade_mode.into());
125        let request = build_json_request(&parameters);
126        let response: ChangeMarginResponse = self
127            .client
128            .post_signed(
129                API::Position(Position::SwitchIsolated),
130                self.recv_window.into(),
131                Some(request),
132            )
133            .await?;
134        Ok(response)
135    }
136
137    /// Set the position mode.
138    ///
139    /// # Arguments
140    ///
141    /// * `req` - The MarginModeRequest containing the necessary information.
142    ///
143    /// # Returns
144    ///
145    /// * Result<MarginModeResponse> - The result of setting the position mode.
146    pub async fn set_position_mode<'b>(
147        &self,
148        req: MarginModeRequest<'_>,
149    ) -> Result<MarginModeResponse, BybitError> {
150        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
151        parameters.insert("category".into(), req.category.as_str().into());
152        if let Some(v) = req.symbol {
153            parameters.insert("symbol".into(), v.into());
154        }
155        if let Some(v) = req.coin {
156            parameters.insert("coin".into(), v.into());
157        }
158        parameters.insert("mode".into(), req.mode.into());
159        let request = build_json_request(&parameters);
160        let response: MarginModeResponse = self
161            .client
162            .post_signed(
163                API::Position(Position::SwitchMode),
164                self.recv_window.into(),
165                Some(request),
166            )
167            .await?;
168        Ok(response)
169    }
170
171    /// Set the risk limit.
172    ///
173    /// # Arguments
174    ///
175    /// * `req` - The SetRiskLimitRequest containing the necessary information.
176    ///
177    /// # Returns
178    ///
179    /// * Result<SetRiskLimitResult> - The result of setting the risk limit.
180    pub async fn set_risk_limit<'b>(
181        &self,
182        req: SetRiskLimit<'_>,
183    ) -> Result<SetRiskLimitResponse, BybitError> {
184        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
185        parameters.insert("category".into(), req.category.as_str().into());
186        parameters.insert("symbol".into(), req.symbol.into());
187        parameters.insert("riskId".into(), req.risk_id.into());
188        if let Some(v) = req.position_idx {
189            parameters.insert("positionIdx".into(), v.into());
190        }
191        let request = build_json_request(&parameters);
192        let response: SetRiskLimitResponse = self
193            .client
194            .post_signed(
195                API::Position(Position::SetRiskLimit),
196                self.recv_window.into(),
197                Some(request),
198            )
199            .await?;
200        Ok(response)
201    }
202
203    /// Set the trading stop.
204    ///
205    /// # Arguments
206    ///
207    /// * `req` - The TradingStopRequest containing the necessary information.
208    ///
209    /// # Returns
210    ///
211    /// * Result<TradingStopResponse> - The result of setting the trading stop.
212    pub async fn set_trading_stop<'b>(
213        &self,
214        req: TradingStopRequest<'_>,
215    ) -> Result<TradingStopResponse, BybitError> {
216        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
217        parameters.insert("category".into(), req.category.as_str().into());
218        parameters.insert("symbol".into(), req.symbol.into());
219        if let Some(v) = req.take_profit {
220            parameters.insert("takeProfit".into(), v.into());
221        }
222        if let Some(v) = req.stop_loss {
223            parameters.insert("stopLoss".into(), v.into());
224        }
225        if let Some(v) = req.tp_trigger_by {
226            parameters.insert("tpTriggerBy".into(), v.into());
227        }
228        if let Some(v) = req.sl_trigger_by {
229            parameters.insert("slTriggerBy".into(), v.into());
230        }
231        if let Some(v) = req.tpsl_mode {
232            parameters.insert("tpslMode".into(), v.into());
233        }
234        if let Some(v) = req.tp_order_type {
235            parameters.insert("tpOrderType".into(), v.as_str().into());
236        }
237        if let Some(v) = req.sl_order_type {
238            parameters.insert("slOrderType".into(), v.as_str().into());
239        }
240        if let Some(v) = req.tp_size {
241            parameters.insert("tpSize".into(), v.into());
242        }
243        if let Some(v) = req.sl_size {
244            parameters.insert("slSize".into(), v.into());
245        }
246        if let Some(v) = req.tp_limit_price {
247            parameters.insert("tpLimitPrice".into(), v.into());
248        }
249        if let Some(v) = req.sl_limit_price {
250            parameters.insert("slLimitPrice".into(), v.into());
251        }
252        parameters.insert("positionIdx".into(), req.position_idx.into());
253        let request = build_json_request(&parameters);
254        let response: TradingStopResponse = self
255            .client
256            .post_signed(
257                API::Position(Position::SetTradingStop),
258                self.recv_window.into(),
259                Some(request),
260            )
261            .await?;
262        Ok(response)
263    }
264
265    /// Set the auto-add margin mode for a given position.
266    ///
267    /// # Arguments
268    ///
269    /// * `req` - The AddMarginRequest containing the necessary information.
270    ///
271    /// # Returns
272    ///
273    /// A result containing the AddMarginResponse.
274    pub async fn set_add_margin<'b>(
275        &self,
276        req: AddMarginRequest<'_>,
277    ) -> Result<AddMarginResponse, BybitError> {
278        // Create a new BTreeMap to store the parameters
279        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
280
281        // Add the category and symbol parameters
282        parameters.insert("category".into(), req.category.as_str().into());
283        parameters.insert("symbol".into(), req.symbol.into());
284
285        // Add the autoAddMargin parameter based on the value of `auto_add`
286        if req.auto_add {
287            parameters.insert("autoAddMargin".into(), 1.into());
288        } else {
289            parameters.insert("autoAddMargin".into(), 0.into());
290        }
291
292        // Add the positionIdx parameter if it is not None
293        if let Some(v) = req.position_idx {
294            parameters.insert("positionIdx".into(), v.into());
295        }
296
297        // Build the JSON request
298        let request = build_json_request(&parameters);
299
300        // Send the POST request to the server
301        let response: AddMarginResponse = self
302            .client
303            .post_signed(
304                API::Position(Position::SetAutoaddMargin),
305                self.recv_window.into(),
306                Some(request),
307            )
308            .await?;
309
310        // Return the response
311        Ok(response)
312    }
313
314    /// Set the auto add margin.
315    ///
316    /// # Arguments
317    ///
318    /// * `req` - The AddReduceMarginRequest containing the necessary information.
319    ///
320    /// # Returns
321    ///
322    /// * Result<AddReduceMarginResponse> - The result of setting the auto add margin.
323    pub async fn add_or_reduce_margin<'b>(
324        &self,
325        req: AddReduceMarginRequest<'_>,
326    ) -> Result<AddReduceMarginResponse, BybitError> {
327        // Create a new BTreeMap to store the parameters
328        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
329
330        // Add the category and symbol parameters
331        parameters.insert("category".into(), req.category.as_str().into());
332        parameters.insert("symbol".into(), req.symbol.into());
333
334        // Add the margin parameter
335        parameters.insert("margin".into(), req.margin.into());
336
337        // Add the positionIdx parameter if it is not None
338        if let Some(v) = req.position_idx {
339            parameters.insert("positionIdx".into(), v.into());
340        }
341
342        // Build the JSON request
343        let request = build_json_request(&parameters);
344
345        // Send the POST request to the server
346        let response: AddReduceMarginResponse = self
347            .client
348            .post_signed(
349                API::Position(Position::AddorReduceMargin),
350                self.recv_window.into(),
351                Some(request),
352            )
353            .await?;
354
355        // Return the response
356        Ok(response)
357    }
358
359    pub async fn get_closed_pnl<'b>(
360        &self,
361        req: ClosedPnlRequest<'_>,
362    ) -> Result<ClosedPnlResponse, BybitError> {
363        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
364        parameters.insert("category".into(), req.category.as_str().into());
365        if let Some(v) = req.symbol {
366            parameters.insert("symbol".into(), v.into());
367        }
368
369        if let Some(start_str) = req.start_time.as_ref().map(|s| s.as_ref()) {
370            let start_millis = date_to_milliseconds(start_str);
371            parameters
372                .entry("end".to_owned())
373                .or_insert_with(|| start_millis.to_string().into());
374        }
375        if let Some(end_str) = req.end_time.as_ref().map(|s| s.as_ref()) {
376            let end_millis = date_to_milliseconds(end_str);
377            parameters
378                .entry("end".to_owned())
379                .or_insert_with(|| end_millis.to_string().into());
380        }
381        if let Some(v) = req.limit {
382            parameters.insert("limit".into(), v.into());
383        }
384        let request = build_request(&parameters);
385        let response: ClosedPnlResponse = self
386            .client
387            .get_signed(
388                API::Position(Position::ClosedPnl),
389                self.recv_window.into(),
390                Some(request),
391            )
392            .await?;
393        Ok(response)
394    }
395
396    /// Moves positions from one user to another.
397    ///
398    /// # Arguments
399    ///
400    /// * `req` - The request containing the fromUid, toUid, and list of positions to move.
401    ///
402    /// # Returns
403    ///
404    /// A result containing the response.
405    pub async fn move_position<'b>(
406        &self,
407        req: MovePositionRequest<'_>,
408    ) -> Result<MovePositionResponse, BybitError> {
409        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
410        // The user id of the account to move the position from.
411        parameters.insert("fromUid".into(), req.from_uid.into());
412        // The user id of the account to move the position to.
413        parameters.insert("toUid".into(), req.to_uid.into());
414        // The list of positions to move.
415        parameters.insert("list".into(), json!(req.list));
416        let request = build_json_request(&parameters);
417        let response: MovePositionResponse = self
418            .client
419            .post_signed(
420                API::Position(Position::MovePosition),
421                self.recv_window.into(),
422                Some(request),
423            )
424            .await?;
425        Ok(response)
426    }
427
428    /// Retrieves the history of position movements.
429    ///
430    /// # Arguments
431    ///
432    /// * `req` - The request containing the parameters for the history of position movements.
433    ///
434    /// # Returns
435    ///
436    /// A result containing the response.
437    pub async fn move_position_history<'b>(
438        &self,
439        req: MoveHistoryRequest<'_>,
440    ) -> Result<MoveHistoryResponse, BybitError> {
441        // Create a new BTreeMap to hold the parameters.
442        let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
443
444        // If the category is specified, add it to the parameters.
445        if let Some(category) = req.category {
446            parameters.insert("category".into(), category.as_str().into());
447        }
448
449        // If the symbol is specified, add it to the parameters.
450        if let Some(symbol) = req.symbol {
451            parameters.insert("symbol".into(), symbol.into());
452        }
453
454        // If the start time is specified, convert it to milliseconds and insert it into the parameters.
455        if let Some(start_str) = req.start_time.as_ref().map(|s| s.as_ref()) {
456            let start_millis = date_to_milliseconds(start_str);
457            parameters
458                .entry("end".to_owned())
459                .or_insert_with(|| start_millis.to_string().into());
460        }
461
462        // If the end time is specified, convert it to milliseconds and insert it into the parameters.
463        if let Some(end_str) = req.end_time.as_ref().map(|s| s.as_ref()) {
464            let end_millis = date_to_milliseconds(end_str);
465            parameters
466                .entry("end".to_owned())
467                .or_insert_with(|| end_millis.to_string().into());
468        }
469
470        // If the status is specified, add it to the parameters.
471        if let Some(status) = req.status {
472            parameters.insert("status".into(), status.into());
473        }
474
475        // If the block trade id is specified, add it to the parameters.
476        if let Some(block_trade_id) = req.block_trade_id {
477            parameters.insert("blockTradeId".into(), block_trade_id.into());
478        }
479
480        // If the limit is specified, add it to the parameters.
481        if let Some(limit) = req.limit {
482            parameters.insert("limit".into(), limit.into());
483        }
484
485        // Build the request using the parameters.
486        let request = build_request(&parameters);
487
488        // Send a GET request to the Bybit API to retrieve the history of position movements.
489        let response: MoveHistoryResponse = self
490            .client
491            .get_signed(
492                API::Position(Position::MovePositionHistory),
493                self.recv_window.into(),
494                Some(request),
495            )
496            .await?;
497
498        // Return the response.
499        Ok(response)
500    }
501}