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