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, nanos::UnixNanos, time::get_atomic_clock_realtime,
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::{BINANCE_SPOT_RATE_LIMITS, BinanceRateLimitQuota},
76 credential::Credential,
77 enums::{
78 BinanceEnvironment, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
79 BinanceSide, BinanceTimeInForce,
80 },
81 models::BinanceErrorResponse,
82 parse::{
83 get_currency, parse_fill_report_sbe, parse_klines_to_bars,
84 parse_new_order_response_sbe, parse_order_status_report_sbe, parse_spot_instrument_sbe,
85 parse_spot_trades_sbe,
86 },
87 sbe::spot::{
88 ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
89 error_response_codec::{self, ErrorResponseDecoder},
90 message_header_codec::MessageHeaderDecoder,
91 },
92 urls::get_http_base_url,
93 },
94 spot::enums::{
95 BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
96 order_type_to_binance_spot,
97 },
98};
99
100pub const SBE_SCHEMA_HEADER: &str = "3:2";
102
103const SPOT_API_PATH: &str = "/api/v3";
105
106const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
108
109const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
111
112struct RateLimitConfig {
113 default_quota: Option<Quota>,
114 keyed_quotas: Vec<(String, Quota)>,
115 order_keys: Vec<String>,
116}
117
118#[derive(Debug, Clone)]
129pub struct BinanceRawSpotHttpClient {
130 client: HttpClient,
131 base_url: String,
132 credential: Option<Credential>,
133 recv_window: Option<u64>,
134 order_rate_keys: Vec<String>,
135}
136
137impl BinanceRawSpotHttpClient {
138 pub fn new(
144 environment: BinanceEnvironment,
145 api_key: Option<String>,
146 api_secret: Option<String>,
147 base_url_override: Option<String>,
148 recv_window: Option<u64>,
149 timeout_secs: Option<u64>,
150 proxy_url: Option<String>,
151 ) -> BinanceSpotHttpResult<Self> {
152 let RateLimitConfig {
153 default_quota,
154 keyed_quotas,
155 order_keys,
156 } = Self::rate_limit_config();
157
158 let credential = match (api_key, api_secret) {
159 (Some(key), Some(secret)) => Some(Credential::new(key, secret)),
160 (None, None) => None,
161 _ => return Err(BinanceSpotHttpError::MissingCredentials),
162 };
163
164 let base_url = base_url_override.unwrap_or_else(|| {
165 get_http_base_url(BinanceProductType::Spot, environment).to_string()
166 });
167
168 let headers = Self::default_headers(&credential);
169
170 let client = HttpClient::new(
171 headers,
172 vec!["X-MBX-APIKEY".to_string()],
173 keyed_quotas,
174 default_quota,
175 timeout_secs,
176 proxy_url,
177 )?;
178
179 Ok(Self {
180 client,
181 base_url,
182 credential,
183 recv_window,
184 order_rate_keys: order_keys,
185 })
186 }
187
188 #[must_use]
190 pub const fn schema_id() -> u16 {
191 SBE_SCHEMA_ID
192 }
193
194 #[must_use]
196 pub const fn schema_version() -> u16 {
197 SBE_SCHEMA_VERSION
198 }
199
200 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
206 where
207 P: Serialize + ?Sized,
208 {
209 self.request(Method::GET, path, params, false, false).await
210 }
211
212 pub async fn get_signed<P>(
218 &self,
219 path: &str,
220 params: Option<&P>,
221 ) -> BinanceSpotHttpResult<Vec<u8>>
222 where
223 P: Serialize + ?Sized,
224 {
225 self.request(Method::GET, path, params, true, false).await
226 }
227
228 async fn request<P>(
229 &self,
230 method: Method,
231 path: &str,
232 params: Option<&P>,
233 signed: bool,
234 use_order_quota: bool,
235 ) -> BinanceSpotHttpResult<Vec<u8>>
236 where
237 P: Serialize + ?Sized,
238 {
239 let mut query = params
240 .map(serde_urlencoded::to_string)
241 .transpose()
242 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
243 .unwrap_or_default();
244
245 let mut headers = HashMap::new();
246 if signed {
247 let cred = self
248 .credential
249 .as_ref()
250 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
251
252 if !query.is_empty() {
253 query.push('&');
254 }
255
256 let timestamp = Utc::now().timestamp_millis();
257 query.push_str(&format!("timestamp={timestamp}"));
258
259 if let Some(recv_window) = self.recv_window {
260 query.push_str(&format!("&recvWindow={recv_window}"));
261 }
262
263 let signature = cred.sign(&query);
264 query.push_str(&format!("&signature={signature}"));
265 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
266 }
267
268 let url = self.build_url(path, &query);
269 let keys = self.rate_limit_keys(use_order_quota);
270
271 let response = self
272 .client
273 .request(
274 method,
275 url,
276 None::<&HashMap<String, Vec<String>>>,
277 Some(headers),
278 None,
279 None,
280 Some(keys),
281 )
282 .await?;
283
284 if !response.status.is_success() {
285 return self.parse_error_response(response);
286 }
287
288 Ok(response.body.to_vec())
289 }
290
291 fn build_url(&self, path: &str, query: &str) -> String {
292 let normalized_path = if path.starts_with('/') {
293 path.to_string()
294 } else {
295 format!("/{path}")
296 };
297
298 let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
299 if !query.is_empty() {
300 url.push('?');
301 url.push_str(query);
302 }
303 url
304 }
305
306 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
307 if use_orders {
308 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
309 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
310 keys.extend(self.order_rate_keys.iter().cloned());
311 keys
312 } else {
313 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
314 }
315 }
316
317 fn parse_error_response<T>(&self, response: HttpResponse) -> BinanceSpotHttpResult<T> {
318 let status = response.status.as_u16();
319 let body = &response.body;
320
321 if let Ok(body_str) = std::str::from_utf8(body)
323 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
324 {
325 return Err(BinanceSpotHttpError::BinanceError {
326 code: err.code,
327 message: err.msg,
328 });
329 }
330
331 if let Some((code, message)) = Self::try_decode_sbe_error(body) {
333 return Err(BinanceSpotHttpError::BinanceError {
334 code: code.into(),
335 message,
336 });
337 }
338
339 Err(BinanceSpotHttpError::UnexpectedStatus {
340 status,
341 body: hex::encode(body),
342 })
343 }
344
345 fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
349 const HEADER_LEN: usize = 8;
350 if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
351 return None;
352 }
353
354 let buf = ReadBuf::new(body);
355
356 let header = MessageHeaderDecoder::default().wrap(buf, 0);
358 if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
359 return None;
360 }
361
362 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
364 let code = decoder.code();
365
366 let msg_coords = decoder.msg_decoder();
368 let msg_bytes = decoder.msg_slice(msg_coords);
369 let message = String::from_utf8_lossy(msg_bytes).into_owned();
370
371 Some((code, message))
372 }
373
374 fn default_headers(credential: &Option<Credential>) -> HashMap<String, String> {
375 let mut headers = HashMap::new();
376 headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
377 headers.insert("Accept".to_string(), "application/sbe".to_string());
378 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
379 if let Some(cred) = credential {
380 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
381 }
382 headers
383 }
384
385 fn rate_limit_config() -> RateLimitConfig {
386 let quotas = BINANCE_SPOT_RATE_LIMITS;
387 let mut keyed = Vec::new();
388 let mut order_keys = Vec::new();
389 let mut default = None;
390
391 for quota in quotas {
392 if let Some(q) = Self::quota_from(quota) {
393 match quota.rate_limit_type {
394 BinanceRateLimitType::RequestWeight if default.is_none() => {
395 default = Some(q);
396 }
397 BinanceRateLimitType::Orders => {
398 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
399 order_keys.push(key.clone());
400 keyed.push((key, q));
401 }
402 _ => {}
403 }
404 }
405 }
406
407 let default_quota =
408 default.unwrap_or_else(|| Quota::per_second(NonZeroU32::new(10).unwrap()));
409
410 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
411
412 RateLimitConfig {
413 default_quota: Some(default_quota),
414 keyed_quotas: keyed,
415 order_keys,
416 }
417 }
418
419 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
420 let burst = NonZeroU32::new(quota.limit)?;
421 match quota.interval {
422 BinanceRateLimitInterval::Second => Some(Quota::per_second(burst)),
423 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
424 BinanceRateLimitInterval::Day => {
425 Quota::with_period(std::time::Duration::from_secs(86_400))
426 .map(|q| q.allow_burst(burst))
427 }
428 }
429 }
430
431 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
437 let bytes = self.get("ping", None::<&()>).await?;
438 parse::decode_ping(&bytes)?;
439 Ok(())
440 }
441
442 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
450 let bytes = self.get("time", None::<&()>).await?;
451 let timestamp = parse::decode_server_time(&bytes)?;
452 Ok(timestamp)
453 }
454
455 pub async fn exchange_info(
461 &self,
462 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
463 let bytes = self.get("exchangeInfo", None::<&()>).await?;
464 let info = parse::decode_exchange_info(&bytes)?;
465 Ok(info)
466 }
467
468 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
474 let bytes = self.get("depth", Some(params)).await?;
475 let depth = parse::decode_depth(&bytes)?;
476 Ok(depth)
477 }
478
479 pub async fn trades(
485 &self,
486 symbol: &str,
487 limit: Option<u32>,
488 ) -> BinanceSpotHttpResult<BinanceTrades> {
489 let params = TradesParams {
490 symbol: symbol.to_string(),
491 limit,
492 };
493 let bytes = self.get("trades", Some(¶ms)).await?;
494 let trades = parse::decode_trades(&bytes)?;
495 Ok(trades)
496 }
497
498 pub async fn klines(
504 &self,
505 symbol: &str,
506 interval: &str,
507 start_time: Option<i64>,
508 end_time: Option<i64>,
509 limit: Option<u32>,
510 ) -> BinanceSpotHttpResult<BinanceKlines> {
511 let params = KlinesParams {
512 symbol: symbol.to_string(),
513 interval: interval.to_string(),
514 start_time,
515 end_time,
516 time_zone: None,
517 limit,
518 };
519 let bytes = self.get("klines", Some(¶ms)).await?;
520 let klines = parse::decode_klines(&bytes)?;
521 Ok(klines)
522 }
523
524 async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
526 where
527 P: Serialize + ?Sized,
528 {
529 let query = params
530 .map(serde_urlencoded::to_string)
531 .transpose()
532 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
533 .unwrap_or_default();
534
535 let url = self.build_url(path, &query);
536 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
537
538 let response = self
539 .client
540 .request(
541 Method::GET,
542 url,
543 None::<&HashMap<String, Vec<String>>>,
544 None,
545 None,
546 None,
547 Some(keys),
548 )
549 .await?;
550
551 if !response.status.is_success() {
552 return self.parse_error_response(response);
553 }
554
555 Ok(response.body.to_vec())
556 }
557
558 pub async fn ticker_24hr(
566 &self,
567 symbol: Option<&str>,
568 ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
569 let params = symbol.map(TickerParams::for_symbol);
570 let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
571
572 if symbol.is_some() {
574 let ticker: Ticker24hr = serde_json::from_slice(&bytes)
575 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
576 Ok(vec![ticker])
577 } else {
578 let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
579 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
580 Ok(tickers)
581 }
582 }
583
584 pub async fn ticker_price(
592 &self,
593 symbol: Option<&str>,
594 ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
595 let params = symbol.map(TickerParams::for_symbol);
596 let bytes = self.get_json("ticker/price", params.as_ref()).await?;
597
598 if symbol.is_some() {
600 let ticker: TickerPrice = serde_json::from_slice(&bytes)
601 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
602 Ok(vec![ticker])
603 } else {
604 let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
605 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
606 Ok(tickers)
607 }
608 }
609
610 pub async fn ticker_book(
618 &self,
619 symbol: Option<&str>,
620 ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
621 let params = symbol.map(TickerParams::for_symbol);
622 let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
623
624 if symbol.is_some() {
626 let ticker: BookTicker = serde_json::from_slice(&bytes)
627 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
628 Ok(vec![ticker])
629 } else {
630 let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
631 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
632 Ok(tickers)
633 }
634 }
635
636 pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
642 let params = AvgPriceParams::new(symbol);
643 let bytes = self.get_json("avgPrice", Some(¶ms)).await?;
644
645 let avg_price: AvgPrice = serde_json::from_slice(&bytes)
646 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
647 Ok(avg_price)
648 }
649
650 pub async fn get_trade_fee(
659 &self,
660 symbol: Option<&str>,
661 ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
662 let params = symbol.map(TradeFeeParams::for_symbol);
663 let bytes = self
664 .get_signed_sapi("asset/tradeFee", params.as_ref())
665 .await?;
666
667 let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
668 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
669 Ok(fees)
670 }
671
672 async fn get_signed_sapi<P>(
674 &self,
675 path: &str,
676 params: Option<&P>,
677 ) -> BinanceSpotHttpResult<Vec<u8>>
678 where
679 P: Serialize + ?Sized,
680 {
681 let cred = self
682 .credential
683 .as_ref()
684 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
685
686 let mut query = params
687 .map(serde_urlencoded::to_string)
688 .transpose()
689 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
690 .unwrap_or_default();
691
692 if !query.is_empty() {
693 query.push('&');
694 }
695
696 let timestamp = Utc::now().timestamp_millis();
697 query.push_str(&format!("timestamp={timestamp}"));
698
699 if let Some(recv_window) = self.recv_window {
700 query.push_str(&format!("&recvWindow={recv_window}"));
701 }
702
703 let signature = cred.sign(&query);
704 query.push_str(&format!("&signature={signature}"));
705
706 let normalized_path = if path.starts_with('/') {
708 path.to_string()
709 } else {
710 format!("/{path}")
711 };
712
713 let mut url = format!("{}/sapi/v1{}", self.base_url, normalized_path);
714 if !query.is_empty() {
715 url.push('?');
716 url.push_str(&query);
717 }
718
719 let mut headers = HashMap::new();
720 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
721
722 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
723
724 let response = self
725 .client
726 .request(
727 Method::GET,
728 url,
729 None::<&HashMap<String, Vec<String>>>,
730 Some(headers),
731 None,
732 None,
733 Some(keys),
734 )
735 .await?;
736
737 if !response.status.is_success() {
738 return self.parse_error_response(response);
739 }
740
741 Ok(response.body.to_vec())
742 }
743
744 fn percent_encode(input: &str) -> String {
746 let mut result = String::with_capacity(input.len() * 3);
747 for byte in input.bytes() {
748 match byte {
749 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
751 result.push(byte as char);
752 }
753 _ => {
754 result.push('%');
755 result.push_str(&format!("{byte:02X}"));
756 }
757 }
758 }
759 result
760 }
761
762 pub async fn batch_submit_orders(
773 &self,
774 orders: &[BatchOrderItem],
775 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
776 if orders.is_empty() {
777 return Ok(Vec::new());
778 }
779
780 if orders.len() > 5 {
781 return Err(BinanceSpotHttpError::ValidationError(
782 "Batch order limit is 5 orders maximum".to_string(),
783 ));
784 }
785
786 let batch_json = serde_json::to_string(orders)
787 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
788
789 let bytes = self
790 .batch_request(Method::POST, "batchOrders", &batch_json)
791 .await?;
792
793 let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
794 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
795
796 Ok(results)
797 }
798
799 pub async fn batch_cancel_orders(
810 &self,
811 cancels: &[BatchCancelItem],
812 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
813 if cancels.is_empty() {
814 return Ok(Vec::new());
815 }
816
817 if cancels.len() > 5 {
818 return Err(BinanceSpotHttpError::ValidationError(
819 "Batch cancel limit is 5 orders maximum".to_string(),
820 ));
821 }
822
823 let batch_json = serde_json::to_string(cancels)
824 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
825
826 let bytes = self
827 .batch_request(Method::DELETE, "batchOrders", &batch_json)
828 .await?;
829
830 let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
831 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
832
833 Ok(results)
834 }
835
836 async fn batch_request(
838 &self,
839 method: Method,
840 path: &str,
841 batch_json: &str,
842 ) -> BinanceSpotHttpResult<Vec<u8>> {
843 let cred = self
844 .credential
845 .as_ref()
846 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
847
848 let encoded_batch = Self::percent_encode(batch_json);
849 let timestamp = Utc::now().timestamp_millis();
850 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
851
852 if let Some(recv_window) = self.recv_window {
853 query.push_str(&format!("&recvWindow={recv_window}"));
854 }
855
856 let signature = cred.sign(&query);
857 query.push_str(&format!("&signature={signature}"));
858
859 let url = self.build_url(path, &query);
860
861 let mut headers = HashMap::new();
862 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
863
864 let keys = self.rate_limit_keys(true);
865
866 let response = self
867 .client
868 .request(
869 method,
870 url,
871 None::<&HashMap<String, Vec<String>>>,
872 Some(headers),
873 None,
874 None,
875 Some(keys),
876 )
877 .await?;
878
879 if !response.status.is_success() {
880 return self.parse_error_response(response);
881 }
882
883 Ok(response.body.to_vec())
884 }
885
886 pub async fn account(
892 &self,
893 params: &AccountInfoParams,
894 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
895 let bytes = self.get_signed("account", Some(params)).await?;
896 let response = parse::decode_account(&bytes)?;
897 Ok(response)
898 }
899
900 pub async fn account_trades(
906 &self,
907 symbol: &str,
908 order_id: Option<i64>,
909 start_time: Option<i64>,
910 end_time: Option<i64>,
911 limit: Option<u32>,
912 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
913 let params = AccountTradesParams {
914 symbol: symbol.to_string(),
915 order_id,
916 start_time,
917 end_time,
918 from_id: None,
919 limit,
920 };
921 let bytes = self.get_signed("myTrades", Some(¶ms)).await?;
922 let response = parse::decode_account_trades(&bytes)?;
923 Ok(response)
924 }
925
926 pub async fn query_order(
934 &self,
935 symbol: &str,
936 order_id: Option<i64>,
937 client_order_id: Option<&str>,
938 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
939 let params = QueryOrderParams {
940 symbol: symbol.to_string(),
941 order_id,
942 orig_client_order_id: client_order_id.map(|s| s.to_string()),
943 };
944 let bytes = self.get_signed("order", Some(¶ms)).await?;
945 let response = parse::decode_order(&bytes)?;
946 Ok(response)
947 }
948
949 pub async fn open_orders(
955 &self,
956 symbol: Option<&str>,
957 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
958 let params = OpenOrdersParams {
959 symbol: symbol.map(|s| s.to_string()),
960 };
961 let bytes = self.get_signed("openOrders", Some(¶ms)).await?;
962 let response = parse::decode_orders(&bytes)?;
963 Ok(response)
964 }
965
966 pub async fn all_orders(
972 &self,
973 symbol: &str,
974 start_time: Option<i64>,
975 end_time: Option<i64>,
976 limit: Option<u32>,
977 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
978 let params = AllOrdersParams {
979 symbol: symbol.to_string(),
980 order_id: None,
981 start_time,
982 end_time,
983 limit,
984 };
985 let bytes = self.get_signed("allOrders", Some(¶ms)).await?;
986 let response = parse::decode_orders(&bytes)?;
987 Ok(response)
988 }
989
990 async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
992 where
993 P: Serialize + ?Sized,
994 {
995 self.request(Method::POST, path, params, true, true).await
996 }
997
998 async fn delete_order<P>(
1000 &self,
1001 path: &str,
1002 params: Option<&P>,
1003 ) -> BinanceSpotHttpResult<Vec<u8>>
1004 where
1005 P: Serialize + ?Sized,
1006 {
1007 self.request(Method::DELETE, path, params, true, true).await
1008 }
1009
1010 #[allow(clippy::too_many_arguments)]
1016 pub async fn new_order(
1017 &self,
1018 symbol: &str,
1019 side: BinanceSide,
1020 order_type: BinanceSpotOrderType,
1021 time_in_force: Option<BinanceTimeInForce>,
1022 quantity: Option<&str>,
1023 price: Option<&str>,
1024 client_order_id: Option<&str>,
1025 stop_price: Option<&str>,
1026 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1027 let params = NewOrderParams {
1028 symbol: symbol.to_string(),
1029 side,
1030 order_type,
1031 time_in_force,
1032 quantity: quantity.map(|s| s.to_string()),
1033 quote_order_qty: None,
1034 price: price.map(|s| s.to_string()),
1035 new_client_order_id: client_order_id.map(|s| s.to_string()),
1036 stop_price: stop_price.map(|s| s.to_string()),
1037 trailing_delta: None,
1038 iceberg_qty: None,
1039 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1040 self_trade_prevention_mode: None,
1041 strategy_id: None,
1042 strategy_type: None,
1043 };
1044 let bytes = self.post_order("order", Some(¶ms)).await?;
1045 let response = parse::decode_new_order_full(&bytes)?;
1046 Ok(response)
1047 }
1048
1049 #[allow(clippy::too_many_arguments)]
1055 pub async fn cancel_replace_order(
1056 &self,
1057 symbol: &str,
1058 side: BinanceSide,
1059 order_type: BinanceSpotOrderType,
1060 time_in_force: Option<BinanceTimeInForce>,
1061 quantity: Option<&str>,
1062 price: Option<&str>,
1063 cancel_order_id: Option<i64>,
1064 cancel_client_order_id: Option<&str>,
1065 new_client_order_id: Option<&str>,
1066 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1067 let params = CancelReplaceOrderParams {
1068 symbol: symbol.to_string(),
1069 side,
1070 order_type,
1071 cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1072 time_in_force,
1073 quantity: quantity.map(|s| s.to_string()),
1074 quote_order_qty: None,
1075 price: price.map(|s| s.to_string()),
1076 cancel_order_id,
1077 cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1078 new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1079 stop_price: None,
1080 trailing_delta: None,
1081 iceberg_qty: None,
1082 new_order_resp_type: Some(BinanceOrderResponseType::Full),
1083 self_trade_prevention_mode: None,
1084 };
1085 let bytes = self
1086 .post_order("order/cancelReplace", Some(¶ms))
1087 .await?;
1088 let response = parse::decode_new_order_full(&bytes)?;
1089 Ok(response)
1090 }
1091
1092 pub async fn cancel_order(
1100 &self,
1101 symbol: &str,
1102 order_id: Option<i64>,
1103 client_order_id: Option<&str>,
1104 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1105 let params = match (order_id, client_order_id) {
1106 (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1107 (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1108 (None, None) => {
1109 return Err(BinanceSpotHttpError::ValidationError(
1110 "Either order_id or client_order_id must be provided".to_string(),
1111 ));
1112 }
1113 };
1114 let bytes = self.delete_order("order", Some(¶ms)).await?;
1115 let response = parse::decode_cancel_order(&bytes)?;
1116 Ok(response)
1117 }
1118
1119 pub async fn cancel_open_orders(
1125 &self,
1126 symbol: &str,
1127 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1128 let params = CancelOpenOrdersParams::new(symbol.to_string());
1129 let bytes = self.delete_order("openOrders", Some(¶ms)).await?;
1130 let response = parse::decode_cancel_open_orders(&bytes)?;
1131 Ok(response)
1132 }
1133
1134 async fn request_with_api_key<P>(
1136 &self,
1137 method: Method,
1138 path: &str,
1139 params: Option<&P>,
1140 ) -> BinanceSpotHttpResult<Vec<u8>>
1141 where
1142 P: Serialize + ?Sized,
1143 {
1144 let cred = self
1145 .credential
1146 .as_ref()
1147 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1148
1149 let query = params
1150 .map(serde_urlencoded::to_string)
1151 .transpose()
1152 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1153 .unwrap_or_default();
1154
1155 let url = self.build_url(path, &query);
1156
1157 let mut headers = HashMap::new();
1158 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
1159
1160 let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1161
1162 let response = self
1163 .client
1164 .request(
1165 method,
1166 url,
1167 None::<&HashMap<String, Vec<String>>>,
1168 Some(headers),
1169 None,
1170 None,
1171 Some(keys),
1172 )
1173 .await?;
1174
1175 if !response.status.is_success() {
1176 return self.parse_error_response(response);
1177 }
1178
1179 Ok(response.body.to_vec())
1180 }
1181
1182 pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1191 let bytes = self
1192 .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1193 .await?;
1194
1195 let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1196 .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1197
1198 Ok(response)
1199 }
1200
1201 pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1209 let params = ListenKeyParams::new(listen_key);
1210 self.request_with_api_key(Method::PUT, "userDataStream", Some(¶ms))
1211 .await?;
1212 Ok(())
1213 }
1214
1215 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1221 let params = ListenKeyParams::new(listen_key);
1222 self.request_with_api_key(Method::DELETE, "userDataStream", Some(¶ms))
1223 .await?;
1224 Ok(())
1225 }
1226}
1227
1228#[cfg_attr(
1234 feature = "python",
1235 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
1236)]
1237pub struct BinanceSpotHttpClient {
1238 inner: Arc<BinanceRawSpotHttpClient>,
1239 instruments_cache: Arc<DashMap<Ustr, InstrumentAny>>,
1240}
1241
1242impl Clone for BinanceSpotHttpClient {
1243 fn clone(&self) -> Self {
1244 Self {
1245 inner: self.inner.clone(),
1246 instruments_cache: self.instruments_cache.clone(),
1247 }
1248 }
1249}
1250
1251impl Debug for BinanceSpotHttpClient {
1252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1253 f.debug_struct(stringify!(BinanceSpotHttpClient))
1254 .field("inner", &self.inner)
1255 .field("instruments_cached", &self.instruments_cache.len())
1256 .finish()
1257 }
1258}
1259
1260impl BinanceSpotHttpClient {
1261 pub fn new(
1267 environment: BinanceEnvironment,
1268 api_key: Option<String>,
1269 api_secret: Option<String>,
1270 base_url_override: Option<String>,
1271 recv_window: Option<u64>,
1272 timeout_secs: Option<u64>,
1273 proxy_url: Option<String>,
1274 ) -> BinanceSpotHttpResult<Self> {
1275 let inner = BinanceRawSpotHttpClient::new(
1276 environment,
1277 api_key,
1278 api_secret,
1279 base_url_override,
1280 recv_window,
1281 timeout_secs,
1282 proxy_url,
1283 )?;
1284
1285 Ok(Self {
1286 inner: Arc::new(inner),
1287 instruments_cache: Arc::new(DashMap::new()),
1288 })
1289 }
1290
1291 #[must_use]
1293 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
1294 &self.inner
1295 }
1296
1297 #[must_use]
1299 pub const fn schema_id() -> u16 {
1300 SBE_SCHEMA_ID
1301 }
1302
1303 #[must_use]
1305 pub const fn schema_version() -> u16 {
1306 SBE_SCHEMA_VERSION
1307 }
1308
1309 fn generate_ts_init(&self) -> UnixNanos {
1311 UnixNanos::from(chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64)
1312 }
1313
1314 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
1316 self.instruments_cache
1317 .get(&symbol)
1318 .map(|entry| entry.value().clone())
1319 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
1320 }
1321
1322 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
1324 for inst in instruments {
1325 self.instruments_cache
1326 .insert(inst.raw_symbol().inner(), inst);
1327 }
1328 }
1329
1330 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1332 self.instruments_cache
1333 .insert(instrument.raw_symbol().inner(), instrument);
1334 }
1335
1336 #[must_use]
1338 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1339 self.instruments_cache
1340 .get(symbol)
1341 .map(|entry| entry.value().clone())
1342 }
1343
1344 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
1350 self.inner.ping().await
1351 }
1352
1353 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
1361 self.inner.server_time().await
1362 }
1363
1364 pub async fn exchange_info(
1370 &self,
1371 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
1372 self.inner.exchange_info().await
1373 }
1374
1375 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
1384 let info = self.exchange_info().await?;
1385 let ts_init = self.generate_ts_init();
1386
1387 let mut instruments = Vec::with_capacity(info.symbols.len());
1388 for symbol in &info.symbols {
1389 match parse_spot_instrument_sbe(symbol, ts_init, ts_init) {
1390 Ok(instrument) => instruments.push(instrument),
1391 Err(e) => {
1392 log::debug!(
1393 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1394 symbol.symbol
1395 );
1396 }
1397 }
1398 }
1399
1400 self.cache_instruments(instruments.clone());
1402
1403 log::info!("Loaded spot instruments: count={}", instruments.len());
1404 Ok(instruments)
1405 }
1406
1407 pub async fn request_trades(
1414 &self,
1415 instrument_id: InstrumentId,
1416 limit: Option<u32>,
1417 ) -> anyhow::Result<Vec<TradeTick>> {
1418 let symbol = instrument_id.symbol.inner();
1419 let instrument = self.instrument_from_cache(symbol)?;
1420 let ts_init = self.generate_ts_init();
1421
1422 let trades = self
1423 .inner
1424 .trades(symbol.as_str(), limit)
1425 .await
1426 .map_err(|e| anyhow::anyhow!(e))?;
1427
1428 parse_spot_trades_sbe(&trades, &instrument, ts_init)
1429 }
1430
1431 pub async fn request_bars(
1438 &self,
1439 bar_type: BarType,
1440 start: Option<DateTime<Utc>>,
1441 end: Option<DateTime<Utc>>,
1442 limit: Option<u32>,
1443 ) -> anyhow::Result<Vec<Bar>> {
1444 anyhow::ensure!(
1445 bar_type.aggregation_source() == AggregationSource::External,
1446 "Only EXTERNAL aggregation is supported"
1447 );
1448
1449 let spec = bar_type.spec();
1450 let step = spec.step.get();
1451 let interval = match spec.aggregation {
1452 BarAggregation::Second => {
1453 anyhow::bail!("Binance Spot does not support second-level kline intervals")
1454 }
1455 BarAggregation::Minute => format!("{step}m"),
1456 BarAggregation::Hour => format!("{step}h"),
1457 BarAggregation::Day => format!("{step}d"),
1458 BarAggregation::Week => format!("{step}w"),
1459 BarAggregation::Month => format!("{step}M"),
1460 a => anyhow::bail!("Binance does not support {a:?} aggregation"),
1461 };
1462
1463 let symbol = bar_type.instrument_id().symbol;
1464 let instrument = self.instrument_from_cache(symbol.inner())?;
1465 let ts_init = self.generate_ts_init();
1466
1467 let klines = self
1468 .inner
1469 .klines(
1470 symbol.as_str(),
1471 &interval,
1472 start.map(|dt| dt.timestamp_millis()),
1473 end.map(|dt| dt.timestamp_millis()),
1474 limit,
1475 )
1476 .await
1477 .map_err(|e| anyhow::anyhow!(e))?;
1478
1479 parse_klines_to_bars(&klines, bar_type, &instrument, ts_init)
1480 }
1481
1482 pub async fn request_account_state(
1488 &self,
1489 account_id: AccountId,
1490 ) -> anyhow::Result<AccountState> {
1491 let ts_init = get_atomic_clock_realtime().get_time_ns();
1492 let params = AccountInfoParams::default();
1493 let account_info = self.inner.account(¶ms).await?;
1494 Ok(account_info.to_account_state(account_id, ts_init))
1495 }
1496
1497 pub async fn request_order_status_report(
1506 &self,
1507 account_id: AccountId,
1508 instrument_id: InstrumentId,
1509 venue_order_id: Option<VenueOrderId>,
1510 client_order_id: Option<ClientOrderId>,
1511 ) -> anyhow::Result<OrderStatusReport> {
1512 anyhow::ensure!(
1513 venue_order_id.is_some() || client_order_id.is_some(),
1514 "Either venue_order_id or client_order_id must be provided"
1515 );
1516
1517 let symbol = instrument_id.symbol.inner();
1518 let instrument = self.instrument_from_cache(symbol)?;
1519 let ts_init = self.generate_ts_init();
1520
1521 let order_id = venue_order_id
1522 .map(|id| id.inner().parse::<i64>())
1523 .transpose()
1524 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1525
1526 let client_id_str = client_order_id.map(|id| id.to_string());
1527
1528 let order = self
1529 .inner
1530 .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
1531 .await
1532 .map_err(|e| anyhow::anyhow!(e))?;
1533
1534 parse_order_status_report_sbe(&order, account_id, &instrument, ts_init)
1535 }
1536
1537 #[allow(clippy::too_many_arguments)]
1547 pub async fn request_order_status_reports(
1548 &self,
1549 account_id: AccountId,
1550 instrument_id: Option<InstrumentId>,
1551 start: Option<DateTime<Utc>>,
1552 end: Option<DateTime<Utc>>,
1553 open_only: bool,
1554 limit: Option<u32>,
1555 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1556 let ts_init = self.generate_ts_init();
1557 let symbol = instrument_id.map(|id| id.symbol.to_string());
1558
1559 let orders = if open_only {
1560 self.inner
1561 .open_orders(symbol.as_deref())
1562 .await
1563 .map_err(|e| anyhow::anyhow!(e))?
1564 } else {
1565 let symbol = symbol
1566 .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
1567 self.inner
1568 .all_orders(
1569 &symbol,
1570 start.map(|dt| dt.timestamp_millis()),
1571 end.map(|dt| dt.timestamp_millis()),
1572 limit,
1573 )
1574 .await
1575 .map_err(|e| anyhow::anyhow!(e))?
1576 };
1577
1578 orders
1579 .iter()
1580 .map(|order| {
1581 let symbol = Ustr::from(&order.symbol);
1582 let instrument = self.instrument_from_cache(symbol)?;
1583 parse_order_status_report_sbe(order, account_id, &instrument, ts_init)
1584 })
1585 .collect()
1586 }
1587
1588 #[allow(clippy::too_many_arguments)]
1595 pub async fn request_fill_reports(
1596 &self,
1597 account_id: AccountId,
1598 instrument_id: InstrumentId,
1599 venue_order_id: Option<VenueOrderId>,
1600 start: Option<DateTime<Utc>>,
1601 end: Option<DateTime<Utc>>,
1602 limit: Option<u32>,
1603 ) -> anyhow::Result<Vec<FillReport>> {
1604 let ts_init = self.generate_ts_init();
1605 let symbol = instrument_id.symbol.inner();
1606
1607 let order_id = venue_order_id
1608 .map(|id| id.inner().parse::<i64>())
1609 .transpose()
1610 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1611
1612 let trades = self
1613 .inner
1614 .account_trades(
1615 symbol.as_str(),
1616 order_id,
1617 start.map(|dt| dt.timestamp_millis()),
1618 end.map(|dt| dt.timestamp_millis()),
1619 limit,
1620 )
1621 .await
1622 .map_err(|e| anyhow::anyhow!(e))?;
1623
1624 trades
1625 .iter()
1626 .map(|trade| {
1627 let symbol = Ustr::from(&trade.symbol);
1628 let instrument = self.instrument_from_cache(symbol)?;
1629 let commission_currency = get_currency(&trade.commission_asset);
1630 parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
1631 })
1632 .collect()
1633 }
1634
1635 #[allow(clippy::too_many_arguments)]
1648 pub async fn submit_order(
1649 &self,
1650 account_id: AccountId,
1651 instrument_id: InstrumentId,
1652 client_order_id: ClientOrderId,
1653 order_side: OrderSide,
1654 order_type: OrderType,
1655 quantity: Quantity,
1656 time_in_force: TimeInForce,
1657 price: Option<Price>,
1658 trigger_price: Option<Price>,
1659 post_only: bool,
1660 ) -> anyhow::Result<OrderStatusReport> {
1661 let symbol = instrument_id.symbol.inner();
1662 let instrument = self.instrument_from_cache(symbol)?;
1663 let ts_init = self.generate_ts_init();
1664
1665 let binance_side = BinanceSide::try_from(order_side)?;
1666 let binance_order_type = order_type_to_binance_spot(order_type, post_only)?;
1667
1668 let is_stop_order = matches!(order_type, OrderType::StopMarket | OrderType::StopLimit);
1670 if is_stop_order && trigger_price.is_none() {
1671 anyhow::bail!("Stop orders require a trigger price");
1672 }
1673
1674 let requires_price = matches!(
1676 binance_order_type,
1677 BinanceSpotOrderType::Limit
1678 | BinanceSpotOrderType::StopLossLimit
1679 | BinanceSpotOrderType::TakeProfitLimit
1680 | BinanceSpotOrderType::LimitMaker
1681 );
1682 if requires_price && price.is_none() {
1683 anyhow::bail!("{binance_order_type:?} orders require a price");
1684 }
1685
1686 let supports_tif = matches!(
1688 binance_order_type,
1689 BinanceSpotOrderType::Limit
1690 | BinanceSpotOrderType::StopLossLimit
1691 | BinanceSpotOrderType::TakeProfitLimit
1692 );
1693 let binance_tif = if supports_tif {
1694 Some(BinanceTimeInForce::try_from(time_in_force)?)
1695 } else {
1696 None
1697 };
1698
1699 let qty_str = quantity.to_string();
1700 let price_str = price.map(|p| p.to_string());
1701 let stop_price_str = trigger_price.map(|p| p.to_string());
1702 let client_id_str = client_order_id.to_string();
1703
1704 let response = self
1705 .inner
1706 .new_order(
1707 symbol.as_str(),
1708 binance_side,
1709 binance_order_type,
1710 binance_tif,
1711 Some(&qty_str),
1712 price_str.as_deref(),
1713 Some(&client_id_str),
1714 stop_price_str.as_deref(),
1715 )
1716 .await
1717 .map_err(|e| anyhow::anyhow!(e))?;
1718
1719 parse_new_order_response_sbe(&response, account_id, &instrument, ts_init)
1720 }
1721
1722 pub async fn submit_order_list(
1730 &self,
1731 orders: &[BatchOrderItem],
1732 ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1733 self.inner.batch_submit_orders(orders).await
1734 }
1735
1736 #[allow(clippy::too_many_arguments)]
1745 pub async fn modify_order(
1746 &self,
1747 account_id: AccountId,
1748 instrument_id: InstrumentId,
1749 venue_order_id: VenueOrderId,
1750 client_order_id: ClientOrderId,
1751 order_side: OrderSide,
1752 order_type: OrderType,
1753 quantity: Quantity,
1754 time_in_force: TimeInForce,
1755 price: Option<Price>,
1756 ) -> anyhow::Result<OrderStatusReport> {
1757 let symbol = instrument_id.symbol.inner();
1758 let instrument = self.instrument_from_cache(symbol)?;
1759 let ts_init = self.generate_ts_init();
1760
1761 let binance_side = BinanceSide::try_from(order_side)?;
1762 let binance_order_type = order_type_to_binance_spot(order_type, false)?;
1763 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1764
1765 let cancel_order_id: i64 = venue_order_id
1766 .inner()
1767 .parse()
1768 .map_err(|_| anyhow::anyhow!("Invalid venue order ID: {venue_order_id}"))?;
1769
1770 let qty_str = quantity.to_string();
1771 let price_str = price.map(|p| p.to_string());
1772 let client_id_str = client_order_id.to_string();
1773
1774 let response = self
1775 .inner
1776 .cancel_replace_order(
1777 symbol.as_str(),
1778 binance_side,
1779 binance_order_type,
1780 Some(binance_tif),
1781 Some(&qty_str),
1782 price_str.as_deref(),
1783 Some(cancel_order_id),
1784 None,
1785 Some(&client_id_str),
1786 )
1787 .await
1788 .map_err(|e| anyhow::anyhow!(e))?;
1789
1790 parse_new_order_response_sbe(&response, account_id, &instrument, ts_init)
1791 }
1792
1793 pub async fn cancel_order(
1801 &self,
1802 instrument_id: InstrumentId,
1803 venue_order_id: Option<VenueOrderId>,
1804 client_order_id: Option<ClientOrderId>,
1805 ) -> anyhow::Result<VenueOrderId> {
1806 let symbol = instrument_id.symbol.inner();
1807
1808 let order_id = venue_order_id
1809 .map(|id| id.inner().parse::<i64>())
1810 .transpose()
1811 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1812
1813 let client_id_str = client_order_id.map(|id| id.to_string());
1814
1815 let response = self
1816 .inner
1817 .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
1818 .await
1819 .map_err(|e| anyhow::anyhow!(e))?;
1820
1821 Ok(VenueOrderId::new(response.order_id.to_string()))
1822 }
1823
1824 pub async fn batch_cancel_orders(
1832 &self,
1833 cancels: &[BatchCancelItem],
1834 ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
1835 self.inner.batch_cancel_orders(cancels).await
1836 }
1837
1838 pub async fn cancel_all_orders(
1846 &self,
1847 instrument_id: InstrumentId,
1848 ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
1849 let symbol = instrument_id.symbol.inner();
1850
1851 let responses = self
1852 .inner
1853 .cancel_open_orders(symbol.as_str())
1854 .await
1855 .map_err(|e| anyhow::anyhow!(e))?;
1856
1857 Ok(responses
1858 .into_iter()
1859 .map(|r| {
1860 (
1861 VenueOrderId::new(r.order_id.to_string()),
1862 ClientOrderId::new(&r.orig_client_order_id),
1863 )
1864 })
1865 .collect())
1866 }
1867}
1868
1869#[cfg(test)]
1870mod tests {
1871 use rstest::rstest;
1872
1873 use super::*;
1874
1875 #[rstest]
1876 fn test_schema_constants() {
1877 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
1878 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 2);
1879 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
1880 assert_eq!(BinanceSpotHttpClient::schema_version(), 2);
1881 }
1882
1883 #[rstest]
1884 fn test_sbe_schema_header() {
1885 assert_eq!(SBE_SCHEMA_HEADER, "3:2");
1886 }
1887
1888 #[rstest]
1889 fn test_default_headers_include_sbe() {
1890 let headers = BinanceRawSpotHttpClient::default_headers(&None);
1891
1892 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
1893 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:2".to_string()));
1894 }
1895
1896 #[rstest]
1897 fn test_rate_limit_config() {
1898 let config = BinanceRawSpotHttpClient::rate_limit_config();
1899
1900 assert!(config.default_quota.is_some());
1901 assert_eq!(config.order_keys.len(), 2);
1903 }
1904}