1use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, sync::Arc};
35
36use chrono::Utc;
37use dashmap::DashMap;
38use nautilus_core::{consts::NAUTILUS_USER_AGENT, nanos::UnixNanos};
39use nautilus_model::{
40 data::TradeTick,
41 enums::{OrderSide, OrderType, TimeInForce},
42 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
43 instruments::{Instrument, any::InstrumentAny},
44 reports::{FillReport, OrderStatusReport},
45 types::{Price, Quantity},
46};
47use nautilus_network::{
48 http::{HttpClient, HttpResponse, Method},
49 ratelimiter::quota::Quota,
50};
51use serde::Serialize;
52use ustr::Ustr;
53
54use super::{
55 error::{BinanceSpotHttpError, BinanceSpotHttpResult},
56 models::{
57 BinanceAccountInfo, BinanceAccountTrade, BinanceCancelOrderResponse, BinanceDepth,
58 BinanceNewOrderResponse, BinanceOrderResponse, BinanceTrades,
59 },
60 parse,
61 query::{
62 AccountInfoParams, AccountTradesParams, AllOrdersParams, CancelOpenOrdersParams,
63 CancelOrderParams, CancelReplaceOrderParams, DepthParams, NewOrderParams, OpenOrdersParams,
64 QueryOrderParams, TradesParams,
65 },
66};
67use crate::{
68 common::{
69 consts::BINANCE_SPOT_RATE_LIMITS,
70 credential::Credential,
71 enums::{BinanceEnvironment, BinanceProductType, BinanceSide, BinanceTimeInForce},
72 models::BinanceErrorResponse,
73 sbe::spot::{SBE_SCHEMA_ID, SBE_SCHEMA_VERSION},
74 urls::get_http_base_url,
75 },
76 spot::enums::BinanceSpotOrderType,
77};
78
79pub const SBE_SCHEMA_HEADER: &str = "3:2";
81
82const SPOT_API_PATH: &str = "/api/v3";
84
85const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
87
88const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
90
91#[derive(Debug, Clone)]
102pub struct BinanceRawSpotHttpClient {
103 client: HttpClient,
104 base_url: String,
105 credential: Option<Credential>,
106 recv_window: Option<u64>,
107 order_rate_keys: Vec<String>,
108}
109
110impl BinanceRawSpotHttpClient {
111 pub fn new(
117 environment: BinanceEnvironment,
118 api_key: Option<String>,
119 api_secret: Option<String>,
120 base_url_override: Option<String>,
121 recv_window: Option<u64>,
122 timeout_secs: Option<u64>,
123 proxy_url: Option<String>,
124 ) -> BinanceSpotHttpResult<Self> {
125 let RateLimitConfig {
126 default_quota,
127 keyed_quotas,
128 order_keys,
129 } = Self::rate_limit_config();
130
131 let credential = match (api_key, api_secret) {
132 (Some(key), Some(secret)) => Some(Credential::new(key, secret)),
133 (None, None) => None,
134 _ => return Err(BinanceSpotHttpError::MissingCredentials),
135 };
136
137 let base_url = base_url_override.unwrap_or_else(|| {
138 get_http_base_url(BinanceProductType::Spot, environment).to_string()
139 });
140
141 let headers = Self::default_headers(&credential);
142
143 let client = HttpClient::new(
144 headers,
145 vec!["X-MBX-APIKEY".to_string()],
146 keyed_quotas,
147 default_quota,
148 timeout_secs,
149 proxy_url,
150 )?;
151
152 Ok(Self {
153 client,
154 base_url,
155 credential,
156 recv_window,
157 order_rate_keys: order_keys,
158 })
159 }
160
161 #[must_use]
163 pub const fn schema_id() -> u16 {
164 SBE_SCHEMA_ID
165 }
166
167 #[must_use]
169 pub const fn schema_version() -> u16 {
170 SBE_SCHEMA_VERSION
171 }
172
173 pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
175 where
176 P: Serialize + ?Sized,
177 {
178 self.request(Method::GET, path, params, false, false).await
179 }
180
181 pub async fn get_signed<P>(
183 &self,
184 path: &str,
185 params: Option<&P>,
186 ) -> BinanceSpotHttpResult<Vec<u8>>
187 where
188 P: Serialize + ?Sized,
189 {
190 self.request(Method::GET, path, params, true, false).await
191 }
192
193 pub async fn post_order<P>(
195 &self,
196 path: &str,
197 params: Option<&P>,
198 ) -> BinanceSpotHttpResult<Vec<u8>>
199 where
200 P: Serialize + ?Sized,
201 {
202 self.request(Method::POST, path, params, true, true).await
203 }
204
205 pub async fn delete_order<P>(
207 &self,
208 path: &str,
209 params: Option<&P>,
210 ) -> BinanceSpotHttpResult<Vec<u8>>
211 where
212 P: Serialize + ?Sized,
213 {
214 self.request(Method::DELETE, path, params, true, true).await
215 }
216
217 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
223 let bytes = self.get("ping", None::<&()>).await?;
224 parse::decode_ping(&bytes)?;
225 Ok(())
226 }
227
228 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
236 let bytes = self.get("time", None::<&()>).await?;
237 let timestamp = parse::decode_server_time(&bytes)?;
238 Ok(timestamp)
239 }
240
241 pub async fn exchange_info(
247 &self,
248 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
249 let bytes = self.get("exchangeInfo", None::<&()>).await?;
250 let info = parse::decode_exchange_info(&bytes)?;
251 Ok(info)
252 }
253
254 pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
260 let bytes = self.get("depth", Some(params)).await?;
261 let depth = parse::decode_depth(&bytes)?;
262 Ok(depth)
263 }
264
265 pub async fn trades(&self, params: &TradesParams) -> BinanceSpotHttpResult<BinanceTrades> {
271 let bytes = self.get("trades", Some(params)).await?;
272 let trades = parse::decode_trades(&bytes)?;
273 Ok(trades)
274 }
275
276 pub async fn new_order(
282 &self,
283 params: &NewOrderParams,
284 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
285 let bytes = self.post_order("order", Some(params)).await?;
286 let response = parse::decode_new_order_full(&bytes)?;
287 Ok(response)
288 }
289
290 pub async fn cancel_order(
296 &self,
297 params: &CancelOrderParams,
298 ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
299 let bytes = self.delete_order("order", Some(params)).await?;
300 let response = parse::decode_cancel_order(&bytes)?;
301 Ok(response)
302 }
303
304 pub async fn cancel_open_orders(
310 &self,
311 params: &CancelOpenOrdersParams,
312 ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
313 let bytes = self.delete_order("openOrders", Some(params)).await?;
314 let response = parse::decode_cancel_open_orders(&bytes)?;
315 Ok(response)
316 }
317
318 pub async fn cancel_replace_order(
324 &self,
325 params: &CancelReplaceOrderParams,
326 ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
327 let bytes = self.post_order("order/cancelReplace", Some(params)).await?;
328 let response = parse::decode_new_order_full(&bytes)?;
329 Ok(response)
330 }
331
332 pub async fn query_order(
338 &self,
339 params: &QueryOrderParams,
340 ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
341 let bytes = self.get_signed("order", Some(params)).await?;
342 let response = parse::decode_order(&bytes)?;
343 Ok(response)
344 }
345
346 pub async fn open_orders(
352 &self,
353 params: &OpenOrdersParams,
354 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
355 let bytes = self.get_signed("openOrders", Some(params)).await?;
356 let response = parse::decode_orders(&bytes)?;
357 Ok(response)
358 }
359
360 pub async fn all_orders(
366 &self,
367 params: &AllOrdersParams,
368 ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
369 let bytes = self.get_signed("allOrders", Some(params)).await?;
370 let response = parse::decode_orders(&bytes)?;
371 Ok(response)
372 }
373
374 pub async fn account(
380 &self,
381 params: &AccountInfoParams,
382 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
383 let bytes = self.get_signed("account", Some(params)).await?;
384 let response = parse::decode_account(&bytes)?;
385 Ok(response)
386 }
387
388 pub async fn account_trades(
394 &self,
395 params: &AccountTradesParams,
396 ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
397 let bytes = self.get_signed("myTrades", Some(params)).await?;
398 let response = parse::decode_account_trades(&bytes)?;
399 Ok(response)
400 }
401
402 async fn request<P>(
403 &self,
404 method: Method,
405 path: &str,
406 params: Option<&P>,
407 signed: bool,
408 use_order_quota: bool,
409 ) -> BinanceSpotHttpResult<Vec<u8>>
410 where
411 P: Serialize + ?Sized,
412 {
413 let mut query = params
414 .map(serde_urlencoded::to_string)
415 .transpose()
416 .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
417 .unwrap_or_default();
418
419 let mut headers = HashMap::new();
420 if signed {
421 let cred = self
422 .credential
423 .as_ref()
424 .ok_or(BinanceSpotHttpError::MissingCredentials)?;
425
426 if !query.is_empty() {
427 query.push('&');
428 }
429
430 let timestamp = Utc::now().timestamp_millis();
431 query.push_str(&format!("timestamp={timestamp}"));
432
433 if let Some(recv_window) = self.recv_window {
434 query.push_str(&format!("&recvWindow={recv_window}"));
435 }
436
437 let signature = cred.sign(&query);
438 query.push_str(&format!("&signature={signature}"));
439 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
440 }
441
442 let url = self.build_url(path, &query);
443 let keys = self.rate_limit_keys(use_order_quota);
444
445 let response = self
446 .client
447 .request(
448 method,
449 url,
450 None::<&HashMap<String, Vec<String>>>,
451 Some(headers),
452 None,
453 None,
454 Some(keys),
455 )
456 .await?;
457
458 if !response.status.is_success() {
459 return self.parse_error_response(response);
460 }
461
462 Ok(response.body.to_vec())
463 }
464
465 fn build_url(&self, path: &str, query: &str) -> String {
466 let normalized_path = if path.starts_with('/') {
467 path.to_string()
468 } else {
469 format!("/{path}")
470 };
471
472 let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
473 if !query.is_empty() {
474 url.push('?');
475 url.push_str(query);
476 }
477 url
478 }
479
480 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
481 if use_orders {
482 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
483 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
484 keys.extend(self.order_rate_keys.iter().cloned());
485 keys
486 } else {
487 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
488 }
489 }
490
491 fn parse_error_response<T>(&self, response: HttpResponse) -> BinanceSpotHttpResult<T> {
492 let status = response.status.as_u16();
493 let body_hex = hex::encode(&response.body);
494
495 if let Ok(body_str) = std::str::from_utf8(&response.body)
497 && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
498 {
499 return Err(BinanceSpotHttpError::BinanceError {
500 code: err.code,
501 message: err.msg,
502 });
503 }
504
505 Err(BinanceSpotHttpError::UnexpectedStatus {
506 status,
507 body: body_hex,
508 })
509 }
510
511 fn default_headers(credential: &Option<Credential>) -> HashMap<String, String> {
512 let mut headers = HashMap::new();
513 headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
514 headers.insert("Accept".to_string(), "application/sbe".to_string());
515 headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
516 if let Some(cred) = credential {
517 headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
518 }
519 headers
520 }
521
522 fn rate_limit_config() -> RateLimitConfig {
523 let quotas = BINANCE_SPOT_RATE_LIMITS;
524 let mut keyed = Vec::new();
525 let mut order_keys = Vec::new();
526 let mut default = None;
527
528 for quota in quotas {
529 if let Some(q) = Self::quota_from(quota) {
530 if quota.rate_limit_type == "REQUEST_WEIGHT" && default.is_none() {
531 default = Some(q);
532 } else if quota.rate_limit_type == "ORDERS" {
533 let key = format!("{}:{}", BINANCE_ORDERS_RATE_KEY, quota.interval);
534 order_keys.push(key.clone());
535 keyed.push((key, q));
536 }
537 }
538 }
539
540 let default_quota =
541 default.unwrap_or_else(|| Quota::per_second(NonZeroU32::new(10).unwrap()));
542
543 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
544
545 RateLimitConfig {
546 default_quota: Some(default_quota),
547 keyed_quotas: keyed,
548 order_keys,
549 }
550 }
551
552 fn quota_from(quota: &crate::common::consts::BinanceRateLimitQuota) -> Option<Quota> {
553 let burst = NonZeroU32::new(quota.limit)?;
554 match quota.interval {
555 "SECOND" => Some(Quota::per_second(burst)),
556 "MINUTE" => Some(Quota::per_minute(burst)),
557 "DAY" => Quota::with_period(std::time::Duration::from_secs(86_400))
558 .map(|q| q.allow_burst(burst)),
559 _ => None,
560 }
561 }
562}
563
564struct RateLimitConfig {
565 default_quota: Option<Quota>,
566 keyed_quotas: Vec<(String, Quota)>,
567 order_keys: Vec<String>,
568}
569
570#[cfg_attr(
576 feature = "python",
577 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance")
578)]
579pub struct BinanceSpotHttpClient {
580 inner: Arc<BinanceRawSpotHttpClient>,
581 instruments_cache: Arc<DashMap<Ustr, InstrumentAny>>,
582}
583
584impl Clone for BinanceSpotHttpClient {
585 fn clone(&self) -> Self {
586 Self {
587 inner: self.inner.clone(),
588 instruments_cache: self.instruments_cache.clone(),
589 }
590 }
591}
592
593impl Debug for BinanceSpotHttpClient {
594 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595 f.debug_struct("BinanceSpotHttpClient")
596 .field("inner", &self.inner)
597 .field("instruments_cached", &self.instruments_cache.len())
598 .finish()
599 }
600}
601
602impl BinanceSpotHttpClient {
603 pub fn new(
609 environment: BinanceEnvironment,
610 api_key: Option<String>,
611 api_secret: Option<String>,
612 base_url_override: Option<String>,
613 recv_window: Option<u64>,
614 timeout_secs: Option<u64>,
615 proxy_url: Option<String>,
616 ) -> BinanceSpotHttpResult<Self> {
617 let inner = BinanceRawSpotHttpClient::new(
618 environment,
619 api_key,
620 api_secret,
621 base_url_override,
622 recv_window,
623 timeout_secs,
624 proxy_url,
625 )?;
626
627 Ok(Self {
628 inner: Arc::new(inner),
629 instruments_cache: Arc::new(DashMap::new()),
630 })
631 }
632
633 #[must_use]
635 pub fn inner(&self) -> &BinanceRawSpotHttpClient {
636 &self.inner
637 }
638
639 #[must_use]
641 pub const fn schema_id() -> u16 {
642 SBE_SCHEMA_ID
643 }
644
645 #[must_use]
647 pub const fn schema_version() -> u16 {
648 SBE_SCHEMA_VERSION
649 }
650
651 fn generate_ts_init(&self) -> UnixNanos {
653 UnixNanos::from(chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64)
654 }
655
656 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
658 self.instruments_cache
659 .get(&symbol)
660 .map(|entry| entry.value().clone())
661 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
662 }
663
664 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
666 for inst in instruments {
667 self.instruments_cache
668 .insert(inst.raw_symbol().inner(), inst);
669 }
670 }
671
672 pub fn cache_instrument(&self, instrument: InstrumentAny) {
674 self.instruments_cache
675 .insert(instrument.raw_symbol().inner(), instrument);
676 }
677
678 #[must_use]
680 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
681 self.instruments_cache
682 .get(symbol)
683 .map(|entry| entry.value().clone())
684 }
685
686 pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
692 self.inner.ping().await
693 }
694
695 pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
703 self.inner.server_time().await
704 }
705
706 pub async fn exchange_info(
712 &self,
713 ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
714 self.inner.exchange_info().await
715 }
716
717 pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
726 let info = self.exchange_info().await?;
727 let ts_init = self.generate_ts_init();
728
729 let mut instruments = Vec::with_capacity(info.symbols.len());
730 for symbol in &info.symbols {
731 match crate::common::parse::parse_spot_instrument_sbe(symbol, ts_init, ts_init) {
732 Ok(instrument) => instruments.push(instrument),
733 Err(e) => {
734 tracing::debug!(
735 symbol = %symbol.symbol,
736 error = %e,
737 "Skipping symbol during instrument parsing"
738 );
739 }
740 }
741 }
742
743 self.cache_instruments(instruments.clone());
745
746 tracing::info!(count = instruments.len(), "Loaded spot instruments");
747 Ok(instruments)
748 }
749
750 pub async fn request_trades(
757 &self,
758 instrument_id: InstrumentId,
759 limit: Option<u32>,
760 ) -> anyhow::Result<Vec<TradeTick>> {
761 let symbol = instrument_id.symbol.inner();
762 let instrument = self.instrument_from_cache(symbol)?;
763 let ts_init = self.generate_ts_init();
764
765 let params = TradesParams {
766 symbol: symbol.to_string(),
767 limit,
768 };
769
770 let trades = self
771 .inner
772 .trades(¶ms)
773 .await
774 .map_err(|e| anyhow::anyhow!(e))?;
775
776 crate::common::parse::parse_spot_trades_sbe(&trades, &instrument, ts_init)
777 }
778
779 #[allow(clippy::too_many_arguments)]
792 pub async fn submit_order(
793 &self,
794 account_id: AccountId,
795 instrument_id: InstrumentId,
796 client_order_id: ClientOrderId,
797 order_side: OrderSide,
798 order_type: OrderType,
799 quantity: Quantity,
800 time_in_force: TimeInForce,
801 price: Option<Price>,
802 trigger_price: Option<Price>,
803 post_only: bool,
804 ) -> anyhow::Result<OrderStatusReport> {
805 let symbol = instrument_id.symbol.inner();
806 let instrument = self.instrument_from_cache(symbol)?;
807 let ts_init = self.generate_ts_init();
808
809 let binance_side = match order_side {
810 OrderSide::Buy => BinanceSide::Buy,
811 OrderSide::Sell => BinanceSide::Sell,
812 _ => anyhow::bail!("Invalid order side: {order_side:?}"),
813 };
814
815 let is_stop_order = matches!(order_type, OrderType::StopMarket | OrderType::StopLimit);
816 if is_stop_order && trigger_price.is_none() {
817 anyhow::bail!("Stop orders require a trigger price");
818 }
819
820 let binance_order_type = match (order_type, post_only) {
821 (OrderType::Market, _) => BinanceSpotOrderType::Market,
822 (OrderType::Limit, true) => BinanceSpotOrderType::LimitMaker,
823 (OrderType::Limit, false) => BinanceSpotOrderType::Limit,
824 (OrderType::StopMarket, _) => BinanceSpotOrderType::StopLoss,
825 (OrderType::StopLimit, _) => BinanceSpotOrderType::StopLossLimit,
826 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
827 };
828
829 let binance_tif = match time_in_force {
830 TimeInForce::Gtc => BinanceTimeInForce::Gtc,
831 TimeInForce::Ioc => BinanceTimeInForce::Ioc,
832 TimeInForce::Fok => BinanceTimeInForce::Fok,
833 TimeInForce::Gtd => BinanceTimeInForce::Gtd,
834 _ => anyhow::bail!("Unsupported time in force: {time_in_force:?}"),
835 };
836
837 let params = NewOrderParams {
838 symbol: symbol.to_string(),
839 side: binance_side,
840 order_type: binance_order_type,
841 time_in_force: Some(binance_tif),
842 quantity: Some(quantity.to_string()),
843 quote_order_qty: None,
844 price: price.map(|p| p.to_string()),
845 new_client_order_id: Some(client_order_id.to_string()),
846 stop_price: trigger_price.map(|p| p.to_string()),
847 trailing_delta: None,
848 iceberg_qty: None,
849 new_order_resp_type: None,
850 self_trade_prevention_mode: None,
851 };
852
853 let response = self
854 .inner
855 .new_order(¶ms)
856 .await
857 .map_err(|e| anyhow::anyhow!(e))?;
858
859 crate::common::parse::parse_new_order_response_sbe(
860 &response,
861 account_id,
862 &instrument,
863 ts_init,
864 )
865 }
866
867 pub async fn cancel_order(
873 &self,
874 instrument_id: InstrumentId,
875 venue_order_id: VenueOrderId,
876 ) -> anyhow::Result<VenueOrderId> {
877 let symbol = instrument_id.symbol.inner().to_string();
878
879 let order_id: i64 = venue_order_id
880 .inner()
881 .parse()
882 .map_err(|_| anyhow::anyhow!("Invalid venue order ID: {venue_order_id}"))?;
883
884 let params = CancelOrderParams::by_order_id(&symbol, order_id);
885
886 let response = self
887 .inner
888 .cancel_order(¶ms)
889 .await
890 .map_err(|e| anyhow::anyhow!(e))?;
891
892 Ok(VenueOrderId::new(response.order_id.to_string()))
893 }
894
895 pub async fn cancel_order_by_client_id(
901 &self,
902 instrument_id: InstrumentId,
903 client_order_id: ClientOrderId,
904 ) -> anyhow::Result<VenueOrderId> {
905 let symbol = instrument_id.symbol.inner().to_string();
906 let params = CancelOrderParams::by_client_order_id(&symbol, client_order_id.to_string());
907
908 let response = self
909 .inner
910 .cancel_order(¶ms)
911 .await
912 .map_err(|e| anyhow::anyhow!(e))?;
913
914 Ok(VenueOrderId::new(response.order_id.to_string()))
915 }
916
917 pub async fn cancel_all_orders(
925 &self,
926 instrument_id: InstrumentId,
927 ) -> anyhow::Result<Vec<VenueOrderId>> {
928 let symbol = instrument_id.symbol.inner().to_string();
929 let params = CancelOpenOrdersParams::new(symbol);
930
931 let responses = self
932 .inner
933 .cancel_open_orders(¶ms)
934 .await
935 .map_err(|e| anyhow::anyhow!(e))?;
936
937 Ok(responses
938 .into_iter()
939 .map(|r| VenueOrderId::new(r.order_id.to_string()))
940 .collect())
941 }
942
943 #[allow(clippy::too_many_arguments)]
952 pub async fn modify_order(
953 &self,
954 account_id: AccountId,
955 instrument_id: InstrumentId,
956 venue_order_id: VenueOrderId,
957 client_order_id: ClientOrderId,
958 order_side: OrderSide,
959 order_type: OrderType,
960 quantity: Quantity,
961 time_in_force: TimeInForce,
962 price: Option<Price>,
963 ) -> anyhow::Result<OrderStatusReport> {
964 let symbol = instrument_id.symbol.inner();
965 let instrument = self.instrument_from_cache(symbol)?;
966 let ts_init = self.generate_ts_init();
967
968 let binance_side = match order_side {
969 OrderSide::Buy => BinanceSide::Buy,
970 OrderSide::Sell => BinanceSide::Sell,
971 _ => anyhow::bail!("Invalid order side: {order_side:?}"),
972 };
973
974 let binance_order_type = match order_type {
975 OrderType::Market => BinanceSpotOrderType::Market,
976 OrderType::Limit => BinanceSpotOrderType::Limit,
977 _ => anyhow::bail!("Unsupported order type for modify: {order_type:?}"),
978 };
979
980 let binance_tif = match time_in_force {
981 TimeInForce::Gtc => BinanceTimeInForce::Gtc,
982 TimeInForce::Ioc => BinanceTimeInForce::Ioc,
983 TimeInForce::Fok => BinanceTimeInForce::Fok,
984 TimeInForce::Gtd => BinanceTimeInForce::Gtd,
985 _ => anyhow::bail!("Unsupported time in force: {time_in_force:?}"),
986 };
987
988 let cancel_order_id: i64 = venue_order_id
989 .inner()
990 .parse()
991 .map_err(|_| anyhow::anyhow!("Invalid venue order ID: {venue_order_id}"))?;
992
993 let params = CancelReplaceOrderParams {
994 symbol: symbol.to_string(),
995 side: binance_side,
996 order_type: binance_order_type,
997 cancel_replace_mode: crate::spot::enums::BinanceCancelReplaceMode::StopOnFailure,
998 time_in_force: Some(binance_tif),
999 quantity: Some(quantity.to_string()),
1000 quote_order_qty: None,
1001 price: price.map(|p| p.to_string()),
1002 cancel_order_id: Some(cancel_order_id),
1003 cancel_orig_client_order_id: None,
1004 new_client_order_id: Some(client_order_id.to_string()),
1005 stop_price: None,
1006 trailing_delta: None,
1007 iceberg_qty: None,
1008 new_order_resp_type: None,
1009 self_trade_prevention_mode: None,
1010 };
1011
1012 let response = self
1013 .inner
1014 .cancel_replace_order(¶ms)
1015 .await
1016 .map_err(|e| anyhow::anyhow!(e))?;
1017
1018 crate::common::parse::parse_new_order_response_sbe(
1019 &response,
1020 account_id,
1021 &instrument,
1022 ts_init,
1023 )
1024 }
1025
1026 pub async fn request_order_status(
1033 &self,
1034 account_id: AccountId,
1035 instrument_id: InstrumentId,
1036 params: &QueryOrderParams,
1037 ) -> anyhow::Result<OrderStatusReport> {
1038 let symbol = instrument_id.symbol.inner();
1039 let instrument = self.instrument_from_cache(symbol)?;
1040 let ts_init = self.generate_ts_init();
1041
1042 let order = self
1043 .inner
1044 .query_order(params)
1045 .await
1046 .map_err(|e| anyhow::anyhow!(e))?;
1047
1048 crate::common::parse::parse_order_status_report_sbe(
1049 &order,
1050 account_id,
1051 &instrument,
1052 ts_init,
1053 )
1054 }
1055
1056 pub async fn request_open_orders(
1063 &self,
1064 account_id: AccountId,
1065 params: &OpenOrdersParams,
1066 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1067 let ts_init = self.generate_ts_init();
1068
1069 let orders = self
1070 .inner
1071 .open_orders(params)
1072 .await
1073 .map_err(|e| anyhow::anyhow!(e))?;
1074
1075 orders
1076 .iter()
1077 .map(|order| {
1078 let symbol = Ustr::from(&order.symbol);
1079 let instrument = self.instrument_from_cache(symbol)?;
1080 crate::common::parse::parse_order_status_report_sbe(
1081 order,
1082 account_id,
1083 &instrument,
1084 ts_init,
1085 )
1086 })
1087 .collect()
1088 }
1089
1090 pub async fn request_order_history(
1097 &self,
1098 account_id: AccountId,
1099 params: &AllOrdersParams,
1100 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1101 let ts_init = self.generate_ts_init();
1102
1103 let orders = self
1104 .inner
1105 .all_orders(params)
1106 .await
1107 .map_err(|e| anyhow::anyhow!(e))?;
1108
1109 orders
1110 .iter()
1111 .map(|order| {
1112 let symbol = Ustr::from(&order.symbol);
1113 let instrument = self.instrument_from_cache(symbol)?;
1114 crate::common::parse::parse_order_status_report_sbe(
1115 order,
1116 account_id,
1117 &instrument,
1118 ts_init,
1119 )
1120 })
1121 .collect()
1122 }
1123
1124 pub async fn request_account_state(
1130 &self,
1131 params: &AccountInfoParams,
1132 ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
1133 self.inner.account(params).await
1134 }
1135
1136 pub async fn request_fill_reports(
1143 &self,
1144 account_id: AccountId,
1145 params: &AccountTradesParams,
1146 ) -> anyhow::Result<Vec<FillReport>> {
1147 let ts_init = self.generate_ts_init();
1148
1149 let trades = self
1150 .inner
1151 .account_trades(params)
1152 .await
1153 .map_err(|e| anyhow::anyhow!(e))?;
1154
1155 trades
1156 .iter()
1157 .map(|trade| {
1158 let symbol = Ustr::from(&trade.symbol);
1159 let instrument = self.instrument_from_cache(symbol)?;
1160 let commission_currency =
1161 crate::common::parse::get_currency(&trade.commission_asset);
1162 crate::common::parse::parse_fill_report_sbe(
1163 trade,
1164 account_id,
1165 &instrument,
1166 commission_currency,
1167 ts_init,
1168 )
1169 })
1170 .collect()
1171 }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use rstest::rstest;
1177
1178 use super::*;
1179
1180 #[rstest]
1181 fn test_schema_constants() {
1182 assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
1183 assert_eq!(BinanceRawSpotHttpClient::schema_version(), 2);
1184 assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
1185 assert_eq!(BinanceSpotHttpClient::schema_version(), 2);
1186 }
1187
1188 #[rstest]
1189 fn test_sbe_schema_header() {
1190 assert_eq!(SBE_SCHEMA_HEADER, "3:2");
1191 }
1192
1193 #[rstest]
1194 fn test_default_headers_include_sbe() {
1195 let headers = BinanceRawSpotHttpClient::default_headers(&None);
1196
1197 assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
1198 assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:2".to_string()));
1199 }
1200
1201 #[rstest]
1202 fn test_rate_limit_config() {
1203 let config = BinanceRawSpotHttpClient::rate_limit_config();
1204
1205 assert!(config.default_quota.is_some());
1206 assert_eq!(config.order_keys.len(), 2);
1208 }
1209}