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#[derive(Clone)]
21pub struct PlaceOrderParams {
22 pub symbol: SmartString,
24 pub side: SmartString, pub order_type: SmartString, pub time_in_force: SmartString, pub quantity: SmartString,
32 pub price: SmartString,
34 pub client_order_id: SmartString,
36}
37
38pub struct OcoOrderParams<'a> {
40 pub symbol: &'a str,
42 pub side: &'a str,
44 pub quantity: &'a str,
46 pub above_type: &'a str, pub below_type: &'a str, pub above_price: Option<&'a str>,
52 pub above_stop_price: Option<&'a str>,
54 pub below_price: Option<&'a str>,
56 pub below_stop_price: Option<&'a str>,
58 pub list_client_order_id: Option<&'a str>,
60}
61
62pub struct SorOrderParams<'a> {
64 pub symbol: &'a str,
66 pub side: &'a str,
68 pub order_type: &'a str, pub quantity: &'a str,
72 pub price: Option<&'a str>, pub time_in_force: Option<&'a str>,
76 pub client_order_id: Option<&'a str>,
78 pub strategy_id: Option<i64>,
80 pub strategy_type: Option<i32>,
82}
83
84pub struct BatchOrderParams {
86 pub orders: Vec<PlaceOrderParams>,
88}
89
90#[derive(Debug, Clone, Serialize)]
92pub struct NativeBatchOrderParams {
93 #[serde(rename = "batchOrders")]
94 pub batch_orders: String, }
97
98#[derive(Debug, Clone, Serialize)]
100pub struct BinanceNativeOrder {
101 pub symbol: String,
103 pub side: String,
105 #[serde(rename = "type")]
107 pub order_type: String,
108 #[serde(rename = "timeInForce")]
110 pub time_in_force: String,
111 pub quantity: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub price: Option<String>,
116 #[serde(rename = "newClientOrderId")]
118 pub client_order_id: String,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct BatchOrderResult {
124 #[serde(flatten)]
125 pub order: Option<BinanceOrderResponse>,
127 pub code: Option<i32>,
129 pub msg: Option<SmartString>,
131}
132
133type BinanceBatchOrderResponse = Vec<BatchOrderResult>;
135
136#[derive(Debug, Clone)]
138pub struct BinanceRestClient {
139 auth: Arc<BinanceAuth>,
141
142 client: Client,
144
145 api_url: SmartString,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct BinanceOrderResponse {
152 pub symbol: SmartString,
154 #[serde(rename = "orderId")]
155 pub order_id: u64,
157 #[serde(rename = "clientOrderId")]
158 pub client_order_id: SmartString,
160 #[serde(rename = "transactTime")]
161 pub transaction_time: u64,
163 pub status: SmartString,
165 #[serde(rename = "executedQty", default)]
166 pub executed_qty: SmartString,
168 #[serde(rename = "origQty")]
169 pub original_qty: SmartString,
171 pub price: SmartString,
173 #[serde(rename = "timeInForce")]
174 pub time_in_force: SmartString,
176 #[serde(rename = "type")]
177 pub order_type: SmartString,
179 pub side: SmartString,
181}
182
183#[derive(Debug, Serialize, Deserialize)]
185pub struct BinanceError {
186 pub code: i32,
188 pub msg: SmartString,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct BinanceSymbolInfo {
195 pub symbol: SmartString,
197 pub status: SmartString,
199 #[serde(rename = "baseAsset")]
200 pub base_asset: SmartString,
202 #[serde(rename = "quoteAsset")]
203 pub quote_asset: SmartString,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct BinanceSorConfig {
210 #[serde(rename = "baseAsset")]
211 pub base_asset: SmartString,
213 pub symbols: Vec<SmartString>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct BinanceExchangeInfo {
220 pub timezone: SmartString,
222 #[serde(rename = "serverTime")]
223 pub server_time: u64,
225 pub symbols: Vec<BinanceSymbolInfo>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub sors: Option<Vec<BinanceSorConfig>>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct BinanceOcoResponse {
235 #[serde(rename = "orderListId")]
236 pub order_list_id: i64,
238 #[serde(rename = "contingencyType")]
239 pub contingency_type: SmartString,
241 #[serde(rename = "listStatusType")]
242 pub list_status_type: SmartString,
244 #[serde(rename = "listOrderStatus")]
245 pub list_order_status: SmartString,
247 #[serde(rename = "listClientOrderId")]
248 pub list_client_order_id: SmartString,
250 #[serde(rename = "transactionTime")]
251 pub transaction_time: u64,
253 pub symbol: SmartString,
255 pub orders: Vec<BinanceOrderInfo>,
257 #[serde(rename = "orderReports")]
258 pub order_reports: Vec<BinanceOrderResponse>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct BinanceOrderInfo {
265 pub symbol: SmartString,
267 #[serde(rename = "orderId")]
268 pub order_id: u64,
270 #[serde(rename = "clientOrderId")]
271 pub client_order_id: SmartString,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct BinanceSorResponse {
278 pub symbol: SmartString,
280 #[serde(rename = "orderId")]
281 pub order_id: u64,
283 #[serde(rename = "clientOrderId")]
284 pub client_order_id: SmartString,
286 #[serde(rename = "transactTime")]
287 pub transaction_time: u64,
289 pub status: SmartString,
291 #[serde(rename = "executedQty")]
292 pub executed_qty: SmartString,
294 #[serde(rename = "origQty")]
295 pub original_qty: SmartString,
297 pub price: SmartString,
299 #[serde(rename = "workingFloor")]
300 pub working_floor: SmartString,
302 #[serde(rename = "usedSor")]
303 pub used_sor: bool,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct BinanceAmendResponse {
310 pub symbol: SmartString,
312 #[serde(rename = "orderId")]
313 pub order_id: u64,
315 #[serde(rename = "clientOrderId")]
316 pub client_order_id: SmartString,
318 #[serde(rename = "transactTime")]
319 pub transaction_time: u64,
321 pub status: SmartString,
323 #[serde(rename = "executedQty")]
324 pub executed_qty: SmartString,
326 #[serde(rename = "origQty")]
327 pub original_qty: SmartString,
329 pub price: SmartString,
331 #[serde(rename = "type")]
332 pub order_type: SmartString,
334 pub side: SmartString,
336}
337
338impl BinanceRestClient {
339 #[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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 pub async fn cancel_all_orders(&self, symbol: &str) -> Result<Vec<BinanceOrderResponse>> {
493 let request_params = [("symbol", symbol)];
494
495 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 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 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 pub async fn create_listen_key(&self) -> Result<String> {
528 let url = format!("{}/api/v3/userDataStream", self.api_url);
529
530 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 pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
547 let url = format!("{}/api/v3/userDataStream", self.api_url);
548
549 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 pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
566 let url = format!("{}/api/v3/userDataStream", self.api_url);
567
568 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 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 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 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 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 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 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 if params.orders.len() <= 5 {
730 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 match self.place_native_batch_orders(native_orders).await {
751 Ok(batch_response) => {
752 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 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 req_err.is_timeout()
778 || req_err.is_connect()
779 || req_err.status().is_some_and(|s| s.as_u16() == 429) } else {
781 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 log::error!("Native batch API failed with transport error: {e}");
793 return Err(e);
794 }
795
796 log::warn!(
799 "Native batch API failed with non-transport error, falling back to concurrent execution: {e}"
800 );
801 }
802 }
803 }
804
805 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 let results = future::join_all(order_futures).await;
817 Ok(results)
818 }
819
820 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 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(), };
849 let time_in_force = "GTC".to_string(); 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 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 match self
887 .place_batch_orders(BatchOrderParams {
888 orders: order_params_refs,
889 })
890 .await
891 {
892 Ok(results) => {
893 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 let ems_error = EMSError::from(e);
908
909 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 has_transport_error {
927 return Ok(BatchResult::transport_failure(
928 transport_error.unwrap(),
929 orders.len(),
930 processing_time,
931 ));
932 }
933
934 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 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 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 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 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 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 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 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 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 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 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 if rate_limit_info.is_approaching_limit() {
1118 log::warn!("[Binance] Rate limit approaching: {summary}");
1119 }
1120 }
1121
1122 if status.is_success() {
1123 if std::any::TypeId::of::<T>() == std::any::TypeId::of::<()>() {
1125 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 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 let error_bytes = response.bytes().await?;
1139 let mut error_vec = error_bytes.to_vec();
1140
1141 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 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 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 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 let api_key: SmartString = "test_api_key".into();
1221 let private_key = BASE64.encode([1u8; 32]); 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 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 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 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}