1use std::{collections::HashMap, num::NonZeroU32, sync::Arc, time::Duration};
19
20use ahash::AHashMap;
21use chrono::{DateTime, Utc};
22use dashmap::DashMap;
23use nautilus_core::{
24 consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime,
25};
26use nautilus_model::{
27 data::{Bar, BarType, TradeTick},
28 enums::{
29 AggregationSource, AggressorSide, BarAggregation, MarketStatusAction, OrderSide, OrderType,
30 TimeInForce,
31 },
32 events::AccountState,
33 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
34 instruments::any::InstrumentAny,
35 reports::{FillReport, OrderStatusReport},
36 types::{Currency, Price, Quantity},
37};
38use nautilus_network::{
39 http::{HttpClient, HttpResponse, Method},
40 ratelimiter::quota::Quota,
41};
42use serde::{Deserialize, Serialize, de::DeserializeOwned};
43use ustr::Ustr;
44
45use super::{
46 error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
47 models::{
48 BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
49 BinanceFuturesAccountInfo, BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
50 BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesKline,
51 BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
52 BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
53 BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest, BinanceOrderBook,
54 BinancePositionRisk, BinancePriceTicker, BinanceServerTime, BinanceUserTrade,
55 ListenKeyResponse,
56 },
57 query::{
58 BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAlgoOrderQueryParams,
59 BinanceAllAlgoOrdersParams, BinanceAllOrdersParams, BinanceBookTickerParams,
60 BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams, BinanceCancelOrderParams,
61 BinanceDepthParams, BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
62 BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
63 BinanceOpenAlgoOrdersParams, BinanceOpenInterestParams, BinanceOpenOrdersParams,
64 BinanceOrderQueryParams, BinancePositionRiskParams, BinanceSetLeverageParams,
65 BinanceSetMarginTypeParams, BinanceTicker24hrParams, BinanceTradesParams,
66 BinanceUserTradesParams, ListenKeyParams,
67 },
68};
69use crate::common::{
70 consts::{
71 BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
72 BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID, BinanceRateLimitQuota,
73 },
74 credential::SigningCredential,
75 encoder::encode_broker_id,
76 enums::{
77 BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
78 BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
79 BinanceSide, BinanceTimeInForce, BinanceWorkingType,
80 },
81 models::BinanceErrorResponse,
82 parse::{parse_coinm_instrument, parse_usdm_instrument},
83 symbol::{format_binance_symbol, format_instrument_id},
84 urls::get_http_base_url,
85};
86
87const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
88const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
89
90#[derive(Debug, Clone)]
92pub struct BinanceRawFuturesHttpClient {
93 client: HttpClient,
94 base_url: String,
95 api_path: &'static str,
96 credential: Option<SigningCredential>,
97 recv_window: Option<u64>,
98 order_rate_keys: Vec<String>,
99}
100
101impl BinanceRawFuturesHttpClient {
102 #[must_use]
104 pub fn http_client(&self) -> &HttpClient {
105 &self.client
106 }
107
108 #[expect(clippy::too_many_arguments)]
114 pub fn new(
115 product_type: BinanceProductType,
116 environment: BinanceEnvironment,
117 api_key: Option<String>,
118 api_secret: Option<String>,
119 base_url_override: Option<String>,
120 recv_window: Option<u64>,
121 timeout_secs: Option<u64>,
122 proxy_url: Option<String>,
123 ) -> BinanceFuturesHttpResult<Self> {
124 let RateLimitConfig {
125 default_quota,
126 keyed_quotas,
127 order_keys,
128 } = Self::rate_limit_config(product_type);
129
130 let credential = match (api_key, api_secret) {
131 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
132 (None, None) => None,
133 _ => return Err(BinanceFuturesHttpError::MissingCredentials),
134 };
135
136 let base_url = base_url_override
137 .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
138
139 let api_path = Self::resolve_api_path(product_type);
140 let headers = Self::default_headers(&credential);
141
142 let client = HttpClient::new(
143 headers,
144 vec![BINANCE_API_KEY_HEADER.to_string()],
145 keyed_quotas,
146 default_quota,
147 timeout_secs,
148 proxy_url,
149 )?;
150
151 Ok(Self {
152 client,
153 base_url,
154 api_path,
155 credential,
156 recv_window,
157 order_rate_keys: order_keys,
158 })
159 }
160
161 pub async fn get<P, T>(
167 &self,
168 path: &str,
169 params: Option<&P>,
170 signed: bool,
171 use_order_quota: bool,
172 ) -> BinanceFuturesHttpResult<T>
173 where
174 P: Serialize + ?Sized,
175 T: DeserializeOwned,
176 {
177 self.request(Method::GET, path, params, signed, use_order_quota, None)
178 .await
179 }
180
181 pub async fn post<P, T>(
187 &self,
188 path: &str,
189 params: Option<&P>,
190 body: Option<Vec<u8>>,
191 signed: bool,
192 use_order_quota: bool,
193 ) -> BinanceFuturesHttpResult<T>
194 where
195 P: Serialize + ?Sized,
196 T: DeserializeOwned,
197 {
198 self.request(Method::POST, path, params, signed, use_order_quota, body)
199 .await
200 }
201
202 pub async fn request_put<P, T>(
208 &self,
209 path: &str,
210 params: Option<&P>,
211 signed: bool,
212 use_order_quota: bool,
213 ) -> BinanceFuturesHttpResult<T>
214 where
215 P: Serialize + ?Sized,
216 T: DeserializeOwned,
217 {
218 self.request(Method::PUT, path, params, signed, use_order_quota, None)
219 .await
220 }
221
222 pub async fn request_delete<P, T>(
228 &self,
229 path: &str,
230 params: Option<&P>,
231 signed: bool,
232 use_order_quota: bool,
233 ) -> BinanceFuturesHttpResult<T>
234 where
235 P: Serialize + ?Sized,
236 T: DeserializeOwned,
237 {
238 self.request(Method::DELETE, path, params, signed, use_order_quota, None)
239 .await
240 }
241
242 pub async fn batch_request<T: Serialize>(
248 &self,
249 path: &str,
250 items: &[T],
251 use_order_quota: bool,
252 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
253 self.batch_request_method(Method::POST, path, items, use_order_quota)
254 .await
255 }
256
257 pub async fn batch_request_delete<T: Serialize>(
263 &self,
264 path: &str,
265 items: &[T],
266 use_order_quota: bool,
267 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
268 self.batch_request_method(Method::DELETE, path, items, use_order_quota)
269 .await
270 }
271
272 pub async fn batch_request_put<T: Serialize>(
278 &self,
279 path: &str,
280 items: &[T],
281 use_order_quota: bool,
282 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
283 self.batch_request_method(Method::PUT, path, items, use_order_quota)
284 .await
285 }
286
287 async fn batch_request_method<T: Serialize>(
288 &self,
289 method: Method,
290 path: &str,
291 items: &[T],
292 use_order_quota: bool,
293 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
294 let cred = self
295 .credential
296 .as_ref()
297 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
298
299 let batch_json = serde_json::to_string(items)
300 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
301
302 let encoded_batch = Self::percent_encode(&batch_json);
303 let timestamp = Utc::now().timestamp_millis();
304 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
305
306 if let Some(recv_window) = self.recv_window {
307 query.push_str(&format!("&recvWindow={recv_window}"));
308 }
309
310 let signature = Self::percent_encode(&cred.sign(&query));
311 query.push_str(&format!("&signature={signature}"));
312
313 let url = self.build_url(path, &query);
314
315 let mut headers = HashMap::new();
316 headers.insert(
317 BINANCE_API_KEY_HEADER.to_string(),
318 cred.api_key().to_string(),
319 );
320
321 let keys = self.rate_limit_keys(use_order_quota);
322
323 let response = self
324 .client
325 .request(
326 method,
327 url,
328 None::<&HashMap<String, Vec<String>>>,
329 Some(headers),
330 None,
331 None,
332 Some(keys),
333 )
334 .await?;
335
336 if !response.status.is_success() {
337 return self.parse_error_response(&response);
338 }
339
340 serde_json::from_slice(&response.body)
341 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
342 }
343
344 fn percent_encode(input: &str) -> String {
346 let mut result = String::with_capacity(input.len() * 3);
347 for byte in input.bytes() {
348 match byte {
349 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
350 result.push(byte as char);
351 }
352 _ => {
353 result.push('%');
354 result.push_str(&format!("{byte:02X}"));
355 }
356 }
357 }
358 result
359 }
360
361 async fn request<P, T>(
362 &self,
363 method: Method,
364 path: &str,
365 params: Option<&P>,
366 signed: bool,
367 use_order_quota: bool,
368 body: Option<Vec<u8>>,
369 ) -> BinanceFuturesHttpResult<T>
370 where
371 P: Serialize + ?Sized,
372 T: DeserializeOwned,
373 {
374 let mut query = params
375 .map(serde_urlencoded::to_string)
376 .transpose()
377 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
378 .unwrap_or_default();
379
380 let mut headers = HashMap::new();
381
382 if signed {
383 let cred = self
384 .credential
385 .as_ref()
386 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
387
388 if !query.is_empty() {
389 query.push('&');
390 }
391
392 let timestamp = Utc::now().timestamp_millis();
393 query.push_str(&format!("timestamp={timestamp}"));
394
395 if let Some(recv_window) = self.recv_window {
396 query.push_str(&format!("&recvWindow={recv_window}"));
397 }
398
399 let signature = Self::percent_encode(&cred.sign(&query));
403 query.push_str(&format!("&signature={signature}"));
404 headers.insert(
405 BINANCE_API_KEY_HEADER.to_string(),
406 cred.api_key().to_string(),
407 );
408 }
409
410 let url = self.build_url(path, &query);
411 let keys = self.rate_limit_keys(use_order_quota);
412
413 let response = self
414 .client
415 .request(
416 method,
417 url,
418 None::<&HashMap<String, Vec<String>>>,
419 Some(headers),
420 body,
421 None,
422 Some(keys),
423 )
424 .await?;
425
426 if !response.status.is_success() {
427 return self.parse_error_response(&response);
428 }
429
430 serde_json::from_slice::<T>(&response.body)
431 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
432 }
433
434 fn build_url(&self, path: &str, query: &str) -> String {
435 let url_path = if path.starts_with("/fapi/") || path.starts_with("/dapi/") {
437 path.to_string()
438 } else if path.starts_with('/') {
439 format!("{}{}", self.api_path, path)
440 } else {
441 format!("{}/{}", self.api_path, path)
442 };
443
444 let mut url = format!("{}{}", self.base_url, url_path);
445
446 if !query.is_empty() {
447 url.push('?');
448 url.push_str(query);
449 }
450 url
451 }
452
453 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
454 if use_orders {
455 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
456 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
457 keys.extend(self.order_rate_keys.iter().cloned());
458 keys
459 } else {
460 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
461 }
462 }
463
464 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
465 let status = response.status.as_u16();
466 let body = String::from_utf8_lossy(&response.body).to_string();
467
468 if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
469 return Err(BinanceFuturesHttpError::BinanceError {
470 code: err.code,
471 message: err.msg,
472 });
473 }
474
475 Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
476 }
477
478 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
479 let mut headers = HashMap::new();
480 headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
481
482 if let Some(cred) = credential {
483 headers.insert(
484 BINANCE_API_KEY_HEADER.to_string(),
485 cred.api_key().to_string(),
486 );
487 }
488 headers
489 }
490
491 fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
492 match product_type {
493 BinanceProductType::UsdM => BINANCE_FAPI_PATH,
494 BinanceProductType::CoinM => BINANCE_DAPI_PATH,
495 _ => BINANCE_FAPI_PATH, }
497 }
498
499 fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
500 let quotas = match product_type {
501 BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
502 BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
503 _ => BINANCE_FAPI_RATE_LIMITS,
504 };
505
506 let mut keyed = Vec::new();
507 let mut order_keys = Vec::new();
508 let mut default = None;
509
510 for quota in quotas {
511 if let Some(q) = Self::quota_from(quota) {
512 match quota.rate_limit_type {
513 BinanceRateLimitType::RequestWeight if default.is_none() => {
514 default = Some(q);
515 }
516 BinanceRateLimitType::Orders => {
517 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
518 order_keys.push(key.clone());
519 keyed.push((key, q));
520 }
521 _ => {}
522 }
523 }
524 }
525
526 let default_quota = default.unwrap_or_else(|| {
527 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
528 });
529
530 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
531
532 RateLimitConfig {
533 default_quota: Some(default_quota),
534 keyed_quotas: keyed,
535 order_keys,
536 }
537 }
538
539 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
540 let burst = NonZeroU32::new(quota.limit)?;
541 match quota.interval {
542 BinanceRateLimitInterval::Second => Quota::per_second(burst),
543 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
544 BinanceRateLimitInterval::Day => {
545 Quota::with_period(Duration::from_secs(SECONDS_IN_DAY))
546 .map(|q| q.allow_burst(burst))
547 }
548 BinanceRateLimitInterval::Unknown => None,
549 }
550 }
551
552 pub async fn ticker_24h(
558 &self,
559 params: &BinanceTicker24hrParams,
560 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
561 self.get("ticker/24hr", Some(params), false, false).await
562 }
563
564 pub async fn book_ticker(
570 &self,
571 params: &BinanceBookTickerParams,
572 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
573 self.get("ticker/bookTicker", Some(params), false, false)
574 .await
575 }
576
577 pub async fn price_ticker(
583 &self,
584 symbol: Option<&str>,
585 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
586 #[derive(Serialize)]
587 struct Params<'a> {
588 #[serde(skip_serializing_if = "Option::is_none")]
589 symbol: Option<&'a str>,
590 }
591 self.get("ticker/price", Some(&Params { symbol }), false, false)
592 .await
593 }
594
595 pub async fn depth(
601 &self,
602 params: &BinanceDepthParams,
603 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
604 self.get("depth", Some(params), false, false).await
605 }
606
607 pub async fn mark_price(
613 &self,
614 params: &BinanceMarkPriceParams,
615 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
616 let response: MarkPriceResponse =
617 self.get("premiumIndex", Some(params), false, false).await?;
618 Ok(response.into())
619 }
620
621 pub async fn funding_rate(
627 &self,
628 params: &BinanceFundingRateParams,
629 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
630 self.get("fundingRate", Some(params), false, false).await
631 }
632
633 pub async fn open_interest(
639 &self,
640 params: &BinanceOpenInterestParams,
641 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
642 self.get("openInterest", Some(params), false, false).await
643 }
644
645 pub async fn trades(
651 &self,
652 params: &BinanceTradesParams,
653 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
654 self.get("trades", Some(params), false, false).await
655 }
656
657 pub async fn klines(
663 &self,
664 params: &BinanceKlinesParams,
665 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
666 self.get("klines", Some(params), false, false).await
667 }
668
669 pub async fn set_leverage(
675 &self,
676 params: &BinanceSetLeverageParams,
677 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
678 self.post("leverage", Some(params), None, true, false).await
679 }
680
681 pub async fn set_margin_type(
687 &self,
688 params: &BinanceSetMarginTypeParams,
689 ) -> BinanceFuturesHttpResult<serde_json::Value> {
690 self.post("marginType", Some(params), None, true, false)
691 .await
692 }
693
694 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
700 self.get::<(), _>("positionSide/dual", None, true, false)
701 .await
702 }
703
704 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
710 self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
711 .await
712 }
713
714 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
720 let params = ListenKeyParams {
721 listen_key: listen_key.to_string(),
722 };
723 let _: serde_json::Value = self
724 .request_put("listenKey", Some(¶ms), true, false)
725 .await?;
726 Ok(())
727 }
728
729 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
735 let params = ListenKeyParams {
736 listen_key: listen_key.to_string(),
737 };
738 let _: serde_json::Value = self
739 .request_delete("listenKey", Some(¶ms), true, false)
740 .await?;
741 Ok(())
742 }
743
744 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
750 let path = if self.api_path.starts_with("/fapi") {
752 "/fapi/v2/account"
753 } else {
754 "/dapi/v1/account"
755 };
756 self.get::<(), _>(path, None, true, false).await
757 }
758
759 pub async fn query_positions(
765 &self,
766 params: &BinancePositionRiskParams,
767 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
768 let path = if self.api_path.starts_with("/fapi") {
770 "/fapi/v2/positionRisk"
771 } else {
772 "/dapi/v1/positionRisk"
773 };
774 self.get(path, Some(params), true, false).await
775 }
776
777 pub async fn query_user_trades(
783 &self,
784 params: &BinanceUserTradesParams,
785 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
786 self.get("userTrades", Some(params), true, false).await
787 }
788
789 pub async fn query_order(
795 &self,
796 params: &BinanceOrderQueryParams,
797 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
798 self.get("order", Some(params), true, false).await
799 }
800
801 pub async fn query_open_orders(
807 &self,
808 params: &BinanceOpenOrdersParams,
809 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
810 self.get("openOrders", Some(params), true, false).await
811 }
812
813 pub async fn query_all_orders(
819 &self,
820 params: &BinanceAllOrdersParams,
821 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
822 self.get("allOrders", Some(params), true, false).await
823 }
824
825 pub async fn submit_order(
831 &self,
832 params: &BinanceNewOrderParams,
833 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
834 self.post("order", Some(params), None, true, true).await
835 }
836
837 pub async fn submit_order_list(
843 &self,
844 orders: &[BatchOrderItem],
845 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
846 if orders.is_empty() {
847 return Ok(Vec::new());
848 }
849
850 if orders.len() > 5 {
851 return Err(BinanceFuturesHttpError::ValidationError(
852 "Batch order limit is 5 orders maximum".to_string(),
853 ));
854 }
855
856 self.batch_request("batchOrders", orders, true).await
857 }
858
859 pub async fn modify_order(
865 &self,
866 params: &BinanceModifyOrderParams,
867 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
868 self.request_put("order", Some(params), true, true).await
869 }
870
871 pub async fn batch_modify_orders(
877 &self,
878 modifies: &[BatchModifyItem],
879 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
880 if modifies.is_empty() {
881 return Ok(Vec::new());
882 }
883
884 if modifies.len() > 5 {
885 return Err(BinanceFuturesHttpError::ValidationError(
886 "Batch modify limit is 5 orders maximum".to_string(),
887 ));
888 }
889
890 self.batch_request_put("batchOrders", modifies, true).await
891 }
892
893 pub async fn cancel_order(
899 &self,
900 params: &BinanceCancelOrderParams,
901 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
902 self.request_delete("order", Some(params), true, true).await
903 }
904
905 pub async fn cancel_all_orders(
911 &self,
912 params: &BinanceCancelAllOrdersParams,
913 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
914 self.request_delete("allOpenOrders", Some(params), true, true)
915 .await
916 }
917
918 pub async fn batch_cancel_orders(
924 &self,
925 cancels: &[BatchCancelItem],
926 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
927 if cancels.is_empty() {
928 return Ok(Vec::new());
929 }
930
931 if cancels.len() > 5 {
932 return Err(BinanceFuturesHttpError::ValidationError(
933 "Batch cancel limit is 5 orders maximum".to_string(),
934 ));
935 }
936
937 self.batch_request_delete("batchOrders", cancels, true)
938 .await
939 }
940
941 pub async fn submit_algo_order(
950 &self,
951 params: &BinanceNewAlgoOrderParams,
952 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
953 self.post("algoOrder", Some(params), None, true, true).await
954 }
955
956 pub async fn cancel_algo_order(
964 &self,
965 params: &BinanceAlgoOrderQueryParams,
966 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
967 self.request_delete("algoOrder", Some(params), true, true)
968 .await
969 }
970
971 pub async fn query_algo_order(
979 &self,
980 params: &BinanceAlgoOrderQueryParams,
981 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
982 self.get("algoOrder", Some(params), true, false).await
983 }
984
985 pub async fn query_open_algo_orders(
991 &self,
992 params: &BinanceOpenAlgoOrdersParams,
993 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
994 self.get("openAlgoOrders", Some(params), true, false).await
995 }
996
997 pub async fn query_all_algo_orders(
1003 &self,
1004 params: &BinanceAllAlgoOrdersParams,
1005 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1006 self.get("allAlgoOrders", Some(params), true, false).await
1007 }
1008
1009 pub async fn cancel_all_algo_orders(
1015 &self,
1016 params: &BinanceCancelAllAlgoOrdersParams,
1017 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1018 self.request_delete("algoOpenOrders", Some(params), true, true)
1019 .await
1020 }
1021}
1022
1023#[derive(Debug, Deserialize)]
1025#[serde(untagged)]
1026enum MarkPriceResponse {
1027 Single(BinanceFuturesMarkPrice),
1028 Multiple(Vec<BinanceFuturesMarkPrice>),
1029}
1030
1031impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1032 fn from(response: MarkPriceResponse) -> Self {
1033 match response {
1034 MarkPriceResponse::Single(price) => vec![price],
1035 MarkPriceResponse::Multiple(prices) => prices,
1036 }
1037 }
1038}
1039
1040struct RateLimitConfig {
1041 default_quota: Option<Quota>,
1042 keyed_quotas: Vec<(String, Quota)>,
1043 order_keys: Vec<String>,
1044}
1045
1046#[derive(Clone, Debug)]
1048pub enum BinanceFuturesInstrument {
1049 UsdM(BinanceFuturesUsdSymbol),
1051 CoinM(BinanceFuturesCoinSymbol),
1053}
1054
1055impl BinanceFuturesInstrument {
1056 #[must_use]
1058 pub const fn symbol(&self) -> Ustr {
1059 match self {
1060 Self::UsdM(s) => s.symbol,
1061 Self::CoinM(s) => s.symbol,
1062 }
1063 }
1064
1065 #[must_use]
1067 pub const fn price_precision(&self) -> i32 {
1068 match self {
1069 Self::UsdM(s) => s.price_precision,
1070 Self::CoinM(s) => s.price_precision,
1071 }
1072 }
1073
1074 #[must_use]
1076 pub const fn quantity_precision(&self) -> i32 {
1077 match self {
1078 Self::UsdM(s) => s.quantity_precision,
1079 Self::CoinM(s) => s.quantity_precision,
1080 }
1081 }
1082
1083 #[must_use]
1085 pub fn id(&self) -> InstrumentId {
1086 match self {
1087 Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1088 Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1089 }
1090 }
1091
1092 #[must_use]
1094 pub fn quote_currency(&self) -> Currency {
1095 let quote_asset = match self {
1096 Self::UsdM(s) => &s.quote_asset,
1097 Self::CoinM(s) => &s.quote_asset,
1098 };
1099 Currency::from(quote_asset.as_str())
1100 }
1101}
1102
1103#[derive(Debug, Clone)]
1105pub struct BinanceFuturesHttpClient {
1106 inner: Arc<BinanceRawFuturesHttpClient>,
1107 product_type: BinanceProductType,
1108 clock: &'static AtomicTime,
1109 instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1110 treat_expired_as_canceled: bool,
1111}
1112
1113impl BinanceFuturesHttpClient {
1114 #[expect(clippy::too_many_arguments)]
1120 pub fn new(
1121 product_type: BinanceProductType,
1122 environment: BinanceEnvironment,
1123 clock: &'static AtomicTime,
1124 api_key: Option<String>,
1125 api_secret: Option<String>,
1126 base_url_override: Option<String>,
1127 recv_window: Option<u64>,
1128 timeout_secs: Option<u64>,
1129 proxy_url: Option<String>,
1130 treat_expired_as_canceled: bool,
1131 ) -> BinanceFuturesHttpResult<Self> {
1132 match product_type {
1133 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1134 _ => {
1135 return Err(BinanceFuturesHttpError::ValidationError(format!(
1136 "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1137 )));
1138 }
1139 }
1140
1141 let raw = BinanceRawFuturesHttpClient::new(
1142 product_type,
1143 environment,
1144 api_key,
1145 api_secret,
1146 base_url_override,
1147 recv_window,
1148 timeout_secs,
1149 proxy_url,
1150 )?;
1151
1152 Ok(Self {
1153 inner: Arc::new(raw),
1154 product_type,
1155 clock,
1156 instruments: Arc::new(DashMap::new()),
1157 treat_expired_as_canceled,
1158 })
1159 }
1160
1161 #[must_use]
1163 pub const fn product_type(&self) -> BinanceProductType {
1164 self.product_type
1165 }
1166
1167 #[must_use]
1169 pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1170 &self.inner
1171 }
1172
1173 #[must_use]
1175 pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1176 Arc::clone(&self.instruments)
1177 }
1178
1179 pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1185 self.inner
1186 .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1187 .await
1188 }
1189
1190 pub async fn set_leverage(
1196 &self,
1197 params: &BinanceSetLeverageParams,
1198 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1199 self.inner.set_leverage(params).await
1200 }
1201
1202 pub async fn set_margin_type(
1208 &self,
1209 params: &BinanceSetMarginTypeParams,
1210 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1211 self.inner.set_margin_type(params).await
1212 }
1213
1214 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1220 self.inner.query_hedge_mode().await
1221 }
1222
1223 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1229 self.inner.create_listen_key().await
1230 }
1231
1232 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1238 self.inner.keepalive_listen_key(listen_key).await
1239 }
1240
1241 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1247 self.inner.close_listen_key(listen_key).await
1248 }
1249
1250 pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1256 match self.product_type {
1257 BinanceProductType::UsdM => {
1258 let info: BinanceFuturesUsdExchangeInfo = self
1259 .inner
1260 .get("exchangeInfo", None::<&()>, false, false)
1261 .await?;
1262
1263 for symbol in info.symbols {
1264 self.instruments
1265 .insert(symbol.symbol, BinanceFuturesInstrument::UsdM(symbol));
1266 }
1267 }
1268 BinanceProductType::CoinM => {
1269 let info: BinanceFuturesCoinExchangeInfo = self
1270 .inner
1271 .get("exchangeInfo", None::<&()>, false, false)
1272 .await?;
1273
1274 for symbol in info.symbols {
1275 self.instruments
1276 .insert(symbol.symbol, BinanceFuturesInstrument::CoinM(symbol));
1277 }
1278 }
1279 _ => {
1280 return Err(BinanceFuturesHttpError::ValidationError(
1281 "Invalid product type for futures".to_string(),
1282 ));
1283 }
1284 }
1285
1286 Ok(())
1287 }
1288
1289 pub async fn request_symbol_statuses(
1299 &self,
1300 ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1301 let mut statuses = AHashMap::new();
1302
1303 match self.product_type {
1304 BinanceProductType::UsdM => {
1305 let info: BinanceFuturesUsdExchangeInfo = self
1306 .inner
1307 .get("exchangeInfo", None::<&()>, false, false)
1308 .await?;
1309
1310 for symbol in &info.symbols {
1311 statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1312 }
1313 }
1314 BinanceProductType::CoinM => {
1315 let info: BinanceFuturesCoinExchangeInfo = self
1316 .inner
1317 .get("exchangeInfo", None::<&()>, false, false)
1318 .await?;
1319
1320 for symbol in &info.symbols {
1321 let action = symbol
1322 .contract_status
1323 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1324 statuses.insert(symbol.symbol, action);
1325 }
1326 }
1327 _ => {
1328 return Err(BinanceFuturesHttpError::ValidationError(
1329 "Invalid product type for futures".to_string(),
1330 ));
1331 }
1332 }
1333
1334 Ok(statuses)
1335 }
1336
1337 pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1343 let ts_init = UnixNanos::default();
1344
1345 let instruments = match self.product_type {
1346 BinanceProductType::UsdM => {
1347 let info: BinanceFuturesUsdExchangeInfo = self
1348 .inner
1349 .get("exchangeInfo", None::<&()>, false, false)
1350 .await?;
1351
1352 let mut instruments = Vec::with_capacity(info.symbols.len());
1353
1354 for symbol in info.symbols {
1355 self.instruments.insert(
1357 symbol.symbol,
1358 BinanceFuturesInstrument::UsdM(symbol.clone()),
1359 );
1360
1361 match parse_usdm_instrument(&symbol, ts_init, ts_init) {
1362 Ok(instrument) => instruments.push(instrument),
1363 Err(e) => {
1364 log::debug!(
1365 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1366 symbol.symbol
1367 );
1368 }
1369 }
1370 }
1371
1372 log::info!(
1373 "Loaded USD-M perpetual instruments: count={}",
1374 instruments.len()
1375 );
1376 instruments
1377 }
1378 BinanceProductType::CoinM => {
1379 let info: BinanceFuturesCoinExchangeInfo = self
1380 .inner
1381 .get("exchangeInfo", None::<&()>, false, false)
1382 .await?;
1383
1384 let mut instruments = Vec::with_capacity(info.symbols.len());
1385 for symbol in info.symbols {
1386 self.instruments.insert(
1388 symbol.symbol,
1389 BinanceFuturesInstrument::CoinM(symbol.clone()),
1390 );
1391
1392 match parse_coinm_instrument(&symbol, ts_init, ts_init) {
1393 Ok(instrument) => instruments.push(instrument),
1394 Err(e) => {
1395 log::debug!(
1396 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1397 symbol.symbol
1398 );
1399 }
1400 }
1401 }
1402
1403 log::info!(
1404 "Loaded COIN-M perpetual instruments: count={}",
1405 instruments.len()
1406 );
1407 instruments
1408 }
1409 _ => {
1410 return Err(BinanceFuturesHttpError::ValidationError(
1411 "Invalid product type for futures".to_string(),
1412 ));
1413 }
1414 };
1415
1416 Ok(instruments)
1417 }
1418
1419 pub async fn ticker_24h(
1425 &self,
1426 params: &BinanceTicker24hrParams,
1427 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1428 self.inner.ticker_24h(params).await
1429 }
1430
1431 pub async fn book_ticker(
1437 &self,
1438 params: &BinanceBookTickerParams,
1439 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1440 self.inner.book_ticker(params).await
1441 }
1442
1443 pub async fn price_ticker(
1449 &self,
1450 symbol: Option<&str>,
1451 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1452 self.inner.price_ticker(symbol).await
1453 }
1454
1455 pub async fn depth(
1461 &self,
1462 params: &BinanceDepthParams,
1463 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1464 self.inner.depth(params).await
1465 }
1466
1467 pub async fn mark_price(
1473 &self,
1474 params: &BinanceMarkPriceParams,
1475 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
1476 self.inner.mark_price(params).await
1477 }
1478
1479 pub async fn funding_rate(
1485 &self,
1486 params: &BinanceFundingRateParams,
1487 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
1488 self.inner.funding_rate(params).await
1489 }
1490
1491 pub async fn open_interest(
1497 &self,
1498 params: &BinanceOpenInterestParams,
1499 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
1500 self.inner.open_interest(params).await
1501 }
1502
1503 pub async fn query_order(
1509 &self,
1510 params: &BinanceOrderQueryParams,
1511 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1512 self.inner.query_order(params).await
1513 }
1514
1515 pub async fn query_open_orders(
1521 &self,
1522 params: &BinanceOpenOrdersParams,
1523 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1524 self.inner.query_open_orders(params).await
1525 }
1526
1527 pub async fn query_all_orders(
1533 &self,
1534 params: &BinanceAllOrdersParams,
1535 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1536 self.inner.query_all_orders(params).await
1537 }
1538
1539 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1545 self.inner.query_account().await
1546 }
1547
1548 pub async fn query_positions(
1554 &self,
1555 params: &BinancePositionRiskParams,
1556 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1557 self.inner.query_positions(params).await
1558 }
1559
1560 pub async fn query_user_trades(
1566 &self,
1567 params: &BinanceUserTradesParams,
1568 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1569 self.inner.query_user_trades(params).await
1570 }
1571
1572 #[expect(clippy::too_many_arguments)]
1582 pub async fn submit_order(
1583 &self,
1584 account_id: AccountId,
1585 instrument_id: InstrumentId,
1586 client_order_id: ClientOrderId,
1587 order_side: OrderSide,
1588 order_type: OrderType,
1589 quantity: Quantity,
1590 time_in_force: TimeInForce,
1591 price: Option<Price>,
1592 trigger_price: Option<Price>,
1593 reduce_only: bool,
1594 post_only: bool,
1595 position_side: Option<BinancePositionSide>,
1596 price_match: Option<BinancePriceMatch>,
1597 ) -> anyhow::Result<OrderStatusReport> {
1598 let symbol = format_binance_symbol(&instrument_id);
1599 let size_precision = self.get_size_precision(&symbol)?;
1600
1601 let binance_side = BinanceSide::try_from(order_side)?;
1602 let binance_order_type = order_type_to_binance_futures(order_type)?;
1603 let binance_tif = if post_only {
1604 BinanceTimeInForce::Gtx
1605 } else {
1606 BinanceTimeInForce::try_from(time_in_force)?
1607 };
1608
1609 let requires_trigger_price = matches!(
1610 order_type,
1611 OrderType::StopMarket
1612 | OrderType::StopLimit
1613 | OrderType::TrailingStopMarket
1614 | OrderType::MarketIfTouched
1615 | OrderType::LimitIfTouched
1616 );
1617
1618 if requires_trigger_price && trigger_price.is_none() {
1619 anyhow::bail!("Order type {order_type:?} requires a trigger price");
1620 }
1621
1622 let requires_time_in_force = matches!(
1624 order_type,
1625 OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
1626 );
1627
1628 let qty_str = quantity.to_string();
1629 let price_str = if price_match.is_some() {
1630 None
1631 } else {
1632 price.map(|p| p.to_string())
1633 };
1634 let stop_price_str = trigger_price.map(|p| p.to_string());
1635 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1636
1637 let params = BinanceNewOrderParams {
1638 symbol,
1639 side: binance_side,
1640 order_type: binance_order_type,
1641 time_in_force: if requires_time_in_force {
1642 Some(binance_tif)
1643 } else {
1644 None
1645 },
1646 quantity: Some(qty_str),
1647 price: price_str,
1648 new_client_order_id: Some(client_id_str),
1649 stop_price: stop_price_str,
1650 reduce_only: if reduce_only { Some(true) } else { None },
1651 position_side,
1652 close_position: None,
1653 activation_price: None,
1654 callback_rate: None,
1655 working_type: None,
1656 price_protect: None,
1657 new_order_resp_type: None,
1658 good_till_date: None,
1659 recv_window: None,
1660 price_match,
1661 self_trade_prevention_mode: None,
1662 };
1663
1664 let order = self.inner.submit_order(¶ms).await?;
1665 let ts_init = self.clock.get_time_ns();
1666 order.to_order_status_report(
1667 account_id,
1668 instrument_id,
1669 size_precision,
1670 self.treat_expired_as_canceled,
1671 ts_init,
1672 )
1673 }
1674
1675 #[expect(clippy::too_many_arguments)]
1688 pub async fn submit_algo_order(
1689 &self,
1690 account_id: AccountId,
1691 instrument_id: InstrumentId,
1692 client_order_id: ClientOrderId,
1693 order_side: OrderSide,
1694 order_type: OrderType,
1695 quantity: Quantity,
1696 time_in_force: TimeInForce,
1697 price: Option<Price>,
1698 trigger_price: Option<Price>,
1699 reduce_only: bool,
1700 close_position: bool,
1701 position_side: Option<BinancePositionSide>,
1702 activation_price: Option<Price>,
1703 callback_rate: Option<String>,
1704 working_type: Option<BinanceWorkingType>,
1705 ) -> anyhow::Result<OrderStatusReport> {
1706 let symbol = format_binance_symbol(&instrument_id);
1707 let size_precision = self.get_size_precision(&symbol)?;
1708
1709 let binance_side = BinanceSide::try_from(order_side)?;
1710 let binance_order_type = order_type_to_binance_futures(order_type)?;
1711 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1712
1713 anyhow::ensure!(
1714 trigger_price.is_some(),
1715 "Algo order type {order_type:?} requires a trigger price"
1716 );
1717
1718 let requires_time_in_force =
1720 matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
1721
1722 let price_str = price.map(|p| p.to_string());
1723 let trigger_price_str = trigger_price.map(|p| p.to_string());
1724 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1725
1726 let params = if close_position {
1728 BinanceNewAlgoOrderParams {
1729 symbol,
1730 side: binance_side,
1731 order_type: binance_order_type,
1732 algo_type: BinanceAlgoType::Conditional,
1733 position_side,
1734 quantity: None,
1735 price: price_str,
1736 trigger_price: trigger_price_str,
1737 time_in_force: if requires_time_in_force {
1738 Some(binance_tif)
1739 } else {
1740 None
1741 },
1742 working_type,
1743 close_position: Some(true),
1744 price_protect: None,
1745 reduce_only: None,
1746 activation_price: activation_price.map(|p| p.to_string()),
1747 callback_rate,
1748 client_algo_id: Some(client_id_str),
1749 good_till_date: None,
1750 recv_window: None,
1751 }
1752 } else {
1753 let qty_str = quantity.to_string();
1754 BinanceNewAlgoOrderParams {
1755 symbol,
1756 side: binance_side,
1757 order_type: binance_order_type,
1758 algo_type: BinanceAlgoType::Conditional,
1759 position_side,
1760 quantity: Some(qty_str),
1761 price: price_str,
1762 trigger_price: trigger_price_str,
1763 time_in_force: if requires_time_in_force {
1764 Some(binance_tif)
1765 } else {
1766 None
1767 },
1768 working_type,
1769 close_position: None,
1770 price_protect: None,
1771 reduce_only: if reduce_only { Some(true) } else { None },
1772 activation_price: activation_price.map(|p| p.to_string()),
1773 callback_rate,
1774 client_algo_id: Some(client_id_str),
1775 good_till_date: None,
1776 recv_window: None,
1777 }
1778 };
1779
1780 let order = self.inner.submit_algo_order(¶ms).await?;
1781 let ts_init = self.clock.get_time_ns();
1782 order.to_order_status_report(account_id, instrument_id, size_precision, ts_init)
1783 }
1784
1785 pub async fn submit_order_list(
1794 &self,
1795 orders: &[BatchOrderItem],
1796 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1797 self.inner.submit_order_list(orders).await
1798 }
1799
1800 #[expect(clippy::too_many_arguments)]
1811 pub async fn modify_order(
1812 &self,
1813 account_id: AccountId,
1814 instrument_id: InstrumentId,
1815 venue_order_id: Option<VenueOrderId>,
1816 client_order_id: Option<ClientOrderId>,
1817 order_side: OrderSide,
1818 quantity: Quantity,
1819 price: Price,
1820 ) -> anyhow::Result<OrderStatusReport> {
1821 anyhow::ensure!(
1822 venue_order_id.is_some() || client_order_id.is_some(),
1823 "Either venue_order_id or client_order_id must be provided"
1824 );
1825
1826 let symbol = format_binance_symbol(&instrument_id);
1827 let size_precision = self.get_size_precision(&symbol)?;
1828
1829 let binance_side = BinanceSide::try_from(order_side)?;
1830
1831 let order_id = venue_order_id
1832 .map(|id| id.inner().parse::<i64>())
1833 .transpose()
1834 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1835
1836 let params = BinanceModifyOrderParams {
1837 symbol,
1838 order_id,
1839 orig_client_order_id: client_order_id
1840 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1841 side: binance_side,
1842 quantity: quantity.to_string(),
1843 price: price.to_string(),
1844 recv_window: None,
1845 };
1846
1847 let order = self.inner.modify_order(¶ms).await?;
1848 let ts_init = self.clock.get_time_ns();
1849 order.to_order_status_report(
1850 account_id,
1851 instrument_id,
1852 size_precision,
1853 self.treat_expired_as_canceled,
1854 ts_init,
1855 )
1856 }
1857
1858 pub async fn batch_modify_orders(
1867 &self,
1868 modifies: &[BatchModifyItem],
1869 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1870 self.inner.batch_modify_orders(modifies).await
1871 }
1872
1873 pub async fn cancel_order(
1883 &self,
1884 instrument_id: InstrumentId,
1885 venue_order_id: Option<VenueOrderId>,
1886 client_order_id: Option<ClientOrderId>,
1887 ) -> anyhow::Result<VenueOrderId> {
1888 anyhow::ensure!(
1889 venue_order_id.is_some() || client_order_id.is_some(),
1890 "Either venue_order_id or client_order_id must be provided"
1891 );
1892
1893 let symbol = format_binance_symbol(&instrument_id);
1894
1895 let order_id = venue_order_id
1896 .map(|id| id.inner().parse::<i64>())
1897 .transpose()
1898 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1899
1900 let params = BinanceCancelOrderParams {
1901 symbol,
1902 order_id,
1903 orig_client_order_id: client_order_id
1904 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1905 recv_window: None,
1906 };
1907
1908 let order = self.inner.cancel_order(¶ms).await?;
1909 Ok(VenueOrderId::new(order.order_id.to_string()))
1910 }
1911
1912 pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
1921 let params = BinanceAlgoOrderQueryParams {
1922 algo_id: None,
1923 client_algo_id: Some(encode_broker_id(
1924 &client_order_id,
1925 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1926 )),
1927 recv_window: None,
1928 };
1929
1930 let response = self.inner.cancel_algo_order(¶ms).await?;
1931 if response.code.parse::<i32>().unwrap_or(0) == 200 {
1932 Ok(())
1933 } else {
1934 anyhow::bail!(
1935 "Cancel algo order failed: code={}, msg={}",
1936 response.code,
1937 response.msg
1938 )
1939 }
1940 }
1941
1942 pub async fn cancel_all_orders(
1948 &self,
1949 instrument_id: InstrumentId,
1950 ) -> anyhow::Result<Vec<VenueOrderId>> {
1951 let symbol = format_binance_symbol(&instrument_id);
1952
1953 let params = BinanceCancelAllOrdersParams {
1954 symbol,
1955 recv_window: None,
1956 };
1957
1958 let response = self.inner.cancel_all_orders(¶ms).await?;
1959 if response.code == 200 {
1960 Ok(vec![])
1961 } else {
1962 anyhow::bail!("Cancel all orders failed: {}", response.msg);
1963 }
1964 }
1965
1966 pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
1972 let symbol = format_binance_symbol(&instrument_id);
1973
1974 let params = BinanceCancelAllAlgoOrdersParams {
1975 symbol,
1976 recv_window: None,
1977 };
1978
1979 let response = self.inner.cancel_all_algo_orders(¶ms).await?;
1980 if response.code == 200 {
1981 Ok(())
1982 } else {
1983 anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
1984 }
1985 }
1986
1987 pub async fn batch_cancel_orders(
1996 &self,
1997 cancels: &[BatchCancelItem],
1998 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1999 self.inner.batch_cancel_orders(cancels).await
2000 }
2001
2002 pub async fn query_open_algo_orders(
2010 &self,
2011 instrument_id: Option<InstrumentId>,
2012 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2013 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2014
2015 let params = BinanceOpenAlgoOrdersParams {
2016 symbol,
2017 recv_window: None,
2018 };
2019
2020 self.inner.query_open_algo_orders(¶ms).await
2021 }
2022
2023 pub async fn query_algo_order(
2029 &self,
2030 client_order_id: ClientOrderId,
2031 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2032 let params = BinanceAlgoOrderQueryParams {
2033 algo_id: None,
2034 client_algo_id: Some(encode_broker_id(
2035 &client_order_id,
2036 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2037 )),
2038 recv_window: None,
2039 };
2040
2041 self.inner.query_algo_order(¶ms).await
2042 }
2043
2044 fn get_size_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2046 let instrument = self
2047 .instruments
2048 .get(&Ustr::from(symbol))
2049 .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2050
2051 let precision = match instrument.value() {
2052 BinanceFuturesInstrument::UsdM(s) => s.quantity_precision,
2053 BinanceFuturesInstrument::CoinM(s) => s.quantity_precision,
2054 };
2055
2056 Ok(precision as u8)
2057 }
2058
2059 fn get_price_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2061 let instrument = self
2062 .instruments
2063 .get(&Ustr::from(symbol))
2064 .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2065
2066 let precision = match instrument.value() {
2067 BinanceFuturesInstrument::UsdM(s) => s.price_precision,
2068 BinanceFuturesInstrument::CoinM(s) => s.price_precision,
2069 };
2070
2071 Ok(precision as u8)
2072 }
2073
2074 pub async fn request_account_state(
2080 &self,
2081 account_id: AccountId,
2082 ) -> anyhow::Result<AccountState> {
2083 let ts_init = UnixNanos::default();
2084 let account_info = self.inner.query_account().await?;
2085 account_info.to_account_state(account_id, ts_init)
2086 }
2087
2088 pub async fn request_order_status_report(
2096 &self,
2097 account_id: AccountId,
2098 instrument_id: InstrumentId,
2099 venue_order_id: Option<VenueOrderId>,
2100 client_order_id: Option<ClientOrderId>,
2101 ) -> anyhow::Result<OrderStatusReport> {
2102 anyhow::ensure!(
2103 venue_order_id.is_some() || client_order_id.is_some(),
2104 "Either venue_order_id or client_order_id must be provided"
2105 );
2106
2107 let symbol = format_binance_symbol(&instrument_id);
2108 let size_precision = self.get_size_precision(&symbol)?;
2109
2110 let order_id = venue_order_id
2111 .map(|id| id.inner().parse::<i64>())
2112 .transpose()
2113 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2114
2115 let orig_client_order_id =
2116 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2117
2118 let params = BinanceOrderQueryParams {
2119 symbol,
2120 order_id,
2121 orig_client_order_id,
2122 recv_window: None,
2123 };
2124
2125 let order = self.inner.query_order(¶ms).await?;
2126 let ts_init = self.clock.get_time_ns();
2127 order.to_order_status_report(
2128 account_id,
2129 instrument_id,
2130 size_precision,
2131 self.treat_expired_as_canceled,
2132 ts_init,
2133 )
2134 }
2135
2136 pub async fn request_order_status_reports(
2144 &self,
2145 account_id: AccountId,
2146 instrument_id: Option<InstrumentId>,
2147 open_only: bool,
2148 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2149 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2150
2151 let orders = if open_only {
2152 let params = BinanceOpenOrdersParams {
2153 symbol: symbol.clone(),
2154 recv_window: None,
2155 };
2156 self.inner.query_open_orders(¶ms).await?
2157 } else {
2158 let symbol = symbol.ok_or_else(|| {
2160 anyhow::anyhow!("instrument_id is required for historical orders")
2161 })?;
2162 let params = BinanceAllOrdersParams {
2163 symbol,
2164 order_id: None,
2165 start_time: None,
2166 end_time: None,
2167 limit: None,
2168 recv_window: None,
2169 };
2170 self.inner.query_all_orders(¶ms).await?
2171 };
2172
2173 let ts_init = self.clock.get_time_ns();
2174 let mut reports = Vec::with_capacity(orders.len());
2175
2176 for order in orders {
2177 let order_instrument_id = instrument_id.unwrap_or_else(|| {
2178 let suffix = self.product_type.suffix();
2180 InstrumentId::from(format!("{}{}.BINANCE", order.symbol, suffix))
2181 });
2182
2183 let size_precision = self.get_size_precision(&order.symbol).unwrap_or(8); match order.to_order_status_report(
2186 account_id,
2187 order_instrument_id,
2188 size_precision,
2189 self.treat_expired_as_canceled,
2190 ts_init,
2191 ) {
2192 Ok(report) => reports.push(report),
2193 Err(e) => {
2194 log::warn!("Failed to parse order status report: {e}");
2195 }
2196 }
2197 }
2198
2199 Ok(reports)
2200 }
2201
2202 pub async fn request_fill_reports(
2208 &self,
2209 account_id: AccountId,
2210 instrument_id: InstrumentId,
2211 venue_order_id: Option<VenueOrderId>,
2212 start: Option<i64>,
2213 end: Option<i64>,
2214 limit: Option<u32>,
2215 ) -> anyhow::Result<Vec<FillReport>> {
2216 let symbol = format_binance_symbol(&instrument_id);
2217 let size_precision = self.get_size_precision(&symbol)?;
2218 let price_precision = self.get_price_precision(&symbol)?;
2219
2220 let order_id = venue_order_id
2221 .map(|id| id.inner().parse::<i64>())
2222 .transpose()
2223 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2224
2225 let params = BinanceUserTradesParams {
2226 symbol,
2227 order_id,
2228 start_time: start,
2229 end_time: end,
2230 from_id: None,
2231 limit,
2232 recv_window: None,
2233 };
2234
2235 let trades = self.inner.query_user_trades(¶ms).await?;
2236
2237 let ts_init = self.clock.get_time_ns();
2238 let mut reports = Vec::with_capacity(trades.len());
2239
2240 for trade in trades {
2241 match trade.to_fill_report(
2242 account_id,
2243 instrument_id,
2244 price_precision,
2245 size_precision,
2246 ts_init,
2247 ) {
2248 Ok(report) => reports.push(report),
2249 Err(e) => {
2250 log::warn!("Failed to parse fill report: {e}");
2251 }
2252 }
2253 }
2254
2255 Ok(reports)
2256 }
2257
2258 pub async fn request_trades(
2264 &self,
2265 instrument_id: InstrumentId,
2266 limit: Option<u32>,
2267 ) -> anyhow::Result<Vec<TradeTick>> {
2268 let symbol = format_binance_symbol(&instrument_id);
2269 let size_precision = self.get_size_precision(&symbol)?;
2270 let price_precision = self.get_price_precision(&symbol)?;
2271
2272 let params = BinanceTradesParams { symbol, limit };
2273
2274 let trades = self.inner.trades(¶ms).await?;
2275 let ts_init = UnixNanos::default();
2276
2277 let mut result = Vec::with_capacity(trades.len());
2278 for trade in trades {
2279 let price: f64 = trade.price.parse().unwrap_or(0.0);
2280 let size: f64 = trade.qty.parse().unwrap_or(0.0);
2281 let ts_event = UnixNanos::from_millis(trade.time as u64);
2282
2283 let aggressor_side = if trade.is_buyer_maker {
2284 AggressorSide::Seller
2285 } else {
2286 AggressorSide::Buyer
2287 };
2288
2289 let tick = TradeTick::new(
2290 instrument_id,
2291 Price::new(price, price_precision),
2292 Quantity::new(size, size_precision),
2293 aggressor_side,
2294 TradeId::new(trade.id.to_string()),
2295 ts_event,
2296 ts_init,
2297 );
2298 result.push(tick);
2299 }
2300
2301 Ok(result)
2302 }
2303
2304 pub async fn request_bars(
2311 &self,
2312 bar_type: BarType,
2313 start: Option<DateTime<Utc>>,
2314 end: Option<DateTime<Utc>>,
2315 limit: Option<u32>,
2316 ) -> anyhow::Result<Vec<Bar>> {
2317 anyhow::ensure!(
2318 bar_type.aggregation_source() == AggregationSource::External,
2319 "Only EXTERNAL aggregation is supported"
2320 );
2321
2322 let spec = bar_type.spec();
2323 let step = spec.step.get();
2324 let interval = match spec.aggregation {
2325 BarAggregation::Second => {
2326 anyhow::bail!("Binance Futures does not support second-level kline intervals")
2327 }
2328 BarAggregation::Minute => format!("{step}m"),
2329 BarAggregation::Hour => format!("{step}h"),
2330 BarAggregation::Day => format!("{step}d"),
2331 BarAggregation::Week => format!("{step}w"),
2332 BarAggregation::Month => format!("{step}M"),
2333 a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
2334 };
2335
2336 let symbol = format_binance_symbol(&bar_type.instrument_id());
2337 let price_precision = self.get_price_precision(&symbol)?;
2338 let size_precision = self.get_size_precision(&symbol)?;
2339
2340 let params = BinanceKlinesParams {
2341 symbol,
2342 interval,
2343 start_time: start.map(|dt| dt.timestamp_millis()),
2344 end_time: end.map(|dt| dt.timestamp_millis()),
2345 limit,
2346 };
2347
2348 let klines = self.inner.klines(¶ms).await?;
2349 let ts_init = UnixNanos::default();
2350
2351 let mut result = Vec::with_capacity(klines.len());
2352 for kline in klines {
2353 let open: f64 = kline.open.parse().unwrap_or(0.0);
2354 let high: f64 = kline.high.parse().unwrap_or(0.0);
2355 let low: f64 = kline.low.parse().unwrap_or(0.0);
2356 let close: f64 = kline.close.parse().unwrap_or(0.0);
2357 let volume: f64 = kline.volume.parse().unwrap_or(0.0);
2358
2359 let ts_event = UnixNanos::from_millis(kline.close_time as u64);
2361
2362 let bar = Bar::new(
2363 bar_type,
2364 Price::new(open, price_precision),
2365 Price::new(high, price_precision),
2366 Price::new(low, price_precision),
2367 Price::new(close, price_precision),
2368 Quantity::new(volume, size_precision),
2369 ts_event,
2370 ts_init,
2371 );
2372 result.push(bar);
2373 }
2374
2375 Ok(result)
2376 }
2377}
2378
2379#[must_use]
2384pub fn is_algo_order_type(order_type: OrderType) -> bool {
2385 matches!(
2386 order_type,
2387 OrderType::StopMarket
2388 | OrderType::StopLimit
2389 | OrderType::MarketIfTouched
2390 | OrderType::LimitIfTouched
2391 | OrderType::TrailingStopMarket
2392 )
2393}
2394
2395pub(crate) fn order_type_to_binance_futures(
2397 order_type: OrderType,
2398) -> anyhow::Result<BinanceFuturesOrderType> {
2399 match order_type {
2400 OrderType::Market => Ok(BinanceFuturesOrderType::Market),
2401 OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
2402 OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
2403 OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
2404 OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
2405 OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
2406 OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
2407 _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
2408 }
2409}
2410
2411#[cfg(test)]
2412mod tests {
2413 use nautilus_core::time::get_atomic_clock_realtime;
2414 use nautilus_network::http::{HttpStatus, StatusCode};
2415 use rstest::rstest;
2416 use tokio_util::bytes::Bytes;
2417
2418 use super::*;
2419
2420 #[rstest]
2421 fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
2422 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
2423
2424 assert!(config.default_quota.is_some());
2425 assert_eq!(config.order_keys.len(), 2);
2426 assert!(config.order_keys.iter().any(|k| k.contains("Second")));
2427 assert!(config.order_keys.iter().any(|k| k.contains("Minute")));
2428 }
2429
2430 #[rstest]
2431 fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
2432 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
2433
2434 assert!(config.default_quota.is_some());
2435 assert_eq!(config.order_keys.len(), 2);
2436 }
2437
2438 #[rstest]
2439 fn test_quota_from_unknown_interval_returns_none() {
2440 let quota = BinanceRateLimitQuota {
2441 rate_limit_type: BinanceRateLimitType::Orders,
2442 interval: BinanceRateLimitInterval::Unknown,
2443 interval_num: 1,
2444 limit: 10,
2445 };
2446
2447 assert!(BinanceRawFuturesHttpClient::quota_from("a).is_none());
2448 }
2449
2450 #[rstest]
2451 fn test_create_client_rejects_spot_product_type() {
2452 let result = BinanceFuturesHttpClient::new(
2453 BinanceProductType::Spot,
2454 BinanceEnvironment::Mainnet,
2455 get_atomic_clock_realtime(),
2456 None,
2457 None,
2458 None,
2459 None,
2460 None,
2461 None,
2462 false,
2463 );
2464
2465 assert!(result.is_err());
2466 }
2467
2468 fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
2469 BinanceRawFuturesHttpClient::new(
2470 BinanceProductType::UsdM,
2471 BinanceEnvironment::Mainnet,
2472 None,
2473 None,
2474 None,
2475 None,
2476 None,
2477 None,
2478 )
2479 .expect("Failed to create test client")
2480 }
2481
2482 #[rstest]
2483 fn test_parse_error_response_binance_error() {
2484 let client = create_test_raw_client();
2485 let response = HttpResponse {
2486 status: HttpStatus::new(StatusCode::BAD_REQUEST),
2487 headers: HashMap::new(),
2488 body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
2489 };
2490
2491 let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
2492
2493 match result {
2494 Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
2495 assert_eq!(code, -1121);
2496 assert_eq!(message, "Invalid symbol.");
2497 }
2498 other => panic!("Expected BinanceError, was {other:?}"),
2499 }
2500 }
2501}