rusty_ems/exchanges/
binance_rest.rs

1use crate::error::EMSError;
2use crate::error::batch_errors::{BatchResult, ErrorClassification, OrderResult};
3use crate::error::exchange_errors::{extract_rate_limit_info_detailed, parse_binance_error};
4use anyhow::{Result, anyhow};
5use futures::future;
6use log;
7use quanta::Clock;
8use reqwest::Client;
9use rusty_common::SmartString;
10use rusty_common::auth::exchanges::binance::BinanceAuth;
11use rusty_common::collections::FxHashMap;
12use rusty_model::trading_order::Order;
13use serde::{Deserialize, Serialize};
14use simd_json::prelude::{ValueAsScalar, ValueObjectAccess};
15use smallvec::SmallVec;
16use std::sync::Arc;
17
18/// Parameters required to place an order on Binance
19/// Uses owned strings to avoid lifetime issues with Clone trait
20#[derive(Clone)]
21pub struct PlaceOrderParams {
22    /// Trading symbol (e.g., "BTCUSDT")
23    pub symbol: SmartString,
24    /// Order side - "BUY" or "SELL"
25    pub side: SmartString, // "BUY" or "SELL"
26    /// Order type - "LIMIT", "MARKET", etc.
27    pub order_type: SmartString, // "LIMIT", "MARKET", etc.
28    /// Time in force - "GTC", "IOC", "FOK"
29    pub time_in_force: SmartString, // "GTC", "IOC", "FOK"
30    /// Order quantity as a string
31    pub quantity: SmartString,
32    /// Order price as a string
33    pub price: SmartString,
34    /// Client-generated order ID for tracking
35    pub client_order_id: SmartString,
36}
37
38/// Parameters for OCO (One-Cancels-Other) orders
39pub struct OcoOrderParams<'a> {
40    /// Trading symbol (e.g., "BTCUSDT")
41    pub symbol: &'a str,
42    /// Order side - "BUY" or "SELL"
43    pub side: &'a str,
44    /// Order quantity as a string
45    pub quantity: &'a str,
46    /// Order type for above price - "STOP_LOSS_LIMIT", "LIMIT_MAKER", etc.
47    pub above_type: &'a str, // "STOP_LOSS_LIMIT", "LIMIT_MAKER", etc.
48    /// Order type for below price - "STOP_LOSS", "STOP_LOSS_LIMIT", etc.
49    pub below_type: &'a str, // "STOP_LOSS", "STOP_LOSS_LIMIT", etc.
50    /// Price for above order
51    pub above_price: Option<&'a str>,
52    /// Stop price for above order
53    pub above_stop_price: Option<&'a str>,
54    /// Price for below order
55    pub below_price: Option<&'a str>,
56    /// Stop price for below order
57    pub below_stop_price: Option<&'a str>,
58    /// Client-generated list order ID for tracking
59    pub list_client_order_id: Option<&'a str>,
60}
61
62/// Parameters for SOR (Smart Order Routing) orders
63pub struct SorOrderParams<'a> {
64    /// Trading symbol (e.g., "BTCUSDT")
65    pub symbol: &'a str,
66    /// Order side - "BUY" or "SELL"
67    pub side: &'a str,
68    /// Order type - "LIMIT" or "MARKET"
69    pub order_type: &'a str, // "LIMIT" or "MARKET"
70    /// Order quantity as a string
71    pub quantity: &'a str,
72    /// Order price (required for LIMIT orders)
73    pub price: Option<&'a str>, // Required for LIMIT orders
74    /// Time in force specification
75    pub time_in_force: Option<&'a str>,
76    /// Client-generated order ID for tracking
77    pub client_order_id: Option<&'a str>,
78    /// Strategy identifier for SOR routing
79    pub strategy_id: Option<i64>,
80    /// Strategy type for SOR routing
81    pub strategy_type: Option<i32>,
82}
83
84/// Parameters for batch order operations
85pub struct BatchOrderParams {
86    /// Vector of order parameters for batch processing
87    pub orders: Vec<PlaceOrderParams>,
88}
89
90/// Parameters for native Binance batch order operations (max 5 orders)
91#[derive(Debug, Clone, Serialize)]
92pub struct NativeBatchOrderParams {
93    #[serde(rename = "batchOrders")]
94    /// JSON string containing the array of orders for batch processing
95    pub batch_orders: String, // JSON string of order array
96}
97
98/// Individual order in a native batch request
99#[derive(Debug, Clone, Serialize)]
100pub struct BinanceNativeOrder {
101    /// Trading symbol (e.g., "BTCUSDT")
102    pub symbol: String,
103    /// Order side - "BUY" or "SELL"
104    pub side: String,
105    /// Order type - "LIMIT", "MARKET", etc.
106    #[serde(rename = "type")]
107    pub order_type: String,
108    /// Time in force specification - "GTC", "IOC", "FOK"
109    #[serde(rename = "timeInForce")]
110    pub time_in_force: String,
111    /// Order quantity as a string
112    pub quantity: String,
113    /// Order price (optional for market orders)
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub price: Option<String>,
116    /// Client-generated order ID for tracking
117    #[serde(rename = "newClientOrderId")]
118    pub client_order_id: String,
119}
120
121/// Result for individual order in batch response
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct BatchOrderResult {
124    #[serde(flatten)]
125    /// Order response data if the order was successful
126    pub order: Option<BinanceOrderResponse>,
127    /// Error code if the order failed
128    pub code: Option<i32>,
129    /// Error message if the order failed
130    pub msg: Option<SmartString>,
131}
132
133/// Response from native batch order API
134type BinanceBatchOrderResponse = Vec<BatchOrderResult>;
135
136/// Binance REST client for making authenticated API calls
137#[derive(Debug, Clone)]
138pub struct BinanceRestClient {
139    /// Authentication handler
140    auth: Arc<BinanceAuth>,
141
142    /// HTTP client for REST API calls
143    client: Client,
144
145    /// Base URL for Binance API
146    api_url: SmartString,
147}
148
149/// Order response from Binance
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct BinanceOrderResponse {
152    /// Trading symbol (e.g., "BTCUSDT")
153    pub symbol: SmartString,
154    #[serde(rename = "orderId")]
155    /// Unique order ID assigned by Binance
156    pub order_id: u64,
157    #[serde(rename = "clientOrderId")]
158    /// Client-generated order ID for tracking
159    pub client_order_id: SmartString,
160    #[serde(rename = "transactTime")]
161    /// Transaction timestamp from Binance
162    pub transaction_time: u64,
163    /// Current order status (NEW, PARTIALLY_FILLED, FILLED, CANCELED, etc.)
164    pub status: SmartString,
165    #[serde(rename = "executedQty", default)]
166    /// Quantity of the order that has been executed
167    pub executed_qty: SmartString,
168    #[serde(rename = "origQty")]
169    /// Original quantity of the order
170    pub original_qty: SmartString,
171    /// Order price as a string
172    pub price: SmartString,
173    #[serde(rename = "timeInForce")]
174    /// Time in force specification (GTC, IOC, FOK)
175    pub time_in_force: SmartString,
176    #[serde(rename = "type")]
177    /// Order type (LIMIT, MARKET, STOP_LOSS, etc.)
178    pub order_type: SmartString,
179    /// Order side (BUY or SELL)
180    pub side: SmartString,
181}
182
183/// Error response from Binance
184#[derive(Debug, Serialize, Deserialize)]
185pub struct BinanceError {
186    /// Binance error code
187    pub code: i32,
188    /// Error message description
189    pub msg: SmartString,
190}
191
192/// Symbol information from Binance
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct BinanceSymbolInfo {
195    /// Trading symbol (e.g., "BTCUSDT")
196    pub symbol: SmartString,
197    /// Trading status (TRADING, HALT, BREAK, etc.)
198    pub status: SmartString,
199    #[serde(rename = "baseAsset")]
200    /// Base asset symbol (e.g., "BTC")
201    pub base_asset: SmartString,
202    #[serde(rename = "quoteAsset")]
203    /// Quote asset symbol (e.g., "USDT")
204    pub quote_asset: SmartString,
205}
206
207/// SOR (Smart Order Routing) configuration entry
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct BinanceSorConfig {
210    #[serde(rename = "baseAsset")]
211    /// Base asset for SOR configuration
212    pub base_asset: SmartString,
213    /// List of symbols available for SOR routing
214    pub symbols: Vec<SmartString>,
215}
216
217/// Exchange information from Binance
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct BinanceExchangeInfo {
220    /// Exchange timezone
221    pub timezone: SmartString,
222    #[serde(rename = "serverTime")]
223    /// Current server time in milliseconds
224    pub server_time: u64,
225    /// List of trading symbols and their information
226    pub symbols: Vec<BinanceSymbolInfo>,
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    /// SOR (Smart Order Routing) configurations if available
229    pub sors: Option<Vec<BinanceSorConfig>>,
230}
231
232/// OCO Order List response
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct BinanceOcoResponse {
235    #[serde(rename = "orderListId")]
236    /// Unique order list ID assigned by Binance
237    pub order_list_id: i64,
238    #[serde(rename = "contingencyType")]
239    /// Type of contingency order (OCO)
240    pub contingency_type: SmartString,
241    #[serde(rename = "listStatusType")]
242    /// Status type of the order list
243    pub list_status_type: SmartString,
244    #[serde(rename = "listOrderStatus")]
245    /// Current status of the order list
246    pub list_order_status: SmartString,
247    #[serde(rename = "listClientOrderId")]
248    /// Client-generated order list ID for tracking
249    pub list_client_order_id: SmartString,
250    #[serde(rename = "transactionTime")]
251    /// Transaction timestamp from Binance
252    pub transaction_time: u64,
253    /// Trading symbol for the OCO order
254    pub symbol: SmartString,
255    /// List of order information in the OCO
256    pub orders: Vec<BinanceOrderInfo>,
257    #[serde(rename = "orderReports")]
258    /// Detailed order reports for each order in the OCO
259    pub order_reports: Vec<BinanceOrderResponse>,
260}
261
262/// Order information in OCO response
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct BinanceOrderInfo {
265    /// Trading symbol (e.g., "BTCUSDT")
266    pub symbol: SmartString,
267    #[serde(rename = "orderId")]
268    /// Unique order ID assigned by Binance
269    pub order_id: u64,
270    #[serde(rename = "clientOrderId")]
271    /// Client-generated order ID for tracking
272    pub client_order_id: SmartString,
273}
274
275/// SOR Order response
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct BinanceSorResponse {
278    /// Trading symbol (e.g., "BTCUSDT")
279    pub symbol: SmartString,
280    #[serde(rename = "orderId")]
281    /// Unique order ID assigned by Binance
282    pub order_id: u64,
283    #[serde(rename = "clientOrderId")]
284    /// Client-generated order ID for tracking
285    pub client_order_id: SmartString,
286    #[serde(rename = "transactTime")]
287    /// Transaction timestamp from Binance
288    pub transaction_time: u64,
289    /// Current order status (NEW, PARTIALLY_FILLED, FILLED, etc.)
290    pub status: SmartString,
291    #[serde(rename = "executedQty")]
292    /// Quantity of the order that has been executed
293    pub executed_qty: SmartString,
294    #[serde(rename = "origQty")]
295    /// Original quantity of the order
296    pub original_qty: SmartString,
297    /// Order price as a string
298    pub price: SmartString,
299    #[serde(rename = "workingFloor")]
300    /// Trading floor where the order is being processed
301    pub working_floor: SmartString,
302    #[serde(rename = "usedSor")]
303    /// Whether Smart Order Routing was used for this order
304    pub used_sor: bool,
305}
306
307/// Amended order response
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct BinanceAmendResponse {
310    /// Trading symbol (e.g., "BTCUSDT")
311    pub symbol: SmartString,
312    #[serde(rename = "orderId")]
313    /// Unique order ID assigned by Binance
314    pub order_id: u64,
315    #[serde(rename = "clientOrderId")]
316    /// Client-generated order ID for tracking
317    pub client_order_id: SmartString,
318    #[serde(rename = "transactTime")]
319    /// Transaction timestamp from Binance
320    pub transaction_time: u64,
321    /// Current order status after amendment
322    pub status: SmartString,
323    #[serde(rename = "executedQty")]
324    /// Quantity of the order that has been executed
325    pub executed_qty: SmartString,
326    #[serde(rename = "origQty")]
327    /// Original quantity of the order after amendment
328    pub original_qty: SmartString,
329    /// Order price as a string
330    pub price: SmartString,
331    #[serde(rename = "type")]
332    /// Order type (LIMIT, MARKET, etc.)
333    pub order_type: SmartString,
334    /// Order side (BUY or SELL)
335    pub side: SmartString,
336}
337
338impl BinanceRestClient {
339    /// Create a new Binance REST client with HMAC authentication
340    #[must_use]
341    pub fn new_hmac(api_key: SmartString, secret_key: SmartString) -> Self {
342        Self {
343            auth: Arc::new(BinanceAuth::new_hmac(api_key, secret_key)),
344            client: Client::new(),
345            api_url: "https://api.binance.com".into(),
346        }
347    }
348
349    /// Create a new Binance REST client with Ed25519 authentication
350    pub fn new_ed25519(api_key: SmartString, private_key: SmartString) -> Result<Self> {
351        let auth = BinanceAuth::new_ed25519(api_key, private_key)
352            .map_err(|e| anyhow!("Failed to create Ed25519 auth: {}", e))?;
353
354        Ok(Self {
355            auth: Arc::new(auth),
356            client: Client::new(),
357            api_url: "https://api.binance.com".into(),
358        })
359    }
360
361    /// Create a new Binance REST client from existing `BinanceAuth`
362    #[must_use]
363    pub fn new_with_auth(auth: BinanceAuth) -> Self {
364        Self {
365            auth: Arc::new(auth),
366            client: Client::new(),
367            api_url: "https://api.binance.com".into(),
368        }
369    }
370
371    /// Set a custom API URL (for testing or using testnet)
372    #[must_use]
373    pub fn with_api_url(mut self, api_url: SmartString) -> Self {
374        self.api_url = api_url;
375        self
376    }
377
378    /// Generate headers for authenticated requests
379    fn generate_headers(
380        &self,
381        method: &str,
382        path: &str,
383        body: Option<&str>,
384    ) -> Result<reqwest::header::HeaderMap> {
385        let auth_headers = self
386            .auth
387            .generate_headers(method, path, None, body)
388            .map_err(|e| anyhow!("Auth header generation failed: {}", e))?;
389
390        let mut headers = reqwest::header::HeaderMap::new();
391
392        for (key, value) in auth_headers {
393            let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())
394                .map_err(|e| anyhow!("Invalid header name: {}", e))?;
395            let header_value = reqwest::header::HeaderValue::from_str(&value)
396                .map_err(|e| anyhow!("Invalid header value: {}", e))?;
397            headers.insert(header_name, header_value);
398        }
399
400        Ok(headers)
401    }
402
403    /// Place a new order on Binance
404    pub async fn place_order(&self, params: PlaceOrderParams) -> Result<BinanceOrderResponse> {
405        let request_params = [
406            ("symbol", params.symbol.as_str()),
407            ("side", params.side.as_str()),
408            ("type", params.order_type.as_str()),
409            ("timeInForce", params.time_in_force.as_str()),
410            ("quantity", params.quantity.as_str()),
411            ("price", params.price.as_str()),
412            ("newClientOrderId", params.client_order_id.as_str()),
413        ];
414
415        // Generate signed query string using the improved auth system
416        let signed_query = self
417            .auth
418            .generate_signed_query_string(Some(&request_params))
419            .map_err(|e| anyhow!("Auth error: {}", e))?;
420
421        let url = format!("{}/api/v3/order", self.api_url);
422
423        // Generate headers using the improved auth system
424        let headers = self.generate_headers("POST", "/api/v3/order", None)?;
425
426        let response = self
427            .client
428            .post(&url)
429            .headers(headers)
430            .body(signed_query.to_string())
431            .send()
432            .await?;
433
434        self.handle_response::<BinanceOrderResponse>(response).await
435    }
436
437    /// Cancel an existing order on Binance
438    pub async fn cancel_order(
439        &self,
440        symbol: &str,
441        client_order_id: &str,
442    ) -> Result<BinanceOrderResponse> {
443        let request_params = [("symbol", symbol), ("origClientOrderId", client_order_id)];
444
445        // Generate signed query string using the improved auth system
446        let signed_query = self
447            .auth
448            .generate_signed_query_string(Some(&request_params))
449            .map_err(|e| anyhow!("Auth error: {}", e))?;
450
451        let url = format!("{}/api/v3/order", self.api_url);
452
453        // Generate headers using the improved auth system
454        let headers = self.generate_headers("DELETE", "/api/v3/order", None)?;
455
456        let response = self
457            .client
458            .delete(&url)
459            .headers(headers)
460            .body(signed_query.to_string())
461            .send()
462            .await?;
463
464        self.handle_response::<BinanceOrderResponse>(response).await
465    }
466
467    /// Get order status from Binance
468    pub async fn get_order_status(
469        &self,
470        symbol: &str,
471        client_order_id: &str,
472    ) -> Result<BinanceOrderResponse> {
473        let request_params = [("symbol", symbol), ("origClientOrderId", client_order_id)];
474
475        // Generate signed query string using the improved auth system
476        let signed_query = self
477            .auth
478            .generate_signed_query_string(Some(&request_params))
479            .map_err(|e| anyhow!("Auth error: {}", e))?;
480
481        let url = format!("{}/api/v3/order?{}", self.api_url, signed_query);
482
483        // Generate headers using the improved auth system
484        let headers = self.generate_headers("GET", "/api/v3/order", None)?;
485
486        let response = self.client.get(&url).headers(headers).send().await?;
487
488        self.handle_response::<BinanceOrderResponse>(response).await
489    }
490
491    /// Cancel all open orders for a specific symbol
492    pub async fn cancel_all_orders(&self, symbol: &str) -> Result<Vec<BinanceOrderResponse>> {
493        let request_params = [("symbol", symbol)];
494
495        // Generate signed query string using the improved auth system
496        let signed_query = self
497            .auth
498            .generate_signed_query_string(Some(&request_params))
499            .map_err(|e| anyhow!("Auth error: {}", e))?;
500
501        let url = format!("{}/api/v3/openOrders", self.api_url);
502
503        // Generate headers using the improved auth system
504        let headers = self.generate_headers("DELETE", "/api/v3/openOrders", None)?;
505
506        let response = self
507            .client
508            .delete(&url)
509            .headers(headers)
510            .body(signed_query.to_string())
511            .send()
512            .await?;
513
514        self.handle_response::<Vec<BinanceOrderResponse>>(response)
515            .await
516    }
517
518    /// Get exchange information (public endpoint, no authentication required)
519    pub async fn get_exchange_info(&self) -> Result<BinanceExchangeInfo> {
520        let url = format!("{}/api/v3/exchangeInfo", self.api_url);
521        let response = self.client.get(&url).send().await?;
522
523        self.handle_response::<BinanceExchangeInfo>(response).await
524    }
525
526    /// Create a new listen key for user data stream
527    pub async fn create_listen_key(&self) -> Result<String> {
528        let url = format!("{}/api/v3/userDataStream", self.api_url);
529
530        // Generate headers using the improved auth system
531        let headers = self.generate_headers("POST", "/api/v3/userDataStream", None)?;
532
533        let response = self.client.post(&url).headers(headers).send().await?;
534
535        let response_body: simd_json::OwnedValue = self.handle_response(response).await?;
536        let listen_key = response_body
537            .get("listenKey")
538            .and_then(|v| v.as_str())
539            .ok_or_else(|| anyhow!("Invalid listen key response"))?
540            .to_string();
541
542        Ok(listen_key)
543    }
544
545    /// Refresh existing listen key to keep it active
546    pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
547        let url = format!("{}/api/v3/userDataStream", self.api_url);
548
549        // Generate headers using the improved auth system
550        let headers = self.generate_headers("PUT", "/api/v3/userDataStream", None)?;
551
552        let response = self
553            .client
554            .put(&url)
555            .headers(headers)
556            .query(&[("listenKey", listen_key)])
557            .send()
558            .await?;
559
560        self.handle_response::<()>(response).await?;
561        Ok(())
562    }
563
564    /// Delete a listen key
565    pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
566        let url = format!("{}/api/v3/userDataStream", self.api_url);
567
568        // Generate headers using the improved auth system
569        let headers = self.generate_headers("DELETE", "/api/v3/userDataStream", None)?;
570
571        let response = self
572            .client
573            .delete(&url)
574            .headers(headers)
575            .query(&[("listenKey", listen_key)])
576            .send()
577            .await?;
578
579        self.handle_response::<()>(response).await?;
580        Ok(())
581    }
582
583    /// Place an OCO (One-Cancels-Other) order
584    pub async fn place_oco_order(&self, params: OcoOrderParams<'_>) -> Result<BinanceOcoResponse> {
585        let mut request_params = vec![
586            ("symbol", params.symbol),
587            ("side", params.side),
588            ("quantity", params.quantity),
589            ("aboveType", params.above_type),
590            ("belowType", params.below_type),
591        ];
592
593        if let Some(above_price) = params.above_price {
594            request_params.push(("abovePrice", above_price));
595        }
596        if let Some(above_stop_price) = params.above_stop_price {
597            request_params.push(("aboveStopPrice", above_stop_price));
598        }
599        if let Some(below_price) = params.below_price {
600            request_params.push(("belowPrice", below_price));
601        }
602        if let Some(below_stop_price) = params.below_stop_price {
603            request_params.push(("belowStopPrice", below_stop_price));
604        }
605        if let Some(list_client_order_id) = params.list_client_order_id {
606            request_params.push(("listClientOrderId", list_client_order_id));
607        }
608
609        let signed_query = self
610            .auth
611            .generate_signed_query_string(Some(&request_params))
612            .map_err(|e| anyhow!("Auth error: {}", e))?;
613
614        let url = format!("{}/api/v3/orderList/oco", self.api_url);
615        let headers = self.generate_headers("POST", "/api/v3/orderList/oco", None)?;
616
617        let response = self
618            .client
619            .post(&url)
620            .headers(headers)
621            .body(signed_query.to_string())
622            .send()
623            .await?;
624
625        self.handle_response::<BinanceOcoResponse>(response).await
626    }
627
628    /// Place a SOR (Smart Order Routing) order
629    pub async fn place_sor_order(&self, params: SorOrderParams<'_>) -> Result<BinanceSorResponse> {
630        let mut request_params = vec![
631            ("symbol", params.symbol),
632            ("side", params.side),
633            ("type", params.order_type),
634            ("quantity", params.quantity),
635        ];
636
637        if let Some(price) = params.price {
638            request_params.push(("price", price));
639        }
640        if let Some(time_in_force) = params.time_in_force {
641            request_params.push(("timeInForce", time_in_force));
642        }
643        if let Some(client_order_id) = params.client_order_id {
644            request_params.push(("newClientOrderId", client_order_id));
645        }
646        let strategy_id_str;
647        if let Some(strategy_id) = params.strategy_id {
648            strategy_id_str = strategy_id.to_string();
649            request_params.push(("strategyId", &strategy_id_str));
650        }
651        let strategy_type_str;
652        if let Some(strategy_type) = params.strategy_type {
653            strategy_type_str = strategy_type.to_string();
654            request_params.push(("strategyType", &strategy_type_str));
655        }
656
657        let signed_query = self
658            .auth
659            .generate_signed_query_string(Some(&request_params))
660            .map_err(|e| anyhow!("Auth error: {}", e))?;
661
662        let url = format!("{}/api/v3/sor/order", self.api_url);
663        let headers = self.generate_headers("POST", "/api/v3/sor/order", None)?;
664
665        let response = self
666            .client
667            .post(&url)
668            .headers(headers)
669            .body(signed_query.to_string())
670            .send()
671            .await?;
672
673        self.handle_response::<BinanceSorResponse>(response).await
674    }
675
676    /// Place multiple orders using native Binance batch API (max 5 orders)
677    pub async fn place_native_batch_orders(
678        &self,
679        orders: Vec<BinanceNativeOrder>,
680    ) -> Result<BinanceBatchOrderResponse> {
681        if orders.is_empty() {
682            return Ok(Vec::new());
683        }
684
685        if orders.len() > 5 {
686            return Err(anyhow!(
687                "Native batch orders limited to 5 orders, got {}",
688                orders.len()
689            ));
690        }
691
692        // Convert orders to JSON string
693        let batch_orders_json = simd_json::to_string(&orders)
694            .map_err(|e| anyhow!("Failed to serialize batch orders: {}", e))?;
695
696        let request_params = [("batchOrders", batch_orders_json.as_str())];
697
698        // Generate signed query string
699        let signed_query = self
700            .auth
701            .generate_signed_query_string(Some(&request_params))
702            .map_err(|e| anyhow!("Auth error: {}", e))?;
703
704        let url = format!("{}/fapi/v1/batchOrders", self.api_url);
705        let headers = self.generate_headers("POST", "/fapi/v1/batchOrders", None)?;
706
707        let response = self
708            .client
709            .post(&url)
710            .headers(headers)
711            .body(signed_query.to_string())
712            .send()
713            .await?;
714
715        self.handle_response::<BinanceBatchOrderResponse>(response)
716            .await
717    }
718
719    /// Place multiple orders in a batch (uses native API for ≤5 orders, concurrent for larger batches)
720    pub async fn place_batch_orders(
721        &self,
722        params: BatchOrderParams,
723    ) -> Result<Vec<Result<BinanceOrderResponse>>> {
724        if params.orders.is_empty() {
725            return Ok(Vec::new());
726        }
727
728        // Try native batch API for small batches (≤5 orders)
729        if params.orders.len() <= 5 {
730            // Convert to native batch format
731            let native_orders: Vec<BinanceNativeOrder> = params
732                .orders
733                .iter()
734                .map(|order| BinanceNativeOrder {
735                    symbol: order.symbol.to_string(),
736                    side: order.side.to_string(),
737                    order_type: order.order_type.to_string(),
738                    time_in_force: order.time_in_force.to_string(),
739                    quantity: order.quantity.to_string(),
740                    price: if order.price != "0" && !order.price.is_empty() {
741                        Some(order.price.to_string())
742                    } else {
743                        None
744                    },
745                    client_order_id: order.client_order_id.to_string(),
746                })
747                .collect();
748
749            // Try native batch API first
750            match self.place_native_batch_orders(native_orders).await {
751                Ok(batch_response) => {
752                    // Convert batch response to expected format
753                    let results: Vec<Result<BinanceOrderResponse>> = batch_response
754                        .into_iter()
755                        .map(|result| {
756                            if let Some(order) = result.order {
757                                Ok(order)
758                            } else {
759                                let error_msg = result.msg.unwrap_or_else(|| {
760                                    format!("Batch order failed with code: {:?}", result.code)
761                                        .into()
762                                });
763                                Err(anyhow!("Batch order error: {}", error_msg))
764                            }
765                        })
766                        .collect();
767                    return Ok(results);
768                }
769                Err(e) => {
770                    // Only fallback for specific errors, not transport/auth issues
771                    // Check if this is a transport-level error that would affect all requests
772                    let is_transport_error = if let Some(ems_error) = e.downcast_ref::<EMSError>() {
773                        use crate::error::batch_errors::ErrorClassification;
774                        ems_error.is_transport_error()
775                    } else if let Some(req_err) = e.downcast_ref::<reqwest::Error>() {
776                        // Check reqwest errors directly
777                        req_err.is_timeout()
778                            || req_err.is_connect()
779                            || req_err.status().is_some_and(|s| s.as_u16() == 429) // Rate limit
780                    } else {
781                        // For other errors, check the error string for transport-related patterns
782                        let error_str = e.to_string().to_lowercase();
783                        error_str.contains("connection")
784                            || error_str.contains("timeout")
785                            || error_str.contains("network")
786                            || error_str.contains("refused")
787                            || error_str.contains("unreachable")
788                    };
789
790                    if is_transport_error {
791                        // Propagate transport errors - fallback won't help
792                        log::error!("Native batch API failed with transport error: {e}");
793                        return Err(e);
794                    }
795
796                    // Fall back to concurrent execution for API-specific errors
797                    // (e.g., batch endpoint not available, malformed batch request)
798                    log::warn!(
799                        "Native batch API failed with non-transport error, falling back to concurrent execution: {e}"
800                    );
801                }
802            }
803        }
804
805        // Fall back to concurrent execution for large batches or if native API fails
806        let order_futures: Vec<_> = params
807            .orders
808            .into_iter()
809            .map(|order_params| {
810                let self_ref = self;
811                async move { self_ref.place_order(order_params).await }
812            })
813            .collect();
814
815        // Execute all orders concurrently
816        let results = future::join_all(order_futures).await;
817        Ok(results)
818    }
819
820    /// Place multiple orders with improved error handling that separates transport from per-order errors
821    pub async fn place_batch_orders_improved(
822        &self,
823        orders: SmallVec<[Order; 8]>,
824    ) -> Result<BatchResult<BinanceOrderResponse>> {
825        let clock = Clock::new();
826        let start_time = clock.raw();
827
828        if orders.is_empty() {
829            return Ok(BatchResult::success(
830                FxHashMap::default(),
831                clock.raw() - start_time,
832            ));
833        }
834
835        // Convert orders to Binance format with owned strings to avoid lifetime issues
836        let order_params: Vec<(String, String, String, String, String, String, String)> = orders
837            .iter()
838            .map(|order| {
839                let symbol = order.symbol.to_string();
840                let side = match order.side {
841                    rusty_model::enums::OrderSide::Buy => "BUY".to_string(),
842                    rusty_model::enums::OrderSide::Sell => "SELL".to_string(),
843                };
844                let order_type = match order.order_type {
845                    rusty_model::enums::OrderType::Market => "MARKET".to_string(),
846                    rusty_model::enums::OrderType::Limit => "LIMIT".to_string(),
847                    _ => "LIMIT".to_string(), // Default to limit
848                };
849                let time_in_force = "GTC".to_string(); // Good Till Cancelled
850                let quantity = order.quantity.to_string();
851                let price = order
852                    .price
853                    .map_or_else(|| "0".to_string(), |p| p.to_string());
854                let client_order_id = order.id.into_uuid().to_string();
855                (
856                    symbol,
857                    side,
858                    order_type,
859                    time_in_force,
860                    quantity,
861                    price,
862                    client_order_id,
863                )
864            })
865            .collect();
866
867        // Convert to PlaceOrderParams with owned strings
868        let order_params_refs: Vec<PlaceOrderParams> = order_params
869            .iter()
870            .map(
871                |(symbol, side, order_type, time_in_force, quantity, price, client_order_id)| {
872                    PlaceOrderParams {
873                        symbol: symbol.clone().into(),
874                        side: side.clone().into(),
875                        order_type: order_type.clone().into(),
876                        time_in_force: time_in_force.clone().into(),
877                        quantity: quantity.clone().into(),
878                        price: price.clone().into(),
879                        client_order_id: client_order_id.clone().into(),
880                    }
881                },
882            )
883            .collect();
884
885        // Try the existing batch order method
886        match self
887            .place_batch_orders(BatchOrderParams {
888                orders: order_params_refs,
889            })
890            .await
891        {
892            Ok(results) => {
893                // Process results and classify errors
894                let mut order_results = FxHashMap::default();
895                let mut has_transport_error = false;
896                let mut transport_error = None;
897
898                for (i, result) in results.into_iter().enumerate() {
899                    let order_id = orders[i].id.into_uuid().to_string().into();
900
901                    match result {
902                        Ok(response) => {
903                            order_results.insert(order_id, OrderResult::success(response));
904                        }
905                        Err(e) => {
906                            // Convert anyhow error to EMSError
907                            let ems_error = EMSError::from(e);
908
909                            // Check if this is a transport error that affects all orders
910                            if ems_error.is_transport_error() && !has_transport_error {
911                                has_transport_error = true;
912                                transport_error = Some(ems_error.clone());
913                            }
914
915                            order_results.insert(
916                                order_id,
917                                OrderResult::failed(ems_error, orders[i].clone()),
918                            );
919                        }
920                    }
921                }
922
923                let processing_time = clock.raw() - start_time;
924
925                // If we detected a transport error, treat the entire batch as a transport failure
926                if has_transport_error {
927                    return Ok(BatchResult::transport_failure(
928                        transport_error.unwrap(),
929                        orders.len(),
930                        processing_time,
931                    ));
932                }
933
934                // Determine batch status based on results
935                let successful_count = order_results
936                    .values()
937                    .filter(|result| result.is_success())
938                    .count();
939
940                if successful_count == orders.len() {
941                    Ok(BatchResult::success(order_results, processing_time))
942                } else if successful_count > 0 {
943                    Ok(BatchResult::partial_success(order_results, processing_time))
944                } else {
945                    Ok(BatchResult::all_failed(order_results, processing_time))
946                }
947            }
948            Err(e) => {
949                // The entire batch failed at the transport level
950                let ems_error = EMSError::from(e);
951                let processing_time = clock.raw() - start_time;
952
953                if ems_error.is_transport_error() {
954                    Ok(BatchResult::transport_failure(
955                        ems_error,
956                        orders.len(),
957                        processing_time,
958                    ))
959                } else {
960                    // Unexpected error type, treat as transport failure
961                    Ok(BatchResult::transport_failure(
962                        EMSError::internal(format!("Unexpected batch error: {ems_error}")),
963                        orders.len(),
964                        processing_time,
965                    ))
966                }
967            }
968        }
969    }
970
971    /// Cancel an order list (OCO, OTO, etc.)
972    pub async fn cancel_order_list(
973        &self,
974        symbol: &str,
975        order_list_id: i64,
976    ) -> Result<BinanceOcoResponse> {
977        let request_params = [
978            ("symbol", symbol),
979            ("orderListId", &order_list_id.to_string()),
980        ];
981
982        let signed_query = self
983            .auth
984            .generate_signed_query_string(Some(&request_params))
985            .map_err(|e| anyhow!("Auth error: {}", e))?;
986
987        let url = format!("{}/api/v3/orderList", self.api_url);
988        let headers = self.generate_headers("DELETE", "/api/v3/orderList", None)?;
989
990        let response = self
991            .client
992            .delete(&url)
993            .headers(headers)
994            .body(signed_query.to_string())
995            .send()
996            .await?;
997
998        self.handle_response::<BinanceOcoResponse>(response).await
999    }
1000
1001    /// Amend an order keeping queue priority (reduce quantity only)
1002    pub async fn amend_order_keep_priority(
1003        &self,
1004        symbol: &str,
1005        client_order_id: &str,
1006        new_quantity: rust_decimal::Decimal,
1007    ) -> Result<BinanceAmendResponse> {
1008        let request_params = [
1009            ("symbol", symbol),
1010            ("origClientOrderId", client_order_id),
1011            ("newQty", &new_quantity.to_string()),
1012        ];
1013
1014        let signed_query = self
1015            .auth
1016            .generate_signed_query_string(Some(&request_params))
1017            .map_err(|e| anyhow!("Auth error: {}", e))?;
1018
1019        let url = format!("{}/api/v3/order/amend/keepPriority", self.api_url);
1020        let headers = self.generate_headers("PUT", "/api/v3/order/amend/keepPriority", None)?;
1021
1022        let response = self
1023            .client
1024            .put(&url)
1025            .headers(headers)
1026            .body(signed_query.to_string())
1027            .send()
1028            .await?;
1029
1030        self.handle_response::<BinanceAmendResponse>(response).await
1031    }
1032
1033    /// Test SOR order placement (validation only)
1034    pub async fn test_sor_order(&self, params: SorOrderParams<'_>) -> Result<()> {
1035        let mut request_params = vec![
1036            ("symbol", params.symbol),
1037            ("side", params.side),
1038            ("type", params.order_type),
1039            ("quantity", params.quantity),
1040        ];
1041
1042        if let Some(price) = params.price {
1043            request_params.push(("price", price));
1044        }
1045        if let Some(time_in_force) = params.time_in_force {
1046            request_params.push(("timeInForce", time_in_force));
1047        }
1048        if let Some(client_order_id) = params.client_order_id {
1049            request_params.push(("newClientOrderId", client_order_id));
1050        }
1051
1052        let signed_query = self
1053            .auth
1054            .generate_signed_query_string(Some(&request_params))
1055            .map_err(|e| anyhow!("Auth error: {}", e))?;
1056
1057        let url = format!("{}/api/v3/sor/order/test", self.api_url);
1058        let headers = self.generate_headers("POST", "/api/v3/sor/order/test", None)?;
1059
1060        let response = self
1061            .client
1062            .post(&url)
1063            .headers(headers)
1064            .body(signed_query.to_string())
1065            .send()
1066            .await?;
1067
1068        self.handle_response::<()>(response).await?;
1069        Ok(())
1070    }
1071
1072    /// Generate WebSocket authentication message
1073    /// This exposes the unified auth system's WebSocket functionality
1074    pub fn generate_ws_auth(&self) -> Result<SmartString> {
1075        self.auth
1076            .generate_ws_auth()
1077            .map_err(|e| anyhow!("WebSocket auth error: {}", e))
1078    }
1079
1080    /// Get SOR configuration from exchange info
1081    pub async fn get_sor_config(&self) -> Result<Vec<BinanceSorConfig>> {
1082        let exchange_info = self.get_exchange_info().await?;
1083        Ok(exchange_info.sors.unwrap_or_default())
1084    }
1085
1086    /// Get allocations for SOR trades
1087    pub async fn get_my_allocations(&self, symbol: &str) -> Result<simd_json::OwnedValue> {
1088        let request_params = [("symbol", symbol)];
1089
1090        let signed_query = self
1091            .auth
1092            .generate_signed_query_string(Some(&request_params))
1093            .map_err(|e| anyhow!("Auth error: {}", e))?;
1094
1095        let url = format!("{}/api/v3/myAllocations?{}", self.api_url, signed_query);
1096        let headers = self.generate_headers("GET", "/api/v3/myAllocations", None)?;
1097
1098        let response = self.client.get(&url).headers(headers).send().await?;
1099        self.handle_response::<simd_json::OwnedValue>(response)
1100            .await
1101    }
1102
1103    /// Centralized response handling with improved error parsing
1104    async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
1105    where
1106        T: serde::de::DeserializeOwned + 'static,
1107    {
1108        let status = response.status();
1109
1110        // Parse and log rate limit information for all responses
1111        let rate_limit_info = extract_rate_limit_info_detailed(response.headers());
1112        let summary = rate_limit_info.summary();
1113        if summary != "no_rate_limit_info" {
1114            log::trace!("[Binance] Rate limits: {summary}");
1115
1116            // Warn if approaching rate limits
1117            if rate_limit_info.is_approaching_limit() {
1118                log::warn!("[Binance] Rate limit approaching: {summary}");
1119            }
1120        }
1121
1122        if status.is_success() {
1123            // Special handling for unit type endpoints that don't return data
1124            if std::any::TypeId::of::<T>() == std::any::TypeId::of::<()>() {
1125                // Safety: We've confirmed T is (), so this transmute is safe
1126                return Ok(unsafe { std::mem::transmute_copy(&()) });
1127            }
1128
1129            let response_bytes = response.bytes().await?;
1130            let mut response_vec = response_bytes.to_vec();
1131            // SAFETY: response_vec is valid UTF-8 bytes from HTTP response
1132            let response_body: T = simd_json::from_slice(&mut response_vec)
1133                .map_err(|e| anyhow!("Failed to parse response: {}", e))?;
1134            return Ok(response_body);
1135        }
1136
1137        // Handle error responses
1138        let error_bytes = response.bytes().await?;
1139        let mut error_vec = error_bytes.to_vec();
1140
1141        // Handle rate limit exceeded with detailed information
1142        if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
1143            let retry_info = if let Some(retry_ms) = rate_limit_info.get_retry_after_ms() {
1144                format!(" (retry after {retry_ms}ms)")
1145            } else {
1146                String::new()
1147            };
1148
1149            log::warn!("[Binance] Rate limit exceeded{retry_info}. Details: {summary}");
1150        }
1151
1152        // Try to parse using centralized Binance error parser
1153        let error_result = if let Ok(json_value) =
1154            simd_json::from_slice::<simd_json::value::owned::Value>(&mut error_vec)
1155        {
1156            if let Some(ems_error) = parse_binance_error(&json_value, "Binance") {
1157                return Err(anyhow!(ems_error));
1158            }
1159            // If centralized parser doesn't handle it, try old format
1160            if let Ok(error) = simd_json::from_slice::<BinanceError>(&mut error_vec) {
1161                format!("Binance error: {} (code: {})", error.msg, error.code)
1162            } else {
1163                let error_text = String::from_utf8_lossy(&error_bytes);
1164                format!("HTTP {status} error: {error_text}")
1165            }
1166        } else {
1167            // Fallback to raw string representation for non-JSON errors
1168            let error_text = String::from_utf8_lossy(&error_bytes);
1169            format!("HTTP {status} error: {error_text}")
1170        };
1171
1172        let error_message = error_result;
1173
1174        Err(anyhow!(error_message))
1175    }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181    use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
1182    use dotenv::dotenv;
1183    use std::env;
1184
1185    #[tokio::test]
1186    async fn test_get_exchange_info() {
1187        dotenv().ok();
1188
1189        let api_key = env::var("BINANCE_API_KEY").unwrap_or_default();
1190        let secret_key = env::var("BINANCE_SECRET_KEY").unwrap_or_default();
1191
1192        if api_key.is_empty() || secret_key.is_empty() {
1193            println!("Skipping test: BINANCE_API_KEY and BINANCE_SECRET_KEY must be set");
1194            return;
1195        }
1196
1197        let client = BinanceRestClient::new_hmac(api_key.into(), secret_key.into());
1198
1199        let result = client.get_exchange_info().await;
1200        assert!(result.is_ok(), "Failed to get exchange info: {result:?}");
1201
1202        let exchange_info = result.unwrap();
1203        assert!(!exchange_info.symbols.is_empty(), "No symbols returned");
1204
1205        println!("Found {} symbols", exchange_info.symbols.len());
1206        for (i, symbol_info) in exchange_info.symbols.iter().take(5).enumerate() {
1207            println!(
1208                "  {}: {} ({}/{})",
1209                i + 1,
1210                symbol_info.symbol,
1211                symbol_info.base_asset,
1212                symbol_info.quote_asset
1213            );
1214        }
1215    }
1216
1217    #[tokio::test]
1218    async fn test_ed25519_authentication() {
1219        // Test creating client with Ed25519 authentication
1220        let api_key: SmartString = "test_api_key".into();
1221        let private_key = BASE64.encode([1u8; 32]); // Test key
1222
1223        let client = BinanceRestClient::new_ed25519(api_key, private_key.into());
1224        assert!(client.is_ok(), "Failed to create Ed25519 client");
1225
1226        let client = client.unwrap();
1227
1228        // Test WebSocket auth generation (only works with Ed25519)
1229        let ws_auth = client.generate_ws_auth();
1230        assert!(
1231            ws_auth.is_ok(),
1232            "Failed to generate WebSocket auth with Ed25519"
1233        );
1234
1235        let auth_message = ws_auth.unwrap();
1236        assert!(auth_message.contains("session.logon"));
1237        assert!(auth_message.contains("test_api_key"));
1238    }
1239
1240    #[test]
1241    fn test_header_generation() {
1242        let client = BinanceRestClient::new_hmac("test_api_key".into(), "test_secret".into());
1243
1244        // Test GET headers
1245        let headers = client.generate_headers("GET", "/api/v3/account", None);
1246        assert!(headers.is_ok());
1247        let headers = headers.unwrap();
1248        assert!(headers.contains_key("x-mbx-apikey"));
1249
1250        // Test POST headers
1251        let headers = client.generate_headers("POST", "/api/v3/order", Some("test body"));
1252        assert!(headers.is_ok());
1253        let headers = headers.unwrap();
1254        assert!(headers.contains_key("x-mbx-apikey"));
1255        assert!(headers.contains_key("content-type"));
1256    }
1257}