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