ccxt_exchanges/binance/rest/
spot.rs

1//! Binance spot trading operations.
2//!
3//! This module contains all spot trading methods including order creation,
4//! cancellation, and order management.
5
6use super::super::{Binance, parser, signed_request::HttpMethod};
7use ccxt_core::{
8    Error, ParseError, Result,
9    types::{Order, OrderSide, OrderType},
10};
11use std::collections::{BTreeMap, HashMap};
12use tracing::warn;
13
14impl Binance {
15    /// Create a new order.
16    ///
17    /// # Arguments
18    ///
19    /// * `symbol` - Trading pair symbol.
20    /// * `order_type` - Order type (Market, Limit, StopLoss, etc.).
21    /// * `side` - Order side (Buy or Sell).
22    /// * `amount` - Order quantity.
23    /// * `price` - Optional price (required for limit orders).
24    /// * `params` - Additional parameters.
25    ///
26    /// # Returns
27    ///
28    /// Returns the created [`Order`] structure with order details.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if authentication fails, market is not found, or the API request fails.
33    pub async fn create_order(
34        &self,
35        symbol: &str,
36        order_type: OrderType,
37        side: OrderSide,
38        amount: f64,
39        price: Option<f64>,
40        params: Option<HashMap<String, String>>,
41    ) -> Result<Order> {
42        let market = self.base().market(symbol).await?;
43        let mut request_params = BTreeMap::new();
44
45        request_params.insert("symbol".to_string(), market.id.clone());
46        request_params.insert(
47            "side".to_string(),
48            match side {
49                OrderSide::Buy => "BUY".to_string(),
50                OrderSide::Sell => "SELL".to_string(),
51            },
52        );
53        request_params.insert(
54            "type".to_string(),
55            match order_type {
56                OrderType::Market => "MARKET".to_string(),
57                OrderType::Limit => "LIMIT".to_string(),
58                OrderType::StopLoss => "STOP_LOSS".to_string(),
59                OrderType::StopLossLimit => "STOP_LOSS_LIMIT".to_string(),
60                OrderType::TakeProfit => "TAKE_PROFIT".to_string(),
61                OrderType::TakeProfitLimit => "TAKE_PROFIT_LIMIT".to_string(),
62                OrderType::LimitMaker => "LIMIT_MAKER".to_string(),
63                OrderType::StopMarket => "STOP_MARKET".to_string(),
64                OrderType::StopLimit => "STOP_LIMIT".to_string(),
65                OrderType::TrailingStop => "TRAILING_STOP_MARKET".to_string(),
66            },
67        );
68        request_params.insert("quantity".to_string(), amount.to_string());
69
70        if let Some(p) = price {
71            request_params.insert("price".to_string(), p.to_string());
72        }
73
74        // Limit orders require timeInForce parameter
75        if order_type == OrderType::Limit
76            || order_type == OrderType::StopLossLimit
77            || order_type == OrderType::TakeProfitLimit
78        {
79            if !request_params.contains_key("timeInForce") {
80                request_params.insert("timeInForce".to_string(), "GTC".to_string());
81            }
82        }
83
84        if let Some(extra) = params {
85            for (k, v) in extra {
86                request_params.insert(k, v);
87            }
88        }
89
90        // Handle cost parameter for market buy orders (quoteOrderQty)
91        if order_type == OrderType::Market && side == OrderSide::Buy {
92            if let Some(cost_str) = request_params.get("cost") {
93                request_params.insert("quoteOrderQty".to_string(), cost_str.clone());
94                request_params.remove("quantity");
95                request_params.remove("cost");
96            }
97        }
98
99        // Handle conditional order parameters
100        if matches!(
101            order_type,
102            OrderType::StopLoss
103                | OrderType::StopLossLimit
104                | OrderType::TakeProfit
105                | OrderType::TakeProfitLimit
106                | OrderType::StopMarket
107        ) {
108            if !request_params.contains_key("stopPrice") {
109                if let Some(stop_loss) = request_params.get("stopLossPrice") {
110                    request_params.insert("stopPrice".to_string(), stop_loss.clone());
111                } else if let Some(take_profit) = request_params.get("takeProfitPrice") {
112                    request_params.insert("stopPrice".to_string(), take_profit.clone());
113                }
114            }
115        }
116
117        // Trailing stop handling for spot market
118        if order_type == OrderType::TrailingStop {
119            if market.is_spot() {
120                if !request_params.contains_key("trailingDelta") {
121                    if let Some(percent_str) = request_params.get("trailingPercent") {
122                        if let Ok(percent) = percent_str.parse::<f64>() {
123                            let delta = (percent * 100.0) as i64;
124                            request_params.insert("trailingDelta".to_string(), delta.to_string());
125                            request_params.remove("trailingPercent");
126                        }
127                    }
128                }
129            }
130        }
131
132        let url = format!("{}/order", self.urls().private);
133        let data = self
134            .signed_request(url)
135            .method(HttpMethod::Post)
136            .params(request_params)
137            .execute()
138            .await?;
139
140        parser::parse_order(&data, Some(&market))
141    }
142
143    /// Cancel an order.
144    ///
145    /// # Arguments
146    ///
147    /// * `id` - Order ID.
148    /// * `symbol` - Trading pair symbol.
149    ///
150    /// # Returns
151    ///
152    /// Returns the cancelled [`Order`] information.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if authentication fails, market is not found, or the API request fails.
157    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
158        let market = self.base().market(symbol).await?;
159        let url = format!("{}/order", self.urls().private);
160
161        let data = self
162            .signed_request(url)
163            .method(HttpMethod::Delete)
164            .param("symbol", &market.id)
165            .param("orderId", id)
166            .execute()
167            .await?;
168
169        parser::parse_order(&data, Some(&market))
170    }
171
172    /// Fetch order details.
173    ///
174    /// # Arguments
175    ///
176    /// * `id` - Order ID.
177    /// * `symbol` - Trading pair symbol.
178    ///
179    /// # Returns
180    ///
181    /// Returns the [`Order`] information.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if authentication fails, market is not found, or the API request fails.
186    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
187        let market = self.base().market(symbol).await?;
188        let url = format!("{}/order", self.urls().private);
189
190        let data = self
191            .signed_request(url)
192            .param("symbol", &market.id)
193            .param("orderId", id)
194            .execute()
195            .await?;
196
197        parser::parse_order(&data, Some(&market))
198    }
199
200    /// Fetch open (unfilled) orders.
201    ///
202    /// # Arguments
203    ///
204    /// * `symbol` - Optional trading pair symbol. If `None`, fetches all open orders.
205    ///
206    /// # Returns
207    ///
208    /// Returns a vector of open [`Order`] structures.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if authentication fails or the API request fails.
213    pub async fn fetch_open_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
214        let market = if let Some(sym) = symbol {
215            Some(self.base().market(sym).await?)
216        } else {
217            None
218        };
219
220        let url = format!("{}/openOrders", self.urls().private);
221
222        let data = self
223            .signed_request(url)
224            .optional_param("symbol", market.as_ref().map(|m| &m.id))
225            .execute()
226            .await?;
227
228        let orders_array = data.as_array().ok_or_else(|| {
229            Error::from(ParseError::invalid_format(
230                "data",
231                "Expected array of orders",
232            ))
233        })?;
234
235        let mut orders = Vec::new();
236        for order_data in orders_array {
237            match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
238                Ok(order) => orders.push(order),
239                Err(e) => {
240                    warn!(error = %e, "Failed to parse order");
241                }
242            }
243        }
244
245        Ok(orders)
246    }
247
248    /// Fetch closed (completed) orders.
249    ///
250    /// # Arguments
251    ///
252    /// * `symbol` - Optional trading pair symbol.
253    /// * `since` - Optional start timestamp (milliseconds).
254    /// * `limit` - Optional limit on number of orders (default 500, max 1000).
255    ///
256    /// # Returns
257    ///
258    /// Returns a vector of closed [`Order`] structures.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if authentication fails or the API request fails.
263    pub async fn fetch_closed_orders(
264        &self,
265        symbol: Option<&str>,
266        since: Option<i64>,
267        limit: Option<u32>,
268    ) -> Result<Vec<Order>> {
269        let all_orders = self.fetch_orders(symbol, since, None).await?;
270
271        let mut closed_orders: Vec<Order> = all_orders
272            .into_iter()
273            .filter(|order| order.status == ccxt_core::types::OrderStatus::Closed)
274            .collect();
275
276        if let Some(l) = limit {
277            closed_orders.truncate(l as usize);
278        }
279
280        Ok(closed_orders)
281    }
282
283    /// Cancel all open orders.
284    ///
285    /// # Arguments
286    ///
287    /// * `symbol` - Trading pair symbol.
288    ///
289    /// # Returns
290    ///
291    /// Returns a vector of cancelled [`Order`] structures.
292    ///
293    /// # Errors
294    ///
295    /// Returns an error if authentication fails, market is not found, or the API request fails.
296    pub async fn cancel_all_orders(&self, symbol: &str) -> Result<Vec<Order>> {
297        let market = self.base().market(symbol).await?;
298        let url = format!("{}/openOrders", self.urls().private);
299
300        let data = self
301            .signed_request(url)
302            .method(HttpMethod::Delete)
303            .param("symbol", &market.id)
304            .execute()
305            .await?;
306
307        let orders_array = data.as_array().ok_or_else(|| {
308            Error::from(ParseError::invalid_format(
309                "data",
310                "Expected array of orders",
311            ))
312        })?;
313
314        let mut orders = Vec::new();
315        for order_data in orders_array {
316            match parser::parse_order(order_data, Some(&market)) {
317                Ok(order) => orders.push(order),
318                Err(e) => {
319                    warn!(error = %e, "Failed to parse order");
320                }
321            }
322        }
323
324        Ok(orders)
325    }
326
327    /// Cancel multiple orders.
328    ///
329    /// # Arguments
330    ///
331    /// * `ids` - Vector of order IDs to cancel.
332    /// * `symbol` - Trading pair symbol.
333    ///
334    /// # Returns
335    ///
336    /// Returns a vector of cancelled [`Order`] structures.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if authentication fails, market is not found, or the API request fails.
341    pub async fn cancel_orders(&self, ids: Vec<String>, symbol: &str) -> Result<Vec<Order>> {
342        let market = self.base().market(symbol).await?;
343
344        let order_ids_json = serde_json::to_string(&ids).map_err(|e| {
345            Error::from(ParseError::invalid_format(
346                "data",
347                format!("Failed to serialize order IDs: {}", e),
348            ))
349        })?;
350
351        let url = format!("{}/openOrders", self.urls().private);
352
353        let data = self
354            .signed_request(url)
355            .method(HttpMethod::Delete)
356            .param("symbol", &market.id)
357            .param("orderIdList", order_ids_json)
358            .execute()
359            .await?;
360
361        let orders_array = data.as_array().ok_or_else(|| {
362            Error::from(ParseError::invalid_format(
363                "data",
364                "Expected array of orders",
365            ))
366        })?;
367
368        let mut orders = Vec::new();
369        for order_data in orders_array {
370            match parser::parse_order(order_data, Some(&market)) {
371                Ok(order) => orders.push(order),
372                Err(e) => {
373                    warn!(error = %e, "Failed to parse order");
374                }
375            }
376        }
377
378        Ok(orders)
379    }
380
381    /// Fetch all orders (historical and current).
382    ///
383    /// # Arguments
384    ///
385    /// * `symbol` - Optional trading pair symbol.
386    /// * `since` - Optional start timestamp (milliseconds).
387    /// * `limit` - Optional limit on number of orders (default 500, max 1000).
388    ///
389    /// # Returns
390    ///
391    /// Returns a vector of [`Order`] structures.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if authentication fails or the API request fails.
396    pub async fn fetch_orders(
397        &self,
398        symbol: Option<&str>,
399        since: Option<i64>,
400        limit: Option<u32>,
401    ) -> Result<Vec<Order>> {
402        let market = if let Some(sym) = symbol {
403            Some(self.base().market(sym).await?)
404        } else {
405            None
406        };
407
408        let url = format!("{}/allOrders", self.urls().private);
409
410        let data = self
411            .signed_request(url)
412            .optional_param("symbol", market.as_ref().map(|m| &m.id))
413            .optional_param("startTime", since)
414            .optional_param("limit", limit)
415            .execute()
416            .await?;
417
418        let orders_array = data.as_array().ok_or_else(|| {
419            Error::from(ParseError::invalid_format(
420                "data",
421                "Expected array of orders",
422            ))
423        })?;
424
425        let mut orders = Vec::new();
426        for order_data in orders_array {
427            match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
428                Ok(order) => orders.push(order),
429                Err(e) => {
430                    warn!(error = %e, "Failed to parse order");
431                }
432            }
433        }
434
435        Ok(orders)
436    }
437
438    /// Create a stop-loss order.
439    ///
440    /// # Arguments
441    ///
442    /// * `symbol` - Trading pair symbol.
443    /// * `side` - Order side (Buy/Sell).
444    /// * `amount` - Order quantity.
445    /// * `stop_price` - Stop-loss trigger price.
446    /// * `price` - Optional limit price (if `None`, creates market stop-loss order).
447    /// * `params` - Optional additional parameters.
448    ///
449    /// # Returns
450    ///
451    /// Returns the created stop-loss [`Order`].
452    ///
453    /// # Errors
454    ///
455    /// Returns an error if authentication fails or the API request fails.
456    pub async fn create_stop_loss_order(
457        &self,
458        symbol: &str,
459        side: OrderSide,
460        amount: f64,
461        stop_price: f64,
462        price: Option<f64>,
463        params: Option<HashMap<String, String>>,
464    ) -> Result<Order> {
465        let mut request_params = params.unwrap_or_default();
466
467        request_params.insert("stopPrice".to_string(), stop_price.to_string());
468
469        let order_type = if price.is_some() {
470            OrderType::StopLossLimit
471        } else {
472            OrderType::StopLoss
473        };
474
475        self.create_order(
476            symbol,
477            order_type,
478            side,
479            amount,
480            price,
481            Some(request_params),
482        )
483        .await
484    }
485
486    /// Create a take-profit order.
487    ///
488    /// # Arguments
489    ///
490    /// * `symbol` - Trading pair symbol.
491    /// * `side` - Order side (Buy/Sell).
492    /// * `amount` - Order quantity.
493    /// * `take_profit_price` - Take-profit trigger price.
494    /// * `price` - Optional limit price (if `None`, creates market take-profit order).
495    /// * `params` - Optional additional parameters.
496    ///
497    /// # Returns
498    ///
499    /// Returns the created take-profit [`Order`].
500    ///
501    /// # Errors
502    ///
503    /// Returns an error if authentication fails or the API request fails.
504    pub async fn create_take_profit_order(
505        &self,
506        symbol: &str,
507        side: OrderSide,
508        amount: f64,
509        take_profit_price: f64,
510        price: Option<f64>,
511        params: Option<HashMap<String, String>>,
512    ) -> Result<Order> {
513        let mut request_params = params.unwrap_or_default();
514
515        request_params.insert("stopPrice".to_string(), take_profit_price.to_string());
516
517        let order_type = if price.is_some() {
518            OrderType::TakeProfitLimit
519        } else {
520            OrderType::TakeProfit
521        };
522
523        self.create_order(
524            symbol,
525            order_type,
526            side,
527            amount,
528            price,
529            Some(request_params),
530        )
531        .await
532    }
533
534    /// Create a trailing stop order.
535    ///
536    /// # Arguments
537    ///
538    /// * `symbol` - Trading pair symbol.
539    /// * `side` - Order side (Buy/Sell).
540    /// * `amount` - Order quantity.
541    /// * `trailing_percent` - Trailing percentage (e.g., 2.0 for 2%).
542    /// * `activation_price` - Optional activation price (not supported for spot markets).
543    /// * `params` - Optional additional parameters.
544    ///
545    /// # Returns
546    ///
547    /// Returns the created trailing stop [`Order`].
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if authentication fails or the API request fails.
552    pub async fn create_trailing_stop_order(
553        &self,
554        symbol: &str,
555        side: OrderSide,
556        amount: f64,
557        trailing_percent: f64,
558        activation_price: Option<f64>,
559        params: Option<HashMap<String, String>>,
560    ) -> Result<Order> {
561        let mut request_params = params.unwrap_or_default();
562
563        request_params.insert("trailingPercent".to_string(), trailing_percent.to_string());
564
565        if let Some(activation) = activation_price {
566            request_params.insert("activationPrice".to_string(), activation.to_string());
567        }
568
569        self.create_order(
570            symbol,
571            OrderType::TrailingStop,
572            side,
573            amount,
574            None,
575            Some(request_params),
576        )
577        .await
578    }
579}