Skip to main content

nautilus_binance/futures/http/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance Futures HTTP client for USD-M and COIN-M markets.
17
18use std::{
19    collections::HashMap,
20    num::NonZeroU32,
21    sync::{Arc, LazyLock, Weak},
22    time::Duration,
23};
24
25use ahash::{AHashMap, AHashSet};
26use aws_lc_rs::digest;
27use dashmap::DashMap;
28use jiff::Timestamp;
29use nautilus_common::cache::InstrumentLookupError;
30use nautilus_core::{AtomicMap, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime};
31use nautilus_model::{
32    data::{Bar, BarType, BookOrder, FundingRateUpdate, TradeTick},
33    enums::{
34        AggregationSource, AggressorSide, BarAggregation, BookType, MarketStatusAction, OrderSide,
35        OrderType, TimeInForce,
36    },
37    events::AccountState,
38    identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
39    instruments::{Instrument, any::InstrumentAny},
40    orderbook::OrderBook,
41    reports::{FillReport, OrderStatusReport},
42    types::{Currency, Price, Quantity, fixed::FIXED_PRECISION},
43};
44use nautilus_network::{
45    http::{
46        HttpClient, HttpRedirectPolicy, HttpResponse, Method, create_standard_nautilus_headers,
47    },
48    ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota},
49    retry::{RetryConfig, RetryError, RetryManager},
50};
51use parking_lot::Mutex;
52use rust_decimal::Decimal;
53use serde::{Deserialize, Serialize, de::DeserializeOwned};
54use ustr::Ustr;
55
56use super::{
57    error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
58    models::{
59        BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
60        BinanceFuturesAccountInfo, BinanceFuturesAggTrade, BinanceFuturesAlgoOrder,
61        BinanceFuturesAlgoOrderCancelResponse, BinanceFuturesCoinExchangeInfo,
62        BinanceFuturesCoinSymbol, BinanceFuturesCommissionRate, BinanceFuturesKline,
63        BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
64        BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
65        BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest,
66        BinanceOpenInterestHistRecord, BinanceOrderBook, BinancePositionRisk, BinancePriceTicker,
67        BinanceServerTime, BinanceUserTrade, ListenKeyResponse,
68    },
69    query::{
70        BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAggTradesParams,
71        BinanceAlgoOrderQueryParams, BinanceAllAlgoOrdersParams, BinanceAllOrdersParams,
72        BinanceBookTickerParams, BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams,
73        BinanceCancelOrderParams, BinanceCommissionRateParams, BinanceDepthParams,
74        BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
75        BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
76        BinanceOpenAlgoOrdersParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams,
77        BinanceOpenOrdersParams, BinanceOrderQueryParams, BinancePositionRiskParams,
78        BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceTicker24hrParams,
79        BinanceTradesParams, BinanceUserTradesParams, ListenKeyParams,
80    },
81};
82use crate::{
83    common::{
84        bar::BinanceBar,
85        consts::{
86            BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
87            BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID,
88            BINANCE_RETRY_AFTER_HEADER, BinanceRateLimitQuota,
89        },
90        credential::SigningCredential,
91        encoder::encode_broker_id,
92        enums::{
93            BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
94            BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
95            BinanceSide, BinanceTimeInForce, BinanceWorkingType,
96        },
97        fees::futures_fee_tier_rates,
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/// A Binance Futures Algo Service order with optional matching-engine details.
152#[derive(Debug)]
153pub struct BinanceFuturesAlgoOrderQueryResult {
154    /// The original conditional order from the Algo Service.
155    pub algo: BinanceFuturesAlgoOrder,
156    /// The matching-engine order created after the condition triggered, when enrichment succeeds.
157    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/// Raw HTTP client for Binance Futures REST API.
171#[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    /// Returns a reference to the underlying HTTP client.
184    #[must_use]
185    pub fn http_client(&self) -> &HttpClient {
186        &self.client
187    }
188
189    /// Returns whether this client has complete signing credentials.
190    #[must_use]
191    pub const fn has_credentials(&self) -> bool {
192        self.credential.is_some()
193    }
194
195    /// Creates a new Binance raw futures HTTP client.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if credentials are incomplete or the HTTP client fails to build.
200    #[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    /// Performs a GET request and deserializes the response body.
357    ///
358    /// # Errors
359    ///
360    /// Returns an error if the request fails or response deserialization fails.
361    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    /// Performs a POST request with optional body and signed query.
377    ///
378    /// # Errors
379    ///
380    /// Returns an error if the request fails or response deserialization fails.
381    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    /// Performs a PUT request with signed query.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if the request fails or response deserialization fails.
402    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    /// Performs a DELETE request with signed query.
418    ///
419    /// # Errors
420    ///
421    /// Returns an error if the request fails or response deserialization fails.
422    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    /// Performs a batch POST request with batchOrders parameter.
438    ///
439    /// # Errors
440    ///
441    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
442    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    /// Performs a batch DELETE request with batchOrders parameter.
453    ///
454    /// # Errors
455    ///
456    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
457    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    /// Performs a batch PUT request with batchOrders parameter.
468    ///
469    /// # Errors
470    ///
471    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
472    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}&timestamp={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_with_url_redacted(
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    /// Percent-encodes a string for use in URL query parameters.
540    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            // Percent-encode the signature: Ed25519 signatures are base64 and
647            // contain `+`, `/`, `=` which are not URL-safe. HMAC hex is
648            // already safe but percent-encoding it is a no-op.
649            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_with_url_redacted(
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        // Full API paths (e.g., /fapi/v2/account) bypass the default api_path
683        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, // Default to USD-M
753        }
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    /// Fetches 24hr ticker statistics.
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if the request fails.
827    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    /// Fetches best bid/ask prices.
835    ///
836    /// # Errors
837    ///
838    /// Returns an error if the request fails.
839    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    /// Fetches price ticker.
848    ///
849    /// # Errors
850    ///
851    /// Returns an error if the request fails.
852    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    /// Fetches order book depth.
866    ///
867    /// # Errors
868    ///
869    /// Returns an error if the request fails.
870    pub async fn depth(
871        &self,
872        params: &BinanceDepthParams,
873    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
874        self.get("depth", Some(params), false, false).await
875    }
876
877    /// Fetches mark price and funding rate.
878    ///
879    /// # Errors
880    ///
881    /// Returns an error if the request fails.
882    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    /// Fetches funding rate history.
892    ///
893    /// # Errors
894    ///
895    /// Returns an error if the request fails.
896    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    /// Fetches current open interest for a symbol.
904    ///
905    /// # Errors
906    ///
907    /// Returns an error if the request fails.
908    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    /// Fetches historical open interest statistics for a symbol or pair.
916    ///
917    /// # Errors
918    ///
919    /// Returns an error if the request fails.
920    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    /// Fetches recent public trades for a symbol.
929    ///
930    /// # Errors
931    ///
932    /// Returns an error if the request fails.
933    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    /// Fetches aggregate public trades for a symbol.
941    ///
942    /// # Errors
943    ///
944    /// Returns an error if documented Binance Futures bounds are violated or the request fails.
945    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    /// Fetches kline/candlestick data for a symbol.
978    ///
979    /// # Errors
980    ///
981    /// Returns an error if the request fails.
982    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    /// Sets leverage for a symbol.
990    ///
991    /// # Errors
992    ///
993    /// Returns an error if the request fails.
994    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    /// Sets margin type for a symbol.
1002    ///
1003    /// # Errors
1004    ///
1005    /// Returns an error if the request fails.
1006    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    /// Queries hedge mode (dual side position) setting.
1015    ///
1016    /// # Errors
1017    ///
1018    /// Returns an error if the request fails.
1019    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1020        self.get::<(), _>("positionSide/dual", None, true, false)
1021            .await
1022    }
1023
1024    /// Creates a listen key for user data stream.
1025    ///
1026    /// # Errors
1027    ///
1028    /// Returns an error if the request fails.
1029    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1030        self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
1031            .await
1032    }
1033
1034    /// Keeps alive an existing listen key.
1035    ///
1036    /// # Errors
1037    ///
1038    /// Returns an error if the request fails.
1039    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(&params), true, false)
1045            .await?;
1046        Ok(())
1047    }
1048
1049    /// Closes an existing listen key.
1050    ///
1051    /// # Errors
1052    ///
1053    /// Returns an error if the request fails.
1054    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1055        let params = ListenKeyParams {
1056            listen_key: listen_key.into(),
1057        };
1058        let _: serde_json::Value = self
1059            .request_delete("listenKey", Some(&params), true, false)
1060            .await?;
1061        Ok(())
1062    }
1063
1064    /// Fetches account information including balances and positions.
1065    ///
1066    /// # Errors
1067    ///
1068    /// Returns an error if the request fails.
1069    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1070        // USD-M uses /fapi/v2/account, COIN-M uses /dapi/v1/account
1071        let path = if self.api_path.starts_with("/fapi") {
1072            "/fapi/v2/account"
1073        } else {
1074            "/dapi/v1/account"
1075        };
1076        self.get::<(), _>(path, None, true, false).await
1077    }
1078
1079    /// Fetches account-specific maker and taker commission rates for one symbol.
1080    ///
1081    /// # Errors
1082    ///
1083    /// Returns an error if credentials are missing or the request fails.
1084    pub async fn commission_rate(
1085        &self,
1086        params: &BinanceCommissionRateParams,
1087    ) -> BinanceFuturesHttpResult<BinanceFuturesCommissionRate> {
1088        self.get("commissionRate", Some(params), true, false).await
1089    }
1090
1091    /// Fetches position risk information.
1092    ///
1093    /// # Errors
1094    ///
1095    /// Returns an error if the request fails.
1096    pub async fn query_positions(
1097        &self,
1098        params: &BinancePositionRiskParams,
1099    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1100        // USD-M uses /fapi/v2/positionRisk, COIN-M uses /dapi/v1/positionRisk
1101        let path = if self.api_path.starts_with("/fapi") {
1102            "/fapi/v2/positionRisk"
1103        } else {
1104            "/dapi/v1/positionRisk"
1105        };
1106        self.get(path, Some(params), true, false).await
1107    }
1108
1109    /// Fetches user trades for a symbol.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns an error if the request fails.
1114    pub async fn query_user_trades(
1115        &self,
1116        params: &BinanceUserTradesParams,
1117    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1118        self.get("userTrades", Some(params), true, false).await
1119    }
1120
1121    /// Queries a single order by order ID or client order ID.
1122    ///
1123    /// # Errors
1124    ///
1125    /// Returns an error if the request fails.
1126    pub async fn query_order(
1127        &self,
1128        params: &BinanceOrderQueryParams,
1129    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1130        self.get("order", Some(params), true, false).await
1131    }
1132
1133    /// Queries all open orders.
1134    ///
1135    /// # Errors
1136    ///
1137    /// Returns an error if the request fails.
1138    pub async fn query_open_orders(
1139        &self,
1140        params: &BinanceOpenOrdersParams,
1141    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1142        self.get("openOrders", Some(params), true, false).await
1143    }
1144
1145    /// Queries all orders (including historical).
1146    ///
1147    /// # Errors
1148    ///
1149    /// Returns an error if the request fails.
1150    pub async fn query_all_orders(
1151        &self,
1152        params: &BinanceAllOrdersParams,
1153    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1154        self.get("allOrders", Some(params), true, false).await
1155    }
1156
1157    /// Submits a new order.
1158    ///
1159    /// # Errors
1160    ///
1161    /// Returns an error if the request fails.
1162    pub async fn submit_order(
1163        &self,
1164        params: &BinanceNewOrderParams,
1165    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1166        self.post("order", Some(params), None, true, true).await
1167    }
1168
1169    /// Submits multiple orders in a single request (up to 5 orders).
1170    ///
1171    /// # Errors
1172    ///
1173    /// Returns an error if the batch exceeds 5 orders or the request fails.
1174    pub async fn submit_order_list(
1175        &self,
1176        orders: &[BatchOrderItem],
1177    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1178        if orders.is_empty() {
1179            return Ok(Vec::new());
1180        }
1181
1182        if orders.len() > 5 {
1183            return Err(BinanceFuturesHttpError::ValidationError(
1184                "Batch order limit is 5 orders maximum".to_string(),
1185            ));
1186        }
1187
1188        self.batch_request("batchOrders", orders, true).await
1189    }
1190
1191    /// Modifies an existing order (price and quantity only).
1192    ///
1193    /// # Errors
1194    ///
1195    /// Returns an error if the request fails.
1196    pub async fn modify_order(
1197        &self,
1198        params: &BinanceModifyOrderParams,
1199    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1200        self.request_put("order", Some(params), true, true).await
1201    }
1202
1203    /// Modifies multiple orders in a single request (up to 5 orders).
1204    ///
1205    /// # Errors
1206    ///
1207    /// Returns an error if the batch exceeds 5 orders or the request fails.
1208    pub async fn batch_modify_orders(
1209        &self,
1210        modifies: &[BatchModifyItem],
1211    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1212        if modifies.is_empty() {
1213            return Ok(Vec::new());
1214        }
1215
1216        if modifies.len() > 5 {
1217            return Err(BinanceFuturesHttpError::ValidationError(
1218                "Batch modify limit is 5 orders maximum".to_string(),
1219            ));
1220        }
1221
1222        self.batch_request_put("batchOrders", modifies, true).await
1223    }
1224
1225    /// Cancels an existing order.
1226    ///
1227    /// # Errors
1228    ///
1229    /// Returns an error if the request fails.
1230    pub async fn cancel_order(
1231        &self,
1232        params: &BinanceCancelOrderParams,
1233    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1234        self.request_delete("order", Some(params), true, true).await
1235    }
1236
1237    /// Cancels all open orders for a symbol.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Returns an error if the request fails.
1242    pub async fn cancel_all_orders(
1243        &self,
1244        params: &BinanceCancelAllOrdersParams,
1245    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1246        self.request_delete("allOpenOrders", Some(params), true, true)
1247            .await
1248    }
1249
1250    /// Cancels multiple orders in a single request (up to 10 orders).
1251    ///
1252    /// # Errors
1253    ///
1254    /// Returns an error if the batch exceeds 10 orders or the request fails.
1255    pub async fn batch_cancel_orders(
1256        &self,
1257        cancels: &[BatchCancelItem],
1258    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1259        if cancels.is_empty() {
1260            return Ok(Vec::new());
1261        }
1262
1263        if cancels.len() > 10 {
1264            return Err(BinanceFuturesHttpError::ValidationError(
1265                "Batch cancel limit is 10 orders maximum".to_string(),
1266            ));
1267        }
1268
1269        let params = Self::batch_cancel_params(cancels)?;
1270        self.request_delete("batchOrders", Some(&params), true, true)
1271            .await
1272    }
1273
1274    fn batch_cancel_params(
1275        cancels: &[BatchCancelItem],
1276    ) -> BinanceFuturesHttpResult<BatchCancelParams> {
1277        let symbol = cancels[0].symbol.clone();
1278        let mut order_ids = Vec::new();
1279        let mut client_order_ids = Vec::new();
1280
1281        for cancel in cancels {
1282            if cancel.symbol != symbol {
1283                return Err(BinanceFuturesHttpError::ValidationError(
1284                    "Batch cancel orders must use the same symbol".to_string(),
1285                ));
1286            }
1287
1288            if let Some(order_id) = cancel.order_id {
1289                order_ids.push(order_id);
1290            }
1291
1292            if let Some(client_order_id) = &cancel.orig_client_order_id {
1293                client_order_ids.push(client_order_id.clone());
1294            }
1295        }
1296
1297        if order_ids.is_empty() && client_order_ids.is_empty() {
1298            return Err(BinanceFuturesHttpError::ValidationError(
1299                "Batch cancel requires at least one order ID or client order ID".to_string(),
1300            ));
1301        }
1302
1303        if !order_ids.is_empty() && !client_order_ids.is_empty() {
1304            return Err(BinanceFuturesHttpError::ValidationError(
1305                "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1306            ));
1307        }
1308
1309        let order_id_list = if order_ids.is_empty() {
1310            None
1311        } else {
1312            Some(
1313                serde_json::to_string(&order_ids)
1314                    .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1315            )
1316        };
1317        let orig_client_order_id_list = if client_order_ids.is_empty() {
1318            None
1319        } else {
1320            Some(
1321                serde_json::to_string(&client_order_ids)
1322                    .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1323            )
1324        };
1325
1326        Ok(BatchCancelParams {
1327            symbol,
1328            order_id_list,
1329            orig_client_order_id_list,
1330        })
1331    }
1332
1333    /// Submits a new algo order (conditional order).
1334    ///
1335    /// Algo orders include STOP_MARKET, STOP (stop-limit), TAKE_PROFIT, TAKE_PROFIT_MARKET,
1336    /// and TRAILING_STOP_MARKET order types.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns an error if the request fails.
1341    pub async fn submit_algo_order(
1342        &self,
1343        params: &BinanceNewAlgoOrderParams,
1344    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1345        self.post("algoOrder", Some(params), None, true, true).await
1346    }
1347
1348    /// Cancels an algo order.
1349    ///
1350    /// Must provide either `algo_id` or `client_algo_id`.
1351    ///
1352    /// # Errors
1353    ///
1354    /// Returns an error if the request fails.
1355    pub async fn cancel_algo_order(
1356        &self,
1357        params: &BinanceAlgoOrderQueryParams,
1358    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1359        self.request_delete("algoOrder", Some(params), true, true)
1360            .await
1361    }
1362
1363    /// Queries a single algo order.
1364    ///
1365    /// Must provide either `algo_id` or `client_algo_id`.
1366    ///
1367    /// # Errors
1368    ///
1369    /// Returns an error if the request fails.
1370    pub async fn query_algo_order(
1371        &self,
1372        params: &BinanceAlgoOrderQueryParams,
1373    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1374        self.get("algoOrder", Some(params), true, false).await
1375    }
1376
1377    /// Queries all open algo orders.
1378    ///
1379    /// # Errors
1380    ///
1381    /// Returns an error if the request fails.
1382    pub async fn query_open_algo_orders(
1383        &self,
1384        params: &BinanceOpenAlgoOrdersParams,
1385    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1386        self.get("openAlgoOrders", Some(params), true, false).await
1387    }
1388
1389    /// Queries all algo orders including historical (7-day limit).
1390    ///
1391    /// # Errors
1392    ///
1393    /// Returns an error if the request fails.
1394    pub async fn query_all_algo_orders(
1395        &self,
1396        params: &BinanceAllAlgoOrdersParams,
1397    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1398        self.get("allAlgoOrders", Some(params), true, false).await
1399    }
1400
1401    /// Cancels all open algo orders for a symbol.
1402    ///
1403    /// # Errors
1404    ///
1405    /// Returns an error if the request fails.
1406    pub async fn cancel_all_algo_orders(
1407        &self,
1408        params: &BinanceCancelAllAlgoOrdersParams,
1409    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1410        self.request_delete("algoOpenOrders", Some(params), true, true)
1411            .await
1412    }
1413}
1414
1415/// Response wrapper for mark price endpoint.
1416#[derive(Debug, Deserialize)]
1417#[serde(untagged)]
1418enum MarkPriceResponse {
1419    Single(BinanceFuturesMarkPrice),
1420    Multiple(Vec<BinanceFuturesMarkPrice>),
1421}
1422
1423impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1424    fn from(response: MarkPriceResponse) -> Self {
1425        match response {
1426            MarkPriceResponse::Single(price) => vec![price],
1427            MarkPriceResponse::Multiple(prices) => prices,
1428        }
1429    }
1430}
1431
1432struct RateLimitConfig {
1433    request_quota: Quota,
1434    order_quotas: Vec<(String, Quota)>,
1435    order_keys: Vec<String>,
1436}
1437
1438/// In-memory cache entry for Binance Futures instruments.
1439#[derive(Clone, Debug)]
1440pub enum BinanceFuturesInstrument {
1441    /// USD-M futures symbol.
1442    UsdM(BinanceFuturesUsdSymbol),
1443    /// COIN-M futures symbol.
1444    CoinM(BinanceFuturesCoinSymbol),
1445}
1446
1447impl BinanceFuturesInstrument {
1448    /// Returns the symbol name for the instrument.
1449    #[must_use]
1450    pub const fn symbol(&self) -> Ustr {
1451        match self {
1452            Self::UsdM(s) => s.symbol,
1453            Self::CoinM(s) => s.symbol,
1454        }
1455    }
1456
1457    /// Returns the price precision for the instrument.
1458    #[must_use]
1459    pub const fn price_precision(&self) -> i32 {
1460        match self {
1461            Self::UsdM(s) => s.price_precision,
1462            Self::CoinM(s) => s.price_precision,
1463        }
1464    }
1465
1466    /// Returns the quantity precision for the instrument.
1467    #[must_use]
1468    pub const fn quantity_precision(&self) -> i32 {
1469        match self {
1470            Self::UsdM(s) => s.quantity_precision,
1471            Self::CoinM(s) => s.quantity_precision,
1472        }
1473    }
1474
1475    /// Returns validated price and quantity precision values.
1476    ///
1477    /// # Errors
1478    ///
1479    /// Returns an error if either venue precision is outside the domain type range.
1480    pub fn precisions(&self) -> BinanceFuturesHttpResult<(u8, u8)> {
1481        let price_precision = u8::try_from(self.price_precision()).map_err(|_| {
1482            BinanceFuturesHttpError::ValidationError(format!(
1483                "Invalid Binance Futures price precision {} for {}",
1484                self.price_precision(),
1485                self.symbol()
1486            ))
1487        })?;
1488        let quantity_precision = u8::try_from(self.quantity_precision()).map_err(|_| {
1489            BinanceFuturesHttpError::ValidationError(format!(
1490                "Invalid Binance Futures quantity precision {} for {}",
1491                self.quantity_precision(),
1492                self.symbol()
1493            ))
1494        })?;
1495
1496        if price_precision > FIXED_PRECISION || quantity_precision > FIXED_PRECISION {
1497            return Err(BinanceFuturesHttpError::ValidationError(format!(
1498                "Binance Futures precision exceeds maximum {FIXED_PRECISION} for {}: price={price_precision}, quantity={quantity_precision}",
1499                self.symbol()
1500            )));
1501        }
1502
1503        Ok((price_precision, quantity_precision))
1504    }
1505
1506    /// Returns the Nautilus-formatted instrument ID.
1507    #[must_use]
1508    pub fn id(&self) -> InstrumentId {
1509        match self {
1510            Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1511            Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1512        }
1513    }
1514
1515    /// Returns the quote currency for the instrument.
1516    #[must_use]
1517    pub fn quote_currency(&self) -> Currency {
1518        let quote_asset = match self {
1519            Self::UsdM(s) => &s.quote_asset,
1520            Self::CoinM(s) => &s.quote_asset,
1521        };
1522        Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1523    }
1524}
1525
1526/// Binance Futures HTTP client for USD-M and COIN-M perpetuals and delivery contracts.
1527#[derive(Debug, Clone)]
1528pub struct BinanceFuturesHttpClient {
1529    inner: Arc<BinanceRawFuturesHttpClient>,
1530    product_type: BinanceProductType,
1531    clock: &'static AtomicTime,
1532    instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1533    instruments_reconciliation: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
1534    instruments_load_lock: Arc<tokio::sync::Mutex<()>>,
1535    treat_expired_as_canceled: bool,
1536}
1537
1538impl BinanceFuturesHttpClient {
1539    /// Creates a new [`BinanceFuturesHttpClient`] instance.
1540    ///
1541    /// # Errors
1542    ///
1543    /// Returns an error if the product type is invalid or HTTP client creation fails.
1544    #[expect(clippy::too_many_arguments)]
1545    pub fn new(
1546        product_type: BinanceProductType,
1547        environment: BinanceEnvironment,
1548        clock: &'static AtomicTime,
1549        api_key: Option<String>,
1550        api_secret: Option<String>,
1551        base_url_override: Option<String>,
1552        recv_window: Option<u64>,
1553        timeout_secs: Option<u64>,
1554        proxy_url: Option<String>,
1555        treat_expired_as_canceled: bool,
1556    ) -> BinanceFuturesHttpResult<Self> {
1557        match product_type {
1558            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1559            _ => {
1560                return Err(BinanceFuturesHttpError::ValidationError(format!(
1561                    "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1562                )));
1563            }
1564        }
1565
1566        let raw = BinanceRawFuturesHttpClient::new(
1567            product_type,
1568            environment,
1569            api_key,
1570            api_secret,
1571            base_url_override,
1572            recv_window,
1573            timeout_secs,
1574            proxy_url,
1575        )?;
1576
1577        Ok(Self {
1578            inner: Arc::new(raw),
1579            product_type,
1580            clock,
1581            instruments: Arc::new(DashMap::new()),
1582            instruments_reconciliation: Arc::new(AtomicMap::new()),
1583            instruments_load_lock: Arc::new(tokio::sync::Mutex::new(())),
1584            treat_expired_as_canceled,
1585        })
1586    }
1587
1588    pub(crate) fn with_retry_config(mut self, config: RetryConfig) -> Self {
1589        Arc::make_mut(&mut self.inner).retry_manager = Arc::new(RetryManager::new(config));
1590        self
1591    }
1592
1593    /// Returns the product type (UsdM or CoinM).
1594    #[must_use]
1595    pub const fn product_type(&self) -> BinanceProductType {
1596        self.product_type
1597    }
1598
1599    /// Returns a reference to the inner raw HTTP client.
1600    #[must_use]
1601    pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1602        &self.inner
1603    }
1604
1605    /// Returns a clone of the instruments cache Arc.
1606    #[must_use]
1607    pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1608        Arc::clone(&self.instruments)
1609    }
1610
1611    /// Returns the execution-owned parsed instrument for an exact ID.
1612    pub(crate) fn instrument_reconciliation(
1613        &self,
1614        instrument_id: &InstrumentId,
1615    ) -> Option<InstrumentAny> {
1616        self.instruments_reconciliation.get_cloned(instrument_id)
1617    }
1618
1619    /// Returns whether this client has complete signing credentials.
1620    #[must_use]
1621    pub fn has_credentials(&self) -> bool {
1622        self.inner.has_credentials()
1623    }
1624
1625    /// Replaces the raw instrument metadata after a complete catalog fetch.
1626    fn replace_instruments(
1627        &self,
1628        instruments: Vec<(Ustr, BinanceFuturesInstrument)>,
1629    ) -> BinanceFuturesHttpResult<()> {
1630        let mut snapshot = AHashMap::with_capacity(instruments.len());
1631        for (symbol, instrument) in instruments {
1632            instrument.precisions()?;
1633            if instrument.symbol() != symbol {
1634                return Err(BinanceFuturesHttpError::ValidationError(format!(
1635                    "Binance Futures catalog key {symbol} does not match instrument symbol {}",
1636                    instrument.symbol()
1637                )));
1638            }
1639            let expected_id = format_instrument_id(&symbol, self.product_type);
1640            if instrument.id() != expected_id {
1641                return Err(BinanceFuturesHttpError::ValidationError(format!(
1642                    "Binance Futures catalog instrument ID {} does not match expected ID {expected_id}",
1643                    instrument.id()
1644                )));
1645            }
1646
1647            if snapshot.insert(symbol, instrument).is_some() {
1648                return Err(BinanceFuturesHttpError::ValidationError(format!(
1649                    "Duplicate Binance Futures catalog symbol {symbol}"
1650                )));
1651            }
1652        }
1653
1654        let symbols: AHashSet<_> = snapshot.keys().copied().collect();
1655        for (symbol, instrument) in snapshot {
1656            self.instruments.insert(symbol, instrument);
1657        }
1658        self.instruments
1659            .retain(|symbol, _| symbols.contains(symbol));
1660        Ok(())
1661    }
1662
1663    /// Returns server time.
1664    ///
1665    /// # Errors
1666    ///
1667    /// Returns an error if the request fails.
1668    pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1669        self.inner
1670            .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1671            .await
1672    }
1673
1674    /// Sets leverage for a symbol.
1675    ///
1676    /// # Errors
1677    ///
1678    /// Returns an error if the request fails.
1679    pub async fn set_leverage(
1680        &self,
1681        params: &BinanceSetLeverageParams,
1682    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1683        self.inner.set_leverage(params).await
1684    }
1685
1686    /// Sets margin type for a symbol.
1687    ///
1688    /// # Errors
1689    ///
1690    /// Returns an error if the request fails.
1691    pub async fn set_margin_type(
1692        &self,
1693        params: &BinanceSetMarginTypeParams,
1694    ) -> BinanceFuturesHttpResult<serde_json::Value> {
1695        self.inner.set_margin_type(params).await
1696    }
1697
1698    /// Queries hedge mode (dual side position) setting.
1699    ///
1700    /// # Errors
1701    ///
1702    /// Returns an error if the request fails.
1703    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1704        self.inner.query_hedge_mode().await
1705    }
1706
1707    /// Creates a listen key for user data stream.
1708    ///
1709    /// # Errors
1710    ///
1711    /// Returns an error if the request fails.
1712    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1713        self.inner.create_listen_key().await
1714    }
1715
1716    /// Keeps alive an existing listen key.
1717    ///
1718    /// # Errors
1719    ///
1720    /// Returns an error if the request fails.
1721    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1722        self.inner.keepalive_listen_key(listen_key).await
1723    }
1724
1725    /// Closes an existing listen key.
1726    ///
1727    /// # Errors
1728    ///
1729    /// Returns an error if the request fails.
1730    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1731        self.inner.close_listen_key(listen_key).await
1732    }
1733
1734    /// Fetches exchange information and populates the instrument cache.
1735    ///
1736    /// # Errors
1737    ///
1738    /// Returns an error if the request fails or the product type is invalid.
1739    pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1740        let _guard = self.instruments_load_lock.lock().await;
1741        let instruments = match self.product_type {
1742            BinanceProductType::UsdM => {
1743                let info: BinanceFuturesUsdExchangeInfo = self
1744                    .inner
1745                    .get("exchangeInfo", None::<&()>, false, false)
1746                    .await?;
1747
1748                info.symbols
1749                    .into_iter()
1750                    .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::UsdM(symbol)))
1751                    .collect()
1752            }
1753            BinanceProductType::CoinM => {
1754                let info: BinanceFuturesCoinExchangeInfo = self
1755                    .inner
1756                    .get("exchangeInfo", None::<&()>, false, false)
1757                    .await?;
1758
1759                info.symbols
1760                    .into_iter()
1761                    .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::CoinM(symbol)))
1762                    .collect()
1763            }
1764            _ => {
1765                return Err(BinanceFuturesHttpError::ValidationError(
1766                    "Invalid product type for futures".to_string(),
1767                ));
1768            }
1769        };
1770
1771        self.replace_instruments(instruments)
1772    }
1773
1774    /// Fetches exchange info and returns the current status of each symbol.
1775    ///
1776    /// Builds a fresh status snapshot from the response without disturbing the
1777    /// shared instruments cache, so a transient failure does not break other
1778    /// HTTP operations that depend on cached precision data.
1779    ///
1780    /// # Errors
1781    ///
1782    /// Returns an error if the request fails or the product type is invalid.
1783    pub async fn request_symbol_statuses(
1784        &self,
1785    ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1786        let mut statuses = AHashMap::new();
1787
1788        match self.product_type {
1789            BinanceProductType::UsdM => {
1790                let info: BinanceFuturesUsdExchangeInfo = self
1791                    .inner
1792                    .get("exchangeInfo", None::<&()>, false, false)
1793                    .await?;
1794
1795                for symbol in &info.symbols {
1796                    statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1797                }
1798            }
1799            BinanceProductType::CoinM => {
1800                let info: BinanceFuturesCoinExchangeInfo = self
1801                    .inner
1802                    .get("exchangeInfo", None::<&()>, false, false)
1803                    .await?;
1804
1805                for symbol in &info.symbols {
1806                    let action = symbol
1807                        .contract_status
1808                        .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1809                    statuses.insert(symbol.symbol, action);
1810                }
1811            }
1812            _ => {
1813                return Err(BinanceFuturesHttpError::ValidationError(
1814                    "Invalid product type for futures".to_string(),
1815                ));
1816            }
1817        }
1818
1819        Ok(statuses)
1820    }
1821
1822    /// Fetches exchange information and returns parsed Nautilus instruments.
1823    ///
1824    /// # Errors
1825    ///
1826    /// Returns an error if the request fails or the product type is invalid.
1827    pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1828        self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default())
1829            .await
1830    }
1831
1832    /// Fetches, selects, and parses the configured instrument catalog.
1833    ///
1834    /// Non-trading symbols are skipped with a debug log unless explicitly
1835    /// selected via `load_ids` or the `symbols` filter.
1836    ///
1837    /// Account-wide Futures VIP rates provide the fallback when credentials are
1838    /// present. Exact per-symbol commission queries are opt-in.
1839    ///
1840    /// # Errors
1841    ///
1842    /// Returns an error if configuration or the catalog request is invalid.
1843    pub async fn request_instruments_with_config(
1844        &self,
1845        config: &BinanceInstrumentProviderConfig,
1846    ) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1847        let _guard = self.instruments_load_lock.lock().await;
1848        config
1849            .validate(self.product_type)
1850            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1851        let selector = BinanceInstrumentSelector::new(config)
1852            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1853        let ts_init = UnixNanos::default();
1854        let fallback_fees = self.futures_fallback_fees(config).await;
1855        let mut cache = Vec::new();
1856        let mut reconciliation = AHashMap::new();
1857
1858        let instruments = match self.product_type {
1859            BinanceProductType::UsdM => {
1860                let info: BinanceFuturesUsdExchangeInfo = self
1861                    .inner
1862                    .get("exchangeInfo", None::<&()>, false, false)
1863                    .await?;
1864
1865                let mut instruments = Vec::with_capacity(info.symbols.len());
1866
1867                for symbol in info.symbols {
1868                    let instrument_id =
1869                        format_instrument_id(&symbol.symbol, BinanceProductType::UsdM);
1870                    cache.push((
1871                        symbol.symbol,
1872                        BinanceFuturesInstrument::UsdM(symbol.clone()),
1873                    ));
1874
1875                    if !selector.includes(
1876                        instrument_id,
1877                        &symbol.symbol,
1878                        &symbol.base_asset,
1879                        &symbol.quote_asset,
1880                        Some(&symbol.contract_type),
1881                    ) {
1882                        continue;
1883                    }
1884
1885                    let fees = self
1886                        .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1887                        .await;
1888
1889                    match parse_usdm_instrument_with_fees(
1890                        &symbol,
1891                        Some(fees.0),
1892                        Some(fees.1),
1893                        ts_init,
1894                        ts_init,
1895                    ) {
1896                        Ok(instrument) => {
1897                            validate_reconciliation_instrument(
1898                                &mut reconciliation,
1899                                instrument_id,
1900                                &instrument,
1901                            )?;
1902                            instruments.push(instrument);
1903                        }
1904                        Err(e) => {
1905                            log_futures_instrument_parse_error(
1906                                config,
1907                                &selector,
1908                                instrument_id,
1909                                &symbol.symbol,
1910                                &e,
1911                            );
1912                        }
1913                    }
1914                }
1915
1916                log::debug!(
1917                    "Loaded USD-M Futures instruments: count={}",
1918                    instruments.len()
1919                );
1920                instruments
1921            }
1922            BinanceProductType::CoinM => {
1923                let info: BinanceFuturesCoinExchangeInfo = self
1924                    .inner
1925                    .get("exchangeInfo", None::<&()>, false, false)
1926                    .await?;
1927
1928                let mut instruments = Vec::with_capacity(info.symbols.len());
1929                for symbol in info.symbols {
1930                    let instrument_id =
1931                        format_instrument_id(&symbol.symbol, BinanceProductType::CoinM);
1932                    cache.push((
1933                        symbol.symbol,
1934                        BinanceFuturesInstrument::CoinM(symbol.clone()),
1935                    ));
1936
1937                    if !selector.includes(
1938                        instrument_id,
1939                        &symbol.symbol,
1940                        &symbol.base_asset,
1941                        &symbol.quote_asset,
1942                        Some(&symbol.contract_type),
1943                    ) {
1944                        continue;
1945                    }
1946
1947                    let fees = self
1948                        .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1949                        .await;
1950
1951                    match parse_coinm_instrument_with_fees(
1952                        &symbol,
1953                        Some(fees.0),
1954                        Some(fees.1),
1955                        ts_init,
1956                        ts_init,
1957                    ) {
1958                        Ok(instrument) => {
1959                            validate_reconciliation_instrument(
1960                                &mut reconciliation,
1961                                instrument_id,
1962                                &instrument,
1963                            )?;
1964                            instruments.push(instrument);
1965                        }
1966                        Err(e) => {
1967                            log_futures_instrument_parse_error(
1968                                config,
1969                                &selector,
1970                                instrument_id,
1971                                &symbol.symbol,
1972                                &e,
1973                            );
1974                        }
1975                    }
1976                }
1977
1978                log::debug!(
1979                    "Loaded COIN-M Futures instruments: count={}",
1980                    instruments.len()
1981                );
1982                instruments
1983            }
1984            _ => {
1985                return Err(BinanceFuturesHttpError::ValidationError(
1986                    "Invalid product type for futures".to_string(),
1987                ));
1988            }
1989        };
1990
1991        self.replace_instruments(cache)?;
1992        self.instruments_reconciliation.store(reconciliation);
1993        Ok(instruments)
1994    }
1995
1996    async fn futures_fallback_fees(
1997        &self,
1998        config: &BinanceInstrumentProviderConfig,
1999    ) -> (Decimal, Decimal) {
2000        if !self.has_credentials() {
2001            return futures_fee_tier_rates(0);
2002        }
2003
2004        match self.query_account().await {
2005            Ok(account) => futures_fee_tier_rates(account.fee_tier),
2006            Err(e) => {
2007                if config.log_warnings {
2008                    log::warn!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
2009                } else {
2010                    log::debug!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
2011                }
2012                futures_fee_tier_rates(0)
2013            }
2014        }
2015    }
2016
2017    async fn futures_symbol_fees(
2018        &self,
2019        config: &BinanceInstrumentProviderConfig,
2020        symbol: &str,
2021        fallback: (Decimal, Decimal),
2022    ) -> (Decimal, Decimal) {
2023        if !config.query_commission_rates || !self.has_credentials() {
2024            return fallback;
2025        }
2026
2027        let params = BinanceCommissionRateParams {
2028            symbol: symbol.to_string(),
2029        };
2030
2031        match self.inner.commission_rate(&params).await {
2032            Ok(response) => parse_futures_commission_rates(&response).unwrap_or_else(|e| {
2033                log_futures_commission_fallback(config, symbol, &e, fallback);
2034                fallback
2035            }),
2036            Err(e) => {
2037                log_futures_commission_fallback(config, symbol, &e, fallback);
2038                fallback
2039            }
2040        }
2041    }
2042
2043    /// Fetches 24hr ticker statistics.
2044    ///
2045    /// # Errors
2046    ///
2047    /// Returns an error if the request fails.
2048    pub async fn ticker_24h(
2049        &self,
2050        params: &BinanceTicker24hrParams,
2051    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
2052        self.inner.ticker_24h(params).await
2053    }
2054
2055    /// Fetches best bid/ask prices.
2056    ///
2057    /// # Errors
2058    ///
2059    /// Returns an error if the request fails.
2060    pub async fn book_ticker(
2061        &self,
2062        params: &BinanceBookTickerParams,
2063    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
2064        self.inner.book_ticker(params).await
2065    }
2066
2067    /// Fetches price ticker.
2068    ///
2069    /// # Errors
2070    ///
2071    /// Returns an error if the request fails.
2072    pub async fn price_ticker(
2073        &self,
2074        symbol: Option<&str>,
2075    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
2076        self.inner.price_ticker(symbol).await
2077    }
2078
2079    /// Fetches order book depth.
2080    ///
2081    /// # Errors
2082    ///
2083    /// Returns an error if the request fails.
2084    pub async fn depth(
2085        &self,
2086        params: &BinanceDepthParams,
2087    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
2088        self.inner.depth(params).await
2089    }
2090
2091    /// Fetches mark price and funding rate.
2092    ///
2093    /// # Errors
2094    ///
2095    /// Returns an error if the request fails.
2096    pub async fn mark_price(
2097        &self,
2098        params: &BinanceMarkPriceParams,
2099    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
2100        self.inner.mark_price(params).await
2101    }
2102
2103    /// Fetches funding rate history.
2104    ///
2105    /// # Errors
2106    ///
2107    /// Returns an error if the request fails.
2108    pub async fn funding_rate(
2109        &self,
2110        params: &BinanceFundingRateParams,
2111    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
2112        self.inner.funding_rate(params).await
2113    }
2114
2115    /// Fetches current open interest for a symbol.
2116    ///
2117    /// # Errors
2118    ///
2119    /// Returns an error if the request fails.
2120    pub async fn open_interest(
2121        &self,
2122        params: &BinanceOpenInterestParams,
2123    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
2124        self.inner.open_interest(params).await
2125    }
2126
2127    /// Fetches historical open interest statistics for a symbol or pair.
2128    ///
2129    /// # Errors
2130    ///
2131    /// Returns an error if the request fails.
2132    pub async fn open_interest_hist(
2133        &self,
2134        params: &BinanceOpenInterestHistParams,
2135    ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
2136        self.inner.open_interest_hist(params).await
2137    }
2138
2139    /// Queries a single order by order ID or client order ID.
2140    ///
2141    /// # Errors
2142    ///
2143    /// Returns an error if the request fails.
2144    pub async fn query_order(
2145        &self,
2146        params: &BinanceOrderQueryParams,
2147    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
2148        self.inner.query_order(params).await
2149    }
2150
2151    /// Queries all open orders.
2152    ///
2153    /// # Errors
2154    ///
2155    /// Returns an error if the request fails.
2156    pub async fn query_open_orders(
2157        &self,
2158        params: &BinanceOpenOrdersParams,
2159    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2160        self.inner.query_open_orders(params).await
2161    }
2162
2163    /// Queries all orders (including historical).
2164    ///
2165    /// # Errors
2166    ///
2167    /// Returns an error if the request fails.
2168    pub async fn query_all_orders(
2169        &self,
2170        params: &BinanceAllOrdersParams,
2171    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2172        self.inner.query_all_orders(params).await
2173    }
2174
2175    /// Fetches account information including balances and positions.
2176    ///
2177    /// # Errors
2178    ///
2179    /// Returns an error if the request fails.
2180    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
2181        self.inner.query_account().await
2182    }
2183
2184    /// Fetches position risk information.
2185    ///
2186    /// # Errors
2187    ///
2188    /// Returns an error if the request fails.
2189    pub async fn query_positions(
2190        &self,
2191        params: &BinancePositionRiskParams,
2192    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
2193        self.inner.query_positions(params).await
2194    }
2195
2196    /// Fetches user trades for a symbol.
2197    ///
2198    /// # Errors
2199    ///
2200    /// Returns an error if the request fails.
2201    pub async fn query_user_trades(
2202        &self,
2203        params: &BinanceUserTradesParams,
2204    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
2205        self.inner.query_user_trades(params).await
2206    }
2207
2208    /// Submits a new order.
2209    ///
2210    /// # Errors
2211    ///
2212    /// Returns an error if:
2213    /// - The instrument is not cached.
2214    /// - The order type or time-in-force is unsupported.
2215    /// - Stop orders are submitted without a trigger price.
2216    /// - The request fails.
2217    #[expect(clippy::too_many_arguments)]
2218    pub async fn submit_order(
2219        &self,
2220        account_id: AccountId,
2221        instrument_id: InstrumentId,
2222        client_order_id: ClientOrderId,
2223        order_side: OrderSide,
2224        order_type: OrderType,
2225        quantity: Quantity,
2226        time_in_force: TimeInForce,
2227        price: Option<Price>,
2228        trigger_price: Option<Price>,
2229        reduce_only: bool,
2230        post_only: bool,
2231        rpi: bool,
2232        position_side: Option<BinancePositionSide>,
2233        price_match: Option<BinancePriceMatch>,
2234        good_till_date: Option<i64>,
2235    ) -> anyhow::Result<OrderStatusReport> {
2236        let (symbol, price_precision, size_precision) =
2237            self.cached_precisions_by_id(instrument_id)?;
2238
2239        let binance_side = BinanceSide::try_from(order_side)?;
2240        let binance_order_type = order_type_to_binance_futures(order_type)?;
2241        let binance_tif = if rpi {
2242            BinanceTimeInForce::Rpi
2243        } else if post_only {
2244            BinanceTimeInForce::Gtx
2245        } else {
2246            BinanceTimeInForce::try_from(time_in_force)?
2247        };
2248
2249        let requires_trigger_price = matches!(
2250            order_type,
2251            OrderType::StopMarket
2252                | OrderType::StopLimit
2253                | OrderType::TrailingStopMarket
2254                | OrderType::MarketIfTouched
2255                | OrderType::LimitIfTouched
2256        );
2257
2258        if requires_trigger_price && trigger_price.is_none() {
2259            anyhow::bail!("Order type {order_type:?} requires a trigger price");
2260        }
2261
2262        // MARKET and STOP_MARKET orders don't accept timeInForce
2263        let requires_time_in_force = matches!(
2264            order_type,
2265            OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
2266        );
2267
2268        let qty_str = quantity.to_string();
2269        let price_str = if price_match.is_some() {
2270            None
2271        } else {
2272            price.map(|p| p.to_string())
2273        };
2274        let stop_price_str = trigger_price.map(|p| p.to_string());
2275        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2276
2277        let params = BinanceNewOrderParams {
2278            symbol,
2279            side: binance_side,
2280            order_type: binance_order_type,
2281            time_in_force: if requires_time_in_force {
2282                Some(binance_tif)
2283            } else {
2284                None
2285            },
2286            quantity: Some(qty_str),
2287            price: price_str,
2288            new_client_order_id: Some(client_id_str),
2289            stop_price: stop_price_str,
2290            reduce_only: reduce_only_param(reduce_only, position_side),
2291            position_side,
2292            close_position: None,
2293            activation_price: None,
2294            callback_rate: None,
2295            working_type: None,
2296            price_protect: None,
2297            new_order_resp_type: None,
2298            good_till_date,
2299            recv_window: None,
2300            price_match,
2301            self_trade_prevention_mode: None,
2302        };
2303
2304        let order = self.inner.submit_order(&params).await?;
2305        let ts_init = self.clock.get_time_ns();
2306        order.to_order_status_report(
2307            account_id,
2308            instrument_id,
2309            price_precision,
2310            size_precision,
2311            self.treat_expired_as_canceled,
2312            ts_init,
2313        )
2314    }
2315
2316    /// Submits an algo order (conditional order) to the Binance Algo Service.
2317    ///
2318    /// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
2319    /// This method handles StopMarket, StopLimit, MarketIfTouched, LimitIfTouched,
2320    /// and TrailingStopMarket orders.
2321    ///
2322    /// # Errors
2323    ///
2324    /// Returns an error if:
2325    /// - The order type requires a trigger price but none is provided.
2326    /// - The instrument is not cached.
2327    /// - The request fails.
2328    #[expect(clippy::too_many_arguments)]
2329    pub async fn submit_algo_order(
2330        &self,
2331        account_id: AccountId,
2332        instrument_id: InstrumentId,
2333        client_order_id: ClientOrderId,
2334        order_side: OrderSide,
2335        order_type: OrderType,
2336        quantity: Quantity,
2337        time_in_force: TimeInForce,
2338        price: Option<Price>,
2339        trigger_price: Option<Price>,
2340        reduce_only: bool,
2341        close_position: bool,
2342        position_side: Option<BinancePositionSide>,
2343        activation_price: Option<Price>,
2344        callback_rate: Option<String>,
2345        working_type: Option<BinanceWorkingType>,
2346        good_till_date: Option<i64>,
2347    ) -> anyhow::Result<OrderStatusReport> {
2348        let (symbol, price_precision, size_precision) =
2349            self.cached_precisions_by_id(instrument_id)?;
2350
2351        let binance_side = BinanceSide::try_from(order_side)?;
2352        let binance_order_type = order_type_to_binance_futures(order_type)?;
2353        let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
2354
2355        let requires_trigger_price = matches!(
2356            order_type,
2357            OrderType::StopMarket
2358                | OrderType::StopLimit
2359                | OrderType::MarketIfTouched
2360                | OrderType::LimitIfTouched
2361        );
2362        anyhow::ensure!(
2363            !requires_trigger_price || trigger_price.is_some(),
2364            "Algo order type {order_type:?} requires a trigger price"
2365        );
2366
2367        // Limit orders require time in force
2368        let requires_time_in_force =
2369            matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
2370
2371        let price_str = price.map(|p| p.to_string());
2372        let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
2373            None
2374        } else {
2375            trigger_price.map(|p| p.to_string())
2376        };
2377        let reduce_only = reduce_only_param(reduce_only, position_side);
2378        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2379
2380        // closePosition is mutually exclusive with quantity and reduceOnly
2381        let params = if close_position {
2382            BinanceNewAlgoOrderParams {
2383                symbol,
2384                side: binance_side,
2385                order_type: binance_order_type,
2386                algo_type: BinanceAlgoType::Conditional,
2387                position_side,
2388                quantity: None,
2389                price: price_str,
2390                trigger_price: trigger_price_str,
2391                time_in_force: if requires_time_in_force {
2392                    Some(binance_tif)
2393                } else {
2394                    None
2395                },
2396                working_type,
2397                close_position: Some(true),
2398                price_protect: None,
2399                reduce_only: None,
2400                activation_price: activation_price.map(|p| p.to_string()),
2401                callback_rate,
2402                client_algo_id: Some(client_id_str),
2403                good_till_date,
2404                recv_window: None,
2405            }
2406        } else {
2407            let qty_str = quantity.to_string();
2408            BinanceNewAlgoOrderParams {
2409                symbol,
2410                side: binance_side,
2411                order_type: binance_order_type,
2412                algo_type: BinanceAlgoType::Conditional,
2413                position_side,
2414                quantity: Some(qty_str),
2415                price: price_str,
2416                trigger_price: trigger_price_str,
2417                time_in_force: if requires_time_in_force {
2418                    Some(binance_tif)
2419                } else {
2420                    None
2421                },
2422                working_type,
2423                close_position: None,
2424                price_protect: None,
2425                reduce_only,
2426                activation_price: activation_price.map(|p| p.to_string()),
2427                callback_rate,
2428                client_algo_id: Some(client_id_str),
2429                good_till_date,
2430                recv_window: None,
2431            }
2432        };
2433
2434        let order = self.inner.submit_algo_order(&params).await?;
2435        let ts_init = self.clock.get_time_ns();
2436        order.to_order_status_report(
2437            account_id,
2438            instrument_id,
2439            price_precision,
2440            size_precision,
2441            ts_init,
2442        )
2443    }
2444
2445    /// Submits multiple orders in a single request (up to 5 orders).
2446    ///
2447    /// Each order in the batch is processed independently. The response contains
2448    /// the result for each order, which can be either a success or an error.
2449    ///
2450    /// # Errors
2451    ///
2452    /// Returns an error if the batch exceeds 5 orders or the request fails.
2453    pub async fn submit_order_list(
2454        &self,
2455        orders: &[BatchOrderItem],
2456    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2457        self.inner.submit_order_list(orders).await
2458    }
2459
2460    /// Modifies an existing order (price and quantity only).
2461    ///
2462    /// Either `venue_order_id` or `client_order_id` must be provided.
2463    ///
2464    /// # Errors
2465    ///
2466    /// Returns an error if:
2467    /// - Neither venue_order_id nor client_order_id is provided.
2468    /// - The instrument is not cached.
2469    /// - The request fails.
2470    #[expect(clippy::too_many_arguments)]
2471    pub async fn modify_order(
2472        &self,
2473        account_id: AccountId,
2474        instrument_id: InstrumentId,
2475        venue_order_id: Option<VenueOrderId>,
2476        client_order_id: Option<ClientOrderId>,
2477        order_side: OrderSide,
2478        quantity: Quantity,
2479        price: Price,
2480    ) -> anyhow::Result<OrderStatusReport> {
2481        anyhow::ensure!(
2482            venue_order_id.is_some() || client_order_id.is_some(),
2483            "Either venue_order_id or client_order_id must be provided"
2484        );
2485
2486        let (symbol, price_precision, size_precision) =
2487            self.cached_precisions_by_id(instrument_id)?;
2488
2489        let binance_side = BinanceSide::try_from(order_side)?;
2490
2491        let order_id = venue_order_id
2492            .map(|id| id.inner().parse::<i64>())
2493            .transpose()
2494            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2495
2496        let params = BinanceModifyOrderParams {
2497            symbol,
2498            order_id,
2499            orig_client_order_id: client_order_id
2500                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2501            side: binance_side,
2502            quantity: quantity.to_string(),
2503            price: price.to_string(),
2504            recv_window: None,
2505        };
2506
2507        let order = self.inner.modify_order(&params).await?;
2508        let ts_init = self.clock.get_time_ns();
2509        order.to_order_status_report(
2510            account_id,
2511            instrument_id,
2512            price_precision,
2513            size_precision,
2514            self.treat_expired_as_canceled,
2515            ts_init,
2516        )
2517    }
2518
2519    /// Modifies multiple orders in a single request (up to 5 orders).
2520    ///
2521    /// Each modify in the batch is processed independently. The response contains
2522    /// the result for each modify, which can be either a success or an error.
2523    ///
2524    /// # Errors
2525    ///
2526    /// Returns an error if the batch exceeds 5 orders or the request fails.
2527    pub async fn batch_modify_orders(
2528        &self,
2529        modifies: &[BatchModifyItem],
2530    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2531        self.inner.batch_modify_orders(modifies).await
2532    }
2533
2534    /// Cancels an order by venue order ID or client order ID.
2535    ///
2536    /// Either `venue_order_id` or `client_order_id` must be provided.
2537    ///
2538    /// # Errors
2539    ///
2540    /// Returns an error if:
2541    /// - Neither venue_order_id nor client_order_id is provided.
2542    /// - The request fails.
2543    pub async fn cancel_order(
2544        &self,
2545        instrument_id: InstrumentId,
2546        venue_order_id: Option<VenueOrderId>,
2547        client_order_id: Option<ClientOrderId>,
2548    ) -> anyhow::Result<VenueOrderId> {
2549        anyhow::ensure!(
2550            venue_order_id.is_some() || client_order_id.is_some(),
2551            "Either venue_order_id or client_order_id must be provided"
2552        );
2553
2554        let symbol = format_binance_symbol(&instrument_id);
2555
2556        let order_id = match venue_order_id {
2557            Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2558                Ok(order_id) => Some(order_id),
2559                Err(e) if client_order_id.is_some() => {
2560                    log::warn!(
2561                        "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2562                    );
2563                    None
2564                }
2565                Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2566            },
2567            None => None,
2568        };
2569
2570        let params = BinanceCancelOrderParams {
2571            symbol,
2572            order_id,
2573            orig_client_order_id: client_order_id
2574                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2575            recv_window: None,
2576        };
2577
2578        let order = self.inner.cancel_order(&params).await?;
2579        Ok(VenueOrderId::new(order.order_id.to_string()))
2580    }
2581
2582    /// Cancels an algo order (conditional order) via the Binance Algo Service.
2583    ///
2584    /// Use the `client_algo_id` which corresponds to the `client_order_id` used
2585    /// when submitting the algo order.
2586    ///
2587    /// # Errors
2588    ///
2589    /// Returns an error if the request fails.
2590    pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2591        let params = BinanceAlgoOrderQueryParams {
2592            algo_id: None,
2593            client_algo_id: Some(encode_broker_id(
2594                &client_order_id,
2595                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2596            )),
2597            recv_window: None,
2598        };
2599
2600        let response = self.inner.cancel_algo_order(&params).await?;
2601        if response.code.parse::<i32>().unwrap_or(0) == 200 {
2602            Ok(())
2603        } else {
2604            anyhow::bail!(
2605                "Cancel algo order failed: code={}, msg={}",
2606                response.code,
2607                response.msg
2608            )
2609        }
2610    }
2611
2612    /// Cancels all open orders for a symbol.
2613    ///
2614    /// # Errors
2615    ///
2616    /// Returns an error if the request fails.
2617    pub async fn cancel_all_orders(
2618        &self,
2619        instrument_id: InstrumentId,
2620    ) -> anyhow::Result<Vec<VenueOrderId>> {
2621        let symbol = format_binance_symbol(&instrument_id);
2622
2623        let params = BinanceCancelAllOrdersParams {
2624            symbol,
2625            recv_window: None,
2626        };
2627
2628        let response = self.inner.cancel_all_orders(&params).await?;
2629        if response.code == 200 {
2630            Ok(vec![])
2631        } else {
2632            anyhow::bail!("Cancel all orders failed: {}", response.msg);
2633        }
2634    }
2635
2636    /// Cancels all open algo orders for a symbol.
2637    ///
2638    /// # Errors
2639    ///
2640    /// Returns an error if the request fails.
2641    pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2642        let symbol = format_binance_symbol(&instrument_id);
2643
2644        let params = BinanceCancelAllAlgoOrdersParams {
2645            symbol,
2646            recv_window: None,
2647        };
2648
2649        let response = self.inner.cancel_all_algo_orders(&params).await?;
2650        if response.code == 200 {
2651            Ok(())
2652        } else {
2653            anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2654        }
2655    }
2656
2657    /// Cancels multiple orders in a single request (up to 10 orders).
2658    ///
2659    /// Each cancel in the batch is processed independently. The response contains
2660    /// the result for each cancel, which can be either a success or an error.
2661    ///
2662    /// # Errors
2663    ///
2664    /// Returns an error if the batch exceeds 10 orders or the request fails.
2665    pub async fn batch_cancel_orders(
2666        &self,
2667        cancels: &[BatchCancelItem],
2668    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2669        self.inner.batch_cancel_orders(cancels).await
2670    }
2671
2672    /// Queries open algo orders (conditional orders).
2673    ///
2674    /// Returns all open algo orders, optionally filtered by symbol.
2675    ///
2676    /// # Errors
2677    ///
2678    /// Returns an error if the request fails.
2679    pub async fn query_open_algo_orders(
2680        &self,
2681        instrument_id: Option<InstrumentId>,
2682    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2683        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2684
2685        let params = BinanceOpenAlgoOrdersParams {
2686            symbol,
2687            recv_window: None,
2688        };
2689
2690        self.inner.query_open_algo_orders(&params).await
2691    }
2692
2693    /// Queries a single algo order by client_order_id.
2694    ///
2695    /// # Errors
2696    ///
2697    /// Returns an error if the request fails.
2698    pub async fn query_algo_order(
2699        &self,
2700        client_order_id: ClientOrderId,
2701    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2702        let params = BinanceAlgoOrderQueryParams {
2703            algo_id: None,
2704            client_algo_id: Some(encode_broker_id(
2705                &client_order_id,
2706                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2707            )),
2708            recv_window: None,
2709        };
2710
2711        self.inner.query_algo_order(&params).await
2712    }
2713
2714    /// Queries a single algo order by venue order ID.
2715    ///
2716    /// # Errors
2717    ///
2718    /// Returns an error if the venue order ID is not numeric or the request fails.
2719    pub async fn query_algo_order_by_venue_order_id(
2720        &self,
2721        venue_order_id: VenueOrderId,
2722    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2723        let algo_id = venue_order_id
2724            .inner()
2725            .parse::<i64>()
2726            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2727        let params = BinanceAlgoOrderQueryParams {
2728            algo_id: Some(algo_id),
2729            client_algo_id: None,
2730            recv_window: None,
2731        };
2732
2733        self.inner.query_algo_order(&params).await
2734    }
2735
2736    async fn query_historical_algo_order_by_venue_order_id(
2737        &self,
2738        instrument_id: InstrumentId,
2739        venue_order_id: VenueOrderId,
2740    ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrder>> {
2741        let algo_id = venue_order_id
2742            .inner()
2743            .parse::<i64>()
2744            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2745        let symbol = format_binance_symbol(&instrument_id);
2746        let params = BinanceAllAlgoOrdersParams {
2747            symbol,
2748            algo_id: Some(algo_id),
2749            start_time: None,
2750            end_time: None,
2751            page: None,
2752            limit: Some(1),
2753            recv_window: None,
2754        };
2755        let order = self
2756            .inner
2757            .query_all_algo_orders(&params)
2758            .await?
2759            .into_iter()
2760            .next()
2761            .filter(|order| order.algo_id == algo_id);
2762
2763        Ok(order)
2764    }
2765
2766    /// Queries an algo order by venue ID, falling back to recent history when needed.
2767    ///
2768    /// Uses the client order ID only when no venue order ID is available.
2769    /// Matching-engine details are best-effort; remote enrichment failures fall back to the
2770    /// Algo Service execution fields.
2771    ///
2772    /// # Errors
2773    ///
2774    /// Returns an error if an ID is invalid, the Algo Service request fails, or matching-engine
2775    /// enrichment fails because of a local configuration or validation error.
2776    pub async fn query_algo_order_with_history(
2777        &self,
2778        instrument_id: InstrumentId,
2779        client_order_id: Option<ClientOrderId>,
2780        algo_venue_order_id: Option<VenueOrderId>,
2781    ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrderQueryResult>> {
2782        let order = if let Some(venue_order_id) = algo_venue_order_id {
2783            match self
2784                .query_algo_order_by_venue_order_id(venue_order_id)
2785                .await
2786            {
2787                Ok(order) => Some(order),
2788                Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => {
2789                    self.query_historical_algo_order_by_venue_order_id(
2790                        instrument_id,
2791                        venue_order_id,
2792                    )
2793                    .await?
2794                }
2795                Err(e) => return Err(e),
2796            }
2797        } else {
2798            let Some(client_order_id) = client_order_id else {
2799                return Ok(None);
2800            };
2801
2802            match self.query_algo_order(client_order_id).await {
2803                Ok(order) => Some(order),
2804                Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => None,
2805                Err(e) => return Err(e),
2806            }
2807        };
2808
2809        let Some(order) = order else {
2810            return Ok(None);
2811        };
2812        let actual = if let Some(actual_order_id) = order
2813            .actual_order_id
2814            .as_deref()
2815            .filter(|id| !id.is_empty())
2816            .map(str::parse::<i64>)
2817            .transpose()
2818            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
2819        {
2820            let params = BinanceOrderQueryParams {
2821                symbol: format_binance_symbol(&instrument_id),
2822                order_id: Some(actual_order_id),
2823                orig_client_order_id: None,
2824                recv_window: None,
2825            };
2826
2827            match self.inner.query_order(&params).await {
2828                Ok(actual) => Some(actual),
2829                Err(
2830                    e @ (BinanceFuturesHttpError::MissingCredentials
2831                    | BinanceFuturesHttpError::ValidationError(_)),
2832                ) => return Err(e),
2833                Err(e) => {
2834                    log::warn!(
2835                        "Failed to enrich algo order with matching-engine order \
2836                         {actual_order_id}: {e}; falling back to Algo Service execution fields"
2837                    );
2838                    None
2839                }
2840            }
2841        } else {
2842            None
2843        };
2844
2845        Ok(Some(BinanceFuturesAlgoOrderQueryResult {
2846            algo: order,
2847            actual,
2848        }))
2849    }
2850
2851    /// Requests the current account state.
2852    ///
2853    /// # Errors
2854    ///
2855    /// Returns an error if the request fails or parsing fails.
2856    pub async fn request_account_state(
2857        &self,
2858        account_id: AccountId,
2859    ) -> anyhow::Result<AccountState> {
2860        let ts_init = UnixNanos::default();
2861        let account_info = self.inner.query_account().await?;
2862        account_info.to_account_state(account_id, ts_init)
2863    }
2864
2865    /// Requests a single order status report.
2866    ///
2867    /// Either `venue_order_id` or `client_order_id` must be provided.
2868    ///
2869    /// # Errors
2870    ///
2871    /// Returns an error if the request fails or parsing fails.
2872    pub async fn request_order_status_report(
2873        &self,
2874        account_id: AccountId,
2875        instrument_id: InstrumentId,
2876        venue_order_id: Option<VenueOrderId>,
2877        client_order_id: Option<ClientOrderId>,
2878    ) -> anyhow::Result<OrderStatusReport> {
2879        anyhow::ensure!(
2880            venue_order_id.is_some() || client_order_id.is_some(),
2881            "Either venue_order_id or client_order_id must be provided"
2882        );
2883
2884        let (symbol, price_precision, size_precision) =
2885            self.cached_precisions_by_id(instrument_id)?;
2886
2887        let order_id = venue_order_id
2888            .map(|id| id.inner().parse::<i64>())
2889            .transpose()
2890            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2891
2892        let orig_client_order_id =
2893            client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2894
2895        let params = BinanceOrderQueryParams {
2896            symbol,
2897            order_id,
2898            orig_client_order_id,
2899            recv_window: None,
2900        };
2901
2902        let order = self.inner.query_order(&params).await?;
2903        let ts_init = self.clock.get_time_ns();
2904        order.to_order_status_report(
2905            account_id,
2906            instrument_id,
2907            price_precision,
2908            size_precision,
2909            self.treat_expired_as_canceled,
2910            ts_init,
2911        )
2912    }
2913
2914    /// Requests order status reports for open orders.
2915    ///
2916    /// If `instrument_id` is None, returns all open orders.
2917    ///
2918    /// # Errors
2919    ///
2920    /// Returns an error if the request fails or parsing fails.
2921    pub async fn request_order_status_reports(
2922        &self,
2923        account_id: AccountId,
2924        instrument_id: Option<InstrumentId>,
2925        open_only: bool,
2926    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2927        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2928
2929        let orders = if open_only {
2930            let params = BinanceOpenOrdersParams {
2931                symbol: symbol.clone(),
2932                recv_window: None,
2933            };
2934            self.inner.query_open_orders(&params).await?
2935        } else {
2936            // For historical orders, symbol is required
2937            let symbol = symbol.ok_or_else(|| {
2938                anyhow::anyhow!("instrument_id is required for historical orders")
2939            })?;
2940            let params = BinanceAllOrdersParams {
2941                symbol,
2942                order_id: None,
2943                start_time: None,
2944                end_time: None,
2945                limit: None,
2946                recv_window: None,
2947            };
2948            self.inner.query_all_orders(&params).await?
2949        };
2950
2951        let ts_init = self.clock.get_time_ns();
2952        let mut reports = Vec::with_capacity(orders.len());
2953
2954        for order in orders {
2955            let order_instrument_id = instrument_id
2956                .unwrap_or_else(|| format_instrument_id(&order.symbol, self.product_type));
2957            let (_, price_precision, size_precision) =
2958                self.cached_precisions_by_id(order_instrument_id)?;
2959
2960            match order.to_order_status_report(
2961                account_id,
2962                order_instrument_id,
2963                price_precision,
2964                size_precision,
2965                self.treat_expired_as_canceled,
2966                ts_init,
2967            ) {
2968                Ok(report) => reports.push(report),
2969                Err(e) => {
2970                    log::warn!("Failed to parse order status report: {e}");
2971                }
2972            }
2973        }
2974
2975        Ok(reports)
2976    }
2977
2978    /// Requests fill reports for a symbol.
2979    ///
2980    /// # Errors
2981    ///
2982    /// Returns an error if the request fails or parsing fails.
2983    #[expect(clippy::too_many_arguments)]
2984    pub async fn request_fill_reports(
2985        &self,
2986        account_id: AccountId,
2987        instrument_id: InstrumentId,
2988        venue_order_id: Option<VenueOrderId>,
2989        start: Option<i64>,
2990        end: Option<i64>,
2991        limit: Option<u32>,
2992        bnfcr_currency: Currency,
2993    ) -> anyhow::Result<Vec<FillReport>> {
2994        let (symbol, price_precision, size_precision) =
2995            self.cached_precisions_by_id(instrument_id)?;
2996
2997        let order_id = venue_order_id
2998            .map(|id| id.inner().parse::<i64>())
2999            .transpose()
3000            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3001
3002        let params = BinanceUserTradesParams {
3003            symbol,
3004            order_id,
3005            start_time: start,
3006            end_time: end,
3007            from_id: None,
3008            limit,
3009            recv_window: None,
3010        };
3011
3012        let trades = self.inner.query_user_trades(&params).await?;
3013
3014        let ts_init = self.clock.get_time_ns();
3015        let mut reports = Vec::with_capacity(trades.len());
3016
3017        for trade in trades {
3018            match trade.to_fill_report(
3019                account_id,
3020                instrument_id,
3021                price_precision,
3022                size_precision,
3023                bnfcr_currency,
3024                ts_init,
3025            ) {
3026                Ok(report) => reports.push(report),
3027                Err(e) => {
3028                    log::warn!("Failed to parse fill report: {e}");
3029                }
3030            }
3031        }
3032
3033        Ok(reports)
3034    }
3035
3036    /// Requests recent public trades for an instrument.
3037    ///
3038    /// # Errors
3039    ///
3040    /// Returns an error if the request fails, instrument is not cached, or parsing fails.
3041    pub async fn request_trades(
3042        &self,
3043        instrument_id: InstrumentId,
3044        limit: Option<u32>,
3045    ) -> anyhow::Result<Vec<TradeTick>> {
3046        let (symbol, price_precision, size_precision) =
3047            self.cached_precisions_by_id(instrument_id)?;
3048
3049        let params = BinanceTradesParams { symbol, limit };
3050
3051        let trades = self.inner.trades(&params).await?;
3052        let ts_init = UnixNanos::default();
3053
3054        let mut result = Vec::with_capacity(trades.len());
3055        for trade in trades {
3056            let tick = parse_futures_trade_tick(
3057                &trade,
3058                instrument_id,
3059                price_precision,
3060                size_precision,
3061                ts_init,
3062            )?;
3063            result.push(tick);
3064        }
3065
3066        Ok(result)
3067    }
3068
3069    /// Requests bounded aggregate public trades for an instrument.
3070    ///
3071    /// # Errors
3072    ///
3073    /// Returns an error if a supplied time bound is invalid or parsing fails.
3074    pub async fn request_agg_trades(
3075        &self,
3076        instrument_id: InstrumentId,
3077        start: Option<Timestamp>,
3078        end: Option<Timestamp>,
3079        limit: Option<u32>,
3080    ) -> anyhow::Result<Vec<TradeTick>> {
3081        let cutoff =
3082            self.clock.get_time_ns().to_datetime_utc() - jiff::SignedDuration::from_hours(24);
3083        anyhow::ensure!(
3084            start.as_ref().is_none_or(|value| value >= &cutoff)
3085                && end.as_ref().is_none_or(|value| value >= &cutoff),
3086            "Binance Futures aggregate trade history is limited to the past 24 hours"
3087        );
3088        let (symbol, price_precision, size_precision) =
3089            self.cached_precisions_by_id(instrument_id)?;
3090        let params = BinanceAggTradesParams {
3091            symbol,
3092            from_id: None,
3093            start_time: start.map(|value| value.as_millisecond()),
3094            end_time: end.map(|value| value.as_millisecond()),
3095            limit,
3096        };
3097        let trades = self.inner.agg_trades(&params).await?;
3098        trades
3099            .iter()
3100            .map(|trade| {
3101                let ts_init = parse_millis(trade.time, "Futures aggregate trade time")?;
3102                parse_futures_agg_trade_tick(
3103                    trade,
3104                    instrument_id,
3105                    price_precision,
3106                    size_precision,
3107                    ts_init,
3108                )
3109            })
3110            .collect()
3111    }
3112
3113    /// Requests bar (kline/candlestick) data for an instrument.
3114    ///
3115    /// # Errors
3116    ///
3117    /// Returns an error if the bar type is not supported, instrument is not cached,
3118    /// or the request fails.
3119    pub async fn request_binance_bars(
3120        &self,
3121        bar_type: BarType,
3122        start: Option<Timestamp>,
3123        end: Option<Timestamp>,
3124        limit: Option<u32>,
3125    ) -> anyhow::Result<Vec<BinanceBar>> {
3126        anyhow::ensure!(
3127            bar_type.aggregation_source() == AggregationSource::External,
3128            "Only EXTERNAL aggregation is supported"
3129        );
3130
3131        let spec = bar_type.spec();
3132        let step = spec.step.get();
3133        let interval = match spec.aggregation {
3134            BarAggregation::Second => {
3135                anyhow::bail!("Binance Futures does not support second-level kline intervals")
3136            }
3137            BarAggregation::Minute => format!("{step}m"),
3138            BarAggregation::Hour => format!("{step}h"),
3139            BarAggregation::Day => format!("{step}d"),
3140            BarAggregation::Week => format!("{step}w"),
3141            BarAggregation::Month => format!("{step}M"),
3142            a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
3143        };
3144
3145        let instrument_id = bar_type.instrument_id();
3146        let (symbol, price_precision, size_precision) =
3147            self.cached_precisions_by_id(instrument_id)?;
3148
3149        let params = BinanceKlinesParams {
3150            symbol,
3151            interval,
3152            start_time: start.map(|dt| dt.as_millisecond()),
3153            end_time: end.map(|dt| dt.as_millisecond()),
3154            limit,
3155        };
3156
3157        let klines = self.inner.klines(&params).await?;
3158        let now = self.clock.get_time_ns();
3159
3160        let mut result = Vec::with_capacity(klines.len());
3161        for kline in klines {
3162            let ts_init = parse_millis(kline.close_time, "Futures kline close time")?;
3163            let bar = parse_futures_kline_binance_bar(
3164                &kline,
3165                bar_type,
3166                price_precision,
3167                size_precision,
3168                ts_init,
3169            )?;
3170
3171            if bar.ts_event < now {
3172                result.push(bar);
3173            }
3174        }
3175
3176        Ok(result)
3177    }
3178
3179    /// Requests core bars for an instrument.
3180    ///
3181    /// # Errors
3182    ///
3183    /// Returns an error if the bar type is unsupported or the request fails.
3184    pub async fn request_bars(
3185        &self,
3186        bar_type: BarType,
3187        start: Option<Timestamp>,
3188        end: Option<Timestamp>,
3189        limit: Option<u32>,
3190    ) -> anyhow::Result<Vec<Bar>> {
3191        Ok(self
3192            .request_binance_bars(bar_type, start, end, limit)
3193            .await?
3194            .into_iter()
3195            .map(|bar| bar.bar())
3196            .collect())
3197    }
3198
3199    /// Requests an explicit L2 order-book snapshot.
3200    ///
3201    /// # Errors
3202    ///
3203    /// Returns an error for an invalid depth, missing instrument, request failure, or invalid level.
3204    pub async fn request_book_snapshot(
3205        &self,
3206        instrument_id: InstrumentId,
3207        depth: Option<u32>,
3208    ) -> anyhow::Result<OrderBook> {
3209        if depth.is_some_and(|value| !crate::common::consts::BINANCE_BOOK_DEPTHS.contains(&value)) {
3210            anyhow::bail!(
3211                "invalid Binance Futures order-book depth; valid values are {:?}",
3212                crate::common::consts::BINANCE_BOOK_DEPTHS
3213            );
3214        }
3215        let (symbol, price_precision, size_precision) =
3216            self.cached_precisions_by_id(instrument_id)?;
3217        let params = BinanceDepthParams {
3218            symbol,
3219            limit: depth,
3220        };
3221        let snapshot = self.inner.depth(&params).await?;
3222        let ts_event = self.clock.get_time_ns();
3223        let sequence = u64::try_from(snapshot.last_update_id)
3224            .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3225        let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3226        for (index, level) in snapshot.bids.iter().enumerate() {
3227            let order = BookOrder::new(
3228                OrderSide::Buy,
3229                parse_required_price_at_precision(&level.0, price_precision, "bid price")?,
3230                parse_required_quantity_at_precision(&level.1, size_precision, "bid quantity")?,
3231                index as u64,
3232            );
3233            book.add(order, 0, sequence, ts_event);
3234        }
3235        let bid_count = snapshot.bids.len();
3236        for (index, level) in snapshot.asks.iter().enumerate() {
3237            let order = BookOrder::new(
3238                OrderSide::Sell,
3239                parse_required_price_at_precision(&level.0, price_precision, "ask price")?,
3240                parse_required_quantity_at_precision(&level.1, size_precision, "ask quantity")?,
3241                (bid_count + index) as u64,
3242            );
3243            book.add(order, 0, sequence, ts_event);
3244        }
3245        Ok(book)
3246    }
3247
3248    fn cached_precisions_by_id(
3249        &self,
3250        instrument_id: InstrumentId,
3251    ) -> anyhow::Result<(String, u8, u8)> {
3252        let symbol = format_binance_symbol(&instrument_id);
3253        let instrument = self.instrument_metadata(instrument_id)?;
3254        let (price_precision, size_precision) = instrument.precisions()?;
3255
3256        Ok((symbol, price_precision, size_precision))
3257    }
3258
3259    pub(crate) fn instrument_metadata(
3260        &self,
3261        instrument_id: InstrumentId,
3262    ) -> anyhow::Result<BinanceFuturesInstrument> {
3263        let symbol = format_binance_symbol(&instrument_id);
3264        let instrument = self
3265            .instruments
3266            .get(&Ustr::from(symbol.as_str()))
3267            .map(|instrument| instrument.value().clone())
3268            .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
3269        if instrument.id() != instrument_id {
3270            return Err(InstrumentLookupError::not_found(instrument_id).into());
3271        }
3272
3273        Ok(instrument)
3274    }
3275
3276    /// Requests historical funding rates for an instrument.
3277    ///
3278    /// # Errors
3279    ///
3280    /// Returns an error if the request fails or parsing fails.
3281    pub async fn request_funding_rates(
3282        &self,
3283        instrument_id: InstrumentId,
3284        start: Option<Timestamp>,
3285        end: Option<Timestamp>,
3286        limit: Option<u32>,
3287    ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3288        let params = BinanceFundingRateParams {
3289            symbol: Some(format_binance_symbol(&instrument_id)),
3290            start_time: start.map(|dt| dt.as_millisecond()),
3291            end_time: end.map(|dt| dt.as_millisecond()),
3292            limit,
3293        };
3294
3295        let rates = self.inner.funding_rate(&params).await?;
3296        let ts_init = UnixNanos::default();
3297
3298        let mut result = Vec::with_capacity(rates.len());
3299        for rate in rates {
3300            result.push(parse_futures_funding_rate_update(
3301                &rate,
3302                instrument_id,
3303                ts_init,
3304            )?);
3305        }
3306
3307        Ok(result)
3308    }
3309}
3310
3311fn parse_futures_trade_tick(
3312    trade: &BinanceFuturesTrade,
3313    instrument_id: InstrumentId,
3314    price_precision: u8,
3315    size_precision: u8,
3316    ts_init: UnixNanos,
3317) -> anyhow::Result<TradeTick> {
3318    let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
3319        .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3320    let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
3321        .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3322    let ts_event = parse_millis(trade.time, "Futures trade time")?;
3323
3324    let aggressor_side = if trade.is_buyer_maker {
3325        AggressorSide::Sell
3326    } else {
3327        AggressorSide::Buy
3328    };
3329
3330    Ok(TradeTick::new(
3331        instrument_id,
3332        price,
3333        size,
3334        aggressor_side,
3335        TradeId::new(trade.id.to_string()),
3336        ts_event,
3337        ts_init,
3338    ))
3339}
3340
3341fn parse_futures_agg_trade_tick(
3342    trade: &BinanceFuturesAggTrade,
3343    instrument_id: InstrumentId,
3344    price_precision: u8,
3345    size_precision: u8,
3346    ts_init: UnixNanos,
3347) -> anyhow::Result<TradeTick> {
3348    let trade = BinanceFuturesTrade {
3349        id: trade.id,
3350        price: trade.price.clone(),
3351        qty: trade.qty.clone(),
3352        quote_qty: String::new(),
3353        time: trade.time,
3354        is_buyer_maker: trade.is_buyer_maker,
3355    };
3356    parse_futures_trade_tick(
3357        &trade,
3358        instrument_id,
3359        price_precision,
3360        size_precision,
3361        ts_init,
3362    )
3363}
3364
3365fn parse_futures_kline_binance_bar(
3366    kline: &BinanceFuturesKline,
3367    bar_type: BarType,
3368    price_precision: u8,
3369    size_precision: u8,
3370    ts_init: UnixNanos,
3371) -> anyhow::Result<BinanceBar> {
3372    let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
3373        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3374    let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
3375        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3376    let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
3377        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3378    let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
3379        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3380    let volume =
3381        parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
3382            .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3383    let ts_event = parse_millis(kline.close_time, "Futures kline close time")?;
3384
3385    let quote_volume = kline.quote_volume.parse::<Decimal>().map_err(|e| {
3386        anyhow::anyhow!(
3387            "invalid Futures kline {} quote volume: {e}",
3388            kline.open_time
3389        )
3390    })?;
3391    let taker_buy_base_volume = kline
3392        .taker_buy_base_volume
3393        .parse::<Decimal>()
3394        .map_err(|e| {
3395            anyhow::anyhow!(
3396                "invalid Futures kline {} taker buy base volume: {e}",
3397                kline.open_time
3398            )
3399        })?;
3400    let taker_buy_quote_volume = kline
3401        .taker_buy_quote_volume
3402        .parse::<Decimal>()
3403        .map_err(|e| {
3404            anyhow::anyhow!(
3405                "invalid Futures kline {} taker buy quote volume: {e}",
3406                kline.open_time
3407            )
3408        })?;
3409    let count = u64::try_from(kline.num_trades).map_err(|_| {
3410        anyhow::anyhow!(
3411            "invalid Futures kline {} negative trade count",
3412            kline.open_time
3413        )
3414    })?;
3415
3416    Ok(BinanceBar::new(
3417        bar_type,
3418        open,
3419        high,
3420        low,
3421        close,
3422        volume,
3423        quote_volume,
3424        count,
3425        taker_buy_base_volume,
3426        taker_buy_quote_volume,
3427        ts_event,
3428        ts_init,
3429    ))
3430}
3431
3432fn parse_futures_funding_rate_update(
3433    rate: &BinanceFundingRate,
3434    instrument_id: InstrumentId,
3435    ts_init: UnixNanos,
3436) -> anyhow::Result<FundingRateUpdate> {
3437    let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
3438        anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
3439    })?;
3440    let ts_event = parse_millis(rate.funding_time, "Futures funding time")?;
3441
3442    Ok(FundingRateUpdate::new(
3443        instrument_id,
3444        funding_rate,
3445        None, // Funding interval is not provided by the history endpoint
3446        None, // Next funding time is not provided by the history endpoint
3447        ts_event,
3448        ts_init,
3449    ))
3450}
3451
3452fn parse_futures_commission_rates(
3453    response: &BinanceFuturesCommissionRate,
3454) -> anyhow::Result<(Decimal, Decimal)> {
3455    Ok((
3456        response.maker_commission_rate.parse()?,
3457        response.taker_commission_rate.parse()?,
3458    ))
3459}
3460
3461fn validate_reconciliation_instrument(
3462    instruments: &mut AHashMap<InstrumentId, InstrumentAny>,
3463    expected_id: InstrumentId,
3464    instrument: &InstrumentAny,
3465) -> BinanceFuturesHttpResult<()> {
3466    let parsed_id = instrument.id();
3467    if parsed_id != expected_id {
3468        return Err(BinanceFuturesHttpError::ValidationError(format!(
3469            "Parsed Binance Futures instrument ID {parsed_id} does not match expected ID {expected_id}"
3470        )));
3471    }
3472
3473    if instruments.insert(parsed_id, instrument.clone()).is_some() {
3474        return Err(BinanceFuturesHttpError::ValidationError(format!(
3475            "Duplicate parsed Binance Futures instrument ID {parsed_id}"
3476        )));
3477    }
3478
3479    Ok(())
3480}
3481
3482fn log_futures_instrument_parse_error(
3483    config: &BinanceInstrumentProviderConfig,
3484    selector: &BinanceInstrumentSelector,
3485    instrument_id: InstrumentId,
3486    symbol: &str,
3487    error: &anyhow::Error,
3488) {
3489    let explicit = selector.is_explicit(instrument_id, symbol);
3490    if should_warn_on_instrument_parse_error(config.log_warnings, explicit, error) {
3491        log::warn!("Skipping Binance Futures instrument {symbol}: {error}");
3492    } else {
3493        log::debug!("Skipping Binance Futures instrument {symbol}: {error}");
3494    }
3495}
3496
3497fn log_futures_commission_fallback(
3498    config: &BinanceInstrumentProviderConfig,
3499    symbol: &str,
3500    error: &dyn std::fmt::Display,
3501    fallback: (Decimal, Decimal),
3502) {
3503    if config.log_warnings {
3504        log::warn!(
3505            "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3506            fallback.0,
3507            fallback.1,
3508        );
3509    } else {
3510        log::debug!(
3511            "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3512            fallback.0,
3513            fallback.1,
3514        );
3515    }
3516}
3517
3518/// Checks if an order type requires the Binance Algo Service API.
3519///
3520/// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
3521/// The traditional `/fapi/v1/order` endpoint returns error `-4120` for these types.
3522#[must_use]
3523pub fn is_algo_order_type(order_type: OrderType) -> bool {
3524    matches!(
3525        order_type,
3526        OrderType::StopMarket
3527            | OrderType::StopLimit
3528            | OrderType::MarketIfTouched
3529            | OrderType::LimitIfTouched
3530            | OrderType::TrailingStopMarket
3531    )
3532}
3533
3534/// Converts a Nautilus order type to a Binance Futures order type.
3535pub(crate) fn order_type_to_binance_futures(
3536    order_type: OrderType,
3537) -> anyhow::Result<BinanceFuturesOrderType> {
3538    match order_type {
3539        OrderType::Market => Ok(BinanceFuturesOrderType::Market),
3540        OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
3541        OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
3542        OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
3543        OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
3544        OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
3545        OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
3546        _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
3547    }
3548}
3549
3550#[cfg(test)]
3551mod tests {
3552    use nautilus_core::time::get_atomic_clock_realtime;
3553    use nautilus_network::http::{HttpStatus, StatusCode};
3554    use nautilus_testkit::http::assert_http_redirect_rejected;
3555    use rstest::rstest;
3556    use tokio_util::bytes::Bytes;
3557
3558    use super::*;
3559    use crate::common::enums::BinanceTradingStatus;
3560
3561    #[tokio::test]
3562    async fn test_authenticated_client_rejects_redirects() {
3563        let client = BinanceRawFuturesHttpClient::new(
3564            BinanceProductType::UsdM,
3565            BinanceEnvironment::Testnet,
3566            Some("key".into()),
3567            Some("secret".into()),
3568            None,
3569            None,
3570            Some(3),
3571            None,
3572        )
3573        .unwrap()
3574        .client;
3575        assert_http_redirect_rejected(|url| async move {
3576            client
3577                .get(url, None, None, Some(3), None)
3578                .await
3579                .unwrap()
3580                .status
3581                .as_u16()
3582        })
3583        .await;
3584    }
3585
3586    #[rstest]
3587    fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
3588        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3589
3590        assert_eq!(config.request_quota.burst_size().get(), 2_400);
3591        assert_eq!(config.order_keys.len(), 2);
3592        assert!(
3593            config
3594                .order_keys
3595                .iter()
3596                .any(|key| key == "binance:orders:10:Second")
3597        );
3598        assert!(
3599            config
3600                .order_keys
3601                .iter()
3602                .any(|key| key == "binance:orders:1:Minute")
3603        );
3604
3605        let ten_second_quota = config
3606            .order_quotas
3607            .iter()
3608            .find(|(key, _)| key == "binance:orders:10:Second")
3609            .map(|(_, quota)| quota)
3610            .expect("USD-M 10-second order quota");
3611        assert_eq!(ten_second_quota.burst_size().get(), 300);
3612        assert_eq!(
3613            ten_second_quota.replenish_interval(),
3614            Duration::from_nanos(33_333_333)
3615        );
3616    }
3617
3618    #[rstest]
3619    fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
3620        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3621
3622        assert_eq!(config.request_quota.burst_size().get(), 2_400);
3623        assert_eq!(config.order_keys.len(), 2);
3624        assert!(
3625            config
3626                .order_keys
3627                .iter()
3628                .any(|key| key == "binance:orders:10:Second")
3629        );
3630        assert!(
3631            config
3632                .order_keys
3633                .iter()
3634                .any(|key| key == "binance:orders:1:Minute")
3635        );
3636
3637        let ten_second_quota = config
3638            .order_quotas
3639            .iter()
3640            .find(|(key, _)| key == "binance:orders:10:Second")
3641            .map(|(_, quota)| quota)
3642            .expect("COIN-M 10-second order quota");
3643        let minute_quota = config
3644            .order_quotas
3645            .iter()
3646            .find(|(key, _)| key == "binance:orders:1:Minute")
3647            .map(|(_, quota)| quota)
3648            .expect("COIN-M one-minute order quota");
3649
3650        assert_eq!(ten_second_quota.burst_size().get(), 300);
3651        assert_eq!(minute_quota.burst_size().get(), 1_200);
3652    }
3653
3654    #[rstest]
3655    fn test_rate_limiters_share_usdm_and_coinm_scopes() {
3656        let account = Some(BinanceFuturesAccountScope([1; 32]));
3657        let usdm_config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3658        let coinm_config =
3659            BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3660
3661        let usdm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3662            BinanceEnvironment::Live,
3663            None,
3664            None,
3665            account,
3666            usdm_config.request_quota,
3667            usdm_config.order_quotas,
3668        );
3669        let coinm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3670            BinanceEnvironment::Live,
3671            Some(get_http_base_url(
3672                BinanceProductType::CoinM,
3673                BinanceEnvironment::Live,
3674            )),
3675            None,
3676            account,
3677            coinm_config.request_quota,
3678            coinm_config.order_quotas,
3679        );
3680        let public = create_test_rate_limiters(BinanceEnvironment::Live, None, None, None);
3681
3682        assert_eq!(usdm.len(), 2);
3683        assert_eq!(coinm.len(), 2);
3684        assert_eq!(public.len(), 1);
3685        assert!(Arc::ptr_eq(&usdm[0], &coinm[0]));
3686        assert!(Arc::ptr_eq(&usdm[0], &public[0]));
3687        assert!(Arc::ptr_eq(&usdm[1], &coinm[1]));
3688    }
3689
3690    #[rstest]
3691    fn test_rate_limiters_isolate_unrelated_scopes() {
3692        let account_a = Some(BinanceFuturesAccountScope([2; 32]));
3693        let account_b = Some(BinanceFuturesAccountScope([3; 32]));
3694
3695        let live_direct =
3696            create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_a);
3697        let testnet_direct =
3698            create_test_rate_limiters(BinanceEnvironment::Testnet, None, None, account_a);
3699        let demo_direct =
3700            create_test_rate_limiters(BinanceEnvironment::Demo, None, None, account_a);
3701        let custom_a = create_test_rate_limiters(
3702            BinanceEnvironment::Live,
3703            Some("http://127.0.0.1:41001"),
3704            None,
3705            account_a,
3706        );
3707        let custom_b = create_test_rate_limiters(
3708            BinanceEnvironment::Live,
3709            Some("http://127.0.0.1:41002"),
3710            None,
3711            account_a,
3712        );
3713        let proxy_a = create_test_rate_limiters(
3714            BinanceEnvironment::Live,
3715            None,
3716            Some("http://127.0.0.1:42001"),
3717            account_a,
3718        );
3719        let proxy_b = create_test_rate_limiters(
3720            BinanceEnvironment::Live,
3721            None,
3722            Some("http://127.0.0.1:42002"),
3723            account_a,
3724        );
3725        let other_account =
3726            create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_b);
3727
3728        assert!(!Arc::ptr_eq(&live_direct[0], &testnet_direct[0]));
3729        assert!(!Arc::ptr_eq(&live_direct[0], &demo_direct[0]));
3730        assert!(!Arc::ptr_eq(&live_direct[0], &custom_a[0]));
3731        assert!(!Arc::ptr_eq(&custom_a[0], &custom_b[0]));
3732        assert!(!Arc::ptr_eq(&proxy_a[0], &proxy_b[0]));
3733        assert!(Arc::ptr_eq(&proxy_a[1], &proxy_b[1]));
3734        assert!(Arc::ptr_eq(&live_direct[0], &other_account[0]));
3735        assert!(!Arc::ptr_eq(&live_direct[1], &other_account[1]));
3736    }
3737
3738    fn create_test_rate_limiters(
3739        environment: BinanceEnvironment,
3740        base_url_override: Option<&str>,
3741        proxy_url: Option<&str>,
3742        account_scope: Option<BinanceFuturesAccountScope>,
3743    ) -> Vec<BinanceFuturesRateLimiter> {
3744        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3745        BinanceRawFuturesHttpClient::shared_rate_limiters(
3746            environment,
3747            base_url_override,
3748            proxy_url,
3749            account_scope,
3750            config.request_quota,
3751            config.order_quotas,
3752        )
3753    }
3754
3755    #[rstest]
3756    fn test_rate_limit_keys_usdm_include_order_buckets() {
3757        let client = BinanceRawFuturesHttpClient::new(
3758            BinanceProductType::UsdM,
3759            BinanceEnvironment::Live,
3760            None,
3761            None,
3762            None,
3763            None,
3764            None,
3765            None,
3766        )
3767        .unwrap();
3768
3769        assert_eq!(
3770            client.rate_limit_keys(false),
3771            vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
3772        );
3773        assert_eq!(
3774            client.rate_limit_keys(true),
3775            vec![
3776                BINANCE_GLOBAL_RATE_KEY.to_string(),
3777                "binance:orders:10:Second".to_string(),
3778                "binance:orders:1:Minute".to_string(),
3779            ]
3780        );
3781    }
3782
3783    #[rstest]
3784    fn test_quota_from_unknown_interval_returns_none() {
3785        let quota = BinanceRateLimitQuota {
3786            rate_limit_type: BinanceRateLimitType::Orders,
3787            interval: BinanceRateLimitInterval::Unknown,
3788            interval_num: 1,
3789            limit: 10,
3790        };
3791
3792        assert!(BinanceRawFuturesHttpClient::quota_from(&quota).is_none());
3793    }
3794
3795    #[rstest]
3796    fn test_create_client_rejects_spot_product_type() {
3797        let result = BinanceFuturesHttpClient::new(
3798            BinanceProductType::Spot,
3799            BinanceEnvironment::Live,
3800            get_atomic_clock_realtime(),
3801            None,
3802            None,
3803            None,
3804            None,
3805            None,
3806            None,
3807            false,
3808        );
3809
3810        result.unwrap_err();
3811    }
3812
3813    #[rstest]
3814    fn test_parse_futures_trade_tick_rejects_invalid_price() {
3815        let trade = BinanceFuturesTrade {
3816            id: 100,
3817            price: "not-a-number".to_string(),
3818            qty: "0.001".to_string(),
3819            quote_qty: "50.00".to_string(),
3820            time: 1_625_474_304_000,
3821            is_buyer_maker: false,
3822        };
3823
3824        let result = parse_futures_trade_tick(
3825            &trade,
3826            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3827            2,
3828            3,
3829            UnixNanos::from(1_000_000_000u64),
3830        );
3831
3832        let error = result.unwrap_err().to_string();
3833        assert!(error.contains("trade.price"));
3834        assert!(error.contains("100"));
3835    }
3836
3837    #[rstest]
3838    fn test_parse_futures_kline_bar_rejects_invalid_volume() {
3839        let kline = BinanceFuturesKline {
3840            open_time: 1_625_474_304_000,
3841            open: "50000.00".to_string(),
3842            high: "51000.00".to_string(),
3843            low: "49000.00".to_string(),
3844            close: "50500.00".to_string(),
3845            volume: "not-a-number".to_string(),
3846            close_time: 1_625_474_364_000,
3847            quote_volume: "631250.00".to_string(),
3848            num_trades: 100,
3849            taker_buy_base_volume: "6.2".to_string(),
3850            taker_buy_quote_volume: "313100.00".to_string(),
3851        };
3852
3853        let result = parse_futures_kline_binance_bar(
3854            &kline,
3855            BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
3856            2,
3857            3,
3858            UnixNanos::from(1_000_000_000u64),
3859        );
3860
3861        let error = result.unwrap_err().to_string();
3862        assert!(error.contains("kline.volume"));
3863        assert!(error.contains("1625474304000"));
3864    }
3865
3866    #[rstest]
3867    #[case::limit(
3868        BinanceAggTradesParams {
3869            symbol: "BTCUSDT".to_string(),
3870            from_id: None,
3871            start_time: None,
3872            end_time: None,
3873            limit: Some(1001),
3874        },
3875        "Validation error: aggregate trade limit must not exceed 1000"
3876    )]
3877    #[case::order(
3878        BinanceAggTradesParams {
3879            symbol: "BTCUSDT".to_string(),
3880            from_id: None,
3881            start_time: Some(2000),
3882            end_time: Some(1000),
3883            limit: Some(1000),
3884        },
3885        "Validation error: aggregate trade startTime must not exceed endTime"
3886    )]
3887    #[case::range(
3888        BinanceAggTradesParams {
3889            symbol: "BTCUSDT".to_string(),
3890            from_id: None,
3891            start_time: Some(1000),
3892            end_time: Some(3_601_000),
3893            limit: Some(1000),
3894        },
3895        "Validation error: aggregate trade time range must be less than one hour"
3896    )]
3897    #[case::overflow(
3898        BinanceAggTradesParams {
3899            symbol: "BTCUSDT".to_string(),
3900            from_id: None,
3901            start_time: Some(i64::MIN),
3902            end_time: Some(i64::MAX),
3903            limit: Some(1000),
3904        },
3905        "Validation error: aggregate trade time range must be less than one hour"
3906    )]
3907    #[tokio::test]
3908    async fn test_agg_trades_rejects_invalid_bounds(
3909        #[case] params: BinanceAggTradesParams,
3910        #[case] expected: &str,
3911    ) {
3912        let error = create_test_raw_client()
3913            .agg_trades(&params)
3914            .await
3915            .unwrap_err();
3916
3917        assert_eq!(error.to_string(), expected);
3918    }
3919
3920    #[rstest]
3921    #[case::start(true, false)]
3922    #[case::end(false, true)]
3923    #[case::both(true, true)]
3924    #[tokio::test]
3925    async fn test_request_agg_trades_rejects_history_older_than_24_hours(
3926        #[case] include_start: bool,
3927        #[case] include_end: bool,
3928    ) {
3929        let client = create_test_client();
3930        let start = Timestamp::now() - jiff::SignedDuration::from_hours(25);
3931        let end = start + jiff::SignedDuration::from_mins(30);
3932
3933        let error = client
3934            .request_agg_trades(
3935                InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3936                include_start.then_some(start),
3937                include_end.then_some(end),
3938                Some(1000),
3939            )
3940            .await
3941            .unwrap_err();
3942
3943        assert_eq!(
3944            error.to_string(),
3945            "Binance Futures aggregate trade history is limited to the past 24 hours"
3946        );
3947    }
3948
3949    fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
3950        BinanceRawFuturesHttpClient::new(
3951            BinanceProductType::UsdM,
3952            BinanceEnvironment::Live,
3953            None,
3954            None,
3955            None,
3956            None,
3957            None,
3958            None,
3959        )
3960        .expect("Failed to create test client")
3961    }
3962
3963    fn create_test_client() -> BinanceFuturesHttpClient {
3964        BinanceFuturesHttpClient::new(
3965            BinanceProductType::UsdM,
3966            BinanceEnvironment::Live,
3967            get_atomic_clock_realtime(),
3968            None,
3969            None,
3970            Some("http://127.0.0.1:1".to_string()),
3971            None,
3972            Some(1),
3973            None,
3974            false,
3975        )
3976        .expect("Failed to create test client")
3977    }
3978
3979    fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
3980        BinanceFuturesUsdSymbol {
3981            symbol: Ustr::from("BTCUSDT"),
3982            pair: Ustr::from("BTCUSDT"),
3983            contract_type: "PERPETUAL".to_string(),
3984            delivery_date: 4_133_404_800_000,
3985            onboard_date: 1_569_398_400_000,
3986            status: BinanceTradingStatus::Trading,
3987            maint_margin_percent: "2.5000".to_string(),
3988            required_margin_percent: "5.0000".to_string(),
3989            base_asset: Ustr::from("BTC"),
3990            quote_asset: Ustr::from("USDT"),
3991            margin_asset: Ustr::from("USDT"),
3992            price_precision: 2,
3993            quantity_precision: 3,
3994            base_asset_precision: 8,
3995            quote_precision: 8,
3996            underlying_type: None,
3997            underlying_sub_type: Vec::new(),
3998            settle_plan: None,
3999            trigger_protect: None,
4000            liquidation_fee: None,
4001            market_take_bound: None,
4002            order_types: Vec::new(),
4003            time_in_force: Vec::new(),
4004            filters: Vec::new(),
4005        }
4006    }
4007
4008    #[rstest]
4009    fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
4010        let client = create_test_client();
4011        client.instruments_cache().insert(
4012            Ustr::from("BTCUSDT"),
4013            BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4014        );
4015
4016        let (symbol, price_precision, size_precision) = client
4017            .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
4018            .unwrap();
4019
4020        assert_eq!(symbol, "BTCUSDT");
4021        assert_eq!(price_precision, 2);
4022        assert_eq!(size_precision, 3);
4023    }
4024
4025    #[rstest]
4026    fn test_cached_precisions_by_id_rejects_spot_alias() {
4027        let client = create_test_client();
4028        client.instruments_cache().insert(
4029            Ustr::from("BTCUSDT"),
4030            BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4031        );
4032
4033        let error = client
4034            .cached_precisions_by_id(InstrumentId::from("BTCUSDT.BINANCE"))
4035            .unwrap_err();
4036
4037        assert_eq!(
4038            error.to_string(),
4039            InstrumentLookupError::not_found(InstrumentId::from("BTCUSDT.BINANCE")).to_string()
4040        );
4041    }
4042
4043    #[rstest]
4044    fn test_invalid_precision_preserves_previous_raw_snapshot() {
4045        let client = create_test_client();
4046        let valid = test_usdm_symbol();
4047        client
4048            .replace_instruments(vec![(
4049                valid.symbol,
4050                BinanceFuturesInstrument::UsdM(valid.clone()),
4051            )])
4052            .unwrap();
4053        let mut invalid = valid;
4054        invalid.price_precision = i32::from(FIXED_PRECISION) + 1;
4055
4056        let error = client
4057            .replace_instruments(vec![(
4058                invalid.symbol,
4059                BinanceFuturesInstrument::UsdM(invalid),
4060            )])
4061            .unwrap_err();
4062        let retained = client
4063            .instruments_cache()
4064            .get(&Ustr::from("BTCUSDT"))
4065            .map(|instrument| instrument.value().clone())
4066            .unwrap();
4067
4068        assert!(error.to_string().contains("precision exceeds maximum"));
4069        assert_eq!(retained.precisions().unwrap(), (2, 3));
4070    }
4071
4072    #[rstest]
4073    #[tokio::test]
4074    async fn test_submit_algo_order_stop_market_requires_trigger_price() {
4075        let client = create_test_client();
4076        client.instruments_cache().insert(
4077            Ustr::from("BTCUSDT"),
4078            BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4079        );
4080
4081        let result = client
4082            .submit_algo_order(
4083                AccountId::from("BINANCE-001"),
4084                InstrumentId::from("BTCUSDT-PERP.BINANCE"),
4085                ClientOrderId::new("missing-trigger-test-001"),
4086                OrderSide::Sell,
4087                OrderType::StopMarket,
4088                Quantity::from("0.001"),
4089                TimeInForce::Gtc,
4090                None,
4091                None,
4092                false,
4093                false,
4094                None,
4095                None,
4096                None,
4097                None,
4098                None,
4099            )
4100            .await;
4101
4102        let error = result.unwrap_err().to_string();
4103        assert_eq!(error, "Algo order type StopMarket requires a trigger price");
4104    }
4105
4106    #[rstest]
4107    fn test_batch_cancel_params_builds_order_id_list() {
4108        let items = vec![
4109            BatchCancelItem::by_order_id("BTCUSDT", 123),
4110            BatchCancelItem::by_order_id("BTCUSDT", 456),
4111        ];
4112
4113        let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4114
4115        assert_eq!(params.symbol, "BTCUSDT");
4116        assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
4117        assert_eq!(params.orig_client_order_id_list, None);
4118    }
4119
4120    #[rstest]
4121    fn test_batch_cancel_params_builds_client_order_id_list() {
4122        let items = vec![
4123            BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
4124            BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
4125        ];
4126
4127        let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4128
4129        assert_eq!(params.symbol, "BTCUSDT");
4130        assert_eq!(params.order_id_list, None);
4131        assert_eq!(
4132            params.orig_client_order_id_list.as_deref(),
4133            Some("[\"first-order\",\"second-order\"]"),
4134        );
4135    }
4136
4137    #[rstest]
4138    fn test_batch_cancel_params_rejects_mixed_symbols() {
4139        let items = vec![
4140            BatchCancelItem::by_order_id("BTCUSDT", 123),
4141            BatchCancelItem::by_order_id("ETHUSDT", 456),
4142        ];
4143
4144        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4145
4146        assert_validation_error(result, "same symbol");
4147    }
4148
4149    #[rstest]
4150    fn test_batch_cancel_params_rejects_mixed_id_types() {
4151        let items = vec![
4152            BatchCancelItem::by_order_id("BTCUSDT", 123),
4153            BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
4154        ];
4155
4156        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4157
4158        assert_validation_error(result, "not both");
4159    }
4160
4161    #[rstest]
4162    fn test_batch_cancel_params_rejects_items_without_ids() {
4163        let items = vec![BatchCancelItem {
4164            symbol: "BTCUSDT".to_string(),
4165            order_id: None,
4166            orig_client_order_id: None,
4167        }];
4168
4169        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4170
4171        assert_validation_error(result, "at least one order ID or client order ID");
4172    }
4173
4174    #[rstest]
4175    #[tokio::test]
4176    async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
4177        let client = create_test_raw_client();
4178        let items = (0..11)
4179            .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
4180            .collect::<Vec<_>>();
4181
4182        let result = client.batch_cancel_orders(&items).await;
4183
4184        match result {
4185            Err(BinanceFuturesHttpError::ValidationError(message)) => {
4186                assert!(message.contains("10 orders maximum"));
4187            }
4188            other => panic!("Expected ValidationError, was {other:?}"),
4189        }
4190    }
4191
4192    fn assert_validation_error(
4193        result: BinanceFuturesHttpResult<BatchCancelParams>,
4194        expected_message: &str,
4195    ) {
4196        match result {
4197            Err(BinanceFuturesHttpError::ValidationError(message)) => {
4198                assert!(message.contains(expected_message));
4199            }
4200            other => panic!("Expected ValidationError, was {other:?}"),
4201        }
4202    }
4203
4204    #[rstest]
4205    fn test_parse_error_response_binance_error() {
4206        let client = create_test_raw_client();
4207        let response = HttpResponse {
4208            status: HttpStatus::new(StatusCode::BAD_REQUEST),
4209            headers: HashMap::new(),
4210            body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
4211        };
4212
4213        let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
4214
4215        match result {
4216            Err(BinanceFuturesHttpError::BinanceError {
4217                code,
4218                message,
4219                status,
4220                retry_after,
4221            }) => {
4222                assert_eq!(code, -1121);
4223                assert_eq!(message, "Invalid symbol.");
4224                assert_eq!(status, 400);
4225                assert_eq!(retry_after, None);
4226            }
4227            other => panic!("Expected BinanceError, was {other:?}"),
4228        }
4229    }
4230}