1use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, sync::Arc};
35
36use chrono::{DateTime, Utc};
37use dashmap::DashMap;
38use nautilus_core::{
39 consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, hex, nanos::UnixNanos, time::AtomicTime,
40};
41use nautilus_model::{
42 data::{Bar, BarType, TradeTick},
43 enums::{AggregationSource, BarAggregation, OrderSide, OrderType, TimeInForce},
44 events::AccountState,
45 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
46 instruments::{Instrument, any::InstrumentAny},
47 reports::{FillReport, OrderStatusReport},
48 types::{Price, Quantity},
49};
50use nautilus_network::{
51 http::{HttpClient, HttpResponse, Method},
52 ratelimiter::quota::Quota,
53};
54use serde::Serialize;
55use ustr::Ustr;
56
57use super::{
58 error::{BinanceSpotHttpError, BinanceSpotHttpResult},
59 models::{
60 AvgPrice, BatchCancelResult, BatchOrderResult, BinanceAccountInfo, BinanceAccountTrade,
61 BinanceCancelOrderResponse, BinanceDepth, BinanceKlines, BinanceNewOrderResponse,
62 BinanceOrderResponse, BinanceTrades, BookTicker, ListenKeyResponse, Ticker24hr,
63 TickerPrice, TradeFee,
64 },
65 parse,
66 query::{
67 AccountInfoParams, AccountTradesParams, AllOrdersParams, AvgPriceParams, BatchCancelItem,
68 BatchOrderItem, CancelOpenOrdersParams, CancelOrderParams, CancelReplaceOrderParams,
69 DepthParams, KlinesParams, ListenKeyParams, NewOrderParams, OpenOrdersParams,
70 QueryOrderParams, TickerParams, TradeFeeParams, TradesParams,
71 },
72};
73use crate::{
74 common::{
75 consts::{
76 BINANCE_API_KEY_HEADER, BINANCE_NAUTILUS_SPOT_BROKER_ID, BINANCE_SPOT_RATE_LIMITS,
77 BinanceRateLimitQuota,
78 },
79 credential::SigningCredential,
80 encoder::{decode_broker_id, encode_broker_id},
81 enums::{
82 BinanceEnvironment, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
83 BinanceSide, BinanceTimeInForce,
84 },
85 models::BinanceErrorResponse,
86 parse::{
87 get_currency, parse_fill_report_sbe, parse_klines_to_bars,
88 parse_new_order_response_sbe, parse_order_status_report_sbe, parse_spot_instrument_sbe,
89 parse_spot_trades_sbe,
90 },
91 urls::get_http_base_url,
92 },
93 spot::{
94 enums::{
95 BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
96 order_type_to_binance_spot, time_in_force_to_binance_spot,
97 },
98 sbe::spot::{
99 ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
100 error_response_codec::{self, ErrorResponseDecoder},
101 message_header_codec::MessageHeaderDecoder,
102 },
103 },
104};
105
106pub const SBE_SCHEMA_HEADER: &str = "3:3";
108
109use crate::common::consts::BINANCE_SPOT_API_PATH as SPOT_API_PATH;
110
111const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
113
114const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
116
117struct RateLimitConfig {
118 default_quota: Option<Quota>,
119 keyed_quotas: Vec<(String, Quota)>,
120 order_keys: Vec<String>,
121}
122
123#[derive(Debug, Clone)]
134pub struct BinanceRawSpotHttpClient {
135 client: HttpClient,
136 base_url: String,
137 credential: Option<SigningCredential>,
138 recv_window: Option<u64>,
139 order_rate_keys: Vec<String>,
140}
141
142impl BinanceRawSpotHttpClient {
143 pub fn new(
149 environment: BinanceEnvironment,
150 api_key: Option<String>,
151 api_secret: Option<String>,
152 base_url_override: Option<String>,
153 recv_window: Option<u64>,
154 timeout_secs: Option<u64>,
155 proxy_url: Option<String>,
156 ) -> BinanceSpotHttpResult<Self> {
157 let RateLimitConfig {
158 default_quota,
159 keyed_quotas,
160 order_keys,
161 } = Self::rate_limit_config();
162
163 let credential = match (api_key, api_secret) {
164 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
165 (None, None) => None,
166 _ => return Err(BinanceSpotHttpError::MissingCredentials),
167 };
168
169 let base_url = base_url_override.unwrap_or_else(|| {
170 get_http_base_url(BinanceProductType::Spot, environment).to_string()
171 });
172
173 let headers = Self::default_headers(&credential);
174
175 let client = HttpClient::new(
176 headers,
177 vec![BINANCE_API_KEY_HEADER.to_string()],
178 keyed_quotas,
179 default_quota,
180 timeout_secs,
181 proxy_url,
182 )?;
183
184 Ok(Self {
185 client,
186 base_url,
187 credential,
188 recv_window,
189 order_rate_keys: order_keys,
190 })
191 }
192
193 #[must_use]
195 pub const fn schema_id() -> u16 {
196 SBE_SCHEMA_ID
197 }
198
199 #[must_use]
201 pub const fn schema_version() -> u16 {
202 SBE_SCHEMA_VERSION
203 }
204
205 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
211 where
212 P: Serialize + ?Sized,
213 {
214 self.request(Method::GET, path, params, false, false).await
215 }
216
217 pub async fn get_signed<P>(
223 &self,
224 path: &str,
225 params: Option<&P>,
226 ) -> BinanceSpotHttpResult<Vec<u8>>
227 where
228 P: Serialize + ?Sized,
229 {
230 self.request(Method::GET, path, params, true, false).await
231 }
232
233 pub async fn post_signed<P>(
239 &self,
240 path: &str,
241 params: Option<&P>,
242 ) -> BinanceSpotHttpResult<Vec<u8>>
243 where
244 P: Serialize + ?Sized,
245 {
246 self.request(Method::POST, path, params, true, true).await
247 }
248
249 pub async fn delete_signed<P>(
255 &self,
256 path: &str,
257 params: Option<&P>,
258 ) -> BinanceSpotHttpResult<Vec<u8>>
259 where
260 P: Serialize + ?Sized,
261 {
262 self.request(Method::DELETE, path, params, true, true).await
263 }
264
265 async fn request<P>(
266 &self,
267 method: Method,
268 path: &str,
269 params: Option<&P>,
270 signed: bool,
271 use_order_quota: bool,
272 ) -> BinanceSpotHttpResult<Vec<u8>>
273 where
274 P: Serialize + ?Sized,
275 {
276 let mut query = params
277 .map(serde_urlencoded::to_string)
278 .transpose()
279 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
280 .unwrap_or_default();
281
282 let mut headers = HashMap::new();
283
284 if signed {
285 let cred = self
286 .credential
287 .as_ref()
288 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
289
290 if !query.is_empty() {
291 query.push('&');
292 }
293
294 let timestamp = Utc::now().timestamp_millis();
295 query.push_str(&format!("timestamp={timestamp}"));
296
297 if let Some(recv_window) = self.recv_window {
298 query.push_str(&format!("&recvWindow={recv_window}"));
299 }
300
301 let signature = Self::percent_encode(&cred.sign(&query));
302 query.push_str(&format!("&signature={signature}"));
303 headers.insert(
304 BINANCE_API_KEY_HEADER.to_string(),
305 cred.api_key().to_string(),
306 );
307 }
308
309 let url = self.build_url(path, &query);
310 let keys = self.rate_limit_keys(use_order_quota);
311
312 let response = self
313 .client
314 .request(
315 method,
316 url,
317 None::<&HashMap<String, Vec<String>>>,
318 Some(headers),
319 None,
320 None,
321 Some(keys),
322 )
323 .await?;
324
325 if !response.status.is_success() {
326 return self.parse_error_response(&response);
327 }
328
329 Ok(response.body.to_vec())
330 }
331
332 fn build_url(&self, path: &str, query: &str) -> String {
333 let normalized_path = if path.starts_with('/') {
334 path.to_string()
335 } else {
336 format!("/{path}")
337 };
338
339 let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
340
341 if !query.is_empty() {
342 url.push('?');
343 url.push_str(query);
344 }
345 url
346 }
347
348 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
349 if use_orders {
350 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
351 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
352 keys.extend(self.order_rate_keys.iter().cloned());
353 keys
354 } else {
355 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
356 }
357 }
358
359 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceSpotHttpResult<T> {
360 let status = response.status.as_u16();
361 let body = &response.body;
362
363 if let Ok(body_str) = std::str::from_utf8(body)
365 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
366 {
367 return Err(BinanceSpotHttpError::BinanceError {
368 code: err.code,
369 message: err.msg,
370 });
371 }
372
373 if let Some((code, message)) = Self::try_decode_sbe_error(body) {
375 return Err(BinanceSpotHttpError::BinanceError {
376 code: code.into(),
377 message,
378 });
379 }
380
381 Err(BinanceSpotHttpError::UnexpectedStatus {
382 status,
383 body: hex::encode(body),
384 })
385 }
386
387 fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
391 const HEADER_LEN: usize = 8;
392 if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
393 return None;
394 }
395
396 let buf = ReadBuf::new(body);
397
398 let header = MessageHeaderDecoder::default().wrap(buf, 0);
400 if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
401 return None;
402 }
403
404 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
406 let code = decoder.code();
407
408 let msg_coords = decoder.msg_decoder();
410 let msg_bytes = decoder.msg_slice(msg_coords);
411 let message = String::from_utf8_lossy(msg_bytes).into_owned();
412
413 Some((code, message))
414 }
415
416 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
417 let mut headers = HashMap::new();
418 headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
419 headers.insert("Accept".to_string(), "application/sbe".to_string());
420 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
421
422 if let Some(cred) = credential {
423 headers.insert(
424 BINANCE_API_KEY_HEADER.to_string(),
425 cred.api_key().to_string(),
426 );
427 }
428 headers
429 }
430
431 fn rate_limit_config() -> RateLimitConfig {
432 let quotas = BINANCE_SPOT_RATE_LIMITS;
433 let mut keyed = Vec::new();
434 let mut order_keys = Vec::new();
435 let mut default = None;
436
437 for quota in quotas {
438 if let Some(q) = Self::quota_from(quota) {
439 match quota.rate_limit_type {
440 BinanceRateLimitType::RequestWeight if default.is_none() => {
441 default = Some(q);
442 }
443 BinanceRateLimitType::Orders => {
444 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
445 order_keys.push(key.clone());
446 keyed.push((key, q));
447 }
448 _ => {}
449 }
450 }
451 }
452
453 let default_quota = default.unwrap_or_else(|| {
454 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
455 });
456
457 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
458
459 RateLimitConfig {
460 default_quota: Some(default_quota),
461 keyed_quotas: keyed,
462 order_keys,
463 }
464 }
465
466 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
467 let burst = NonZeroU32::new(quota.limit)?;
468 match quota.interval {
469 BinanceRateLimitInterval::Second => Quota::per_second(burst),
470 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
471 BinanceRateLimitInterval::Day => {
472 Quota::with_period(std::time::Duration::from_secs(SECONDS_IN_DAY))
473 .map(|q| q.allow_burst(burst))
474 }
475 BinanceRateLimitInterval::Unknown => None,
476 }
477 }
478
479 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
485 let bytes = self.get("ping", None::<&()>).await?;
486 parse::decode_ping(&bytes)?;
487 Ok(())
488 }
489
490 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
498 let bytes = self.get("time", None::<&()>).await?;
499 let timestamp = parse::decode_server_time(&bytes)?;
500 Ok(timestamp)
501 }
502
503 pub async fn exchange_info(
509 &self,
510 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
511 let bytes = self.get("exchangeInfo", None::<&()>).await?;
512 let info = parse::decode_exchange_info(&bytes)?;
513 Ok(info)
514 }
515
516 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
522 let bytes = self.get("depth", Some(params)).await?;
523 let depth = parse::decode_depth(&bytes)?;
524 Ok(depth)
525 }
526
527 pub async fn trades(
533 &self,
534 symbol: &str,
535 limit: Option<u32>,
536 ) -> BinanceSpotHttpResult<BinanceTrades> {
537 let params = TradesParams {
538 symbol: symbol.to_string(),
539 limit,
540 };
541 let bytes = self.get("trades", Some(¶ms)).await?;
542 let trades = parse::decode_trades(&bytes)?;
543 Ok(trades)
544 }
545
546 pub async fn klines(
552 &self,
553 symbol: &str,
554 interval: &str,
555 start_time: Option<i64>,
556 end_time: Option<i64>,
557 limit: Option<u32>,
558 ) -> BinanceSpotHttpResult<BinanceKlines> {
559 let params = KlinesParams {
560 symbol: symbol.to_string(),
561 interval: interval.to_string(),
562 start_time,
563 end_time,
564 time_zone: None,
565 limit,
566 };
567 let bytes = self.get("klines", Some(¶ms)).await?;
568 let klines = parse::decode_klines(&bytes)?;
569 Ok(klines)
570 }
571
572 async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
574 where
575 P: Serialize + ?Sized,
576 {
577 let query = params
578 .map(serde_urlencoded::to_string)
579 .transpose()
580 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
581 .unwrap_or_default();
582
583 let url = self.build_url(path, &query);
584 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
585
586 let response = self
587 .client
588 .request(
589 Method::GET,
590 url,
591 None::<&HashMap<String, Vec<String>>>,
592 None,
593 None,
594 None,
595 Some(keys),
596 )
597 .await?;
598
599 if !response.status.is_success() {
600 return self.parse_error_response(&response);
601 }
602
603 Ok(response.body.to_vec())
604 }
605
606 pub async fn ticker_24hr(
614 &self,
615 symbol: Option<&str>,
616 ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
617 let params = symbol.map(TickerParams::for_symbol);
618 let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
619
620 if symbol.is_some() {
622 let ticker: Ticker24hr = serde_json::from_slice(&bytes)
623 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
624 Ok(vec![ticker])
625 } else {
626 let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
627 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
628 Ok(tickers)
629 }
630 }
631
632 pub async fn ticker_price(
640 &self,
641 symbol: Option<&str>,
642 ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
643 let params = symbol.map(TickerParams::for_symbol);
644 let bytes = self.get_json("ticker/price", params.as_ref()).await?;
645
646 if symbol.is_some() {
648 let ticker: TickerPrice = serde_json::from_slice(&bytes)
649 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
650 Ok(vec![ticker])
651 } else {
652 let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
653 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
654 Ok(tickers)
655 }
656 }
657
658 pub async fn ticker_book(
666 &self,
667 symbol: Option<&str>,
668 ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
669 let params = symbol.map(TickerParams::for_symbol);
670 let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
671
672 if symbol.is_some() {
674 let ticker: BookTicker = serde_json::from_slice(&bytes)
675 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
676 Ok(vec![ticker])
677 } else {
678 let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
679 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
680 Ok(tickers)
681 }
682 }
683
684 pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
690 let params = AvgPriceParams::new(symbol);
691 let bytes = self.get_json("avgPrice", Some(¶ms)).await?;
692
693 let avg_price: AvgPrice = serde_json::from_slice(&bytes)
694 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
695 Ok(avg_price)
696 }
697
698 pub async fn get_trade_fee(
707 &self,
708 symbol: Option<&str>,
709 ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
710 let params = symbol.map(TradeFeeParams::for_symbol);
711 let bytes = self
712 .get_signed_sapi("asset/tradeFee", params.as_ref())
713 .await?;
714
715 let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
716 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
717 Ok(fees)
718 }
719
720 async fn get_signed_sapi<P>(
722 &self,
723 path: &str,
724 params: Option<&P>,
725 ) -> BinanceSpotHttpResult<Vec<u8>>
726 where
727 P: Serialize + ?Sized,
728 {
729 let cred = self
730 .credential
731 .as_ref()
732 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
733
734 let mut query = params
735 .map(serde_urlencoded::to_string)
736 .transpose()
737 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
738 .unwrap_or_default();
739
740 if !query.is_empty() {
741 query.push('&');
742 }
743
744 let timestamp = Utc::now().timestamp_millis();
745 query.push_str(&format!("timestamp={timestamp}"));
746
747 if let Some(recv_window) = self.recv_window {
748 query.push_str(&format!("&recvWindow={recv_window}"));
749 }
750
751 let signature = Self::percent_encode(&cred.sign(&query));
752 query.push_str(&format!("&signature={signature}"));
753
754 let normalized_path = if path.starts_with('/') {
756 path.to_string()
757 } else {
758 format!("/{path}")
759 };
760
761 let mut url = format!("{}/sapi/v1{}", self.base_url, normalized_path);
762
763 if !query.is_empty() {
764 url.push('?');
765 url.push_str(&query);
766 }
767
768 let mut headers = HashMap::new();
769 headers.insert(
770 BINANCE_API_KEY_HEADER.to_string(),
771 cred.api_key().to_string(),
772 );
773
774 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
775
776 let response = self
777 .client
778 .request(
779 Method::GET,
780 url,
781 None::<&HashMap<String, Vec<String>>>,
782 Some(headers),
783 None,
784 None,
785 Some(keys),
786 )
787 .await?;
788
789 if !response.status.is_success() {
790 return self.parse_error_response(&response);
791 }
792
793 Ok(response.body.to_vec())
794 }
795
796 fn percent_encode(input: &str) -> String {
798 let mut result = String::with_capacity(input.len() * 3);
799 for byte in input.bytes() {
800 match byte {
801 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
803 result.push(byte as char);
804 }
805 _ => {
806 result.push('%');
807 result.push_str(&format!("{byte:02X}"));
808 }
809 }
810 }
811 result
812 }
813
814 pub async fn batch_submit_orders(
825 &self,
826 orders: &[BatchOrderItem],
827 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
828 if orders.is_empty() {
829 return Ok(Vec::new());
830 }
831
832 if orders.len() > 5 {
833 return Err(BinanceSpotHttpError::ValidationError(
834 "Batch order limit is 5 orders maximum".to_string(),
835 ));
836 }
837
838 let batch_json = serde_json::to_string(orders)
839 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
840
841 let bytes = self
842 .batch_request(Method::POST, "batchOrders", &batch_json)
843 .await?;
844
845 let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
846 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
847
848 Ok(results)
849 }
850
851 pub async fn batch_cancel_orders(
862 &self,
863 cancels: &[BatchCancelItem],
864 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
865 if cancels.is_empty() {
866 return Ok(Vec::new());
867 }
868
869 if cancels.len() > 5 {
870 return Err(BinanceSpotHttpError::ValidationError(
871 "Batch cancel limit is 5 orders maximum".to_string(),
872 ));
873 }
874
875 let batch_json = serde_json::to_string(cancels)
876 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
877
878 let bytes = self
879 .batch_request(Method::DELETE, "batchOrders", &batch_json)
880 .await?;
881
882 let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
883 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
884
885 Ok(results)
886 }
887
888 async fn batch_request(
890 &self,
891 method: Method,
892 path: &str,
893 batch_json: &str,
894 ) -> BinanceSpotHttpResult<Vec<u8>> {
895 let cred = self
896 .credential
897 .as_ref()
898 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
899
900 let encoded_batch = Self::percent_encode(batch_json);
901 let timestamp = Utc::now().timestamp_millis();
902 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
903
904 if let Some(recv_window) = self.recv_window {
905 query.push_str(&format!("&recvWindow={recv_window}"));
906 }
907
908 let signature = Self::percent_encode(&cred.sign(&query));
909 query.push_str(&format!("&signature={signature}"));
910
911 let url = self.build_url(path, &query);
912
913 let mut headers = HashMap::new();
914 headers.insert(
915 BINANCE_API_KEY_HEADER.to_string(),
916 cred.api_key().to_string(),
917 );
918
919 let keys = self.rate_limit_keys(true);
920
921 let response = self
922 .client
923 .request(
924 method,
925 url,
926 None::<&HashMap<String, Vec<String>>>,
927 Some(headers),
928 None,
929 None,
930 Some(keys),
931 )
932 .await?;
933
934 if !response.status.is_success() {
935 return self.parse_error_response(&response);
936 }
937
938 Ok(response.body.to_vec())
939 }
940
941 pub async fn account(
947 &self,
948 params: &AccountInfoParams,
949 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
950 let bytes = self.get_signed("account", Some(params)).await?;
951 let response = parse::decode_account(&bytes)?;
952 Ok(response)
953 }
954
955 pub async fn account_trades(
961 &self,
962 symbol: &str,
963 order_id: Option<i64>,
964 start_time: Option<i64>,
965 end_time: Option<i64>,
966 limit: Option<u32>,
967 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
968 let params = AccountTradesParams {
969 symbol: symbol.to_string(),
970 order_id,
971 start_time,
972 end_time,
973 from_id: None,
974 limit,
975 };
976 let bytes = self.get_signed("myTrades", Some(¶ms)).await?;
977 let response = parse::decode_account_trades(&bytes)?;
978 Ok(response)
979 }
980
981 pub async fn query_order(
989 &self,
990 symbol: &str,
991 order_id: Option<i64>,
992 client_order_id: Option<&str>,
993 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
994 let params = QueryOrderParams {
995 symbol: symbol.to_string(),
996 order_id,
997 orig_client_order_id: client_order_id.map(|s| s.to_string()),
998 };
999 let bytes = self.get_signed("order", Some(¶ms)).await?;
1000 let response = parse::decode_order(&bytes)?;
1001 Ok(response)
1002 }
1003
1004 pub async fn open_orders(
1010 &self,
1011 symbol: Option<&str>,
1012 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1013 let params = OpenOrdersParams {
1014 symbol: symbol.map(|s| s.to_string()),
1015 };
1016 let bytes = self.get_signed("openOrders", Some(¶ms)).await?;
1017 let response = parse::decode_orders(&bytes)?;
1018 Ok(response)
1019 }
1020
1021 pub async fn all_orders(
1027 &self,
1028 symbol: &str,
1029 start_time: Option<i64>,
1030 end_time: Option<i64>,
1031 limit: Option<u32>,
1032 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1033 let params = AllOrdersParams {
1034 symbol: symbol.to_string(),
1035 order_id: None,
1036 start_time,
1037 end_time,
1038 limit,
1039 };
1040 let bytes = self.get_signed("allOrders", Some(¶ms)).await?;
1041 let response = parse::decode_orders(&bytes)?;
1042 Ok(response)
1043 }
1044
1045 async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1047 where
1048 P: Serialize + ?Sized,
1049 {
1050 self.post_signed(path, params).await
1051 }
1052
1053 async fn delete_order<P>(
1055 &self,
1056 path: &str,
1057 params: Option<&P>,
1058 ) -> BinanceSpotHttpResult<Vec<u8>>
1059 where
1060 P: Serialize + ?Sized,
1061 {
1062 self.delete_signed(path, params).await
1063 }
1064
1065 #[expect(clippy::too_many_arguments)]
1071 pub async fn new_order(
1072 &self,
1073 symbol: &str,
1074 side: BinanceSide,
1075 order_type: BinanceSpotOrderType,
1076 time_in_force: Option<BinanceTimeInForce>,
1077 quantity: Option<&str>,
1078 price: Option<&str>,
1079 client_order_id: Option<&str>,
1080 stop_price: Option<&str>,
1081 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1082 let params = NewOrderParams {
1083 symbol: symbol.to_string(),
1084 side,
1085 order_type,
1086 time_in_force,
1087 quantity: quantity.map(|s| s.to_string()),
1088 quote_order_qty: None,
1089 price: price.map(|s| s.to_string()),
1090 new_client_order_id: client_order_id.map(|s| s.to_string()),
1091 stop_price: stop_price.map(|s| s.to_string()),
1092 trailing_delta: None,
1093 iceberg_qty: None,
1094 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1095 self_trade_prevention_mode: None,
1096 strategy_id: None,
1097 strategy_type: None,
1098 };
1099 let bytes = self.post_order("order", Some(¶ms)).await?;
1100 let response = parse::decode_new_order_full(&bytes)?;
1101 Ok(response)
1102 }
1103
1104 #[expect(clippy::too_many_arguments)]
1114 pub async fn new_order_full(
1115 &self,
1116 symbol: &str,
1117 side: BinanceSide,
1118 order_type: BinanceSpotOrderType,
1119 time_in_force: Option<BinanceTimeInForce>,
1120 quantity: Option<&str>,
1121 quote_order_qty: Option<&str>,
1122 price: Option<&str>,
1123 client_order_id: Option<&str>,
1124 stop_price: Option<&str>,
1125 iceberg_qty: Option<&str>,
1126 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1127 let params = NewOrderParams {
1128 symbol: symbol.to_string(),
1129 side,
1130 order_type,
1131 time_in_force,
1132 quantity: quantity.map(|s| s.to_string()),
1133 quote_order_qty: quote_order_qty.map(|s| s.to_string()),
1134 price: price.map(|s| s.to_string()),
1135 new_client_order_id: client_order_id.map(|s| s.to_string()),
1136 stop_price: stop_price.map(|s| s.to_string()),
1137 trailing_delta: None,
1138 iceberg_qty: iceberg_qty.map(|s| s.to_string()),
1139 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1140 self_trade_prevention_mode: None,
1141 strategy_id: None,
1142 strategy_type: None,
1143 };
1144 let bytes = self.post_order("order", Some(¶ms)).await?;
1145 let response = parse::decode_new_order_full(&bytes)?;
1146 Ok(response)
1147 }
1148
1149 #[expect(clippy::too_many_arguments)]
1155 pub async fn cancel_replace_order(
1156 &self,
1157 symbol: &str,
1158 side: BinanceSide,
1159 order_type: BinanceSpotOrderType,
1160 time_in_force: Option<BinanceTimeInForce>,
1161 quantity: Option<&str>,
1162 price: Option<&str>,
1163 cancel_order_id: Option<i64>,
1164 cancel_client_order_id: Option<&str>,
1165 new_client_order_id: Option<&str>,
1166 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1167 let params = CancelReplaceOrderParams {
1168 symbol: symbol.to_string(),
1169 side,
1170 order_type,
1171 cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1172 time_in_force,
1173 quantity: quantity.map(|s| s.to_string()),
1174 quote_order_qty: None,
1175 price: price.map(|s| s.to_string()),
1176 cancel_order_id,
1177 cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1178 new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1179 stop_price: None,
1180 trailing_delta: None,
1181 iceberg_qty: None,
1182 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1183 self_trade_prevention_mode: None,
1184 };
1185 let bytes = self
1186 .post_order("order/cancelReplace", Some(¶ms))
1187 .await?;
1188 let response = parse::decode_new_order_full(&bytes)?;
1189 Ok(response)
1190 }
1191
1192 pub async fn cancel_order(
1200 &self,
1201 symbol: &str,
1202 order_id: Option<i64>,
1203 client_order_id: Option<&str>,
1204 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1205 let params = match (order_id, client_order_id) {
1206 (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1207 (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1208 (None, None) => {
1209 return Err(BinanceSpotHttpError::ValidationError(
1210 "Either order_id or client_order_id must be provided".to_string(),
1211 ));
1212 }
1213 };
1214 let bytes = self.delete_order("order", Some(¶ms)).await?;
1215 let response = parse::decode_cancel_order(&bytes)?;
1216 Ok(response)
1217 }
1218
1219 pub async fn cancel_open_orders(
1225 &self,
1226 symbol: &str,
1227 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1228 let params = CancelOpenOrdersParams::new(symbol.to_string());
1229 let bytes = self.delete_order("openOrders", Some(¶ms)).await?;
1230 let response = parse::decode_cancel_open_orders(&bytes)?;
1231 Ok(response)
1232 }
1233
1234 async fn request_with_api_key<P>(
1236 &self,
1237 method: Method,
1238 path: &str,
1239 params: Option<&P>,
1240 ) -> BinanceSpotHttpResult<Vec<u8>>
1241 where
1242 P: Serialize + ?Sized,
1243 {
1244 let cred = self
1245 .credential
1246 .as_ref()
1247 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1248
1249 let query = params
1250 .map(serde_urlencoded::to_string)
1251 .transpose()
1252 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1253 .unwrap_or_default();
1254
1255 let url = self.build_url(path, &query);
1256
1257 let mut headers = HashMap::new();
1258 headers.insert(
1259 BINANCE_API_KEY_HEADER.to_string(),
1260 cred.api_key().to_string(),
1261 );
1262
1263 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1264
1265 let response = self
1266 .client
1267 .request(
1268 method,
1269 url,
1270 None::<&HashMap<String, Vec<String>>>,
1271 Some(headers),
1272 None,
1273 None,
1274 Some(keys),
1275 )
1276 .await?;
1277
1278 if !response.status.is_success() {
1279 return self.parse_error_response(&response);
1280 }
1281
1282 Ok(response.body.to_vec())
1283 }
1284
1285 pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1294 let bytes = self
1295 .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1296 .await?;
1297
1298 let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1299 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1300
1301 Ok(response)
1302 }
1303
1304 pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1312 let params = ListenKeyParams::new(listen_key);
1313 self.request_with_api_key(Method::PUT, "userDataStream", Some(¶ms))
1314 .await?;
1315 Ok(())
1316 }
1317
1318 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1324 let params = ListenKeyParams::new(listen_key);
1325 self.request_with_api_key(Method::DELETE, "userDataStream", Some(¶ms))
1326 .await?;
1327 Ok(())
1328 }
1329}
1330
1331pub struct BinanceSpotHttpClient {
1337 inner: Arc<BinanceRawSpotHttpClient>,
1338 clock: &'static AtomicTime,
1339 instruments_cache: Arc<DashMap<Ustr, InstrumentAny>>,
1340}
1341
1342impl Clone for BinanceSpotHttpClient {
1343 fn clone(&self) -> Self {
1344 Self {
1345 inner: self.inner.clone(),
1346 clock: self.clock,
1347 instruments_cache: self.instruments_cache.clone(),
1348 }
1349 }
1350}
1351
1352impl Debug for BinanceSpotHttpClient {
1353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1354 f.debug_struct(stringify!(BinanceSpotHttpClient))
1355 .field("inner", &self.inner)
1356 .field("instruments_cached", &self.instruments_cache.len())
1357 .finish()
1358 }
1359}
1360
1361impl BinanceSpotHttpClient {
1362 #[expect(clippy::too_many_arguments)]
1368 pub fn new(
1369 environment: BinanceEnvironment,
1370 clock: &'static AtomicTime,
1371 api_key: Option<String>,
1372 api_secret: Option<String>,
1373 base_url_override: Option<String>,
1374 recv_window: Option<u64>,
1375 timeout_secs: Option<u64>,
1376 proxy_url: Option<String>,
1377 ) -> BinanceSpotHttpResult<Self> {
1378 let inner = BinanceRawSpotHttpClient::new(
1379 environment,
1380 api_key,
1381 api_secret,
1382 base_url_override,
1383 recv_window,
1384 timeout_secs,
1385 proxy_url,
1386 )?;
1387
1388 Ok(Self {
1389 inner: Arc::new(inner),
1390 clock,
1391 instruments_cache: Arc::new(DashMap::new()),
1392 })
1393 }
1394
1395 #[must_use]
1397 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
1398 &self.inner
1399 }
1400
1401 #[must_use]
1403 pub const fn schema_id() -> u16 {
1404 SBE_SCHEMA_ID
1405 }
1406
1407 #[must_use]
1409 pub const fn schema_version() -> u16 {
1410 SBE_SCHEMA_VERSION
1411 }
1412
1413 fn generate_ts_init(&self) -> UnixNanos {
1415 self.clock.get_time_ns()
1416 }
1417
1418 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
1420 self.instruments_cache
1421 .get(&symbol)
1422 .map(|entry| entry.value().clone())
1423 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
1424 }
1425
1426 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
1428 for inst in instruments {
1429 self.instruments_cache
1430 .insert(inst.raw_symbol().inner(), inst);
1431 }
1432 }
1433
1434 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1436 self.instruments_cache
1437 .insert(instrument.raw_symbol().inner(), instrument);
1438 }
1439
1440 #[must_use]
1442 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1443 self.instruments_cache
1444 .get(symbol)
1445 .map(|entry| entry.value().clone())
1446 }
1447
1448 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
1454 self.inner.ping().await
1455 }
1456
1457 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
1465 self.inner.server_time().await
1466 }
1467
1468 pub async fn exchange_info(
1474 &self,
1475 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
1476 self.inner.exchange_info().await
1477 }
1478
1479 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
1488 let info = self.exchange_info().await?;
1489 let ts_init = self.generate_ts_init();
1490
1491 let mut instruments = Vec::with_capacity(info.symbols.len());
1492 for symbol in &info.symbols {
1493 match parse_spot_instrument_sbe(symbol, ts_init, ts_init) {
1494 Ok(instrument) => instruments.push(instrument),
1495 Err(e) => {
1496 log::debug!(
1497 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1498 symbol.symbol
1499 );
1500 }
1501 }
1502 }
1503
1504 self.cache_instruments(instruments.clone());
1506
1507 log::info!("Loaded spot instruments: count={}", instruments.len());
1508 Ok(instruments)
1509 }
1510
1511 pub async fn request_trades(
1518 &self,
1519 instrument_id: InstrumentId,
1520 limit: Option<u32>,
1521 ) -> anyhow::Result<Vec<TradeTick>> {
1522 let symbol = instrument_id.symbol.inner();
1523 let instrument = self.instrument_from_cache(symbol)?;
1524 let ts_init = self.generate_ts_init();
1525
1526 let trades = self
1527 .inner
1528 .trades(symbol.as_str(), limit)
1529 .await
1530 .map_err(|e| anyhow::anyhow!(e))?;
1531
1532 parse_spot_trades_sbe(&trades, &instrument, ts_init)
1533 }
1534
1535 pub async fn request_bars(
1542 &self,
1543 bar_type: BarType,
1544 start: Option<DateTime<Utc>>,
1545 end: Option<DateTime<Utc>>,
1546 limit: Option<u32>,
1547 ) -> anyhow::Result<Vec<Bar>> {
1548 anyhow::ensure!(
1549 bar_type.aggregation_source() == AggregationSource::External,
1550 "Only EXTERNAL aggregation is supported"
1551 );
1552
1553 let spec = bar_type.spec();
1554 let step = spec.step.get();
1555 let interval = match spec.aggregation {
1556 BarAggregation::Second => {
1557 anyhow::bail!("Binance Spot does not support second-level kline intervals")
1558 }
1559 BarAggregation::Minute => format!("{step}m"),
1560 BarAggregation::Hour => format!("{step}h"),
1561 BarAggregation::Day => format!("{step}d"),
1562 BarAggregation::Week => format!("{step}w"),
1563 BarAggregation::Month => format!("{step}M"),
1564 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
1565 };
1566
1567 let symbol = bar_type.instrument_id().symbol;
1568 let instrument = self.instrument_from_cache(symbol.inner())?;
1569 let ts_init = self.generate_ts_init();
1570
1571 let klines = self
1572 .inner
1573 .klines(
1574 symbol.as_str(),
1575 &interval,
1576 start.map(|dt| dt.timestamp_millis()),
1577 end.map(|dt| dt.timestamp_millis()),
1578 limit,
1579 )
1580 .await
1581 .map_err(|e| anyhow::anyhow!(e))?;
1582
1583 parse_klines_to_bars(&klines, bar_type, &instrument, ts_init)
1584 }
1585
1586 pub async fn request_account_state(
1592 &self,
1593 account_id: AccountId,
1594 ) -> anyhow::Result<AccountState> {
1595 let ts_init = self.clock.get_time_ns();
1596 let params = AccountInfoParams::default();
1597 let account_info = self.inner.account(¶ms).await?;
1598 Ok(account_info.to_account_state(account_id, ts_init))
1599 }
1600
1601 pub async fn request_order_status_report(
1610 &self,
1611 account_id: AccountId,
1612 instrument_id: InstrumentId,
1613 venue_order_id: Option<VenueOrderId>,
1614 client_order_id: Option<ClientOrderId>,
1615 ) -> anyhow::Result<OrderStatusReport> {
1616 anyhow::ensure!(
1617 venue_order_id.is_some() || client_order_id.is_some(),
1618 "Either venue_order_id or client_order_id must be provided"
1619 );
1620
1621 let symbol = instrument_id.symbol.inner();
1622 let instrument = self.instrument_from_cache(symbol)?;
1623 let ts_init = self.generate_ts_init();
1624
1625 let order_id = venue_order_id
1626 .map(|id| id.inner().parse::<i64>())
1627 .transpose()
1628 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1629
1630 let client_id_str =
1631 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
1632
1633 let order = self
1634 .inner
1635 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
1636 .await
1637 .map_err(|e| anyhow::anyhow!(e))?;
1638
1639 parse_order_status_report_sbe(
1640 &order,
1641 account_id,
1642 &instrument,
1643 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1644 ts_init,
1645 )
1646 }
1647
1648 pub async fn request_order_status_reports(
1658 &self,
1659 account_id: AccountId,
1660 instrument_id: Option<InstrumentId>,
1661 start: Option<DateTime<Utc>>,
1662 end: Option<DateTime<Utc>>,
1663 open_only: bool,
1664 limit: Option<u32>,
1665 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1666 let ts_init = self.generate_ts_init();
1667 let symbol = instrument_id.map(|id| id.symbol.to_string());
1668
1669 let orders = if open_only {
1670 self.inner
1671 .open_orders(symbol.as_deref())
1672 .await
1673 .map_err(|e| anyhow::anyhow!(e))?
1674 } else {
1675 let symbol = symbol
1676 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
1677 self.inner
1678 .all_orders(
1679 &symbol,
1680 start.map(|dt| dt.timestamp_millis()),
1681 end.map(|dt| dt.timestamp_millis()),
1682 limit,
1683 )
1684 .await
1685 .map_err(|e| anyhow::anyhow!(e))?
1686 };
1687
1688 orders
1689 .iter()
1690 .map(|order| {
1691 let symbol = Ustr::from(&order.symbol);
1692 let instrument = self.instrument_from_cache(symbol)?;
1693 parse_order_status_report_sbe(
1694 order,
1695 account_id,
1696 &instrument,
1697 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1698 ts_init,
1699 )
1700 })
1701 .collect()
1702 }
1703
1704 pub async fn request_fill_reports(
1711 &self,
1712 account_id: AccountId,
1713 instrument_id: InstrumentId,
1714 venue_order_id: Option<VenueOrderId>,
1715 start: Option<DateTime<Utc>>,
1716 end: Option<DateTime<Utc>>,
1717 limit: Option<u32>,
1718 ) -> anyhow::Result<Vec<FillReport>> {
1719 let ts_init = self.generate_ts_init();
1720 let symbol = instrument_id.symbol.inner();
1721
1722 let order_id = venue_order_id
1723 .map(|id| id.inner().parse::<i64>())
1724 .transpose()
1725 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1726
1727 let trades = self
1728 .inner
1729 .account_trades(
1730 symbol.as_str(),
1731 order_id,
1732 start.map(|dt| dt.timestamp_millis()),
1733 end.map(|dt| dt.timestamp_millis()),
1734 limit,
1735 )
1736 .await
1737 .map_err(|e| anyhow::anyhow!(e))?;
1738
1739 trades
1740 .iter()
1741 .map(|trade| {
1742 let symbol = Ustr::from(&trade.symbol);
1743 let instrument = self.instrument_from_cache(symbol)?;
1744 let commission_currency = get_currency(&trade.commission_asset);
1745 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
1746 })
1747 .collect()
1748 }
1749
1750 #[expect(clippy::too_many_arguments)]
1763 pub async fn submit_order(
1764 &self,
1765 account_id: AccountId,
1766 instrument_id: InstrumentId,
1767 client_order_id: ClientOrderId,
1768 order_side: OrderSide,
1769 order_type: OrderType,
1770 quantity: Quantity,
1771 time_in_force: TimeInForce,
1772 price: Option<Price>,
1773 trigger_price: Option<Price>,
1774 post_only: bool,
1775 quote_quantity: bool,
1776 display_qty: Option<Quantity>,
1777 ) -> anyhow::Result<OrderStatusReport> {
1778 let symbol = instrument_id.symbol.inner();
1779 let instrument = self.instrument_from_cache(symbol)?;
1780 let ts_init = self.generate_ts_init();
1781
1782 let binance_side = BinanceSide::try_from(order_side)?;
1783 let binance_order_type = order_type_to_binance_spot(order_type, post_only)?;
1784
1785 let requires_trigger = matches!(
1787 order_type,
1788 OrderType::StopMarket
1789 | OrderType::StopLimit
1790 | OrderType::MarketIfTouched
1791 | OrderType::LimitIfTouched
1792 );
1793
1794 if requires_trigger && trigger_price.is_none() {
1795 anyhow::bail!("Conditional orders require a trigger price");
1796 }
1797
1798 let requires_price = matches!(
1800 binance_order_type,
1801 BinanceSpotOrderType::Limit
1802 | BinanceSpotOrderType::StopLossLimit
1803 | BinanceSpotOrderType::TakeProfitLimit
1804 | BinanceSpotOrderType::LimitMaker
1805 );
1806
1807 if requires_price && price.is_none() {
1808 anyhow::bail!("{binance_order_type:?} orders require a price");
1809 }
1810
1811 let supports_tif = matches!(
1813 binance_order_type,
1814 BinanceSpotOrderType::Limit
1815 | BinanceSpotOrderType::StopLossLimit
1816 | BinanceSpotOrderType::TakeProfitLimit
1817 );
1818 let binance_tif = if supports_tif {
1819 Some(time_in_force_to_binance_spot(time_in_force)?)
1820 } else {
1821 None
1822 };
1823
1824 let qty_str = quantity.to_string();
1825 let price_str = price.map(|p| p.to_string());
1826 let stop_price_str = trigger_price.map(|p| p.to_string());
1827 let iceberg_qty_str = display_qty.map(|q| q.to_string());
1828 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
1829
1830 if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
1831 anyhow::bail!("quoteOrderQty is only supported for MARKET orders");
1832 }
1833
1834 let (base_qty, quote_qty) = if quote_quantity {
1835 (None, Some(qty_str.as_str()))
1836 } else {
1837 (Some(qty_str.as_str()), None)
1838 };
1839
1840 let response = self
1841 .inner
1842 .new_order_full(
1843 symbol.as_str(),
1844 binance_side,
1845 binance_order_type,
1846 binance_tif,
1847 base_qty,
1848 quote_qty,
1849 price_str.as_deref(),
1850 Some(&client_id_str),
1851 stop_price_str.as_deref(),
1852 iceberg_qty_str.as_deref(),
1853 )
1854 .await?;
1855
1856 parse_new_order_response_sbe(
1857 &response,
1858 account_id,
1859 &instrument,
1860 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1861 ts_init,
1862 )
1863 }
1864
1865 pub async fn submit_order_list(
1873 &self,
1874 orders: &[BatchOrderItem],
1875 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1876 self.inner.batch_submit_orders(orders).await
1877 }
1878
1879 #[expect(clippy::too_many_arguments)]
1888 pub async fn modify_order(
1889 &self,
1890 account_id: AccountId,
1891 instrument_id: InstrumentId,
1892 venue_order_id: VenueOrderId,
1893 client_order_id: ClientOrderId,
1894 order_side: OrderSide,
1895 order_type: OrderType,
1896 quantity: Quantity,
1897 time_in_force: TimeInForce,
1898 price: Option<Price>,
1899 ) -> anyhow::Result<OrderStatusReport> {
1900 let symbol = instrument_id.symbol.inner();
1901 let instrument = self.instrument_from_cache(symbol)?;
1902 let ts_init = self.generate_ts_init();
1903
1904 let binance_side = BinanceSide::try_from(order_side)?;
1905 let binance_order_type = order_type_to_binance_spot(order_type, false)?;
1906 let binance_tif = time_in_force_to_binance_spot(time_in_force)?;
1907
1908 let cancel_order_id: i64 = venue_order_id
1909 .inner()
1910 .parse()
1911 .map_err(|_| anyhow::anyhow!("Invalid venue order ID: {venue_order_id}"))?;
1912
1913 let qty_str = quantity.to_string();
1914 let price_str = price.map(|p| p.to_string());
1915 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
1916
1917 let response = self
1918 .inner
1919 .cancel_replace_order(
1920 symbol.as_str(),
1921 binance_side,
1922 binance_order_type,
1923 Some(binance_tif),
1924 Some(&qty_str),
1925 price_str.as_deref(),
1926 Some(cancel_order_id),
1927 None,
1928 Some(&client_id_str),
1929 )
1930 .await
1931 .map_err(|e| anyhow::anyhow!(e))?;
1932
1933 parse_new_order_response_sbe(
1934 &response,
1935 account_id,
1936 &instrument,
1937 BINANCE_NAUTILUS_SPOT_BROKER_ID,
1938 ts_init,
1939 )
1940 }
1941
1942 pub async fn cancel_order(
1950 &self,
1951 instrument_id: InstrumentId,
1952 venue_order_id: Option<VenueOrderId>,
1953 client_order_id: Option<ClientOrderId>,
1954 ) -> anyhow::Result<VenueOrderId> {
1955 let symbol = instrument_id.symbol.inner();
1956
1957 let order_id = venue_order_id
1958 .map(|id| id.inner().parse::<i64>())
1959 .transpose()
1960 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1961
1962 let client_id_str =
1963 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
1964
1965 let response = self
1966 .inner
1967 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
1968 .await
1969 .map_err(|e| anyhow::anyhow!(e))?;
1970
1971 Ok(VenueOrderId::new(response.order_id.to_string()))
1972 }
1973
1974 pub async fn batch_cancel_orders(
1982 &self,
1983 cancels: &[BatchCancelItem],
1984 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
1985 self.inner.batch_cancel_orders(cancels).await
1986 }
1987
1988 pub async fn cancel_all_orders(
1996 &self,
1997 instrument_id: InstrumentId,
1998 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
1999 let symbol = instrument_id.symbol.inner();
2000
2001 let responses = self
2002 .inner
2003 .cancel_open_orders(symbol.as_str())
2004 .await
2005 .map_err(|e| anyhow::anyhow!(e))?;
2006
2007 Ok(responses
2008 .into_iter()
2009 .map(|r| {
2010 (
2011 VenueOrderId::new(r.order_id.to_string()),
2012 ClientOrderId::new(decode_broker_id(
2013 &r.orig_client_order_id,
2014 BINANCE_NAUTILUS_SPOT_BROKER_ID,
2015 )),
2016 )
2017 })
2018 .collect())
2019 }
2020}
2021
2022#[cfg(test)]
2023mod tests {
2024 use rstest::rstest;
2025
2026 use super::*;
2027
2028 #[rstest]
2029 fn test_schema_constants() {
2030 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
2031 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 3);
2032 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
2033 assert_eq!(BinanceSpotHttpClient::schema_version(), 3);
2034 }
2035
2036 #[rstest]
2037 fn test_sbe_schema_header() {
2038 assert_eq!(SBE_SCHEMA_HEADER, "3:3");
2039 }
2040
2041 #[rstest]
2042 fn test_default_headers_include_sbe() {
2043 let headers = BinanceRawSpotHttpClient::default_headers(&None);
2044
2045 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
2046 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:3".to_string()));
2047 }
2048
2049 #[rstest]
2050 fn test_rate_limit_config() {
2051 let config = BinanceRawSpotHttpClient::rate_limit_config();
2052
2053 assert!(config.default_quota.is_some());
2054 assert_eq!(config.order_keys.len(), 2);
2056 }
2057
2058 #[rstest]
2059 fn test_quota_from_unknown_interval_returns_none() {
2060 let quota = BinanceRateLimitQuota {
2061 rate_limit_type: BinanceRateLimitType::Orders,
2062 interval: BinanceRateLimitInterval::Unknown,
2063 interval_num: 1,
2064 limit: 10,
2065 };
2066
2067 assert!(BinanceRawSpotHttpClient::quota_from("a).is_none());
2068 }
2069}