1use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, sync::Arc};
35
36use chrono::{DateTime, Utc};
37use dashmap::DashMap;
38use nautilus_common::cache::InstrumentLookupError;
39use nautilus_core::{
40 consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, hex, nanos::UnixNanos, time::AtomicTime,
41};
42use nautilus_model::{
43 data::{Bar, BarType, TradeTick},
44 enums::{AggregationSource, BarAggregation, OrderSide, OrderType, TimeInForce},
45 events::AccountState,
46 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
47 instruments::{Instrument, any::InstrumentAny},
48 reports::{FillReport, OrderStatusReport},
49 types::{Price, Quantity},
50};
51use nautilus_network::{
52 http::{HttpClient, HttpResponse, Method, USER_AGENT},
53 ratelimiter::quota::Quota,
54};
55use serde::Serialize;
56use ustr::Ustr;
57
58use super::{
59 error::{BinanceSpotHttpError, BinanceSpotHttpResult},
60 models::{
61 AvgPrice, BatchCancelResult, BatchOrderResult, BinanceAccountInfo, BinanceAccountTrade,
62 BinanceCancelOrderResponse, BinanceDepth, BinanceKlines, BinanceNewOrderResponse,
63 BinanceOrderResponse, BinanceTrades, BookTicker, ListenKeyResponse,
64 NewOcoOrderListResponse, Ticker24hr, TickerPrice, TradeFee,
65 },
66 parse,
67 query::{
68 AccountInfoParams, AccountTradesParams, AllOrdersParams, AvgPriceParams, BatchCancelItem,
69 BatchOrderItem, CancelOpenOrdersParams, CancelOrderParams, CancelReplaceOrderParams,
70 DepthParams, KlinesParams, ListenKeyParams, NewOcoOrderListParams, NewOrderParams,
71 OpenOrdersParams, QueryOrderParams, TickerParams, TradeFeeParams, TradesParams,
72 },
73};
74use crate::{
75 common::{
76 consts::{
77 BINANCE_API_KEY_HEADER, BINANCE_NAUTILUS_SPOT_BROKER_ID, BINANCE_NO_SUCH_ORDER_CODE,
78 BINANCE_SPOT_RATE_LIMITS, BinanceRateLimitQuota,
79 },
80 credential::SigningCredential,
81 encoder::{decode_broker_id, encode_broker_id},
82 enums::{
83 BinanceEnvironment, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
84 BinanceSide, BinanceTimeInForce,
85 },
86 models::BinanceErrorResponse,
87 parse::{
88 get_currency, parse_fill_report_sbe, parse_klines_to_bars,
89 parse_new_order_response_sbe, parse_order_status_report_sbe, parse_spot_instrument_sbe,
90 parse_spot_trades_sbe,
91 },
92 urls::get_http_base_url,
93 },
94 spot::{
95 enums::{
96 BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
97 order_type_to_binance_spot, time_in_force_to_binance_spot,
98 },
99 sbe::spot::{
100 ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
101 error_response_codec::{self, ErrorResponseDecoder},
102 message_header_codec::MessageHeaderDecoder,
103 },
104 },
105};
106
107pub const SBE_SCHEMA_HEADER: &str = "3:4";
109
110use crate::common::consts::BINANCE_SPOT_API_PATH as SPOT_API_PATH;
111
112const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
114
115const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
117
118struct RateLimitConfig {
119 default_quota: Option<Quota>,
120 keyed_quotas: Vec<(String, Quota)>,
121 order_keys: Vec<String>,
122}
123
124#[derive(Debug, Clone)]
135pub struct BinanceRawSpotHttpClient {
136 client: HttpClient,
137 base_url: String,
138 credential: Option<SigningCredential>,
139 recv_window: Option<u64>,
140 order_rate_keys: Vec<String>,
141}
142
143impl BinanceRawSpotHttpClient {
144 pub fn new(
150 environment: BinanceEnvironment,
151 api_key: Option<String>,
152 api_secret: Option<String>,
153 base_url_override: Option<String>,
154 recv_window: Option<u64>,
155 timeout_secs: Option<u64>,
156 proxy_url: Option<String>,
157 ) -> BinanceSpotHttpResult<Self> {
158 let RateLimitConfig {
159 default_quota,
160 keyed_quotas,
161 order_keys,
162 } = Self::rate_limit_config();
163
164 let credential = match (api_key, api_secret) {
165 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
166 (None, None) => None,
167 _ => return Err(BinanceSpotHttpError::MissingCredentials),
168 };
169
170 let base_url = base_url_override.unwrap_or_else(|| {
171 get_http_base_url(BinanceProductType::Spot, environment).to_string()
172 });
173
174 let headers = Self::default_headers(&credential);
175
176 let client = HttpClient::new(
177 headers,
178 vec![BINANCE_API_KEY_HEADER.to_string()],
179 keyed_quotas,
180 default_quota,
181 timeout_secs,
182 proxy_url,
183 )?;
184
185 Ok(Self {
186 client,
187 base_url,
188 credential,
189 recv_window,
190 order_rate_keys: order_keys,
191 })
192 }
193
194 #[must_use]
196 pub const fn schema_id() -> u16 {
197 SBE_SCHEMA_ID
198 }
199
200 #[must_use]
202 pub const fn schema_version() -> u16 {
203 SBE_SCHEMA_VERSION
204 }
205
206 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
212 where
213 P: Serialize + ?Sized,
214 {
215 self.request(Method::GET, path, params, false, false).await
216 }
217
218 pub async fn get_signed<P>(
224 &self,
225 path: &str,
226 params: Option<&P>,
227 ) -> BinanceSpotHttpResult<Vec<u8>>
228 where
229 P: Serialize + ?Sized,
230 {
231 self.request(Method::GET, path, params, true, false).await
232 }
233
234 pub async fn post_signed<P>(
240 &self,
241 path: &str,
242 params: Option<&P>,
243 ) -> BinanceSpotHttpResult<Vec<u8>>
244 where
245 P: Serialize + ?Sized,
246 {
247 self.request(Method::POST, path, params, true, true).await
248 }
249
250 pub async fn post_signed_json<P>(
256 &self,
257 path: &str,
258 params: Option<&P>,
259 ) -> BinanceSpotHttpResult<Vec<u8>>
260 where
261 P: Serialize + ?Sized,
262 {
263 self.request_with_extra_headers(
264 Method::POST,
265 path,
266 params,
267 true,
268 true,
269 Some(HashMap::from([(
270 "Accept".to_string(),
271 "application/json".to_string(),
272 )])),
273 )
274 .await
275 }
276
277 pub async fn delete_signed<P>(
283 &self,
284 path: &str,
285 params: Option<&P>,
286 ) -> BinanceSpotHttpResult<Vec<u8>>
287 where
288 P: Serialize + ?Sized,
289 {
290 self.request(Method::DELETE, path, params, true, true).await
291 }
292
293 async fn request<P>(
294 &self,
295 method: Method,
296 path: &str,
297 params: Option<&P>,
298 signed: bool,
299 use_order_quota: bool,
300 ) -> BinanceSpotHttpResult<Vec<u8>>
301 where
302 P: Serialize + ?Sized,
303 {
304 self.request_with_extra_headers(method, path, params, signed, use_order_quota, None)
305 .await
306 }
307
308 async fn request_with_extra_headers<P>(
309 &self,
310 method: Method,
311 path: &str,
312 params: Option<&P>,
313 signed: bool,
314 use_order_quota: bool,
315 extra_headers: Option<HashMap<String, String>>,
316 ) -> BinanceSpotHttpResult<Vec<u8>>
317 where
318 P: Serialize + ?Sized,
319 {
320 let mut query = params
321 .map(serde_urlencoded::to_string)
322 .transpose()
323 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
324 .unwrap_or_default();
325
326 let mut headers = extra_headers.unwrap_or_default();
327
328 if signed {
329 let cred = self
330 .credential
331 .as_ref()
332 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
333
334 if !query.is_empty() {
335 query.push('&');
336 }
337
338 let timestamp = Utc::now().timestamp_millis();
339 query.push_str(&format!("timestamp={timestamp}"));
340
341 if let Some(recv_window) = self.recv_window {
342 query.push_str(&format!("&recvWindow={recv_window}"));
343 }
344
345 let signature = Self::percent_encode(&cred.sign(&query));
346 query.push_str(&format!("&signature={signature}"));
347 headers.insert(
348 BINANCE_API_KEY_HEADER.to_string(),
349 cred.api_key().to_string(),
350 );
351 }
352
353 let url = self.build_url(path, &query);
354 let keys = self.rate_limit_keys(use_order_quota);
355
356 let response = self
357 .client
358 .request(
359 method,
360 url,
361 None::<&HashMap<String, Vec<String>>>,
362 Some(headers),
363 None,
364 None,
365 Some(keys),
366 )
367 .await?;
368
369 if !response.status.is_success() {
370 return self.parse_error_response(&response);
371 }
372
373 Ok(response.body.to_vec())
374 }
375
376 fn build_url(&self, path: &str, query: &str) -> String {
377 let normalized_path = if path.starts_with('/') {
378 path.to_string()
379 } else {
380 format!("/{path}")
381 };
382
383 let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
384
385 if !query.is_empty() {
386 url.push('?');
387 url.push_str(query);
388 }
389 url
390 }
391
392 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
393 if use_orders {
394 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
395 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
396 keys.extend(self.order_rate_keys.iter().cloned());
397 keys
398 } else {
399 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
400 }
401 }
402
403 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceSpotHttpResult<T> {
404 let status = response.status.as_u16();
405 let body = &response.body;
406
407 if let Ok(body_str) = std::str::from_utf8(body)
409 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
410 {
411 return Err(BinanceSpotHttpError::BinanceError {
412 code: err.code,
413 message: err.msg,
414 });
415 }
416
417 if let Some((code, message)) = Self::try_decode_sbe_error(body) {
419 return Err(BinanceSpotHttpError::BinanceError {
420 code: code.into(),
421 message,
422 });
423 }
424
425 Err(BinanceSpotHttpError::UnexpectedStatus {
426 status,
427 body: hex::encode(body),
428 })
429 }
430
431 fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
435 const HEADER_LEN: usize = 8;
436 if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
437 return None;
438 }
439
440 let buf = ReadBuf::new(body);
441
442 let header = MessageHeaderDecoder::default().wrap(buf, 0);
444 if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
445 return None;
446 }
447
448 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
450 let code = decoder.code();
451
452 let msg_coords = decoder.msg_decoder();
454 let msg_bytes = decoder.msg_slice(msg_coords);
455 let message = String::from_utf8_lossy(msg_bytes).into_owned();
456
457 Some((code, message))
458 }
459
460 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
461 let mut headers = HashMap::new();
462 headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
463 headers.insert("Accept".to_string(), "application/sbe".to_string());
464 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
465
466 if let Some(cred) = credential {
467 headers.insert(
468 BINANCE_API_KEY_HEADER.to_string(),
469 cred.api_key().to_string(),
470 );
471 }
472 headers
473 }
474
475 fn rate_limit_config() -> RateLimitConfig {
476 let quotas = BINANCE_SPOT_RATE_LIMITS;
477 let mut keyed = Vec::new();
478 let mut order_keys = Vec::new();
479 let mut default = None;
480
481 for quota in quotas {
482 if let Some(q) = Self::quota_from(quota) {
483 match quota.rate_limit_type {
484 BinanceRateLimitType::RequestWeight if default.is_none() => {
485 default = Some(q);
486 }
487 BinanceRateLimitType::Orders => {
488 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
489 order_keys.push(key.clone());
490 keyed.push((key, q));
491 }
492 _ => {}
493 }
494 }
495 }
496
497 let default_quota = default.unwrap_or_else(|| {
498 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
499 });
500
501 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
502
503 RateLimitConfig {
504 default_quota: Some(default_quota),
505 keyed_quotas: keyed,
506 order_keys,
507 }
508 }
509
510 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
511 let burst = NonZeroU32::new(quota.limit)?;
512 match quota.interval {
513 BinanceRateLimitInterval::Second => Quota::per_second(burst),
514 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
515 BinanceRateLimitInterval::Day => {
516 Quota::with_period(std::time::Duration::from_secs(SECONDS_IN_DAY))
517 .map(|q| q.allow_burst(burst))
518 }
519 BinanceRateLimitInterval::Unknown => None,
520 }
521 }
522
523 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
529 let bytes = self.get("ping", None::<&()>).await?;
530 parse::decode_ping(&bytes)?;
531 Ok(())
532 }
533
534 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
542 let bytes = self.get("time", None::<&()>).await?;
543 let timestamp = parse::decode_server_time(&bytes)?;
544 Ok(timestamp)
545 }
546
547 pub async fn exchange_info(
553 &self,
554 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
555 let bytes = self.get("exchangeInfo", None::<&()>).await?;
556 let info = parse::decode_exchange_info(&bytes)?;
557 Ok(info)
558 }
559
560 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
566 let bytes = self.get("depth", Some(params)).await?;
567 let depth = parse::decode_depth(&bytes)?;
568 Ok(depth)
569 }
570
571 pub async fn trades(
577 &self,
578 symbol: &str,
579 limit: Option<u32>,
580 ) -> BinanceSpotHttpResult<BinanceTrades> {
581 let params = TradesParams {
582 symbol: symbol.to_string(),
583 limit,
584 };
585 let bytes = self.get("trades", Some(¶ms)).await?;
586 let trades = parse::decode_trades(&bytes)?;
587 Ok(trades)
588 }
589
590 pub async fn klines(
596 &self,
597 symbol: &str,
598 interval: &str,
599 start_time: Option<i64>,
600 end_time: Option<i64>,
601 limit: Option<u32>,
602 ) -> BinanceSpotHttpResult<BinanceKlines> {
603 let params = KlinesParams {
604 symbol: symbol.to_string(),
605 interval: interval.to_string(),
606 start_time,
607 end_time,
608 time_zone: None,
609 limit,
610 };
611 let bytes = self.get("klines", Some(¶ms)).await?;
612 let klines = parse::decode_klines(&bytes)?;
613 Ok(klines)
614 }
615
616 async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
618 where
619 P: Serialize + ?Sized,
620 {
621 let query = params
622 .map(serde_urlencoded::to_string)
623 .transpose()
624 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
625 .unwrap_or_default();
626
627 let url = self.build_url(path, &query);
628 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
629
630 let response = self
631 .client
632 .request(
633 Method::GET,
634 url,
635 None::<&HashMap<String, Vec<String>>>,
636 None,
637 None,
638 None,
639 Some(keys),
640 )
641 .await?;
642
643 if !response.status.is_success() {
644 return self.parse_error_response(&response);
645 }
646
647 Ok(response.body.to_vec())
648 }
649
650 pub async fn ticker_24hr(
658 &self,
659 symbol: Option<&str>,
660 ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
661 let params = symbol.map(TickerParams::for_symbol);
662 let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
663
664 if symbol.is_some() {
666 let ticker: Ticker24hr = serde_json::from_slice(&bytes)
667 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
668 Ok(vec![ticker])
669 } else {
670 let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
671 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
672 Ok(tickers)
673 }
674 }
675
676 pub async fn ticker_price(
684 &self,
685 symbol: Option<&str>,
686 ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
687 let params = symbol.map(TickerParams::for_symbol);
688 let bytes = self.get_json("ticker/price", params.as_ref()).await?;
689
690 if symbol.is_some() {
692 let ticker: TickerPrice = serde_json::from_slice(&bytes)
693 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
694 Ok(vec![ticker])
695 } else {
696 let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
697 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
698 Ok(tickers)
699 }
700 }
701
702 pub async fn ticker_book(
710 &self,
711 symbol: Option<&str>,
712 ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
713 let params = symbol.map(TickerParams::for_symbol);
714 let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
715
716 if symbol.is_some() {
718 let ticker: BookTicker = serde_json::from_slice(&bytes)
719 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
720 Ok(vec![ticker])
721 } else {
722 let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
723 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
724 Ok(tickers)
725 }
726 }
727
728 pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
734 let params = AvgPriceParams::new(symbol);
735 let bytes = self.get_json("avgPrice", Some(¶ms)).await?;
736
737 let avg_price: AvgPrice = serde_json::from_slice(&bytes)
738 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
739 Ok(avg_price)
740 }
741
742 pub async fn get_trade_fee(
751 &self,
752 symbol: Option<&str>,
753 ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
754 let params = symbol.map(TradeFeeParams::for_symbol);
755 let bytes = self
756 .get_signed_sapi("asset/tradeFee", params.as_ref())
757 .await?;
758
759 let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
760 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
761 Ok(fees)
762 }
763
764 async fn get_signed_sapi<P>(
766 &self,
767 path: &str,
768 params: Option<&P>,
769 ) -> BinanceSpotHttpResult<Vec<u8>>
770 where
771 P: Serialize + ?Sized,
772 {
773 let cred = self
774 .credential
775 .as_ref()
776 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
777
778 let mut query = params
779 .map(serde_urlencoded::to_string)
780 .transpose()
781 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
782 .unwrap_or_default();
783
784 if !query.is_empty() {
785 query.push('&');
786 }
787
788 let timestamp = Utc::now().timestamp_millis();
789 query.push_str(&format!("timestamp={timestamp}"));
790
791 if let Some(recv_window) = self.recv_window {
792 query.push_str(&format!("&recvWindow={recv_window}"));
793 }
794
795 let signature = Self::percent_encode(&cred.sign(&query));
796 query.push_str(&format!("&signature={signature}"));
797
798 let normalized_path = if path.starts_with('/') {
800 path.to_string()
801 } else {
802 format!("/{path}")
803 };
804
805 let mut url = format!("{}/sapi/v1{}", self.base_url, normalized_path);
806
807 if !query.is_empty() {
808 url.push('?');
809 url.push_str(&query);
810 }
811
812 let mut headers = HashMap::new();
813 headers.insert(
814 BINANCE_API_KEY_HEADER.to_string(),
815 cred.api_key().to_string(),
816 );
817
818 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
819
820 let response = self
821 .client
822 .request(
823 Method::GET,
824 url,
825 None::<&HashMap<String, Vec<String>>>,
826 Some(headers),
827 None,
828 None,
829 Some(keys),
830 )
831 .await?;
832
833 if !response.status.is_success() {
834 return self.parse_error_response(&response);
835 }
836
837 Ok(response.body.to_vec())
838 }
839
840 fn percent_encode(input: &str) -> String {
842 let mut result = String::with_capacity(input.len() * 3);
843 for byte in input.bytes() {
844 match byte {
845 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
847 result.push(byte as char);
848 }
849 _ => {
850 result.push('%');
851 result.push_str(&format!("{byte:02X}"));
852 }
853 }
854 }
855 result
856 }
857
858 pub async fn batch_submit_orders(
869 &self,
870 orders: &[BatchOrderItem],
871 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
872 if orders.is_empty() {
873 return Ok(Vec::new());
874 }
875
876 if orders.len() > 5 {
877 return Err(BinanceSpotHttpError::ValidationError(
878 "Batch order limit is 5 orders maximum".to_string(),
879 ));
880 }
881
882 let batch_json = serde_json::to_string(orders)
883 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
884
885 let bytes = self
886 .batch_request(Method::POST, "batchOrders", &batch_json)
887 .await?;
888
889 let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
890 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
891
892 Ok(results)
893 }
894
895 pub async fn batch_cancel_orders(
906 &self,
907 cancels: &[BatchCancelItem],
908 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
909 if cancels.is_empty() {
910 return Ok(Vec::new());
911 }
912
913 if cancels.len() > 5 {
914 return Err(BinanceSpotHttpError::ValidationError(
915 "Batch cancel limit is 5 orders maximum".to_string(),
916 ));
917 }
918
919 let batch_json = serde_json::to_string(cancels)
920 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
921
922 let bytes = self
923 .batch_request(Method::DELETE, "batchOrders", &batch_json)
924 .await?;
925
926 let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
927 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
928
929 Ok(results)
930 }
931
932 async fn batch_request(
934 &self,
935 method: Method,
936 path: &str,
937 batch_json: &str,
938 ) -> BinanceSpotHttpResult<Vec<u8>> {
939 let cred = self
940 .credential
941 .as_ref()
942 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
943
944 let encoded_batch = Self::percent_encode(batch_json);
945 let timestamp = Utc::now().timestamp_millis();
946 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
947
948 if let Some(recv_window) = self.recv_window {
949 query.push_str(&format!("&recvWindow={recv_window}"));
950 }
951
952 let signature = Self::percent_encode(&cred.sign(&query));
953 query.push_str(&format!("&signature={signature}"));
954
955 let url = self.build_url(path, &query);
956
957 let mut headers = HashMap::new();
958 headers.insert(
959 BINANCE_API_KEY_HEADER.to_string(),
960 cred.api_key().to_string(),
961 );
962
963 let keys = self.rate_limit_keys(true);
964
965 let response = self
966 .client
967 .request(
968 method,
969 url,
970 None::<&HashMap<String, Vec<String>>>,
971 Some(headers),
972 None,
973 None,
974 Some(keys),
975 )
976 .await?;
977
978 if !response.status.is_success() {
979 return self.parse_error_response(&response);
980 }
981
982 Ok(response.body.to_vec())
983 }
984
985 pub async fn account(
991 &self,
992 params: &AccountInfoParams,
993 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
994 let bytes = self.get_signed("account", Some(params)).await?;
995 let response = parse::decode_account(&bytes)?;
996 Ok(response)
997 }
998
999 pub async fn account_trades(
1005 &self,
1006 symbol: &str,
1007 order_id: Option<i64>,
1008 start_time: Option<i64>,
1009 end_time: Option<i64>,
1010 limit: Option<u32>,
1011 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1012 let params = AccountTradesParams {
1013 symbol: symbol.to_string(),
1014 order_id,
1015 start_time,
1016 end_time,
1017 from_id: None,
1018 limit,
1019 };
1020 let bytes = self.get_signed("myTrades", Some(¶ms)).await?;
1021 let response = parse::decode_account_trades(&bytes)?;
1022 Ok(response)
1023 }
1024
1025 pub async fn query_order(
1033 &self,
1034 symbol: &str,
1035 order_id: Option<i64>,
1036 client_order_id: Option<&str>,
1037 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
1038 let params = QueryOrderParams {
1039 symbol: symbol.to_string(),
1040 order_id,
1041 orig_client_order_id: client_order_id.map(|s| s.to_string()),
1042 };
1043 let bytes = self.get_signed("order", Some(¶ms)).await?;
1044 let response = parse::decode_order(&bytes)?;
1045 Ok(response)
1046 }
1047
1048 pub async fn open_orders(
1054 &self,
1055 symbol: Option<&str>,
1056 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1057 let params = OpenOrdersParams {
1058 symbol: symbol.map(|s| s.to_string()),
1059 };
1060 let bytes = self.get_signed("openOrders", Some(¶ms)).await?;
1061 let response = parse::decode_orders(&bytes)?;
1062 Ok(response)
1063 }
1064
1065 pub async fn all_orders(
1071 &self,
1072 symbol: &str,
1073 start_time: Option<i64>,
1074 end_time: Option<i64>,
1075 limit: Option<u32>,
1076 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1077 let params = AllOrdersParams {
1078 symbol: symbol.to_string(),
1079 order_id: None,
1080 start_time,
1081 end_time,
1082 limit,
1083 };
1084 let bytes = self.get_signed("allOrders", Some(¶ms)).await?;
1085 let response = parse::decode_orders(&bytes)?;
1086 Ok(response)
1087 }
1088
1089 async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1091 where
1092 P: Serialize + ?Sized,
1093 {
1094 self.post_signed(path, params).await
1095 }
1096
1097 async fn delete_order<P>(
1099 &self,
1100 path: &str,
1101 params: Option<&P>,
1102 ) -> BinanceSpotHttpResult<Vec<u8>>
1103 where
1104 P: Serialize + ?Sized,
1105 {
1106 self.delete_signed(path, params).await
1107 }
1108
1109 #[expect(clippy::too_many_arguments)]
1115 pub async fn new_order(
1116 &self,
1117 symbol: &str,
1118 side: BinanceSide,
1119 order_type: BinanceSpotOrderType,
1120 time_in_force: Option<BinanceTimeInForce>,
1121 quantity: Option<&str>,
1122 price: Option<&str>,
1123 client_order_id: Option<&str>,
1124 stop_price: Option<&str>,
1125 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1126 let params = NewOrderParams {
1127 symbol: symbol.to_string(),
1128 side,
1129 order_type,
1130 time_in_force,
1131 quantity: quantity.map(|s| s.to_string()),
1132 quote_order_qty: None,
1133 price: price.map(|s| s.to_string()),
1134 new_client_order_id: client_order_id.map(|s| s.to_string()),
1135 stop_price: stop_price.map(|s| s.to_string()),
1136 trailing_delta: None,
1137 iceberg_qty: None,
1138 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1139 self_trade_prevention_mode: None,
1140 strategy_id: None,
1141 strategy_type: None,
1142 };
1143 let bytes = self.post_order("order", Some(¶ms)).await?;
1144 let response = parse::decode_new_order_full(&bytes)?;
1145 Ok(response)
1146 }
1147
1148 #[expect(clippy::too_many_arguments)]
1158 pub async fn new_order_full(
1159 &self,
1160 symbol: &str,
1161 side: BinanceSide,
1162 order_type: BinanceSpotOrderType,
1163 time_in_force: Option<BinanceTimeInForce>,
1164 quantity: Option<&str>,
1165 quote_order_qty: Option<&str>,
1166 price: Option<&str>,
1167 client_order_id: Option<&str>,
1168 stop_price: Option<&str>,
1169 iceberg_qty: Option<&str>,
1170 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1171 let params = NewOrderParams {
1172 symbol: symbol.to_string(),
1173 side,
1174 order_type,
1175 time_in_force,
1176 quantity: quantity.map(|s| s.to_string()),
1177 quote_order_qty: quote_order_qty.map(|s| s.to_string()),
1178 price: price.map(|s| s.to_string()),
1179 new_client_order_id: client_order_id.map(|s| s.to_string()),
1180 stop_price: stop_price.map(|s| s.to_string()),
1181 trailing_delta: None,
1182 iceberg_qty: iceberg_qty.map(|s| s.to_string()),
1183 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1184 self_trade_prevention_mode: None,
1185 strategy_id: None,
1186 strategy_type: None,
1187 };
1188 let bytes = self.post_order("order", Some(¶ms)).await?;
1189 let response = parse::decode_new_order_full(&bytes)?;
1190 Ok(response)
1191 }
1192
1193 pub async fn new_oco_order_list(
1199 &self,
1200 params: &NewOcoOrderListParams,
1201 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
1202 let bytes = self.post_signed_json("orderList/oco", Some(params)).await?;
1203 serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1204 }
1205
1206 #[expect(clippy::too_many_arguments)]
1212 pub async fn cancel_replace_order(
1213 &self,
1214 symbol: &str,
1215 side: BinanceSide,
1216 order_type: BinanceSpotOrderType,
1217 time_in_force: Option<BinanceTimeInForce>,
1218 quantity: Option<&str>,
1219 price: Option<&str>,
1220 cancel_order_id: Option<i64>,
1221 cancel_client_order_id: Option<&str>,
1222 new_client_order_id: Option<&str>,
1223 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1224 let params = CancelReplaceOrderParams {
1225 symbol: symbol.to_string(),
1226 side,
1227 order_type,
1228 cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1229 time_in_force,
1230 quantity: quantity.map(|s| s.to_string()),
1231 quote_order_qty: None,
1232 price: price.map(|s| s.to_string()),
1233 cancel_order_id,
1234 cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1235 new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1236 stop_price: None,
1237 trailing_delta: None,
1238 iceberg_qty: None,
1239 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1240 self_trade_prevention_mode: None,
1241 };
1242 let bytes = self
1243 .post_order("order/cancelReplace", Some(¶ms))
1244 .await?;
1245 let response = parse::decode_new_order_full(&bytes)?;
1246 Ok(response)
1247 }
1248
1249 pub async fn cancel_order(
1257 &self,
1258 symbol: &str,
1259 order_id: Option<i64>,
1260 client_order_id: Option<&str>,
1261 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1262 let params = match (order_id, client_order_id) {
1263 (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1264 (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1265 (None, None) => {
1266 return Err(BinanceSpotHttpError::ValidationError(
1267 "Either order_id or client_order_id must be provided".to_string(),
1268 ));
1269 }
1270 };
1271 let bytes = self.delete_order("order", Some(¶ms)).await?;
1272 let response = parse::decode_cancel_order(&bytes)?;
1273 Ok(response)
1274 }
1275
1276 pub async fn cancel_open_orders(
1282 &self,
1283 symbol: &str,
1284 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1285 let params = CancelOpenOrdersParams::new(symbol.to_string());
1286 let bytes = self.delete_order("openOrders", Some(¶ms)).await?;
1287 let response = parse::decode_cancel_open_orders(&bytes)?;
1288 Ok(response)
1289 }
1290
1291 async fn request_with_api_key<P>(
1293 &self,
1294 method: Method,
1295 path: &str,
1296 params: Option<&P>,
1297 ) -> BinanceSpotHttpResult<Vec<u8>>
1298 where
1299 P: Serialize + ?Sized,
1300 {
1301 let cred = self
1302 .credential
1303 .as_ref()
1304 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1305
1306 let query = params
1307 .map(serde_urlencoded::to_string)
1308 .transpose()
1309 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1310 .unwrap_or_default();
1311
1312 let url = self.build_url(path, &query);
1313
1314 let mut headers = HashMap::new();
1315 headers.insert(
1316 BINANCE_API_KEY_HEADER.to_string(),
1317 cred.api_key().to_string(),
1318 );
1319
1320 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1321
1322 let response = self
1323 .client
1324 .request(
1325 method,
1326 url,
1327 None::<&HashMap<String, Vec<String>>>,
1328 Some(headers),
1329 None,
1330 None,
1331 Some(keys),
1332 )
1333 .await?;
1334
1335 if !response.status.is_success() {
1336 return self.parse_error_response(&response);
1337 }
1338
1339 Ok(response.body.to_vec())
1340 }
1341
1342 pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1351 let bytes = self
1352 .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1353 .await?;
1354
1355 let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1356 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1357
1358 Ok(response)
1359 }
1360
1361 pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1369 let params = ListenKeyParams::new(listen_key);
1370 self.request_with_api_key(Method::PUT, "userDataStream", Some(¶ms))
1371 .await?;
1372 Ok(())
1373 }
1374
1375 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1381 let params = ListenKeyParams::new(listen_key);
1382 self.request_with_api_key(Method::DELETE, "userDataStream", Some(¶ms))
1383 .await?;
1384 Ok(())
1385 }
1386}
1387
1388pub struct BinanceSpotHttpClient {
1394 inner: Arc<BinanceRawSpotHttpClient>,
1395 clock: &'static AtomicTime,
1396 instruments_cache: Arc<DashMap<Ustr, InstrumentAny>>,
1397}
1398
1399impl Clone for BinanceSpotHttpClient {
1400 fn clone(&self) -> Self {
1401 Self {
1402 inner: self.inner.clone(),
1403 clock: self.clock,
1404 instruments_cache: self.instruments_cache.clone(),
1405 }
1406 }
1407}
1408
1409impl Debug for BinanceSpotHttpClient {
1410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1411 f.debug_struct(stringify!(BinanceSpotHttpClient))
1412 .field("inner", &self.inner)
1413 .field("instruments_cached", &self.instruments_cache.len())
1414 .finish()
1415 }
1416}
1417
1418impl BinanceSpotHttpClient {
1419 #[expect(clippy::too_many_arguments)]
1425 pub fn new(
1426 environment: BinanceEnvironment,
1427 clock: &'static AtomicTime,
1428 api_key: Option<String>,
1429 api_secret: Option<String>,
1430 base_url_override: Option<String>,
1431 recv_window: Option<u64>,
1432 timeout_secs: Option<u64>,
1433 proxy_url: Option<String>,
1434 ) -> BinanceSpotHttpResult<Self> {
1435 let inner = BinanceRawSpotHttpClient::new(
1436 environment,
1437 api_key,
1438 api_secret,
1439 base_url_override,
1440 recv_window,
1441 timeout_secs,
1442 proxy_url,
1443 )?;
1444
1445 Ok(Self {
1446 inner: Arc::new(inner),
1447 clock,
1448 instruments_cache: Arc::new(DashMap::new()),
1449 })
1450 }
1451
1452 #[must_use]
1454 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
1455 &self.inner
1456 }
1457
1458 #[must_use]
1460 pub const fn schema_id() -> u16 {
1461 SBE_SCHEMA_ID
1462 }
1463
1464 #[must_use]
1466 pub const fn schema_version() -> u16 {
1467 SBE_SCHEMA_VERSION
1468 }
1469
1470 fn generate_ts_init(&self) -> UnixNanos {
1472 self.clock.get_time_ns()
1473 }
1474
1475 fn command_validation_error(message: impl Into<String>) -> anyhow::Error {
1476 anyhow::anyhow!(BinanceSpotHttpError::ValidationError(message.into()))
1477 }
1478
1479 fn response_parse_error(message: impl Into<String>) -> anyhow::Error {
1480 anyhow::anyhow!(BinanceSpotHttpError::ResponseParseError(message.into()))
1481 }
1482
1483 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
1485 self.instruments_cache
1486 .get(&symbol)
1487 .map(|entry| entry.value().clone())
1488 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
1489 }
1490
1491 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
1493 for inst in instruments {
1494 self.instruments_cache
1495 .insert(inst.raw_symbol().inner(), inst);
1496 }
1497 }
1498
1499 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1501 self.instruments_cache
1502 .insert(instrument.raw_symbol().inner(), instrument);
1503 }
1504
1505 #[must_use]
1507 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1508 self.instruments_cache
1509 .get(symbol)
1510 .map(|entry| entry.value().clone())
1511 }
1512
1513 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
1519 self.inner.ping().await
1520 }
1521
1522 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
1530 self.inner.server_time().await
1531 }
1532
1533 pub async fn exchange_info(
1539 &self,
1540 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
1541 self.inner.exchange_info().await
1542 }
1543
1544 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
1553 let info = self.exchange_info().await?;
1554 let ts_init = self.generate_ts_init();
1555
1556 let mut instruments = Vec::with_capacity(info.symbols.len());
1557 for symbol in &info.symbols {
1558 match parse_spot_instrument_sbe(symbol, ts_init, ts_init) {
1559 Ok(instrument) => instruments.push(instrument),
1560 Err(e) => {
1561 log::debug!(
1562 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1563 symbol.symbol
1564 );
1565 }
1566 }
1567 }
1568
1569 self.cache_instruments(instruments.clone());
1571
1572 log::debug!("Loaded spot instruments: count={}", instruments.len());
1573 Ok(instruments)
1574 }
1575
1576 pub async fn request_trades(
1583 &self,
1584 instrument_id: InstrumentId,
1585 limit: Option<u32>,
1586 ) -> anyhow::Result<Vec<TradeTick>> {
1587 let symbol = instrument_id.symbol.inner();
1588 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
1589 let ts_init = self.generate_ts_init();
1590
1591 let trades = self
1592 .inner
1593 .trades(symbol.as_str(), limit)
1594 .await
1595 .map_err(|e| anyhow::anyhow!(e))?;
1596
1597 parse_spot_trades_sbe(&trades, &instrument, ts_init)
1598 }
1599
1600 pub async fn request_bars(
1607 &self,
1608 bar_type: BarType,
1609 start: Option<DateTime<Utc>>,
1610 end: Option<DateTime<Utc>>,
1611 limit: Option<u32>,
1612 ) -> anyhow::Result<Vec<Bar>> {
1613 anyhow::ensure!(
1614 bar_type.aggregation_source() == AggregationSource::External,
1615 "Only EXTERNAL aggregation is supported"
1616 );
1617
1618 let spec = bar_type.spec();
1619 let step = spec.step.get();
1620 let interval = match spec.aggregation {
1621 BarAggregation::Second => {
1622 anyhow::bail!("Binance Spot does not support second-level kline intervals")
1623 }
1624 BarAggregation::Minute => format!("{step}m"),
1625 BarAggregation::Hour => format!("{step}h"),
1626 BarAggregation::Day => format!("{step}d"),
1627 BarAggregation::Week => format!("{step}w"),
1628 BarAggregation::Month => format!("{step}M"),
1629 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
1630 };
1631
1632 let instrument_id = bar_type.instrument_id();
1633 let symbol = instrument_id.symbol;
1634 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
1635 let ts_init = self.generate_ts_init();
1636
1637 let klines = self
1638 .inner
1639 .klines(
1640 symbol.as_str(),
1641 &interval,
1642 start.map(|dt| dt.timestamp_millis()),
1643 end.map(|dt| dt.timestamp_millis()),
1644 limit,
1645 )
1646 .await
1647 .map_err(|e| anyhow::anyhow!(e))?;
1648
1649 parse_klines_to_bars(&klines, bar_type, &instrument, ts_init)
1650 }
1651
1652 fn instrument_from_cache_by_id(
1653 &self,
1654 instrument_id: InstrumentId,
1655 ) -> anyhow::Result<InstrumentAny> {
1656 self.instruments_cache
1657 .get(&instrument_id.symbol.inner())
1658 .map(|entry| entry.value().clone())
1659 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
1660 }
1661
1662 pub async fn request_account_state(
1668 &self,
1669 account_id: AccountId,
1670 ) -> anyhow::Result<AccountState> {
1671 let ts_init = self.clock.get_time_ns();
1672 let params = AccountInfoParams::default();
1673 let account_info = self.inner.account(¶ms).await?;
1674 Ok(account_info.to_account_state(account_id, ts_init))
1675 }
1676
1677 pub async fn request_order_status_report(
1686 &self,
1687 account_id: AccountId,
1688 instrument_id: InstrumentId,
1689 venue_order_id: Option<VenueOrderId>,
1690 client_order_id: Option<ClientOrderId>,
1691 ) -> anyhow::Result<Option<OrderStatusReport>> {
1692 anyhow::ensure!(
1693 venue_order_id.is_some() || client_order_id.is_some(),
1694 "Either venue_order_id or client_order_id must be provided"
1695 );
1696
1697 let symbol = instrument_id.symbol.inner();
1698 let instrument = self.instrument_from_cache(symbol)?;
1699 let ts_init = self.generate_ts_init();
1700
1701 let order_id = venue_order_id
1702 .map(|id| id.inner().parse::<i64>())
1703 .transpose()
1704 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1705
1706 let client_id_str =
1707 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
1708
1709 let order = match self
1710 .inner
1711 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
1712 .await
1713 {
1714 Ok(order) => order,
1715 Err(e) if Self::is_no_such_order_error(&e) => {
1716 log::debug!("Binance Spot order not found: instrument_id={instrument_id}");
1717 return Ok(None);
1718 }
1719 Err(e) => anyhow::bail!(e),
1720 };
1721
1722 parse_order_status_report_sbe(
1723 &order,
1724 account_id,
1725 &instrument,
1726 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1727 ts_init,
1728 )
1729 .map(Some)
1730 }
1731
1732 const fn is_no_such_order_error(error: &BinanceSpotHttpError) -> bool {
1733 matches!(
1734 error,
1735 BinanceSpotHttpError::BinanceError { code, .. } if *code == BINANCE_NO_SUCH_ORDER_CODE
1736 )
1737 }
1738
1739 pub async fn request_order_status_reports(
1749 &self,
1750 account_id: AccountId,
1751 instrument_id: Option<InstrumentId>,
1752 start: Option<DateTime<Utc>>,
1753 end: Option<DateTime<Utc>>,
1754 open_only: bool,
1755 limit: Option<u32>,
1756 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1757 let ts_init = self.generate_ts_init();
1758 let symbol = instrument_id.map(|id| id.symbol.to_string());
1759
1760 let orders = if open_only {
1761 self.inner
1762 .open_orders(symbol.as_deref())
1763 .await
1764 .map_err(|e| anyhow::anyhow!(e))?
1765 } else {
1766 let symbol = symbol
1767 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
1768 self.inner
1769 .all_orders(
1770 &symbol,
1771 start.map(|dt| dt.timestamp_millis()),
1772 end.map(|dt| dt.timestamp_millis()),
1773 limit,
1774 )
1775 .await
1776 .map_err(|e| anyhow::anyhow!(e))?
1777 };
1778
1779 orders
1780 .iter()
1781 .map(|order| {
1782 let symbol = Ustr::from(&order.symbol);
1783 let instrument = self.instrument_from_cache(symbol)?;
1784 parse_order_status_report_sbe(
1785 order,
1786 account_id,
1787 &instrument,
1788 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1789 ts_init,
1790 )
1791 })
1792 .collect()
1793 }
1794
1795 pub async fn request_fill_reports(
1802 &self,
1803 account_id: AccountId,
1804 instrument_id: InstrumentId,
1805 venue_order_id: Option<VenueOrderId>,
1806 start: Option<DateTime<Utc>>,
1807 end: Option<DateTime<Utc>>,
1808 limit: Option<u32>,
1809 ) -> anyhow::Result<Vec<FillReport>> {
1810 let ts_init = self.generate_ts_init();
1811 let symbol = instrument_id.symbol.inner();
1812
1813 let order_id = venue_order_id
1814 .map(|id| id.inner().parse::<i64>())
1815 .transpose()
1816 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1817
1818 let trades = self
1819 .inner
1820 .account_trades(
1821 symbol.as_str(),
1822 order_id,
1823 start.map(|dt| dt.timestamp_millis()),
1824 end.map(|dt| dt.timestamp_millis()),
1825 limit,
1826 )
1827 .await
1828 .map_err(|e| anyhow::anyhow!(e))?;
1829
1830 trades
1831 .iter()
1832 .map(|trade| {
1833 let symbol = Ustr::from(&trade.symbol);
1834 let instrument = self.instrument_from_cache(symbol)?;
1835 let commission_currency = get_currency(&trade.commission_asset);
1836 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
1837 })
1838 .collect()
1839 }
1840
1841 #[expect(clippy::too_many_arguments)]
1854 pub async fn submit_order(
1855 &self,
1856 account_id: AccountId,
1857 instrument_id: InstrumentId,
1858 client_order_id: ClientOrderId,
1859 order_side: OrderSide,
1860 order_type: OrderType,
1861 quantity: Quantity,
1862 time_in_force: TimeInForce,
1863 price: Option<Price>,
1864 trigger_price: Option<Price>,
1865 post_only: bool,
1866 quote_quantity: bool,
1867 display_qty: Option<Quantity>,
1868 ) -> anyhow::Result<OrderStatusReport> {
1869 let symbol = instrument_id.symbol.inner();
1870 let instrument = self
1871 .instrument_from_cache(symbol)
1872 .map_err(|e| Self::command_validation_error(e.to_string()))?;
1873 let ts_init = self.generate_ts_init();
1874
1875 let binance_side = BinanceSide::try_from(order_side)
1876 .map_err(|e| Self::command_validation_error(e.to_string()))?;
1877 let binance_order_type = order_type_to_binance_spot(order_type, post_only)
1878 .map_err(|e| Self::command_validation_error(e.to_string()))?;
1879
1880 let requires_trigger = matches!(
1882 order_type,
1883 OrderType::StopMarket
1884 | OrderType::StopLimit
1885 | OrderType::MarketIfTouched
1886 | OrderType::LimitIfTouched
1887 );
1888
1889 if requires_trigger && trigger_price.is_none() {
1890 return Err(Self::command_validation_error(
1891 "Conditional orders require a trigger price",
1892 ));
1893 }
1894
1895 let requires_price = matches!(
1897 binance_order_type,
1898 BinanceSpotOrderType::Limit
1899 | BinanceSpotOrderType::StopLossLimit
1900 | BinanceSpotOrderType::TakeProfitLimit
1901 | BinanceSpotOrderType::LimitMaker
1902 );
1903
1904 if requires_price && price.is_none() {
1905 return Err(Self::command_validation_error(format!(
1906 "{binance_order_type:?} orders require a price"
1907 )));
1908 }
1909
1910 let supports_tif = matches!(
1912 binance_order_type,
1913 BinanceSpotOrderType::Limit
1914 | BinanceSpotOrderType::StopLossLimit
1915 | BinanceSpotOrderType::TakeProfitLimit
1916 );
1917 let binance_tif = if supports_tif {
1918 Some(
1919 time_in_force_to_binance_spot(time_in_force)
1920 .map_err(|e| Self::command_validation_error(e.to_string()))?,
1921 )
1922 } else {
1923 None
1924 };
1925
1926 let qty_str = quantity.to_string();
1927 let price_str = price.map(|p| p.to_string());
1928 let stop_price_str = trigger_price.map(|p| p.to_string());
1929 let iceberg_qty_str = display_qty.map(|q| q.to_string());
1930 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
1931
1932 if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
1933 return Err(Self::command_validation_error(
1934 "quoteOrderQty is only supported for MARKET orders",
1935 ));
1936 }
1937
1938 let (base_qty, quote_qty) = if quote_quantity {
1939 (None, Some(qty_str.as_str()))
1940 } else {
1941 (Some(qty_str.as_str()), None)
1942 };
1943
1944 let response = self
1945 .inner
1946 .new_order_full(
1947 symbol.as_str(),
1948 binance_side,
1949 binance_order_type,
1950 binance_tif,
1951 base_qty,
1952 quote_qty,
1953 price_str.as_deref(),
1954 Some(&client_id_str),
1955 stop_price_str.as_deref(),
1956 iceberg_qty_str.as_deref(),
1957 )
1958 .await?;
1959
1960 parse_new_order_response_sbe(
1961 &response,
1962 account_id,
1963 &instrument,
1964 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1965 ts_init,
1966 )
1967 .map_err(|e| Self::response_parse_error(e.to_string()))
1968 }
1969
1970 pub async fn submit_order_list(
1978 &self,
1979 orders: &[BatchOrderItem],
1980 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1981 self.inner.batch_submit_orders(orders).await
1982 }
1983
1984 pub async fn submit_oco_order_list(
1990 &self,
1991 params: &NewOcoOrderListParams,
1992 ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
1993 self.inner.new_oco_order_list(params).await
1994 }
1995
1996 #[expect(clippy::too_many_arguments)]
2005 pub async fn modify_order(
2006 &self,
2007 account_id: AccountId,
2008 instrument_id: InstrumentId,
2009 venue_order_id: VenueOrderId,
2010 client_order_id: ClientOrderId,
2011 order_side: OrderSide,
2012 order_type: OrderType,
2013 quantity: Quantity,
2014 time_in_force: TimeInForce,
2015 price: Option<Price>,
2016 ) -> anyhow::Result<OrderStatusReport> {
2017 let symbol = instrument_id.symbol.inner();
2018 let instrument = self
2019 .instrument_from_cache(symbol)
2020 .map_err(|e| Self::command_validation_error(e.to_string()))?;
2021 let ts_init = self.generate_ts_init();
2022
2023 let binance_side = BinanceSide::try_from(order_side)
2024 .map_err(|e| Self::command_validation_error(e.to_string()))?;
2025 let binance_order_type = order_type_to_binance_spot(order_type, false)
2026 .map_err(|e| Self::command_validation_error(e.to_string()))?;
2027 let binance_tif = time_in_force_to_binance_spot(time_in_force)
2028 .map_err(|e| Self::command_validation_error(e.to_string()))?;
2029
2030 let cancel_order_id: i64 = venue_order_id.inner().parse().map_err(|_| {
2031 Self::command_validation_error(format!("Invalid venue order ID: {venue_order_id}"))
2032 })?;
2033
2034 let qty_str = quantity.to_string();
2035 let price_str = price.map(|p| p.to_string());
2036 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
2037
2038 let response = self
2039 .inner
2040 .cancel_replace_order(
2041 symbol.as_str(),
2042 binance_side,
2043 binance_order_type,
2044 Some(binance_tif),
2045 Some(&qty_str),
2046 price_str.as_deref(),
2047 Some(cancel_order_id),
2048 None,
2049 Some(&client_id_str),
2050 )
2051 .await
2052 .map_err(|e| anyhow::anyhow!(e))?;
2053
2054 parse_new_order_response_sbe(
2055 &response,
2056 account_id,
2057 &instrument,
2058 BINANCE_NAUTILUS_SPOT_BROKER_ID,
2059 ts_init,
2060 )
2061 .map_err(|e| Self::response_parse_error(e.to_string()))
2062 }
2063
2064 pub async fn cancel_order(
2072 &self,
2073 instrument_id: InstrumentId,
2074 venue_order_id: Option<VenueOrderId>,
2075 client_order_id: Option<ClientOrderId>,
2076 ) -> anyhow::Result<VenueOrderId> {
2077 let symbol = instrument_id.symbol.inner();
2078
2079 let order_id = match venue_order_id {
2080 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2081 Ok(order_id) => Some(order_id),
2082 Err(e) if client_order_id.is_some() => {
2083 log::warn!(
2084 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2085 );
2086 None
2087 }
2088 Err(e) => {
2089 return Err(Self::command_validation_error(format!(
2090 "Invalid venue order ID: {e}"
2091 )));
2092 }
2093 },
2094 None => None,
2095 };
2096
2097 let client_id_str =
2098 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
2099
2100 let response = self
2101 .inner
2102 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
2103 .await
2104 .map_err(|e| anyhow::anyhow!(e))?;
2105
2106 Ok(VenueOrderId::new(response.order_id.to_string()))
2107 }
2108
2109 pub async fn batch_cancel_orders(
2117 &self,
2118 cancels: &[BatchCancelItem],
2119 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
2120 self.inner.batch_cancel_orders(cancels).await
2121 }
2122
2123 pub async fn cancel_all_orders(
2131 &self,
2132 instrument_id: InstrumentId,
2133 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
2134 let symbol = instrument_id.symbol.inner();
2135
2136 let responses = self
2137 .inner
2138 .cancel_open_orders(symbol.as_str())
2139 .await
2140 .map_err(|e| anyhow::anyhow!(e))?;
2141
2142 Ok(responses
2143 .into_iter()
2144 .map(|r| {
2145 (
2146 VenueOrderId::new(r.order_id.to_string()),
2147 ClientOrderId::new(decode_broker_id(
2148 &r.orig_client_order_id,
2149 BINANCE_NAUTILUS_SPOT_BROKER_ID,
2150 )),
2151 )
2152 })
2153 .collect())
2154 }
2155}
2156
2157#[cfg(test)]
2158mod tests {
2159 use rstest::rstest;
2160
2161 use super::*;
2162
2163 #[rstest]
2164 fn test_schema_constants() {
2165 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
2166 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 4);
2167 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
2168 assert_eq!(BinanceSpotHttpClient::schema_version(), 4);
2169 }
2170
2171 #[rstest]
2172 fn test_sbe_schema_header() {
2173 assert_eq!(SBE_SCHEMA_HEADER, "3:4");
2174 }
2175
2176 #[rstest]
2177 fn test_default_headers_include_sbe() {
2178 let headers = BinanceRawSpotHttpClient::default_headers(&None);
2179
2180 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
2181 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:4".to_string()));
2182 }
2183
2184 #[rstest]
2185 fn test_rate_limit_config() {
2186 let config = BinanceRawSpotHttpClient::rate_limit_config();
2187
2188 assert!(config.default_quota.is_some());
2189 assert_eq!(config.order_keys.len(), 2);
2191 }
2192
2193 #[rstest]
2194 fn test_quota_from_unknown_interval_returns_none() {
2195 let quota = BinanceRateLimitQuota {
2196 rate_limit_type: BinanceRateLimitType::Orders,
2197 interval: BinanceRateLimitInterval::Unknown,
2198 interval_num: 1,
2199 limit: 10,
2200 };
2201
2202 assert!(BinanceRawSpotHttpClient::quota_from("a).is_none());
2203 }
2204}