Skip to main content

nautilus_binance/futures/http/
client.rs

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