1use std::{
19 collections::HashMap,
20 num::NonZeroU32,
21 sync::{Arc, LazyLock, Weak},
22 time::Duration,
23};
24
25use ahash::{AHashMap, AHashSet};
26use aws_lc_rs::digest;
27use dashmap::DashMap;
28use jiff::Timestamp;
29use nautilus_common::cache::InstrumentLookupError;
30use nautilus_core::{AtomicMap, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime};
31use nautilus_model::{
32 data::{Bar, BarType, BookOrder, FundingRateUpdate, TradeTick},
33 enums::{
34 AggregationSource, AggressorSide, BarAggregation, BookType, MarketStatusAction, OrderSide,
35 OrderType, TimeInForce,
36 },
37 events::AccountState,
38 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
39 instruments::{Instrument, any::InstrumentAny},
40 orderbook::OrderBook,
41 reports::{FillReport, OrderStatusReport},
42 types::{Currency, Price, Quantity, fixed::FIXED_PRECISION},
43};
44use nautilus_network::{
45 http::{
46 HttpClient, HttpRedirectPolicy, HttpResponse, Method, create_standard_nautilus_headers,
47 },
48 ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota},
49 retry::{RetryConfig, RetryError, RetryManager},
50};
51use parking_lot::Mutex;
52use rust_decimal::Decimal;
53use serde::{Deserialize, Serialize, de::DeserializeOwned};
54use ustr::Ustr;
55
56use super::{
57 error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
58 models::{
59 BatchOrderError, BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse,
60 BinanceFundingRate, BinanceFuturesAccountInfo, BinanceFuturesAggTrade,
61 BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
62 BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesCommissionRate,
63 BinanceFuturesKline, BinanceFuturesMarkPrice, BinanceFuturesOrder,
64 BinanceFuturesTicker24hr, BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo,
65 BinanceFuturesUsdSymbol, BinanceHedgeModeResponse, BinanceLeverageResponse,
66 BinanceOpenInterest, BinanceOpenInterestHistRecord, BinanceOrderBook, BinancePositionRisk,
67 BinancePriceTicker, BinanceServerTime, BinanceUserTrade, ListenKeyResponse,
68 },
69 query::{
70 BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAggTradesParams,
71 BinanceAlgoOrderQueryParams, BinanceAllAlgoOrdersParams, BinanceAllOrdersParams,
72 BinanceBookTickerParams, BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams,
73 BinanceCancelOrderParams, BinanceCommissionRateParams, BinanceDepthParams,
74 BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
75 BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
76 BinanceOpenAlgoOrdersParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams,
77 BinanceOpenOrdersParams, BinanceOrderQueryParams, BinancePositionRiskParams,
78 BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceTicker24hrParams,
79 BinanceTradesParams, BinanceUserTradesParams, ListenKeyParams,
80 },
81};
82use crate::{
83 common::{
84 bar::BinanceBar,
85 consts::{
86 BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
87 BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID,
88 BINANCE_NO_SUCH_ORDER_CODE, BINANCE_RETRY_AFTER_HEADER, BinanceRateLimitQuota,
89 },
90 credential::SigningCredential,
91 encoder::{encode_broker_id, legacy_client_order_id},
92 enums::{
93 BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
94 BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
95 BinanceSide, BinanceTimeInForce, BinanceWorkingType,
96 },
97 instruments::BinanceInstrumentSelector,
98 models::BinanceErrorResponse,
99 parse::{
100 parse_coinm_instrument_with_fees, parse_millis, parse_required_price_at_precision,
101 parse_required_quantity_at_precision, parse_usdm_instrument_with_fees,
102 should_warn_on_instrument_parse_error,
103 },
104 symbol::{format_binance_symbol, format_instrument_id},
105 urls::get_http_base_url,
106 },
107 config::BinanceInstrumentProviderConfig,
108 futures::conversions::reduce_only_param,
109};
110
111const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
112const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
113
114type BinanceFuturesLimiter = RateLimiter<Ustr, MonotonicClock>;
115type BinanceFuturesRateLimiter = Arc<BinanceFuturesLimiter>;
116type BinanceFuturesRateLimiterRegistry<S> = Mutex<AHashMap<S, Weak<BinanceFuturesLimiter>>>;
117
118#[derive(Clone, PartialEq, Eq, Hash)]
119enum BinanceFuturesEndpointScope {
120 Environment(BinanceEnvironment),
121 Custom {
122 environment: BinanceEnvironment,
123 base_url: String,
124 },
125}
126
127#[derive(Clone, PartialEq, Eq, Hash)]
128struct BinanceFuturesRequestScope {
129 endpoint: BinanceFuturesEndpointScope,
130 proxy_url_digest: Option<[u8; 32]>,
131}
132
133#[derive(Clone, Copy, PartialEq, Eq, Hash)]
134struct BinanceFuturesAccountScope([u8; 32]);
135
136#[derive(Clone, PartialEq, Eq, Hash)]
137struct BinanceFuturesOrderScope {
138 endpoint: BinanceFuturesEndpointScope,
139 account: BinanceFuturesAccountScope,
140}
141
142static BINANCE_FUTURES_REQUEST_LIMITERS: LazyLock<
143 BinanceFuturesRateLimiterRegistry<BinanceFuturesRequestScope>,
144> = LazyLock::new(|| Mutex::new(AHashMap::new()));
145
146static BINANCE_FUTURES_ORDER_LIMITERS: LazyLock<
147 BinanceFuturesRateLimiterRegistry<BinanceFuturesOrderScope>,
148> = LazyLock::new(|| Mutex::new(AHashMap::new()));
149
150#[derive(Debug)]
152pub struct BinanceFuturesAlgoOrderQueryResult {
153 pub algo: BinanceFuturesAlgoOrder,
155 pub actual: Option<BinanceFuturesOrder>,
157}
158
159#[derive(Debug, Serialize)]
160#[serde(rename_all = "camelCase")]
161struct BatchCancelParams {
162 symbol: String,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 order_id_list: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 orig_client_order_id_list: Option<String>,
167}
168
169#[derive(Debug, Clone)]
171pub struct BinanceRawFuturesHttpClient {
172 retry_manager: Arc<RetryManager<BinanceFuturesHttpError>>,
173 client: HttpClient,
174 base_url: String,
175 api_path: &'static str,
176 credential: Option<SigningCredential>,
177 recv_window: Option<u64>,
178 order_rate_keys: Vec<String>,
179}
180
181impl BinanceRawFuturesHttpClient {
182 #[must_use]
184 pub fn http_client(&self) -> &HttpClient {
185 &self.client
186 }
187
188 #[must_use]
190 pub const fn has_credentials(&self) -> bool {
191 self.credential.is_some()
192 }
193
194 #[expect(clippy::too_many_arguments)]
200 pub fn new(
201 product_type: BinanceProductType,
202 environment: BinanceEnvironment,
203 api_key: Option<String>,
204 api_secret: Option<String>,
205 base_url_override: Option<String>,
206 recv_window: Option<u64>,
207 timeout_secs: Option<u64>,
208 proxy_url: Option<String>,
209 ) -> BinanceFuturesHttpResult<Self> {
210 let RateLimitConfig {
211 request_quota,
212 order_quotas,
213 order_keys,
214 } = Self::rate_limit_config(product_type);
215
216 let credential = match (api_key, api_secret) {
217 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
218 (None, None) => None,
219 _ => return Err(BinanceFuturesHttpError::MissingCredentials),
220 };
221
222 let account_scope = credential.as_ref().map(Self::account_scope);
223 let rate_limiters = Self::shared_rate_limiters(
224 environment,
225 base_url_override.as_deref(),
226 proxy_url.as_deref(),
227 account_scope,
228 request_quota,
229 order_quotas,
230 );
231 let base_url = base_url_override
232 .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
233 let api_path = Self::resolve_api_path(product_type);
234 let headers = Self::default_headers(&credential);
235
236 let client = HttpClient::builder()
237 .redirect_policy(HttpRedirectPolicy::Reject)
238 .headers(headers)
239 .header_keys(vec![
240 BINANCE_API_KEY_HEADER.to_string(),
241 BINANCE_RETRY_AFTER_HEADER.to_string(),
242 ])
243 .maybe_timeout_secs(timeout_secs)
244 .maybe_proxy_url(proxy_url)
245 .rate_limiters(rate_limiters)
246 .build()?;
247
248 Ok(Self {
249 retry_manager: Arc::new(RetryManager::new(crate::common::http::retry_config())),
250 client,
251 base_url,
252 api_path,
253 credential,
254 recv_window,
255 order_rate_keys: order_keys,
256 })
257 }
258
259 fn shared_rate_limiters(
260 environment: BinanceEnvironment,
261 base_url_override: Option<&str>,
262 proxy_url: Option<&str>,
263 account_scope: Option<BinanceFuturesAccountScope>,
264 request_quota: Quota,
265 order_quotas: Vec<(String, Quota)>,
266 ) -> Vec<BinanceFuturesRateLimiter> {
267 let endpoint = Self::endpoint_scope(environment, base_url_override);
268 let request_scope = BinanceFuturesRequestScope {
269 endpoint: endpoint.clone(),
270 proxy_url_digest: proxy_url.map(Self::sha256),
271 };
272 let request_limiter = Self::request_rate_limiter(request_scope, request_quota);
273 let mut limiters = vec![request_limiter];
274
275 if let Some(account) = account_scope {
276 let order_scope = BinanceFuturesOrderScope { endpoint, account };
277 limiters.push(Self::order_rate_limiter(order_scope, order_quotas));
278 }
279
280 limiters
281 }
282
283 fn endpoint_scope(
284 environment: BinanceEnvironment,
285 base_url_override: Option<&str>,
286 ) -> BinanceFuturesEndpointScope {
287 let Some(base_url) = base_url_override else {
288 return BinanceFuturesEndpointScope::Environment(environment);
289 };
290
291 let normalized = base_url.trim_end_matches('/');
292 let official_urls = [
293 get_http_base_url(BinanceProductType::UsdM, environment),
294 get_http_base_url(BinanceProductType::CoinM, environment),
295 ];
296
297 if official_urls.contains(&normalized) {
298 BinanceFuturesEndpointScope::Environment(environment)
299 } else {
300 BinanceFuturesEndpointScope::Custom {
301 environment,
302 base_url: normalized.to_string(),
303 }
304 }
305 }
306
307 fn account_scope(credential: &SigningCredential) -> BinanceFuturesAccountScope {
308 BinanceFuturesAccountScope(Self::sha256(credential.api_key()))
309 }
310
311 fn sha256(value: &str) -> [u8; 32] {
312 digest::digest(&digest::SHA256, value.as_bytes())
313 .as_ref()
314 .try_into()
315 .expect("SHA-256 digest must contain 32 bytes")
316 }
317
318 fn request_rate_limiter(
319 scope: BinanceFuturesRequestScope,
320 quota: Quota,
321 ) -> BinanceFuturesRateLimiter {
322 let mut registry = BINANCE_FUTURES_REQUEST_LIMITERS.lock();
323
324 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
325 return limiter;
326 }
327
328 let limiter = Arc::new(RateLimiter::new_with_quota(
329 None,
330 vec![(Ustr::from(BINANCE_GLOBAL_RATE_KEY), quota)],
331 ));
332 registry.insert(scope, Arc::downgrade(&limiter));
333 limiter
334 }
335
336 fn order_rate_limiter(
337 scope: BinanceFuturesOrderScope,
338 quotas: Vec<(String, Quota)>,
339 ) -> BinanceFuturesRateLimiter {
340 let mut registry = BINANCE_FUTURES_ORDER_LIMITERS.lock();
341
342 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
343 return limiter;
344 }
345
346 let quotas = quotas
347 .into_iter()
348 .map(|(key, quota)| (Ustr::from(&key), quota))
349 .collect();
350 let limiter = Arc::new(RateLimiter::new_with_quota(None, quotas));
351 registry.insert(scope, Arc::downgrade(&limiter));
352 limiter
353 }
354
355 pub async fn get<P, T>(
361 &self,
362 path: &str,
363 params: Option<&P>,
364 signed: bool,
365 use_order_quota: bool,
366 ) -> BinanceFuturesHttpResult<T>
367 where
368 P: Serialize + ?Sized,
369 T: DeserializeOwned,
370 {
371 self.request(Method::GET, path, params, signed, use_order_quota, None)
372 .await
373 }
374
375 pub async fn post<P, T>(
381 &self,
382 path: &str,
383 params: Option<&P>,
384 body: Option<Vec<u8>>,
385 signed: bool,
386 use_order_quota: bool,
387 ) -> BinanceFuturesHttpResult<T>
388 where
389 P: Serialize + ?Sized,
390 T: DeserializeOwned,
391 {
392 self.request(Method::POST, path, params, signed, use_order_quota, body)
393 .await
394 }
395
396 pub async fn request_put<P, T>(
402 &self,
403 path: &str,
404 params: Option<&P>,
405 signed: bool,
406 use_order_quota: bool,
407 ) -> BinanceFuturesHttpResult<T>
408 where
409 P: Serialize + ?Sized,
410 T: DeserializeOwned,
411 {
412 self.request(Method::PUT, path, params, signed, use_order_quota, None)
413 .await
414 }
415
416 pub async fn request_delete<P, T>(
422 &self,
423 path: &str,
424 params: Option<&P>,
425 signed: bool,
426 use_order_quota: bool,
427 ) -> BinanceFuturesHttpResult<T>
428 where
429 P: Serialize + ?Sized,
430 T: DeserializeOwned,
431 {
432 self.request(Method::DELETE, path, params, signed, use_order_quota, None)
433 .await
434 }
435
436 pub async fn batch_request<T: Serialize>(
442 &self,
443 path: &str,
444 items: &[T],
445 use_order_quota: bool,
446 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
447 self.batch_request_method(Method::POST, path, items, use_order_quota)
448 .await
449 }
450
451 pub async fn batch_request_delete<T: Serialize>(
457 &self,
458 path: &str,
459 items: &[T],
460 use_order_quota: bool,
461 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
462 self.batch_request_method(Method::DELETE, path, items, use_order_quota)
463 .await
464 }
465
466 pub async fn batch_request_put<T: Serialize>(
472 &self,
473 path: &str,
474 items: &[T],
475 use_order_quota: bool,
476 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
477 self.batch_request_method(Method::PUT, path, items, use_order_quota)
478 .await
479 }
480
481 async fn batch_request_method<T: Serialize>(
482 &self,
483 method: Method,
484 path: &str,
485 items: &[T],
486 use_order_quota: bool,
487 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
488 let cred = self
489 .credential
490 .as_ref()
491 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
492
493 let batch_json = serde_json::to_string(items)
494 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
495
496 let encoded_batch = Self::percent_encode(&batch_json);
497 let timestamp = Timestamp::now().as_millisecond();
498 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
499
500 if let Some(recv_window) = self.recv_window {
501 query.push_str(&format!("&recvWindow={recv_window}"));
502 }
503
504 let signature = Self::percent_encode(&cred.sign(&query));
505 query.push_str(&format!("&signature={signature}"));
506
507 let url = self.build_url(path, &query);
508
509 let mut headers = HashMap::new();
510 headers.insert(
511 BINANCE_API_KEY_HEADER.to_string(),
512 cred.api_key().to_string(),
513 );
514
515 let keys = self.rate_limit_keys(use_order_quota);
516
517 let response = self
518 .client
519 .request(
520 method,
521 url,
522 None::<&HashMap<String, Vec<String>>>,
523 Some(headers),
524 None,
525 None,
526 Some(keys),
527 )
528 .await?;
529
530 if !response.status.is_success() {
531 return self.parse_error_response(&response);
532 }
533
534 serde_json::from_slice(&response.body)
535 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
536 }
537
538 fn percent_encode(input: &str) -> String {
540 let mut result = String::with_capacity(input.len() * 3);
541 for byte in input.bytes() {
542 match byte {
543 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
544 result.push(byte as char);
545 }
546 _ => {
547 result.push('%');
548 result.push_str(&format!("{byte:02X}"));
549 }
550 }
551 }
552 result
553 }
554
555 async fn request<P, T>(
556 &self,
557 method: Method,
558 path: &str,
559 params: Option<&P>,
560 signed: bool,
561 use_order_quota: bool,
562 body: Option<Vec<u8>>,
563 ) -> BinanceFuturesHttpResult<T>
564 where
565 P: Serialize + ?Sized,
566 T: DeserializeOwned,
567 {
568 let operation = || {
569 self.request_once(
570 method.clone(),
571 path,
572 params,
573 signed,
574 use_order_quota,
575 body.clone(),
576 )
577 };
578
579 if method != Method::GET {
580 return operation().await;
581 }
582 self.retry_manager
583 .invocation(
584 path,
585 operation,
586 BinanceFuturesHttpError::is_retryable,
587 |e| match e {
588 RetryError::Canceled => {
589 BinanceFuturesHttpError::Canceled("HTTP requests canceled".to_string())
590 }
591 RetryError::OperationTimeout { timeout_ms } => {
592 BinanceFuturesHttpError::Timeout(format!("Request exceeded {timeout_ms}ms"))
593 }
594 RetryError::InvalidConfiguration { message } => {
595 BinanceFuturesHttpError::ValidationError(message)
596 }
597 e @ RetryError::ElapsedBudgetExceeded { .. } => {
598 BinanceFuturesHttpError::RetryBudgetExceeded(e.to_string())
599 }
600 },
601 )
602 .retry_delay(&BinanceFuturesHttpError::retry_after)
603 .execute()
604 .await
605 }
606
607 async fn request_once<P, T>(
608 &self,
609 method: Method,
610 path: &str,
611 params: Option<&P>,
612 signed: bool,
613 use_order_quota: bool,
614 body: Option<Vec<u8>>,
615 ) -> BinanceFuturesHttpResult<T>
616 where
617 P: Serialize + ?Sized,
618 T: DeserializeOwned,
619 {
620 let mut query = params
621 .map(serde_urlencoded::to_string)
622 .transpose()
623 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
624 .unwrap_or_default();
625
626 let mut headers = HashMap::new();
627
628 if signed {
629 let cred = self
630 .credential
631 .as_ref()
632 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
633
634 if !query.is_empty() {
635 query.push('&');
636 }
637
638 let timestamp = Timestamp::now().as_millisecond();
639 query.push_str(&format!("timestamp={timestamp}"));
640
641 if let Some(recv_window) = self.recv_window {
642 query.push_str(&format!("&recvWindow={recv_window}"));
643 }
644
645 let signature = Self::percent_encode(&cred.sign(&query));
649 query.push_str(&format!("&signature={signature}"));
650 headers.insert(
651 BINANCE_API_KEY_HEADER.to_string(),
652 cred.api_key().to_string(),
653 );
654 }
655
656 let url = self.build_url(path, &query);
657 let keys = self.rate_limit_keys(use_order_quota);
658
659 let response = self
660 .client
661 .request(
662 method,
663 url,
664 None::<&HashMap<String, Vec<String>>>,
665 Some(headers),
666 body,
667 None,
668 Some(keys),
669 )
670 .await?;
671
672 if !response.status.is_success() {
673 return self.parse_error_response(&response);
674 }
675
676 serde_json::from_slice::<T>(&response.body)
677 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
678 }
679
680 fn build_url(&self, path: &str, query: &str) -> String {
681 let url_path = if path.starts_with("/fapi/")
683 || path.starts_with("/dapi/")
684 || path.starts_with("/futures/data/")
685 {
686 path.to_string()
687 } else if path.starts_with('/') {
688 format!("{}{}", self.api_path, path)
689 } else {
690 format!("{}/{}", self.api_path, path)
691 };
692
693 let mut url = format!("{}{}", self.base_url, url_path);
694
695 if !query.is_empty() {
696 url.push('?');
697 url.push_str(query);
698 }
699 url
700 }
701
702 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
703 if use_orders {
704 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
705 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
706 keys.extend(self.order_rate_keys.iter().cloned());
707 keys
708 } else {
709 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
710 }
711 }
712
713 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
714 let status = response.status.as_u16();
715 let body = String::from_utf8_lossy(&response.body).to_string();
716 let retry_after = crate::common::http::retry_after(&response.headers, Timestamp::now());
717
718 if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
719 return Err(BinanceFuturesHttpError::BinanceError {
720 code: err.code,
721 message: err.msg,
722 status,
723 retry_after,
724 });
725 }
726
727 Err(BinanceFuturesHttpError::UnexpectedStatus {
728 status,
729 body,
730 retry_after,
731 })
732 }
733
734 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
735 let mut headers: HashMap<String, String> =
736 create_standard_nautilus_headers().into_iter().collect();
737
738 if let Some(cred) = credential {
739 headers.insert(
740 BINANCE_API_KEY_HEADER.to_string(),
741 cred.api_key().to_string(),
742 );
743 }
744 headers
745 }
746
747 fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
748 match product_type {
749 BinanceProductType::UsdM => BINANCE_FAPI_PATH,
750 BinanceProductType::CoinM => BINANCE_DAPI_PATH,
751 _ => BINANCE_FAPI_PATH, }
753 }
754
755 fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
756 let quotas = match product_type {
757 BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
758 BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
759 _ => BINANCE_FAPI_RATE_LIMITS,
760 };
761
762 let mut order_quotas = Vec::new();
763 let mut order_keys = Vec::new();
764 let mut request_quota = None;
765
766 for quota in quotas {
767 if let Some(q) = Self::quota_from(quota) {
768 match quota.rate_limit_type {
769 BinanceRateLimitType::RequestWeight if request_quota.is_none() => {
770 request_quota = Some(q);
771 }
772 BinanceRateLimitType::Orders => {
773 let key = format!(
774 "{}:{}:{:?}",
775 BINANCE_ORDERS_RATE_KEY, quota.interval_num, quota.interval
776 );
777 order_keys.push(key.clone());
778 order_quotas.push((key, q));
779 }
780 _ => {}
781 }
782 }
783 }
784
785 let request_quota = request_quota.unwrap_or_else(|| {
786 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
787 });
788
789 RateLimitConfig {
790 request_quota,
791 order_quotas,
792 order_keys,
793 }
794 }
795
796 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
797 let burst = NonZeroU32::new(quota.limit)?;
798 let period = Self::quota_period(quota)?;
799 let replenish_interval_ns = period.as_nanos() / u128::from(quota.limit);
800 let replenish_interval_ns = u64::try_from(replenish_interval_ns).ok()?;
801
802 Quota::with_period(Duration::from_nanos(replenish_interval_ns))
803 .map(|q| q.allow_burst(burst))
804 }
805
806 fn quota_period(quota: &BinanceRateLimitQuota) -> Option<Duration> {
807 match quota.interval {
808 BinanceRateLimitInterval::Second => {
809 Some(Duration::from_secs(u64::from(quota.interval_num)))
810 }
811 BinanceRateLimitInterval::Minute => {
812 Some(Duration::from_secs(60 * u64::from(quota.interval_num)))
813 }
814 BinanceRateLimitInterval::Day => Some(Duration::from_secs(
815 SECONDS_IN_DAY * u64::from(quota.interval_num),
816 )),
817 BinanceRateLimitInterval::Unknown => None,
818 }
819 }
820
821 pub async fn ticker_24h(
827 &self,
828 params: &BinanceTicker24hrParams,
829 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
830 self.get("ticker/24hr", Some(params), false, false).await
831 }
832
833 pub async fn book_ticker(
839 &self,
840 params: &BinanceBookTickerParams,
841 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
842 self.get("ticker/bookTicker", Some(params), false, false)
843 .await
844 }
845
846 pub async fn price_ticker(
852 &self,
853 symbol: Option<&str>,
854 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
855 #[derive(Serialize)]
856 struct Params<'a> {
857 #[serde(skip_serializing_if = "Option::is_none")]
858 symbol: Option<&'a str>,
859 }
860 self.get("ticker/price", Some(&Params { symbol }), false, false)
861 .await
862 }
863
864 pub async fn depth(
870 &self,
871 params: &BinanceDepthParams,
872 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
873 self.get("depth", Some(params), false, false).await
874 }
875
876 pub async fn mark_price(
882 &self,
883 params: &BinanceMarkPriceParams,
884 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
885 let response: MarkPriceResponse =
886 self.get("premiumIndex", Some(params), false, false).await?;
887 Ok(response.into())
888 }
889
890 pub async fn funding_rate(
896 &self,
897 params: &BinanceFundingRateParams,
898 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
899 self.get("fundingRate", Some(params), false, false).await
900 }
901
902 pub async fn open_interest(
908 &self,
909 params: &BinanceOpenInterestParams,
910 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
911 self.get("openInterest", Some(params), false, false).await
912 }
913
914 pub async fn open_interest_hist(
920 &self,
921 params: &BinanceOpenInterestHistParams,
922 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
923 self.get("/futures/data/openInterestHist", Some(params), false, false)
924 .await
925 }
926
927 pub async fn trades(
933 &self,
934 params: &BinanceTradesParams,
935 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
936 self.get("trades", Some(params), false, false).await
937 }
938
939 pub async fn agg_trades(
945 &self,
946 params: &BinanceAggTradesParams,
947 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAggTrade>> {
948 if params.limit.is_some_and(|limit| limit > 1000) {
949 return Err(BinanceFuturesHttpError::ValidationError(
950 "aggregate trade limit must not exceed 1000".to_string(),
951 ));
952 }
953
954 if let (Some(start), Some(end)) = (params.start_time, params.end_time) {
955 if start > end {
956 return Err(BinanceFuturesHttpError::ValidationError(
957 "aggregate trade startTime must not exceed endTime".to_string(),
958 ));
959 }
960 let Some(range) = end.checked_sub(start) else {
961 return Err(BinanceFuturesHttpError::ValidationError(
962 "aggregate trade time range must be less than one hour".to_string(),
963 ));
964 };
965
966 if range >= 3_600_000 {
967 return Err(BinanceFuturesHttpError::ValidationError(
968 "aggregate trade time range must be less than one hour".to_string(),
969 ));
970 }
971 }
972
973 self.get("aggTrades", Some(params), false, false).await
974 }
975
976 pub async fn klines(
982 &self,
983 params: &BinanceKlinesParams,
984 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
985 self.get("klines", Some(params), false, false).await
986 }
987
988 pub async fn set_leverage(
994 &self,
995 params: &BinanceSetLeverageParams,
996 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
997 self.post("leverage", Some(params), None, true, false).await
998 }
999
1000 pub async fn set_margin_type(
1006 &self,
1007 params: &BinanceSetMarginTypeParams,
1008 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1009 self.post("marginType", Some(params), None, true, false)
1010 .await
1011 }
1012
1013 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1019 self.get::<(), _>("positionSide/dual", None, true, false)
1020 .await
1021 }
1022
1023 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1029 self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
1030 .await
1031 }
1032
1033 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1039 let params = ListenKeyParams {
1040 listen_key: listen_key.into(),
1041 };
1042 let _: serde_json::Value = self
1043 .request_put("listenKey", Some(¶ms), true, false)
1044 .await?;
1045 Ok(())
1046 }
1047
1048 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1054 let params = ListenKeyParams {
1055 listen_key: listen_key.into(),
1056 };
1057 let _: serde_json::Value = self
1058 .request_delete("listenKey", Some(¶ms), true, false)
1059 .await?;
1060 Ok(())
1061 }
1062
1063 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1069 let path = if self.api_path.starts_with("/fapi") {
1071 "/fapi/v2/account"
1072 } else {
1073 "/dapi/v1/account"
1074 };
1075 self.get::<(), _>(path, None, true, false).await
1076 }
1077
1078 pub async fn commission_rate(
1084 &self,
1085 params: &BinanceCommissionRateParams,
1086 ) -> BinanceFuturesHttpResult<BinanceFuturesCommissionRate> {
1087 self.get("commissionRate", Some(params), true, false).await
1088 }
1089
1090 pub async fn query_positions(
1096 &self,
1097 params: &BinancePositionRiskParams,
1098 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1099 let path = if self.api_path.starts_with("/fapi") {
1101 "/fapi/v2/positionRisk"
1102 } else {
1103 "/dapi/v1/positionRisk"
1104 };
1105 self.get(path, Some(params), true, false).await
1106 }
1107
1108 pub async fn query_user_trades(
1114 &self,
1115 params: &BinanceUserTradesParams,
1116 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1117 self.get("userTrades", Some(params), true, false).await
1118 }
1119
1120 pub async fn query_order(
1126 &self,
1127 params: &BinanceOrderQueryParams,
1128 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1129 let Some(legacy) = params
1130 .orig_client_order_id
1131 .as_deref()
1132 .and_then(|id| legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID))
1133 else {
1134 return self.get("order", Some(params), true, false).await;
1135 };
1136
1137 let mut query = params.clone();
1138 if query.order_id.is_some() {
1139 query.orig_client_order_id = None;
1140 return self.get("order", Some(&query), true, false).await;
1141 }
1142
1143 let current: BinanceFuturesHttpResult<BinanceFuturesOrder> =
1144 self.get("order", Some(&query), true, false).await;
1145
1146 if let Err(e) = ¤t
1147 && !matches!(
1148 e,
1149 BinanceFuturesHttpError::BinanceError {
1150 code: BINANCE_NO_SUCH_ORDER_CODE,
1151 ..
1152 }
1153 )
1154 {
1155 return current;
1156 }
1157
1158 query.orig_client_order_id = Some(legacy);
1159 let historical: BinanceFuturesHttpResult<BinanceFuturesOrder> =
1160 self.get("order", Some(&query), true, false).await;
1161
1162 match (current, historical) {
1163 (Ok(current), Ok(historical)) if current.order_id != historical.order_id => {
1164 Err(BinanceFuturesHttpError::ValidationError(
1165 "Ambiguous historical client order ID".to_string(),
1166 ))
1167 }
1168 (Ok(current), Ok(_)) => Ok(current),
1169 (
1170 Ok(current),
1171 Err(BinanceFuturesHttpError::BinanceError {
1172 code: BINANCE_NO_SUCH_ORDER_CODE,
1173 ..
1174 }),
1175 ) => Ok(current),
1176 (Err(_), Ok(historical)) => Ok(historical),
1177 (_, Err(e)) => Err(e),
1178 }
1179 }
1180
1181 pub(crate) async fn resolve_order_identity(
1182 &self,
1183 symbol: &str,
1184 order_id: &mut Option<i64>,
1185 client_order_id: &mut Option<String>,
1186 ) -> BinanceFuturesHttpResult<()> {
1187 let Some(id) = client_order_id
1188 .as_deref()
1189 .filter(|id| legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some())
1190 else {
1191 return Ok(());
1192 };
1193
1194 *order_id = Some(self.resolve_order_id(symbol, *order_id, id).await?);
1195 *client_order_id = None;
1196 Ok(())
1197 }
1198
1199 async fn resolve_order_id(
1200 &self,
1201 symbol: &str,
1202 order_id: Option<i64>,
1203 client_order_id: &str,
1204 ) -> BinanceFuturesHttpResult<i64> {
1205 if let Some(order_id) = order_id {
1206 return Ok(order_id);
1207 }
1208
1209 self.query_order(&BinanceOrderQueryParams {
1210 symbol: symbol.to_string(),
1211 order_id: None,
1212 orig_client_order_id: Some(client_order_id.to_string()),
1213 recv_window: None,
1214 })
1215 .await
1216 .map(|order| order.order_id)
1217 .map_err(identity_lookup_error)
1218 }
1219
1220 pub async fn query_open_orders(
1226 &self,
1227 params: &BinanceOpenOrdersParams,
1228 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1229 self.get("openOrders", Some(params), true, false).await
1230 }
1231
1232 pub async fn query_all_orders(
1238 &self,
1239 params: &BinanceAllOrdersParams,
1240 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1241 self.get("allOrders", Some(params), true, false).await
1242 }
1243
1244 pub async fn submit_order(
1250 &self,
1251 params: &BinanceNewOrderParams,
1252 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1253 self.post("order", Some(params), None, true, true).await
1254 }
1255
1256 pub async fn submit_order_list(
1262 &self,
1263 orders: &[BatchOrderItem],
1264 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1265 if orders.is_empty() {
1266 return Ok(Vec::new());
1267 }
1268
1269 if orders.len() > 5 {
1270 return Err(BinanceFuturesHttpError::ValidationError(
1271 "Batch order limit is 5 orders maximum".to_string(),
1272 ));
1273 }
1274
1275 self.batch_request("batchOrders", orders, true).await
1276 }
1277
1278 pub async fn modify_order(
1284 &self,
1285 params: &BinanceModifyOrderParams,
1286 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1287 if let Some(id) = params.orig_client_order_id.as_deref()
1288 && legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1289 {
1290 let mut resolved = params.clone();
1291 resolved.order_id = Some(
1292 self.resolve_order_id(¶ms.symbol, params.order_id, id)
1293 .await?,
1294 );
1295 resolved.orig_client_order_id = None;
1296 return self.request_put("order", Some(&resolved), true, true).await;
1297 }
1298
1299 self.request_put("order", Some(params), true, true).await
1300 }
1301
1302 pub async fn batch_modify_orders(
1308 &self,
1309 modifies: &[BatchModifyItem],
1310 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1311 if modifies.is_empty() {
1312 return Ok(Vec::new());
1313 }
1314
1315 if modifies.len() > 5 {
1316 return Err(BinanceFuturesHttpError::ValidationError(
1317 "Batch modify limit is 5 orders maximum".to_string(),
1318 ));
1319 }
1320
1321 if modifies.iter().any(|item| {
1322 item.orig_client_order_id.as_deref().is_some_and(|id| {
1323 legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1324 })
1325 }) {
1326 let mut resolved = Vec::with_capacity(modifies.len());
1327 let mut results = Vec::with_capacity(modifies.len());
1328 for item in modifies {
1329 let mut item = item.clone();
1330 if let Some(id) = item.orig_client_order_id.as_deref()
1331 && legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1332 {
1333 match self.resolve_order_id(&item.symbol, item.order_id, id).await {
1334 Ok(order_id) => {
1335 item.order_id = Some(order_id);
1336 item.orig_client_order_id = None;
1337 }
1338 Err(BinanceFuturesHttpError::BinanceError {
1339 code: BINANCE_NO_SUCH_ORDER_CODE,
1340 message,
1341 ..
1342 }) => {
1343 results.push(Some(BatchOrderResult::Error(BatchOrderError {
1344 code: BINANCE_NO_SUCH_ORDER_CODE,
1345 msg: message,
1346 })));
1347
1348 continue;
1349 }
1350 Err(e) => return Err(e),
1351 }
1352 }
1353
1354 resolved.push(item);
1355 results.push(None);
1356 }
1357
1358 let responses = if resolved.is_empty() {
1359 Vec::new()
1360 } else {
1361 self.batch_request_put("batchOrders", &resolved, true)
1362 .await?
1363 };
1364
1365 return merge_batch_results(results, responses);
1366 }
1367
1368 self.batch_request_put("batchOrders", modifies, true).await
1369 }
1370
1371 pub async fn cancel_order(
1377 &self,
1378 params: &BinanceCancelOrderParams,
1379 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1380 if let Some(id) = params.orig_client_order_id.as_deref()
1381 && legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1382 {
1383 let mut resolved = params.clone();
1384 resolved.order_id = Some(
1385 self.resolve_order_id(¶ms.symbol, params.order_id, id)
1386 .await?,
1387 );
1388 resolved.orig_client_order_id = None;
1389 return self
1390 .request_delete("order", Some(&resolved), true, true)
1391 .await;
1392 }
1393
1394 self.request_delete("order", Some(params), true, true).await
1395 }
1396
1397 pub async fn cancel_all_orders(
1403 &self,
1404 params: &BinanceCancelAllOrdersParams,
1405 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1406 self.request_delete("allOpenOrders", Some(params), true, true)
1407 .await
1408 }
1409
1410 pub async fn batch_cancel_orders(
1416 &self,
1417 cancels: &[BatchCancelItem],
1418 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1419 if cancels.is_empty() {
1420 return Ok(Vec::new());
1421 }
1422
1423 if cancels.len() > 10 {
1424 return Err(BinanceFuturesHttpError::ValidationError(
1425 "Batch cancel limit is 10 orders maximum".to_string(),
1426 ));
1427 }
1428
1429 let params = Self::batch_cancel_params(cancels)?;
1430 if cancels.iter().any(|item| {
1431 item.orig_client_order_id.as_deref().is_some_and(|id| {
1432 legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1433 })
1434 }) {
1435 let mut resolved = Vec::with_capacity(cancels.len());
1436 let mut results = Vec::with_capacity(cancels.len());
1437 for item in cancels {
1438 let order = self
1439 .query_order(&BinanceOrderQueryParams {
1440 symbol: item.symbol.clone(),
1441 order_id: item.order_id,
1442 orig_client_order_id: item.orig_client_order_id.clone(),
1443 recv_window: None,
1444 })
1445 .await
1446 .map_err(identity_lookup_error);
1447
1448 match order {
1449 Ok(order) => {
1450 resolved.push(BatchCancelItem::by_order_id(
1451 item.symbol.clone(),
1452 order.order_id,
1453 ));
1454 results.push(None);
1455 }
1456 Err(BinanceFuturesHttpError::BinanceError {
1457 code: BINANCE_NO_SUCH_ORDER_CODE,
1458 message,
1459 ..
1460 }) => {
1461 results.push(Some(BatchOrderResult::Error(BatchOrderError {
1462 code: BINANCE_NO_SUCH_ORDER_CODE,
1463 msg: message,
1464 })));
1465 }
1466 Err(e) => return Err(e),
1467 }
1468 }
1469
1470 let responses = if resolved.is_empty() {
1471 Vec::new()
1472 } else {
1473 let params = Self::batch_cancel_params(&resolved)?;
1474 self.request_delete("batchOrders", Some(¶ms), true, true)
1475 .await?
1476 };
1477
1478 return merge_batch_results(results, responses);
1479 }
1480
1481 self.request_delete("batchOrders", Some(¶ms), true, true)
1482 .await
1483 }
1484
1485 fn batch_cancel_params(
1486 cancels: &[BatchCancelItem],
1487 ) -> BinanceFuturesHttpResult<BatchCancelParams> {
1488 let symbol = cancels[0].symbol.clone();
1489 let mut order_ids = Vec::new();
1490 let mut client_order_ids = Vec::new();
1491
1492 for cancel in cancels {
1493 if cancel.symbol != symbol {
1494 return Err(BinanceFuturesHttpError::ValidationError(
1495 "Batch cancel orders must use the same symbol".to_string(),
1496 ));
1497 }
1498
1499 if let Some(order_id) = cancel.order_id {
1500 order_ids.push(order_id);
1501 }
1502
1503 if let Some(client_order_id) = &cancel.orig_client_order_id {
1504 client_order_ids.push(client_order_id.clone());
1505 }
1506 }
1507
1508 if order_ids.is_empty() && client_order_ids.is_empty() {
1509 return Err(BinanceFuturesHttpError::ValidationError(
1510 "Batch cancel requires at least one order ID or client order ID".to_string(),
1511 ));
1512 }
1513
1514 if !order_ids.is_empty() && !client_order_ids.is_empty() {
1515 return Err(BinanceFuturesHttpError::ValidationError(
1516 "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1517 ));
1518 }
1519
1520 let order_id_list = if order_ids.is_empty() {
1521 None
1522 } else {
1523 Some(
1524 serde_json::to_string(&order_ids)
1525 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1526 )
1527 };
1528 let orig_client_order_id_list = if client_order_ids.is_empty() {
1529 None
1530 } else {
1531 Some(
1532 serde_json::to_string(&client_order_ids)
1533 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1534 )
1535 };
1536
1537 Ok(BatchCancelParams {
1538 symbol,
1539 order_id_list,
1540 orig_client_order_id_list,
1541 })
1542 }
1543
1544 pub async fn submit_algo_order(
1553 &self,
1554 params: &BinanceNewAlgoOrderParams,
1555 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1556 self.post("algoOrder", Some(params), None, true, true).await
1557 }
1558
1559 pub async fn cancel_algo_order(
1567 &self,
1568 params: &BinanceAlgoOrderQueryParams,
1569 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1570 if let Some(id) = params.client_algo_id.as_deref()
1571 && legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID).is_some()
1572 {
1573 let mut resolved = params.clone();
1574 if resolved.algo_id.is_none() {
1575 resolved.algo_id = Some(
1576 self.query_algo_order(params)
1577 .await
1578 .map_err(identity_lookup_error)?
1579 .algo_id,
1580 );
1581 }
1582
1583 resolved.client_algo_id = None;
1584 return self
1585 .request_delete("algoOrder", Some(&resolved), true, true)
1586 .await;
1587 }
1588
1589 self.request_delete("algoOrder", Some(params), true, true)
1590 .await
1591 }
1592
1593 pub async fn query_algo_order(
1601 &self,
1602 params: &BinanceAlgoOrderQueryParams,
1603 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1604 let Some(legacy) = params
1605 .client_algo_id
1606 .as_deref()
1607 .and_then(|id| legacy_client_order_id(id, BINANCE_NAUTILUS_FUTURES_BROKER_ID))
1608 else {
1609 return self.get("algoOrder", Some(params), true, false).await;
1610 };
1611
1612 let mut query = params.clone();
1613 if query.algo_id.is_some() {
1614 query.client_algo_id = None;
1615 return self.get("algoOrder", Some(&query), true, false).await;
1616 }
1617
1618 let current: BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> =
1619 self.get("algoOrder", Some(&query), true, false).await;
1620
1621 if let Err(e) = ¤t
1622 && !matches!(
1623 e,
1624 BinanceFuturesHttpError::BinanceError {
1625 code: BINANCE_NO_SUCH_ORDER_CODE,
1626 ..
1627 }
1628 )
1629 {
1630 return current;
1631 }
1632
1633 query.client_algo_id = Some(legacy);
1634 let historical: BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> =
1635 self.get("algoOrder", Some(&query), true, false).await;
1636
1637 match (current, historical) {
1638 (Ok(current), Ok(historical)) if current.algo_id != historical.algo_id => {
1639 Err(BinanceFuturesHttpError::ValidationError(
1640 "Ambiguous historical client algo ID".to_string(),
1641 ))
1642 }
1643 (Ok(current), Ok(_)) => Ok(current),
1644 (
1645 Ok(current),
1646 Err(BinanceFuturesHttpError::BinanceError {
1647 code: BINANCE_NO_SUCH_ORDER_CODE,
1648 ..
1649 }),
1650 ) => Ok(current),
1651 (Err(_), Ok(historical)) => Ok(historical),
1652 (_, Err(e)) => Err(e),
1653 }
1654 }
1655
1656 pub async fn query_open_algo_orders(
1662 &self,
1663 params: &BinanceOpenAlgoOrdersParams,
1664 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1665 self.get("openAlgoOrders", Some(params), true, false).await
1666 }
1667
1668 pub async fn query_all_algo_orders(
1674 &self,
1675 params: &BinanceAllAlgoOrdersParams,
1676 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1677 self.get("allAlgoOrders", Some(params), true, false).await
1678 }
1679
1680 pub async fn cancel_all_algo_orders(
1686 &self,
1687 params: &BinanceCancelAllAlgoOrdersParams,
1688 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1689 self.request_delete("algoOpenOrders", Some(params), true, true)
1690 .await
1691 }
1692}
1693
1694#[derive(Debug, Deserialize)]
1696#[serde(untagged)]
1697enum MarkPriceResponse {
1698 Single(BinanceFuturesMarkPrice),
1699 Multiple(Vec<BinanceFuturesMarkPrice>),
1700}
1701
1702impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1703 fn from(response: MarkPriceResponse) -> Self {
1704 match response {
1705 MarkPriceResponse::Single(price) => vec![price],
1706 MarkPriceResponse::Multiple(prices) => prices,
1707 }
1708 }
1709}
1710
1711struct RateLimitConfig {
1712 request_quota: Quota,
1713 order_quotas: Vec<(String, Quota)>,
1714 order_keys: Vec<String>,
1715}
1716
1717#[derive(Clone, Debug)]
1719pub enum BinanceFuturesInstrument {
1720 UsdM(BinanceFuturesUsdSymbol),
1722 CoinM(BinanceFuturesCoinSymbol),
1724}
1725
1726impl BinanceFuturesInstrument {
1727 #[must_use]
1729 pub const fn symbol(&self) -> Ustr {
1730 match self {
1731 Self::UsdM(s) => s.symbol,
1732 Self::CoinM(s) => s.symbol,
1733 }
1734 }
1735
1736 #[must_use]
1738 pub const fn price_precision(&self) -> i32 {
1739 match self {
1740 Self::UsdM(s) => s.price_precision,
1741 Self::CoinM(s) => s.price_precision,
1742 }
1743 }
1744
1745 #[must_use]
1747 pub const fn quantity_precision(&self) -> i32 {
1748 match self {
1749 Self::UsdM(s) => s.quantity_precision,
1750 Self::CoinM(s) => s.quantity_precision,
1751 }
1752 }
1753
1754 pub fn precisions(&self) -> BinanceFuturesHttpResult<(u8, u8)> {
1760 let price_precision = u8::try_from(self.price_precision()).map_err(|_| {
1761 BinanceFuturesHttpError::ValidationError(format!(
1762 "Invalid Binance Futures price precision {} for {}",
1763 self.price_precision(),
1764 self.symbol()
1765 ))
1766 })?;
1767 let quantity_precision = u8::try_from(self.quantity_precision()).map_err(|_| {
1768 BinanceFuturesHttpError::ValidationError(format!(
1769 "Invalid Binance Futures quantity precision {} for {}",
1770 self.quantity_precision(),
1771 self.symbol()
1772 ))
1773 })?;
1774
1775 if price_precision > FIXED_PRECISION || quantity_precision > FIXED_PRECISION {
1776 return Err(BinanceFuturesHttpError::ValidationError(format!(
1777 "Binance Futures precision exceeds maximum {FIXED_PRECISION} for {}: price={price_precision}, quantity={quantity_precision}",
1778 self.symbol()
1779 )));
1780 }
1781
1782 Ok((price_precision, quantity_precision))
1783 }
1784
1785 #[must_use]
1787 pub fn id(&self) -> InstrumentId {
1788 match self {
1789 Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1790 Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1791 }
1792 }
1793
1794 #[must_use]
1796 pub fn quote_currency(&self) -> Currency {
1797 let quote_asset = match self {
1798 Self::UsdM(s) => &s.quote_asset,
1799 Self::CoinM(s) => &s.quote_asset,
1800 };
1801 Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1802 }
1803}
1804
1805#[derive(Debug, Clone)]
1807pub struct BinanceFuturesHttpClient {
1808 inner: Arc<BinanceRawFuturesHttpClient>,
1809 product_type: BinanceProductType,
1810 clock: &'static AtomicTime,
1811 instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1812 instruments_reconciliation: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
1813 instruments_load_lock: Arc<tokio::sync::Mutex<()>>,
1814 treat_expired_as_canceled: bool,
1815}
1816
1817impl BinanceFuturesHttpClient {
1818 #[expect(clippy::too_many_arguments)]
1824 pub fn new(
1825 product_type: BinanceProductType,
1826 environment: BinanceEnvironment,
1827 clock: &'static AtomicTime,
1828 api_key: Option<String>,
1829 api_secret: Option<String>,
1830 base_url_override: Option<String>,
1831 recv_window: Option<u64>,
1832 timeout_secs: Option<u64>,
1833 proxy_url: Option<String>,
1834 treat_expired_as_canceled: bool,
1835 ) -> BinanceFuturesHttpResult<Self> {
1836 match product_type {
1837 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1838 _ => {
1839 return Err(BinanceFuturesHttpError::ValidationError(format!(
1840 "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1841 )));
1842 }
1843 }
1844
1845 let raw = BinanceRawFuturesHttpClient::new(
1846 product_type,
1847 environment,
1848 api_key,
1849 api_secret,
1850 base_url_override,
1851 recv_window,
1852 timeout_secs,
1853 proxy_url,
1854 )?;
1855
1856 Ok(Self {
1857 inner: Arc::new(raw),
1858 product_type,
1859 clock,
1860 instruments: Arc::new(DashMap::new()),
1861 instruments_reconciliation: Arc::new(AtomicMap::new()),
1862 instruments_load_lock: Arc::new(tokio::sync::Mutex::new(())),
1863 treat_expired_as_canceled,
1864 })
1865 }
1866
1867 pub(crate) fn with_retry_config(mut self, config: RetryConfig) -> Self {
1868 Arc::make_mut(&mut self.inner).retry_manager = Arc::new(RetryManager::new(config));
1869 self
1870 }
1871
1872 #[must_use]
1874 pub const fn product_type(&self) -> BinanceProductType {
1875 self.product_type
1876 }
1877
1878 #[must_use]
1880 pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1881 &self.inner
1882 }
1883
1884 #[must_use]
1886 pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1887 Arc::clone(&self.instruments)
1888 }
1889
1890 pub(crate) fn instrument_reconciliation(
1892 &self,
1893 instrument_id: &InstrumentId,
1894 ) -> Option<InstrumentAny> {
1895 self.instruments_reconciliation.get_cloned(instrument_id)
1896 }
1897
1898 #[must_use]
1900 pub fn has_credentials(&self) -> bool {
1901 self.inner.has_credentials()
1902 }
1903
1904 fn replace_instruments(
1906 &self,
1907 instruments: Vec<(Ustr, BinanceFuturesInstrument)>,
1908 ) -> BinanceFuturesHttpResult<()> {
1909 let mut snapshot = AHashMap::with_capacity(instruments.len());
1910 for (symbol, instrument) in instruments {
1911 instrument.precisions()?;
1912 if instrument.symbol() != symbol {
1913 return Err(BinanceFuturesHttpError::ValidationError(format!(
1914 "Binance Futures catalog key {symbol} does not match instrument symbol {}",
1915 instrument.symbol()
1916 )));
1917 }
1918 let expected_id = format_instrument_id(&symbol, self.product_type);
1919 if instrument.id() != expected_id {
1920 return Err(BinanceFuturesHttpError::ValidationError(format!(
1921 "Binance Futures catalog instrument ID {} does not match expected ID {expected_id}",
1922 instrument.id()
1923 )));
1924 }
1925
1926 if snapshot.insert(symbol, instrument).is_some() {
1927 return Err(BinanceFuturesHttpError::ValidationError(format!(
1928 "Duplicate Binance Futures catalog symbol {symbol}"
1929 )));
1930 }
1931 }
1932
1933 let symbols: AHashSet<_> = snapshot.keys().copied().collect();
1934 for (symbol, instrument) in snapshot {
1935 self.instruments.insert(symbol, instrument);
1936 }
1937 self.instruments
1938 .retain(|symbol, _| symbols.contains(symbol));
1939 Ok(())
1940 }
1941
1942 pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1948 self.inner
1949 .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1950 .await
1951 }
1952
1953 pub async fn set_leverage(
1959 &self,
1960 params: &BinanceSetLeverageParams,
1961 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1962 self.inner.set_leverage(params).await
1963 }
1964
1965 pub async fn set_margin_type(
1971 &self,
1972 params: &BinanceSetMarginTypeParams,
1973 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1974 self.inner.set_margin_type(params).await
1975 }
1976
1977 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1983 self.inner.query_hedge_mode().await
1984 }
1985
1986 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1992 self.inner.create_listen_key().await
1993 }
1994
1995 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
2001 self.inner.keepalive_listen_key(listen_key).await
2002 }
2003
2004 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
2010 self.inner.close_listen_key(listen_key).await
2011 }
2012
2013 pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
2019 let _guard = self.instruments_load_lock.lock().await;
2020 let instruments = match self.product_type {
2021 BinanceProductType::UsdM => {
2022 let info: BinanceFuturesUsdExchangeInfo = self
2023 .inner
2024 .get("exchangeInfo", None::<&()>, false, false)
2025 .await?;
2026
2027 info.symbols
2028 .into_iter()
2029 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::UsdM(symbol)))
2030 .collect()
2031 }
2032 BinanceProductType::CoinM => {
2033 let info: BinanceFuturesCoinExchangeInfo = self
2034 .inner
2035 .get("exchangeInfo", None::<&()>, false, false)
2036 .await?;
2037
2038 info.symbols
2039 .into_iter()
2040 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::CoinM(symbol)))
2041 .collect()
2042 }
2043 _ => {
2044 return Err(BinanceFuturesHttpError::ValidationError(
2045 "Invalid product type for futures".to_string(),
2046 ));
2047 }
2048 };
2049
2050 self.replace_instruments(instruments)
2051 }
2052
2053 pub async fn request_symbol_statuses(
2063 &self,
2064 ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
2065 let mut statuses = AHashMap::new();
2066
2067 match self.product_type {
2068 BinanceProductType::UsdM => {
2069 let info: BinanceFuturesUsdExchangeInfo = self
2070 .inner
2071 .get("exchangeInfo", None::<&()>, false, false)
2072 .await?;
2073
2074 for symbol in &info.symbols {
2075 statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
2076 }
2077 }
2078 BinanceProductType::CoinM => {
2079 let info: BinanceFuturesCoinExchangeInfo = self
2080 .inner
2081 .get("exchangeInfo", None::<&()>, false, false)
2082 .await?;
2083
2084 for symbol in &info.symbols {
2085 let action = symbol
2086 .contract_status
2087 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
2088 statuses.insert(symbol.symbol, action);
2089 }
2090 }
2091 _ => {
2092 return Err(BinanceFuturesHttpError::ValidationError(
2093 "Invalid product type for futures".to_string(),
2094 ));
2095 }
2096 }
2097
2098 Ok(statuses)
2099 }
2100
2101 pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
2107 self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default())
2108 .await
2109 }
2110
2111 pub async fn request_instruments_with_config(
2123 &self,
2124 config: &BinanceInstrumentProviderConfig,
2125 ) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
2126 let _guard = self.instruments_load_lock.lock().await;
2127 config
2128 .validate(self.product_type)
2129 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2130 let selector = BinanceInstrumentSelector::new(config)
2131 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2132 let ts_init = UnixNanos::default();
2133 let mut cache = Vec::new();
2134 let mut reconciliation = AHashMap::new();
2135
2136 let instruments = match self.product_type {
2137 BinanceProductType::UsdM => {
2138 let info: BinanceFuturesUsdExchangeInfo = self
2139 .inner
2140 .get("exchangeInfo", None::<&()>, false, false)
2141 .await?;
2142
2143 let mut instruments = Vec::with_capacity(info.symbols.len());
2144
2145 for symbol in info.symbols {
2146 let instrument_id =
2147 format_instrument_id(&symbol.symbol, BinanceProductType::UsdM);
2148 cache.push((
2149 symbol.symbol,
2150 BinanceFuturesInstrument::UsdM(symbol.clone()),
2151 ));
2152
2153 if !selector.includes(
2154 instrument_id,
2155 &symbol.symbol,
2156 &symbol.base_asset,
2157 &symbol.quote_asset,
2158 Some(&symbol.contract_type),
2159 ) {
2160 continue;
2161 }
2162
2163 match parse_usdm_instrument_with_fees(&symbol, ts_init, ts_init) {
2164 Ok(instrument) => {
2165 validate_reconciliation_instrument(
2166 &mut reconciliation,
2167 instrument_id,
2168 &instrument,
2169 )?;
2170 instruments.push(instrument);
2171 }
2172 Err(e) => {
2173 log_futures_instrument_parse_error(
2174 config,
2175 &selector,
2176 instrument_id,
2177 &symbol.symbol,
2178 &e,
2179 );
2180 }
2181 }
2182 }
2183
2184 log::debug!(
2185 "Loaded USD-M Futures instruments: count={}",
2186 instruments.len()
2187 );
2188 instruments
2189 }
2190 BinanceProductType::CoinM => {
2191 let info: BinanceFuturesCoinExchangeInfo = self
2192 .inner
2193 .get("exchangeInfo", None::<&()>, false, false)
2194 .await?;
2195
2196 let mut instruments = Vec::with_capacity(info.symbols.len());
2197 for symbol in info.symbols {
2198 let instrument_id =
2199 format_instrument_id(&symbol.symbol, BinanceProductType::CoinM);
2200 cache.push((
2201 symbol.symbol,
2202 BinanceFuturesInstrument::CoinM(symbol.clone()),
2203 ));
2204
2205 if !selector.includes(
2206 instrument_id,
2207 &symbol.symbol,
2208 &symbol.base_asset,
2209 &symbol.quote_asset,
2210 Some(&symbol.contract_type),
2211 ) {
2212 continue;
2213 }
2214
2215 match parse_coinm_instrument_with_fees(&symbol, ts_init, ts_init) {
2216 Ok(instrument) => {
2217 validate_reconciliation_instrument(
2218 &mut reconciliation,
2219 instrument_id,
2220 &instrument,
2221 )?;
2222 instruments.push(instrument);
2223 }
2224 Err(e) => {
2225 log_futures_instrument_parse_error(
2226 config,
2227 &selector,
2228 instrument_id,
2229 &symbol.symbol,
2230 &e,
2231 );
2232 }
2233 }
2234 }
2235
2236 log::debug!(
2237 "Loaded COIN-M Futures instruments: count={}",
2238 instruments.len()
2239 );
2240 instruments
2241 }
2242 _ => {
2243 return Err(BinanceFuturesHttpError::ValidationError(
2244 "Invalid product type for futures".to_string(),
2245 ));
2246 }
2247 };
2248
2249 self.replace_instruments(cache)?;
2250 self.instruments_reconciliation.store(reconciliation);
2251 Ok(instruments)
2252 }
2253
2254 pub async fn ticker_24h(
2260 &self,
2261 params: &BinanceTicker24hrParams,
2262 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
2263 self.inner.ticker_24h(params).await
2264 }
2265
2266 pub async fn book_ticker(
2272 &self,
2273 params: &BinanceBookTickerParams,
2274 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
2275 self.inner.book_ticker(params).await
2276 }
2277
2278 pub async fn price_ticker(
2284 &self,
2285 symbol: Option<&str>,
2286 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
2287 self.inner.price_ticker(symbol).await
2288 }
2289
2290 pub async fn depth(
2296 &self,
2297 params: &BinanceDepthParams,
2298 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
2299 self.inner.depth(params).await
2300 }
2301
2302 pub async fn mark_price(
2308 &self,
2309 params: &BinanceMarkPriceParams,
2310 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
2311 self.inner.mark_price(params).await
2312 }
2313
2314 pub async fn funding_rate(
2320 &self,
2321 params: &BinanceFundingRateParams,
2322 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
2323 self.inner.funding_rate(params).await
2324 }
2325
2326 pub async fn open_interest(
2332 &self,
2333 params: &BinanceOpenInterestParams,
2334 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
2335 self.inner.open_interest(params).await
2336 }
2337
2338 pub async fn open_interest_hist(
2344 &self,
2345 params: &BinanceOpenInterestHistParams,
2346 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
2347 self.inner.open_interest_hist(params).await
2348 }
2349
2350 pub async fn query_order(
2356 &self,
2357 params: &BinanceOrderQueryParams,
2358 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
2359 self.inner.query_order(params).await
2360 }
2361
2362 pub async fn query_open_orders(
2368 &self,
2369 params: &BinanceOpenOrdersParams,
2370 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2371 self.inner.query_open_orders(params).await
2372 }
2373
2374 pub async fn query_all_orders(
2380 &self,
2381 params: &BinanceAllOrdersParams,
2382 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2383 self.inner.query_all_orders(params).await
2384 }
2385
2386 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
2392 self.inner.query_account().await
2393 }
2394
2395 pub async fn query_positions(
2401 &self,
2402 params: &BinancePositionRiskParams,
2403 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
2404 self.inner.query_positions(params).await
2405 }
2406
2407 pub async fn query_user_trades(
2413 &self,
2414 params: &BinanceUserTradesParams,
2415 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
2416 self.inner.query_user_trades(params).await
2417 }
2418
2419 #[expect(clippy::too_many_arguments)]
2429 pub async fn submit_order(
2430 &self,
2431 account_id: AccountId,
2432 instrument_id: InstrumentId,
2433 client_order_id: ClientOrderId,
2434 order_side: OrderSide,
2435 order_type: OrderType,
2436 quantity: Quantity,
2437 time_in_force: TimeInForce,
2438 price: Option<Price>,
2439 trigger_price: Option<Price>,
2440 reduce_only: bool,
2441 post_only: bool,
2442 rpi: bool,
2443 position_side: Option<BinancePositionSide>,
2444 price_match: Option<BinancePriceMatch>,
2445 good_till_date: Option<i64>,
2446 ) -> anyhow::Result<OrderStatusReport> {
2447 let (symbol, price_precision, size_precision) =
2448 self.cached_precisions_by_id(instrument_id)?;
2449
2450 let binance_side = BinanceSide::try_from(order_side)?;
2451 let binance_order_type = order_type_to_binance_futures(order_type)?;
2452 let binance_tif = if rpi {
2453 BinanceTimeInForce::Rpi
2454 } else if post_only {
2455 BinanceTimeInForce::Gtx
2456 } else {
2457 BinanceTimeInForce::try_from(time_in_force)?
2458 };
2459
2460 let requires_trigger_price = matches!(
2461 order_type,
2462 OrderType::StopMarket
2463 | OrderType::StopLimit
2464 | OrderType::TrailingStopMarket
2465 | OrderType::MarketIfTouched
2466 | OrderType::LimitIfTouched
2467 );
2468
2469 if requires_trigger_price && trigger_price.is_none() {
2470 anyhow::bail!("Order type {order_type:?} requires a trigger price");
2471 }
2472
2473 let requires_time_in_force = matches!(
2475 order_type,
2476 OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
2477 );
2478
2479 let qty_str = quantity.to_string();
2480 let price_str = if price_match.is_some() {
2481 None
2482 } else {
2483 price.map(|p| p.to_string())
2484 };
2485 let stop_price_str = trigger_price.map(|p| p.to_string());
2486 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2487
2488 let params = BinanceNewOrderParams {
2489 symbol,
2490 side: binance_side,
2491 order_type: binance_order_type,
2492 time_in_force: if requires_time_in_force {
2493 Some(binance_tif)
2494 } else {
2495 None
2496 },
2497 quantity: Some(qty_str),
2498 price: price_str,
2499 new_client_order_id: Some(client_id_str),
2500 stop_price: stop_price_str,
2501 reduce_only: reduce_only_param(reduce_only, position_side),
2502 position_side,
2503 close_position: None,
2504 activation_price: None,
2505 callback_rate: None,
2506 working_type: None,
2507 price_protect: None,
2508 new_order_resp_type: None,
2509 good_till_date,
2510 recv_window: None,
2511 price_match,
2512 self_trade_prevention_mode: None,
2513 };
2514
2515 let order = self.inner.submit_order(¶ms).await?;
2516 let ts_init = self.clock.get_time_ns();
2517 order.to_order_status_report(
2518 account_id,
2519 instrument_id,
2520 price_precision,
2521 size_precision,
2522 self.treat_expired_as_canceled,
2523 ts_init,
2524 )
2525 }
2526
2527 #[expect(clippy::too_many_arguments)]
2540 pub async fn submit_algo_order(
2541 &self,
2542 account_id: AccountId,
2543 instrument_id: InstrumentId,
2544 client_order_id: ClientOrderId,
2545 order_side: OrderSide,
2546 order_type: OrderType,
2547 quantity: Quantity,
2548 time_in_force: TimeInForce,
2549 price: Option<Price>,
2550 trigger_price: Option<Price>,
2551 reduce_only: bool,
2552 close_position: bool,
2553 position_side: Option<BinancePositionSide>,
2554 activation_price: Option<Price>,
2555 callback_rate: Option<String>,
2556 working_type: Option<BinanceWorkingType>,
2557 good_till_date: Option<i64>,
2558 ) -> anyhow::Result<OrderStatusReport> {
2559 let (symbol, price_precision, size_precision) =
2560 self.cached_precisions_by_id(instrument_id)?;
2561
2562 let binance_side = BinanceSide::try_from(order_side)?;
2563 let binance_order_type = order_type_to_binance_futures(order_type)?;
2564 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
2565
2566 let requires_trigger_price = matches!(
2567 order_type,
2568 OrderType::StopMarket
2569 | OrderType::StopLimit
2570 | OrderType::MarketIfTouched
2571 | OrderType::LimitIfTouched
2572 );
2573 anyhow::ensure!(
2574 !requires_trigger_price || trigger_price.is_some(),
2575 "Algo order type {order_type:?} requires a trigger price"
2576 );
2577
2578 let requires_time_in_force =
2580 matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
2581
2582 let price_str = price.map(|p| p.to_string());
2583 let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
2584 None
2585 } else {
2586 trigger_price.map(|p| p.to_string())
2587 };
2588 let reduce_only = reduce_only_param(reduce_only, position_side);
2589 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2590
2591 let params = if close_position {
2593 BinanceNewAlgoOrderParams {
2594 symbol,
2595 side: binance_side,
2596 order_type: binance_order_type,
2597 algo_type: BinanceAlgoType::Conditional,
2598 position_side,
2599 quantity: None,
2600 price: price_str,
2601 trigger_price: trigger_price_str,
2602 time_in_force: if requires_time_in_force {
2603 Some(binance_tif)
2604 } else {
2605 None
2606 },
2607 working_type,
2608 close_position: Some(true),
2609 price_protect: None,
2610 reduce_only: None,
2611 activation_price: activation_price.map(|p| p.to_string()),
2612 callback_rate,
2613 client_algo_id: Some(client_id_str),
2614 good_till_date,
2615 recv_window: None,
2616 }
2617 } else {
2618 let qty_str = quantity.to_string();
2619 BinanceNewAlgoOrderParams {
2620 symbol,
2621 side: binance_side,
2622 order_type: binance_order_type,
2623 algo_type: BinanceAlgoType::Conditional,
2624 position_side,
2625 quantity: Some(qty_str),
2626 price: price_str,
2627 trigger_price: trigger_price_str,
2628 time_in_force: if requires_time_in_force {
2629 Some(binance_tif)
2630 } else {
2631 None
2632 },
2633 working_type,
2634 close_position: None,
2635 price_protect: None,
2636 reduce_only,
2637 activation_price: activation_price.map(|p| p.to_string()),
2638 callback_rate,
2639 client_algo_id: Some(client_id_str),
2640 good_till_date,
2641 recv_window: None,
2642 }
2643 };
2644
2645 let order = self.inner.submit_algo_order(¶ms).await?;
2646 let ts_init = self.clock.get_time_ns();
2647 order.to_order_status_report(
2648 account_id,
2649 instrument_id,
2650 price_precision,
2651 size_precision,
2652 ts_init,
2653 )
2654 }
2655
2656 pub async fn submit_order_list(
2665 &self,
2666 orders: &[BatchOrderItem],
2667 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2668 self.inner.submit_order_list(orders).await
2669 }
2670
2671 #[expect(clippy::too_many_arguments)]
2682 pub async fn modify_order(
2683 &self,
2684 account_id: AccountId,
2685 instrument_id: InstrumentId,
2686 venue_order_id: Option<VenueOrderId>,
2687 client_order_id: Option<ClientOrderId>,
2688 order_side: OrderSide,
2689 quantity: Quantity,
2690 price: Price,
2691 ) -> anyhow::Result<OrderStatusReport> {
2692 anyhow::ensure!(
2693 venue_order_id.is_some() || client_order_id.is_some(),
2694 "Either venue_order_id or client_order_id must be provided"
2695 );
2696
2697 let (symbol, price_precision, size_precision) =
2698 self.cached_precisions_by_id(instrument_id)?;
2699
2700 let binance_side = BinanceSide::try_from(order_side)?;
2701
2702 let order_id = venue_order_id
2703 .map(|id| id.inner().parse::<i64>())
2704 .transpose()
2705 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2706
2707 let params = BinanceModifyOrderParams {
2708 symbol,
2709 order_id,
2710 orig_client_order_id: client_order_id
2711 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2712 side: binance_side,
2713 quantity: quantity.to_string(),
2714 price: price.to_string(),
2715 recv_window: None,
2716 };
2717
2718 let order = self.inner.modify_order(¶ms).await?;
2719 let ts_init = self.clock.get_time_ns();
2720 order.to_order_status_report(
2721 account_id,
2722 instrument_id,
2723 price_precision,
2724 size_precision,
2725 self.treat_expired_as_canceled,
2726 ts_init,
2727 )
2728 }
2729
2730 pub async fn batch_modify_orders(
2739 &self,
2740 modifies: &[BatchModifyItem],
2741 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2742 self.inner.batch_modify_orders(modifies).await
2743 }
2744
2745 pub async fn cancel_order(
2755 &self,
2756 instrument_id: InstrumentId,
2757 venue_order_id: Option<VenueOrderId>,
2758 client_order_id: Option<ClientOrderId>,
2759 ) -> anyhow::Result<VenueOrderId> {
2760 anyhow::ensure!(
2761 venue_order_id.is_some() || client_order_id.is_some(),
2762 "Either venue_order_id or client_order_id must be provided"
2763 );
2764
2765 let symbol = format_binance_symbol(&instrument_id);
2766
2767 let order_id = match venue_order_id {
2768 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2769 Ok(order_id) => Some(order_id),
2770 Err(e) if client_order_id.is_some() => {
2771 log::warn!(
2772 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2773 );
2774 None
2775 }
2776 Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2777 },
2778 None => None,
2779 };
2780
2781 let params = BinanceCancelOrderParams {
2782 symbol,
2783 order_id,
2784 orig_client_order_id: client_order_id
2785 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2786 recv_window: None,
2787 };
2788
2789 let order = self.inner.cancel_order(¶ms).await?;
2790 Ok(VenueOrderId::new(order.order_id.to_string()))
2791 }
2792
2793 pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2802 let params = BinanceAlgoOrderQueryParams {
2803 algo_id: None,
2804 client_algo_id: Some(encode_broker_id(
2805 &client_order_id,
2806 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2807 )),
2808 recv_window: None,
2809 };
2810
2811 let response = self.inner.cancel_algo_order(¶ms).await?;
2812 let code = response.code.parse::<i64>()?;
2813 if code == 200 {
2814 Ok(())
2815 } else {
2816 Err(BinanceFuturesHttpError::BinanceError {
2817 code,
2818 message: response.msg,
2819 status: 200,
2820 retry_after: None,
2821 }
2822 .into())
2823 }
2824 }
2825
2826 pub async fn cancel_all_orders(
2832 &self,
2833 instrument_id: InstrumentId,
2834 ) -> anyhow::Result<Vec<VenueOrderId>> {
2835 let symbol = format_binance_symbol(&instrument_id);
2836
2837 let params = BinanceCancelAllOrdersParams {
2838 symbol,
2839 recv_window: None,
2840 };
2841
2842 let response = self.inner.cancel_all_orders(¶ms).await?;
2843 if response.code == 200 {
2844 Ok(vec![])
2845 } else {
2846 anyhow::bail!("Cancel all orders failed: {}", response.msg);
2847 }
2848 }
2849
2850 pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2856 let symbol = format_binance_symbol(&instrument_id);
2857
2858 let params = BinanceCancelAllAlgoOrdersParams {
2859 symbol,
2860 recv_window: None,
2861 };
2862
2863 let response = self.inner.cancel_all_algo_orders(¶ms).await?;
2864 if response.code == 200 {
2865 Ok(())
2866 } else {
2867 anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2868 }
2869 }
2870
2871 pub async fn batch_cancel_orders(
2880 &self,
2881 cancels: &[BatchCancelItem],
2882 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2883 self.inner.batch_cancel_orders(cancels).await
2884 }
2885
2886 pub async fn query_open_algo_orders(
2894 &self,
2895 instrument_id: Option<InstrumentId>,
2896 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2897 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2898
2899 let params = BinanceOpenAlgoOrdersParams {
2900 symbol,
2901 recv_window: None,
2902 };
2903
2904 self.inner.query_open_algo_orders(¶ms).await
2905 }
2906
2907 pub async fn query_algo_order(
2913 &self,
2914 client_order_id: ClientOrderId,
2915 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2916 let params = BinanceAlgoOrderQueryParams {
2917 algo_id: None,
2918 client_algo_id: Some(encode_broker_id(
2919 &client_order_id,
2920 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2921 )),
2922 recv_window: None,
2923 };
2924
2925 self.inner.query_algo_order(¶ms).await
2926 }
2927
2928 pub async fn query_algo_order_by_venue_order_id(
2934 &self,
2935 venue_order_id: VenueOrderId,
2936 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2937 let algo_id = venue_order_id
2938 .inner()
2939 .parse::<i64>()
2940 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2941 let params = BinanceAlgoOrderQueryParams {
2942 algo_id: Some(algo_id),
2943 client_algo_id: None,
2944 recv_window: None,
2945 };
2946
2947 self.inner.query_algo_order(¶ms).await
2948 }
2949
2950 async fn query_historical_algo_order_by_venue_order_id(
2951 &self,
2952 instrument_id: InstrumentId,
2953 venue_order_id: VenueOrderId,
2954 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrder>> {
2955 let algo_id = venue_order_id
2956 .inner()
2957 .parse::<i64>()
2958 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2959 let symbol = format_binance_symbol(&instrument_id);
2960 let params = BinanceAllAlgoOrdersParams {
2961 symbol,
2962 algo_id: Some(algo_id),
2963 start_time: None,
2964 end_time: None,
2965 page: None,
2966 limit: Some(1),
2967 recv_window: None,
2968 };
2969 let order = self
2970 .inner
2971 .query_all_algo_orders(¶ms)
2972 .await?
2973 .into_iter()
2974 .next()
2975 .filter(|order| order.algo_id == algo_id);
2976
2977 Ok(order)
2978 }
2979
2980 pub async fn query_algo_order_with_history(
2991 &self,
2992 instrument_id: InstrumentId,
2993 client_order_id: Option<ClientOrderId>,
2994 algo_venue_order_id: Option<VenueOrderId>,
2995 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrderQueryResult>> {
2996 let order = if let Some(venue_order_id) = algo_venue_order_id {
2997 match self
2998 .query_algo_order_by_venue_order_id(venue_order_id)
2999 .await
3000 {
3001 Ok(order) => Some(order),
3002 Err(BinanceFuturesHttpError::BinanceError {
3003 code: BINANCE_NO_SUCH_ORDER_CODE,
3004 ..
3005 }) => {
3006 self.query_historical_algo_order_by_venue_order_id(
3007 instrument_id,
3008 venue_order_id,
3009 )
3010 .await?
3011 }
3012 Err(e) => return Err(e),
3013 }
3014 } else {
3015 let Some(client_order_id) = client_order_id else {
3016 return Ok(None);
3017 };
3018
3019 match self.query_algo_order(client_order_id).await {
3020 Ok(order) => Some(order),
3021 Err(BinanceFuturesHttpError::BinanceError {
3022 code: BINANCE_NO_SUCH_ORDER_CODE,
3023 ..
3024 }) => None,
3025 Err(e) => return Err(e),
3026 }
3027 };
3028
3029 let Some(order) = order else {
3030 return Ok(None);
3031 };
3032 let actual = if let Some(actual_order_id) = order
3033 .actual_order_id
3034 .as_deref()
3035 .filter(|id| !id.is_empty())
3036 .map(str::parse::<i64>)
3037 .transpose()
3038 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
3039 {
3040 let params = BinanceOrderQueryParams {
3041 symbol: format_binance_symbol(&instrument_id),
3042 order_id: Some(actual_order_id),
3043 orig_client_order_id: None,
3044 recv_window: None,
3045 };
3046
3047 match self.inner.query_order(¶ms).await {
3048 Ok(actual) => Some(actual),
3049 Err(
3050 e @ (BinanceFuturesHttpError::MissingCredentials
3051 | BinanceFuturesHttpError::ValidationError(_)),
3052 ) => return Err(e),
3053 Err(e) => {
3054 log::warn!(
3055 "Failed to enrich algo order with matching-engine order \
3056 {actual_order_id}: {e}; falling back to Algo Service execution fields"
3057 );
3058 None
3059 }
3060 }
3061 } else {
3062 None
3063 };
3064
3065 Ok(Some(BinanceFuturesAlgoOrderQueryResult {
3066 algo: order,
3067 actual,
3068 }))
3069 }
3070
3071 pub async fn request_account_state(
3077 &self,
3078 account_id: AccountId,
3079 ) -> anyhow::Result<AccountState> {
3080 let ts_init = UnixNanos::default();
3081 let account_info = self.inner.query_account().await?;
3082 account_info.to_account_state(account_id, ts_init)
3083 }
3084
3085 pub async fn request_order_status_report(
3093 &self,
3094 account_id: AccountId,
3095 instrument_id: InstrumentId,
3096 venue_order_id: Option<VenueOrderId>,
3097 client_order_id: Option<ClientOrderId>,
3098 ) -> anyhow::Result<OrderStatusReport> {
3099 anyhow::ensure!(
3100 venue_order_id.is_some() || client_order_id.is_some(),
3101 "Either venue_order_id or client_order_id must be provided"
3102 );
3103
3104 let (symbol, price_precision, size_precision) =
3105 self.cached_precisions_by_id(instrument_id)?;
3106
3107 let order_id = venue_order_id
3108 .map(|id| id.inner().parse::<i64>())
3109 .transpose()
3110 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3111
3112 let orig_client_order_id =
3113 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
3114
3115 let params = BinanceOrderQueryParams {
3116 symbol,
3117 order_id,
3118 orig_client_order_id,
3119 recv_window: None,
3120 };
3121
3122 let order = self.inner.query_order(¶ms).await?;
3123 let ts_init = self.clock.get_time_ns();
3124 order.to_order_status_report(
3125 account_id,
3126 instrument_id,
3127 price_precision,
3128 size_precision,
3129 self.treat_expired_as_canceled,
3130 ts_init,
3131 )
3132 }
3133
3134 pub async fn request_order_status_reports(
3142 &self,
3143 account_id: AccountId,
3144 instrument_id: Option<InstrumentId>,
3145 open_only: bool,
3146 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3147 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
3148
3149 let orders = if open_only {
3150 let params = BinanceOpenOrdersParams {
3151 symbol: symbol.clone(),
3152 recv_window: None,
3153 };
3154 self.inner.query_open_orders(¶ms).await?
3155 } else {
3156 let symbol = symbol.ok_or_else(|| {
3158 anyhow::anyhow!("instrument_id is required for historical orders")
3159 })?;
3160 let params = BinanceAllOrdersParams {
3161 symbol,
3162 order_id: None,
3163 start_time: None,
3164 end_time: None,
3165 limit: None,
3166 recv_window: None,
3167 };
3168 self.inner.query_all_orders(¶ms).await?
3169 };
3170
3171 let ts_init = self.clock.get_time_ns();
3172 let mut reports = Vec::with_capacity(orders.len());
3173
3174 for order in orders {
3175 let order_instrument_id = instrument_id
3176 .unwrap_or_else(|| format_instrument_id(&order.symbol, self.product_type));
3177 let (_, price_precision, size_precision) =
3178 self.cached_precisions_by_id(order_instrument_id)?;
3179
3180 match order.to_order_status_report(
3181 account_id,
3182 order_instrument_id,
3183 price_precision,
3184 size_precision,
3185 self.treat_expired_as_canceled,
3186 ts_init,
3187 ) {
3188 Ok(report) => reports.push(report),
3189 Err(e) => {
3190 log::warn!("Failed to parse order status report: {e}");
3191 }
3192 }
3193 }
3194
3195 Ok(reports)
3196 }
3197
3198 #[expect(clippy::too_many_arguments)]
3204 pub async fn request_fill_reports(
3205 &self,
3206 account_id: AccountId,
3207 instrument_id: InstrumentId,
3208 venue_order_id: Option<VenueOrderId>,
3209 start: Option<i64>,
3210 end: Option<i64>,
3211 limit: Option<u32>,
3212 bnfcr_currency: Currency,
3213 ) -> anyhow::Result<Vec<FillReport>> {
3214 let (symbol, price_precision, size_precision) =
3215 self.cached_precisions_by_id(instrument_id)?;
3216
3217 let order_id = venue_order_id
3218 .map(|id| id.inner().parse::<i64>())
3219 .transpose()
3220 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3221
3222 let params = BinanceUserTradesParams {
3223 symbol,
3224 order_id,
3225 start_time: start,
3226 end_time: end,
3227 from_id: None,
3228 limit,
3229 recv_window: None,
3230 };
3231
3232 let trades = self.inner.query_user_trades(¶ms).await?;
3233
3234 let ts_init = self.clock.get_time_ns();
3235 let mut reports = Vec::with_capacity(trades.len());
3236
3237 for trade in trades {
3238 match trade.to_fill_report(
3239 account_id,
3240 instrument_id,
3241 price_precision,
3242 size_precision,
3243 bnfcr_currency,
3244 ts_init,
3245 ) {
3246 Ok(report) => reports.push(report),
3247 Err(e) => {
3248 log::warn!("Failed to parse fill report: {e}");
3249 }
3250 }
3251 }
3252
3253 Ok(reports)
3254 }
3255
3256 pub async fn request_trades(
3262 &self,
3263 instrument_id: InstrumentId,
3264 limit: Option<u32>,
3265 ) -> anyhow::Result<Vec<TradeTick>> {
3266 let (symbol, price_precision, size_precision) =
3267 self.cached_precisions_by_id(instrument_id)?;
3268
3269 let params = BinanceTradesParams { symbol, limit };
3270
3271 let trades = self.inner.trades(¶ms).await?;
3272 let ts_init = UnixNanos::default();
3273
3274 let mut result = Vec::with_capacity(trades.len());
3275 for trade in trades {
3276 let tick = parse_futures_trade_tick(
3277 &trade,
3278 instrument_id,
3279 price_precision,
3280 size_precision,
3281 ts_init,
3282 )?;
3283 result.push(tick);
3284 }
3285
3286 Ok(result)
3287 }
3288
3289 pub async fn request_agg_trades(
3295 &self,
3296 instrument_id: InstrumentId,
3297 start: Option<Timestamp>,
3298 end: Option<Timestamp>,
3299 limit: Option<u32>,
3300 ) -> anyhow::Result<Vec<TradeTick>> {
3301 let cutoff =
3302 self.clock.get_time_ns().to_datetime_utc() - jiff::SignedDuration::from_hours(24);
3303 anyhow::ensure!(
3304 start.as_ref().is_none_or(|value| value >= &cutoff)
3305 && end.as_ref().is_none_or(|value| value >= &cutoff),
3306 "Binance Futures aggregate trade history is limited to the past 24 hours"
3307 );
3308 let (symbol, price_precision, size_precision) =
3309 self.cached_precisions_by_id(instrument_id)?;
3310 let params = BinanceAggTradesParams {
3311 symbol,
3312 from_id: None,
3313 start_time: start.map(|value| value.as_millisecond()),
3314 end_time: end.map(|value| value.as_millisecond()),
3315 limit,
3316 };
3317 let trades = self.inner.agg_trades(¶ms).await?;
3318 trades
3319 .iter()
3320 .map(|trade| {
3321 let ts_init = parse_millis(trade.time, "Futures aggregate trade time")?;
3322 parse_futures_agg_trade_tick(
3323 trade,
3324 instrument_id,
3325 price_precision,
3326 size_precision,
3327 ts_init,
3328 )
3329 })
3330 .collect()
3331 }
3332
3333 pub async fn request_binance_bars(
3340 &self,
3341 bar_type: BarType,
3342 start: Option<Timestamp>,
3343 end: Option<Timestamp>,
3344 limit: Option<u32>,
3345 ) -> anyhow::Result<Vec<BinanceBar>> {
3346 anyhow::ensure!(
3347 bar_type.aggregation_source() == AggregationSource::External,
3348 "Only EXTERNAL aggregation is supported"
3349 );
3350
3351 let spec = bar_type.spec();
3352 let step = spec.step.get();
3353 let interval = match spec.aggregation {
3354 BarAggregation::Second => {
3355 anyhow::bail!("Binance Futures does not support second-level kline intervals")
3356 }
3357 BarAggregation::Minute => format!("{step}m"),
3358 BarAggregation::Hour => format!("{step}h"),
3359 BarAggregation::Day => format!("{step}d"),
3360 BarAggregation::Week => format!("{step}w"),
3361 BarAggregation::Month => format!("{step}M"),
3362 a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
3363 };
3364
3365 let instrument_id = bar_type.instrument_id();
3366 let (symbol, price_precision, size_precision) =
3367 self.cached_precisions_by_id(instrument_id)?;
3368
3369 let params = BinanceKlinesParams {
3370 symbol,
3371 interval,
3372 start_time: start.map(|dt| dt.as_millisecond()),
3373 end_time: end.map(|dt| dt.as_millisecond()),
3374 limit,
3375 };
3376
3377 let klines = self.inner.klines(¶ms).await?;
3378 let now = self.clock.get_time_ns();
3379
3380 let mut result = Vec::with_capacity(klines.len());
3381 for kline in klines {
3382 let ts_init = parse_millis(kline.close_time, "Futures kline close time")?;
3383 let bar = parse_futures_kline_binance_bar(
3384 &kline,
3385 bar_type,
3386 price_precision,
3387 size_precision,
3388 ts_init,
3389 )?;
3390
3391 if bar.ts_event < now {
3392 result.push(bar);
3393 }
3394 }
3395
3396 Ok(result)
3397 }
3398
3399 pub async fn request_bars(
3405 &self,
3406 bar_type: BarType,
3407 start: Option<Timestamp>,
3408 end: Option<Timestamp>,
3409 limit: Option<u32>,
3410 ) -> anyhow::Result<Vec<Bar>> {
3411 Ok(self
3412 .request_binance_bars(bar_type, start, end, limit)
3413 .await?
3414 .into_iter()
3415 .map(|bar| bar.bar())
3416 .collect())
3417 }
3418
3419 pub async fn request_book_snapshot(
3426 &self,
3427 instrument_id: InstrumentId,
3428 depth: Option<u32>,
3429 ) -> anyhow::Result<OrderBook> {
3430 if depth.is_some_and(|value| !crate::common::consts::BINANCE_BOOK_DEPTHS.contains(&value)) {
3431 anyhow::bail!(
3432 "invalid Binance Futures order-book depth; valid values are {:?}",
3433 crate::common::consts::BINANCE_BOOK_DEPTHS
3434 );
3435 }
3436 let (symbol, price_precision, size_precision) =
3437 self.cached_precisions_by_id(instrument_id)?;
3438 let params = BinanceDepthParams {
3439 symbol,
3440 limit: depth,
3441 };
3442 let snapshot = self.inner.depth(¶ms).await?;
3443 let ts_event = self.clock.get_time_ns();
3444 let sequence = u64::try_from(snapshot.last_update_id)
3445 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3446 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3447 for (index, level) in snapshot.bids.iter().enumerate() {
3448 let order = BookOrder::new(
3449 OrderSide::Buy,
3450 parse_required_price_at_precision(&level.0, price_precision, "bid price")?,
3451 parse_required_quantity_at_precision(&level.1, size_precision, "bid quantity")?,
3452 index as u64,
3453 );
3454 book.add(order, 0, sequence, ts_event);
3455 }
3456 let bid_count = snapshot.bids.len();
3457 for (index, level) in snapshot.asks.iter().enumerate() {
3458 let order = BookOrder::new(
3459 OrderSide::Sell,
3460 parse_required_price_at_precision(&level.0, price_precision, "ask price")?,
3461 parse_required_quantity_at_precision(&level.1, size_precision, "ask quantity")?,
3462 (bid_count + index) as u64,
3463 );
3464 book.add(order, 0, sequence, ts_event);
3465 }
3466 Ok(book)
3467 }
3468
3469 fn cached_precisions_by_id(
3470 &self,
3471 instrument_id: InstrumentId,
3472 ) -> anyhow::Result<(String, u8, u8)> {
3473 let symbol = format_binance_symbol(&instrument_id);
3474 let instrument = self.instrument_metadata(instrument_id)?;
3475 let (price_precision, size_precision) = instrument.precisions()?;
3476
3477 Ok((symbol, price_precision, size_precision))
3478 }
3479
3480 pub(crate) fn instrument_metadata(
3481 &self,
3482 instrument_id: InstrumentId,
3483 ) -> anyhow::Result<BinanceFuturesInstrument> {
3484 let symbol = format_binance_symbol(&instrument_id);
3485 let instrument = self
3486 .instruments
3487 .get(&Ustr::from(symbol.as_str()))
3488 .map(|instrument| instrument.value().clone())
3489 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
3490 if instrument.id() != instrument_id {
3491 return Err(InstrumentLookupError::not_found(instrument_id).into());
3492 }
3493
3494 Ok(instrument)
3495 }
3496
3497 pub async fn request_funding_rates(
3503 &self,
3504 instrument_id: InstrumentId,
3505 start: Option<Timestamp>,
3506 end: Option<Timestamp>,
3507 limit: Option<u32>,
3508 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3509 let params = BinanceFundingRateParams {
3510 symbol: Some(format_binance_symbol(&instrument_id)),
3511 start_time: start.map(|dt| dt.as_millisecond()),
3512 end_time: end.map(|dt| dt.as_millisecond()),
3513 limit,
3514 };
3515
3516 let rates = self.inner.funding_rate(¶ms).await?;
3517 let ts_init = UnixNanos::default();
3518
3519 let mut result = Vec::with_capacity(rates.len());
3520 for rate in rates {
3521 result.push(parse_futures_funding_rate_update(
3522 &rate,
3523 instrument_id,
3524 ts_init,
3525 )?);
3526 }
3527
3528 Ok(result)
3529 }
3530}
3531
3532fn identity_lookup_error(error: BinanceFuturesHttpError) -> BinanceFuturesHttpError {
3533 match error {
3535 BinanceFuturesHttpError::BinanceError {
3536 code: -2011 | BINANCE_NO_SUCH_ORDER_CODE,
3537 ..
3538 } => error,
3539 _ => BinanceFuturesHttpError::ValidationError(format!(
3540 "Order identity lookup failed before submission: {error}"
3541 )),
3542 }
3543}
3544
3545fn merge_batch_results(
3546 results: Vec<Option<BatchOrderResult>>,
3547 responses: Vec<BatchOrderResult>,
3548) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
3549 if results.iter().filter(|result| result.is_none()).count() != responses.len() {
3550 return Err(BinanceFuturesHttpError::JsonError(
3551 "Batch response length does not match submitted orders".to_string(),
3552 ));
3553 }
3554
3555 let mut responses = responses.into_iter();
3556 Ok(results
3557 .into_iter()
3558 .filter_map(|result| result.or_else(|| responses.next()))
3559 .collect())
3560}
3561
3562fn parse_futures_trade_tick(
3563 trade: &BinanceFuturesTrade,
3564 instrument_id: InstrumentId,
3565 price_precision: u8,
3566 size_precision: u8,
3567 ts_init: UnixNanos,
3568) -> anyhow::Result<TradeTick> {
3569 let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
3570 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3571 let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
3572 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3573 let ts_event = parse_millis(trade.time, "Futures trade time")?;
3574
3575 let aggressor_side = if trade.is_buyer_maker {
3576 AggressorSide::Sell
3577 } else {
3578 AggressorSide::Buy
3579 };
3580
3581 Ok(TradeTick::new(
3582 instrument_id,
3583 price,
3584 size,
3585 aggressor_side,
3586 TradeId::new(trade.id.to_string()),
3587 ts_event,
3588 ts_init,
3589 ))
3590}
3591
3592fn parse_futures_agg_trade_tick(
3593 trade: &BinanceFuturesAggTrade,
3594 instrument_id: InstrumentId,
3595 price_precision: u8,
3596 size_precision: u8,
3597 ts_init: UnixNanos,
3598) -> anyhow::Result<TradeTick> {
3599 let trade = BinanceFuturesTrade {
3600 id: trade.id,
3601 price: trade.price.clone(),
3602 qty: trade.qty.clone(),
3603 quote_qty: String::new(),
3604 time: trade.time,
3605 is_buyer_maker: trade.is_buyer_maker,
3606 };
3607 parse_futures_trade_tick(
3608 &trade,
3609 instrument_id,
3610 price_precision,
3611 size_precision,
3612 ts_init,
3613 )
3614}
3615
3616fn parse_futures_kline_binance_bar(
3617 kline: &BinanceFuturesKline,
3618 bar_type: BarType,
3619 price_precision: u8,
3620 size_precision: u8,
3621 ts_init: UnixNanos,
3622) -> anyhow::Result<BinanceBar> {
3623 let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
3624 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3625 let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
3626 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3627 let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
3628 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3629 let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
3630 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3631 let volume =
3632 parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
3633 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3634 let ts_event = parse_millis(kline.close_time, "Futures kline close time")?;
3635
3636 let quote_volume = kline.quote_volume.parse::<Decimal>().map_err(|e| {
3637 anyhow::anyhow!(
3638 "invalid Futures kline {} quote volume: {e}",
3639 kline.open_time
3640 )
3641 })?;
3642 let taker_buy_base_volume = kline
3643 .taker_buy_base_volume
3644 .parse::<Decimal>()
3645 .map_err(|e| {
3646 anyhow::anyhow!(
3647 "invalid Futures kline {} taker buy base volume: {e}",
3648 kline.open_time
3649 )
3650 })?;
3651 let taker_buy_quote_volume = kline
3652 .taker_buy_quote_volume
3653 .parse::<Decimal>()
3654 .map_err(|e| {
3655 anyhow::anyhow!(
3656 "invalid Futures kline {} taker buy quote volume: {e}",
3657 kline.open_time
3658 )
3659 })?;
3660 let count = u64::try_from(kline.num_trades).map_err(|_| {
3661 anyhow::anyhow!(
3662 "invalid Futures kline {} negative trade count",
3663 kline.open_time
3664 )
3665 })?;
3666
3667 Ok(BinanceBar::new(
3668 bar_type,
3669 open,
3670 high,
3671 low,
3672 close,
3673 volume,
3674 quote_volume,
3675 count,
3676 taker_buy_base_volume,
3677 taker_buy_quote_volume,
3678 ts_event,
3679 ts_init,
3680 ))
3681}
3682
3683fn parse_futures_funding_rate_update(
3684 rate: &BinanceFundingRate,
3685 instrument_id: InstrumentId,
3686 ts_init: UnixNanos,
3687) -> anyhow::Result<FundingRateUpdate> {
3688 let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
3689 anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
3690 })?;
3691 let ts_event = parse_millis(rate.funding_time, "Futures funding time")?;
3692
3693 Ok(FundingRateUpdate::new(
3694 instrument_id,
3695 funding_rate,
3696 None, None, ts_event,
3699 ts_init,
3700 ))
3701}
3702
3703fn validate_reconciliation_instrument(
3704 instruments: &mut AHashMap<InstrumentId, InstrumentAny>,
3705 expected_id: InstrumentId,
3706 instrument: &InstrumentAny,
3707) -> BinanceFuturesHttpResult<()> {
3708 let parsed_id = instrument.id();
3709 if parsed_id != expected_id {
3710 return Err(BinanceFuturesHttpError::ValidationError(format!(
3711 "Parsed Binance Futures instrument ID {parsed_id} does not match expected ID {expected_id}"
3712 )));
3713 }
3714
3715 if instruments.insert(parsed_id, instrument.clone()).is_some() {
3716 return Err(BinanceFuturesHttpError::ValidationError(format!(
3717 "Duplicate parsed Binance Futures instrument ID {parsed_id}"
3718 )));
3719 }
3720
3721 Ok(())
3722}
3723
3724fn log_futures_instrument_parse_error(
3725 config: &BinanceInstrumentProviderConfig,
3726 selector: &BinanceInstrumentSelector,
3727 instrument_id: InstrumentId,
3728 symbol: &str,
3729 error: &anyhow::Error,
3730) {
3731 let explicit = selector.is_explicit(instrument_id, symbol);
3732 if should_warn_on_instrument_parse_error(config.log_warnings, explicit, error) {
3733 log::warn!("Skipping Binance Futures instrument {symbol}: {error}");
3734 } else {
3735 log::debug!("Skipping Binance Futures instrument {symbol}: {error}");
3736 }
3737}
3738
3739#[must_use]
3744pub fn is_algo_order_type(order_type: OrderType) -> bool {
3745 matches!(
3746 order_type,
3747 OrderType::StopMarket
3748 | OrderType::StopLimit
3749 | OrderType::MarketIfTouched
3750 | OrderType::LimitIfTouched
3751 | OrderType::TrailingStopMarket
3752 )
3753}
3754
3755pub(crate) fn order_type_to_binance_futures(
3757 order_type: OrderType,
3758) -> anyhow::Result<BinanceFuturesOrderType> {
3759 match order_type {
3760 OrderType::Market => Ok(BinanceFuturesOrderType::Market),
3761 OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
3762 OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
3763 OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
3764 OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
3765 OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
3766 OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
3767 _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
3768 }
3769}
3770
3771#[cfg(test)]
3772mod tests {
3773 use nautilus_core::time::get_atomic_clock_realtime;
3774 use nautilus_live::execution::failure::CommandFailure;
3775 use nautilus_network::http::{HttpStatus, StatusCode};
3776 use nautilus_testkit::http::assert_http_redirect_rejected;
3777 use rstest::rstest;
3778 use tokio_util::bytes::Bytes;
3779
3780 use super::*;
3781 use crate::common::{enums::BinanceTradingStatus, failure::classify_futures_http_failure};
3782
3783 #[tokio::test]
3784 async fn test_authenticated_client_rejects_redirects() {
3785 let client = BinanceRawFuturesHttpClient::new(
3786 BinanceProductType::UsdM,
3787 BinanceEnvironment::Testnet,
3788 Some("key".into()),
3789 Some("secret".into()),
3790 None,
3791 None,
3792 Some(3),
3793 None,
3794 )
3795 .unwrap()
3796 .client;
3797 assert_http_redirect_rejected(|url| async move {
3798 client
3799 .get(url, None, None, Some(3), None)
3800 .await
3801 .unwrap()
3802 .status
3803 .as_u16()
3804 })
3805 .await;
3806 }
3807
3808 #[rstest]
3809 fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
3810 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3811
3812 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3813 assert_eq!(config.order_keys.len(), 2);
3814 assert!(
3815 config
3816 .order_keys
3817 .iter()
3818 .any(|key| key == "binance:orders:10:Second")
3819 );
3820 assert!(
3821 config
3822 .order_keys
3823 .iter()
3824 .any(|key| key == "binance:orders:1:Minute")
3825 );
3826
3827 let ten_second_quota = config
3828 .order_quotas
3829 .iter()
3830 .find(|(key, _)| key == "binance:orders:10:Second")
3831 .map(|(_, quota)| quota)
3832 .expect("USD-M 10-second order quota");
3833 assert_eq!(ten_second_quota.burst_size().get(), 300);
3834 assert_eq!(
3835 ten_second_quota.replenish_interval(),
3836 Duration::from_nanos(33_333_333)
3837 );
3838 }
3839
3840 #[rstest]
3841 fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
3842 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3843
3844 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3845 assert_eq!(config.order_keys.len(), 2);
3846 assert!(
3847 config
3848 .order_keys
3849 .iter()
3850 .any(|key| key == "binance:orders:10:Second")
3851 );
3852 assert!(
3853 config
3854 .order_keys
3855 .iter()
3856 .any(|key| key == "binance:orders:1:Minute")
3857 );
3858
3859 let ten_second_quota = config
3860 .order_quotas
3861 .iter()
3862 .find(|(key, _)| key == "binance:orders:10:Second")
3863 .map(|(_, quota)| quota)
3864 .expect("COIN-M 10-second order quota");
3865 let minute_quota = config
3866 .order_quotas
3867 .iter()
3868 .find(|(key, _)| key == "binance:orders:1:Minute")
3869 .map(|(_, quota)| quota)
3870 .expect("COIN-M one-minute order quota");
3871
3872 assert_eq!(ten_second_quota.burst_size().get(), 300);
3873 assert_eq!(minute_quota.burst_size().get(), 1_200);
3874 }
3875
3876 #[rstest]
3877 fn test_rate_limiters_share_usdm_and_coinm_scopes() {
3878 let account = Some(BinanceFuturesAccountScope([1; 32]));
3879 let usdm_config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3880 let coinm_config =
3881 BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3882
3883 let usdm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3884 BinanceEnvironment::Live,
3885 None,
3886 None,
3887 account,
3888 usdm_config.request_quota,
3889 usdm_config.order_quotas,
3890 );
3891 let coinm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3892 BinanceEnvironment::Live,
3893 Some(get_http_base_url(
3894 BinanceProductType::CoinM,
3895 BinanceEnvironment::Live,
3896 )),
3897 None,
3898 account,
3899 coinm_config.request_quota,
3900 coinm_config.order_quotas,
3901 );
3902 let public = create_test_rate_limiters(BinanceEnvironment::Live, None, None, None);
3903
3904 assert_eq!(usdm.len(), 2);
3905 assert_eq!(coinm.len(), 2);
3906 assert_eq!(public.len(), 1);
3907 assert!(Arc::ptr_eq(&usdm[0], &coinm[0]));
3908 assert!(Arc::ptr_eq(&usdm[0], &public[0]));
3909 assert!(Arc::ptr_eq(&usdm[1], &coinm[1]));
3910 }
3911
3912 #[rstest]
3913 fn test_rate_limiters_isolate_unrelated_scopes() {
3914 let account_a = Some(BinanceFuturesAccountScope([2; 32]));
3915 let account_b = Some(BinanceFuturesAccountScope([3; 32]));
3916
3917 let live_direct =
3918 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_a);
3919 let testnet_direct =
3920 create_test_rate_limiters(BinanceEnvironment::Testnet, None, None, account_a);
3921 let demo_direct =
3922 create_test_rate_limiters(BinanceEnvironment::Demo, None, None, account_a);
3923 let custom_a = create_test_rate_limiters(
3924 BinanceEnvironment::Live,
3925 Some("http://127.0.0.1:41001"),
3926 None,
3927 account_a,
3928 );
3929 let custom_b = create_test_rate_limiters(
3930 BinanceEnvironment::Live,
3931 Some("http://127.0.0.1:41002"),
3932 None,
3933 account_a,
3934 );
3935 let proxy_a = create_test_rate_limiters(
3936 BinanceEnvironment::Live,
3937 None,
3938 Some("http://127.0.0.1:42001"),
3939 account_a,
3940 );
3941 let proxy_b = create_test_rate_limiters(
3942 BinanceEnvironment::Live,
3943 None,
3944 Some("http://127.0.0.1:42002"),
3945 account_a,
3946 );
3947 let other_account =
3948 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_b);
3949
3950 assert!(!Arc::ptr_eq(&live_direct[0], &testnet_direct[0]));
3951 assert!(!Arc::ptr_eq(&live_direct[0], &demo_direct[0]));
3952 assert!(!Arc::ptr_eq(&live_direct[0], &custom_a[0]));
3953 assert!(!Arc::ptr_eq(&custom_a[0], &custom_b[0]));
3954 assert!(!Arc::ptr_eq(&proxy_a[0], &proxy_b[0]));
3955 assert!(Arc::ptr_eq(&proxy_a[1], &proxy_b[1]));
3956 assert!(Arc::ptr_eq(&live_direct[0], &other_account[0]));
3957 assert!(!Arc::ptr_eq(&live_direct[1], &other_account[1]));
3958 }
3959
3960 fn create_test_rate_limiters(
3961 environment: BinanceEnvironment,
3962 base_url_override: Option<&str>,
3963 proxy_url: Option<&str>,
3964 account_scope: Option<BinanceFuturesAccountScope>,
3965 ) -> Vec<BinanceFuturesRateLimiter> {
3966 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3967 BinanceRawFuturesHttpClient::shared_rate_limiters(
3968 environment,
3969 base_url_override,
3970 proxy_url,
3971 account_scope,
3972 config.request_quota,
3973 config.order_quotas,
3974 )
3975 }
3976
3977 #[rstest]
3978 fn test_rate_limit_keys_usdm_include_order_buckets() {
3979 let client = BinanceRawFuturesHttpClient::new(
3980 BinanceProductType::UsdM,
3981 BinanceEnvironment::Live,
3982 None,
3983 None,
3984 None,
3985 None,
3986 None,
3987 None,
3988 )
3989 .unwrap();
3990
3991 assert_eq!(
3992 client.rate_limit_keys(false),
3993 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
3994 );
3995 assert_eq!(
3996 client.rate_limit_keys(true),
3997 vec![
3998 BINANCE_GLOBAL_RATE_KEY.to_string(),
3999 "binance:orders:10:Second".to_string(),
4000 "binance:orders:1:Minute".to_string(),
4001 ]
4002 );
4003 }
4004
4005 #[rstest]
4006 fn test_quota_from_unknown_interval_returns_none() {
4007 let quota = BinanceRateLimitQuota {
4008 rate_limit_type: BinanceRateLimitType::Orders,
4009 interval: BinanceRateLimitInterval::Unknown,
4010 interval_num: 1,
4011 limit: 10,
4012 };
4013
4014 assert!(BinanceRawFuturesHttpClient::quota_from("a).is_none());
4015 }
4016
4017 #[rstest]
4018 fn test_create_client_rejects_spot_product_type() {
4019 let result = BinanceFuturesHttpClient::new(
4020 BinanceProductType::Spot,
4021 BinanceEnvironment::Live,
4022 get_atomic_clock_realtime(),
4023 None,
4024 None,
4025 None,
4026 None,
4027 None,
4028 None,
4029 false,
4030 );
4031
4032 result.unwrap_err();
4033 }
4034
4035 #[rstest]
4036 fn test_parse_futures_trade_tick_rejects_invalid_price() {
4037 let trade = BinanceFuturesTrade {
4038 id: 100,
4039 price: "not-a-number".to_string(),
4040 qty: "0.001".to_string(),
4041 quote_qty: "50.00".to_string(),
4042 time: 1_625_474_304_000,
4043 is_buyer_maker: false,
4044 };
4045
4046 let result = parse_futures_trade_tick(
4047 &trade,
4048 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
4049 2,
4050 3,
4051 UnixNanos::from(1_000_000_000u64),
4052 );
4053
4054 let error = result.unwrap_err().to_string();
4055 assert!(error.contains("trade.price"));
4056 assert!(error.contains("100"));
4057 }
4058
4059 #[rstest]
4060 fn test_parse_futures_kline_bar_rejects_invalid_volume() {
4061 let kline = BinanceFuturesKline {
4062 open_time: 1_625_474_304_000,
4063 open: "50000.00".to_string(),
4064 high: "51000.00".to_string(),
4065 low: "49000.00".to_string(),
4066 close: "50500.00".to_string(),
4067 volume: "not-a-number".to_string(),
4068 close_time: 1_625_474_364_000,
4069 quote_volume: "631250.00".to_string(),
4070 num_trades: 100,
4071 taker_buy_base_volume: "6.2".to_string(),
4072 taker_buy_quote_volume: "313100.00".to_string(),
4073 };
4074
4075 let result = parse_futures_kline_binance_bar(
4076 &kline,
4077 BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
4078 2,
4079 3,
4080 UnixNanos::from(1_000_000_000u64),
4081 );
4082
4083 let error = result.unwrap_err().to_string();
4084 assert!(error.contains("kline.volume"));
4085 assert!(error.contains("1625474304000"));
4086 }
4087
4088 #[rstest]
4089 #[case::limit(
4090 BinanceAggTradesParams {
4091 symbol: "BTCUSDT".to_string(),
4092 from_id: None,
4093 start_time: None,
4094 end_time: None,
4095 limit: Some(1001),
4096 },
4097 "Validation error: aggregate trade limit must not exceed 1000"
4098 )]
4099 #[case::order(
4100 BinanceAggTradesParams {
4101 symbol: "BTCUSDT".to_string(),
4102 from_id: None,
4103 start_time: Some(2000),
4104 end_time: Some(1000),
4105 limit: Some(1000),
4106 },
4107 "Validation error: aggregate trade startTime must not exceed endTime"
4108 )]
4109 #[case::range(
4110 BinanceAggTradesParams {
4111 symbol: "BTCUSDT".to_string(),
4112 from_id: None,
4113 start_time: Some(1000),
4114 end_time: Some(3_601_000),
4115 limit: Some(1000),
4116 },
4117 "Validation error: aggregate trade time range must be less than one hour"
4118 )]
4119 #[case::overflow(
4120 BinanceAggTradesParams {
4121 symbol: "BTCUSDT".to_string(),
4122 from_id: None,
4123 start_time: Some(i64::MIN),
4124 end_time: Some(i64::MAX),
4125 limit: Some(1000),
4126 },
4127 "Validation error: aggregate trade time range must be less than one hour"
4128 )]
4129 #[tokio::test]
4130 async fn test_agg_trades_rejects_invalid_bounds(
4131 #[case] params: BinanceAggTradesParams,
4132 #[case] expected: &str,
4133 ) {
4134 let error = create_test_raw_client()
4135 .agg_trades(¶ms)
4136 .await
4137 .unwrap_err();
4138
4139 assert_eq!(error.to_string(), expected);
4140 }
4141
4142 #[rstest]
4143 #[case::start(true, false)]
4144 #[case::end(false, true)]
4145 #[case::both(true, true)]
4146 #[tokio::test]
4147 async fn test_request_agg_trades_rejects_history_older_than_24_hours(
4148 #[case] include_start: bool,
4149 #[case] include_end: bool,
4150 ) {
4151 let client = create_test_client();
4152 let start = Timestamp::now() - jiff::SignedDuration::from_hours(25);
4153 let end = start + jiff::SignedDuration::from_mins(30);
4154
4155 let error = client
4156 .request_agg_trades(
4157 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
4158 include_start.then_some(start),
4159 include_end.then_some(end),
4160 Some(1000),
4161 )
4162 .await
4163 .unwrap_err();
4164
4165 assert_eq!(
4166 error.to_string(),
4167 "Binance Futures aggregate trade history is limited to the past 24 hours"
4168 );
4169 }
4170
4171 fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
4172 BinanceRawFuturesHttpClient::new(
4173 BinanceProductType::UsdM,
4174 BinanceEnvironment::Live,
4175 None,
4176 None,
4177 None,
4178 None,
4179 None,
4180 None,
4181 )
4182 .expect("Failed to create test client")
4183 }
4184
4185 fn create_test_client() -> BinanceFuturesHttpClient {
4186 BinanceFuturesHttpClient::new(
4187 BinanceProductType::UsdM,
4188 BinanceEnvironment::Live,
4189 get_atomic_clock_realtime(),
4190 None,
4191 None,
4192 Some("http://127.0.0.1:1".to_string()),
4193 None,
4194 Some(1),
4195 None,
4196 false,
4197 )
4198 .expect("Failed to create test client")
4199 }
4200
4201 fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
4202 BinanceFuturesUsdSymbol {
4203 symbol: Ustr::from("BTCUSDT"),
4204 pair: Ustr::from("BTCUSDT"),
4205 contract_type: "PERPETUAL".to_string(),
4206 delivery_date: 4_133_404_800_000,
4207 onboard_date: 1_569_398_400_000,
4208 status: BinanceTradingStatus::Trading,
4209 maint_margin_percent: "2.5000".to_string(),
4210 required_margin_percent: "5.0000".to_string(),
4211 base_asset: Ustr::from("BTC"),
4212 quote_asset: Ustr::from("USDT"),
4213 margin_asset: Ustr::from("USDT"),
4214 price_precision: 2,
4215 quantity_precision: 3,
4216 base_asset_precision: 8,
4217 quote_precision: 8,
4218 underlying_type: None,
4219 underlying_sub_type: Vec::new(),
4220 settle_plan: None,
4221 trigger_protect: None,
4222 liquidation_fee: None,
4223 market_take_bound: None,
4224 order_types: Vec::new(),
4225 time_in_force: Vec::new(),
4226 filters: Vec::new(),
4227 }
4228 }
4229
4230 #[rstest]
4231 fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
4232 let client = create_test_client();
4233 client.instruments_cache().insert(
4234 Ustr::from("BTCUSDT"),
4235 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4236 );
4237
4238 let (symbol, price_precision, size_precision) = client
4239 .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
4240 .unwrap();
4241
4242 assert_eq!(symbol, "BTCUSDT");
4243 assert_eq!(price_precision, 2);
4244 assert_eq!(size_precision, 3);
4245 }
4246
4247 #[rstest]
4248 fn test_cached_precisions_by_id_rejects_spot_alias() {
4249 let client = create_test_client();
4250 client.instruments_cache().insert(
4251 Ustr::from("BTCUSDT"),
4252 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4253 );
4254
4255 let error = client
4256 .cached_precisions_by_id(InstrumentId::from("BTCUSDT.BINANCE"))
4257 .unwrap_err();
4258
4259 assert_eq!(
4260 error.to_string(),
4261 InstrumentLookupError::not_found(InstrumentId::from("BTCUSDT.BINANCE")).to_string()
4262 );
4263 }
4264
4265 #[rstest]
4266 fn test_invalid_precision_preserves_previous_raw_snapshot() {
4267 let client = create_test_client();
4268 let valid = test_usdm_symbol();
4269 client
4270 .replace_instruments(vec![(
4271 valid.symbol,
4272 BinanceFuturesInstrument::UsdM(valid.clone()),
4273 )])
4274 .unwrap();
4275 let mut invalid = valid;
4276 invalid.price_precision = i32::from(FIXED_PRECISION) + 1;
4277
4278 let error = client
4279 .replace_instruments(vec![(
4280 invalid.symbol,
4281 BinanceFuturesInstrument::UsdM(invalid),
4282 )])
4283 .unwrap_err();
4284 let retained = client
4285 .instruments_cache()
4286 .get(&Ustr::from("BTCUSDT"))
4287 .map(|instrument| instrument.value().clone())
4288 .unwrap();
4289
4290 assert!(error.to_string().contains("precision exceeds maximum"));
4291 assert_eq!(retained.precisions().unwrap(), (2, 3));
4292 }
4293
4294 #[rstest]
4295 #[tokio::test]
4296 async fn test_submit_algo_order_stop_market_requires_trigger_price() {
4297 let client = create_test_client();
4298 client.instruments_cache().insert(
4299 Ustr::from("BTCUSDT"),
4300 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4301 );
4302
4303 let result = client
4304 .submit_algo_order(
4305 AccountId::from("BINANCE-001"),
4306 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
4307 ClientOrderId::new("missing-trigger-test-001"),
4308 OrderSide::Sell,
4309 OrderType::StopMarket,
4310 Quantity::from("0.001"),
4311 TimeInForce::Gtc,
4312 None,
4313 None,
4314 false,
4315 false,
4316 None,
4317 None,
4318 None,
4319 None,
4320 None,
4321 )
4322 .await;
4323
4324 let error = result.unwrap_err().to_string();
4325 assert_eq!(error, "Algo order type StopMarket requires a trigger price");
4326 }
4327
4328 #[rstest]
4329 fn test_batch_cancel_params_builds_order_id_list() {
4330 let items = vec![
4331 BatchCancelItem::by_order_id("BTCUSDT", 123),
4332 BatchCancelItem::by_order_id("BTCUSDT", 456),
4333 ];
4334
4335 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4336
4337 assert_eq!(params.symbol, "BTCUSDT");
4338 assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
4339 assert_eq!(params.orig_client_order_id_list, None);
4340 }
4341
4342 #[rstest]
4343 fn test_batch_cancel_params_builds_client_order_id_list() {
4344 let items = vec![
4345 BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
4346 BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
4347 ];
4348
4349 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4350
4351 assert_eq!(params.symbol, "BTCUSDT");
4352 assert_eq!(params.order_id_list, None);
4353 assert_eq!(
4354 params.orig_client_order_id_list.as_deref(),
4355 Some("[\"first-order\",\"second-order\"]"),
4356 );
4357 }
4358
4359 #[rstest]
4360 fn test_batch_cancel_params_rejects_mixed_symbols() {
4361 let items = vec![
4362 BatchCancelItem::by_order_id("BTCUSDT", 123),
4363 BatchCancelItem::by_order_id("ETHUSDT", 456),
4364 ];
4365
4366 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4367
4368 assert_validation_error(result, "same symbol");
4369 }
4370
4371 #[rstest]
4372 fn test_batch_cancel_params_rejects_mixed_id_types() {
4373 let items = vec![
4374 BatchCancelItem::by_order_id("BTCUSDT", 123),
4375 BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
4376 ];
4377
4378 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4379
4380 assert_validation_error(result, "not both");
4381 }
4382
4383 #[rstest]
4384 fn test_batch_cancel_params_rejects_items_without_ids() {
4385 let items = vec![BatchCancelItem {
4386 symbol: "BTCUSDT".to_string(),
4387 order_id: None,
4388 orig_client_order_id: None,
4389 }];
4390
4391 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4392
4393 assert_validation_error(result, "at least one order ID or client order ID");
4394 }
4395
4396 #[rstest]
4397 #[tokio::test]
4398 async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
4399 let client = create_test_raw_client();
4400 let items = (0..11)
4401 .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
4402 .collect::<Vec<_>>();
4403
4404 let result = client.batch_cancel_orders(&items).await;
4405
4406 match result {
4407 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4408 assert!(message.contains("10 orders maximum"));
4409 }
4410 other => panic!("Expected ValidationError, was {other:?}"),
4411 }
4412 }
4413
4414 #[rstest]
4415 fn test_batch_response_length_mismatch_has_unknown_outcome() {
4416 let error = merge_batch_results(vec![None], Vec::new()).unwrap_err();
4417
4418 assert_eq!(
4419 classify_futures_http_failure(&error),
4420 CommandFailure::Ambiguous(
4421 "JSON error: Batch response length does not match submitted orders".to_string()
4422 ),
4423 );
4424 }
4425
4426 fn assert_validation_error(
4427 result: BinanceFuturesHttpResult<BatchCancelParams>,
4428 expected_message: &str,
4429 ) {
4430 match result {
4431 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4432 assert!(message.contains(expected_message));
4433 }
4434 other => panic!("Expected ValidationError, was {other:?}"),
4435 }
4436 }
4437
4438 #[rstest]
4439 fn test_parse_error_response_binance_error() {
4440 let client = create_test_raw_client();
4441 let response = HttpResponse {
4442 status: HttpStatus::new(StatusCode::BAD_REQUEST),
4443 headers: HashMap::new(),
4444 body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
4445 };
4446
4447 let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
4448
4449 match result {
4450 Err(BinanceFuturesHttpError::BinanceError {
4451 code,
4452 message,
4453 status,
4454 retry_after,
4455 }) => {
4456 assert_eq!(code, -1121);
4457 assert_eq!(message, "Invalid symbol.");
4458 assert_eq!(status, 400);
4459 assert_eq!(retry_after, None);
4460 }
4461 other => panic!("Expected BinanceError, was {other:?}"),
4462 }
4463 }
4464}