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, consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, hex,
41 nanos::UnixNanos, time::AtomicTime,
42};
43use nautilus_model::{
44 data::{Bar, BarType, BookOrder, TradeTick},
45 enums::{
46 AggregationSource, BarAggregation, BookType, MarketStatusAction, OrderSide, OrderType,
47 TimeInForce,
48 },
49 events::AccountState,
50 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
51 instruments::{Instrument, any::InstrumentAny},
52 orderbook::OrderBook,
53 reports::{FillReport, OrderStatusReport},
54 types::{Price, Quantity},
55};
56use nautilus_network::{
57 http::{HttpClient, HttpResponse, Method, USER_AGENT},
58 ratelimiter::quota::Quota,
59};
60use rust_decimal::Decimal;
61use serde::{Deserialize, Serialize};
62use ustr::Ustr;
63
64use super::{
65 error::{BinanceSpotHttpError, BinanceSpotHttpResult},
66 models::{
67 AvgPrice, BatchCancelResult, BatchOrderResult, BinanceAccountCommission,
68 BinanceAccountInfo, BinanceAccountRatesJson, BinanceAccountTrade, BinanceAggTrade,
69 BinanceAggTrades, BinanceBalance, BinanceCancelOrderResponse, BinanceDepth,
70 BinanceExchangeInfoJson, BinanceKline, BinanceKlines, BinanceNewOrderResponse,
71 BinanceOrderFill, BinanceOrderResponse, BinancePriceLevel, BinanceTrade, BinanceTrades,
72 BookTicker, ListenKeyResponse, NewOcoOrderListResponse, Ticker24hr, TickerPrice, TradeFee,
73 },
74 parse,
75 query::{
76 AccountCommissionParams, AccountInfoParams, AccountTradesParams, AggTradesParams,
77 AllOrdersParams, AvgPriceParams, BatchCancelItem, BatchOrderItem, CancelOpenOrdersParams,
78 CancelOrderParams, CancelReplaceOrderParams, DepthParams, KlinesParams, ListenKeyParams,
79 NewOcoOrderListParams, NewOrderParams, OpenOrdersParams, QueryOrderParams, TickerParams,
80 TradeFeeParams, TradesParams,
81 },
82};
83use crate::{
84 common::{
85 consts::{
86 BINANCE_API_KEY_HEADER, BINANCE_NAUTILUS_SPOT_BROKER_ID, BINANCE_NO_SUCH_ORDER_CODE,
87 BINANCE_SPOT_RATE_LIMITS, BINANCE_VENUE, BinanceRateLimitQuota,
88 },
89 credential::SigningCredential,
90 encoder::{decode_client_order_id, encode_broker_id},
91 enums::{
92 BinanceEnvironment, BinanceOrderStatus, BinanceProductType, BinanceRateLimitInterval,
93 BinanceRateLimitType, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
94 },
95 fees::BINANCE_SPOT_FEE_DEFAULT,
96 instruments::BinanceInstrumentSelector,
97 models::BinanceErrorResponse,
98 parse::{
99 get_currency, parse_fill_report_sbe, parse_klines_to_binance_bars,
100 parse_new_order_response_sbe, parse_order_status_report_sbe,
101 parse_spot_instrument_json_with_fees, parse_spot_instrument_sbe_with_fees,
102 parse_spot_trades_sbe,
103 },
104 urls::get_http_base_url,
105 },
106 config::BinanceInstrumentProviderConfig,
107 spot::{
108 enums::{
109 BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
110 order_type_to_binance_spot, time_in_force_to_binance_spot,
111 },
112 sbe::{
113 generated::symbol_status::SymbolStatus,
114 spot::{
115 ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
116 error_response_codec::{self, ErrorResponseDecoder},
117 message_header_codec::MessageHeaderDecoder,
118 order_side::OrderSide as SbeOrderSide,
119 order_status::OrderStatus as SbeOrderStatus,
120 order_type::OrderType as SbeOrderType,
121 self_trade_prevention_mode::SelfTradePreventionMode as SbeSelfTradePreventionMode,
122 time_in_force::TimeInForce as SbeTimeInForce,
123 },
124 },
125 },
126};
127
128pub const SBE_SCHEMA_HEADER: &str = "3:5";
133
134use crate::common::consts::{
135 BINANCE_SAPI_PATH as SAPI_PATH, BINANCE_SPOT_API_PATH as SPOT_API_PATH,
136};
137
138const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
140
141const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
143
144struct RateLimitConfig {
145 default_quota: Option<Quota>,
146 keyed_quotas: Vec<(String, Quota)>,
147 order_keys: Vec<String>,
148}
149
150#[derive(Debug, Deserialize)]
151#[serde(rename_all = "camelCase")]
152struct SpotAccountJson {
153 #[serde(default)]
154 maker_commission: i64,
155 #[serde(default)]
156 taker_commission: i64,
157 #[serde(default)]
158 buyer_commission: i64,
159 #[serde(default)]
160 seller_commission: i64,
161 #[serde(default)]
162 commission_rates: Option<SpotCommissionRatesJson>,
163 can_trade: bool,
164 can_withdraw: bool,
165 can_deposit: bool,
166 #[serde(default)]
167 require_self_trade_prevention: bool,
168 #[serde(default)]
169 prevent_sor: bool,
170 update_time: i64,
171 account_type: String,
172 balances: Vec<SpotBalanceJson>,
173}
174
175#[derive(Debug, Deserialize)]
176struct SpotCommissionRatesJson {
177 maker: String,
178 taker: String,
179 buyer: String,
180 seller: String,
181}
182
183#[derive(Debug, Deserialize)]
184struct SpotBalanceJson {
185 asset: String,
186 free: String,
187 locked: String,
188}
189
190#[derive(Debug, Deserialize)]
191#[serde(rename_all = "camelCase")]
192struct SpotOrderJson {
193 symbol: String,
194 order_id: i64,
195 #[serde(default)]
196 order_list_id: Option<i64>,
197 client_order_id: String,
198 #[serde(default)]
199 orig_client_order_id: String,
200 #[serde(default)]
201 transact_time: i64,
202 #[serde(default)]
203 price: String,
204 #[serde(default)]
205 orig_qty: String,
206 #[serde(default)]
207 executed_qty: String,
208 #[serde(default, rename = "cummulativeQuoteQty")]
209 cummulative_quote_qty: String,
210 status: BinanceOrderStatus,
211 time_in_force: BinanceTimeInForce,
212 #[serde(rename = "type")]
213 order_type: BinanceSpotOrderType,
214 side: BinanceSide,
215 #[serde(default)]
216 stop_price: String,
217 #[serde(default)]
218 iceberg_qty: String,
219 #[serde(default)]
220 time: i64,
221 #[serde(default)]
222 update_time: i64,
223 #[serde(default)]
224 is_working: bool,
225 #[serde(default)]
226 working_time: Option<i64>,
227 #[serde(default)]
228 orig_quote_order_qty: String,
229 #[serde(default)]
230 self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
231 #[serde(default)]
232 fills: Vec<SpotOrderFillJson>,
233}
234
235#[derive(Debug, Deserialize)]
236#[serde(rename_all = "camelCase")]
237struct SpotCancelReplaceJson {
238 new_order_response: SpotOrderJson,
239}
240
241#[derive(Debug, Deserialize)]
242#[serde(rename_all = "camelCase")]
243struct SpotOrderFillJson {
244 price: String,
245 qty: String,
246 commission: String,
247 commission_asset: String,
248 #[serde(default)]
249 trade_id: Option<i64>,
250}
251
252#[derive(Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254struct SpotAccountTradeJson {
255 symbol: String,
256 id: i64,
257 order_id: i64,
258 #[serde(default)]
259 order_list_id: Option<i64>,
260 price: String,
261 qty: String,
262 quote_qty: String,
263 commission: String,
264 commission_asset: String,
265 time: i64,
266 is_buyer: bool,
267 is_maker: bool,
268 is_best_match: bool,
269}
270
271#[derive(Debug, Deserialize)]
272#[serde(rename_all = "camelCase")]
273struct SpotDepthJson {
274 last_update_id: i64,
275 bids: Vec<[String; 2]>,
276 asks: Vec<[String; 2]>,
277}
278
279#[derive(Debug, Deserialize)]
280#[serde(rename_all = "camelCase")]
281struct SpotTradeJson {
282 id: i64,
283 price: String,
284 qty: String,
285 quote_qty: String,
286 time: i64,
287 is_buyer_maker: bool,
288 is_best_match: bool,
289}
290
291#[derive(Debug, Deserialize)]
292struct SpotAggTradeJson {
293 #[serde(rename = "a")]
294 id: i64,
295 #[serde(rename = "p")]
296 price: String,
297 #[serde(rename = "q")]
298 qty: String,
299 #[serde(rename = "f")]
300 first_trade_id: i64,
301 #[serde(rename = "l")]
302 last_trade_id: i64,
303 #[serde(rename = "T")]
304 time: i64,
305 #[serde(rename = "m")]
306 is_buyer_maker: bool,
307 #[serde(rename = "M")]
308 is_best_match: bool,
309}
310
311type SpotKlineJson = (
312 i64,
313 String,
314 String,
315 String,
316 String,
317 String,
318 i64,
319 String,
320 i64,
321 String,
322 String,
323 String,
324);
325
326#[derive(Debug, Clone)]
337pub struct BinanceRawSpotHttpClient {
338 client: HttpClient,
339 base_url: String,
340 credential: Option<SigningCredential>,
341 recv_window: Option<u64>,
342 order_rate_keys: Vec<String>,
343 json_responses: bool,
344}
345
346impl BinanceRawSpotHttpClient {
347 #[must_use]
349 pub fn has_credentials(&self) -> bool {
350 self.credential.is_some()
351 }
352
353 pub fn new(
359 environment: BinanceEnvironment,
360 api_key: Option<String>,
361 api_secret: Option<String>,
362 base_url_override: Option<String>,
363 recv_window: Option<u64>,
364 timeout_secs: Option<u64>,
365 proxy_url: Option<String>,
366 ) -> BinanceSpotHttpResult<Self> {
367 Self::new_with_json_responses(
368 environment,
369 api_key,
370 api_secret,
371 base_url_override,
372 recv_window,
373 timeout_secs,
374 proxy_url,
375 false,
376 )
377 }
378
379 #[expect(clippy::too_many_arguments)]
385 pub fn new_with_json_responses(
386 environment: BinanceEnvironment,
387 api_key: Option<String>,
388 api_secret: Option<String>,
389 base_url_override: Option<String>,
390 recv_window: Option<u64>,
391 timeout_secs: Option<u64>,
392 proxy_url: Option<String>,
393 json_responses: bool,
394 ) -> BinanceSpotHttpResult<Self> {
395 let RateLimitConfig {
396 default_quota,
397 keyed_quotas,
398 order_keys,
399 } = Self::rate_limit_config();
400
401 let credential = match (api_key, api_secret) {
402 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
403 (None, None) => None,
404 _ => return Err(BinanceSpotHttpError::MissingCredentials),
405 };
406
407 let base_url = base_url_override.unwrap_or_else(|| {
408 get_http_base_url(BinanceProductType::Spot, environment).to_string()
409 });
410
411 let headers = Self::default_headers(&credential, json_responses);
412
413 let client = HttpClient::builder()
414 .headers(headers)
415 .header_keys(vec![BINANCE_API_KEY_HEADER.to_string()])
416 .keyed_quotas(keyed_quotas)
417 .maybe_default_quota(default_quota)
418 .maybe_timeout_secs(timeout_secs)
419 .maybe_proxy_url(proxy_url)
420 .build()?;
421
422 Ok(Self {
423 client,
424 base_url,
425 credential,
426 recv_window,
427 order_rate_keys: order_keys,
428 json_responses,
429 })
430 }
431
432 #[must_use]
434 pub const fn schema_id() -> u16 {
435 SBE_SCHEMA_ID
436 }
437
438 #[must_use]
440 pub const fn schema_version() -> u16 {
441 SBE_SCHEMA_VERSION
442 }
443
444 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
450 where
451 P: Serialize + ?Sized,
452 {
453 self.request(Method::GET, path, params, false, false).await
454 }
455
456 pub async fn get_signed<P>(
462 &self,
463 path: &str,
464 params: Option<&P>,
465 ) -> BinanceSpotHttpResult<Vec<u8>>
466 where
467 P: Serialize + ?Sized,
468 {
469 self.request(Method::GET, path, params, true, false).await
470 }
471
472 pub async fn get_signed_json<P>(
478 &self,
479 path: &str,
480 params: Option<&P>,
481 ) -> BinanceSpotHttpResult<Vec<u8>>
482 where
483 P: Serialize + ?Sized,
484 {
485 self.request_with_extra_headers(
486 Method::GET,
487 path,
488 params,
489 true,
490 false,
491 Some(HashMap::from([(
492 "Accept".to_string(),
493 "application/json".to_string(),
494 )])),
495 )
496 .await
497 }
498
499 pub async fn post_signed<P>(
505 &self,
506 path: &str,
507 params: Option<&P>,
508 ) -> BinanceSpotHttpResult<Vec<u8>>
509 where
510 P: Serialize + ?Sized,
511 {
512 self.request(Method::POST, path, params, true, true).await
513 }
514
515 pub async fn post_signed_json<P>(
521 &self,
522 path: &str,
523 params: Option<&P>,
524 ) -> BinanceSpotHttpResult<Vec<u8>>
525 where
526 P: Serialize + ?Sized,
527 {
528 self.request_with_extra_headers(
529 Method::POST,
530 path,
531 params,
532 true,
533 true,
534 Some(HashMap::from([(
535 "Accept".to_string(),
536 "application/json".to_string(),
537 )])),
538 )
539 .await
540 }
541
542 pub async fn delete_signed<P>(
548 &self,
549 path: &str,
550 params: Option<&P>,
551 ) -> BinanceSpotHttpResult<Vec<u8>>
552 where
553 P: Serialize + ?Sized,
554 {
555 self.request(Method::DELETE, path, params, true, true).await
556 }
557
558 pub async fn delete_signed_json<P>(
564 &self,
565 path: &str,
566 params: Option<&P>,
567 ) -> BinanceSpotHttpResult<Vec<u8>>
568 where
569 P: Serialize + ?Sized,
570 {
571 self.request_with_extra_headers(
572 Method::DELETE,
573 path,
574 params,
575 true,
576 true,
577 Some(HashMap::from([(
578 "Accept".to_string(),
579 "application/json".to_string(),
580 )])),
581 )
582 .await
583 }
584
585 async fn request<P>(
586 &self,
587 method: Method,
588 path: &str,
589 params: Option<&P>,
590 signed: bool,
591 use_order_quota: bool,
592 ) -> BinanceSpotHttpResult<Vec<u8>>
593 where
594 P: Serialize + ?Sized,
595 {
596 self.request_with_extra_headers(method, path, params, signed, use_order_quota, None)
597 .await
598 }
599
600 async fn request_with_extra_headers<P>(
601 &self,
602 method: Method,
603 path: &str,
604 params: Option<&P>,
605 signed: bool,
606 use_order_quota: bool,
607 extra_headers: Option<HashMap<String, String>>,
608 ) -> BinanceSpotHttpResult<Vec<u8>>
609 where
610 P: Serialize + ?Sized,
611 {
612 let mut query = params
613 .map(serde_urlencoded::to_string)
614 .transpose()
615 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
616 .unwrap_or_default();
617
618 let mut headers = extra_headers.unwrap_or_default();
619
620 if signed {
621 let cred = self
622 .credential
623 .as_ref()
624 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
625
626 if !query.is_empty() {
627 query.push('&');
628 }
629
630 let timestamp = Timestamp::now().as_millisecond();
631 query.push_str(&format!("timestamp={timestamp}"));
632
633 if let Some(recv_window) = self.recv_window {
634 query.push_str(&format!("&recvWindow={recv_window}"));
635 }
636
637 let signature = Self::percent_encode(&cred.sign(&query));
638 query.push_str(&format!("&signature={signature}"));
639 headers.insert(
640 BINANCE_API_KEY_HEADER.to_string(),
641 cred.api_key().to_string(),
642 );
643 }
644
645 let url = self.build_url(path, &query);
646 let keys = self.rate_limit_keys(use_order_quota);
647
648 let response = self
649 .client
650 .request(
651 method,
652 url,
653 None::<&HashMap<String, Vec<String>>>,
654 Some(headers),
655 None,
656 None,
657 Some(keys),
658 )
659 .await?;
660
661 if !response.status.is_success() {
662 return self.parse_error_response(&response);
663 }
664
665 Ok(response.body.to_vec())
666 }
667
668 fn build_url(&self, path: &str, query: &str) -> String {
669 let normalized_path = if path.starts_with('/') {
670 path.to_string()
671 } else {
672 format!("/{path}")
673 };
674
675 let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
676
677 if !query.is_empty() {
678 url.push('?');
679 url.push_str(query);
680 }
681 url
682 }
683
684 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
685 if use_orders {
686 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
687 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
688 keys.extend(self.order_rate_keys.iter().cloned());
689 keys
690 } else {
691 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
692 }
693 }
694
695 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceSpotHttpResult<T> {
696 let status = response.status.as_u16();
697 let body = &response.body;
698
699 if let Ok(body_str) = std::str::from_utf8(body)
701 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
702 {
703 return Err(BinanceSpotHttpError::BinanceError {
704 code: err.code,
705 message: err.msg,
706 });
707 }
708
709 if let Some((code, message)) = Self::try_decode_sbe_error(body) {
711 return Err(BinanceSpotHttpError::BinanceError {
712 code: code.into(),
713 message,
714 });
715 }
716
717 Err(BinanceSpotHttpError::UnexpectedStatus {
718 status,
719 body: hex::encode(body),
720 })
721 }
722
723 fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
727 const HEADER_LEN: usize = 8;
728 if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
729 return None;
730 }
731
732 let buf = ReadBuf::new(body);
733
734 let header = MessageHeaderDecoder::default().wrap(buf, 0);
736 if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
737 return None;
738 }
739
740 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
742 let code = decoder.code();
743
744 let msg_coords = decoder.msg_decoder();
746 let msg_bytes = decoder.msg_slice(msg_coords);
747 let message = String::from_utf8_lossy(msg_bytes).into_owned();
748
749 Some((code, message))
750 }
751
752 fn default_headers(
753 credential: &Option<SigningCredential>,
754 json_responses: bool,
755 ) -> HashMap<String, String> {
756 let mut headers = HashMap::new();
757 headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
758 if json_responses {
759 headers.insert("Accept".to_string(), "application/json".to_string());
760 } else {
761 headers.insert("Accept".to_string(), "application/sbe".to_string());
762 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
763 }
764
765 if let Some(cred) = credential {
766 headers.insert(
767 BINANCE_API_KEY_HEADER.to_string(),
768 cred.api_key().to_string(),
769 );
770 }
771 headers
772 }
773
774 fn rate_limit_config() -> RateLimitConfig {
775 let quotas = BINANCE_SPOT_RATE_LIMITS;
776 let mut keyed = Vec::new();
777 let mut order_keys = Vec::new();
778 let mut default = None;
779
780 for quota in quotas {
781 if let Some(q) = Self::quota_from(quota) {
782 match quota.rate_limit_type {
783 BinanceRateLimitType::RequestWeight if default.is_none() => {
784 default = Some(q);
785 }
786 BinanceRateLimitType::Orders => {
787 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
788 order_keys.push(key.clone());
789 keyed.push((key, q));
790 }
791 _ => {}
792 }
793 }
794 }
795
796 let default_quota = default.unwrap_or_else(|| {
797 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
798 });
799
800 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
801
802 RateLimitConfig {
803 default_quota: Some(default_quota),
804 keyed_quotas: keyed,
805 order_keys,
806 }
807 }
808
809 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
810 let burst = NonZeroU32::new(quota.limit)?;
811 match quota.interval {
812 BinanceRateLimitInterval::Second => Quota::per_second(burst),
813 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
814 BinanceRateLimitInterval::Day => {
815 Quota::with_period(std::time::Duration::from_secs(SECONDS_IN_DAY))
816 .map(|q| q.allow_burst(burst))
817 }
818 BinanceRateLimitInterval::Unknown => None,
819 }
820 }
821
822 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
828 let bytes = self.get("ping", None::<&()>).await?;
829 parse::decode_ping(&bytes)?;
830 Ok(())
831 }
832
833 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
841 if self.json_responses {
842 #[derive(Deserialize)]
843 #[serde(rename_all = "camelCase")]
844 struct ServerTimeJson {
845 server_time: i64,
846 }
847
848 let bytes = self.get_json("time", None::<&()>).await?;
849 let response: ServerTimeJson = serde_json::from_slice(&bytes)
850 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
851 return millis_to_micros(response.server_time);
852 }
853
854 let bytes = self.get("time", None::<&()>).await?;
855 let timestamp = parse::decode_server_time(&bytes)?;
856 Ok(timestamp)
857 }
858
859 pub async fn exchange_info(
865 &self,
866 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
867 let bytes = self.get("exchangeInfo", None::<&()>).await?;
868 let info = parse::decode_exchange_info(&bytes)?;
869 Ok(info)
870 }
871
872 pub async fn exchange_info_json(&self) -> BinanceSpotHttpResult<BinanceExchangeInfoJson> {
878 let bytes = self.get_json("exchangeInfo", None::<&()>).await?;
879 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
880 }
881
882 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
888 if self.json_responses {
889 let bytes = self.get_json("depth", Some(params)).await?;
890 let response: SpotDepthJson = serde_json::from_slice(&bytes)
891 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
892 spot_depth_from_json(response)
893 } else {
894 let bytes = self.get("depth", Some(params)).await?;
895 Ok(parse::decode_depth(&bytes)?)
896 }
897 }
898
899 pub async fn trades(
905 &self,
906 symbol: &str,
907 limit: Option<u32>,
908 ) -> BinanceSpotHttpResult<BinanceTrades> {
909 let params = TradesParams {
910 symbol: symbol.to_string(),
911 limit,
912 };
913
914 if self.json_responses {
915 let bytes = self.get_json("trades", Some(¶ms)).await?;
916 let response: Vec<SpotTradeJson> = serde_json::from_slice(&bytes)
917 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
918 spot_trades_from_json(response)
919 } else {
920 let bytes = self.get("trades", Some(¶ms)).await?;
921 Ok(parse::decode_trades(&bytes)?)
922 }
923 }
924
925 pub async fn agg_trades(
931 &self,
932 params: &AggTradesParams,
933 ) -> BinanceSpotHttpResult<BinanceAggTrades> {
934 if params.limit.is_some_and(|limit| limit > 1000) {
935 return Err(BinanceSpotHttpError::ValidationError(
936 "aggregate trade limit must not exceed 1000".to_string(),
937 ));
938 }
939
940 if matches!((params.start_time, params.end_time), (Some(start), Some(end)) if start > end) {
941 return Err(BinanceSpotHttpError::ValidationError(
942 "aggregate trade startTime must not exceed endTime".to_string(),
943 ));
944 }
945
946 if self.json_responses {
947 let bytes = self.get_json("aggTrades", Some(params)).await?;
948 let response: Vec<SpotAggTradeJson> = serde_json::from_slice(&bytes)
949 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
950 spot_agg_trades_from_json(response)
951 } else {
952 let bytes = self.get("aggTrades", Some(params)).await?;
953 Ok(parse::decode_agg_trades(&bytes)?)
954 }
955 }
956
957 pub async fn klines(
963 &self,
964 symbol: &str,
965 interval: &str,
966 start_time: Option<i64>,
967 end_time: Option<i64>,
968 limit: Option<u32>,
969 ) -> BinanceSpotHttpResult<BinanceKlines> {
970 let params = KlinesParams {
971 symbol: symbol.to_string(),
972 interval: interval.to_string(),
973 start_time,
974 end_time,
975 time_zone: None,
976 limit,
977 };
978
979 if self.json_responses {
980 let bytes = self.get_json("klines", Some(¶ms)).await?;
981 let response: Vec<SpotKlineJson> = serde_json::from_slice(&bytes)
982 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
983 spot_klines_from_json(response)
984 } else {
985 let bytes = self.get("klines", Some(¶ms)).await?;
986 Ok(parse::decode_klines(&bytes)?)
987 }
988 }
989
990 async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
992 where
993 P: Serialize + ?Sized,
994 {
995 let query = params
996 .map(serde_urlencoded::to_string)
997 .transpose()
998 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
999 .unwrap_or_default();
1000
1001 let url = self.build_url(path, &query);
1002 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1003
1004 let response = self
1005 .client
1006 .request(
1007 Method::GET,
1008 url,
1009 None::<&HashMap<String, Vec<String>>>,
1010 None,
1011 None,
1012 None,
1013 Some(keys),
1014 )
1015 .await?;
1016
1017 if !response.status.is_success() {
1018 return self.parse_error_response(&response);
1019 }
1020
1021 Ok(response.body.to_vec())
1022 }
1023
1024 pub async fn ticker_24hr(
1032 &self,
1033 symbol: Option<&str>,
1034 ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
1035 let params = symbol.map(TickerParams::for_symbol);
1036 let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
1037
1038 if symbol.is_some() {
1040 let ticker: Ticker24hr = serde_json::from_slice(&bytes)
1041 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1042 Ok(vec![ticker])
1043 } else {
1044 let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
1045 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1046 Ok(tickers)
1047 }
1048 }
1049
1050 pub async fn ticker_price(
1058 &self,
1059 symbol: Option<&str>,
1060 ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
1061 let params = symbol.map(TickerParams::for_symbol);
1062 let bytes = self.get_json("ticker/price", params.as_ref()).await?;
1063
1064 if symbol.is_some() {
1066 let ticker: TickerPrice = serde_json::from_slice(&bytes)
1067 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1068 Ok(vec![ticker])
1069 } else {
1070 let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
1071 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1072 Ok(tickers)
1073 }
1074 }
1075
1076 pub async fn ticker_book(
1084 &self,
1085 symbol: Option<&str>,
1086 ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
1087 let params = symbol.map(TickerParams::for_symbol);
1088 let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
1089
1090 if symbol.is_some() {
1092 let ticker: BookTicker = serde_json::from_slice(&bytes)
1093 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1094 Ok(vec![ticker])
1095 } else {
1096 let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
1097 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1098 Ok(tickers)
1099 }
1100 }
1101
1102 pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
1108 let params = AvgPriceParams::new(symbol);
1109 let bytes = self.get_json("avgPrice", Some(¶ms)).await?;
1110
1111 let avg_price: AvgPrice = serde_json::from_slice(&bytes)
1112 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1113 Ok(avg_price)
1114 }
1115
1116 pub async fn get_trade_fee(
1125 &self,
1126 symbol: Option<&str>,
1127 ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
1128 let params = symbol.map(TradeFeeParams::for_symbol);
1129 let bytes = self
1130 .get_signed_sapi("asset/tradeFee", params.as_ref())
1131 .await?;
1132
1133 let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
1134 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1135 Ok(fees)
1136 }
1137
1138 async fn get_signed_sapi<P>(
1140 &self,
1141 path: &str,
1142 params: Option<&P>,
1143 ) -> BinanceSpotHttpResult<Vec<u8>>
1144 where
1145 P: Serialize + ?Sized,
1146 {
1147 let cred = self
1148 .credential
1149 .as_ref()
1150 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1151
1152 let mut query = params
1153 .map(serde_urlencoded::to_string)
1154 .transpose()
1155 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1156 .unwrap_or_default();
1157
1158 if !query.is_empty() {
1159 query.push('&');
1160 }
1161
1162 let timestamp = Timestamp::now().as_millisecond();
1163 query.push_str(&format!("timestamp={timestamp}"));
1164
1165 if let Some(recv_window) = self.recv_window {
1166 query.push_str(&format!("&recvWindow={recv_window}"));
1167 }
1168
1169 let signature = Self::percent_encode(&cred.sign(&query));
1170 query.push_str(&format!("&signature={signature}"));
1171
1172 let normalized_path = if path.starts_with('/') {
1174 path.to_string()
1175 } else {
1176 format!("/{path}")
1177 };
1178
1179 let mut url = format!("{}{}{}", self.base_url, SAPI_PATH, normalized_path);
1180
1181 if !query.is_empty() {
1182 url.push('?');
1183 url.push_str(&query);
1184 }
1185
1186 let mut headers = HashMap::new();
1187 headers.insert(
1188 BINANCE_API_KEY_HEADER.to_string(),
1189 cred.api_key().to_string(),
1190 );
1191
1192 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1193
1194 let response = self
1195 .client
1196 .request(
1197 Method::GET,
1198 url,
1199 None::<&HashMap<String, Vec<String>>>,
1200 Some(headers),
1201 None,
1202 None,
1203 Some(keys),
1204 )
1205 .await?;
1206
1207 if !response.status.is_success() {
1208 return self.parse_error_response(&response);
1209 }
1210
1211 Ok(response.body.to_vec())
1212 }
1213
1214 fn percent_encode(input: &str) -> String {
1216 let mut result = String::with_capacity(input.len() * 3);
1217 for byte in input.bytes() {
1218 match byte {
1219 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1221 result.push(byte as char);
1222 }
1223 _ => {
1224 result.push('%');
1225 result.push_str(&format!("{byte:02X}"));
1226 }
1227 }
1228 }
1229 result
1230 }
1231
1232 pub async fn batch_submit_orders(
1243 &self,
1244 orders: &[BatchOrderItem],
1245 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1246 if orders.is_empty() {
1247 return Ok(Vec::new());
1248 }
1249
1250 if orders.len() > 5 {
1251 return Err(BinanceSpotHttpError::ValidationError(
1252 "Batch order limit is 5 orders maximum".to_string(),
1253 ));
1254 }
1255
1256 let batch_json = serde_json::to_string(orders)
1257 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1258
1259 let bytes = self
1260 .batch_request(Method::POST, "batchOrders", &batch_json)
1261 .await?;
1262
1263 let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
1264 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1265
1266 Ok(results)
1267 }
1268
1269 pub async fn batch_cancel_orders(
1280 &self,
1281 cancels: &[BatchCancelItem],
1282 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
1283 if cancels.is_empty() {
1284 return Ok(Vec::new());
1285 }
1286
1287 if cancels.len() > 5 {
1288 return Err(BinanceSpotHttpError::ValidationError(
1289 "Batch cancel limit is 5 orders maximum".to_string(),
1290 ));
1291 }
1292
1293 let batch_json = serde_json::to_string(cancels)
1294 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1295
1296 let bytes = self
1297 .batch_request(Method::DELETE, "batchOrders", &batch_json)
1298 .await?;
1299
1300 let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
1301 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1302
1303 Ok(results)
1304 }
1305
1306 async fn batch_request(
1308 &self,
1309 method: Method,
1310 path: &str,
1311 batch_json: &str,
1312 ) -> BinanceSpotHttpResult<Vec<u8>> {
1313 let cred = self
1314 .credential
1315 .as_ref()
1316 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1317
1318 let encoded_batch = Self::percent_encode(batch_json);
1319 let timestamp = Timestamp::now().as_millisecond();
1320 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
1321
1322 if let Some(recv_window) = self.recv_window {
1323 query.push_str(&format!("&recvWindow={recv_window}"));
1324 }
1325
1326 let signature = Self::percent_encode(&cred.sign(&query));
1327 query.push_str(&format!("&signature={signature}"));
1328
1329 let url = self.build_url(path, &query);
1330
1331 let mut headers = HashMap::new();
1332 headers.insert(
1333 BINANCE_API_KEY_HEADER.to_string(),
1334 cred.api_key().to_string(),
1335 );
1336
1337 let keys = self.rate_limit_keys(true);
1338
1339 let response = self
1340 .client
1341 .request(
1342 method,
1343 url,
1344 None::<&HashMap<String, Vec<String>>>,
1345 Some(headers),
1346 None,
1347 None,
1348 Some(keys),
1349 )
1350 .await?;
1351
1352 if !response.status.is_success() {
1353 return self.parse_error_response(&response);
1354 }
1355
1356 Ok(response.body.to_vec())
1357 }
1358
1359 pub async fn account(
1365 &self,
1366 params: &AccountInfoParams,
1367 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
1368 if self.json_responses {
1369 let bytes = self.get_signed_json("account", Some(params)).await?;
1370 let response: SpotAccountJson = serde_json::from_slice(&bytes)
1371 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1372 spot_account_from_json(response)
1373 } else {
1374 let bytes = self.get_signed("account", Some(params)).await?;
1375 Ok(parse::decode_account(&bytes)?)
1376 }
1377 }
1378
1379 pub async fn account_commission(
1385 &self,
1386 symbol: &str,
1387 ) -> BinanceSpotHttpResult<BinanceAccountCommission> {
1388 let params = AccountCommissionParams::new(symbol);
1389 let bytes = self
1390 .get_signed_json("account/commission", Some(¶ms))
1391 .await?;
1392 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1393 }
1394
1395 pub async fn account_rates_json(&self) -> BinanceSpotHttpResult<BinanceAccountRatesJson> {
1401 let params = AccountInfoParams::default();
1402 let bytes = self.get_signed_json("account", Some(¶ms)).await?;
1403 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1404 }
1405
1406 pub async fn account_trades(
1412 &self,
1413 symbol: &str,
1414 order_id: Option<i64>,
1415 start_time: Option<i64>,
1416 end_time: Option<i64>,
1417 limit: Option<u32>,
1418 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1419 self.account_trades_with_cursor(symbol, order_id, start_time, end_time, None, limit)
1420 .await
1421 }
1422
1423 async fn account_trades_with_cursor(
1424 &self,
1425 symbol: &str,
1426 order_id: Option<i64>,
1427 start_time: Option<i64>,
1428 end_time: Option<i64>,
1429 from_id: Option<i64>,
1430 limit: Option<u32>,
1431 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1432 let params = AccountTradesParams {
1433 symbol: symbol.to_string(),
1434 order_id,
1435 start_time,
1436 end_time,
1437 from_id,
1438 limit,
1439 };
1440
1441 if self.json_responses {
1442 let bytes = self.get_signed_json("myTrades", Some(¶ms)).await?;
1443 let response: Vec<SpotAccountTradeJson> = serde_json::from_slice(&bytes)
1444 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1445 response
1446 .into_iter()
1447 .map(spot_account_trade_from_json)
1448 .collect()
1449 } else {
1450 let bytes = self.get_signed("myTrades", Some(¶ms)).await?;
1451 Ok(parse::decode_account_trades(&bytes)?)
1452 }
1453 }
1454
1455 pub async fn query_order(
1463 &self,
1464 symbol: &str,
1465 order_id: Option<i64>,
1466 client_order_id: Option<&str>,
1467 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
1468 let params = QueryOrderParams {
1469 symbol: symbol.to_string(),
1470 order_id,
1471 orig_client_order_id: client_order_id.map(|s| s.to_string()),
1472 };
1473
1474 if self.json_responses {
1475 let bytes = self.get_signed_json("order", Some(¶ms)).await?;
1476 let response: SpotOrderJson = serde_json::from_slice(&bytes)
1477 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1478 spot_order_from_json(response)
1479 } else {
1480 let bytes = self.get_signed("order", Some(¶ms)).await?;
1481 Ok(parse::decode_order(&bytes)?)
1482 }
1483 }
1484
1485 pub async fn open_orders(
1491 &self,
1492 symbol: Option<&str>,
1493 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1494 let params = OpenOrdersParams {
1495 symbol: symbol.map(|s| s.to_string()),
1496 };
1497
1498 if self.json_responses {
1499 let bytes = self.get_signed_json("openOrders", Some(¶ms)).await?;
1500 let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1501 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1502 response.into_iter().map(spot_order_from_json).collect()
1503 } else {
1504 let bytes = self.get_signed("openOrders", Some(¶ms)).await?;
1505 Ok(parse::decode_orders(&bytes)?)
1506 }
1507 }
1508
1509 pub async fn all_orders(
1515 &self,
1516 symbol: &str,
1517 start_time: Option<i64>,
1518 end_time: Option<i64>,
1519 limit: Option<u32>,
1520 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1521 let params = AllOrdersParams {
1522 symbol: symbol.to_string(),
1523 order_id: None,
1524 start_time,
1525 end_time,
1526 limit,
1527 };
1528
1529 if self.json_responses {
1530 let bytes = self.get_signed_json("allOrders", Some(¶ms)).await?;
1531 let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1532 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1533 response.into_iter().map(spot_order_from_json).collect()
1534 } else {
1535 let bytes = self.get_signed("allOrders", Some(¶ms)).await?;
1536 Ok(parse::decode_orders(&bytes)?)
1537 }
1538 }
1539
1540 async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1542 where
1543 P: Serialize + ?Sized,
1544 {
1545 if self.json_responses {
1546 self.post_signed_json(path, params).await
1547 } else {
1548 self.post_signed(path, params).await
1549 }
1550 }
1551
1552 async fn delete_order<P>(
1554 &self,
1555 path: &str,
1556 params: Option<&P>,
1557 ) -> BinanceSpotHttpResult<Vec<u8>>
1558 where
1559 P: Serialize + ?Sized,
1560 {
1561 if self.json_responses {
1562 self.delete_signed_json(path, params).await
1563 } else {
1564 self.delete_signed(path, params).await
1565 }
1566 }
1567
1568 #[expect(clippy::too_many_arguments)]
1574 pub async fn new_order(
1575 &self,
1576 symbol: &str,
1577 side: BinanceSide,
1578 order_type: BinanceSpotOrderType,
1579 time_in_force: Option<BinanceTimeInForce>,
1580 quantity: Option<&str>,
1581 price: Option<&str>,
1582 client_order_id: Option<&str>,
1583 stop_price: Option<&str>,
1584 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1585 let params = NewOrderParams {
1586 symbol: symbol.to_string(),
1587 side,
1588 order_type,
1589 time_in_force,
1590 quantity: quantity.map(|s| s.to_string()),
1591 quote_order_qty: None,
1592 price: price.map(|s| s.to_string()),
1593 new_client_order_id: client_order_id.map(|s| s.to_string()),
1594 stop_price: stop_price.map(|s| s.to_string()),
1595 trailing_delta: None,
1596 iceberg_qty: None,
1597 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1598 self_trade_prevention_mode: None,
1599 strategy_id: None,
1600 strategy_type: None,
1601 };
1602 let bytes = self.post_order("order", Some(¶ms)).await?;
1603 self.decode_new_order_response(&bytes)
1604 }
1605
1606 #[expect(clippy::too_many_arguments)]
1616 pub async fn new_order_full(
1617 &self,
1618 symbol: &str,
1619 side: BinanceSide,
1620 order_type: BinanceSpotOrderType,
1621 time_in_force: Option<BinanceTimeInForce>,
1622 quantity: Option<&str>,
1623 quote_order_qty: Option<&str>,
1624 price: Option<&str>,
1625 client_order_id: Option<&str>,
1626 stop_price: Option<&str>,
1627 iceberg_qty: Option<&str>,
1628 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1629 let params = NewOrderParams {
1630 symbol: symbol.to_string(),
1631 side,
1632 order_type,
1633 time_in_force,
1634 quantity: quantity.map(|s| s.to_string()),
1635 quote_order_qty: quote_order_qty.map(|s| s.to_string()),
1636 price: price.map(|s| s.to_string()),
1637 new_client_order_id: client_order_id.map(|s| s.to_string()),
1638 stop_price: stop_price.map(|s| s.to_string()),
1639 trailing_delta: None,
1640 iceberg_qty: iceberg_qty.map(|s| s.to_string()),
1641 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1642 self_trade_prevention_mode: None,
1643 strategy_id: None,
1644 strategy_type: None,
1645 };
1646 let bytes = self.post_order("order", Some(¶ms)).await?;
1647 self.decode_new_order_response(&bytes)
1648 }
1649
1650 pub async fn new_oco_order_list(
1656 &self,
1657 params: &NewOcoOrderListParams,
1658 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
1659 let bytes = self.post_signed_json("orderList/oco", Some(params)).await?;
1660 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1661 }
1662
1663 #[expect(clippy::too_many_arguments)]
1669 pub async fn cancel_replace_order(
1670 &self,
1671 symbol: &str,
1672 side: BinanceSide,
1673 order_type: BinanceSpotOrderType,
1674 time_in_force: Option<BinanceTimeInForce>,
1675 quantity: Option<&str>,
1676 price: Option<&str>,
1677 cancel_order_id: Option<i64>,
1678 cancel_client_order_id: Option<&str>,
1679 new_client_order_id: Option<&str>,
1680 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1681 let params = CancelReplaceOrderParams {
1682 symbol: symbol.to_string(),
1683 side,
1684 order_type,
1685 cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1686 time_in_force,
1687 quantity: quantity.map(|s| s.to_string()),
1688 quote_order_qty: None,
1689 price: price.map(|s| s.to_string()),
1690 cancel_order_id,
1691 cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1692 new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1693 stop_price: None,
1694 trailing_delta: None,
1695 iceberg_qty: None,
1696 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1697 self_trade_prevention_mode: None,
1698 };
1699 let bytes = self
1700 .post_order("order/cancelReplace", Some(¶ms))
1701 .await?;
1702
1703 if self.json_responses {
1704 let response: SpotCancelReplaceJson = serde_json::from_slice(&bytes)
1705 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1706 spot_new_order_from_json(response.new_order_response)
1707 } else {
1708 self.decode_new_order_response(&bytes)
1709 }
1710 }
1711
1712 pub async fn cancel_order(
1720 &self,
1721 symbol: &str,
1722 order_id: Option<i64>,
1723 client_order_id: Option<&str>,
1724 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1725 let params = match (order_id, client_order_id) {
1726 (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1727 (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1728 (None, None) => {
1729 return Err(BinanceSpotHttpError::ValidationError(
1730 "Either order_id or client_order_id must be provided".to_string(),
1731 ));
1732 }
1733 };
1734 let bytes = self.delete_order("order", Some(¶ms)).await?;
1735 self.decode_cancel_order_response(&bytes)
1736 }
1737
1738 pub async fn cancel_open_orders(
1744 &self,
1745 symbol: &str,
1746 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1747 let params = CancelOpenOrdersParams::new(symbol.to_string());
1748 let bytes = self.delete_order("openOrders", Some(¶ms)).await?;
1749 if self.json_responses {
1750 let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1751 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1752 response
1753 .into_iter()
1754 .map(spot_cancel_order_from_json)
1755 .collect()
1756 } else {
1757 Ok(parse::decode_cancel_open_orders(&bytes)?)
1758 }
1759 }
1760
1761 fn decode_new_order_response(
1762 &self,
1763 bytes: &[u8],
1764 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1765 if self.json_responses {
1766 let response: SpotOrderJson = serde_json::from_slice(bytes)
1767 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1768 spot_new_order_from_json(response)
1769 } else {
1770 Ok(parse::decode_new_order_full(bytes)?)
1771 }
1772 }
1773
1774 fn decode_cancel_order_response(
1775 &self,
1776 bytes: &[u8],
1777 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1778 if self.json_responses {
1779 let response: SpotOrderJson = serde_json::from_slice(bytes)
1780 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1781 spot_cancel_order_from_json(response)
1782 } else {
1783 Ok(parse::decode_cancel_order(bytes)?)
1784 }
1785 }
1786
1787 async fn request_with_api_key<P>(
1789 &self,
1790 method: Method,
1791 path: &str,
1792 params: Option<&P>,
1793 ) -> BinanceSpotHttpResult<Vec<u8>>
1794 where
1795 P: Serialize + ?Sized,
1796 {
1797 let cred = self
1798 .credential
1799 .as_ref()
1800 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1801
1802 let query = params
1803 .map(serde_urlencoded::to_string)
1804 .transpose()
1805 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1806 .unwrap_or_default();
1807
1808 let url = self.build_url(path, &query);
1809
1810 let mut headers = HashMap::new();
1811 headers.insert(
1812 BINANCE_API_KEY_HEADER.to_string(),
1813 cred.api_key().to_string(),
1814 );
1815
1816 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1817
1818 let response = self
1819 .client
1820 .request(
1821 method,
1822 url,
1823 None::<&HashMap<String, Vec<String>>>,
1824 Some(headers),
1825 None,
1826 None,
1827 Some(keys),
1828 )
1829 .await?;
1830
1831 if !response.status.is_success() {
1832 return self.parse_error_response(&response);
1833 }
1834
1835 Ok(response.body.to_vec())
1836 }
1837
1838 pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1847 let bytes = self
1848 .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1849 .await?;
1850
1851 let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1852 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1853
1854 Ok(response)
1855 }
1856
1857 pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1865 let params = ListenKeyParams::new(listen_key);
1866 self.request_with_api_key(Method::PUT, "userDataStream", Some(¶ms))
1867 .await?;
1868 Ok(())
1869 }
1870
1871 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1877 let params = ListenKeyParams::new(listen_key);
1878 self.request_with_api_key(Method::DELETE, "userDataStream", Some(¶ms))
1879 .await?;
1880 Ok(())
1881 }
1882}
1883
1884fn spot_depth_from_json(response: SpotDepthJson) -> BinanceSpotHttpResult<BinanceDepth> {
1885 let price_scale = decimal_common_scale(
1886 response
1887 .bids
1888 .iter()
1889 .chain(&response.asks)
1890 .map(|level| level[0].as_str()),
1891 )?;
1892 let qty_scale = decimal_common_scale(
1893 response
1894 .bids
1895 .iter()
1896 .chain(&response.asks)
1897 .map(|level| level[1].as_str()),
1898 )?;
1899 let parse_levels = |levels: Vec<[String; 2]>| {
1900 levels
1901 .into_iter()
1902 .map(|level| {
1903 Ok(BinancePriceLevel {
1904 price_mantissa: decimal_mantissa(&level[0], price_scale)?,
1905 qty_mantissa: decimal_mantissa(&level[1], qty_scale)?,
1906 })
1907 })
1908 .collect::<BinanceSpotHttpResult<Vec<_>>>()
1909 };
1910
1911 Ok(BinanceDepth {
1912 last_update_id: response.last_update_id,
1913 price_exponent: decimal_exponent(price_scale)?,
1914 qty_exponent: decimal_exponent(qty_scale)?,
1915 bids: parse_levels(response.bids)?,
1916 asks: parse_levels(response.asks)?,
1917 })
1918}
1919
1920fn spot_trades_from_json(response: Vec<SpotTradeJson>) -> BinanceSpotHttpResult<BinanceTrades> {
1921 let price_scale = decimal_common_scale(
1922 response
1923 .iter()
1924 .flat_map(|trade| [trade.price.as_str(), trade.quote_qty.as_str()]),
1925 )?;
1926 let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1927 let trades = response
1928 .into_iter()
1929 .map(|trade| {
1930 Ok(BinanceTrade {
1931 id: trade.id,
1932 price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1933 qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1934 quote_qty_mantissa: decimal_mantissa(&trade.quote_qty, price_scale)?,
1935 time: millis_to_micros(trade.time)?,
1936 is_buyer_maker: trade.is_buyer_maker,
1937 is_best_match: trade.is_best_match,
1938 })
1939 })
1940 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
1941
1942 Ok(BinanceTrades {
1943 price_exponent: decimal_exponent(price_scale)?,
1944 qty_exponent: decimal_exponent(qty_scale)?,
1945 trades,
1946 })
1947}
1948
1949fn spot_agg_trades_from_json(
1950 response: Vec<SpotAggTradeJson>,
1951) -> BinanceSpotHttpResult<BinanceAggTrades> {
1952 let price_scale = decimal_common_scale(response.iter().map(|trade| trade.price.as_str()))?;
1953 let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1954 let trades = response
1955 .into_iter()
1956 .map(|trade| {
1957 Ok(BinanceAggTrade {
1958 id: trade.id,
1959 price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1960 qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1961 first_trade_id: trade.first_trade_id,
1962 last_trade_id: trade.last_trade_id,
1963 time: millis_to_micros(trade.time)?,
1964 is_buyer_maker: trade.is_buyer_maker,
1965 is_best_match: trade.is_best_match,
1966 })
1967 })
1968 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
1969
1970 Ok(BinanceAggTrades {
1971 price_exponent: decimal_exponent(price_scale)?,
1972 qty_exponent: decimal_exponent(qty_scale)?,
1973 trades,
1974 })
1975}
1976
1977fn spot_klines_from_json(response: Vec<SpotKlineJson>) -> BinanceSpotHttpResult<BinanceKlines> {
1978 let price_scale = decimal_common_scale(response.iter().flat_map(|kline| {
1979 [
1980 kline.1.as_str(),
1981 kline.2.as_str(),
1982 kline.3.as_str(),
1983 kline.4.as_str(),
1984 kline.7.as_str(),
1985 kline.10.as_str(),
1986 ]
1987 }))?;
1988 let qty_scale = decimal_common_scale(
1989 response
1990 .iter()
1991 .flat_map(|kline| [kline.5.as_str(), kline.9.as_str()]),
1992 )?;
1993 let klines = response
1994 .into_iter()
1995 .map(|kline| {
1996 let volume = decimal_i128_bytes(&kline.5, qty_scale)?;
1997 let quote_volume = decimal_i128_bytes(&kline.7, price_scale)?;
1998 let taker_buy_base_volume = decimal_i128_bytes(&kline.9, qty_scale)?;
1999 let taker_buy_quote_volume = decimal_i128_bytes(&kline.10, price_scale)?;
2000 Ok(BinanceKline {
2001 open_time: millis_to_micros(kline.0)?,
2002 open_price: decimal_mantissa(&kline.1, price_scale)?,
2003 high_price: decimal_mantissa(&kline.2, price_scale)?,
2004 low_price: decimal_mantissa(&kline.3, price_scale)?,
2005 close_price: decimal_mantissa(&kline.4, price_scale)?,
2006 volume,
2007 close_time: millis_to_micros(kline.6)?,
2008 quote_volume,
2009 num_trades: kline.8,
2010 taker_buy_base_volume,
2011 taker_buy_quote_volume,
2012 })
2013 })
2014 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2015
2016 Ok(BinanceKlines {
2017 price_exponent: decimal_exponent(price_scale)?,
2018 qty_exponent: decimal_exponent(qty_scale)?,
2019 klines,
2020 })
2021}
2022
2023fn decimal_i128_bytes(value: &str, scale: u32) -> BinanceSpotHttpResult<[u8; 16]> {
2024 let mut decimal = Decimal::from_str_exact(value)
2025 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2026 decimal.rescale(scale);
2027 Ok(decimal.mantissa().to_le_bytes())
2028}
2029
2030fn spot_account_from_json(response: SpotAccountJson) -> BinanceSpotHttpResult<BinanceAccountInfo> {
2031 let commission_rates = response.commission_rates.map_or_else(
2032 || {
2033 [
2034 Decimal::new(response.maker_commission, 4).to_string(),
2035 Decimal::new(response.taker_commission, 4).to_string(),
2036 Decimal::new(response.buyer_commission, 4).to_string(),
2037 Decimal::new(response.seller_commission, 4).to_string(),
2038 ]
2039 },
2040 |rates| [rates.maker, rates.taker, rates.buyer, rates.seller],
2041 );
2042 let commission_scale = decimal_common_scale(commission_rates.iter().map(String::as_str))?;
2043 let balances = response
2044 .balances
2045 .into_iter()
2046 .map(|balance| {
2047 let scale = decimal_common_scale([balance.free.as_str(), balance.locked.as_str()])?;
2048 Ok(BinanceBalance {
2049 asset: balance.asset,
2050 free_mantissa: decimal_mantissa(&balance.free, scale)?,
2051 locked_mantissa: decimal_mantissa(&balance.locked, scale)?,
2052 exponent: decimal_exponent(scale)?,
2053 })
2054 })
2055 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2056
2057 Ok(BinanceAccountInfo {
2058 commission_exponent: decimal_exponent(commission_scale)?,
2059 maker_commission_mantissa: decimal_mantissa(&commission_rates[0], commission_scale)?,
2060 taker_commission_mantissa: decimal_mantissa(&commission_rates[1], commission_scale)?,
2061 buyer_commission_mantissa: decimal_mantissa(&commission_rates[2], commission_scale)?,
2062 seller_commission_mantissa: decimal_mantissa(&commission_rates[3], commission_scale)?,
2063 can_trade: response.can_trade,
2064 can_withdraw: response.can_withdraw,
2065 can_deposit: response.can_deposit,
2066 require_self_trade_prevention: response.require_self_trade_prevention,
2067 prevent_sor: response.prevent_sor,
2068 update_time: millis_to_micros(response.update_time)?,
2069 account_type: response.account_type,
2070 balances,
2071 })
2072}
2073
2074fn spot_new_order_from_json(
2075 response: SpotOrderJson,
2076) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
2077 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2078 let fills = response
2079 .fills
2080 .iter()
2081 .map(|fill| {
2082 let commission_scale = decimal_common_scale([fill.commission.as_str()])?;
2083 Ok(BinanceOrderFill {
2084 price_mantissa: decimal_mantissa(&fill.price, price_scale)?,
2085 qty_mantissa: decimal_mantissa(&fill.qty, qty_scale)?,
2086 commission_mantissa: decimal_mantissa(&fill.commission, commission_scale)?,
2087 commission_exponent: decimal_exponent(commission_scale)?,
2088 commission_asset: fill.commission_asset.clone(),
2089 trade_id: fill.trade_id,
2090 })
2091 })
2092 .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2093
2094 Ok(BinanceNewOrderResponse {
2095 price_exponent: decimal_exponent(price_scale)?,
2096 qty_exponent: decimal_exponent(qty_scale)?,
2097 order_id: response.order_id,
2098 order_list_id: valid_order_list_id(response.order_list_id),
2099 transact_time: millis_to_micros(response.transact_time)?,
2100 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2101 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2102 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2103 cummulative_quote_qty_mantissa: decimal_mantissa(
2104 &response.cummulative_quote_qty,
2105 price_scale + qty_scale,
2106 )?,
2107 status: spot_sbe_order_status(response.status),
2108 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2109 order_type: spot_sbe_order_type(response.order_type),
2110 side: spot_sbe_order_side(response.side),
2111 stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2112 working_time: response.working_time.map(millis_to_micros).transpose()?,
2113 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2114 client_order_id: response.client_order_id,
2115 symbol: response.symbol,
2116 fills,
2117 expiry_reason: None,
2118 })
2119}
2120
2121fn spot_cancel_order_from_json(
2122 response: SpotOrderJson,
2123) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
2124 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2125 Ok(BinanceCancelOrderResponse {
2126 price_exponent: decimal_exponent(price_scale)?,
2127 qty_exponent: decimal_exponent(qty_scale)?,
2128 order_id: response.order_id,
2129 order_list_id: valid_order_list_id(response.order_list_id),
2130 transact_time: millis_to_micros(response.transact_time)?,
2131 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2132 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2133 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2134 cummulative_quote_qty_mantissa: decimal_mantissa(
2135 &response.cummulative_quote_qty,
2136 price_scale + qty_scale,
2137 )?,
2138 status: spot_sbe_order_status(response.status),
2139 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2140 order_type: spot_sbe_order_type(response.order_type),
2141 side: spot_sbe_order_side(response.side),
2142 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2143 client_order_id: response.client_order_id,
2144 orig_client_order_id: response.orig_client_order_id,
2145 symbol: response.symbol,
2146 })
2147}
2148
2149fn spot_order_from_json(response: SpotOrderJson) -> BinanceSpotHttpResult<BinanceOrderResponse> {
2150 let (price_scale, qty_scale) = spot_order_scales(&response)?;
2151 Ok(BinanceOrderResponse {
2152 price_exponent: decimal_exponent(price_scale)?,
2153 qty_exponent: decimal_exponent(qty_scale)?,
2154 order_id: response.order_id,
2155 order_list_id: valid_order_list_id(response.order_list_id),
2156 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2157 orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2158 executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2159 cummulative_quote_qty_mantissa: decimal_mantissa(
2160 &response.cummulative_quote_qty,
2161 price_scale + qty_scale,
2162 )?,
2163 status: spot_sbe_order_status(response.status),
2164 time_in_force: spot_sbe_time_in_force(response.time_in_force),
2165 order_type: spot_sbe_order_type(response.order_type),
2166 side: spot_sbe_order_side(response.side),
2167 stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2168 iceberg_qty_mantissa: decimal_optional_mantissa(&response.iceberg_qty, qty_scale)?,
2169 time: millis_to_micros(response.time)?,
2170 update_time: millis_to_micros(response.update_time)?,
2171 is_working: response.is_working,
2172 working_time: response.working_time.map(millis_to_micros).transpose()?,
2173 orig_quote_order_qty_mantissa: decimal_mantissa(
2174 &response.orig_quote_order_qty,
2175 price_scale + qty_scale,
2176 )?,
2177 self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2178 client_order_id: response.client_order_id,
2179 symbol: response.symbol,
2180 expiry_reason: None,
2181 })
2182}
2183
2184fn spot_account_trade_from_json(
2185 response: SpotAccountTradeJson,
2186) -> BinanceSpotHttpResult<BinanceAccountTrade> {
2187 let price_scale = decimal_common_scale([response.price.as_str()])?;
2188 let qty_scale = decimal_common_scale([response.qty.as_str()])?;
2189 let commission_scale = decimal_common_scale([response.commission.as_str()])?;
2190 Ok(BinanceAccountTrade {
2191 price_exponent: decimal_exponent(price_scale)?,
2192 qty_exponent: decimal_exponent(qty_scale)?,
2193 commission_exponent: decimal_exponent(commission_scale)?,
2194 id: response.id,
2195 order_id: response.order_id,
2196 order_list_id: valid_order_list_id(response.order_list_id),
2197 price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2198 qty_mantissa: decimal_mantissa(&response.qty, qty_scale)?,
2199 quote_qty_mantissa: decimal_mantissa(&response.quote_qty, price_scale + qty_scale)?,
2200 commission_mantissa: decimal_mantissa(&response.commission, commission_scale)?,
2201 time: millis_to_micros(response.time)?,
2202 is_buyer: response.is_buyer,
2203 is_maker: response.is_maker,
2204 is_best_match: response.is_best_match,
2205 symbol: response.symbol,
2206 commission_asset: response.commission_asset,
2207 })
2208}
2209
2210fn spot_order_scales(response: &SpotOrderJson) -> BinanceSpotHttpResult<(u32, u32)> {
2211 let price_scale = decimal_common_scale(
2212 [response.price.as_str(), response.stop_price.as_str()]
2213 .into_iter()
2214 .chain(response.fills.iter().map(|fill| fill.price.as_str())),
2215 )?;
2216 let qty_scale = decimal_common_scale(
2217 [
2218 response.orig_qty.as_str(),
2219 response.executed_qty.as_str(),
2220 response.iceberg_qty.as_str(),
2221 ]
2222 .into_iter()
2223 .chain(response.fills.iter().map(|fill| fill.qty.as_str())),
2224 )?;
2225 Ok((price_scale, qty_scale))
2226}
2227
2228fn decimal_common_scale<'a>(
2229 values: impl IntoIterator<Item = &'a str>,
2230) -> BinanceSpotHttpResult<u32> {
2231 values
2232 .into_iter()
2233 .filter(|value| !value.is_empty())
2234 .try_fold(0, |scale, value| {
2235 let decimal = Decimal::from_str_exact(value)
2236 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2237 Ok(scale.max(decimal.scale()))
2238 })
2239}
2240
2241fn decimal_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<i64> {
2242 if value.is_empty() {
2243 return Ok(0);
2244 }
2245 let mut decimal = Decimal::from_str_exact(value)
2246 .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2247 decimal.rescale(scale);
2248 i64::try_from(decimal.mantissa()).map_err(|_| {
2249 BinanceSpotHttpError::ResponseParseError(format!(
2250 "decimal mantissa is outside i64 range: {value}"
2251 ))
2252 })
2253}
2254
2255fn decimal_optional_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<Option<i64>> {
2256 let mantissa = decimal_mantissa(value, scale)?;
2257 Ok((mantissa != 0).then_some(mantissa))
2258}
2259
2260fn decimal_exponent(scale: u32) -> BinanceSpotHttpResult<i8> {
2261 i8::try_from(scale)
2262 .map(|scale| -scale)
2263 .map_err(|_| BinanceSpotHttpError::ResponseParseError("decimal scale exceeds i8".into()))
2264}
2265
2266fn millis_to_micros(timestamp: i64) -> BinanceSpotHttpResult<i64> {
2267 timestamp.checked_mul(1_000).ok_or_else(|| {
2268 BinanceSpotHttpError::ResponseParseError(format!(
2269 "timestamp overflows microseconds: {timestamp}"
2270 ))
2271 })
2272}
2273
2274fn valid_order_list_id(order_list_id: Option<i64>) -> Option<i64> {
2275 order_list_id.filter(|value| *value >= 0)
2276}
2277
2278const fn spot_sbe_order_status(status: BinanceOrderStatus) -> SbeOrderStatus {
2279 match status {
2280 BinanceOrderStatus::New => SbeOrderStatus::New,
2281 BinanceOrderStatus::PendingNew => SbeOrderStatus::PendingNew,
2282 BinanceOrderStatus::PartiallyFilled => SbeOrderStatus::PartiallyFilled,
2283 BinanceOrderStatus::Filled => SbeOrderStatus::Filled,
2284 BinanceOrderStatus::Canceled => SbeOrderStatus::Canceled,
2285 BinanceOrderStatus::PendingCancel => SbeOrderStatus::PendingCancel,
2286 BinanceOrderStatus::Rejected => SbeOrderStatus::Rejected,
2287 BinanceOrderStatus::Expired => SbeOrderStatus::Expired,
2288 BinanceOrderStatus::ExpiredInMatch => SbeOrderStatus::ExpiredInMatch,
2289 BinanceOrderStatus::NewInsurance
2290 | BinanceOrderStatus::NewAdl
2291 | BinanceOrderStatus::Unknown => SbeOrderStatus::Unknown,
2292 }
2293}
2294
2295const fn spot_sbe_time_in_force(time_in_force: BinanceTimeInForce) -> SbeTimeInForce {
2296 match time_in_force {
2297 BinanceTimeInForce::Gtc => SbeTimeInForce::Gtc,
2298 BinanceTimeInForce::Ioc => SbeTimeInForce::Ioc,
2299 BinanceTimeInForce::Fok => SbeTimeInForce::Fok,
2300 BinanceTimeInForce::Gtx
2301 | BinanceTimeInForce::Gtd
2302 | BinanceTimeInForce::Rpi
2303 | BinanceTimeInForce::Unknown => SbeTimeInForce::NonRepresentable,
2304 }
2305}
2306
2307const fn spot_sbe_order_type(order_type: BinanceSpotOrderType) -> SbeOrderType {
2308 match order_type {
2309 BinanceSpotOrderType::Market => SbeOrderType::Market,
2310 BinanceSpotOrderType::Limit => SbeOrderType::Limit,
2311 BinanceSpotOrderType::StopLoss => SbeOrderType::StopLoss,
2312 BinanceSpotOrderType::StopLossLimit => SbeOrderType::StopLossLimit,
2313 BinanceSpotOrderType::TakeProfit => SbeOrderType::TakeProfit,
2314 BinanceSpotOrderType::TakeProfitLimit => SbeOrderType::TakeProfitLimit,
2315 BinanceSpotOrderType::LimitMaker => SbeOrderType::LimitMaker,
2316 BinanceSpotOrderType::Unknown => SbeOrderType::NonRepresentable,
2317 }
2318}
2319
2320const fn spot_sbe_order_side(side: BinanceSide) -> SbeOrderSide {
2321 match side {
2322 BinanceSide::Buy => SbeOrderSide::Buy,
2323 BinanceSide::Sell => SbeOrderSide::Sell,
2324 }
2325}
2326
2327fn spot_sbe_stp(mode: Option<BinanceSelfTradePreventionMode>) -> SbeSelfTradePreventionMode {
2328 match mode.unwrap_or(BinanceSelfTradePreventionMode::None) {
2329 BinanceSelfTradePreventionMode::None => SbeSelfTradePreventionMode::None,
2330 BinanceSelfTradePreventionMode::ExpireMaker => SbeSelfTradePreventionMode::ExpireMaker,
2331 BinanceSelfTradePreventionMode::ExpireTaker => SbeSelfTradePreventionMode::ExpireTaker,
2332 BinanceSelfTradePreventionMode::ExpireBoth => SbeSelfTradePreventionMode::ExpireBoth,
2333 BinanceSelfTradePreventionMode::Decrement => SbeSelfTradePreventionMode::Decrement,
2334 BinanceSelfTradePreventionMode::Transfer => SbeSelfTradePreventionMode::Transfer,
2335 BinanceSelfTradePreventionMode::Unknown => SbeSelfTradePreventionMode::NonRepresentable,
2336 }
2337}
2338
2339pub struct BinanceSpotHttpClient {
2345 inner: Arc<BinanceRawSpotHttpClient>,
2346 clock: &'static AtomicTime,
2347 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
2348}
2349
2350impl Clone for BinanceSpotHttpClient {
2351 fn clone(&self) -> Self {
2352 Self {
2353 inner: self.inner.clone(),
2354 clock: self.clock,
2355 instruments_cache: self.instruments_cache.clone(),
2356 }
2357 }
2358}
2359
2360impl Debug for BinanceSpotHttpClient {
2361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2362 f.debug_struct(stringify!(BinanceSpotHttpClient))
2363 .field("inner", &self.inner)
2364 .field("instruments_cached", &self.instruments_cache.len())
2365 .finish()
2366 }
2367}
2368
2369impl BinanceSpotHttpClient {
2370 #[expect(clippy::too_many_arguments)]
2376 pub fn new(
2377 environment: BinanceEnvironment,
2378 clock: &'static AtomicTime,
2379 api_key: Option<String>,
2380 api_secret: Option<String>,
2381 base_url_override: Option<String>,
2382 recv_window: Option<u64>,
2383 timeout_secs: Option<u64>,
2384 proxy_url: Option<String>,
2385 ) -> BinanceSpotHttpResult<Self> {
2386 Self::new_with_json_responses(
2387 environment,
2388 clock,
2389 api_key,
2390 api_secret,
2391 base_url_override,
2392 recv_window,
2393 timeout_secs,
2394 proxy_url,
2395 false,
2396 )
2397 }
2398
2399 #[expect(clippy::too_many_arguments)]
2405 pub fn new_with_json_responses(
2406 environment: BinanceEnvironment,
2407 clock: &'static AtomicTime,
2408 api_key: Option<String>,
2409 api_secret: Option<String>,
2410 base_url_override: Option<String>,
2411 recv_window: Option<u64>,
2412 timeout_secs: Option<u64>,
2413 proxy_url: Option<String>,
2414 json_responses: bool,
2415 ) -> BinanceSpotHttpResult<Self> {
2416 let inner = BinanceRawSpotHttpClient::new_with_json_responses(
2417 environment,
2418 api_key,
2419 api_secret,
2420 base_url_override,
2421 recv_window,
2422 timeout_secs,
2423 proxy_url,
2424 json_responses,
2425 )?;
2426
2427 Ok(Self {
2428 inner: Arc::new(inner),
2429 clock,
2430 instruments_cache: Arc::new(AtomicMap::new()),
2431 })
2432 }
2433
2434 #[must_use]
2436 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
2437 &self.inner
2438 }
2439
2440 #[must_use]
2442 pub fn has_credentials(&self) -> bool {
2443 self.inner.has_credentials()
2444 }
2445
2446 #[must_use]
2448 pub const fn schema_id() -> u16 {
2449 SBE_SCHEMA_ID
2450 }
2451
2452 #[must_use]
2454 pub const fn schema_version() -> u16 {
2455 SBE_SCHEMA_VERSION
2456 }
2457
2458 fn generate_ts_init(&self) -> UnixNanos {
2460 self.clock.get_time_ns()
2461 }
2462
2463 fn command_validation_error(message: impl Into<String>) -> anyhow::Error {
2464 anyhow::anyhow!(BinanceSpotHttpError::ValidationError(message.into()))
2465 }
2466
2467 fn response_parse_error(message: impl Into<String>) -> anyhow::Error {
2468 anyhow::anyhow!(BinanceSpotHttpError::ResponseParseError(message.into()))
2469 }
2470
2471 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
2473 self.instruments_cache
2474 .get_cloned(&symbol)
2475 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
2476 }
2477
2478 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
2480 self.instruments_cache.rcu(move |cache| {
2481 for instrument in &instruments {
2482 cache.insert(instrument.raw_symbol().inner(), instrument.clone());
2483 }
2484 });
2485 }
2486
2487 pub fn replace_instruments(&self, instruments: &[InstrumentAny]) {
2489 let cache = instruments
2490 .iter()
2491 .map(|instrument| (instrument.raw_symbol().inner(), instrument.clone()))
2492 .collect();
2493 self.instruments_cache.store(cache);
2494 }
2495
2496 pub fn cache_instrument(&self, instrument: InstrumentAny) {
2498 self.instruments_cache
2499 .insert(instrument.raw_symbol().inner(), instrument);
2500 }
2501
2502 #[must_use]
2504 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
2505 self.instruments_cache.get_cloned(symbol)
2506 }
2507
2508 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
2514 self.inner.ping().await
2515 }
2516
2517 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
2525 self.inner.server_time().await
2526 }
2527
2528 pub async fn exchange_info(
2534 &self,
2535 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
2536 self.inner.exchange_info().await
2537 }
2538
2539 pub async fn request_symbol_statuses(
2545 &self,
2546 us: bool,
2547 ) -> BinanceSpotHttpResult<AHashMap<InstrumentId, MarketStatusAction>> {
2548 let mut statuses = AHashMap::new();
2549
2550 if us {
2551 let info = self.inner.exchange_info_json().await?;
2552 for symbol in info.symbols {
2553 let instrument_id =
2554 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2555 statuses.insert(instrument_id, spot_json_market_status(&symbol.status));
2556 }
2557 } else {
2558 let info = self.exchange_info().await?;
2559 for symbol in info.symbols {
2560 let instrument_id =
2561 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2562 statuses.insert(
2563 instrument_id,
2564 MarketStatusAction::from(SymbolStatus::from(symbol.status)),
2565 );
2566 }
2567 }
2568 Ok(statuses)
2569 }
2570
2571 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2580 self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default(), false)
2581 .await
2582 }
2583
2584 pub async fn request_instruments_with_config(
2590 &self,
2591 config: &BinanceInstrumentProviderConfig,
2592 us: bool,
2593 ) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2594 config
2595 .validate(BinanceProductType::Spot)
2596 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2597 let selector = BinanceInstrumentSelector::new(config)
2598 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2599 let ts_init = self.generate_ts_init();
2600 let fallback_fees = self.spot_fallback_fees(us).await;
2601
2602 let mut instruments = if us {
2603 if config.query_commission_rates {
2604 if config.log_warnings {
2605 log::warn!(
2606 "Binance US does not expose the Global account/commission endpoint; using account-wide commission rates"
2607 );
2608 } else {
2609 log::debug!(
2610 "Binance US exact per-symbol commission query disabled; using account-wide rates"
2611 );
2612 }
2613 }
2614 let info = self.inner.exchange_info_json().await?;
2615 let mut instruments = Vec::with_capacity(info.symbols.len());
2616 for symbol in &info.symbols {
2617 let instrument_id =
2618 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2619
2620 if !selector.includes(
2621 instrument_id,
2622 &symbol.symbol,
2623 &symbol.base_asset,
2624 &symbol.quote_asset,
2625 None,
2626 ) {
2627 continue;
2628 }
2629
2630 match parse_spot_instrument_json_with_fees(
2631 symbol,
2632 Some(fallback_fees.0),
2633 Some(fallback_fees.1),
2634 ts_init,
2635 ts_init,
2636 ) {
2637 Ok(instrument) => instruments.push(instrument),
2638 Err(e) => log_instrument_parse_error(config, &symbol.symbol, &e),
2639 }
2640 }
2641 instruments
2642 } else {
2643 let info = self.exchange_info().await?;
2644 let mut instruments = Vec::with_capacity(info.symbols.len());
2645 for symbol in &info.symbols {
2646 let instrument_id =
2647 InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2648
2649 if !selector.includes(
2650 instrument_id,
2651 &symbol.symbol,
2652 &symbol.base_asset,
2653 &symbol.quote_asset,
2654 None,
2655 ) {
2656 continue;
2657 }
2658
2659 let fees = self
2660 .spot_symbol_fees(config, &symbol.symbol, fallback_fees)
2661 .await;
2662
2663 match parse_spot_instrument_sbe_with_fees(
2664 symbol,
2665 Some(fees.0),
2666 Some(fees.1),
2667 ts_init,
2668 ts_init,
2669 ) {
2670 Ok(instrument) => instruments.push(instrument),
2671 Err(e) => log_instrument_parse_error(config, &symbol.symbol, &e),
2672 }
2673 }
2674 instruments
2675 };
2676
2677 instruments.shrink_to_fit();
2678 self.replace_instruments(&instruments);
2679
2680 log::debug!("Loaded spot instruments: count={}", instruments.len());
2681 Ok(instruments)
2682 }
2683
2684 async fn spot_fallback_fees(&self, us: bool) -> (Decimal, Decimal) {
2685 if !self.has_credentials() {
2686 return (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT);
2687 }
2688
2689 let result = if us {
2690 self.inner.account_rates_json().await.map(|account| {
2691 parse_commission_rates(
2692 &account.commission_rates.maker,
2693 &account.commission_rates.taker,
2694 )
2695 })
2696 } else {
2697 self.inner
2698 .account(&AccountInfoParams::default())
2699 .await
2700 .map(|account| {
2701 Ok((
2702 decimal_from_mantissa_exponent(
2703 account.maker_commission_mantissa,
2704 account.commission_exponent,
2705 ),
2706 decimal_from_mantissa_exponent(
2707 account.taker_commission_mantissa,
2708 account.commission_exponent,
2709 ),
2710 ))
2711 })
2712 };
2713
2714 match result {
2715 Ok(Ok(fees)) => fees,
2716 Ok(Err(e)) => {
2717 log::warn!("Invalid Binance Spot account commission rates: {e}; using fallback");
2718 (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2719 }
2720 Err(e) => {
2721 log::warn!("Binance Spot account commission query failed: {e}; using fallback");
2722 (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2723 }
2724 }
2725 }
2726
2727 async fn spot_symbol_fees(
2728 &self,
2729 config: &BinanceInstrumentProviderConfig,
2730 symbol: &str,
2731 fallback: (Decimal, Decimal),
2732 ) -> (Decimal, Decimal) {
2733 if !config.query_commission_rates || !self.has_credentials() {
2734 return fallback;
2735 }
2736
2737 match self.inner.account_commission(symbol).await {
2738 Ok(response) => match parse_commission_rates(
2739 &response.standard_commission.maker,
2740 &response.standard_commission.taker,
2741 ) {
2742 Ok(fees) => fees,
2743 Err(e) => {
2744 log::warn!(
2745 "Invalid Binance Spot commission response for {symbol}: {e}; using fallback"
2746 );
2747 fallback
2748 }
2749 },
2750 Err(e) => {
2751 log::warn!(
2752 "Binance Spot commission query failed for {symbol}: {e}; using fallback"
2753 );
2754 fallback
2755 }
2756 }
2757 }
2758
2759 pub async fn request_trades(
2766 &self,
2767 instrument_id: InstrumentId,
2768 limit: Option<u32>,
2769 ) -> anyhow::Result<Vec<TradeTick>> {
2770 let symbol = instrument_id.symbol.inner();
2771 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2772 let ts_init = self.generate_ts_init();
2773
2774 let trades = self
2775 .inner
2776 .trades(symbol.as_str(), limit)
2777 .await
2778 .map_err(|e| anyhow::anyhow!(e))?;
2779
2780 parse_spot_trades_sbe(&trades, &instrument, ts_init)
2781 }
2782
2783 pub async fn request_agg_trades(
2789 &self,
2790 instrument_id: InstrumentId,
2791 start: Option<Timestamp>,
2792 end: Option<Timestamp>,
2793 limit: Option<u32>,
2794 ) -> anyhow::Result<Vec<TradeTick>> {
2795 let symbol = instrument_id.symbol.inner();
2796 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2797 let params = AggTradesParams {
2798 symbol: symbol.to_string(),
2799 from_id: None,
2800 start_time: start.map(|dt| dt.as_millisecond()),
2801 end_time: end.map(|dt| dt.as_millisecond()),
2802 limit,
2803 };
2804 let response = self.inner.agg_trades(¶ms).await?;
2805 let trades = BinanceTrades {
2806 price_exponent: response.price_exponent,
2807 qty_exponent: response.qty_exponent,
2808 trades: response
2809 .trades
2810 .into_iter()
2811 .map(|trade| super::models::BinanceTrade {
2812 id: trade.id,
2813 price_mantissa: trade.price_mantissa,
2814 qty_mantissa: trade.qty_mantissa,
2815 quote_qty_mantissa: 0,
2816 time: trade.time,
2817 is_buyer_maker: trade.is_buyer_maker,
2818 is_best_match: trade.is_best_match,
2819 })
2820 .collect(),
2821 };
2822
2823 let mut parsed = parse_spot_trades_sbe(&trades, &instrument, UnixNanos::default())?;
2824 for trade in &mut parsed {
2825 trade.ts_init = trade.ts_event;
2826 }
2827 Ok(parsed)
2828 }
2829
2830 pub async fn request_binance_bars(
2837 &self,
2838 bar_type: BarType,
2839 start: Option<Timestamp>,
2840 end: Option<Timestamp>,
2841 limit: Option<u32>,
2842 ) -> anyhow::Result<Vec<crate::common::bar::BinanceBar>> {
2843 anyhow::ensure!(
2844 bar_type.aggregation_source() == AggregationSource::External,
2845 "Only EXTERNAL aggregation is supported"
2846 );
2847
2848 let spec = bar_type.spec();
2849 let step = spec.step.get();
2850 let interval = match spec.aggregation {
2851 BarAggregation::Second if step == 1 => "1s".to_string(),
2852 BarAggregation::Second => {
2853 anyhow::bail!("Binance Spot supports only the 1s kline interval")
2854 }
2855 BarAggregation::Minute => format!("{step}m"),
2856 BarAggregation::Hour => format!("{step}h"),
2857 BarAggregation::Day => format!("{step}d"),
2858 BarAggregation::Week => format!("{step}w"),
2859 BarAggregation::Month => format!("{step}M"),
2860 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
2861 };
2862
2863 let instrument_id = bar_type.instrument_id();
2864 let symbol = instrument_id.symbol;
2865 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2866 let klines = self
2867 .inner
2868 .klines(
2869 symbol.as_str(),
2870 &interval,
2871 start.map(|dt| dt.as_millisecond()),
2872 end.map(|dt| dt.as_millisecond()),
2873 limit,
2874 )
2875 .await
2876 .map_err(|e| anyhow::anyhow!(e))?;
2877
2878 let mut bars =
2879 parse_klines_to_binance_bars(&klines, bar_type, &instrument, UnixNanos::default())?;
2880 let now = self.clock.get_time_ns();
2881 bars.retain(|bar| bar.ts_event < now);
2882 for bar in &mut bars {
2883 bar.ts_init = bar.ts_event;
2884 }
2885 Ok(bars)
2886 }
2887
2888 pub async fn request_bars(
2894 &self,
2895 bar_type: BarType,
2896 start: Option<Timestamp>,
2897 end: Option<Timestamp>,
2898 limit: Option<u32>,
2899 ) -> anyhow::Result<Vec<Bar>> {
2900 Ok(self
2901 .request_binance_bars(bar_type, start, end, limit)
2902 .await?
2903 .into_iter()
2904 .map(|bar| bar.bar())
2905 .collect())
2906 }
2907
2908 pub async fn request_book_snapshot(
2914 &self,
2915 instrument_id: InstrumentId,
2916 depth: Option<u32>,
2917 ) -> anyhow::Result<OrderBook> {
2918 if depth.is_some_and(|value| value == 0 || value > 5000) {
2919 anyhow::bail!("Binance Spot order-book depth must be between 1 and 5000");
2920 }
2921 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2922 let params = DepthParams {
2923 symbol: instrument_id.symbol.to_string(),
2924 limit: depth,
2925 };
2926 let snapshot = self.inner.depth(¶ms).await?;
2927 let ts_event = self.generate_ts_init();
2928 Self::parse_book_snapshot_response(instrument_id, &instrument, &snapshot, ts_event)
2929 }
2930
2931 fn parse_book_snapshot_response(
2932 instrument_id: InstrumentId,
2933 instrument: &InstrumentAny,
2934 snapshot: &BinanceDepth,
2935 ts_event: UnixNanos,
2936 ) -> anyhow::Result<OrderBook> {
2937 let sequence = u64::try_from(snapshot.last_update_id)
2938 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
2939 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
2940 let mut add_level = |level: &super::models::BinancePriceLevel,
2941 side: OrderSide,
2942 order_id: usize,
2943 name: &str|
2944 -> anyhow::Result<()> {
2945 let price = Price::from_mantissa_exponent_checked(
2946 level.price_mantissa,
2947 snapshot.price_exponent,
2948 instrument.price_precision(),
2949 )
2950 .map_err(|e| anyhow::anyhow!("invalid {name} price: {e}"))?;
2951 anyhow::ensure!(price.is_positive(), "invalid non-positive {name} price");
2952 let qty_mantissa = u64::try_from(level.qty_mantissa)
2953 .map_err(|_| anyhow::anyhow!("invalid negative {name} quantity"))?;
2954 let quantity = Quantity::from_mantissa_exponent_checked(
2955 qty_mantissa,
2956 snapshot.qty_exponent,
2957 instrument.size_precision(),
2958 )
2959 .map_err(|e| anyhow::anyhow!("invalid {name} quantity: {e}"))?;
2960 anyhow::ensure!(
2961 quantity.is_positive(),
2962 "invalid non-positive {name} quantity"
2963 );
2964 let order = BookOrder::new(
2965 side,
2966 price,
2967 quantity,
2968 u64::try_from(order_id)
2969 .map_err(|_| anyhow::anyhow!("order-book level index overflow"))?,
2970 );
2971 book.add(order, 0, sequence, ts_event);
2972 Ok(())
2973 };
2974
2975 for (index, level) in snapshot.bids.iter().enumerate() {
2976 add_level(level, OrderSide::Buy, index, "bid")?;
2977 }
2978 let bid_count = snapshot.bids.len();
2979 for (index, level) in snapshot.asks.iter().enumerate() {
2980 let order_id = bid_count
2981 .checked_add(index)
2982 .ok_or_else(|| anyhow::anyhow!("order-book level index overflow"))?;
2983 add_level(level, OrderSide::Sell, order_id, "ask")?;
2984 }
2985 Ok(book)
2986 }
2987
2988 fn instrument_from_cache_by_id(
2989 &self,
2990 instrument_id: InstrumentId,
2991 ) -> anyhow::Result<InstrumentAny> {
2992 self.instruments_cache
2993 .get_cloned(&instrument_id.symbol.inner())
2994 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
2995 }
2996
2997 pub async fn request_account_state(
3003 &self,
3004 account_id: AccountId,
3005 ) -> anyhow::Result<AccountState> {
3006 let ts_init = self.clock.get_time_ns();
3007 let params = AccountInfoParams::default();
3008 let account_info = self.inner.account(¶ms).await?;
3009 Ok(account_info.to_account_state(account_id, ts_init))
3010 }
3011
3012 pub async fn request_order_status_report(
3021 &self,
3022 account_id: AccountId,
3023 instrument_id: InstrumentId,
3024 venue_order_id: Option<VenueOrderId>,
3025 client_order_id: Option<ClientOrderId>,
3026 ) -> anyhow::Result<Option<OrderStatusReport>> {
3027 anyhow::ensure!(
3028 venue_order_id.is_some() || client_order_id.is_some(),
3029 "Either venue_order_id or client_order_id must be provided"
3030 );
3031
3032 let symbol = instrument_id.symbol.inner();
3033 let instrument = self.instrument_from_cache(symbol)?;
3034 let ts_init = self.generate_ts_init();
3035
3036 let order_id = venue_order_id
3037 .map(|id| id.inner().parse::<i64>())
3038 .transpose()
3039 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3040
3041 let client_id_str =
3042 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3043
3044 let order = match self
3045 .inner
3046 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
3047 .await
3048 {
3049 Ok(order) => order,
3050 Err(e) if Self::is_no_such_order_error(&e) => {
3051 log::debug!("Binance Spot order not found: instrument_id={instrument_id}");
3052 return Ok(None);
3053 }
3054 Err(e) => anyhow::bail!(e),
3055 };
3056
3057 parse_order_status_report_sbe(
3058 &order,
3059 account_id,
3060 &instrument,
3061 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3062 ts_init,
3063 )
3064 .map(Some)
3065 }
3066
3067 const fn is_no_such_order_error(error: &BinanceSpotHttpError) -> bool {
3068 matches!(
3069 error,
3070 BinanceSpotHttpError::BinanceError { code, .. } if *code == BINANCE_NO_SUCH_ORDER_CODE
3071 )
3072 }
3073
3074 pub async fn request_order_status_reports(
3084 &self,
3085 account_id: AccountId,
3086 instrument_id: Option<InstrumentId>,
3087 start: Option<Timestamp>,
3088 end: Option<Timestamp>,
3089 open_only: bool,
3090 limit: Option<u32>,
3091 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3092 let ts_init = self.generate_ts_init();
3093 let symbol = instrument_id.map(|id| id.symbol.to_string());
3094
3095 let orders = if open_only {
3096 self.inner
3097 .open_orders(symbol.as_deref())
3098 .await
3099 .map_err(|e| anyhow::anyhow!(e))?
3100 } else {
3101 let symbol = symbol
3102 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
3103 self.inner
3104 .all_orders(
3105 &symbol,
3106 start.map(|dt| dt.as_millisecond()),
3107 end.map(|dt| dt.as_millisecond()),
3108 limit,
3109 )
3110 .await
3111 .map_err(|e| anyhow::anyhow!(e))?
3112 };
3113
3114 orders
3115 .iter()
3116 .map(|order| {
3117 let symbol = Ustr::from(&order.symbol);
3118 let instrument = self.instrument_from_cache(symbol)?;
3119 parse_order_status_report_sbe(
3120 order,
3121 account_id,
3122 &instrument,
3123 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3124 ts_init,
3125 )
3126 })
3127 .collect()
3128 }
3129
3130 pub async fn request_fill_reports(
3137 &self,
3138 account_id: AccountId,
3139 instrument_id: InstrumentId,
3140 venue_order_id: Option<VenueOrderId>,
3141 start: Option<Timestamp>,
3142 end: Option<Timestamp>,
3143 limit: Option<u32>,
3144 ) -> anyhow::Result<Vec<FillReport>> {
3145 self.request_fill_reports_with_cursor(
3146 account_id,
3147 instrument_id,
3148 venue_order_id,
3149 start,
3150 end,
3151 None,
3152 limit,
3153 )
3154 .await
3155 }
3156
3157 #[expect(clippy::too_many_arguments)]
3158 pub(crate) async fn request_fill_reports_with_cursor(
3159 &self,
3160 account_id: AccountId,
3161 instrument_id: InstrumentId,
3162 venue_order_id: Option<VenueOrderId>,
3163 start: Option<Timestamp>,
3164 end: Option<Timestamp>,
3165 from_id: Option<i64>,
3166 limit: Option<u32>,
3167 ) -> anyhow::Result<Vec<FillReport>> {
3168 let ts_init = self.generate_ts_init();
3169 let symbol = instrument_id.symbol.inner();
3170
3171 let order_id = venue_order_id
3172 .map(|id| id.inner().parse::<i64>())
3173 .transpose()
3174 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3175
3176 let trades = self
3177 .inner
3178 .account_trades_with_cursor(
3179 symbol.as_str(),
3180 order_id,
3181 start.map(|dt| dt.as_millisecond()),
3182 end.map(|dt| dt.as_millisecond()),
3183 from_id,
3184 limit,
3185 )
3186 .await
3187 .map_err(|e| anyhow::anyhow!(e))?;
3188
3189 trades
3190 .iter()
3191 .map(|trade| {
3192 let symbol = Ustr::from(&trade.symbol);
3193 let instrument = self.instrument_from_cache(symbol)?;
3194 let commission_currency = get_currency(&trade.commission_asset);
3195 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
3196 })
3197 .collect()
3198 }
3199
3200 #[expect(clippy::too_many_arguments)]
3213 pub async fn submit_order(
3214 &self,
3215 account_id: AccountId,
3216 instrument_id: InstrumentId,
3217 client_order_id: ClientOrderId,
3218 order_side: OrderSide,
3219 order_type: OrderType,
3220 quantity: Quantity,
3221 time_in_force: TimeInForce,
3222 price: Option<Price>,
3223 trigger_price: Option<Price>,
3224 post_only: bool,
3225 quote_quantity: bool,
3226 display_qty: Option<Quantity>,
3227 use_gtd: bool,
3228 ) -> anyhow::Result<OrderStatusReport> {
3229 let symbol = instrument_id.symbol.inner();
3230 let instrument = self
3231 .instrument_from_cache(symbol)
3232 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3233 let ts_init = self.generate_ts_init();
3234
3235 let binance_side = BinanceSide::try_from(order_side)
3236 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3237 let binance_order_type = order_type_to_binance_spot(order_type, post_only)
3238 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3239
3240 let requires_trigger = matches!(
3242 order_type,
3243 OrderType::StopMarket
3244 | OrderType::StopLimit
3245 | OrderType::MarketIfTouched
3246 | OrderType::LimitIfTouched
3247 );
3248
3249 if requires_trigger && trigger_price.is_none() {
3250 return Err(Self::command_validation_error(
3251 "Conditional orders require a trigger price",
3252 ));
3253 }
3254
3255 let requires_price = matches!(
3257 binance_order_type,
3258 BinanceSpotOrderType::Limit
3259 | BinanceSpotOrderType::StopLossLimit
3260 | BinanceSpotOrderType::TakeProfitLimit
3261 | BinanceSpotOrderType::LimitMaker
3262 );
3263
3264 if requires_price && price.is_none() {
3265 return Err(Self::command_validation_error(format!(
3266 "{binance_order_type:?} orders require a price"
3267 )));
3268 }
3269
3270 let supports_tif = matches!(
3272 binance_order_type,
3273 BinanceSpotOrderType::Limit
3274 | BinanceSpotOrderType::StopLossLimit
3275 | BinanceSpotOrderType::TakeProfitLimit
3276 );
3277 let binance_tif = if supports_tif {
3278 Some(
3279 time_in_force_to_binance_spot(time_in_force, use_gtd)
3280 .map_err(|e| Self::command_validation_error(e.to_string()))?,
3281 )
3282 } else {
3283 None
3284 };
3285
3286 let qty_str = quantity.to_string();
3287 let price_str = price.map(|p| p.to_string());
3288 let stop_price_str = trigger_price.map(|p| p.to_string());
3289 let iceberg_qty_str = display_qty.map(|q| q.to_string());
3290 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3291
3292 if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
3293 return Err(Self::command_validation_error(
3294 "quoteOrderQty is only supported for MARKET orders",
3295 ));
3296 }
3297
3298 let (base_qty, quote_qty) = if quote_quantity {
3299 (None, Some(qty_str.as_str()))
3300 } else {
3301 (Some(qty_str.as_str()), None)
3302 };
3303
3304 let response = self
3305 .inner
3306 .new_order_full(
3307 symbol.as_str(),
3308 binance_side,
3309 binance_order_type,
3310 binance_tif,
3311 base_qty,
3312 quote_qty,
3313 price_str.as_deref(),
3314 Some(&client_id_str),
3315 stop_price_str.as_deref(),
3316 iceberg_qty_str.as_deref(),
3317 )
3318 .await?;
3319
3320 parse_new_order_response_sbe(
3321 &response,
3322 account_id,
3323 &instrument,
3324 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3325 ts_init,
3326 )
3327 .map_err(|e| Self::response_parse_error(e.to_string()))
3328 }
3329
3330 pub async fn submit_order_list(
3338 &self,
3339 orders: &[BatchOrderItem],
3340 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
3341 self.inner.batch_submit_orders(orders).await
3342 }
3343
3344 pub async fn submit_oco_order_list(
3350 &self,
3351 params: &NewOcoOrderListParams,
3352 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
3353 self.inner.new_oco_order_list(params).await
3354 }
3355
3356 #[expect(clippy::too_many_arguments)]
3365 pub async fn modify_order(
3366 &self,
3367 account_id: AccountId,
3368 instrument_id: InstrumentId,
3369 venue_order_id: VenueOrderId,
3370 client_order_id: ClientOrderId,
3371 order_side: OrderSide,
3372 order_type: OrderType,
3373 quantity: Quantity,
3374 time_in_force: TimeInForce,
3375 price: Option<Price>,
3376 use_gtd: bool,
3377 ) -> anyhow::Result<OrderStatusReport> {
3378 let symbol = instrument_id.symbol.inner();
3379 let instrument = self
3380 .instrument_from_cache(symbol)
3381 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3382 let ts_init = self.generate_ts_init();
3383
3384 let binance_side = BinanceSide::try_from(order_side)
3385 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3386 let binance_order_type = order_type_to_binance_spot(order_type, false)
3387 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3388 let binance_tif = time_in_force_to_binance_spot(time_in_force, use_gtd)
3389 .map_err(|e| Self::command_validation_error(e.to_string()))?;
3390
3391 let cancel_order_id: i64 = venue_order_id.inner().parse().map_err(|_| {
3392 Self::command_validation_error(format!("Invalid venue order ID: {venue_order_id}"))
3393 })?;
3394
3395 let qty_str = quantity.to_string();
3396 let price_str = price.map(|p| p.to_string());
3397 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3398
3399 let response = self
3400 .inner
3401 .cancel_replace_order(
3402 symbol.as_str(),
3403 binance_side,
3404 binance_order_type,
3405 Some(binance_tif),
3406 Some(&qty_str),
3407 price_str.as_deref(),
3408 Some(cancel_order_id),
3409 None,
3410 Some(&client_id_str),
3411 )
3412 .await
3413 .map_err(|e| anyhow::anyhow!(e))?;
3414
3415 parse_new_order_response_sbe(
3416 &response,
3417 account_id,
3418 &instrument,
3419 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3420 ts_init,
3421 )
3422 .map_err(|e| Self::response_parse_error(e.to_string()))
3423 }
3424
3425 pub async fn cancel_order(
3433 &self,
3434 instrument_id: InstrumentId,
3435 venue_order_id: Option<VenueOrderId>,
3436 client_order_id: Option<ClientOrderId>,
3437 ) -> anyhow::Result<VenueOrderId> {
3438 let symbol = instrument_id.symbol.inner();
3439
3440 let order_id = match venue_order_id {
3441 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
3442 Ok(order_id) => Some(order_id),
3443 Err(e) if client_order_id.is_some() => {
3444 log::warn!(
3445 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
3446 );
3447 None
3448 }
3449 Err(e) => {
3450 return Err(Self::command_validation_error(format!(
3451 "Invalid venue order ID: {e}"
3452 )));
3453 }
3454 },
3455 None => None,
3456 };
3457
3458 let client_id_str =
3459 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3460
3461 let response = self
3462 .inner
3463 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
3464 .await
3465 .map_err(|e| anyhow::anyhow!(e))?;
3466
3467 Ok(VenueOrderId::new(response.order_id.to_string()))
3468 }
3469
3470 pub async fn batch_cancel_orders(
3478 &self,
3479 cancels: &[BatchCancelItem],
3480 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
3481 self.inner.batch_cancel_orders(cancels).await
3482 }
3483
3484 pub async fn cancel_all_orders(
3492 &self,
3493 instrument_id: InstrumentId,
3494 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
3495 let symbol = instrument_id.symbol.inner();
3496
3497 let responses = self
3498 .inner
3499 .cancel_open_orders(symbol.as_str())
3500 .await
3501 .map_err(|e| anyhow::anyhow!(e))?;
3502
3503 responses
3504 .into_iter()
3505 .map(|r| {
3506 Ok((
3507 VenueOrderId::new(r.order_id.to_string()),
3508 decode_client_order_id(
3509 &r.orig_client_order_id,
3510 BINANCE_NAUTILUS_SPOT_BROKER_ID,
3511 )?,
3512 ))
3513 })
3514 .collect()
3515 }
3516}
3517
3518fn parse_commission_rates(maker: &str, taker: &str) -> anyhow::Result<(Decimal, Decimal)> {
3519 Ok((
3520 Decimal::from_str_exact(maker)?,
3521 Decimal::from_str_exact(taker)?,
3522 ))
3523}
3524
3525fn spot_json_market_status(status: &str) -> MarketStatusAction {
3526 match status {
3527 "TRADING" => MarketStatusAction::Trading,
3528 "BREAK" => MarketStatusAction::Pause,
3529 _ => MarketStatusAction::NotAvailableForTrading,
3530 }
3531}
3532
3533fn decimal_from_mantissa_exponent(mantissa: i64, exponent: i8) -> Decimal {
3534 if exponent >= 0 {
3535 Decimal::from(mantissa) * Decimal::from(10_i64.pow(exponent as u32))
3536 } else {
3537 Decimal::new(mantissa, (-exponent) as u32)
3538 }
3539}
3540
3541fn log_instrument_parse_error(
3542 config: &BinanceInstrumentProviderConfig,
3543 symbol: &str,
3544 error: &anyhow::Error,
3545) {
3546 if config.log_warnings {
3547 log::warn!("Skipping Binance Spot instrument {symbol}: {error}");
3548 } else {
3549 log::debug!("Skipping Binance Spot instrument {symbol}: {error}");
3550 }
3551}
3552
3553#[cfg(test)]
3554mod tests {
3555 use nautilus_model::instruments::stubs::currency_pair_btcusdt;
3556 use rstest::rstest;
3557
3558 use super::*;
3559 use crate::spot::http::models::BinancePriceLevel;
3560
3561 #[rstest]
3562 fn test_schema_constants() {
3563 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
3564 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 5);
3565 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
3566 assert_eq!(BinanceSpotHttpClient::schema_version(), 5);
3567 }
3568
3569 #[rstest]
3570 fn test_sbe_schema_header() {
3571 assert_eq!(SBE_SCHEMA_HEADER, "3:5");
3572 }
3573
3574 #[rstest]
3575 fn test_parse_book_snapshot_response_rejects_negative_update_id() {
3576 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3577 let snapshot = BinanceDepth {
3578 last_update_id: -1,
3579 price_exponent: -2,
3580 qty_exponent: -5,
3581 bids: vec![],
3582 asks: vec![],
3583 };
3584
3585 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3586 InstrumentId::from("BTCUSDT.BINANCE"),
3587 &instrument,
3588 &snapshot,
3589 UnixNanos::from(1_700_000_000_000_000_001u64),
3590 )
3591 .unwrap_err();
3592
3593 assert_eq!(error.to_string(), "invalid negative order-book update ID");
3594 }
3595
3596 #[rstest]
3597 fn test_parse_book_snapshot_response_rejects_negative_quantity() {
3598 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3599 let snapshot = BinanceDepth {
3600 last_update_id: 12345,
3601 price_exponent: -2,
3602 qty_exponent: -5,
3603 bids: vec![BinancePriceLevel {
3604 price_mantissa: 4_200_001,
3605 qty_mantissa: -1,
3606 }],
3607 asks: vec![],
3608 };
3609
3610 let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3611 InstrumentId::from("BTCUSDT.BINANCE"),
3612 &instrument,
3613 &snapshot,
3614 UnixNanos::from(1_700_000_000_000_000_001u64),
3615 )
3616 .unwrap_err();
3617
3618 assert_eq!(error.to_string(), "invalid negative bid quantity");
3619 }
3620
3621 #[rstest]
3622 fn test_default_headers_include_sbe() {
3623 let headers = BinanceRawSpotHttpClient::default_headers(&None, false);
3624
3625 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
3626 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:5".to_string()));
3627 }
3628
3629 #[rstest]
3630 fn test_json_headers_exclude_sbe() {
3631 let headers = BinanceRawSpotHttpClient::default_headers(&None, true);
3632
3633 assert_eq!(headers.get("Accept"), Some(&"application/json".to_string()));
3634 assert_eq!(headers.get("X-MBX-SBE"), None);
3635 }
3636
3637 #[rstest]
3638 fn test_spot_trades_from_json_preserves_all_fields() {
3639 let response: Vec<SpotTradeJson> = serde_json::from_value(serde_json::json!([{
3640 "id": 17,
3641 "price": "123.45",
3642 "qty": "0.06789",
3643 "quoteQty": "8.3810205",
3644 "time": 1_700_000_000_123_i64,
3645 "isBuyerMaker": true,
3646 "isBestMatch": false
3647 }]))
3648 .unwrap();
3649
3650 let parsed = spot_trades_from_json(response).unwrap();
3651
3652 assert_eq!(parsed.price_exponent, -7);
3653 assert_eq!(parsed.qty_exponent, -5);
3654 assert_eq!(parsed.trades.len(), 1);
3655 assert_eq!(parsed.trades[0].id, 17);
3656 assert_eq!(parsed.trades[0].price_mantissa, 1_234_500_000);
3657 assert_eq!(parsed.trades[0].qty_mantissa, 6_789);
3658 assert_eq!(parsed.trades[0].quote_qty_mantissa, 83_810_205);
3659 assert_eq!(parsed.trades[0].time, 1_700_000_000_123_000);
3660 assert!(parsed.trades[0].is_buyer_maker);
3661 assert!(!parsed.trades[0].is_best_match);
3662 }
3663
3664 #[rstest]
3665 fn test_spot_agg_trades_from_json_preserves_all_fields() {
3666 let response: Vec<SpotAggTradeJson> = serde_json::from_value(serde_json::json!([{
3667 "a": 21,
3668 "p": "432.10",
3669 "q": "1.234",
3670 "f": 31,
3671 "l": 32,
3672 "T": 1_700_000_000_456_i64,
3673 "m": false,
3674 "M": true
3675 }]))
3676 .unwrap();
3677
3678 let parsed = spot_agg_trades_from_json(response).unwrap();
3679
3680 assert_eq!(parsed.price_exponent, -2);
3681 assert_eq!(parsed.qty_exponent, -3);
3682 assert_eq!(parsed.trades.len(), 1);
3683 assert_eq!(parsed.trades[0].id, 21);
3684 assert_eq!(parsed.trades[0].price_mantissa, 43_210);
3685 assert_eq!(parsed.trades[0].qty_mantissa, 1_234);
3686 assert_eq!(parsed.trades[0].first_trade_id, 31);
3687 assert_eq!(parsed.trades[0].last_trade_id, 32);
3688 assert_eq!(parsed.trades[0].time, 1_700_000_000_456_000);
3689 assert!(!parsed.trades[0].is_buyer_maker);
3690 assert!(parsed.trades[0].is_best_match);
3691 }
3692
3693 #[rstest]
3694 fn test_spot_klines_from_json_preserves_all_fields() {
3695 let response: Vec<SpotKlineJson> = serde_json::from_value(serde_json::json!([[
3696 1_700_000_000_000_i64,
3697 "10.10",
3698 "11.20",
3699 "9.30",
3700 "10.40",
3701 "12.345",
3702 1_700_000_059_999_i64,
3703 "128.765",
3704 37,
3705 "5.432",
3706 "56.789",
3707 "0"
3708 ]]))
3709 .unwrap();
3710
3711 let parsed = spot_klines_from_json(response).unwrap();
3712 let kline = &parsed.klines[0];
3713
3714 assert_eq!(parsed.price_exponent, -3);
3715 assert_eq!(parsed.qty_exponent, -3);
3716 assert_eq!(parsed.klines.len(), 1);
3717 assert_eq!(kline.open_time, 1_700_000_000_000_000);
3718 assert_eq!(kline.open_price, 10_100);
3719 assert_eq!(kline.high_price, 11_200);
3720 assert_eq!(kline.low_price, 9_300);
3721 assert_eq!(kline.close_price, 10_400);
3722 assert_eq!(i128::from_le_bytes(kline.volume), 12_345);
3723 assert_eq!(kline.close_time, 1_700_000_059_999_000);
3724 assert_eq!(i128::from_le_bytes(kline.quote_volume), 128_765);
3725 assert_eq!(kline.num_trades, 37);
3726 assert_eq!(i128::from_le_bytes(kline.taker_buy_base_volume), 5_432);
3727 assert_eq!(i128::from_le_bytes(kline.taker_buy_quote_volume), 56_789);
3728 }
3729
3730 #[rstest]
3731 fn test_spot_account_from_json_preserves_all_fields() {
3732 let response: SpotAccountJson = serde_json::from_value(serde_json::json!({
3733 "commissionRates": {
3734 "maker": "0.0008",
3735 "taker": "0.0011",
3736 "buyer": "0.0002",
3737 "seller": "0.0003"
3738 },
3739 "canTrade": true,
3740 "canWithdraw": false,
3741 "canDeposit": true,
3742 "requireSelfTradePrevention": true,
3743 "preventSor": false,
3744 "updateTime": 1_700_000_000_789_i64,
3745 "accountType": "SPOT",
3746 "balances": [{"asset": "USD", "free": "123.45", "locked": "6.789"}]
3747 }))
3748 .unwrap();
3749
3750 let account = spot_account_from_json(response).unwrap();
3751
3752 assert_eq!(account.commission_exponent, -4);
3753 assert_eq!(account.maker_commission_mantissa, 8);
3754 assert_eq!(account.taker_commission_mantissa, 11);
3755 assert_eq!(account.buyer_commission_mantissa, 2);
3756 assert_eq!(account.seller_commission_mantissa, 3);
3757 assert!(account.can_trade);
3758 assert!(!account.can_withdraw);
3759 assert!(account.can_deposit);
3760 assert!(account.require_self_trade_prevention);
3761 assert!(!account.prevent_sor);
3762 assert_eq!(account.update_time, 1_700_000_000_789_000);
3763 assert_eq!(account.account_type, "SPOT");
3764 assert_eq!(account.balances.len(), 1);
3765 assert_eq!(account.balances[0].asset, "USD");
3766 assert_eq!(account.balances[0].free_mantissa, 123_450);
3767 assert_eq!(account.balances[0].locked_mantissa, 6_789);
3768 assert_eq!(account.balances[0].exponent, -3);
3769 }
3770
3771 #[rstest]
3772 fn test_spot_cancel_replace_json_uses_nested_new_order_response() {
3773 let response: SpotCancelReplaceJson = serde_json::from_value(serde_json::json!({
3774 "cancelResult": "SUCCESS",
3775 "newOrderResult": "SUCCESS",
3776 "cancelResponse": {},
3777 "newOrderResponse": {
3778 "symbol": "ETHUSD",
3779 "orderId": 101,
3780 "orderListId": -1,
3781 "clientOrderId": "new-order",
3782 "transactTime": 1_700_000_000_123_i64,
3783 "price": "12.34",
3784 "origQty": "5.678",
3785 "executedQty": "1.234",
3786 "cummulativeQuoteQty": "15.22756",
3787 "status": "PARTIALLY_FILLED",
3788 "timeInForce": "GTC",
3789 "type": "LIMIT",
3790 "side": "BUY",
3791 "stopPrice": "11.11",
3792 "workingTime": 1_700_000_000_124_i64,
3793 "selfTradePreventionMode": "EXPIRE_MAKER",
3794 "fills": [{
3795 "price": "12.34",
3796 "qty": "1.234",
3797 "commission": "0.001234",
3798 "commissionAsset": "USD",
3799 "tradeId": 44
3800 }]
3801 }
3802 }))
3803 .unwrap();
3804
3805 let order = spot_new_order_from_json(response.new_order_response).unwrap();
3806
3807 assert_eq!(order.price_exponent, -2);
3808 assert_eq!(order.qty_exponent, -3);
3809 assert_eq!(order.order_id, 101);
3810 assert_eq!(order.order_list_id, None);
3811 assert_eq!(order.transact_time, 1_700_000_000_123_000);
3812 assert_eq!(order.price_mantissa, 1_234);
3813 assert_eq!(order.orig_qty_mantissa, 5_678);
3814 assert_eq!(order.executed_qty_mantissa, 1_234);
3815 assert_eq!(order.cummulative_quote_qty_mantissa, 1_522_756);
3816 assert_eq!(order.status, SbeOrderStatus::PartiallyFilled);
3817 assert_eq!(order.time_in_force, SbeTimeInForce::Gtc);
3818 assert_eq!(order.order_type, SbeOrderType::Limit);
3819 assert_eq!(order.side, SbeOrderSide::Buy);
3820 assert_eq!(order.stop_price_mantissa, Some(1_111));
3821 assert_eq!(order.working_time, Some(1_700_000_000_124_000));
3822 assert_eq!(
3823 order.self_trade_prevention_mode,
3824 SbeSelfTradePreventionMode::ExpireMaker
3825 );
3826 assert_eq!(order.client_order_id, "new-order");
3827 assert_eq!(order.symbol, "ETHUSD");
3828 assert_eq!(order.fills.len(), 1);
3829 assert_eq!(order.fills[0].price_mantissa, 1_234);
3830 assert_eq!(order.fills[0].qty_mantissa, 1_234);
3831 assert_eq!(order.fills[0].commission_mantissa, 1_234);
3832 assert_eq!(order.fills[0].commission_exponent, -6);
3833 assert_eq!(order.fills[0].commission_asset, "USD");
3834 assert_eq!(order.fills[0].trade_id, Some(44));
3835 assert_eq!(order.expiry_reason, None);
3836 }
3837
3838 #[rstest]
3839 fn test_rate_limit_config() {
3840 let config = BinanceRawSpotHttpClient::rate_limit_config();
3841
3842 assert!(config.default_quota.is_some());
3843 assert_eq!(config.order_keys.len(), 2);
3845 }
3846
3847 #[rstest]
3848 fn test_quota_from_unknown_interval_returns_none() {
3849 let quota = BinanceRateLimitQuota {
3850 rate_limit_type: BinanceRateLimitType::Orders,
3851 interval: BinanceRateLimitInterval::Unknown,
3852 interval_num: 1,
3853 limit: 10,
3854 };
3855
3856 assert!(BinanceRawSpotHttpClient::quota_from("a).is_none());
3857 }
3858
3859 fn create_test_raw_client() -> BinanceRawSpotHttpClient {
3860 BinanceRawSpotHttpClient::new(
3861 BinanceEnvironment::Live,
3862 None,
3863 None,
3864 Some("http://127.0.0.1:1".to_string()),
3865 None,
3866 Some(1),
3867 None,
3868 )
3869 .unwrap()
3870 }
3871
3872 #[rstest]
3873 #[case::limit(
3874 AggTradesParams {
3875 symbol: "BTCUSDT".to_string(),
3876 from_id: None,
3877 start_time: None,
3878 end_time: None,
3879 limit: Some(1001),
3880 },
3881 "Validation error: aggregate trade limit must not exceed 1000"
3882 )]
3883 #[case::bounds(
3884 AggTradesParams {
3885 symbol: "BTCUSDT".to_string(),
3886 from_id: None,
3887 start_time: Some(2000),
3888 end_time: Some(1000),
3889 limit: Some(1000),
3890 },
3891 "Validation error: aggregate trade startTime must not exceed endTime"
3892 )]
3893 #[tokio::test]
3894 async fn test_agg_trades_rejects_invalid_bounds(
3895 #[case] params: AggTradesParams,
3896 #[case] expected: &str,
3897 ) {
3898 let error = create_test_raw_client()
3899 .agg_trades(¶ms)
3900 .await
3901 .unwrap_err();
3902
3903 assert_eq!(error.to_string(), expected);
3904 }
3905}