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