1use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, sync::Arc};
35
36use ahash::AHashMap;
37use jiff::Timestamp;
38use nautilus_common::cache::InstrumentLookupError;
39use nautilus_core::{
40 collections::AtomicMap, datetime::SECONDS_IN_DAY, hex, nanos::UnixNanos, time::AtomicTime,
41};
42use nautilus_model::{
43 data::{Bar, BarType, BookOrder, TradeTick},
44 enums::{
45 AggregationSource, BarAggregation, BookType, MarketStatusAction, OrderSide, OrderType,
46 TimeInForce,
47 },
48 events::AccountState,
49 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
50 instruments::{Instrument, any::InstrumentAny},
51 orderbook::OrderBook,
52 reports::{FillReport, OrderStatusReport},
53 types::{Price, Quantity},
54};
55use nautilus_network::{
56 http::{
57 HttpClient, HttpRedirectPolicy, HttpResponse, Method, create_standard_nautilus_headers,
58 },
59 ratelimiter::quota::Quota,
60 retry::{RetryConfig, RetryError, RetryManager},
61};
62use rust_decimal::Decimal;
63use serde::{Deserialize, Serialize};
64use serde_json::Value;
65use ustr::Ustr;
66
67use super::{
68 error::{BinanceSpotHttpError, BinanceSpotHttpResult},
69 models::{
70 AvgPrice, BatchCancelResult, BatchOrderResult, BinanceAccountCommission,
71 BinanceAccountInfo, BinanceAccountRatesJson, BinanceAccountTrade, BinanceAggTrade,
72 BinanceAggTrades, BinanceBalance, BinanceCancelOpenOrdersResponse,
73 BinanceCancelOrderListResponse, BinanceCancelOrderResponse, BinanceDepth,
74 BinanceExchangeInfoJson, BinanceKline, BinanceKlines, BinanceNewOrderResponse,
75 BinanceOrderFill, BinanceOrderResponse, BinancePriceLevel, BinanceTrade, BinanceTrades,
76 BookTicker, ListenKeyResponse, NewOcoOrderListResponse, OrderListOrder, Ticker24hr,
77 TickerPrice, TradeFee,
78 },
79 parse,
80 query::{
81 AccountCommissionParams, AccountInfoParams, AccountTradesParams, AggTradesParams,
82 AllOrdersParams, AvgPriceParams, BatchCancelItem, BatchOrderItem, CancelOpenOrdersParams,
83 CancelOrderParams, CancelReplaceOrderParams, DepthParams, KlinesParams, ListenKeyParams,
84 NewOcoOrderListParams, NewOrderParams, OpenOrdersParams, QueryOrderParams, TickerParams,
85 TradeFeeParams, TradesParams,
86 },
87};
88use crate::{
89 common::{
90 consts::{
91 BINANCE_API_KEY_HEADER, BINANCE_NAUTILUS_SPOT_BROKER_ID, BINANCE_NO_SUCH_ORDER_CODE,
92 BINANCE_RETRY_AFTER_HEADER, BINANCE_SPOT_RATE_LIMITS, BINANCE_VENUE,
93 BinanceRateLimitQuota,
94 },
95 credential::SigningCredential,
96 encoder::{decode_client_order_id, encode_broker_id},
97 enums::{
98 BinanceEnvironment, BinanceOrderStatus, BinanceProductType, BinanceRateLimitInterval,
99 BinanceRateLimitType, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
100 },
101 fees::BINANCE_SPOT_FEE_DEFAULT,
102 instruments::BinanceInstrumentSelector,
103 models::BinanceErrorResponse,
104 parse::{
105 get_currency, parse_fill_report_sbe, parse_klines_to_binance_bars,
106 parse_new_order_response_sbe, parse_order_status_report_sbe,
107 parse_spot_instrument_json_with_fees, parse_spot_instrument_sbe_with_fees,
108 parse_spot_trades_sbe, should_warn_on_instrument_parse_error,
109 },
110 symbol::format_instrument_id,
111 urls::get_http_base_url,
112 },
113 config::BinanceInstrumentProviderConfig,
114 spot::{
115 enums::{
116 BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
117 order_type_to_binance_spot, time_in_force_to_binance_spot,
118 },
119 sbe::{
120 generated::symbol_status::SymbolStatus,
121 spot::{
122 ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
123 contingency_type::ContingencyType as SbeContingencyType,
124 error_response_codec::{self, ErrorResponseDecoder},
125 list_order_status::ListOrderStatus as SbeListOrderStatus,
126 list_status_type::ListStatusType as SbeListStatusType,
127 message_header_codec::MessageHeaderDecoder,
128 order_side::OrderSide as SbeOrderSide,
129 order_status::OrderStatus as SbeOrderStatus,
130 order_type::OrderType as SbeOrderType,
131 self_trade_prevention_mode::SelfTradePreventionMode as SbeSelfTradePreventionMode,
132 time_in_force::TimeInForce as SbeTimeInForce,
133 },
134 },
135 },
136};
137
138pub const SBE_SCHEMA_HEADER: &str = "3:5";
143
144use crate::common::consts::{
145 BINANCE_SAPI_PATH as SAPI_PATH, BINANCE_SPOT_API_PATH as SPOT_API_PATH,
146};
147
148const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
150
151const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
153
154struct RateLimitConfig {
155 default_quota: Option<Quota>,
156 keyed_quotas: Vec<(String, Quota)>,
157 order_keys: Vec<String>,
158}
159
160#[derive(Debug, Deserialize)]
161#[serde(rename_all = "camelCase")]
162struct SpotAccountJson {
163 #[serde(default)]
164 maker_commission: i64,
165 #[serde(default)]
166 taker_commission: i64,
167 #[serde(default)]
168 buyer_commission: i64,
169 #[serde(default)]
170 seller_commission: i64,
171 #[serde(default)]
172 commission_rates: Option<SpotCommissionRatesJson>,
173 can_trade: bool,
174 can_withdraw: bool,
175 can_deposit: bool,
176 #[serde(default)]
177 require_self_trade_prevention: bool,
178 #[serde(default)]
179 prevent_sor: bool,
180 update_time: i64,
181 account_type: String,
182 balances: Vec<SpotBalanceJson>,
183}
184
185#[derive(Debug, Deserialize)]
186struct SpotCommissionRatesJson {
187 maker: String,
188 taker: String,
189 buyer: String,
190 seller: String,
191}
192
193#[derive(Debug, Deserialize)]
194struct SpotBalanceJson {
195 asset: String,
196 free: String,
197 locked: String,
198}
199
200#[derive(Debug, Deserialize)]
201#[serde(rename_all = "camelCase")]
202struct SpotOrderJson {
203 symbol: String,
204 order_id: i64,
205 #[serde(default)]
206 order_list_id: Option<i64>,
207 client_order_id: String,
208 #[serde(default)]
209 orig_client_order_id: String,
210 #[serde(default)]
211 transact_time: i64,
212 #[serde(default)]
213 price: String,
214 #[serde(default)]
215 orig_qty: String,
216 #[serde(default)]
217 executed_qty: String,
218 #[serde(default, rename = "cummulativeQuoteQty")]
219 cummulative_quote_qty: String,
220 status: BinanceOrderStatus,
221 time_in_force: BinanceTimeInForce,
222 #[serde(rename = "type")]
223 order_type: BinanceSpotOrderType,
224 side: BinanceSide,
225 #[serde(default)]
226 stop_price: String,
227 #[serde(default)]
228 iceberg_qty: String,
229 #[serde(default)]
230 time: i64,
231 #[serde(default)]
232 update_time: i64,
233 #[serde(default)]
234 is_working: bool,
235 #[serde(default)]
236 working_time: Option<i64>,
237 #[serde(default)]
238 orig_quote_order_qty: String,
239 #[serde(default)]
240 self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
241 #[serde(default)]
242 fills: Vec<SpotOrderFillJson>,
243}
244
245#[derive(Debug, Deserialize)]
246#[serde(rename_all = "camelCase")]
247struct SpotCancelOrderListJson {
248 order_list_id: i64,
249 contingency_type: String,
250 list_status_type: String,
251 list_order_status: String,
252 list_client_order_id: String,
253 transaction_time: i64,
254 symbol: String,
255 orders: Vec<OrderListOrder>,
256 order_reports: Vec<SpotOrderJson>,
257}
258
259#[derive(Debug, Deserialize)]
260#[serde(rename_all = "camelCase")]
261struct SpotCancelReplaceJson {
262 new_order_response: SpotOrderJson,
263}
264
265#[derive(Debug, Deserialize)]
266#[serde(rename_all = "camelCase")]
267struct SpotOrderFillJson {
268 price: String,
269 qty: String,
270 commission: String,
271 commission_asset: String,
272 #[serde(default)]
273 trade_id: Option<i64>,
274}
275
276#[derive(Debug, Deserialize)]
277#[serde(rename_all = "camelCase")]
278struct SpotAccountTradeJson {
279 symbol: String,
280 id: i64,
281 order_id: i64,
282 #[serde(default)]
283 order_list_id: Option<i64>,
284 price: String,
285 qty: String,
286 quote_qty: String,
287 commission: String,
288 commission_asset: String,
289 time: i64,
290 is_buyer: bool,
291 is_maker: bool,
292 is_best_match: bool,
293}
294
295#[derive(Debug, Deserialize)]
296#[serde(rename_all = "camelCase")]
297struct SpotDepthJson {
298 last_update_id: i64,
299 bids: Vec<[String; 2]>,
300 asks: Vec<[String; 2]>,
301}
302
303#[derive(Debug, Deserialize)]
304#[serde(rename_all = "camelCase")]
305struct SpotTradeJson {
306 id: i64,
307 price: String,
308 qty: String,
309 quote_qty: String,
310 time: i64,
311 is_buyer_maker: bool,
312 is_best_match: bool,
313}
314
315#[derive(Debug, Deserialize)]
316struct SpotAggTradeJson {
317 #[serde(rename = "a")]
318 id: i64,
319 #[serde(rename = "p")]
320 price: String,
321 #[serde(rename = "q")]
322 qty: String,
323 #[serde(rename = "f")]
324 first_trade_id: i64,
325 #[serde(rename = "l")]
326 last_trade_id: i64,
327 #[serde(rename = "T")]
328 time: i64,
329 #[serde(rename = "m")]
330 is_buyer_maker: bool,
331 #[serde(rename = "M")]
332 is_best_match: bool,
333}
334
335type SpotKlineJson = (
336 i64,
337 String,
338 String,
339 String,
340 String,
341 String,
342 i64,
343 String,
344 i64,
345 String,
346 String,
347 String,
348);
349
350#[derive(Debug, Clone)]
361pub struct BinanceRawSpotHttpClient {
362 retry_manager: Arc<RetryManager<BinanceSpotHttpError>>,
363 client: HttpClient,
364 base_url: String,
365 credential: Option<SigningCredential>,
366 recv_window: Option<u64>,
367 order_rate_keys: Vec<String>,
368 json_responses: bool,
369}
370
371impl BinanceRawSpotHttpClient {
372 #[must_use]
374 pub fn has_credentials(&self) -> bool {
375 self.credential.is_some()
376 }
377
378 pub fn new(
384 environment: BinanceEnvironment,
385 api_key: Option<String>,
386 api_secret: Option<String>,
387 base_url_override: Option<String>,
388 recv_window: Option<u64>,
389 timeout_secs: Option<u64>,
390 proxy_url: Option<String>,
391 ) -> BinanceSpotHttpResult<Self> {
392 Self::new_with_json_responses(
393 environment,
394 api_key,
395 api_secret,
396 base_url_override,
397 recv_window,
398 timeout_secs,
399 proxy_url,
400 false,
401 )
402 }
403
404 #[expect(clippy::too_many_arguments)]
410 pub fn new_with_json_responses(
411 environment: BinanceEnvironment,
412 api_key: Option<String>,
413 api_secret: Option<String>,
414 base_url_override: Option<String>,
415 recv_window: Option<u64>,
416 timeout_secs: Option<u64>,
417 proxy_url: Option<String>,
418 json_responses: bool,
419 ) -> BinanceSpotHttpResult<Self> {
420 let RateLimitConfig {
421 default_quota,
422 keyed_quotas,
423 order_keys,
424 } = Self::rate_limit_config();
425
426 let credential = match (api_key, api_secret) {
427 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
428 (None, None) => None,
429 _ => return Err(BinanceSpotHttpError::MissingCredentials),
430 };
431
432 let base_url = base_url_override.unwrap_or_else(|| {
433 get_http_base_url(BinanceProductType::Spot, environment).to_string()
434 });
435
436 let headers = Self::default_headers(&credential, json_responses);
437
438 let client = HttpClient::builder()
439 .redirect_policy(HttpRedirectPolicy::Reject)
440 .headers(headers)
441 .header_keys(vec![
442 BINANCE_API_KEY_HEADER.to_string(),
443 BINANCE_RETRY_AFTER_HEADER.to_string(),
444 ])
445 .keyed_quotas(keyed_quotas)
446 .maybe_default_quota(default_quota)
447 .maybe_timeout_secs(timeout_secs)
448 .maybe_proxy_url(proxy_url)
449 .build()?;
450
451 Ok(Self {
452 retry_manager: Arc::new(RetryManager::new(crate::common::http::retry_config())),
453 client,
454 base_url,
455 credential,
456 recv_window,
457 order_rate_keys: order_keys,
458 json_responses,
459 })
460 }
461
462 #[must_use]
464 pub const fn schema_id() -> u16 {
465 SBE_SCHEMA_ID
466 }
467
468 #[must_use]
470 pub const fn schema_version() -> u16 {
471 SBE_SCHEMA_VERSION
472 }
473
474 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
480 where
481 P: Serialize + ?Sized,
482 {
483 self.request(Method::GET, path, params, false, false).await
484 }
485
486 pub async fn get_signed<P>(
492 &self,
493 path: &str,
494 params: Option<&P>,
495 ) -> BinanceSpotHttpResult<Vec<u8>>
496 where
497 P: Serialize + ?Sized,
498 {
499 self.request(Method::GET, path, params, true, false).await
500 }
501
502 pub async fn get_signed_json<P>(
508 &self,
509 path: &str,
510 params: Option<&P>,
511 ) -> BinanceSpotHttpResult<Vec<u8>>
512 where
513 P: Serialize + ?Sized,
514 {
515 self.request_with_extra_headers(
516 Method::GET,
517 path,
518 params,
519 true,
520 false,
521 Some(HashMap::from([(
522 "Accept".to_string(),
523 "application/json".to_string(),
524 )])),
525 )
526 .await
527 }
528
529 pub async fn post_signed<P>(
535 &self,
536 path: &str,
537 params: Option<&P>,
538 ) -> BinanceSpotHttpResult<Vec<u8>>
539 where
540 P: Serialize + ?Sized,
541 {
542 self.request(Method::POST, path, params, true, true).await
543 }
544
545 pub async fn post_signed_json<P>(
551 &self,
552 path: &str,
553 params: Option<&P>,
554 ) -> BinanceSpotHttpResult<Vec<u8>>
555 where
556 P: Serialize + ?Sized,
557 {
558 self.request_with_extra_headers(
559 Method::POST,
560 path,
561 params,
562 true,
563 true,
564 Some(HashMap::from([(
565 "Accept".to_string(),
566 "application/json".to_string(),
567 )])),
568 )
569 .await
570 }
571
572 pub async fn delete_signed<P>(
578 &self,
579 path: &str,
580 params: Option<&P>,
581 ) -> BinanceSpotHttpResult<Vec<u8>>
582 where
583 P: Serialize + ?Sized,
584 {
585 self.request(Method::DELETE, path, params, true, true).await
586 }
587
588 pub async fn delete_signed_json<P>(
594 &self,
595 path: &str,
596 params: Option<&P>,
597 ) -> BinanceSpotHttpResult<Vec<u8>>
598 where
599 P: Serialize + ?Sized,
600 {
601 self.request_with_extra_headers(
602 Method::DELETE,
603 path,
604 params,
605 true,
606 true,
607 Some(HashMap::from([(
608 "Accept".to_string(),
609 "application/json".to_string(),
610 )])),
611 )
612 .await
613 }
614
615 async fn request<P>(
616 &self,
617 method: Method,
618 path: &str,
619 params: Option<&P>,
620 signed: bool,
621 use_order_quota: bool,
622 ) -> BinanceSpotHttpResult<Vec<u8>>
623 where
624 P: Serialize + ?Sized,
625 {
626 self.request_with_extra_headers(method, path, params, signed, use_order_quota, None)
627 .await
628 }
629
630 async fn request_with_extra_headers<P>(
631 &self,
632 method: Method,
633 path: &str,
634 params: Option<&P>,
635 signed: bool,
636 use_order_quota: bool,
637 extra_headers: Option<HashMap<String, String>>,
638 ) -> BinanceSpotHttpResult<Vec<u8>>
639 where
640 P: Serialize + ?Sized,
641 {
642 let operation = || {
643 self.request_with_extra_headers_once(
644 method.clone(),
645 path,
646 params,
647 signed,
648 use_order_quota,
649 extra_headers.clone(),
650 )
651 };
652
653 if method != Method::GET {
654 return operation().await;
655 }
656 self.retry_manager
657 .invocation(
658 path,
659 operation,
660 BinanceSpotHttpError::is_retryable,
661 |e| match e {
662 RetryError::Canceled => {
663 BinanceSpotHttpError::Canceled("HTTP requests canceled".to_string())
664 }
665 RetryError::OperationTimeout { timeout_ms } => {
666 BinanceSpotHttpError::Timeout(format!("Request exceeded {timeout_ms}ms"))
667 }
668 RetryError::InvalidConfiguration { message } => {
669 BinanceSpotHttpError::ValidationError(message)
670 }
671 e @ RetryError::ElapsedBudgetExceeded { .. } => {
672 BinanceSpotHttpError::RetryBudgetExceeded(e.to_string())
673 }
674 },
675 )
676 .retry_delay(&BinanceSpotHttpError::retry_after)
677 .execute()
678 .await
679 }
680
681 async fn request_with_extra_headers_once<P>(
682 &self,
683 method: Method,
684 path: &str,
685 params: Option<&P>,
686 signed: bool,
687 use_order_quota: bool,
688 extra_headers: Option<HashMap<String, String>>,
689 ) -> BinanceSpotHttpResult<Vec<u8>>
690 where
691 P: Serialize + ?Sized,
692 {
693 let mut query = params
694 .map(serde_urlencoded::to_string)
695 .transpose()
696 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
697 .unwrap_or_default();
698
699 let mut headers = extra_headers.unwrap_or_default();
700
701 if signed {
702 let cred = self
703 .credential
704 .as_ref()
705 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
706
707 if !query.is_empty() {
708 query.push('&');
709 }
710
711 let timestamp = Timestamp::now().as_millisecond();
712 query.push_str(&format!("timestamp={timestamp}"));
713
714 if let Some(recv_window) = self.recv_window {
715 query.push_str(&format!("&recvWindow={recv_window}"));
716 }
717
718 let signature = Self::percent_encode(&cred.sign(&query));
719 query.push_str(&format!("&signature={signature}"));
720 headers.insert(
721 BINANCE_API_KEY_HEADER.to_string(),
722 cred.api_key().to_string(),
723 );
724 }
725
726 let url = self.build_url(path, &query);
727 let keys = self.rate_limit_keys(use_order_quota);
728
729 let response = self
730 .client
731 .request_with_url_redacted(
732 method,
733 url,
734 None::<&HashMap<String, Vec<String>>>,
735 Some(headers),
736 None,
737 None,
738 Some(keys),
739 )
740 .await?;
741
742 if !response.status.is_success() {
743 return self.parse_error_response(&response);
744 }
745
746 Ok(response.body.to_vec())
747 }
748
749 fn build_url(&self, path: &str, query: &str) -> String {
750 let normalized_path = if path.starts_with('/') {
751 path.to_string()
752 } else {
753 format!("/{path}")
754 };
755
756 let mut url = if normalized_path.starts_with(&format!("{SAPI_PATH}/")) {
757 format!("{}{normalized_path}", self.base_url)
758 } else {
759 format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path)
760 };
761
762 if !query.is_empty() {
763 url.push('?');
764 url.push_str(query);
765 }
766 url
767 }
768
769 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
770 if use_orders {
771 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
772 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
773 keys.extend(self.order_rate_keys.iter().cloned());
774 keys
775 } else {
776 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
777 }
778 }
779
780 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceSpotHttpResult<T> {
781 let status = response.status.as_u16();
782 let body = &response.body;
783 let retry_after = crate::common::http::retry_after(&response.headers, Timestamp::now());
784
785 if let Ok(body_str) = std::str::from_utf8(body)
787 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
788 {
789 return Err(BinanceSpotHttpError::BinanceError {
790 code: err.code,
791 message: err.msg,
792 status,
793 retry_after,
794 });
795 }
796
797 if let Some((code, message)) = Self::try_decode_sbe_error(body) {
799 return Err(BinanceSpotHttpError::BinanceError {
800 code: code.into(),
801 message,
802 status,
803 retry_after,
804 });
805 }
806
807 Err(BinanceSpotHttpError::UnexpectedStatus {
808 status,
809 body: hex::encode(body),
810 retry_after,
811 })
812 }
813
814 fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
818 const HEADER_LEN: usize = 8;
819 if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
820 return None;
821 }
822
823 let buf = ReadBuf::new(body);
824
825 let header = MessageHeaderDecoder::default().wrap(buf, 0);
827 if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
828 return None;
829 }
830
831 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
833 let code = decoder.code();
834
835 let msg_coords = decoder.msg_decoder();
837 let msg_bytes = decoder.msg_slice(msg_coords);
838 let message = String::from_utf8_lossy(msg_bytes).into_owned();
839
840 Some((code, message))
841 }
842
843 fn default_headers(
844 credential: &Option<SigningCredential>,
845 json_responses: bool,
846 ) -> HashMap<String, String> {
847 let mut headers: HashMap<String, String> =
848 create_standard_nautilus_headers().into_iter().collect();
849
850 if json_responses {
851 headers.insert("Accept".to_string(), "application/json".to_string());
852 } else {
853 headers.insert("Accept".to_string(), "application/sbe".to_string());
854 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
855 }
856
857 if let Some(cred) = credential {
858 headers.insert(
859 BINANCE_API_KEY_HEADER.to_string(),
860 cred.api_key().to_string(),
861 );
862 }
863 headers
864 }
865
866 fn rate_limit_config() -> RateLimitConfig {
867 let quotas = BINANCE_SPOT_RATE_LIMITS;
868 let mut keyed = Vec::new();
869 let mut order_keys = Vec::new();
870 let mut default = None;
871
872 for quota in quotas {
873 if let Some(q) = Self::quota_from(quota) {
874 match quota.rate_limit_type {
875 BinanceRateLimitType::RequestWeight if default.is_none() => {
876 default = Some(q);
877 }
878 BinanceRateLimitType::Orders => {
879 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
880 order_keys.push(key.clone());
881 keyed.push((key, q));
882 }
883 _ => {}
884 }
885 }
886 }
887
888 let default_quota = default.unwrap_or_else(|| {
889 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
890 });
891
892 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
893
894 RateLimitConfig {
895 default_quota: Some(default_quota),
896 keyed_quotas: keyed,
897 order_keys,
898 }
899 }
900
901 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
902 let burst = NonZeroU32::new(quota.limit)?;
903 match quota.interval {
904 BinanceRateLimitInterval::Second => Quota::per_second(burst),
905 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
906 BinanceRateLimitInterval::Day => {
907 Quota::with_period(std::time::Duration::from_secs(SECONDS_IN_DAY))
908 .map(|q| q.allow_burst(burst))
909 }
910 BinanceRateLimitInterval::Unknown => None,
911 }
912 }
913
914 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
920 let bytes = self.get("ping", None::<&()>).await?;
921 parse::decode_ping(&bytes)?;
922 Ok(())
923 }
924
925 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
933 if self.json_responses {
934 #[derive(Deserialize)]
935 #[serde(rename_all = "camelCase")]
936 struct ServerTimeJson {
937 server_time: i64,
938 }
939
940 let bytes = self.get_json("time", None::<&()>).await?;
941 let response: ServerTimeJson = serde_json::from_slice(&bytes)
942 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
943 return millis_to_micros(response.server_time);
944 }
945
946 let bytes = self.get("time", None::<&()>).await?;
947 let timestamp = parse::decode_server_time(&bytes)?;
948 Ok(timestamp)
949 }
950
951 pub async fn exchange_info(
957 &self,
958 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
959 let bytes = self.get("exchangeInfo", None::<&()>).await?;
960 let info = parse::decode_exchange_info(&bytes)?;
961 Ok(info)
962 }
963
964 pub async fn exchange_info_json(&self) -> BinanceSpotHttpResult<BinanceExchangeInfoJson> {
970 let bytes = self.get_json("exchangeInfo", None::<&()>).await?;
971 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
972 }
973
974 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
980 if self.json_responses {
981 let bytes = self.get_json("depth", Some(params)).await?;
982 let response: SpotDepthJson = serde_json::from_slice(&bytes)
983 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
984 spot_depth_from_json(response)
985 } else {
986 let bytes = self.get("depth", Some(params)).await?;
987 Ok(parse::decode_depth(&bytes)?)
988 }
989 }
990
991 pub async fn trades(
997 &self,
998 symbol: &str,
999 limit: Option<u32>,
1000 ) -> BinanceSpotHttpResult<BinanceTrades> {
1001 let params = TradesParams {
1002 symbol: symbol.to_string(),
1003 limit,
1004 };
1005
1006 if self.json_responses {
1007 let bytes = self.get_json("trades", Some(¶ms)).await?;
1008 let response: Vec<SpotTradeJson> = serde_json::from_slice(&bytes)
1009 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1010 spot_trades_from_json(response)
1011 } else {
1012 let bytes = self.get("trades", Some(¶ms)).await?;
1013 Ok(parse::decode_trades(&bytes)?)
1014 }
1015 }
1016
1017 pub async fn agg_trades(
1023 &self,
1024 params: &AggTradesParams,
1025 ) -> BinanceSpotHttpResult<BinanceAggTrades> {
1026 if params.limit.is_some_and(|limit| limit > 1000) {
1027 return Err(BinanceSpotHttpError::ValidationError(
1028 "aggregate trade limit must not exceed 1000".to_string(),
1029 ));
1030 }
1031
1032 if matches!((params.start_time, params.end_time), (Some(start), Some(end)) if start > end) {
1033 return Err(BinanceSpotHttpError::ValidationError(
1034 "aggregate trade startTime must not exceed endTime".to_string(),
1035 ));
1036 }
1037
1038 if self.json_responses {
1039 let bytes = self.get_json("aggTrades", Some(params)).await?;
1040 let response: Vec<SpotAggTradeJson> = serde_json::from_slice(&bytes)
1041 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1042 spot_agg_trades_from_json(response)
1043 } else {
1044 let bytes = self.get("aggTrades", Some(params)).await?;
1045 Ok(parse::decode_agg_trades(&bytes)?)
1046 }
1047 }
1048
1049 pub async fn klines(
1055 &self,
1056 symbol: &str,
1057 interval: &str,
1058 start_time: Option<i64>,
1059 end_time: Option<i64>,
1060 limit: Option<u32>,
1061 ) -> BinanceSpotHttpResult<BinanceKlines> {
1062 let params = KlinesParams {
1063 symbol: symbol.to_string(),
1064 interval: interval.to_string(),
1065 start_time,
1066 end_time,
1067 time_zone: None,
1068 limit,
1069 };
1070
1071 if self.json_responses {
1072 let bytes = self.get_json("klines", Some(¶ms)).await?;
1073 let response: Vec<SpotKlineJson> = serde_json::from_slice(&bytes)
1074 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1075 spot_klines_from_json(response)
1076 } else {
1077 let bytes = self.get("klines", Some(¶ms)).await?;
1078 Ok(parse::decode_klines(&bytes)?)
1079 }
1080 }
1081
1082 async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1084 where
1085 P: Serialize + ?Sized,
1086 {
1087 self.request_with_extra_headers(
1088 Method::GET,
1089 path,
1090 params,
1091 false,
1092 false,
1093 Some(HashMap::from([(
1094 "Accept".to_string(),
1095 "application/json".to_string(),
1096 )])),
1097 )
1098 .await
1099 }
1100
1101 pub async fn ticker_24hr(
1109 &self,
1110 symbol: Option<&str>,
1111 ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
1112 let params = symbol.map(TickerParams::for_symbol);
1113 let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
1114
1115 if symbol.is_some() {
1117 let ticker: Ticker24hr = serde_json::from_slice(&bytes)
1118 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1119 Ok(vec![ticker])
1120 } else {
1121 let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
1122 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1123 Ok(tickers)
1124 }
1125 }
1126
1127 pub async fn ticker_price(
1135 &self,
1136 symbol: Option<&str>,
1137 ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
1138 let params = symbol.map(TickerParams::for_symbol);
1139 let bytes = self.get_json("ticker/price", params.as_ref()).await?;
1140
1141 if symbol.is_some() {
1143 let ticker: TickerPrice = serde_json::from_slice(&bytes)
1144 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1145 Ok(vec![ticker])
1146 } else {
1147 let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
1148 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1149 Ok(tickers)
1150 }
1151 }
1152
1153 pub async fn ticker_book(
1161 &self,
1162 symbol: Option<&str>,
1163 ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
1164 let params = symbol.map(TickerParams::for_symbol);
1165 let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
1166
1167 if symbol.is_some() {
1169 let ticker: BookTicker = serde_json::from_slice(&bytes)
1170 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1171 Ok(vec![ticker])
1172 } else {
1173 let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
1174 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1175 Ok(tickers)
1176 }
1177 }
1178
1179 pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
1185 let params = AvgPriceParams::new(symbol);
1186 let bytes = self.get_json("avgPrice", Some(¶ms)).await?;
1187
1188 let avg_price: AvgPrice = serde_json::from_slice(&bytes)
1189 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1190 Ok(avg_price)
1191 }
1192
1193 pub async fn get_trade_fee(
1202 &self,
1203 symbol: Option<&str>,
1204 ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
1205 let params = symbol.map(TradeFeeParams::for_symbol);
1206 let bytes = self
1207 .get_signed_sapi("asset/tradeFee", params.as_ref())
1208 .await?;
1209
1210 let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
1211 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1212 Ok(fees)
1213 }
1214
1215 async fn get_signed_sapi<P>(
1217 &self,
1218 path: &str,
1219 params: Option<&P>,
1220 ) -> BinanceSpotHttpResult<Vec<u8>>
1221 where
1222 P: Serialize + ?Sized,
1223 {
1224 let path = format!("{SAPI_PATH}/{}", path.trim_start_matches('/'));
1225 self.request(Method::GET, &path, params, true, false).await
1226 }
1227
1228 fn percent_encode(input: &str) -> String {
1230 let mut result = String::with_capacity(input.len() * 3);
1231 for byte in input.bytes() {
1232 match byte {
1233 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1235 result.push(byte as char);
1236 }
1237 _ => {
1238 result.push('%');
1239 result.push_str(&format!("{byte:02X}"));
1240 }
1241 }
1242 }
1243 result
1244 }
1245
1246 pub async fn batch_submit_orders(
1257 &self,
1258 orders: &[BatchOrderItem],
1259 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1260 if orders.is_empty() {
1261 return Ok(Vec::new());
1262 }
1263
1264 if orders.len() > 5 {
1265 return Err(BinanceSpotHttpError::ValidationError(
1266 "Batch order limit is 5 orders maximum".to_string(),
1267 ));
1268 }
1269
1270 let batch_json = serde_json::to_string(orders)
1271 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1272
1273 let bytes = self
1274 .batch_request(Method::POST, "batchOrders", &batch_json)
1275 .await?;
1276
1277 let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
1278 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1279
1280 Ok(results)
1281 }
1282
1283 pub async fn batch_cancel_orders(
1294 &self,
1295 cancels: &[BatchCancelItem],
1296 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
1297 if cancels.is_empty() {
1298 return Ok(Vec::new());
1299 }
1300
1301 if cancels.len() > 5 {
1302 return Err(BinanceSpotHttpError::ValidationError(
1303 "Batch cancel limit is 5 orders maximum".to_string(),
1304 ));
1305 }
1306
1307 let batch_json = serde_json::to_string(cancels)
1308 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1309
1310 let bytes = self
1311 .batch_request(Method::DELETE, "batchOrders", &batch_json)
1312 .await?;
1313
1314 let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
1315 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1316
1317 Ok(results)
1318 }
1319
1320 async fn batch_request(
1322 &self,
1323 method: Method,
1324 path: &str,
1325 batch_json: &str,
1326 ) -> BinanceSpotHttpResult<Vec<u8>> {
1327 let cred = self
1328 .credential
1329 .as_ref()
1330 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1331
1332 let encoded_batch = Self::percent_encode(batch_json);
1333 let timestamp = Timestamp::now().as_millisecond();
1334 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
1335
1336 if let Some(recv_window) = self.recv_window {
1337 query.push_str(&format!("&recvWindow={recv_window}"));
1338 }
1339
1340 let signature = Self::percent_encode(&cred.sign(&query));
1341 query.push_str(&format!("&signature={signature}"));
1342
1343 let url = self.build_url(path, &query);
1344
1345 let mut headers = HashMap::new();
1346 headers.insert(
1347 BINANCE_API_KEY_HEADER.to_string(),
1348 cred.api_key().to_string(),
1349 );
1350
1351 let keys = self.rate_limit_keys(true);
1352
1353 let response = self
1354 .client
1355 .request_with_url_redacted(
1356 method,
1357 url,
1358 None::<&HashMap<String, Vec<String>>>,
1359 Some(headers),
1360 None,
1361 None,
1362 Some(keys),
1363 )
1364 .await?;
1365
1366 if !response.status.is_success() {
1367 return self.parse_error_response(&response);
1368 }
1369
1370 Ok(response.body.to_vec())
1371 }
1372
1373 pub async fn account(
1379 &self,
1380 params: &AccountInfoParams,
1381 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
1382 if self.json_responses {
1383 let bytes = self.get_signed_json("account", Some(params)).await?;
1384 let response: SpotAccountJson = serde_json::from_slice(&bytes)
1385 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1386 spot_account_from_json(response)
1387 } else {
1388 let bytes = self.get_signed("account", Some(params)).await?;
1389 Ok(parse::decode_account(&bytes)?)
1390 }
1391 }
1392
1393 pub async fn account_commission(
1399 &self,
1400 symbol: &str,
1401 ) -> BinanceSpotHttpResult<BinanceAccountCommission> {
1402 let params = AccountCommissionParams::new(symbol);
1403 let bytes = self
1404 .get_signed_json("account/commission", Some(¶ms))
1405 .await?;
1406 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1407 }
1408
1409 pub async fn account_rates_json(&self) -> BinanceSpotHttpResult<BinanceAccountRatesJson> {
1415 let params = AccountInfoParams::default();
1416 let bytes = self.get_signed_json("account", Some(¶ms)).await?;
1417 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1418 }
1419
1420 pub async fn account_trades(
1426 &self,
1427 symbol: &str,
1428 order_id: Option<i64>,
1429 start_time: Option<i64>,
1430 end_time: Option<i64>,
1431 limit: Option<u32>,
1432 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1433 self.account_trades_with_cursor(symbol, order_id, start_time, end_time, None, limit)
1434 .await
1435 }
1436
1437 async fn account_trades_with_cursor(
1438 &self,
1439 symbol: &str,
1440 order_id: Option<i64>,
1441 start_time: Option<i64>,
1442 end_time: Option<i64>,
1443 from_id: Option<i64>,
1444 limit: Option<u32>,
1445 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1446 let params = AccountTradesParams {
1447 symbol: symbol.to_string(),
1448 order_id,
1449 start_time,
1450 end_time,
1451 from_id,
1452 limit,
1453 };
1454
1455 if self.json_responses {
1456 let bytes = self.get_signed_json("myTrades", Some(¶ms)).await?;
1457 let response: Vec<SpotAccountTradeJson> = serde_json::from_slice(&bytes)
1458 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1459 response
1460 .into_iter()
1461 .map(spot_account_trade_from_json)
1462 .collect()
1463 } else {
1464 let bytes = self.get_signed("myTrades", Some(¶ms)).await?;
1465 Ok(parse::decode_account_trades(&bytes)?)
1466 }
1467 }
1468
1469 pub async fn query_order(
1477 &self,
1478 symbol: &str,
1479 order_id: Option<i64>,
1480 client_order_id: Option<&str>,
1481 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
1482 let params = QueryOrderParams {
1483 symbol: symbol.to_string(),
1484 order_id,
1485 orig_client_order_id: client_order_id.map(|s| s.to_string()),
1486 };
1487
1488 if self.json_responses {
1489 let bytes = self.get_signed_json("order", Some(¶ms)).await?;
1490 let response: SpotOrderJson = serde_json::from_slice(&bytes)
1491 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1492 spot_order_from_json(response)
1493 } else {
1494 let bytes = self.get_signed("order", Some(¶ms)).await?;
1495 Ok(parse::decode_order(&bytes)?)
1496 }
1497 }
1498
1499 pub async fn open_orders(
1505 &self,
1506 symbol: Option<&str>,
1507 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1508 let params = OpenOrdersParams {
1509 symbol: symbol.map(|s| s.to_string()),
1510 };
1511
1512 if self.json_responses {
1513 let bytes = self.get_signed_json("openOrders", Some(¶ms)).await?;
1514 let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1515 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1516 response.into_iter().map(spot_order_from_json).collect()
1517 } else {
1518 let bytes = self.get_signed("openOrders", Some(¶ms)).await?;
1519 Ok(parse::decode_orders(&bytes)?)
1520 }
1521 }
1522
1523 pub async fn all_orders(
1529 &self,
1530 symbol: &str,
1531 start_time: Option<i64>,
1532 end_time: Option<i64>,
1533 limit: Option<u32>,
1534 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1535 let params = AllOrdersParams {
1536 symbol: symbol.to_string(),
1537 order_id: None,
1538 start_time,
1539 end_time,
1540 limit,
1541 };
1542
1543 if self.json_responses {
1544 let bytes = self.get_signed_json("allOrders", Some(¶ms)).await?;
1545 let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1546 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1547 response.into_iter().map(spot_order_from_json).collect()
1548 } else {
1549 let bytes = self.get_signed("allOrders", Some(¶ms)).await?;
1550 Ok(parse::decode_orders(&bytes)?)
1551 }
1552 }
1553
1554 async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1556 where
1557 P: Serialize + ?Sized,
1558 {
1559 if self.json_responses {
1560 self.post_signed_json(path, params).await
1561 } else {
1562 self.post_signed(path, params).await
1563 }
1564 }
1565
1566 async fn delete_order<P>(
1568 &self,
1569 path: &str,
1570 params: Option<&P>,
1571 ) -> BinanceSpotHttpResult<Vec<u8>>
1572 where
1573 P: Serialize + ?Sized,
1574 {
1575 if self.json_responses {
1576 self.delete_signed_json(path, params).await
1577 } else {
1578 self.delete_signed(path, params).await
1579 }
1580 }
1581
1582 #[expect(clippy::too_many_arguments)]
1588 pub async fn new_order(
1589 &self,
1590 symbol: &str,
1591 side: BinanceSide,
1592 order_type: BinanceSpotOrderType,
1593 time_in_force: Option<BinanceTimeInForce>,
1594 quantity: Option<&str>,
1595 price: Option<&str>,
1596 client_order_id: Option<&str>,
1597 stop_price: Option<&str>,
1598 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1599 let params = NewOrderParams {
1600 symbol: symbol.to_string(),
1601 side,
1602 order_type,
1603 time_in_force,
1604 quantity: quantity.map(|s| s.to_string()),
1605 quote_order_qty: None,
1606 price: price.map(|s| s.to_string()),
1607 new_client_order_id: client_order_id.map(|s| s.to_string()),
1608 stop_price: stop_price.map(|s| s.to_string()),
1609 trailing_delta: None,
1610 iceberg_qty: None,
1611 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1612 self_trade_prevention_mode: None,
1613 strategy_id: None,
1614 strategy_type: None,
1615 };
1616 let bytes = self.post_order("order", Some(¶ms)).await?;
1617 self.decode_new_order_response(&bytes)
1618 }
1619
1620 #[expect(clippy::too_many_arguments)]
1630 pub async fn new_order_full(
1631 &self,
1632 symbol: &str,
1633 side: BinanceSide,
1634 order_type: BinanceSpotOrderType,
1635 time_in_force: Option<BinanceTimeInForce>,
1636 quantity: Option<&str>,
1637 quote_order_qty: Option<&str>,
1638 price: Option<&str>,
1639 client_order_id: Option<&str>,
1640 stop_price: Option<&str>,
1641 iceberg_qty: Option<&str>,
1642 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1643 let params = NewOrderParams {
1644 symbol: symbol.to_string(),
1645 side,
1646 order_type,
1647 time_in_force,
1648 quantity: quantity.map(|s| s.to_string()),
1649 quote_order_qty: quote_order_qty.map(|s| s.to_string()),
1650 price: price.map(|s| s.to_string()),
1651 new_client_order_id: client_order_id.map(|s| s.to_string()),
1652 stop_price: stop_price.map(|s| s.to_string()),
1653 trailing_delta: None,
1654 iceberg_qty: iceberg_qty.map(|s| s.to_string()),
1655 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1656 self_trade_prevention_mode: None,
1657 strategy_id: None,
1658 strategy_type: None,
1659 };
1660 let bytes = self.post_order("order", Some(¶ms)).await?;
1661 self.decode_new_order_response(&bytes)
1662 }
1663
1664 pub async fn new_oco_order_list(
1670 &self,
1671 params: &NewOcoOrderListParams,
1672 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
1673 let bytes = self.post_signed_json("orderList/oco", Some(params)).await?;
1674 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1675 }
1676
1677 #[expect(clippy::too_many_arguments)]
1683 pub async fn cancel_replace_order(
1684 &self,
1685 symbol: &str,
1686 side: BinanceSide,
1687 order_type: BinanceSpotOrderType,
1688 time_in_force: Option<BinanceTimeInForce>,
1689 quantity: Option<&str>,
1690 price: Option<&str>,
1691 cancel_order_id: Option<i64>,
1692 cancel_client_order_id: Option<&str>,
1693 cancel_new_client_order_id: Option<&str>,
1694 new_client_order_id: Option<&str>,
1695 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1696 let params = CancelReplaceOrderParams {
1697 symbol: symbol.to_string(),
1698 side,
1699 order_type,
1700 cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1701 time_in_force,
1702 quantity: quantity.map(|s| s.to_string()),
1703 quote_order_qty: None,
1704 price: price.map(|s| s.to_string()),
1705 cancel_order_id,
1706 cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1707 cancel_new_client_order_id: cancel_new_client_order_id.map(|s| s.to_string()),
1708 new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1709 stop_price: None,
1710 trailing_delta: None,
1711 iceberg_qty: None,
1712 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1713 self_trade_prevention_mode: None,
1714 };
1715 let bytes = self
1716 .post_order("order/cancelReplace", Some(¶ms))
1717 .await?;
1718
1719 if self.json_responses {
1720 let response: SpotCancelReplaceJson = serde_json::from_slice(&bytes)
1721 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1722 spot_new_order_from_json(response.new_order_response)
1723 } else {
1724 Ok(parse::decode_cancel_replace(&bytes)?)
1725 }
1726 }
1727
1728 pub async fn cancel_order(
1736 &self,
1737 symbol: &str,
1738 order_id: Option<i64>,
1739 client_order_id: Option<&str>,
1740 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1741 let params = match (order_id, client_order_id) {
1742 (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1743 (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1744 (None, None) => {
1745 return Err(BinanceSpotHttpError::ValidationError(
1746 "Either order_id or client_order_id must be provided".to_string(),
1747 ));
1748 }
1749 };
1750 let bytes = self.delete_order("order", Some(¶ms)).await?;
1751 self.decode_cancel_order_response(&bytes)
1752 }
1753
1754 pub async fn cancel_open_orders(
1762 &self,
1763 symbol: &str,
1764 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1765 Ok(self
1766 .cancel_open_order_responses(symbol)
1767 .await?
1768 .into_iter()
1769 .flat_map(|response| match response {
1770 BinanceCancelOpenOrdersResponse::Order(response) => vec![response],
1771 BinanceCancelOpenOrdersResponse::OrderList(response) => response.order_reports,
1772 })
1773 .collect())
1774 }
1775
1776 pub async fn cancel_open_order_responses(
1782 &self,
1783 symbol: &str,
1784 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOpenOrdersResponse>> {
1785 let params = CancelOpenOrdersParams::new(symbol.to_string());
1786 let bytes = self.delete_order("openOrders", Some(¶ms)).await?;
1787 if self.json_responses {
1788 spot_cancel_open_orders_from_json(&bytes)
1789 } else {
1790 Ok(parse::decode_cancel_open_orders(&bytes)?)
1791 }
1792 }
1793
1794 fn decode_new_order_response(
1795 &self,
1796 bytes: &[u8],
1797 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1798 if self.json_responses {
1799 let response: SpotOrderJson = serde_json::from_slice(bytes)
1800 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1801 spot_new_order_from_json(response)
1802 } else {
1803 Ok(parse::decode_new_order_full(bytes)?)
1804 }
1805 }
1806
1807 fn decode_cancel_order_response(
1808 &self,
1809 bytes: &[u8],
1810 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1811 if self.json_responses {
1812 let response: SpotOrderJson = serde_json::from_slice(bytes)
1813 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1814 spot_cancel_order_from_json(response)
1815 } else {
1816 Ok(parse::decode_cancel_order(bytes)?)
1817 }
1818 }
1819
1820 async fn request_with_api_key<P>(
1822 &self,
1823 method: Method,
1824 path: &str,
1825 params: Option<&P>,
1826 ) -> BinanceSpotHttpResult<Vec<u8>>
1827 where
1828 P: Serialize + ?Sized,
1829 {
1830 let cred = self
1831 .credential
1832 .as_ref()
1833 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1834
1835 let query = params
1836 .map(serde_urlencoded::to_string)
1837 .transpose()
1838 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1839 .unwrap_or_default();
1840
1841 let url = self.build_url(path, &query);
1842
1843 let mut headers = HashMap::new();
1844 headers.insert(
1845 BINANCE_API_KEY_HEADER.to_string(),
1846 cred.api_key().to_string(),
1847 );
1848
1849 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1850
1851 let response = self
1852 .client
1853 .request_with_url_redacted(
1854 method,
1855 url,
1856 None::<&HashMap<String, Vec<String>>>,
1857 Some(headers),
1858 None,
1859 None,
1860 Some(keys),
1861 )
1862 .await?;
1863
1864 if !response.status.is_success() {
1865 return self.parse_error_response(&response);
1866 }
1867
1868 Ok(response.body.to_vec())
1869 }
1870
1871 pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1880 let bytes = self
1881 .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1882 .await?;
1883
1884 let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1885 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1886
1887 Ok(response)
1888 }
1889
1890 pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1898 let params = ListenKeyParams::new(listen_key);
1899 self.request_with_api_key(Method::PUT, "userDataStream", Some(¶ms))
1900 .await?;
1901 Ok(())
1902 }
1903
1904 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1910 let params = ListenKeyParams::new(listen_key);
1911 self.request_with_api_key(Method::DELETE, "userDataStream", Some(¶ms))
1912 .await?;
1913 Ok(())
1914 }
1915}
1916
1917fn spot_depth_from_json(response: SpotDepthJson) -> BinanceSpotHttpResult<BinanceDepth> {
1918 let price_scale = decimal_common_scale(
1919 response
1920 .bids
1921 .iter()
1922 .chain(&response.asks)
1923 .map(|level| level[0].as_str()),
1924 )?;
1925 let qty_scale = decimal_common_scale(
1926 response
1927 .bids
1928 .iter()
1929 .chain(&response.asks)
1930 .map(|level| level[1].as_str()),
1931 )?;
1932 let parse_levels = |levels: Vec<[String; 2]>| {
1933 levels
1934 .into_iter()
1935 .map(|level| {
1936 Ok(BinancePriceLevel {
1937 price_mantissa: decimal_mantissa(&level[0], price_scale)?,
1938 qty_mantissa: decimal_mantissa(&level[1], qty_scale)?,
1939 })
1940 })
1941 .collect::<BinanceSpotHttpResult<Vec<_>>>()
1942 };
1943
1944 Ok(BinanceDepth {
1945 last_update_id: response.last_update_id,
1946 price_exponent: decimal_exponent(price_scale)?,
1947 qty_exponent: decimal_exponent(qty_scale)?,
1948 bids: parse_levels(response.bids)?,
1949 asks: parse_levels(response.asks)?,
1950 })
1951}
1952
1953fn spot_trades_from_json(response: Vec<SpotTradeJson>) -> BinanceSpotHttpResult<BinanceTrades> {
1954 let price_scale = decimal_common_scale(
1955 response
1956 .iter()
1957 .flat_map(|trade| [trade.price.as_str(), trade.quote_qty.as_str()]),
1958 )?;
1959 let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1960 let trades = response
1961 .into_iter()
1962 .map(|trade| {
1963 Ok(BinanceTrade {
1964 id: trade.id,
1965 price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1966 qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1967 quote_qty_mantissa: decimal_mantissa(&trade.quote_qty, price_scale)?,
1968 time: millis_to_micros(trade.time)?,
1969 is_buyer_maker: trade.is_buyer_maker,
1970 is_best_match: trade.is_best_match,
1971 })
1972 })
1973 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
1974
1975 Ok(BinanceTrades {
1976 price_exponent: decimal_exponent(price_scale)?,
1977 qty_exponent: decimal_exponent(qty_scale)?,
1978 trades,
1979 })
1980}
1981
1982fn spot_agg_trades_from_json(
1983 response: Vec<SpotAggTradeJson>,
1984) -> BinanceSpotHttpResult<BinanceAggTrades> {
1985 let price_scale = decimal_common_scale(response.iter().map(|trade| trade.price.as_str()))?;
1986 let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1987 let trades = response
1988 .into_iter()
1989 .map(|trade| {
1990 Ok(BinanceAggTrade {
1991 id: trade.id,
1992 price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1993 qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1994 first_trade_id: trade.first_trade_id,
1995 last_trade_id: trade.last_trade_id,
1996 time: millis_to_micros(trade.time)?,
1997 is_buyer_maker: trade.is_buyer_maker,
1998 is_best_match: trade.is_best_match,
1999 })
2000 })
2001 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2002
2003 Ok(BinanceAggTrades {
2004 price_exponent: decimal_exponent(price_scale)?,
2005 qty_exponent: decimal_exponent(qty_scale)?,
2006 trades,
2007 })
2008}
2009
2010fn spot_klines_from_json(response: Vec<SpotKlineJson>) -> BinanceSpotHttpResult<BinanceKlines> {
2011 let price_scale = decimal_common_scale(response.iter().flat_map(|kline| {
2012 [
2013 kline.1.as_str(),
2014 kline.2.as_str(),
2015 kline.3.as_str(),
2016 kline.4.as_str(),
2017 kline.7.as_str(),
2018 kline.10.as_str(),
2019 ]
2020 }))?;
2021 let qty_scale = decimal_common_scale(
2022 response
2023 .iter()
2024 .flat_map(|kline| [kline.5.as_str(), kline.9.as_str()]),
2025 )?;
2026 let klines = response
2027 .into_iter()
2028 .map(|kline| {
2029 let volume = decimal_i128_bytes(&kline.5, qty_scale)?;
2030 let quote_volume = decimal_i128_bytes(&kline.7, price_scale)?;
2031 let taker_buy_base_volume = decimal_i128_bytes(&kline.9, qty_scale)?;
2032 let taker_buy_quote_volume = decimal_i128_bytes(&kline.10, price_scale)?;
2033 Ok(BinanceKline {
2034 open_time: millis_to_micros(kline.0)?,
2035 open_price: decimal_mantissa(&kline.1, price_scale)?,
2036 high_price: decimal_mantissa(&kline.2, price_scale)?,
2037 low_price: decimal_mantissa(&kline.3, price_scale)?,
2038 close_price: decimal_mantissa(&kline.4, price_scale)?,
2039 volume,
2040 close_time: millis_to_micros(kline.6)?,
2041 quote_volume,
2042 num_trades: kline.8,
2043 taker_buy_base_volume,
2044 taker_buy_quote_volume,
2045 })
2046 })
2047 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2048
2049 Ok(BinanceKlines {
2050 price_exponent: decimal_exponent(price_scale)?,
2051 qty_exponent: decimal_exponent(qty_scale)?,
2052 klines,
2053 })
2054}
2055
2056fn decimal_i128_bytes(value: &str, scale: u32) -> BinanceSpotHttpResult<[u8; 16]> {
2057 let mut decimal = Decimal::from_str_exact(value)
2058 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2059 decimal.rescale(scale);
2060 Ok(decimal.mantissa().to_le_bytes())
2061}
2062
2063fn spot_account_from_json(response: SpotAccountJson) -> BinanceSpotHttpResult<BinanceAccountInfo> {
2064 let commission_rates = response.commission_rates.map_or_else(
2065 || {
2066 [
2067 Decimal::new(response.maker_commission, 4).to_string(),
2068 Decimal::new(response.taker_commission, 4).to_string(),
2069 Decimal::new(response.buyer_commission, 4).to_string(),
2070 Decimal::new(response.seller_commission, 4).to_string(),
2071 ]
2072 },
2073 |rates| [rates.maker, rates.taker, rates.buyer, rates.seller],
2074 );
2075 let commission_scale = decimal_common_scale(commission_rates.iter().map(String::as_str))?;
2076 let balances = response
2077 .balances
2078 .into_iter()
2079 .map(|balance| {
2080 let scale = decimal_common_scale([balance.free.as_str(), balance.locked.as_str()])?;
2081 Ok(BinanceBalance {
2082 asset: balance.asset,
2083 free_mantissa: decimal_mantissa(&balance.free, scale)?,
2084 locked_mantissa: decimal_mantissa(&balance.locked, scale)?,
2085 exponent: decimal_exponent(scale)?,
2086 })
2087 })
2088 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2089
2090 Ok(BinanceAccountInfo {
2091 commission_exponent: decimal_exponent(commission_scale)?,
2092 maker_commission_mantissa: decimal_mantissa(&commission_rates[0], commission_scale)?,
2093 taker_commission_mantissa: decimal_mantissa(&commission_rates[1], commission_scale)?,
2094 buyer_commission_mantissa: decimal_mantissa(&commission_rates[2], commission_scale)?,
2095 seller_commission_mantissa: decimal_mantissa(&commission_rates[3], commission_scale)?,
2096 can_trade: response.can_trade,
2097 can_withdraw: response.can_withdraw,
2098 can_deposit: response.can_deposit,
2099 require_self_trade_prevention: response.require_self_trade_prevention,
2100 prevent_sor: response.prevent_sor,
2101 update_time: millis_to_micros(response.update_time)?,
2102 account_type: response.account_type,
2103 balances,
2104 })
2105}
2106
2107fn spot_new_order_from_json(
2108 response: SpotOrderJson,
2109) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
2110 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2111 let fills = response
2112 .fills
2113 .iter()
2114 .map(|fill| {
2115 let commission_scale = decimal_common_scale([fill.commission.as_str()])?;
2116 Ok(BinanceOrderFill {
2117 price_mantissa: decimal_mantissa(&fill.price, price_scale)?,
2118 qty_mantissa: decimal_mantissa(&fill.qty, qty_scale)?,
2119 commission_mantissa: decimal_mantissa(&fill.commission, commission_scale)?,
2120 commission_exponent: decimal_exponent(commission_scale)?,
2121 commission_asset: fill.commission_asset.clone(),
2122 trade_id: fill.trade_id,
2123 })
2124 })
2125 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2126
2127 Ok(BinanceNewOrderResponse {
2128 price_exponent: decimal_exponent(price_scale)?,
2129 qty_exponent: decimal_exponent(qty_scale)?,
2130 order_id: response.order_id,
2131 order_list_id: valid_order_list_id(response.order_list_id),
2132 transact_time: millis_to_micros(response.transact_time)?,
2133 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2134 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2135 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2136 cummulative_quote_qty_mantissa: decimal_mantissa(
2137 &response.cummulative_quote_qty,
2138 price_scale + qty_scale,
2139 )?,
2140 status: spot_sbe_order_status(response.status),
2141 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2142 order_type: spot_sbe_order_type(response.order_type),
2143 side: spot_sbe_order_side(response.side),
2144 stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2145 working_time: response.working_time.map(millis_to_micros).transpose()?,
2146 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2147 client_order_id: response.client_order_id,
2148 symbol: response.symbol,
2149 fills,
2150 expiry_reason: None,
2151 })
2152}
2153
2154fn spot_cancel_order_from_json(
2155 response: SpotOrderJson,
2156) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
2157 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2158 Ok(BinanceCancelOrderResponse {
2159 price_exponent: decimal_exponent(price_scale)?,
2160 qty_exponent: decimal_exponent(qty_scale)?,
2161 order_id: response.order_id,
2162 order_list_id: valid_order_list_id(response.order_list_id),
2163 transact_time: millis_to_micros(response.transact_time)?,
2164 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2165 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2166 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2167 cummulative_quote_qty_mantissa: decimal_mantissa(
2168 &response.cummulative_quote_qty,
2169 price_scale + qty_scale,
2170 )?,
2171 status: spot_sbe_order_status(response.status),
2172 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2173 order_type: spot_sbe_order_type(response.order_type),
2174 side: spot_sbe_order_side(response.side),
2175 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2176 client_order_id: response.client_order_id,
2177 orig_client_order_id: response.orig_client_order_id,
2178 symbol: response.symbol,
2179 })
2180}
2181
2182fn spot_cancel_open_orders_from_json(
2183 bytes: &[u8],
2184) -> BinanceSpotHttpResult<Vec<BinanceCancelOpenOrdersResponse>> {
2185 let responses: Vec<Value> = serde_json::from_slice(bytes)
2186 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
2187
2188 responses
2189 .into_iter()
2190 .map(|response| {
2191 let is_order_list =
2192 response.get("orders").is_some() || response.get("orderReports").is_some();
2193 if is_order_list {
2194 let response: SpotCancelOrderListJson = serde_json::from_value(response)
2195 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
2196 spot_cancel_order_list_from_json(response)
2197 .map(BinanceCancelOpenOrdersResponse::OrderList)
2198 } else {
2199 let response: SpotOrderJson = serde_json::from_value(response)
2200 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
2201 spot_cancel_order_from_json(response).map(BinanceCancelOpenOrdersResponse::Order)
2202 }
2203 })
2204 .collect()
2205}
2206
2207fn spot_cancel_order_list_from_json(
2208 response: SpotCancelOrderListJson,
2209) -> BinanceSpotHttpResult<BinanceCancelOrderListResponse> {
2210 let contingency_type = match response.contingency_type.as_str() {
2211 "OCO" => SbeContingencyType::Oco,
2212 "OTO" => SbeContingencyType::Oto,
2213 _ => {
2214 return Err(BinanceSpotHttpError::ResponseParseError(format!(
2215 "unknown order-list contingency type: {}",
2216 response.contingency_type
2217 )));
2218 }
2219 };
2220 let list_status_type = match response.list_status_type.as_str() {
2221 "RESPONSE" => SbeListStatusType::Response,
2222 "EXEC_STARTED" => SbeListStatusType::ExecStarted,
2223 "ALL_DONE" => SbeListStatusType::AllDone,
2224 "UPDATED" => SbeListStatusType::Updated,
2225 _ => {
2226 return Err(BinanceSpotHttpError::ResponseParseError(format!(
2227 "unknown order-list status type: {}",
2228 response.list_status_type
2229 )));
2230 }
2231 };
2232 let list_order_status = match response.list_order_status.as_str() {
2233 "CANCELING" => SbeListOrderStatus::Canceling,
2234 "EXECUTING" => SbeListOrderStatus::Executing,
2235 "ALL_DONE" => SbeListOrderStatus::AllDone,
2236 "REJECT" => SbeListOrderStatus::Reject,
2237 _ => {
2238 return Err(BinanceSpotHttpError::ResponseParseError(format!(
2239 "unknown aggregate order-list status: {}",
2240 response.list_order_status
2241 )));
2242 }
2243 };
2244 let order_reports = response
2245 .order_reports
2246 .into_iter()
2247 .map(spot_cancel_order_from_json)
2248 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2249
2250 Ok(BinanceCancelOrderListResponse {
2251 order_list_id: response.order_list_id,
2252 contingency_type,
2253 list_status_type,
2254 list_order_status,
2255 transaction_time: millis_to_micros(response.transaction_time)?,
2256 list_client_order_id: response.list_client_order_id,
2257 symbol: response.symbol,
2258 orders: response
2259 .orders
2260 .into_iter()
2261 .map(|order| super::models::BinanceCancelOrderListOrder {
2262 symbol: order.symbol,
2263 order_id: order.order_id,
2264 client_order_id: order.client_order_id,
2265 })
2266 .collect(),
2267 order_reports,
2268 })
2269}
2270
2271fn spot_order_from_json(response: SpotOrderJson) -> BinanceSpotHttpResult<BinanceOrderResponse> {
2272 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2273 Ok(BinanceOrderResponse {
2274 price_exponent: decimal_exponent(price_scale)?,
2275 qty_exponent: decimal_exponent(qty_scale)?,
2276 order_id: response.order_id,
2277 order_list_id: valid_order_list_id(response.order_list_id),
2278 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2279 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2280 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2281 cummulative_quote_qty_mantissa: decimal_mantissa(
2282 &response.cummulative_quote_qty,
2283 price_scale + qty_scale,
2284 )?,
2285 status: spot_sbe_order_status(response.status),
2286 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2287 order_type: spot_sbe_order_type(response.order_type),
2288 side: spot_sbe_order_side(response.side),
2289 stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2290 iceberg_qty_mantissa: decimal_optional_mantissa(&response.iceberg_qty, qty_scale)?,
2291 time: millis_to_micros(response.time)?,
2292 update_time: millis_to_micros(response.update_time)?,
2293 is_working: response.is_working,
2294 working_time: response.working_time.map(millis_to_micros).transpose()?,
2295 orig_quote_order_qty_mantissa: decimal_mantissa(
2296 &response.orig_quote_order_qty,
2297 price_scale + qty_scale,
2298 )?,
2299 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2300 client_order_id: response.client_order_id,
2301 symbol: response.symbol,
2302 expiry_reason: None,
2303 })
2304}
2305
2306fn spot_account_trade_from_json(
2307 response: SpotAccountTradeJson,
2308) -> BinanceSpotHttpResult<BinanceAccountTrade> {
2309 let price_scale = decimal_common_scale([response.price.as_str()])?;
2310 let qty_scale = decimal_common_scale([response.qty.as_str()])?;
2311 let commission_scale = decimal_common_scale([response.commission.as_str()])?;
2312 Ok(BinanceAccountTrade {
2313 price_exponent: decimal_exponent(price_scale)?,
2314 qty_exponent: decimal_exponent(qty_scale)?,
2315 commission_exponent: decimal_exponent(commission_scale)?,
2316 id: response.id,
2317 order_id: response.order_id,
2318 order_list_id: valid_order_list_id(response.order_list_id),
2319 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2320 qty_mantissa: decimal_mantissa(&response.qty, qty_scale)?,
2321 quote_qty_mantissa: decimal_mantissa(&response.quote_qty, price_scale + qty_scale)?,
2322 commission_mantissa: decimal_mantissa(&response.commission, commission_scale)?,
2323 time: millis_to_micros(response.time)?,
2324 is_buyer: response.is_buyer,
2325 is_maker: response.is_maker,
2326 is_best_match: response.is_best_match,
2327 symbol: response.symbol,
2328 commission_asset: response.commission_asset,
2329 })
2330}
2331
2332fn spot_order_scales(response: &SpotOrderJson) -> BinanceSpotHttpResult<(u32, u32)> {
2333 let price_scale = decimal_common_scale(
2334 [response.price.as_str(), response.stop_price.as_str()]
2335 .into_iter()
2336 .chain(response.fills.iter().map(|fill| fill.price.as_str())),
2337 )?;
2338 let qty_scale = decimal_common_scale(
2339 [
2340 response.orig_qty.as_str(),
2341 response.executed_qty.as_str(),
2342 response.iceberg_qty.as_str(),
2343 ]
2344 .into_iter()
2345 .chain(response.fills.iter().map(|fill| fill.qty.as_str())),
2346 )?;
2347 Ok((price_scale, qty_scale))
2348}
2349
2350fn decimal_common_scale<'a>(
2351 values: impl IntoIterator<Item = &'a str>,
2352) -> BinanceSpotHttpResult<u32> {
2353 values
2354 .into_iter()
2355 .filter(|value| !value.is_empty())
2356 .try_fold(0, |scale, value| {
2357 let decimal = Decimal::from_str_exact(value)
2358 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2359 Ok(scale.max(decimal.scale()))
2360 })
2361}
2362
2363fn decimal_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<i64> {
2364 if value.is_empty() {
2365 return Ok(0);
2366 }
2367 let mut decimal = Decimal::from_str_exact(value)
2368 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2369 decimal.rescale(scale);
2370 i64::try_from(decimal.mantissa()).map_err(|_| {
2371 BinanceSpotHttpError::ResponseParseError(format!(
2372 "decimal mantissa is outside i64 range: {value}"
2373 ))
2374 })
2375}
2376
2377fn decimal_optional_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<Option<i64>> {
2378 let mantissa = decimal_mantissa(value, scale)?;
2379 Ok((mantissa != 0).then_some(mantissa))
2380}
2381
2382fn decimal_exponent(scale: u32) -> BinanceSpotHttpResult<i8> {
2383 i8::try_from(scale)
2384 .map(|scale| -scale)
2385 .map_err(|_| BinanceSpotHttpError::ResponseParseError("decimal scale exceeds i8".into()))
2386}
2387
2388fn millis_to_micros(timestamp: i64) -> BinanceSpotHttpResult<i64> {
2389 timestamp.checked_mul(1_000).ok_or_else(|| {
2390 BinanceSpotHttpError::ResponseParseError(format!(
2391 "timestamp overflows microseconds: {timestamp}"
2392 ))
2393 })
2394}
2395
2396fn valid_order_list_id(order_list_id: Option<i64>) -> Option<i64> {
2397 order_list_id.filter(|value| *value >= 0)
2398}
2399
2400const fn spot_sbe_order_status(status: BinanceOrderStatus) -> SbeOrderStatus {
2401 match status {
2402 BinanceOrderStatus::New => SbeOrderStatus::New,
2403 BinanceOrderStatus::PendingNew => SbeOrderStatus::PendingNew,
2404 BinanceOrderStatus::PartiallyFilled => SbeOrderStatus::PartiallyFilled,
2405 BinanceOrderStatus::Filled => SbeOrderStatus::Filled,
2406 BinanceOrderStatus::Canceled => SbeOrderStatus::Canceled,
2407 BinanceOrderStatus::PendingCancel => SbeOrderStatus::PendingCancel,
2408 BinanceOrderStatus::Rejected => SbeOrderStatus::Rejected,
2409 BinanceOrderStatus::Expired => SbeOrderStatus::Expired,
2410 BinanceOrderStatus::ExpiredInMatch => SbeOrderStatus::ExpiredInMatch,
2411 BinanceOrderStatus::NewInsurance
2412 | BinanceOrderStatus::NewAdl
2413 | BinanceOrderStatus::Unknown => SbeOrderStatus::Unknown,
2414 }
2415}
2416
2417const fn spot_sbe_time_in_force(time_in_force: BinanceTimeInForce) -> SbeTimeInForce {
2418 match time_in_force {
2419 BinanceTimeInForce::Gtc => SbeTimeInForce::Gtc,
2420 BinanceTimeInForce::Ioc => SbeTimeInForce::Ioc,
2421 BinanceTimeInForce::Fok => SbeTimeInForce::Fok,
2422 BinanceTimeInForce::Gtx
2423 | BinanceTimeInForce::Gtd
2424 | BinanceTimeInForce::Rpi
2425 | BinanceTimeInForce::Unknown => SbeTimeInForce::NonRepresentable,
2426 }
2427}
2428
2429const fn spot_sbe_order_type(order_type: BinanceSpotOrderType) -> SbeOrderType {
2430 match order_type {
2431 BinanceSpotOrderType::Market => SbeOrderType::Market,
2432 BinanceSpotOrderType::Limit => SbeOrderType::Limit,
2433 BinanceSpotOrderType::StopLoss => SbeOrderType::StopLoss,
2434 BinanceSpotOrderType::StopLossLimit => SbeOrderType::StopLossLimit,
2435 BinanceSpotOrderType::TakeProfit => SbeOrderType::TakeProfit,
2436 BinanceSpotOrderType::TakeProfitLimit => SbeOrderType::TakeProfitLimit,
2437 BinanceSpotOrderType::LimitMaker => SbeOrderType::LimitMaker,
2438 BinanceSpotOrderType::Unknown => SbeOrderType::NonRepresentable,
2439 }
2440}
2441
2442const fn spot_sbe_order_side(side: BinanceSide) -> SbeOrderSide {
2443 match side {
2444 BinanceSide::Buy => SbeOrderSide::Buy,
2445 BinanceSide::Sell => SbeOrderSide::Sell,
2446 }
2447}
2448
2449fn spot_sbe_stp(mode: Option<BinanceSelfTradePreventionMode>) -> SbeSelfTradePreventionMode {
2450 match mode.unwrap_or(BinanceSelfTradePreventionMode::None) {
2451 BinanceSelfTradePreventionMode::None => SbeSelfTradePreventionMode::None,
2452 BinanceSelfTradePreventionMode::ExpireMaker => SbeSelfTradePreventionMode::ExpireMaker,
2453 BinanceSelfTradePreventionMode::ExpireTaker => SbeSelfTradePreventionMode::ExpireTaker,
2454 BinanceSelfTradePreventionMode::ExpireBoth => SbeSelfTradePreventionMode::ExpireBoth,
2455 BinanceSelfTradePreventionMode::Decrement => SbeSelfTradePreventionMode::Decrement,
2456 BinanceSelfTradePreventionMode::Transfer => SbeSelfTradePreventionMode::Transfer,
2457 BinanceSelfTradePreventionMode::Unknown => SbeSelfTradePreventionMode::NonRepresentable,
2458 }
2459}
2460
2461pub struct BinanceSpotHttpClient {
2467 inner: Arc<BinanceRawSpotHttpClient>,
2468 clock: &'static AtomicTime,
2469 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
2470}
2471
2472impl Clone for BinanceSpotHttpClient {
2473 fn clone(&self) -> Self {
2474 Self {
2475 inner: self.inner.clone(),
2476 clock: self.clock,
2477 instruments_cache: self.instruments_cache.clone(),
2478 }
2479 }
2480}
2481
2482impl Debug for BinanceSpotHttpClient {
2483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2484 f.debug_struct(stringify!(BinanceSpotHttpClient))
2485 .field("inner", &self.inner)
2486 .field("instruments_cached", &self.instruments_cache.len())
2487 .finish()
2488 }
2489}
2490
2491impl BinanceSpotHttpClient {
2492 #[expect(clippy::too_many_arguments)]
2498 pub fn new(
2499 environment: BinanceEnvironment,
2500 clock: &'static AtomicTime,
2501 api_key: Option<String>,
2502 api_secret: Option<String>,
2503 base_url_override: Option<String>,
2504 recv_window: Option<u64>,
2505 timeout_secs: Option<u64>,
2506 proxy_url: Option<String>,
2507 ) -> BinanceSpotHttpResult<Self> {
2508 Self::new_with_json_responses(
2509 environment,
2510 clock,
2511 api_key,
2512 api_secret,
2513 base_url_override,
2514 recv_window,
2515 timeout_secs,
2516 proxy_url,
2517 false,
2518 )
2519 }
2520
2521 pub(crate) fn with_retry_config(mut self, config: RetryConfig) -> Self {
2522 Arc::make_mut(&mut self.inner).retry_manager = Arc::new(RetryManager::new(config));
2523 self
2524 }
2525
2526 #[expect(clippy::too_many_arguments)]
2532 pub fn new_with_json_responses(
2533 environment: BinanceEnvironment,
2534 clock: &'static AtomicTime,
2535 api_key: Option<String>,
2536 api_secret: Option<String>,
2537 base_url_override: Option<String>,
2538 recv_window: Option<u64>,
2539 timeout_secs: Option<u64>,
2540 proxy_url: Option<String>,
2541 json_responses: bool,
2542 ) -> BinanceSpotHttpResult<Self> {
2543 let inner = BinanceRawSpotHttpClient::new_with_json_responses(
2544 environment,
2545 api_key,
2546 api_secret,
2547 base_url_override,
2548 recv_window,
2549 timeout_secs,
2550 proxy_url,
2551 json_responses,
2552 )?;
2553
2554 Ok(Self {
2555 inner: Arc::new(inner),
2556 clock,
2557 instruments_cache: Arc::new(AtomicMap::new()),
2558 })
2559 }
2560
2561 #[must_use]
2563 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
2564 &self.inner
2565 }
2566
2567 #[must_use]
2569 pub fn has_credentials(&self) -> bool {
2570 self.inner.has_credentials()
2571 }
2572
2573 #[must_use]
2575 pub const fn schema_id() -> u16 {
2576 SBE_SCHEMA_ID
2577 }
2578
2579 #[must_use]
2581 pub const fn schema_version() -> u16 {
2582 SBE_SCHEMA_VERSION
2583 }
2584
2585 fn generate_ts_init(&self) -> UnixNanos {
2587 self.clock.get_time_ns()
2588 }
2589
2590 fn command_validation_error(message: impl Into<String>) -> anyhow::Error {
2591 anyhow::anyhow!(BinanceSpotHttpError::ValidationError(message.into()))
2592 }
2593
2594 fn response_parse_error(message: impl Into<String>) -> anyhow::Error {
2595 anyhow::anyhow!(BinanceSpotHttpError::ResponseParseError(message.into()))
2596 }
2597
2598 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
2600 self.instruments_cache
2601 .get_cloned(&symbol)
2602 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
2603 }
2604
2605 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
2607 self.instruments_cache.rcu(move |cache| {
2608 for instrument in &instruments {
2609 cache.insert(instrument.raw_symbol().inner(), instrument.clone());
2610 }
2611 });
2612 }
2613
2614 pub fn replace_instruments(&self, instruments: &[InstrumentAny]) {
2616 let cache = instruments
2617 .iter()
2618 .map(|instrument| (instrument.raw_symbol().inner(), instrument.clone()))
2619 .collect();
2620 self.instruments_cache.store(cache);
2621 }
2622
2623 pub fn cache_instrument(&self, instrument: InstrumentAny) {
2625 self.instruments_cache
2626 .insert(instrument.raw_symbol().inner(), instrument);
2627 }
2628
2629 #[must_use]
2631 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
2632 self.instruments_cache.get_cloned(symbol)
2633 }
2634
2635 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
2641 self.inner.ping().await
2642 }
2643
2644 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
2652 self.inner.server_time().await
2653 }
2654
2655 pub async fn exchange_info(
2661 &self,
2662 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
2663 self.inner.exchange_info().await
2664 }
2665
2666 pub async fn request_symbol_statuses(
2672 &self,
2673 us: bool,
2674 ) -> BinanceSpotHttpResult<AHashMap<InstrumentId, MarketStatusAction>> {
2675 let mut statuses = AHashMap::new();
2676
2677 if us {
2678 let info = self.inner.exchange_info_json().await?;
2679 for symbol in info.symbols {
2680 let instrument_id =
2681 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2682 statuses.insert(instrument_id, spot_json_market_status(&symbol.status));
2683 }
2684 } else {
2685 let info = self.exchange_info().await?;
2686 for symbol in info.symbols {
2687 let instrument_id =
2688 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2689 statuses.insert(
2690 instrument_id,
2691 MarketStatusAction::from(SymbolStatus::from(symbol.status)),
2692 );
2693 }
2694 }
2695 Ok(statuses)
2696 }
2697
2698 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2707 self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default(), false)
2708 .await
2709 }
2710
2711 pub async fn request_instruments_with_config(
2720 &self,
2721 config: &BinanceInstrumentProviderConfig,
2722 us: bool,
2723 ) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2724 config
2725 .validate(BinanceProductType::Spot)
2726 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2727 let selector = BinanceInstrumentSelector::new(config)
2728 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2729 let ts_init = self.generate_ts_init();
2730 let fallback_fees = self.spot_fallback_fees(us).await;
2731
2732 let mut instruments = if us {
2733 if config.query_commission_rates {
2734 if config.log_warnings {
2735 log::warn!(
2736 "Binance US does not expose the Global account/commission endpoint; using account-wide commission rates"
2737 );
2738 } else {
2739 log::debug!(
2740 "Binance US exact per-symbol commission query disabled; using account-wide rates"
2741 );
2742 }
2743 }
2744 let info = self.inner.exchange_info_json().await?;
2745 let mut instruments = Vec::with_capacity(info.symbols.len());
2746 for symbol in &info.symbols {
2747 let instrument_id =
2748 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2749
2750 if !selector.includes(
2751 instrument_id,
2752 &symbol.symbol,
2753 &symbol.base_asset,
2754 &symbol.quote_asset,
2755 None,
2756 ) {
2757 continue;
2758 }
2759
2760 match parse_spot_instrument_json_with_fees(
2761 symbol,
2762 Some(fallback_fees.0),
2763 Some(fallback_fees.1),
2764 ts_init,
2765 ts_init,
2766 ) {
2767 Ok(instrument) => instruments.push(instrument),
2768 Err(e) => log_instrument_parse_error(
2769 config,
2770 &selector,
2771 instrument_id,
2772 &symbol.symbol,
2773 &e,
2774 ),
2775 }
2776 }
2777 instruments
2778 } else {
2779 let info = self.exchange_info().await?;
2780 let mut instruments = Vec::with_capacity(info.symbols.len());
2781 for symbol in &info.symbols {
2782 let instrument_id =
2783 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2784
2785 if !selector.includes(
2786 instrument_id,
2787 &symbol.symbol,
2788 &symbol.base_asset,
2789 &symbol.quote_asset,
2790 None,
2791 ) {
2792 continue;
2793 }
2794
2795 let fees = self
2796 .spot_symbol_fees(config, &symbol.symbol, fallback_fees)
2797 .await;
2798
2799 match parse_spot_instrument_sbe_with_fees(
2800 symbol,
2801 Some(fees.0),
2802 Some(fees.1),
2803 ts_init,
2804 ts_init,
2805 ) {
2806 Ok(instrument) => instruments.push(instrument),
2807 Err(e) => log_instrument_parse_error(
2808 config,
2809 &selector,
2810 instrument_id,
2811 &symbol.symbol,
2812 &e,
2813 ),
2814 }
2815 }
2816 instruments
2817 };
2818
2819 instruments.shrink_to_fit();
2820 self.replace_instruments(&instruments);
2821
2822 log::debug!("Loaded spot instruments: count={}", instruments.len());
2823 Ok(instruments)
2824 }
2825
2826 async fn spot_fallback_fees(&self, us: bool) -> (Decimal, Decimal) {
2827 if !self.has_credentials() {
2828 return (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT);
2829 }
2830
2831 let result = if us {
2832 self.inner.account_rates_json().await.map(|account| {
2833 parse_commission_rates(
2834 &account.commission_rates.maker,
2835 &account.commission_rates.taker,
2836 )
2837 })
2838 } else {
2839 self.inner
2840 .account(&AccountInfoParams::default())
2841 .await
2842 .map(|account| {
2843 Ok((
2844 decimal_from_mantissa_exponent(
2845 account.maker_commission_mantissa,
2846 account.commission_exponent,
2847 ),
2848 decimal_from_mantissa_exponent(
2849 account.taker_commission_mantissa,
2850 account.commission_exponent,
2851 ),
2852 ))
2853 })
2854 };
2855
2856 match result {
2857 Ok(Ok(fees)) => fees,
2858 Ok(Err(e)) => {
2859 log::warn!("Invalid Binance Spot account commission rates: {e}; using fallback");
2860 (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2861 }
2862 Err(e) => {
2863 log::warn!("Binance Spot account commission query failed: {e}; using fallback");
2864 (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2865 }
2866 }
2867 }
2868
2869 async fn spot_symbol_fees(
2870 &self,
2871 config: &BinanceInstrumentProviderConfig,
2872 symbol: &str,
2873 fallback: (Decimal, Decimal),
2874 ) -> (Decimal, Decimal) {
2875 if !config.query_commission_rates || !self.has_credentials() {
2876 return fallback;
2877 }
2878
2879 match self.inner.account_commission(symbol).await {
2880 Ok(response) => match parse_commission_rates(
2881 &response.standard_commission.maker,
2882 &response.standard_commission.taker,
2883 ) {
2884 Ok(fees) => fees,
2885 Err(e) => {
2886 log::warn!(
2887 "Invalid Binance Spot commission response for {symbol}: {e}; using fallback"
2888 );
2889 fallback
2890 }
2891 },
2892 Err(e) => {
2893 log::warn!(
2894 "Binance Spot commission query failed for {symbol}: {e}; using fallback"
2895 );
2896 fallback
2897 }
2898 }
2899 }
2900
2901 pub async fn request_trades(
2908 &self,
2909 instrument_id: InstrumentId,
2910 limit: Option<u32>,
2911 ) -> anyhow::Result<Vec<TradeTick>> {
2912 let symbol = instrument_id.symbol.inner();
2913 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2914 let ts_init = self.generate_ts_init();
2915
2916 let trades = self
2917 .inner
2918 .trades(symbol.as_str(), limit)
2919 .await
2920 .map_err(|e| anyhow::anyhow!(e))?;
2921
2922 parse_spot_trades_sbe(&trades, &instrument, ts_init)
2923 }
2924
2925 pub async fn request_agg_trades(
2931 &self,
2932 instrument_id: InstrumentId,
2933 start: Option<Timestamp>,
2934 end: Option<Timestamp>,
2935 limit: Option<u32>,
2936 ) -> anyhow::Result<Vec<TradeTick>> {
2937 let symbol = instrument_id.symbol.inner();
2938 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2939 let params = AggTradesParams {
2940 symbol: symbol.to_string(),
2941 from_id: None,
2942 start_time: start.map(|dt| dt.as_millisecond()),
2943 end_time: end.map(|dt| dt.as_millisecond()),
2944 limit,
2945 };
2946 let response = self.inner.agg_trades(¶ms).await?;
2947 let trades = BinanceTrades {
2948 price_exponent: response.price_exponent,
2949 qty_exponent: response.qty_exponent,
2950 trades: response
2951 .trades
2952 .into_iter()
2953 .map(|trade| super::models::BinanceTrade {
2954 id: trade.id,
2955 price_mantissa: trade.price_mantissa,
2956 qty_mantissa: trade.qty_mantissa,
2957 quote_qty_mantissa: 0,
2958 time: trade.time,
2959 is_buyer_maker: trade.is_buyer_maker,
2960 is_best_match: trade.is_best_match,
2961 })
2962 .collect(),
2963 };
2964
2965 let mut parsed = parse_spot_trades_sbe(&trades, &instrument, UnixNanos::default())?;
2966 for trade in &mut parsed {
2967 trade.ts_init = trade.ts_event;
2968 }
2969 Ok(parsed)
2970 }
2971
2972 pub async fn request_binance_bars(
2979 &self,
2980 bar_type: BarType,
2981 start: Option<Timestamp>,
2982 end: Option<Timestamp>,
2983 limit: Option<u32>,
2984 ) -> anyhow::Result<Vec<crate::common::bar::BinanceBar>> {
2985 anyhow::ensure!(
2986 bar_type.aggregation_source() == AggregationSource::External,
2987 "Only EXTERNAL aggregation is supported"
2988 );
2989
2990 let spec = bar_type.spec();
2991 let step = spec.step.get();
2992 let interval = match spec.aggregation {
2993 BarAggregation::Second if step == 1 => "1s".to_string(),
2994 BarAggregation::Second => {
2995 anyhow::bail!("Binance Spot supports only the 1s kline interval")
2996 }
2997 BarAggregation::Minute => format!("{step}m"),
2998 BarAggregation::Hour => format!("{step}h"),
2999 BarAggregation::Day => format!("{step}d"),
3000 BarAggregation::Week => format!("{step}w"),
3001 BarAggregation::Month => format!("{step}M"),
3002 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
3003 };
3004
3005 let instrument_id = bar_type.instrument_id();
3006 let symbol = instrument_id.symbol;
3007 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3008 let klines = self
3009 .inner
3010 .klines(
3011 symbol.as_str(),
3012 &interval,
3013 start.map(|dt| dt.as_millisecond()),
3014 end.map(|dt| dt.as_millisecond()),
3015 limit,
3016 )
3017 .await
3018 .map_err(|e| anyhow::anyhow!(e))?;
3019
3020 let mut bars =
3021 parse_klines_to_binance_bars(&klines, bar_type, &instrument, UnixNanos::default())?;
3022 let now = self.clock.get_time_ns();
3023 bars.retain(|bar| bar.ts_event < now);
3024 for bar in &mut bars {
3025 bar.ts_init = bar.ts_event;
3026 }
3027 Ok(bars)
3028 }
3029
3030 pub async fn request_bars(
3036 &self,
3037 bar_type: BarType,
3038 start: Option<Timestamp>,
3039 end: Option<Timestamp>,
3040 limit: Option<u32>,
3041 ) -> anyhow::Result<Vec<Bar>> {
3042 Ok(self
3043 .request_binance_bars(bar_type, start, end, limit)
3044 .await?
3045 .into_iter()
3046 .map(|bar| bar.bar())
3047 .collect())
3048 }
3049
3050 pub async fn request_book_snapshot(
3056 &self,
3057 instrument_id: InstrumentId,
3058 depth: Option<u32>,
3059 ) -> anyhow::Result<OrderBook> {
3060 if depth.is_some_and(|value| value == 0 || value > 5000) {
3061 anyhow::bail!("Binance Spot order-book depth must be between 1 and 5000");
3062 }
3063 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3064 let params = DepthParams {
3065 symbol: instrument_id.symbol.to_string(),
3066 limit: depth,
3067 };
3068 let snapshot = self.inner.depth(¶ms).await?;
3069 let ts_event = self.generate_ts_init();
3070 Self::parse_book_snapshot_response(instrument_id, &instrument, &snapshot, ts_event)
3071 }
3072
3073 fn parse_book_snapshot_response(
3074 instrument_id: InstrumentId,
3075 instrument: &InstrumentAny,
3076 snapshot: &BinanceDepth,
3077 ts_event: UnixNanos,
3078 ) -> anyhow::Result<OrderBook> {
3079 let sequence = u64::try_from(snapshot.last_update_id)
3080 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3081 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3082 let mut add_level = |level: &super::models::BinancePriceLevel,
3083 side: OrderSide,
3084 order_id: usize,
3085 name: &str|
3086 -> anyhow::Result<()> {
3087 let price = Price::from_mantissa_exponent_checked(
3088 level.price_mantissa,
3089 snapshot.price_exponent,
3090 instrument.price_precision(),
3091 )
3092 .map_err(|e| anyhow::anyhow!("invalid {name} price: {e}"))?;
3093 anyhow::ensure!(price.is_positive(), "invalid non-positive {name} price");
3094 let qty_mantissa = u64::try_from(level.qty_mantissa)
3095 .map_err(|_| anyhow::anyhow!("invalid negative {name} quantity"))?;
3096 let quantity = Quantity::from_mantissa_exponent_checked(
3097 qty_mantissa,
3098 snapshot.qty_exponent,
3099 instrument.size_precision(),
3100 )
3101 .map_err(|e| anyhow::anyhow!("invalid {name} quantity: {e}"))?;
3102 anyhow::ensure!(
3103 quantity.is_positive(),
3104 "invalid non-positive {name} quantity"
3105 );
3106 let order = BookOrder::new(
3107 side,
3108 price,
3109 quantity,
3110 u64::try_from(order_id)
3111 .map_err(|_| anyhow::anyhow!("order-book level index overflow"))?,
3112 );
3113 book.add(order, 0, sequence, ts_event);
3114 Ok(())
3115 };
3116
3117 for (index, level) in snapshot.bids.iter().enumerate() {
3118 add_level(level, OrderSide::Buy, index, "bid")?;
3119 }
3120 let bid_count = snapshot.bids.len();
3121 for (index, level) in snapshot.asks.iter().enumerate() {
3122 let order_id = bid_count
3123 .checked_add(index)
3124 .ok_or_else(|| anyhow::anyhow!("order-book level index overflow"))?;
3125 add_level(level, OrderSide::Sell, order_id, "ask")?;
3126 }
3127 Ok(book)
3128 }
3129
3130 fn instrument_from_cache_by_id(
3131 &self,
3132 instrument_id: InstrumentId,
3133 ) -> anyhow::Result<InstrumentAny> {
3134 self.instruments_cache
3135 .get_cloned(&instrument_id.symbol.inner())
3136 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
3137 }
3138
3139 pub async fn request_account_state(
3145 &self,
3146 account_id: AccountId,
3147 ) -> anyhow::Result<AccountState> {
3148 let ts_init = self.clock.get_time_ns();
3149 let params = AccountInfoParams::default();
3150 let account_info = self.inner.account(¶ms).await?;
3151 Ok(account_info.to_account_state(account_id, ts_init))
3152 }
3153
3154 pub async fn request_order_status_report(
3163 &self,
3164 account_id: AccountId,
3165 instrument_id: InstrumentId,
3166 venue_order_id: Option<VenueOrderId>,
3167 client_order_id: Option<ClientOrderId>,
3168 ) -> anyhow::Result<Option<OrderStatusReport>> {
3169 anyhow::ensure!(
3170 venue_order_id.is_some() || client_order_id.is_some(),
3171 "Either venue_order_id or client_order_id must be provided"
3172 );
3173
3174 let symbol = instrument_id.symbol.inner();
3175 let instrument = self.instrument_from_cache(symbol)?;
3176 let ts_init = self.generate_ts_init();
3177
3178 let order_id = venue_order_id
3179 .map(|id| id.inner().parse::<i64>())
3180 .transpose()
3181 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3182
3183 let client_id_str =
3184 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3185
3186 let order = match self
3187 .inner
3188 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
3189 .await
3190 {
3191 Ok(order) => order,
3192 Err(e) if Self::is_no_such_order_error(&e) => {
3193 log::debug!("Binance Spot order not found: instrument_id={instrument_id}");
3194 return Ok(None);
3195 }
3196 Err(e) => anyhow::bail!(e),
3197 };
3198
3199 parse_order_status_report_sbe(
3200 &order,
3201 account_id,
3202 &instrument,
3203 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3204 ts_init,
3205 )
3206 .map(Some)
3207 }
3208
3209 const fn is_no_such_order_error(error: &BinanceSpotHttpError) -> bool {
3210 matches!(
3211 error,
3212 BinanceSpotHttpError::BinanceError { code, .. } if *code == BINANCE_NO_SUCH_ORDER_CODE
3213 )
3214 }
3215
3216 pub async fn request_order_status_reports(
3226 &self,
3227 account_id: AccountId,
3228 instrument_id: Option<InstrumentId>,
3229 start: Option<Timestamp>,
3230 end: Option<Timestamp>,
3231 open_only: bool,
3232 limit: Option<u32>,
3233 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3234 self.request_order_status_reports_scoped(
3235 account_id,
3236 instrument_id,
3237 start,
3238 end,
3239 open_only,
3240 limit,
3241 None,
3242 )
3243 .await
3244 }
3245
3246 #[expect(clippy::too_many_arguments)]
3247 pub(crate) async fn request_order_status_reports_scoped(
3248 &self,
3249 account_id: AccountId,
3250 instrument_id: Option<InstrumentId>,
3251 start: Option<Timestamp>,
3252 end: Option<Timestamp>,
3253 open_only: bool,
3254 limit: Option<u32>,
3255 provider: Option<&BinanceInstrumentProviderConfig>,
3256 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3257 if instrument_id.is_some_and(|id| provider.is_some_and(|provider| provider.excludes(id))) {
3258 log::debug!("Dropping out-of-scope Binance Spot order request for {instrument_id:?}");
3259 return Ok(Vec::new());
3260 }
3261 let ts_init = self.generate_ts_init();
3262 let symbol = instrument_id.map(|id| id.symbol.to_string());
3263
3264 let orders = if open_only {
3265 self.inner
3266 .open_orders(symbol.as_deref())
3267 .await
3268 .map_err(|e| anyhow::anyhow!(e))?
3269 } else {
3270 let symbol = symbol
3271 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
3272 self.inner
3273 .all_orders(
3274 &symbol,
3275 start.map(|dt| dt.as_millisecond()),
3276 end.map(|dt| dt.as_millisecond()),
3277 limit,
3278 )
3279 .await
3280 .map_err(|e| anyhow::anyhow!(e))?
3281 };
3282
3283 orders
3284 .iter()
3285 .filter(|order| {
3286 let id = format_instrument_id(&Ustr::from(&order.symbol), BinanceProductType::Spot);
3287 if provider.is_some_and(|provider| provider.excludes(id)) {
3288 log::debug!("Dropping out-of-scope Binance Spot order for {id}");
3289 false
3290 } else {
3291 true
3292 }
3293 })
3294 .map(|order| {
3295 let symbol = Ustr::from(&order.symbol);
3296 let instrument = self.instrument_from_cache(symbol)?;
3297 parse_order_status_report_sbe(
3298 order,
3299 account_id,
3300 &instrument,
3301 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3302 ts_init,
3303 )
3304 })
3305 .collect()
3306 }
3307
3308 pub async fn request_fill_reports(
3315 &self,
3316 account_id: AccountId,
3317 instrument_id: InstrumentId,
3318 venue_order_id: Option<VenueOrderId>,
3319 start: Option<Timestamp>,
3320 end: Option<Timestamp>,
3321 limit: Option<u32>,
3322 ) -> anyhow::Result<Vec<FillReport>> {
3323 self.request_fill_reports_with_cursor(
3324 account_id,
3325 instrument_id,
3326 venue_order_id,
3327 start,
3328 end,
3329 None,
3330 limit,
3331 )
3332 .await
3333 }
3334
3335 #[expect(clippy::too_many_arguments)]
3336 pub(crate) async fn request_fill_reports_with_cursor(
3337 &self,
3338 account_id: AccountId,
3339 instrument_id: InstrumentId,
3340 venue_order_id: Option<VenueOrderId>,
3341 start: Option<Timestamp>,
3342 end: Option<Timestamp>,
3343 from_id: Option<i64>,
3344 limit: Option<u32>,
3345 ) -> anyhow::Result<Vec<FillReport>> {
3346 let ts_init = self.generate_ts_init();
3347 let symbol = instrument_id.symbol.inner();
3348
3349 let order_id = venue_order_id
3350 .map(|id| id.inner().parse::<i64>())
3351 .transpose()
3352 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3353
3354 let trades = self
3355 .inner
3356 .account_trades_with_cursor(
3357 symbol.as_str(),
3358 order_id,
3359 start.map(|dt| dt.as_millisecond()),
3360 end.map(|dt| dt.as_millisecond()),
3361 from_id,
3362 limit,
3363 )
3364 .await
3365 .map_err(|e| anyhow::anyhow!(e))?;
3366
3367 trades
3368 .iter()
3369 .map(|trade| {
3370 let symbol = Ustr::from(&trade.symbol);
3371 let instrument = self.instrument_from_cache(symbol)?;
3372 let commission_currency = get_currency(&trade.commission_asset);
3373 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
3374 })
3375 .collect()
3376 }
3377
3378 #[expect(clippy::too_many_arguments)]
3391 pub async fn submit_order(
3392 &self,
3393 account_id: AccountId,
3394 instrument_id: InstrumentId,
3395 client_order_id: ClientOrderId,
3396 order_side: OrderSide,
3397 order_type: OrderType,
3398 quantity: Quantity,
3399 time_in_force: TimeInForce,
3400 price: Option<Price>,
3401 trigger_price: Option<Price>,
3402 post_only: bool,
3403 quote_quantity: bool,
3404 display_qty: Option<Quantity>,
3405 use_gtd: bool,
3406 ) -> anyhow::Result<OrderStatusReport> {
3407 let symbol = instrument_id.symbol.inner();
3408 let instrument = self
3409 .instrument_from_cache(symbol)
3410 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3411 let ts_init = self.generate_ts_init();
3412
3413 let binance_side = BinanceSide::try_from(order_side)
3414 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3415 let binance_order_type = order_type_to_binance_spot(order_type, post_only)
3416 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3417
3418 let requires_trigger = matches!(
3420 order_type,
3421 OrderType::StopMarket
3422 | OrderType::StopLimit
3423 | OrderType::MarketIfTouched
3424 | OrderType::LimitIfTouched
3425 );
3426
3427 if requires_trigger && trigger_price.is_none() {
3428 return Err(Self::command_validation_error(
3429 "Conditional orders require a trigger price",
3430 ));
3431 }
3432
3433 let requires_price = matches!(
3435 binance_order_type,
3436 BinanceSpotOrderType::Limit
3437 | BinanceSpotOrderType::StopLossLimit
3438 | BinanceSpotOrderType::TakeProfitLimit
3439 | BinanceSpotOrderType::LimitMaker
3440 );
3441
3442 if requires_price && price.is_none() {
3443 return Err(Self::command_validation_error(format!(
3444 "{binance_order_type:?} orders require a price"
3445 )));
3446 }
3447
3448 let supports_tif = matches!(
3450 binance_order_type,
3451 BinanceSpotOrderType::Limit
3452 | BinanceSpotOrderType::StopLossLimit
3453 | BinanceSpotOrderType::TakeProfitLimit
3454 );
3455 let binance_tif = if supports_tif {
3456 Some(
3457 time_in_force_to_binance_spot(time_in_force, use_gtd)
3458 .map_err(|e| Self::command_validation_error(e.to_string()))?,
3459 )
3460 } else {
3461 None
3462 };
3463
3464 let qty_str = quantity.to_string();
3465 let price_str = price.map(|p| p.to_string());
3466 let stop_price_str = trigger_price.map(|p| p.to_string());
3467 let iceberg_qty_str = display_qty.map(|q| q.to_string());
3468 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3469
3470 if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
3471 return Err(Self::command_validation_error(
3472 "quoteOrderQty is only supported for MARKET orders",
3473 ));
3474 }
3475
3476 let (base_qty, quote_qty) = if quote_quantity {
3477 (None, Some(qty_str.as_str()))
3478 } else {
3479 (Some(qty_str.as_str()), None)
3480 };
3481
3482 let response = self
3483 .inner
3484 .new_order_full(
3485 symbol.as_str(),
3486 binance_side,
3487 binance_order_type,
3488 binance_tif,
3489 base_qty,
3490 quote_qty,
3491 price_str.as_deref(),
3492 Some(&client_id_str),
3493 stop_price_str.as_deref(),
3494 iceberg_qty_str.as_deref(),
3495 )
3496 .await?;
3497
3498 parse_new_order_response_sbe(
3499 &response,
3500 account_id,
3501 &instrument,
3502 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3503 ts_init,
3504 )
3505 .map_err(|e| Self::response_parse_error(e.to_string()))
3506 }
3507
3508 pub async fn submit_order_list(
3516 &self,
3517 orders: &[BatchOrderItem],
3518 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
3519 self.inner.batch_submit_orders(orders).await
3520 }
3521
3522 pub async fn submit_oco_order_list(
3528 &self,
3529 params: &NewOcoOrderListParams,
3530 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
3531 self.inner.new_oco_order_list(params).await
3532 }
3533
3534 #[expect(clippy::too_many_arguments)]
3543 pub async fn modify_order(
3544 &self,
3545 account_id: AccountId,
3546 instrument_id: InstrumentId,
3547 venue_order_id: VenueOrderId,
3548 client_order_id: ClientOrderId,
3549 order_side: OrderSide,
3550 order_type: OrderType,
3551 quantity: Quantity,
3552 time_in_force: TimeInForce,
3553 price: Option<Price>,
3554 use_gtd: bool,
3555 cancel_new_client_order_id: &str,
3556 ) -> anyhow::Result<OrderStatusReport> {
3557 let symbol = instrument_id.symbol.inner();
3558 let instrument = self
3559 .instrument_from_cache(symbol)
3560 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3561 let ts_init = self.generate_ts_init();
3562
3563 let binance_side = BinanceSide::try_from(order_side)
3564 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3565 let binance_order_type = order_type_to_binance_spot(order_type, false)
3566 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3567 let binance_tif = time_in_force_to_binance_spot(time_in_force, use_gtd)
3568 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3569
3570 let cancel_order_id: i64 = venue_order_id.inner().parse().map_err(|_| {
3571 Self::command_validation_error(format!("Invalid venue order ID: {venue_order_id}"))
3572 })?;
3573
3574 let qty_str = quantity.to_string();
3575 let price_str = price.map(|p| p.to_string());
3576 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3577
3578 let response = self
3579 .inner
3580 .cancel_replace_order(
3581 symbol.as_str(),
3582 binance_side,
3583 binance_order_type,
3584 Some(binance_tif),
3585 Some(&qty_str),
3586 price_str.as_deref(),
3587 Some(cancel_order_id),
3588 None,
3589 Some(cancel_new_client_order_id),
3590 Some(&client_id_str),
3591 )
3592 .await
3593 .map_err(|e| anyhow::anyhow!(e))?;
3594
3595 parse_new_order_response_sbe(
3596 &response,
3597 account_id,
3598 &instrument,
3599 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3600 ts_init,
3601 )
3602 .map_err(|e| Self::response_parse_error(e.to_string()))
3603 }
3604
3605 pub async fn cancel_order(
3613 &self,
3614 instrument_id: InstrumentId,
3615 venue_order_id: Option<VenueOrderId>,
3616 client_order_id: Option<ClientOrderId>,
3617 ) -> anyhow::Result<VenueOrderId> {
3618 let symbol = instrument_id.symbol.inner();
3619
3620 let order_id = match venue_order_id {
3621 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
3622 Ok(order_id) => Some(order_id),
3623 Err(e) if client_order_id.is_some() => {
3624 log::warn!(
3625 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
3626 );
3627 None
3628 }
3629 Err(e) => {
3630 return Err(Self::command_validation_error(format!(
3631 "Invalid venue order ID: {e}"
3632 )));
3633 }
3634 },
3635 None => None,
3636 };
3637
3638 let client_id_str =
3639 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3640
3641 let response = self
3642 .inner
3643 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
3644 .await
3645 .map_err(|e| anyhow::anyhow!(e))?;
3646
3647 Ok(VenueOrderId::new(response.order_id.to_string()))
3648 }
3649
3650 pub async fn batch_cancel_orders(
3658 &self,
3659 cancels: &[BatchCancelItem],
3660 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
3661 self.inner.batch_cancel_orders(cancels).await
3662 }
3663
3664 pub async fn cancel_all_orders(
3672 &self,
3673 instrument_id: InstrumentId,
3674 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
3675 let responses = self.cancel_all_order_responses(instrument_id).await?;
3676
3677 responses
3678 .into_iter()
3679 .flat_map(|response| match response {
3680 BinanceCancelOpenOrdersResponse::Order(response) => vec![response],
3681 BinanceCancelOpenOrdersResponse::OrderList(response) => response.order_reports,
3682 })
3683 .map(|response| {
3684 Ok((
3685 VenueOrderId::new(response.order_id.to_string()),
3686 decode_client_order_id(
3687 &response.orig_client_order_id,
3688 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3689 )?,
3690 ))
3691 })
3692 .collect()
3693 }
3694
3695 pub(crate) async fn cancel_all_order_responses(
3701 &self,
3702 instrument_id: InstrumentId,
3703 ) -> anyhow::Result<Vec<BinanceCancelOpenOrdersResponse>> {
3704 let symbol = instrument_id.symbol.inner();
3705
3706 let responses = self
3707 .inner
3708 .cancel_open_order_responses(symbol.as_str())
3709 .await
3710 .map_err(|e| anyhow::anyhow!(e))?;
3711
3712 Ok(responses)
3713 }
3714}
3715
3716fn parse_commission_rates(maker: &str, taker: &str) -> anyhow::Result<(Decimal, Decimal)> {
3717 Ok((
3718 Decimal::from_str_exact(maker)?,
3719 Decimal::from_str_exact(taker)?,
3720 ))
3721}
3722
3723fn spot_json_market_status(status: &str) -> MarketStatusAction {
3724 match status {
3725 "TRADING" => MarketStatusAction::Trading,
3726 "BREAK" => MarketStatusAction::Pause,
3727 _ => MarketStatusAction::NotAvailableForTrading,
3728 }
3729}
3730
3731fn decimal_from_mantissa_exponent(mantissa: i64, exponent: i8) -> Decimal {
3732 if exponent >= 0 {
3733 Decimal::from(mantissa) * Decimal::from(10_i64.pow(exponent as u32))
3734 } else {
3735 Decimal::new(mantissa, (-exponent) as u32)
3736 }
3737}
3738
3739fn log_instrument_parse_error(
3740 config: &BinanceInstrumentProviderConfig,
3741 selector: &BinanceInstrumentSelector,
3742 instrument_id: InstrumentId,
3743 symbol: &str,
3744 error: &anyhow::Error,
3745) {
3746 let explicit = selector.is_explicit(instrument_id, symbol);
3747 if should_warn_on_instrument_parse_error(config.log_warnings, explicit, error) {
3748 log::warn!("Skipping Binance Spot instrument {symbol}: {error}");
3749 } else {
3750 log::debug!("Skipping Binance Spot instrument {symbol}: {error}");
3751 }
3752}
3753
3754#[cfg(test)]
3755mod tests {
3756 use nautilus_model::instruments::stubs::currency_pair_btcusdt;
3757 use nautilus_testkit::http::assert_http_redirect_rejected;
3758 use rstest::rstest;
3759
3760 use super::*;
3761 use crate::spot::http::models::BinancePriceLevel;
3762
3763 #[tokio::test]
3764 async fn test_authenticated_client_rejects_redirects() {
3765 let client = BinanceRawSpotHttpClient::new(
3766 BinanceEnvironment::Testnet,
3767 Some("key".into()),
3768 Some("secret".into()),
3769 None,
3770 None,
3771 Some(3),
3772 None,
3773 )
3774 .unwrap()
3775 .client;
3776 assert_http_redirect_rejected(|url| async move {
3777 client
3778 .get(url, None, None, Some(3), None)
3779 .await
3780 .unwrap()
3781 .status
3782 .as_u16()
3783 })
3784 .await;
3785 }
3786
3787 #[rstest]
3788 fn test_schema_constants() {
3789 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
3790 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 5);
3791 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
3792 assert_eq!(BinanceSpotHttpClient::schema_version(), 5);
3793 }
3794
3795 #[rstest]
3796 fn test_sbe_schema_header() {
3797 assert_eq!(SBE_SCHEMA_HEADER, "3:5");
3798 }
3799
3800 #[rstest]
3801 fn test_parse_book_snapshot_response_rejects_negative_update_id() {
3802 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3803 let snapshot = BinanceDepth {
3804 last_update_id: -1,
3805 price_exponent: -2,
3806 qty_exponent: -5,
3807 bids: vec![],
3808 asks: vec![],
3809 };
3810
3811 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3812 InstrumentId::from("BTCUSDT.BINANCE"),
3813 &instrument,
3814 &snapshot,
3815 UnixNanos::from(1_700_000_000_000_000_001u64),
3816 )
3817 .unwrap_err();
3818
3819 assert_eq!(error.to_string(), "invalid negative order-book update ID");
3820 }
3821
3822 #[rstest]
3823 fn test_parse_book_snapshot_response_rejects_negative_quantity() {
3824 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3825 let snapshot = BinanceDepth {
3826 last_update_id: 12345,
3827 price_exponent: -2,
3828 qty_exponent: -5,
3829 bids: vec![BinancePriceLevel {
3830 price_mantissa: 4_200_001,
3831 qty_mantissa: -1,
3832 }],
3833 asks: vec![],
3834 };
3835
3836 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3837 InstrumentId::from("BTCUSDT.BINANCE"),
3838 &instrument,
3839 &snapshot,
3840 UnixNanos::from(1_700_000_000_000_000_001u64),
3841 )
3842 .unwrap_err();
3843
3844 assert_eq!(error.to_string(), "invalid negative bid quantity");
3845 }
3846
3847 #[rstest]
3848 fn test_default_headers_include_sbe() {
3849 let headers = BinanceRawSpotHttpClient::default_headers(&None, false);
3850
3851 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
3852 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:5".to_string()));
3853 }
3854
3855 #[rstest]
3856 fn test_json_headers_exclude_sbe() {
3857 let headers = BinanceRawSpotHttpClient::default_headers(&None, true);
3858
3859 assert_eq!(headers.get("Accept"), Some(&"application/json".to_string()));
3860 assert_eq!(headers.get("X-MBX-SBE"), None);
3861 }
3862
3863 #[rstest]
3864 fn test_spot_cancel_open_orders_from_json_decodes_mixed_results() {
3865 let bytes = serde_json::to_vec(&serde_json::json!([
3866 {
3867 "symbol": "BTCUSDT",
3868 "orderId": 11,
3869 "orderListId": -1,
3870 "clientOrderId": "cancel-11",
3871 "origClientOrderId": "orig-11",
3872 "transactTime": 1_734_300_000_000_i64,
3873 "price": "100.00",
3874 "origQty": "1.000",
3875 "executedQty": "0.000",
3876 "cummulativeQuoteQty": "0.00",
3877 "status": "CANCELED",
3878 "timeInForce": "GTC",
3879 "type": "LIMIT",
3880 "side": "SELL"
3881 },
3882 {
3883 "orderListId": 44,
3884 "contingencyType": "OCO",
3885 "listStatusType": "ALL_DONE",
3886 "listOrderStatus": "ALL_DONE",
3887 "listClientOrderId": "list-44",
3888 "transactionTime": 1_734_300_000_000_i64,
3889 "symbol": "BTCUSDT",
3890 "orders": [
3891 {"symbol": "BTCUSDT", "orderId": 21, "clientOrderId": "orig-21"},
3892 {"symbol": "BTCUSDT", "orderId": 22, "clientOrderId": "orig-22"}
3893 ],
3894 "orderReports": [
3895 {
3896 "symbol": "BTCUSDT",
3897 "orderId": 21,
3898 "orderListId": 44,
3899 "clientOrderId": "cancel-list",
3900 "origClientOrderId": "orig-21",
3901 "transactTime": 1_734_300_000_000_i64,
3902 "price": "101.00",
3903 "origQty": "1.000",
3904 "executedQty": "0.000",
3905 "cummulativeQuoteQty": "0.00",
3906 "status": "CANCELED",
3907 "timeInForce": "GTC",
3908 "type": "LIMIT_MAKER",
3909 "side": "SELL"
3910 },
3911 {
3912 "symbol": "BTCUSDT",
3913 "orderId": 22,
3914 "orderListId": 44,
3915 "clientOrderId": "cancel-list",
3916 "origClientOrderId": "orig-22",
3917 "transactTime": 1_734_300_000_000_i64,
3918 "price": "99.00",
3919 "origQty": "1.000",
3920 "executedQty": "0.000",
3921 "cummulativeQuoteQty": "0.00",
3922 "status": "CANCELED",
3923 "timeInForce": "GTC",
3924 "type": "STOP_LOSS_LIMIT",
3925 "side": "SELL"
3926 }
3927 ]
3928 }
3929 ]))
3930 .unwrap();
3931
3932 let responses = spot_cancel_open_orders_from_json(&bytes).unwrap();
3933
3934 assert_eq!(responses.len(), 2);
3935 assert!(matches!(
3936 &responses[0],
3937 BinanceCancelOpenOrdersResponse::Order(order) if order.order_id == 11
3938 ));
3939 let BinanceCancelOpenOrdersResponse::OrderList(order_list) = &responses[1] else {
3940 panic!("Expected JSON order-list result");
3941 };
3942 assert_eq!(order_list.order_list_id, 44);
3943 assert_eq!(order_list.transaction_time, 1_734_300_000_000_000);
3944 assert_eq!(order_list.orders.len(), 2);
3945 assert_eq!(order_list.order_reports.len(), 2);
3946 assert_eq!(order_list.order_reports[0].orig_client_order_id, "orig-21");
3947 assert_eq!(order_list.order_reports[1].orig_client_order_id, "orig-22");
3948 }
3949
3950 #[rstest]
3951 fn test_spot_cancel_open_orders_from_json_rejects_partial_order_list_shape() {
3952 let bytes = br#"[{"orders": []}]"#;
3953
3954 let error = spot_cancel_open_orders_from_json(bytes).unwrap_err();
3955
3956 assert!(matches!(error, BinanceSpotHttpError::JsonError(_)));
3957 }
3958
3959 #[rstest]
3960 fn test_spot_trades_from_json_preserves_all_fields() {
3961 let response: Vec<SpotTradeJson> = serde_json::from_value(serde_json::json!([{
3962 "id": 17,
3963 "price": "123.45",
3964 "qty": "0.06789",
3965 "quoteQty": "8.3810205",
3966 "time": 1_700_000_000_123_i64,
3967 "isBuyerMaker": true,
3968 "isBestMatch": false
3969 }]))
3970 .unwrap();
3971
3972 let parsed = spot_trades_from_json(response).unwrap();
3973
3974 assert_eq!(parsed.price_exponent, -7);
3975 assert_eq!(parsed.qty_exponent, -5);
3976 assert_eq!(parsed.trades.len(), 1);
3977 assert_eq!(parsed.trades[0].id, 17);
3978 assert_eq!(parsed.trades[0].price_mantissa, 1_234_500_000);
3979 assert_eq!(parsed.trades[0].qty_mantissa, 6_789);
3980 assert_eq!(parsed.trades[0].quote_qty_mantissa, 83_810_205);
3981 assert_eq!(parsed.trades[0].time, 1_700_000_000_123_000);
3982 assert!(parsed.trades[0].is_buyer_maker);
3983 assert!(!parsed.trades[0].is_best_match);
3984 }
3985
3986 #[rstest]
3987 fn test_spot_agg_trades_from_json_preserves_all_fields() {
3988 let response: Vec<SpotAggTradeJson> = serde_json::from_value(serde_json::json!([{
3989 "a": 21,
3990 "p": "432.10",
3991 "q": "1.234",
3992 "f": 31,
3993 "l": 32,
3994 "T": 1_700_000_000_456_i64,
3995 "m": false,
3996 "M": true
3997 }]))
3998 .unwrap();
3999
4000 let parsed = spot_agg_trades_from_json(response).unwrap();
4001
4002 assert_eq!(parsed.price_exponent, -2);
4003 assert_eq!(parsed.qty_exponent, -3);
4004 assert_eq!(parsed.trades.len(), 1);
4005 assert_eq!(parsed.trades[0].id, 21);
4006 assert_eq!(parsed.trades[0].price_mantissa, 43_210);
4007 assert_eq!(parsed.trades[0].qty_mantissa, 1_234);
4008 assert_eq!(parsed.trades[0].first_trade_id, 31);
4009 assert_eq!(parsed.trades[0].last_trade_id, 32);
4010 assert_eq!(parsed.trades[0].time, 1_700_000_000_456_000);
4011 assert!(!parsed.trades[0].is_buyer_maker);
4012 assert!(parsed.trades[0].is_best_match);
4013 }
4014
4015 #[rstest]
4016 fn test_spot_klines_from_json_preserves_all_fields() {
4017 let response: Vec<SpotKlineJson> = serde_json::from_value(serde_json::json!([[
4018 1_700_000_000_000_i64,
4019 "10.10",
4020 "11.20",
4021 "9.30",
4022 "10.40",
4023 "12.345",
4024 1_700_000_059_999_i64,
4025 "128.765",
4026 37,
4027 "5.432",
4028 "56.789",
4029 "0"
4030 ]]))
4031 .unwrap();
4032
4033 let parsed = spot_klines_from_json(response).unwrap();
4034 let kline = &parsed.klines[0];
4035
4036 assert_eq!(parsed.price_exponent, -3);
4037 assert_eq!(parsed.qty_exponent, -3);
4038 assert_eq!(parsed.klines.len(), 1);
4039 assert_eq!(kline.open_time, 1_700_000_000_000_000);
4040 assert_eq!(kline.open_price, 10_100);
4041 assert_eq!(kline.high_price, 11_200);
4042 assert_eq!(kline.low_price, 9_300);
4043 assert_eq!(kline.close_price, 10_400);
4044 assert_eq!(i128::from_le_bytes(kline.volume), 12_345);
4045 assert_eq!(kline.close_time, 1_700_000_059_999_000);
4046 assert_eq!(i128::from_le_bytes(kline.quote_volume), 128_765);
4047 assert_eq!(kline.num_trades, 37);
4048 assert_eq!(i128::from_le_bytes(kline.taker_buy_base_volume), 5_432);
4049 assert_eq!(i128::from_le_bytes(kline.taker_buy_quote_volume), 56_789);
4050 }
4051
4052 #[rstest]
4053 fn test_spot_account_from_json_preserves_all_fields() {
4054 let response: SpotAccountJson = serde_json::from_value(serde_json::json!({
4055 "commissionRates": {
4056 "maker": "0.0008",
4057 "taker": "0.0011",
4058 "buyer": "0.0002",
4059 "seller": "0.0003"
4060 },
4061 "canTrade": true,
4062 "canWithdraw": false,
4063 "canDeposit": true,
4064 "requireSelfTradePrevention": true,
4065 "preventSor": false,
4066 "updateTime": 1_700_000_000_789_i64,
4067 "accountType": "SPOT",
4068 "balances": [{"asset": "USD", "free": "123.45", "locked": "6.789"}]
4069 }))
4070 .unwrap();
4071
4072 let account = spot_account_from_json(response).unwrap();
4073
4074 assert_eq!(account.commission_exponent, -4);
4075 assert_eq!(account.maker_commission_mantissa, 8);
4076 assert_eq!(account.taker_commission_mantissa, 11);
4077 assert_eq!(account.buyer_commission_mantissa, 2);
4078 assert_eq!(account.seller_commission_mantissa, 3);
4079 assert!(account.can_trade);
4080 assert!(!account.can_withdraw);
4081 assert!(account.can_deposit);
4082 assert!(account.require_self_trade_prevention);
4083 assert!(!account.prevent_sor);
4084 assert_eq!(account.update_time, 1_700_000_000_789_000);
4085 assert_eq!(account.account_type, "SPOT");
4086 assert_eq!(account.balances.len(), 1);
4087 assert_eq!(account.balances[0].asset, "USD");
4088 assert_eq!(account.balances[0].free_mantissa, 123_450);
4089 assert_eq!(account.balances[0].locked_mantissa, 6_789);
4090 assert_eq!(account.balances[0].exponent, -3);
4091 }
4092
4093 #[rstest]
4094 fn test_spot_cancel_replace_json_uses_nested_new_order_response() {
4095 let response: SpotCancelReplaceJson = serde_json::from_value(serde_json::json!({
4096 "cancelResult": "SUCCESS",
4097 "newOrderResult": "SUCCESS",
4098 "cancelResponse": {},
4099 "newOrderResponse": {
4100 "symbol": "ETHUSD",
4101 "orderId": 101,
4102 "orderListId": -1,
4103 "clientOrderId": "new-order",
4104 "transactTime": 1_700_000_000_123_i64,
4105 "price": "12.34",
4106 "origQty": "5.678",
4107 "executedQty": "1.234",
4108 "cummulativeQuoteQty": "15.22756",
4109 "status": "PARTIALLY_FILLED",
4110 "timeInForce": "GTC",
4111 "type": "LIMIT",
4112 "side": "BUY",
4113 "stopPrice": "11.11",
4114 "workingTime": 1_700_000_000_124_i64,
4115 "selfTradePreventionMode": "EXPIRE_MAKER",
4116 "fills": [{
4117 "price": "12.34",
4118 "qty": "1.234",
4119 "commission": "0.001234",
4120 "commissionAsset": "USD",
4121 "tradeId": 44
4122 }]
4123 }
4124 }))
4125 .unwrap();
4126
4127 let order = spot_new_order_from_json(response.new_order_response).unwrap();
4128
4129 assert_eq!(order.price_exponent, -2);
4130 assert_eq!(order.qty_exponent, -3);
4131 assert_eq!(order.order_id, 101);
4132 assert_eq!(order.order_list_id, None);
4133 assert_eq!(order.transact_time, 1_700_000_000_123_000);
4134 assert_eq!(order.price_mantissa, 1_234);
4135 assert_eq!(order.orig_qty_mantissa, 5_678);
4136 assert_eq!(order.executed_qty_mantissa, 1_234);
4137 assert_eq!(order.cummulative_quote_qty_mantissa, 1_522_756);
4138 assert_eq!(order.status, SbeOrderStatus::PartiallyFilled);
4139 assert_eq!(order.time_in_force, SbeTimeInForce::Gtc);
4140 assert_eq!(order.order_type, SbeOrderType::Limit);
4141 assert_eq!(order.side, SbeOrderSide::Buy);
4142 assert_eq!(order.stop_price_mantissa, Some(1_111));
4143 assert_eq!(order.working_time, Some(1_700_000_000_124_000));
4144 assert_eq!(
4145 order.self_trade_prevention_mode,
4146 SbeSelfTradePreventionMode::ExpireMaker
4147 );
4148 assert_eq!(order.client_order_id, "new-order");
4149 assert_eq!(order.symbol, "ETHUSD");
4150 assert_eq!(order.fills.len(), 1);
4151 assert_eq!(order.fills[0].price_mantissa, 1_234);
4152 assert_eq!(order.fills[0].qty_mantissa, 1_234);
4153 assert_eq!(order.fills[0].commission_mantissa, 1_234);
4154 assert_eq!(order.fills[0].commission_exponent, -6);
4155 assert_eq!(order.fills[0].commission_asset, "USD");
4156 assert_eq!(order.fills[0].trade_id, Some(44));
4157 assert_eq!(order.expiry_reason, None);
4158 }
4159
4160 #[rstest]
4161 fn test_rate_limit_config() {
4162 let config = BinanceRawSpotHttpClient::rate_limit_config();
4163
4164 assert!(config.default_quota.is_some());
4165 assert_eq!(config.order_keys.len(), 2);
4167 }
4168
4169 #[rstest]
4170 fn test_quota_from_unknown_interval_returns_none() {
4171 let quota = BinanceRateLimitQuota {
4172 rate_limit_type: BinanceRateLimitType::Orders,
4173 interval: BinanceRateLimitInterval::Unknown,
4174 interval_num: 1,
4175 limit: 10,
4176 };
4177
4178 assert!(BinanceRawSpotHttpClient::quota_from("a).is_none());
4179 }
4180
4181 fn create_test_raw_client() -> BinanceRawSpotHttpClient {
4182 BinanceRawSpotHttpClient::new(
4183 BinanceEnvironment::Live,
4184 None,
4185 None,
4186 Some("http://127.0.0.1:1".to_string()),
4187 None,
4188 Some(1),
4189 None,
4190 )
4191 .unwrap()
4192 }
4193
4194 #[rstest]
4195 #[case::limit(
4196 AggTradesParams {
4197 symbol: "BTCUSDT".to_string(),
4198 from_id: None,
4199 start_time: None,
4200 end_time: None,
4201 limit: Some(1001),
4202 },
4203 "Validation error: aggregate trade limit must not exceed 1000"
4204 )]
4205 #[case::bounds(
4206 AggTradesParams {
4207 symbol: "BTCUSDT".to_string(),
4208 from_id: None,
4209 start_time: Some(2000),
4210 end_time: Some(1000),
4211 limit: Some(1000),
4212 },
4213 "Validation error: aggregate trade startTime must not exceed endTime"
4214 )]
4215 #[tokio::test]
4216 async fn test_agg_trades_rejects_invalid_bounds(
4217 #[case] params: AggTradesParams,
4218 #[case] expected: &str,
4219 ) {
4220 let error = create_test_raw_client()
4221 .agg_trades(¶ms)
4222 .await
4223 .unwrap_err();
4224
4225 assert_eq!(error.to_string(), expected);
4226 }
4227}