Skip to main content

ccxt_exchanges/binance/rest/futures/
margin.rs

1//! Margin operations for Binance futures.
2
3use super::super::super::{Binance, signed_request::HttpMethod};
4use ccxt_core::{Error, ParseError, Result, types::MarketType};
5use reqwest::header::HeaderMap;
6use rust_decimal::Decimal;
7use serde_json::Value;
8use std::collections::{BTreeMap, HashMap};
9
10impl Binance {
11    /// Set margin mode for a trading pair.
12    pub async fn set_margin_mode(
13        &self,
14        symbol: &str,
15        margin_mode: &str,
16        params: Option<HashMap<String, String>>,
17    ) -> Result<HashMap<String, Value>> {
18        let margin_type = match margin_mode.to_uppercase().as_str() {
19            "ISOLATED" | "ISOLATED_MARGIN" => "ISOLATED",
20            "CROSS" | "CROSSED" | "CROSS_MARGIN" => "CROSSED",
21            _ => {
22                return Err(Error::invalid_request(format!(
23                    "Invalid margin mode: {}. Must be 'isolated' or 'cross'",
24                    margin_mode
25                )));
26            }
27        };
28
29        self.load_markets(false).await?;
30        let market = self.base().market(symbol).await?;
31
32        if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
33            return Err(Error::invalid_request(
34                "set_margin_mode() supports futures and swap markets only".to_string(),
35            ));
36        }
37
38        let url = if market.linear.unwrap_or(true) {
39            format!("{}/marginType", self.urls().fapi_private)
40        } else if market.inverse.unwrap_or(false) {
41            format!("{}/marginType", self.urls().dapi_private)
42        } else {
43            return Err(Error::invalid_request(
44                "Unknown futures market type".to_string(),
45            ));
46        };
47
48        let mut builder = self
49            .signed_request(url)
50            .method(HttpMethod::Post)
51            .param("symbol", &market.id)
52            .param("marginType", margin_type);
53
54        if let Some(p) = params {
55            let params_map: BTreeMap<String, String> = p.into_iter().collect();
56            builder = builder.params(params_map);
57        }
58
59        let response = builder.execute().await?;
60
61        let result: HashMap<String, Value> = serde_json::from_value(response).map_err(|e| {
62            Error::from(ParseError::invalid_format(
63                "data",
64                format!("Failed to parse response: {}", e),
65            ))
66        })?;
67
68        Ok(result)
69    }
70
71    /// Set position mode (hedge mode or one-way mode).
72    pub async fn set_position_mode(&self, dual_side: bool, params: Option<Value>) -> Result<Value> {
73        self.check_required_credentials()?;
74
75        let use_dapi = params
76            .as_ref()
77            .and_then(|p| p.get("type"))
78            .and_then(serde_json::Value::as_str)
79            .is_some_and(|t| t == "delivery" || t == "coin_m");
80
81        let mut request_params = BTreeMap::new();
82        request_params.insert("dualSidePosition".to_string(), dual_side.to_string());
83
84        if let Some(params) = params {
85            if let Some(obj) = params.as_object() {
86                for (key, value) in obj {
87                    if key == "type" {
88                        continue;
89                    }
90                    if let Some(v) = value.as_str() {
91                        request_params.insert(key.clone(), v.to_string());
92                    } else if let Some(v) = value.as_bool() {
93                        request_params.insert(key.clone(), v.to_string());
94                    } else if let Some(v) = value.as_i64() {
95                        request_params.insert(key.clone(), v.to_string());
96                    }
97                }
98            }
99        }
100
101        let timestamp = self.get_signing_timestamp().await?;
102        let auth = self.get_auth()?;
103        let signed_params =
104            auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
105
106        let url = if use_dapi {
107            format!("{}/positionSide/dual", self.urls().dapi_private)
108        } else {
109            format!("{}/positionSide/dual", self.urls().fapi_private)
110        };
111
112        let mut headers = HeaderMap::new();
113        auth.add_auth_headers_reqwest(&mut headers);
114
115        let body = serde_json::to_value(&signed_params).map_err(|e| {
116            Error::from(ParseError::invalid_format(
117                "data",
118                format!("Failed to serialize params: {}", e),
119            ))
120        })?;
121
122        let data = self
123            .base()
124            .http_client
125            .post(&url, Some(headers), Some(body))
126            .await?;
127
128        Ok(data)
129    }
130
131    /// Fetch current position mode.
132    pub async fn fetch_position_mode(&self, params: Option<Value>) -> Result<bool> {
133        self.check_required_credentials()?;
134
135        let use_dapi = params
136            .as_ref()
137            .and_then(|p| p.get("type"))
138            .and_then(serde_json::Value::as_str)
139            .is_some_and(|t| t == "delivery" || t == "coin_m");
140
141        let mut request_params = BTreeMap::new();
142
143        if let Some(params) = params {
144            if let Some(obj) = params.as_object() {
145                for (key, value) in obj {
146                    if key == "type" {
147                        continue;
148                    }
149                    if let Some(v) = value.as_str() {
150                        request_params.insert(key.clone(), v.to_string());
151                    }
152                }
153            }
154        }
155
156        let timestamp = self.get_signing_timestamp().await?;
157        let auth = self.get_auth()?;
158        let signed_params =
159            auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
160
161        let query_string = signed_params
162            .iter()
163            .map(|(k, v)| format!("{}={}", k, v))
164            .collect::<Vec<_>>()
165            .join("&");
166
167        let url = if use_dapi {
168            format!(
169                "{}/positionSide/dual?{}",
170                self.urls().dapi_private,
171                query_string
172            )
173        } else {
174            format!(
175                "{}/positionSide/dual?{}",
176                self.urls().fapi_private,
177                query_string
178            )
179        };
180
181        let mut headers = HeaderMap::new();
182        auth.add_auth_headers_reqwest(&mut headers);
183
184        let data = self.base().http_client.get(&url, Some(headers)).await?;
185
186        if let Some(dual_side) = data.get("dualSidePosition") {
187            if let Some(value) = dual_side.as_bool() {
188                return Ok(value);
189            }
190            if let Some(value_str) = dual_side.as_str() {
191                return Ok(value_str.to_lowercase() == "true");
192            }
193        }
194
195        Err(Error::from(ParseError::invalid_format(
196            "data",
197            "Failed to parse position mode response",
198        )))
199    }
200
201    /// Modify isolated position margin.
202    pub async fn modify_isolated_position_margin(
203        &self,
204        symbol: &str,
205        amount: Decimal,
206        params: Option<Value>,
207    ) -> Result<Value> {
208        self.check_required_credentials()?;
209
210        let market = self.base().market(symbol).await?;
211        let mut request_params = BTreeMap::new();
212        request_params.insert("symbol".to_string(), market.id.clone());
213        request_params.insert("amount".to_string(), amount.abs().to_string());
214        request_params.insert(
215            "type".to_string(),
216            if amount > Decimal::ZERO {
217                "1".to_string()
218            } else {
219                "2".to_string()
220            },
221        );
222
223        if let Some(params) = params {
224            if let Some(obj) = params.as_object() {
225                for (key, value) in obj {
226                    if let Some(v) = value.as_str() {
227                        request_params.insert(key.clone(), v.to_string());
228                    }
229                }
230            }
231        }
232
233        let timestamp = self.get_signing_timestamp().await?;
234        let auth = self.get_auth()?;
235        let signed_params =
236            auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
237
238        let url = if market.linear.unwrap_or(true) {
239            format!("{}/positionMargin", self.urls().fapi_private)
240        } else {
241            format!("{}/positionMargin", self.urls().dapi_private)
242        };
243
244        let mut headers = HeaderMap::new();
245        auth.add_auth_headers_reqwest(&mut headers);
246
247        let body = serde_json::to_value(&signed_params).map_err(|e| {
248            Error::from(ParseError::invalid_format(
249                "data",
250                format!("Failed to serialize params: {}", e),
251            ))
252        })?;
253
254        let data = self
255            .base()
256            .http_client
257            .post(&url, Some(headers), Some(body))
258            .await?;
259
260        Ok(data)
261    }
262}