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 config
2853 .validate(BinanceProductType::Spot)
2854 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2855 let selector = BinanceInstrumentSelector::new(config)
2856 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2857 let ts_init = self.generate_ts_init();
2858
2859 let mut instruments = if us {
2860 if config.query_commission_rates {
2861 if config.log_warnings {
2862 log::warn!("query_commission_rates does not copy commission onto instruments");
2863 } else {
2864 log::debug!("query_commission_rates does not copy commission onto instruments");
2865 }
2866 }
2867 let info = self.inner.exchange_info_json().await?;
2868 let mut instruments = Vec::with_capacity(info.symbols.len());
2869 for symbol in &info.symbols {
2870 let instrument_id =
2871 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2872
2873 if !selector.includes(
2874 instrument_id,
2875 &symbol.symbol,
2876 &symbol.base_asset,
2877 &symbol.quote_asset,
2878 None,
2879 ) {
2880 continue;
2881 }
2882
2883 match parse_spot_instrument_json_with_fees(symbol, ts_init, ts_init) {
2884 Ok(instrument) => instruments.push(instrument),
2885 Err(e) => log_instrument_parse_error(
2886 config,
2887 &selector,
2888 instrument_id,
2889 &symbol.symbol,
2890 &e,
2891 ),
2892 }
2893 }
2894 instruments
2895 } else {
2896 let info = self.exchange_info().await?;
2897 let mut instruments = Vec::with_capacity(info.symbols.len());
2898 for symbol in &info.symbols {
2899 let instrument_id =
2900 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2901
2902 if !selector.includes(
2903 instrument_id,
2904 &symbol.symbol,
2905 &symbol.base_asset,
2906 &symbol.quote_asset,
2907 None,
2908 ) {
2909 continue;
2910 }
2911
2912 match parse_spot_instrument_sbe_with_fees(symbol, ts_init, ts_init) {
2913 Ok(instrument) => instruments.push(instrument),
2914 Err(e) => log_instrument_parse_error(
2915 config,
2916 &selector,
2917 instrument_id,
2918 &symbol.symbol,
2919 &e,
2920 ),
2921 }
2922 }
2923 instruments
2924 };
2925
2926 instruments.shrink_to_fit();
2927 self.replace_instruments(&instruments);
2928
2929 log::debug!("Loaded spot instruments: count={}", instruments.len());
2930 Ok(instruments)
2931 }
2932
2933 pub async fn request_trades(
2940 &self,
2941 instrument_id: InstrumentId,
2942 limit: Option<u32>,
2943 ) -> anyhow::Result<Vec<TradeTick>> {
2944 let symbol = instrument_id.symbol.inner();
2945 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2946 let ts_init = self.generate_ts_init();
2947
2948 let trades = self
2949 .inner
2950 .trades(symbol.as_str(), limit)
2951 .await
2952 .map_err(|e| anyhow::anyhow!(e))?;
2953
2954 parse_spot_trades_sbe(&trades, &instrument, ts_init)
2955 }
2956
2957 pub async fn request_agg_trades(
2963 &self,
2964 instrument_id: InstrumentId,
2965 start: Option<Timestamp>,
2966 end: Option<Timestamp>,
2967 limit: Option<u32>,
2968 ) -> anyhow::Result<Vec<TradeTick>> {
2969 let symbol = instrument_id.symbol.inner();
2970 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2971 let params = AggTradesParams {
2972 symbol: symbol.to_string(),
2973 from_id: None,
2974 start_time: start.map(|dt| dt.as_millisecond()),
2975 end_time: end.map(|dt| dt.as_millisecond()),
2976 limit,
2977 };
2978 let response = self.inner.agg_trades(¶ms).await?;
2979 let trades = BinanceTrades {
2980 price_exponent: response.price_exponent,
2981 qty_exponent: response.qty_exponent,
2982 trades: response
2983 .trades
2984 .into_iter()
2985 .map(|trade| super::models::BinanceTrade {
2986 id: trade.id,
2987 price_mantissa: trade.price_mantissa,
2988 qty_mantissa: trade.qty_mantissa,
2989 quote_qty_mantissa: 0,
2990 time: trade.time,
2991 is_buyer_maker: trade.is_buyer_maker,
2992 is_best_match: trade.is_best_match,
2993 })
2994 .collect(),
2995 };
2996
2997 let mut parsed = parse_spot_trades_sbe(&trades, &instrument, UnixNanos::default())?;
2998 for trade in &mut parsed {
2999 trade.ts_init = trade.ts_event;
3000 }
3001 Ok(parsed)
3002 }
3003
3004 pub async fn request_binance_bars(
3011 &self,
3012 bar_type: BarType,
3013 start: Option<Timestamp>,
3014 end: Option<Timestamp>,
3015 limit: Option<u32>,
3016 ) -> anyhow::Result<Vec<crate::common::bar::BinanceBar>> {
3017 anyhow::ensure!(
3018 bar_type.aggregation_source() == AggregationSource::External,
3019 "Only EXTERNAL aggregation is supported"
3020 );
3021
3022 let spec = bar_type.spec();
3023 let step = spec.step.get();
3024 let interval = match spec.aggregation {
3025 BarAggregation::Second if step == 1 => "1s".to_string(),
3026 BarAggregation::Second => {
3027 anyhow::bail!("Binance Spot supports only the 1s kline interval")
3028 }
3029 BarAggregation::Minute => format!("{step}m"),
3030 BarAggregation::Hour => format!("{step}h"),
3031 BarAggregation::Day => format!("{step}d"),
3032 BarAggregation::Week => format!("{step}w"),
3033 BarAggregation::Month => format!("{step}M"),
3034 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
3035 };
3036
3037 let instrument_id = bar_type.instrument_id();
3038 let symbol = instrument_id.symbol;
3039 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3040 let klines = self
3041 .inner
3042 .klines(
3043 symbol.as_str(),
3044 &interval,
3045 start.map(|dt| dt.as_millisecond()),
3046 end.map(|dt| dt.as_millisecond()),
3047 limit,
3048 )
3049 .await
3050 .map_err(|e| anyhow::anyhow!(e))?;
3051
3052 let mut bars =
3053 parse_klines_to_binance_bars(&klines, bar_type, &instrument, UnixNanos::default())?;
3054 let now = self.clock.get_time_ns();
3055 bars.retain(|bar| bar.ts_event < now);
3056 for bar in &mut bars {
3057 bar.ts_init = bar.ts_event;
3058 }
3059 Ok(bars)
3060 }
3061
3062 pub async fn request_bars(
3068 &self,
3069 bar_type: BarType,
3070 start: Option<Timestamp>,
3071 end: Option<Timestamp>,
3072 limit: Option<u32>,
3073 ) -> anyhow::Result<Vec<Bar>> {
3074 Ok(self
3075 .request_binance_bars(bar_type, start, end, limit)
3076 .await?
3077 .into_iter()
3078 .map(|bar| bar.bar())
3079 .collect())
3080 }
3081
3082 pub async fn request_book_snapshot(
3089 &self,
3090 instrument_id: InstrumentId,
3091 depth: Option<u32>,
3092 ) -> anyhow::Result<OrderBook> {
3093 if depth.is_some_and(|value| value == 0 || value > 5000) {
3094 anyhow::bail!("Binance Spot order-book depth must be between 1 and 5000");
3095 }
3096 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3097 let params = DepthParams {
3098 symbol: instrument_id.symbol.to_string(),
3099 limit: depth,
3100 };
3101 let snapshot = self.inner.depth(¶ms).await?;
3102 let ts_event = self.generate_ts_init();
3103 Self::parse_book_snapshot_response(instrument_id, &instrument, &snapshot, ts_event)
3104 }
3105
3106 fn parse_book_snapshot_response(
3107 instrument_id: InstrumentId,
3108 instrument: &InstrumentAny,
3109 snapshot: &BinanceDepth,
3110 ts_event: UnixNanos,
3111 ) -> anyhow::Result<OrderBook> {
3112 let sequence = u64::try_from(snapshot.last_update_id)
3113 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3114 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3115 let mut add_level = |level: &super::models::BinancePriceLevel,
3116 side: OrderSide,
3117 order_id: usize,
3118 name: &str|
3119 -> anyhow::Result<()> {
3120 let price = Price::from_mantissa_exponent_checked(
3121 level.price_mantissa,
3122 snapshot.price_exponent,
3123 instrument.price_precision(),
3124 )
3125 .map_err(|e| anyhow::anyhow!("invalid {name} price: {e}"))?;
3126 anyhow::ensure!(price.is_positive(), "invalid non-positive {name} price");
3127 let qty_mantissa = u64::try_from(level.qty_mantissa)
3128 .map_err(|_| anyhow::anyhow!("invalid negative {name} quantity"))?;
3129 let quantity = Quantity::from_mantissa_exponent_checked(
3130 qty_mantissa,
3131 snapshot.qty_exponent,
3132 instrument.size_precision(),
3133 )
3134 .map_err(|e| anyhow::anyhow!("invalid {name} quantity: {e}"))?;
3135 anyhow::ensure!(
3136 quantity.is_positive(),
3137 "invalid non-positive {name} quantity"
3138 );
3139 let order = BookOrder::new(
3140 side,
3141 price,
3142 quantity,
3143 u64::try_from(order_id)
3144 .map_err(|_| anyhow::anyhow!("order-book level index overflow"))?,
3145 );
3146 book.add(order, 0, sequence, ts_event);
3147 Ok(())
3148 };
3149
3150 for (index, level) in snapshot.bids.iter().enumerate() {
3151 add_level(level, OrderSide::Buy, index, "bid")?;
3152 }
3153 let bid_count = snapshot.bids.len();
3154 for (index, level) in snapshot.asks.iter().enumerate() {
3155 let order_id = bid_count
3156 .checked_add(index)
3157 .ok_or_else(|| anyhow::anyhow!("order-book level index overflow"))?;
3158 add_level(level, OrderSide::Sell, order_id, "ask")?;
3159 }
3160 Ok(book)
3161 }
3162
3163 fn instrument_from_cache_by_id(
3164 &self,
3165 instrument_id: InstrumentId,
3166 ) -> anyhow::Result<InstrumentAny> {
3167 self.instruments_cache
3168 .get_cloned(&instrument_id.symbol.inner())
3169 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
3170 }
3171
3172 pub async fn request_account_state(
3178 &self,
3179 account_id: AccountId,
3180 ) -> anyhow::Result<AccountState> {
3181 let ts_init = self.clock.get_time_ns();
3182 let params = AccountInfoParams::default();
3183 let account_info = self.inner.account(¶ms).await?;
3184 Ok(account_info.to_account_state(account_id, ts_init))
3185 }
3186
3187 pub async fn request_order_status_report(
3196 &self,
3197 account_id: AccountId,
3198 instrument_id: InstrumentId,
3199 venue_order_id: Option<VenueOrderId>,
3200 client_order_id: Option<ClientOrderId>,
3201 ) -> anyhow::Result<Option<OrderStatusReport>> {
3202 anyhow::ensure!(
3203 venue_order_id.is_some() || client_order_id.is_some(),
3204 "Either venue_order_id or client_order_id must be provided"
3205 );
3206
3207 let symbol = instrument_id.symbol.inner();
3208 let instrument = self.instrument_from_cache(symbol)?;
3209 let ts_init = self.generate_ts_init();
3210
3211 let order_id = venue_order_id
3212 .map(|id| id.inner().parse::<i64>())
3213 .transpose()
3214 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3215
3216 let client_id_str =
3217 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3218
3219 let order = match self
3220 .inner
3221 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
3222 .await
3223 {
3224 Ok(order) => order,
3225 Err(e) if Self::is_no_such_order_error(&e) => {
3226 log::debug!("Binance Spot order not found: instrument_id={instrument_id}");
3227 return Ok(None);
3228 }
3229 Err(e) => anyhow::bail!(e),
3230 };
3231
3232 parse_order_status_report_sbe(
3233 &order,
3234 account_id,
3235 &instrument,
3236 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3237 ts_init,
3238 )
3239 .map(Some)
3240 }
3241
3242 const fn is_no_such_order_error(error: &BinanceSpotHttpError) -> bool {
3243 matches!(
3244 error,
3245 BinanceSpotHttpError::BinanceError { code, .. } if *code == BINANCE_NO_SUCH_ORDER_CODE
3246 )
3247 }
3248
3249 pub async fn request_order_status_reports(
3259 &self,
3260 account_id: AccountId,
3261 instrument_id: Option<InstrumentId>,
3262 start: Option<Timestamp>,
3263 end: Option<Timestamp>,
3264 open_only: bool,
3265 limit: Option<u32>,
3266 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3267 self.request_order_status_reports_scoped(
3268 account_id,
3269 instrument_id,
3270 start,
3271 end,
3272 open_only,
3273 limit,
3274 None,
3275 )
3276 .await
3277 }
3278
3279 #[expect(clippy::too_many_arguments)]
3280 pub(crate) async fn request_order_status_reports_scoped(
3281 &self,
3282 account_id: AccountId,
3283 instrument_id: Option<InstrumentId>,
3284 start: Option<Timestamp>,
3285 end: Option<Timestamp>,
3286 open_only: bool,
3287 limit: Option<u32>,
3288 provider: Option<&BinanceInstrumentProviderConfig>,
3289 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3290 if instrument_id.is_some_and(|id| provider.is_some_and(|provider| provider.excludes(id))) {
3291 log::debug!("Dropping out-of-scope Binance Spot order request for {instrument_id:?}");
3292 return Ok(Vec::new());
3293 }
3294 let ts_init = self.generate_ts_init();
3295 let symbol = instrument_id.map(|id| id.symbol.to_string());
3296
3297 let orders = if open_only {
3298 self.inner
3299 .open_orders(symbol.as_deref())
3300 .await
3301 .map_err(|e| anyhow::anyhow!(e))?
3302 } else {
3303 let symbol = symbol
3304 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
3305 self.inner
3306 .all_orders(
3307 &symbol,
3308 start.map(|dt| dt.as_millisecond()),
3309 end.map(|dt| dt.as_millisecond()),
3310 limit,
3311 )
3312 .await
3313 .map_err(|e| anyhow::anyhow!(e))?
3314 };
3315
3316 orders
3317 .iter()
3318 .filter(|order| {
3319 let id = format_instrument_id(&Ustr::from(&order.symbol), BinanceProductType::Spot);
3320 if provider.is_some_and(|provider| provider.excludes(id)) {
3321 log::debug!("Dropping out-of-scope Binance Spot order for {id}");
3322 false
3323 } else {
3324 true
3325 }
3326 })
3327 .map(|order| {
3328 let symbol = Ustr::from(&order.symbol);
3329 let instrument = self.instrument_from_cache(symbol)?;
3330 parse_order_status_report_sbe(
3331 order,
3332 account_id,
3333 &instrument,
3334 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3335 ts_init,
3336 )
3337 })
3338 .collect()
3339 }
3340
3341 pub async fn request_fill_reports(
3348 &self,
3349 account_id: AccountId,
3350 instrument_id: InstrumentId,
3351 venue_order_id: Option<VenueOrderId>,
3352 start: Option<Timestamp>,
3353 end: Option<Timestamp>,
3354 limit: Option<u32>,
3355 ) -> anyhow::Result<Vec<FillReport>> {
3356 self.request_fill_reports_with_cursor(
3357 account_id,
3358 instrument_id,
3359 venue_order_id,
3360 start,
3361 end,
3362 None,
3363 limit,
3364 )
3365 .await
3366 }
3367
3368 #[expect(clippy::too_many_arguments)]
3369 pub(crate) async fn request_fill_reports_with_cursor(
3370 &self,
3371 account_id: AccountId,
3372 instrument_id: InstrumentId,
3373 venue_order_id: Option<VenueOrderId>,
3374 start: Option<Timestamp>,
3375 end: Option<Timestamp>,
3376 from_id: Option<i64>,
3377 limit: Option<u32>,
3378 ) -> anyhow::Result<Vec<FillReport>> {
3379 let ts_init = self.generate_ts_init();
3380 let symbol = instrument_id.symbol.inner();
3381
3382 let order_id = venue_order_id
3383 .map(|id| id.inner().parse::<i64>())
3384 .transpose()
3385 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3386
3387 let trades = self
3388 .inner
3389 .account_trades_with_cursor(
3390 symbol.as_str(),
3391 order_id,
3392 start.map(|dt| dt.as_millisecond()),
3393 end.map(|dt| dt.as_millisecond()),
3394 from_id,
3395 limit,
3396 )
3397 .await
3398 .map_err(|e| anyhow::anyhow!(e))?;
3399
3400 trades
3401 .iter()
3402 .map(|trade| {
3403 let symbol = Ustr::from(&trade.symbol);
3404 let instrument = self.instrument_from_cache(symbol)?;
3405 let commission_currency = get_currency(&trade.commission_asset);
3406 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
3407 })
3408 .collect()
3409 }
3410
3411 #[expect(clippy::too_many_arguments)]
3424 pub async fn submit_order(
3425 &self,
3426 account_id: AccountId,
3427 instrument_id: InstrumentId,
3428 client_order_id: ClientOrderId,
3429 order_side: OrderSide,
3430 order_type: OrderType,
3431 quantity: Quantity,
3432 time_in_force: TimeInForce,
3433 price: Option<Price>,
3434 trigger_price: Option<Price>,
3435 post_only: bool,
3436 quote_quantity: bool,
3437 display_qty: Option<Quantity>,
3438 use_gtd: bool,
3439 ) -> anyhow::Result<OrderStatusReport> {
3440 let symbol = instrument_id.symbol.inner();
3441 let instrument = self
3442 .instrument_from_cache(symbol)
3443 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3444 let ts_init = self.generate_ts_init();
3445
3446 let binance_side = BinanceSide::try_from(order_side)
3447 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3448 let binance_order_type = order_type_to_binance_spot(order_type, post_only)
3449 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3450
3451 let requires_trigger = matches!(
3453 order_type,
3454 OrderType::StopMarket
3455 | OrderType::StopLimit
3456 | OrderType::MarketIfTouched
3457 | OrderType::LimitIfTouched
3458 );
3459
3460 if requires_trigger && trigger_price.is_none() {
3461 return Err(Self::command_validation_error(
3462 "Conditional orders require a trigger price",
3463 ));
3464 }
3465
3466 let requires_price = matches!(
3468 binance_order_type,
3469 BinanceSpotOrderType::Limit
3470 | BinanceSpotOrderType::StopLossLimit
3471 | BinanceSpotOrderType::TakeProfitLimit
3472 | BinanceSpotOrderType::LimitMaker
3473 );
3474
3475 if requires_price && price.is_none() {
3476 return Err(Self::command_validation_error(format!(
3477 "{binance_order_type:?} orders require a price"
3478 )));
3479 }
3480
3481 let supports_tif = matches!(
3483 binance_order_type,
3484 BinanceSpotOrderType::Limit
3485 | BinanceSpotOrderType::StopLossLimit
3486 | BinanceSpotOrderType::TakeProfitLimit
3487 );
3488 let binance_tif = if supports_tif {
3489 Some(
3490 time_in_force_to_binance_spot(time_in_force, use_gtd)
3491 .map_err(|e| Self::command_validation_error(e.to_string()))?,
3492 )
3493 } else {
3494 None
3495 };
3496
3497 let qty_str = quantity.to_string();
3498 let price_str = price.map(|p| p.to_string());
3499 let stop_price_str = trigger_price.map(|p| p.to_string());
3500 let iceberg_qty_str = display_qty.map(|q| q.to_string());
3501 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3502
3503 if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
3504 return Err(Self::command_validation_error(
3505 "quoteOrderQty is only supported for MARKET orders",
3506 ));
3507 }
3508
3509 let (base_qty, quote_qty) = if quote_quantity {
3510 (None, Some(qty_str.as_str()))
3511 } else {
3512 (Some(qty_str.as_str()), None)
3513 };
3514
3515 let response = self
3516 .inner
3517 .new_order_full(
3518 symbol.as_str(),
3519 binance_side,
3520 binance_order_type,
3521 binance_tif,
3522 base_qty,
3523 quote_qty,
3524 price_str.as_deref(),
3525 Some(&client_id_str),
3526 stop_price_str.as_deref(),
3527 iceberg_qty_str.as_deref(),
3528 )
3529 .await?;
3530
3531 parse_new_order_response_sbe(
3532 &response,
3533 account_id,
3534 &instrument,
3535 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3536 ts_init,
3537 )
3538 .map_err(|e| Self::response_parse_error(e.to_string()))
3539 }
3540
3541 pub async fn submit_order_list(
3549 &self,
3550 orders: &[BatchOrderItem],
3551 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
3552 self.inner.batch_submit_orders(orders).await
3553 }
3554
3555 pub async fn submit_oco_order_list(
3561 &self,
3562 params: &NewOcoOrderListParams,
3563 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
3564 self.inner.new_oco_order_list(params).await
3565 }
3566
3567 #[expect(clippy::too_many_arguments)]
3576 pub async fn modify_order(
3577 &self,
3578 account_id: AccountId,
3579 instrument_id: InstrumentId,
3580 venue_order_id: VenueOrderId,
3581 client_order_id: ClientOrderId,
3582 order_side: OrderSide,
3583 order_type: OrderType,
3584 quantity: Quantity,
3585 time_in_force: TimeInForce,
3586 price: Option<Price>,
3587 use_gtd: bool,
3588 cancel_new_client_order_id: &str,
3589 ) -> anyhow::Result<OrderStatusReport> {
3590 let symbol = instrument_id.symbol.inner();
3591 let instrument = self
3592 .instrument_from_cache(symbol)
3593 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3594 let ts_init = self.generate_ts_init();
3595
3596 let binance_side = BinanceSide::try_from(order_side)
3597 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3598 let binance_order_type = order_type_to_binance_spot(order_type, false)
3599 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3600 let binance_tif = time_in_force_to_binance_spot(time_in_force, use_gtd)
3601 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3602
3603 let cancel_order_id: i64 = venue_order_id.inner().parse().map_err(|_| {
3604 Self::command_validation_error(format!("Invalid venue order ID: {venue_order_id}"))
3605 })?;
3606
3607 let qty_str = quantity.to_string();
3608 let price_str = price.map(|p| p.to_string());
3609 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3610
3611 let response = self
3612 .inner
3613 .cancel_replace_order(
3614 symbol.as_str(),
3615 binance_side,
3616 binance_order_type,
3617 Some(binance_tif),
3618 Some(&qty_str),
3619 price_str.as_deref(),
3620 Some(cancel_order_id),
3621 None,
3622 Some(cancel_new_client_order_id),
3623 Some(&client_id_str),
3624 )
3625 .await
3626 .map_err(|e| anyhow::anyhow!(e))?;
3627
3628 parse_new_order_response_sbe(
3629 &response,
3630 account_id,
3631 &instrument,
3632 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3633 ts_init,
3634 )
3635 .map_err(|e| Self::response_parse_error(e.to_string()))
3636 }
3637
3638 pub async fn cancel_order(
3646 &self,
3647 instrument_id: InstrumentId,
3648 venue_order_id: Option<VenueOrderId>,
3649 client_order_id: Option<ClientOrderId>,
3650 ) -> anyhow::Result<VenueOrderId> {
3651 let symbol = instrument_id.symbol.inner();
3652
3653 let order_id = match venue_order_id {
3654 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
3655 Ok(order_id) => Some(order_id),
3656 Err(e) if client_order_id.is_some() => {
3657 log::warn!(
3658 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
3659 );
3660 None
3661 }
3662 Err(e) => {
3663 return Err(Self::command_validation_error(format!(
3664 "Invalid venue order ID: {e}"
3665 )));
3666 }
3667 },
3668 None => None,
3669 };
3670
3671 let client_id_str =
3672 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3673
3674 let response = self
3675 .inner
3676 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
3677 .await
3678 .map_err(|e| anyhow::anyhow!(e))?;
3679
3680 Ok(VenueOrderId::new(response.order_id.to_string()))
3681 }
3682
3683 pub async fn batch_cancel_orders(
3691 &self,
3692 cancels: &[BatchCancelItem],
3693 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
3694 self.inner.batch_cancel_orders(cancels).await
3695 }
3696
3697 pub async fn cancel_all_orders(
3705 &self,
3706 instrument_id: InstrumentId,
3707 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
3708 let responses = self.cancel_all_order_responses(instrument_id).await?;
3709
3710 responses
3711 .into_iter()
3712 .flat_map(|response| match response {
3713 BinanceCancelOpenOrdersResponse::Order(response) => vec![response],
3714 BinanceCancelOpenOrdersResponse::OrderList(response) => response.order_reports,
3715 })
3716 .map(|response| {
3717 Ok((
3718 VenueOrderId::new(response.order_id.to_string()),
3719 decode_client_order_id(
3720 &response.orig_client_order_id,
3721 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3722 )?,
3723 ))
3724 })
3725 .collect()
3726 }
3727
3728 pub(crate) async fn cancel_all_order_responses(
3734 &self,
3735 instrument_id: InstrumentId,
3736 ) -> anyhow::Result<Vec<BinanceCancelOpenOrdersResponse>> {
3737 let symbol = instrument_id.symbol.inner();
3738
3739 let responses = self
3740 .inner
3741 .cancel_open_order_responses(symbol.as_str())
3742 .await
3743 .map_err(|e| anyhow::anyhow!(e))?;
3744
3745 Ok(responses)
3746 }
3747}
3748
3749fn spot_json_market_status(status: &str) -> MarketStatusAction {
3750 match status {
3751 "TRADING" => MarketStatusAction::Trading,
3752 "BREAK" => MarketStatusAction::Pause,
3753 _ => MarketStatusAction::NotAvailableForTrading,
3754 }
3755}
3756
3757fn log_instrument_parse_error(
3758 config: &BinanceInstrumentProviderConfig,
3759 selector: &BinanceInstrumentSelector,
3760 instrument_id: InstrumentId,
3761 symbol: &str,
3762 error: &anyhow::Error,
3763) {
3764 let explicit = selector.is_explicit(instrument_id, symbol);
3765 if should_warn_on_instrument_parse_error(config.log_warnings, explicit, error) {
3766 log::warn!("Skipping Binance Spot instrument {symbol}: {error}");
3767 } else {
3768 log::debug!("Skipping Binance Spot instrument {symbol}: {error}");
3769 }
3770}
3771
3772#[cfg(test)]
3773mod tests {
3774 use nautilus_model::instruments::stubs::currency_pair_btcusdt;
3775 use nautilus_testkit::http::assert_http_redirect_rejected;
3776 use rstest::rstest;
3777
3778 use super::*;
3779 use crate::spot::http::models::BinancePriceLevel;
3780
3781 #[tokio::test]
3782 async fn test_authenticated_client_rejects_redirects() {
3783 let client = BinanceRawSpotHttpClient::new(
3784 BinanceEnvironment::Testnet,
3785 Some("key".into()),
3786 Some("secret".into()),
3787 None,
3788 None,
3789 Some(3),
3790 None,
3791 )
3792 .unwrap()
3793 .client;
3794 assert_http_redirect_rejected(|url| async move {
3795 client
3796 .get(url, None, None, Some(3), None)
3797 .await
3798 .unwrap()
3799 .status
3800 .as_u16()
3801 })
3802 .await;
3803 }
3804
3805 #[rstest]
3806 fn test_schema_constants() {
3807 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
3808 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 5);
3809 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
3810 assert_eq!(BinanceSpotHttpClient::schema_version(), 5);
3811 }
3812
3813 #[rstest]
3814 fn test_sbe_schema_header() {
3815 assert_eq!(SBE_SCHEMA_HEADER, "3:5");
3816 }
3817
3818 #[rstest]
3819 fn test_parse_book_snapshot_response_rejects_negative_update_id() {
3820 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3821 let snapshot = BinanceDepth {
3822 last_update_id: -1,
3823 price_exponent: -2,
3824 qty_exponent: -5,
3825 bids: vec![],
3826 asks: vec![],
3827 };
3828
3829 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3830 InstrumentId::from("BTCUSDT.BINANCE"),
3831 &instrument,
3832 &snapshot,
3833 UnixNanos::from(1_700_000_000_000_000_001u64),
3834 )
3835 .unwrap_err();
3836
3837 assert_eq!(error.to_string(), "invalid negative order-book update ID");
3838 }
3839
3840 #[rstest]
3841 fn test_parse_book_snapshot_response_rejects_negative_quantity() {
3842 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3843 let snapshot = BinanceDepth {
3844 last_update_id: 12345,
3845 price_exponent: -2,
3846 qty_exponent: -5,
3847 bids: vec![BinancePriceLevel {
3848 price_mantissa: 4_200_001,
3849 qty_mantissa: -1,
3850 }],
3851 asks: vec![],
3852 };
3853
3854 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3855 InstrumentId::from("BTCUSDT.BINANCE"),
3856 &instrument,
3857 &snapshot,
3858 UnixNanos::from(1_700_000_000_000_000_001u64),
3859 )
3860 .unwrap_err();
3861
3862 assert_eq!(error.to_string(), "invalid negative bid quantity");
3863 }
3864
3865 #[rstest]
3866 fn test_default_headers_include_sbe() {
3867 let headers = BinanceRawSpotHttpClient::default_headers(&None, false);
3868
3869 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
3870 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:5".to_string()));
3871 }
3872
3873 #[rstest]
3874 fn test_json_headers_exclude_sbe() {
3875 let headers = BinanceRawSpotHttpClient::default_headers(&None, true);
3876
3877 assert_eq!(headers.get("Accept"), Some(&"application/json".to_string()));
3878 assert_eq!(headers.get("X-MBX-SBE"), None);
3879 }
3880
3881 #[rstest]
3882 fn test_spot_cancel_open_orders_from_json_decodes_mixed_results() {
3883 let bytes = serde_json::to_vec(&serde_json::json!([
3884 {
3885 "symbol": "BTCUSDT",
3886 "orderId": 11,
3887 "orderListId": -1,
3888 "clientOrderId": "cancel-11",
3889 "origClientOrderId": "orig-11",
3890 "transactTime": 1_734_300_000_000_i64,
3891 "price": "100.00",
3892 "origQty": "1.000",
3893 "executedQty": "0.000",
3894 "cummulativeQuoteQty": "0.00",
3895 "status": "CANCELED",
3896 "timeInForce": "GTC",
3897 "type": "LIMIT",
3898 "side": "SELL"
3899 },
3900 {
3901 "orderListId": 44,
3902 "contingencyType": "OCO",
3903 "listStatusType": "ALL_DONE",
3904 "listOrderStatus": "ALL_DONE",
3905 "listClientOrderId": "list-44",
3906 "transactionTime": 1_734_300_000_000_i64,
3907 "symbol": "BTCUSDT",
3908 "orders": [
3909 {"symbol": "BTCUSDT", "orderId": 21, "clientOrderId": "orig-21"},
3910 {"symbol": "BTCUSDT", "orderId": 22, "clientOrderId": "orig-22"}
3911 ],
3912 "orderReports": [
3913 {
3914 "symbol": "BTCUSDT",
3915 "orderId": 21,
3916 "orderListId": 44,
3917 "clientOrderId": "cancel-list",
3918 "origClientOrderId": "orig-21",
3919 "transactTime": 1_734_300_000_000_i64,
3920 "price": "101.00",
3921 "origQty": "1.000",
3922 "executedQty": "0.000",
3923 "cummulativeQuoteQty": "0.00",
3924 "status": "CANCELED",
3925 "timeInForce": "GTC",
3926 "type": "LIMIT_MAKER",
3927 "side": "SELL"
3928 },
3929 {
3930 "symbol": "BTCUSDT",
3931 "orderId": 22,
3932 "orderListId": 44,
3933 "clientOrderId": "cancel-list",
3934 "origClientOrderId": "orig-22",
3935 "transactTime": 1_734_300_000_000_i64,
3936 "price": "99.00",
3937 "origQty": "1.000",
3938 "executedQty": "0.000",
3939 "cummulativeQuoteQty": "0.00",
3940 "status": "CANCELED",
3941 "timeInForce": "GTC",
3942 "type": "STOP_LOSS_LIMIT",
3943 "side": "SELL"
3944 }
3945 ]
3946 }
3947 ]))
3948 .unwrap();
3949
3950 let responses = spot_cancel_open_orders_from_json(&bytes).unwrap();
3951
3952 assert_eq!(responses.len(), 2);
3953 assert!(matches!(
3954 &responses[0],
3955 BinanceCancelOpenOrdersResponse::Order(order) if order.order_id == 11
3956 ));
3957 let BinanceCancelOpenOrdersResponse::OrderList(order_list) = &responses[1] else {
3958 panic!("Expected JSON order-list result");
3959 };
3960 assert_eq!(order_list.order_list_id, 44);
3961 assert_eq!(order_list.transaction_time, 1_734_300_000_000_000);
3962 assert_eq!(order_list.orders.len(), 2);
3963 assert_eq!(order_list.order_reports.len(), 2);
3964 assert_eq!(order_list.order_reports[0].orig_client_order_id, "orig-21");
3965 assert_eq!(order_list.order_reports[1].orig_client_order_id, "orig-22");
3966 }
3967
3968 #[rstest]
3969 fn test_spot_cancel_open_orders_from_json_rejects_partial_order_list_shape() {
3970 let bytes = br#"[{"orders": []}]"#;
3971
3972 let error = spot_cancel_open_orders_from_json(bytes).unwrap_err();
3973
3974 assert!(matches!(error, BinanceSpotHttpError::JsonError(_)));
3975 }
3976
3977 #[rstest]
3978 fn test_spot_trades_from_json_preserves_all_fields() {
3979 let response: Vec<SpotTradeJson> = serde_json::from_value(serde_json::json!([{
3980 "id": 17,
3981 "price": "123.45",
3982 "qty": "0.06789",
3983 "quoteQty": "8.3810205",
3984 "time": 1_700_000_000_123_i64,
3985 "isBuyerMaker": true,
3986 "isBestMatch": false
3987 }]))
3988 .unwrap();
3989
3990 let parsed = spot_trades_from_json(response).unwrap();
3991
3992 assert_eq!(parsed.price_exponent, -7);
3993 assert_eq!(parsed.qty_exponent, -5);
3994 assert_eq!(parsed.trades.len(), 1);
3995 assert_eq!(parsed.trades[0].id, 17);
3996 assert_eq!(parsed.trades[0].price_mantissa, 1_234_500_000);
3997 assert_eq!(parsed.trades[0].qty_mantissa, 6_789);
3998 assert_eq!(parsed.trades[0].quote_qty_mantissa, 83_810_205);
3999 assert_eq!(parsed.trades[0].time, 1_700_000_000_123_000);
4000 assert!(parsed.trades[0].is_buyer_maker);
4001 assert!(!parsed.trades[0].is_best_match);
4002 }
4003
4004 #[rstest]
4005 fn test_spot_agg_trades_from_json_preserves_all_fields() {
4006 let response: Vec<SpotAggTradeJson> = serde_json::from_value(serde_json::json!([{
4007 "a": 21,
4008 "p": "432.10",
4009 "q": "1.234",
4010 "f": 31,
4011 "l": 32,
4012 "T": 1_700_000_000_456_i64,
4013 "m": false,
4014 "M": true
4015 }]))
4016 .unwrap();
4017
4018 let parsed = spot_agg_trades_from_json(response).unwrap();
4019
4020 assert_eq!(parsed.price_exponent, -2);
4021 assert_eq!(parsed.qty_exponent, -3);
4022 assert_eq!(parsed.trades.len(), 1);
4023 assert_eq!(parsed.trades[0].id, 21);
4024 assert_eq!(parsed.trades[0].price_mantissa, 43_210);
4025 assert_eq!(parsed.trades[0].qty_mantissa, 1_234);
4026 assert_eq!(parsed.trades[0].first_trade_id, 31);
4027 assert_eq!(parsed.trades[0].last_trade_id, 32);
4028 assert_eq!(parsed.trades[0].time, 1_700_000_000_456_000);
4029 assert!(!parsed.trades[0].is_buyer_maker);
4030 assert!(parsed.trades[0].is_best_match);
4031 }
4032
4033 #[rstest]
4034 fn test_spot_klines_from_json_preserves_all_fields() {
4035 let response: Vec<SpotKlineJson> = serde_json::from_value(serde_json::json!([[
4036 1_700_000_000_000_i64,
4037 "10.10",
4038 "11.20",
4039 "9.30",
4040 "10.40",
4041 "12.345",
4042 1_700_000_059_999_i64,
4043 "128.765",
4044 37,
4045 "5.432",
4046 "56.789",
4047 "0"
4048 ]]))
4049 .unwrap();
4050
4051 let parsed = spot_klines_from_json(response).unwrap();
4052 let kline = &parsed.klines[0];
4053
4054 assert_eq!(parsed.price_exponent, -3);
4055 assert_eq!(parsed.qty_exponent, -3);
4056 assert_eq!(parsed.klines.len(), 1);
4057 assert_eq!(kline.open_time, 1_700_000_000_000_000);
4058 assert_eq!(kline.open_price, 10_100);
4059 assert_eq!(kline.high_price, 11_200);
4060 assert_eq!(kline.low_price, 9_300);
4061 assert_eq!(kline.close_price, 10_400);
4062 assert_eq!(i128::from_le_bytes(kline.volume), 12_345);
4063 assert_eq!(kline.close_time, 1_700_000_059_999_000);
4064 assert_eq!(i128::from_le_bytes(kline.quote_volume), 128_765);
4065 assert_eq!(kline.num_trades, 37);
4066 assert_eq!(i128::from_le_bytes(kline.taker_buy_base_volume), 5_432);
4067 assert_eq!(i128::from_le_bytes(kline.taker_buy_quote_volume), 56_789);
4068 }
4069
4070 #[rstest]
4071 fn test_spot_account_from_json_preserves_all_fields() {
4072 let response: SpotAccountJson = serde_json::from_value(serde_json::json!({
4073 "commissionRates": {
4074 "maker": "0.0008",
4075 "taker": "0.0011",
4076 "buyer": "0.0002",
4077 "seller": "0.0003"
4078 },
4079 "canTrade": true,
4080 "canWithdraw": false,
4081 "canDeposit": true,
4082 "requireSelfTradePrevention": true,
4083 "preventSor": false,
4084 "updateTime": 1_700_000_000_789_i64,
4085 "accountType": "SPOT",
4086 "balances": [{"asset": "USD", "free": "123.45", "locked": "6.789"}]
4087 }))
4088 .unwrap();
4089
4090 let account = spot_account_from_json(response).unwrap();
4091
4092 assert_eq!(account.commission_exponent, -4);
4093 assert_eq!(account.maker_commission_mantissa, 8);
4094 assert_eq!(account.taker_commission_mantissa, 11);
4095 assert_eq!(account.buyer_commission_mantissa, 2);
4096 assert_eq!(account.seller_commission_mantissa, 3);
4097 assert!(account.can_trade);
4098 assert!(!account.can_withdraw);
4099 assert!(account.can_deposit);
4100 assert!(account.require_self_trade_prevention);
4101 assert!(!account.prevent_sor);
4102 assert_eq!(account.update_time, 1_700_000_000_789_000);
4103 assert_eq!(account.account_type, "SPOT");
4104 assert_eq!(account.balances.len(), 1);
4105 assert_eq!(account.balances[0].asset, "USD");
4106 assert_eq!(account.balances[0].free_mantissa, 123_450);
4107 assert_eq!(account.balances[0].locked_mantissa, 6_789);
4108 assert_eq!(account.balances[0].exponent, -3);
4109 }
4110
4111 #[rstest]
4112 fn test_spot_cancel_replace_json_uses_nested_new_order_response() {
4113 let response: SpotCancelReplaceJson = serde_json::from_value(serde_json::json!({
4114 "cancelResult": "SUCCESS",
4115 "newOrderResult": "SUCCESS",
4116 "cancelResponse": {},
4117 "newOrderResponse": {
4118 "symbol": "ETHUSD",
4119 "orderId": 101,
4120 "orderListId": -1,
4121 "clientOrderId": "new-order",
4122 "transactTime": 1_700_000_000_123_i64,
4123 "price": "12.34",
4124 "origQty": "5.678",
4125 "executedQty": "1.234",
4126 "cummulativeQuoteQty": "15.22756",
4127 "status": "PARTIALLY_FILLED",
4128 "timeInForce": "GTC",
4129 "type": "LIMIT",
4130 "side": "BUY",
4131 "stopPrice": "11.11",
4132 "workingTime": 1_700_000_000_124_i64,
4133 "selfTradePreventionMode": "EXPIRE_MAKER",
4134 "fills": [{
4135 "price": "12.34",
4136 "qty": "1.234",
4137 "commission": "0.001234",
4138 "commissionAsset": "USD",
4139 "tradeId": 44
4140 }]
4141 }
4142 }))
4143 .unwrap();
4144
4145 let order = spot_new_order_from_json(response.new_order_response).unwrap();
4146
4147 assert_eq!(order.price_exponent, -2);
4148 assert_eq!(order.qty_exponent, -3);
4149 assert_eq!(order.order_id, 101);
4150 assert_eq!(order.order_list_id, None);
4151 assert_eq!(order.transact_time, 1_700_000_000_123_000);
4152 assert_eq!(order.price_mantissa, 1_234);
4153 assert_eq!(order.orig_qty_mantissa, 5_678);
4154 assert_eq!(order.executed_qty_mantissa, 1_234);
4155 assert_eq!(order.cummulative_quote_qty_mantissa, 1_522_756);
4156 assert_eq!(order.status, SbeOrderStatus::PartiallyFilled);
4157 assert_eq!(order.time_in_force, SbeTimeInForce::Gtc);
4158 assert_eq!(order.order_type, SbeOrderType::Limit);
4159 assert_eq!(order.side, SbeOrderSide::Buy);
4160 assert_eq!(order.stop_price_mantissa, Some(1_111));
4161 assert_eq!(order.working_time, Some(1_700_000_000_124_000));
4162 assert_eq!(
4163 order.self_trade_prevention_mode,
4164 SbeSelfTradePreventionMode::ExpireMaker
4165 );
4166 assert_eq!(order.client_order_id, "new-order");
4167 assert_eq!(order.symbol, "ETHUSD");
4168 assert_eq!(order.fills.len(), 1);
4169 assert_eq!(order.fills[0].price_mantissa, 1_234);
4170 assert_eq!(order.fills[0].qty_mantissa, 1_234);
4171 assert_eq!(order.fills[0].commission_mantissa, 1_234);
4172 assert_eq!(order.fills[0].commission_exponent, -6);
4173 assert_eq!(order.fills[0].commission_asset, "USD");
4174 assert_eq!(order.fills[0].trade_id, Some(44));
4175 assert_eq!(order.expiry_reason, None);
4176 }
4177
4178 #[rstest]
4179 fn test_rate_limit_config() {
4180 let config = BinanceRawSpotHttpClient::rate_limit_config();
4181
4182 assert!(config.default_quota.is_some());
4183 assert_eq!(config.order_keys.len(), 2);
4185 }
4186
4187 #[rstest]
4188 fn test_quota_from_unknown_interval_returns_none() {
4189 let quota = BinanceRateLimitQuota {
4190 rate_limit_type: BinanceRateLimitType::Orders,
4191 interval: BinanceRateLimitInterval::Unknown,
4192 interval_num: 1,
4193 limit: 10,
4194 };
4195
4196 assert!(BinanceRawSpotHttpClient::quota_from("a).is_none());
4197 }
4198
4199 fn create_test_raw_client() -> BinanceRawSpotHttpClient {
4200 BinanceRawSpotHttpClient::new(
4201 BinanceEnvironment::Live,
4202 None,
4203 None,
4204 Some("http://127.0.0.1:1".to_string()),
4205 None,
4206 Some(1),
4207 None,
4208 )
4209 .unwrap()
4210 }
4211
4212 #[rstest]
4213 #[case::limit(
4214 AggTradesParams {
4215 symbol: "BTCUSDT".to_string(),
4216 from_id: None,
4217 start_time: None,
4218 end_time: None,
4219 limit: Some(1001),
4220 },
4221 "Validation error: aggregate trade limit must not exceed 1000"
4222 )]
4223 #[case::bounds(
4224 AggTradesParams {
4225 symbol: "BTCUSDT".to_string(),
4226 from_id: None,
4227 start_time: Some(2000),
4228 end_time: Some(1000),
4229 limit: Some(1000),
4230 },
4231 "Validation error: aggregate trade startTime must not exceed endTime"
4232 )]
4233 #[tokio::test]
4234 async fn test_agg_trades_rejects_invalid_bounds(
4235 #[case] params: AggTradesParams,
4236 #[case] expected: &str,
4237 ) {
4238 let error = create_test_raw_client()
4239 .agg_trades(¶ms)
4240 .await
4241 .unwrap_err();
4242
4243 assert_eq!(error.to_string(), expected);
4244 }
4245}