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