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::{collections::HashMap, num::NonZeroU32, sync::Arc, time::Duration};
19
20use ahash::AHashMap;
21use chrono::{DateTime, Utc};
22use dashmap::DashMap;
23use nautilus_common::cache::InstrumentLookupError;
24use nautilus_core::{
25    consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime,
26};
27use nautilus_model::{
28    data::{Bar, BarType, FundingRateUpdate, TradeTick},
29    enums::{
30        AggregationSource, AggressorSide, BarAggregation, MarketStatusAction, OrderSide, OrderType,
31        TimeInForce,
32    },
33    events::AccountState,
34    identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
35    instruments::any::InstrumentAny,
36    reports::{FillReport, OrderStatusReport},
37    types::{Currency, Price, Quantity},
38};
39use nautilus_network::{
40    http::{HttpClient, HttpResponse, Method, USER_AGENT},
41    ratelimiter::quota::Quota,
42};
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize, de::DeserializeOwned};
45use ustr::Ustr;
46
47use super::{
48    error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
49    models::{
50        BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
51        BinanceFuturesAccountInfo, BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
52        BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesKline,
53        BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
54        BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
55        BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest,
56        BinanceOpenInterestHistRecord, BinanceOrderBook, BinancePositionRisk, BinancePriceTicker,
57        BinanceServerTime, BinanceUserTrade, ListenKeyResponse,
58    },
59    query::{
60        BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAlgoOrderQueryParams,
61        BinanceAllAlgoOrdersParams, BinanceAllOrdersParams, BinanceBookTickerParams,
62        BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams, BinanceCancelOrderParams,
63        BinanceDepthParams, BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
64        BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
65        BinanceOpenAlgoOrdersParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams,
66        BinanceOpenOrdersParams, BinanceOrderQueryParams, BinancePositionRiskParams,
67        BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceTicker24hrParams,
68        BinanceTradesParams, BinanceUserTradesParams, ListenKeyParams,
69    },
70};
71use crate::{
72    common::{
73        consts::{
74            BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
75            BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID, BinanceRateLimitQuota,
76        },
77        credential::SigningCredential,
78        encoder::encode_broker_id,
79        enums::{
80            BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
81            BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
82            BinanceSide, BinanceTimeInForce, BinanceWorkingType,
83        },
84        models::BinanceErrorResponse,
85        parse::{
86            parse_coinm_instrument, parse_required_price_at_precision,
87            parse_required_quantity_at_precision, parse_usdm_instrument,
88        },
89        symbol::{format_binance_symbol, format_instrument_id},
90        urls::get_http_base_url,
91    },
92    futures::conversions::reduce_only_param,
93};
94
95const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
96const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
97
98#[derive(Debug, Serialize)]
99#[serde(rename_all = "camelCase")]
100struct BatchCancelParams {
101    symbol: String,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    order_id_list: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    orig_client_order_id_list: Option<String>,
106}
107
108/// Raw HTTP client for Binance Futures REST API.
109#[derive(Debug, Clone)]
110pub struct BinanceRawFuturesHttpClient {
111    client: HttpClient,
112    base_url: String,
113    api_path: &'static str,
114    credential: Option<SigningCredential>,
115    recv_window: Option<u64>,
116    order_rate_keys: Vec<String>,
117}
118
119impl BinanceRawFuturesHttpClient {
120    /// Returns a reference to the underlying HTTP client.
121    #[must_use]
122    pub fn http_client(&self) -> &HttpClient {
123        &self.client
124    }
125
126    /// Creates a new Binance raw futures HTTP client.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if credentials are incomplete or the HTTP client fails to build.
131    #[expect(clippy::too_many_arguments)]
132    pub fn new(
133        product_type: BinanceProductType,
134        environment: BinanceEnvironment,
135        api_key: Option<String>,
136        api_secret: Option<String>,
137        base_url_override: Option<String>,
138        recv_window: Option<u64>,
139        timeout_secs: Option<u64>,
140        proxy_url: Option<String>,
141    ) -> BinanceFuturesHttpResult<Self> {
142        let RateLimitConfig {
143            default_quota,
144            keyed_quotas,
145            order_keys,
146        } = Self::rate_limit_config(product_type);
147
148        let credential = match (api_key, api_secret) {
149            (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
150            (None, None) => None,
151            _ => return Err(BinanceFuturesHttpError::MissingCredentials),
152        };
153
154        let base_url = base_url_override
155            .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
156
157        let api_path = Self::resolve_api_path(product_type);
158        let headers = Self::default_headers(&credential);
159
160        let client = HttpClient::new(
161            headers,
162            vec![BINANCE_API_KEY_HEADER.to_string()],
163            keyed_quotas,
164            default_quota,
165            timeout_secs,
166            proxy_url,
167        )?;
168
169        Ok(Self {
170            client,
171            base_url,
172            api_path,
173            credential,
174            recv_window,
175            order_rate_keys: order_keys,
176        })
177    }
178
179    /// Performs a GET request and deserializes the response body.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the request fails or response deserialization fails.
184    pub async fn get<P, T>(
185        &self,
186        path: &str,
187        params: Option<&P>,
188        signed: bool,
189        use_order_quota: bool,
190    ) -> BinanceFuturesHttpResult<T>
191    where
192        P: Serialize + ?Sized,
193        T: DeserializeOwned,
194    {
195        self.request(Method::GET, path, params, signed, use_order_quota, None)
196            .await
197    }
198
199    /// Performs a POST request with optional body and signed query.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the request fails or response deserialization fails.
204    pub async fn post<P, T>(
205        &self,
206        path: &str,
207        params: Option<&P>,
208        body: Option<Vec<u8>>,
209        signed: bool,
210        use_order_quota: bool,
211    ) -> BinanceFuturesHttpResult<T>
212    where
213        P: Serialize + ?Sized,
214        T: DeserializeOwned,
215    {
216        self.request(Method::POST, path, params, signed, use_order_quota, body)
217            .await
218    }
219
220    /// Performs a PUT request with signed query.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the request fails or response deserialization fails.
225    pub async fn request_put<P, T>(
226        &self,
227        path: &str,
228        params: Option<&P>,
229        signed: bool,
230        use_order_quota: bool,
231    ) -> BinanceFuturesHttpResult<T>
232    where
233        P: Serialize + ?Sized,
234        T: DeserializeOwned,
235    {
236        self.request(Method::PUT, path, params, signed, use_order_quota, None)
237            .await
238    }
239
240    /// Performs a DELETE request with signed query.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if the request fails or response deserialization fails.
245    pub async fn request_delete<P, T>(
246        &self,
247        path: &str,
248        params: Option<&P>,
249        signed: bool,
250        use_order_quota: bool,
251    ) -> BinanceFuturesHttpResult<T>
252    where
253        P: Serialize + ?Sized,
254        T: DeserializeOwned,
255    {
256        self.request(Method::DELETE, path, params, signed, use_order_quota, None)
257            .await
258    }
259
260    /// Performs a batch POST request with batchOrders parameter.
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
265    pub async fn batch_request<T: Serialize>(
266        &self,
267        path: &str,
268        items: &[T],
269        use_order_quota: bool,
270    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
271        self.batch_request_method(Method::POST, path, items, use_order_quota)
272            .await
273    }
274
275    /// Performs a batch DELETE request with batchOrders parameter.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
280    pub async fn batch_request_delete<T: Serialize>(
281        &self,
282        path: &str,
283        items: &[T],
284        use_order_quota: bool,
285    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
286        self.batch_request_method(Method::DELETE, path, items, use_order_quota)
287            .await
288    }
289
290    /// Performs a batch PUT request with batchOrders parameter.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
295    pub async fn batch_request_put<T: Serialize>(
296        &self,
297        path: &str,
298        items: &[T],
299        use_order_quota: bool,
300    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
301        self.batch_request_method(Method::PUT, path, items, use_order_quota)
302            .await
303    }
304
305    async fn batch_request_method<T: Serialize>(
306        &self,
307        method: Method,
308        path: &str,
309        items: &[T],
310        use_order_quota: bool,
311    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
312        let cred = self
313            .credential
314            .as_ref()
315            .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
316
317        let batch_json = serde_json::to_string(items)
318            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
319
320        let encoded_batch = Self::percent_encode(&batch_json);
321        let timestamp = Utc::now().timestamp_millis();
322        let mut query = format!("batchOrders={encoded_batch}&timestamp={timestamp}");
323
324        if let Some(recv_window) = self.recv_window {
325            query.push_str(&format!("&recvWindow={recv_window}"));
326        }
327
328        let signature = Self::percent_encode(&cred.sign(&query));
329        query.push_str(&format!("&signature={signature}"));
330
331        let url = self.build_url(path, &query);
332
333        let mut headers = HashMap::new();
334        headers.insert(
335            BINANCE_API_KEY_HEADER.to_string(),
336            cred.api_key().to_string(),
337        );
338
339        let keys = self.rate_limit_keys(use_order_quota);
340
341        let response = self
342            .client
343            .request(
344                method,
345                url,
346                None::<&HashMap<String, Vec<String>>>,
347                Some(headers),
348                None,
349                None,
350                Some(keys),
351            )
352            .await?;
353
354        if !response.status.is_success() {
355            return self.parse_error_response(&response);
356        }
357
358        serde_json::from_slice(&response.body)
359            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
360    }
361
362    /// Percent-encodes a string for use in URL query parameters.
363    fn percent_encode(input: &str) -> String {
364        let mut result = String::with_capacity(input.len() * 3);
365        for byte in input.bytes() {
366            match byte {
367                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
368                    result.push(byte as char);
369                }
370                _ => {
371                    result.push('%');
372                    result.push_str(&format!("{byte:02X}"));
373                }
374            }
375        }
376        result
377    }
378
379    async fn request<P, T>(
380        &self,
381        method: Method,
382        path: &str,
383        params: Option<&P>,
384        signed: bool,
385        use_order_quota: bool,
386        body: Option<Vec<u8>>,
387    ) -> BinanceFuturesHttpResult<T>
388    where
389        P: Serialize + ?Sized,
390        T: DeserializeOwned,
391    {
392        let mut query = params
393            .map(serde_urlencoded::to_string)
394            .transpose()
395            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
396            .unwrap_or_default();
397
398        let mut headers = HashMap::new();
399
400        if signed {
401            let cred = self
402                .credential
403                .as_ref()
404                .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
405
406            if !query.is_empty() {
407                query.push('&');
408            }
409
410            let timestamp = Utc::now().timestamp_millis();
411            query.push_str(&format!("timestamp={timestamp}"));
412
413            if let Some(recv_window) = self.recv_window {
414                query.push_str(&format!("&recvWindow={recv_window}"));
415            }
416
417            // Percent-encode the signature: Ed25519 signatures are base64 and
418            // contain `+`, `/`, `=` which are not URL-safe. HMAC hex is
419            // already safe but percent-encoding it is a no-op.
420            let signature = Self::percent_encode(&cred.sign(&query));
421            query.push_str(&format!("&signature={signature}"));
422            headers.insert(
423                BINANCE_API_KEY_HEADER.to_string(),
424                cred.api_key().to_string(),
425            );
426        }
427
428        let url = self.build_url(path, &query);
429        let keys = self.rate_limit_keys(use_order_quota);
430
431        let response = self
432            .client
433            .request(
434                method,
435                url,
436                None::<&HashMap<String, Vec<String>>>,
437                Some(headers),
438                body,
439                None,
440                Some(keys),
441            )
442            .await?;
443
444        if !response.status.is_success() {
445            return self.parse_error_response(&response);
446        }
447
448        serde_json::from_slice::<T>(&response.body)
449            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
450    }
451
452    fn build_url(&self, path: &str, query: &str) -> String {
453        // Full API paths (e.g., /fapi/v2/account) bypass the default api_path
454        let url_path = if path.starts_with("/fapi/")
455            || path.starts_with("/dapi/")
456            || path.starts_with("/futures/data/")
457        {
458            path.to_string()
459        } else if path.starts_with('/') {
460            format!("{}{}", self.api_path, path)
461        } else {
462            format!("{}/{}", self.api_path, path)
463        };
464
465        let mut url = format!("{}{}", self.base_url, url_path);
466
467        if !query.is_empty() {
468            url.push('?');
469            url.push_str(query);
470        }
471        url
472    }
473
474    fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
475        if use_orders {
476            let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
477            keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
478            keys.extend(self.order_rate_keys.iter().cloned());
479            keys
480        } else {
481            vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
482        }
483    }
484
485    fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
486        let status = response.status.as_u16();
487        let body = String::from_utf8_lossy(&response.body).to_string();
488
489        if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
490            return Err(BinanceFuturesHttpError::BinanceError {
491                code: err.code,
492                message: err.msg,
493            });
494        }
495
496        Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
497    }
498
499    fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
500        let mut headers = HashMap::new();
501        headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
502
503        if let Some(cred) = credential {
504            headers.insert(
505                BINANCE_API_KEY_HEADER.to_string(),
506                cred.api_key().to_string(),
507            );
508        }
509        headers
510    }
511
512    fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
513        match product_type {
514            BinanceProductType::UsdM => BINANCE_FAPI_PATH,
515            BinanceProductType::CoinM => BINANCE_DAPI_PATH,
516            _ => BINANCE_FAPI_PATH, // Default to USD-M
517        }
518    }
519
520    fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
521        let quotas = match product_type {
522            BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
523            BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
524            _ => BINANCE_FAPI_RATE_LIMITS,
525        };
526
527        let mut keyed = Vec::new();
528        let mut order_keys = Vec::new();
529        let mut default = None;
530
531        for quota in quotas {
532            if let Some(q) = Self::quota_from(quota) {
533                match quota.rate_limit_type {
534                    BinanceRateLimitType::RequestWeight if default.is_none() => {
535                        default = Some(q);
536                    }
537                    BinanceRateLimitType::Orders => {
538                        let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
539                        order_keys.push(key.clone());
540                        keyed.push((key, q));
541                    }
542                    _ => {}
543                }
544            }
545        }
546
547        let default_quota = default.unwrap_or_else(|| {
548            Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
549        });
550
551        keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
552
553        RateLimitConfig {
554            default_quota: Some(default_quota),
555            keyed_quotas: keyed,
556            order_keys,
557        }
558    }
559
560    fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
561        let burst = NonZeroU32::new(quota.limit)?;
562        match quota.interval {
563            BinanceRateLimitInterval::Second => Quota::per_second(burst),
564            BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
565            BinanceRateLimitInterval::Day => {
566                Quota::with_period(Duration::from_secs(SECONDS_IN_DAY))
567                    .map(|q| q.allow_burst(burst))
568            }
569            BinanceRateLimitInterval::Unknown => None,
570        }
571    }
572
573    /// Fetches 24hr ticker statistics.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if the request fails.
578    pub async fn ticker_24h(
579        &self,
580        params: &BinanceTicker24hrParams,
581    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
582        self.get("ticker/24hr", Some(params), false, false).await
583    }
584
585    /// Fetches best bid/ask prices.
586    ///
587    /// # Errors
588    ///
589    /// Returns an error if the request fails.
590    pub async fn book_ticker(
591        &self,
592        params: &BinanceBookTickerParams,
593    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
594        self.get("ticker/bookTicker", Some(params), false, false)
595            .await
596    }
597
598    /// Fetches price ticker.
599    ///
600    /// # Errors
601    ///
602    /// Returns an error if the request fails.
603    pub async fn price_ticker(
604        &self,
605        symbol: Option<&str>,
606    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
607        #[derive(Serialize)]
608        struct Params<'a> {
609            #[serde(skip_serializing_if = "Option::is_none")]
610            symbol: Option<&'a str>,
611        }
612        self.get("ticker/price", Some(&Params { symbol }), false, false)
613            .await
614    }
615
616    /// Fetches order book depth.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if the request fails.
621    pub async fn depth(
622        &self,
623        params: &BinanceDepthParams,
624    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
625        self.get("depth", Some(params), false, false).await
626    }
627
628    /// Fetches mark price and funding rate.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if the request fails.
633    pub async fn mark_price(
634        &self,
635        params: &BinanceMarkPriceParams,
636    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
637        let response: MarkPriceResponse =
638            self.get("premiumIndex", Some(params), false, false).await?;
639        Ok(response.into())
640    }
641
642    /// Fetches funding rate history.
643    ///
644    /// # Errors
645    ///
646    /// Returns an error if the request fails.
647    pub async fn funding_rate(
648        &self,
649        params: &BinanceFundingRateParams,
650    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
651        self.get("fundingRate", Some(params), false, false).await
652    }
653
654    /// Fetches current open interest for a symbol.
655    ///
656    /// # Errors
657    ///
658    /// Returns an error if the request fails.
659    pub async fn open_interest(
660        &self,
661        params: &BinanceOpenInterestParams,
662    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
663        self.get("openInterest", Some(params), false, false).await
664    }
665
666    /// Fetches historical open interest statistics for a symbol or pair.
667    ///
668    /// # Errors
669    ///
670    /// Returns an error if the request fails.
671    pub async fn open_interest_hist(
672        &self,
673        params: &BinanceOpenInterestHistParams,
674    ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
675        self.get("/futures/data/openInterestHist", Some(params), false, false)
676            .await
677    }
678
679    /// Fetches recent public trades for a symbol.
680    ///
681    /// # Errors
682    ///
683    /// Returns an error if the request fails.
684    pub async fn trades(
685        &self,
686        params: &BinanceTradesParams,
687    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
688        self.get("trades", Some(params), false, false).await
689    }
690
691    /// Fetches kline/candlestick data for a symbol.
692    ///
693    /// # Errors
694    ///
695    /// Returns an error if the request fails.
696    pub async fn klines(
697        &self,
698        params: &BinanceKlinesParams,
699    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
700        self.get("klines", Some(params), false, false).await
701    }
702
703    /// Sets leverage for a symbol.
704    ///
705    /// # Errors
706    ///
707    /// Returns an error if the request fails.
708    pub async fn set_leverage(
709        &self,
710        params: &BinanceSetLeverageParams,
711    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
712        self.post("leverage", Some(params), None, true, false).await
713    }
714
715    /// Sets margin type for a symbol.
716    ///
717    /// # Errors
718    ///
719    /// Returns an error if the request fails.
720    pub async fn set_margin_type(
721        &self,
722        params: &BinanceSetMarginTypeParams,
723    ) -> BinanceFuturesHttpResult<serde_json::Value> {
724        self.post("marginType", Some(params), None, true, false)
725            .await
726    }
727
728    /// Queries hedge mode (dual side position) setting.
729    ///
730    /// # Errors
731    ///
732    /// Returns an error if the request fails.
733    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
734        self.get::<(), _>("positionSide/dual", None, true, false)
735            .await
736    }
737
738    /// Creates a listen key for user data stream.
739    ///
740    /// # Errors
741    ///
742    /// Returns an error if the request fails.
743    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
744        self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
745            .await
746    }
747
748    /// Keeps alive an existing listen key.
749    ///
750    /// # Errors
751    ///
752    /// Returns an error if the request fails.
753    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
754        let params = ListenKeyParams {
755            listen_key: listen_key.to_string(),
756        };
757        let _: serde_json::Value = self
758            .request_put("listenKey", Some(&params), true, false)
759            .await?;
760        Ok(())
761    }
762
763    /// Closes an existing listen key.
764    ///
765    /// # Errors
766    ///
767    /// Returns an error if the request fails.
768    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
769        let params = ListenKeyParams {
770            listen_key: listen_key.to_string(),
771        };
772        let _: serde_json::Value = self
773            .request_delete("listenKey", Some(&params), true, false)
774            .await?;
775        Ok(())
776    }
777
778    /// Fetches account information including balances and positions.
779    ///
780    /// # Errors
781    ///
782    /// Returns an error if the request fails.
783    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
784        // USD-M uses /fapi/v2/account, COIN-M uses /dapi/v1/account
785        let path = if self.api_path.starts_with("/fapi") {
786            "/fapi/v2/account"
787        } else {
788            "/dapi/v1/account"
789        };
790        self.get::<(), _>(path, None, true, false).await
791    }
792
793    /// Fetches position risk information.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if the request fails.
798    pub async fn query_positions(
799        &self,
800        params: &BinancePositionRiskParams,
801    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
802        // USD-M uses /fapi/v2/positionRisk, COIN-M uses /dapi/v1/positionRisk
803        let path = if self.api_path.starts_with("/fapi") {
804            "/fapi/v2/positionRisk"
805        } else {
806            "/dapi/v1/positionRisk"
807        };
808        self.get(path, Some(params), true, false).await
809    }
810
811    /// Fetches user trades for a symbol.
812    ///
813    /// # Errors
814    ///
815    /// Returns an error if the request fails.
816    pub async fn query_user_trades(
817        &self,
818        params: &BinanceUserTradesParams,
819    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
820        self.get("userTrades", Some(params), true, false).await
821    }
822
823    /// Queries a single order by order ID or client order ID.
824    ///
825    /// # Errors
826    ///
827    /// Returns an error if the request fails.
828    pub async fn query_order(
829        &self,
830        params: &BinanceOrderQueryParams,
831    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
832        self.get("order", Some(params), true, false).await
833    }
834
835    /// Queries all open orders.
836    ///
837    /// # Errors
838    ///
839    /// Returns an error if the request fails.
840    pub async fn query_open_orders(
841        &self,
842        params: &BinanceOpenOrdersParams,
843    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
844        self.get("openOrders", Some(params), true, false).await
845    }
846
847    /// Queries all orders (including historical).
848    ///
849    /// # Errors
850    ///
851    /// Returns an error if the request fails.
852    pub async fn query_all_orders(
853        &self,
854        params: &BinanceAllOrdersParams,
855    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
856        self.get("allOrders", Some(params), true, false).await
857    }
858
859    /// Submits a new order.
860    ///
861    /// # Errors
862    ///
863    /// Returns an error if the request fails.
864    pub async fn submit_order(
865        &self,
866        params: &BinanceNewOrderParams,
867    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
868        self.post("order", Some(params), None, true, true).await
869    }
870
871    /// Submits multiple orders in a single request (up to 5 orders).
872    ///
873    /// # Errors
874    ///
875    /// Returns an error if the batch exceeds 5 orders or the request fails.
876    pub async fn submit_order_list(
877        &self,
878        orders: &[BatchOrderItem],
879    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
880        if orders.is_empty() {
881            return Ok(Vec::new());
882        }
883
884        if orders.len() > 5 {
885            return Err(BinanceFuturesHttpError::ValidationError(
886                "Batch order limit is 5 orders maximum".to_string(),
887            ));
888        }
889
890        self.batch_request("batchOrders", orders, true).await
891    }
892
893    /// Modifies an existing order (price and quantity only).
894    ///
895    /// # Errors
896    ///
897    /// Returns an error if the request fails.
898    pub async fn modify_order(
899        &self,
900        params: &BinanceModifyOrderParams,
901    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
902        self.request_put("order", Some(params), true, true).await
903    }
904
905    /// Modifies multiple orders in a single request (up to 5 orders).
906    ///
907    /// # Errors
908    ///
909    /// Returns an error if the batch exceeds 5 orders or the request fails.
910    pub async fn batch_modify_orders(
911        &self,
912        modifies: &[BatchModifyItem],
913    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
914        if modifies.is_empty() {
915            return Ok(Vec::new());
916        }
917
918        if modifies.len() > 5 {
919            return Err(BinanceFuturesHttpError::ValidationError(
920                "Batch modify limit is 5 orders maximum".to_string(),
921            ));
922        }
923
924        self.batch_request_put("batchOrders", modifies, true).await
925    }
926
927    /// Cancels an existing order.
928    ///
929    /// # Errors
930    ///
931    /// Returns an error if the request fails.
932    pub async fn cancel_order(
933        &self,
934        params: &BinanceCancelOrderParams,
935    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
936        self.request_delete("order", Some(params), true, true).await
937    }
938
939    /// Cancels all open orders for a symbol.
940    ///
941    /// # Errors
942    ///
943    /// Returns an error if the request fails.
944    pub async fn cancel_all_orders(
945        &self,
946        params: &BinanceCancelAllOrdersParams,
947    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
948        self.request_delete("allOpenOrders", Some(params), true, true)
949            .await
950    }
951
952    /// Cancels multiple orders in a single request (up to 10 orders).
953    ///
954    /// # Errors
955    ///
956    /// Returns an error if the batch exceeds 10 orders or the request fails.
957    pub async fn batch_cancel_orders(
958        &self,
959        cancels: &[BatchCancelItem],
960    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
961        if cancels.is_empty() {
962            return Ok(Vec::new());
963        }
964
965        if cancels.len() > 10 {
966            return Err(BinanceFuturesHttpError::ValidationError(
967                "Batch cancel limit is 10 orders maximum".to_string(),
968            ));
969        }
970
971        let params = Self::batch_cancel_params(cancels)?;
972        self.request_delete("batchOrders", Some(&params), true, true)
973            .await
974    }
975
976    fn batch_cancel_params(
977        cancels: &[BatchCancelItem],
978    ) -> BinanceFuturesHttpResult<BatchCancelParams> {
979        let symbol = cancels[0].symbol.clone();
980        let mut order_ids = Vec::new();
981        let mut client_order_ids = Vec::new();
982
983        for cancel in cancels {
984            if cancel.symbol != symbol {
985                return Err(BinanceFuturesHttpError::ValidationError(
986                    "Batch cancel orders must use the same symbol".to_string(),
987                ));
988            }
989
990            if let Some(order_id) = cancel.order_id {
991                order_ids.push(order_id);
992            }
993
994            if let Some(client_order_id) = &cancel.orig_client_order_id {
995                client_order_ids.push(client_order_id.clone());
996            }
997        }
998
999        if order_ids.is_empty() && client_order_ids.is_empty() {
1000            return Err(BinanceFuturesHttpError::ValidationError(
1001                "Batch cancel requires at least one order ID or client order ID".to_string(),
1002            ));
1003        }
1004
1005        if !order_ids.is_empty() && !client_order_ids.is_empty() {
1006            return Err(BinanceFuturesHttpError::ValidationError(
1007                "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1008            ));
1009        }
1010
1011        let order_id_list = if order_ids.is_empty() {
1012            None
1013        } else {
1014            Some(
1015                serde_json::to_string(&order_ids)
1016                    .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1017            )
1018        };
1019        let orig_client_order_id_list = if client_order_ids.is_empty() {
1020            None
1021        } else {
1022            Some(
1023                serde_json::to_string(&client_order_ids)
1024                    .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1025            )
1026        };
1027
1028        Ok(BatchCancelParams {
1029            symbol,
1030            order_id_list,
1031            orig_client_order_id_list,
1032        })
1033    }
1034
1035    /// Submits a new algo order (conditional order).
1036    ///
1037    /// Algo orders include STOP_MARKET, STOP (stop-limit), TAKE_PROFIT, TAKE_PROFIT_MARKET,
1038    /// and TRAILING_STOP_MARKET order types.
1039    ///
1040    /// # Errors
1041    ///
1042    /// Returns an error if the request fails.
1043    pub async fn submit_algo_order(
1044        &self,
1045        params: &BinanceNewAlgoOrderParams,
1046    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1047        self.post("algoOrder", Some(params), None, true, true).await
1048    }
1049
1050    /// Cancels an algo order.
1051    ///
1052    /// Must provide either `algo_id` or `client_algo_id`.
1053    ///
1054    /// # Errors
1055    ///
1056    /// Returns an error if the request fails.
1057    pub async fn cancel_algo_order(
1058        &self,
1059        params: &BinanceAlgoOrderQueryParams,
1060    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1061        self.request_delete("algoOrder", Some(params), true, true)
1062            .await
1063    }
1064
1065    /// Queries a single algo order.
1066    ///
1067    /// Must provide either `algo_id` or `client_algo_id`.
1068    ///
1069    /// # Errors
1070    ///
1071    /// Returns an error if the request fails.
1072    pub async fn query_algo_order(
1073        &self,
1074        params: &BinanceAlgoOrderQueryParams,
1075    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1076        self.get("algoOrder", Some(params), true, false).await
1077    }
1078
1079    /// Queries all open algo orders.
1080    ///
1081    /// # Errors
1082    ///
1083    /// Returns an error if the request fails.
1084    pub async fn query_open_algo_orders(
1085        &self,
1086        params: &BinanceOpenAlgoOrdersParams,
1087    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1088        self.get("openAlgoOrders", Some(params), true, false).await
1089    }
1090
1091    /// Queries all algo orders including historical (7-day limit).
1092    ///
1093    /// # Errors
1094    ///
1095    /// Returns an error if the request fails.
1096    pub async fn query_all_algo_orders(
1097        &self,
1098        params: &BinanceAllAlgoOrdersParams,
1099    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1100        self.get("allAlgoOrders", Some(params), true, false).await
1101    }
1102
1103    /// Cancels all open algo orders for a symbol.
1104    ///
1105    /// # Errors
1106    ///
1107    /// Returns an error if the request fails.
1108    pub async fn cancel_all_algo_orders(
1109        &self,
1110        params: &BinanceCancelAllAlgoOrdersParams,
1111    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1112        self.request_delete("algoOpenOrders", Some(params), true, true)
1113            .await
1114    }
1115}
1116
1117/// Response wrapper for mark price endpoint.
1118#[derive(Debug, Deserialize)]
1119#[serde(untagged)]
1120enum MarkPriceResponse {
1121    Single(BinanceFuturesMarkPrice),
1122    Multiple(Vec<BinanceFuturesMarkPrice>),
1123}
1124
1125impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1126    fn from(response: MarkPriceResponse) -> Self {
1127        match response {
1128            MarkPriceResponse::Single(price) => vec![price],
1129            MarkPriceResponse::Multiple(prices) => prices,
1130        }
1131    }
1132}
1133
1134struct RateLimitConfig {
1135    default_quota: Option<Quota>,
1136    keyed_quotas: Vec<(String, Quota)>,
1137    order_keys: Vec<String>,
1138}
1139
1140/// In-memory cache entry for Binance Futures instruments.
1141#[derive(Clone, Debug)]
1142pub enum BinanceFuturesInstrument {
1143    /// USD-M futures symbol.
1144    UsdM(BinanceFuturesUsdSymbol),
1145    /// COIN-M futures symbol.
1146    CoinM(BinanceFuturesCoinSymbol),
1147}
1148
1149impl BinanceFuturesInstrument {
1150    /// Returns the symbol name for the instrument.
1151    #[must_use]
1152    pub const fn symbol(&self) -> Ustr {
1153        match self {
1154            Self::UsdM(s) => s.symbol,
1155            Self::CoinM(s) => s.symbol,
1156        }
1157    }
1158
1159    /// Returns the price precision for the instrument.
1160    #[must_use]
1161    pub const fn price_precision(&self) -> i32 {
1162        match self {
1163            Self::UsdM(s) => s.price_precision,
1164            Self::CoinM(s) => s.price_precision,
1165        }
1166    }
1167
1168    /// Returns the quantity precision for the instrument.
1169    #[must_use]
1170    pub const fn quantity_precision(&self) -> i32 {
1171        match self {
1172            Self::UsdM(s) => s.quantity_precision,
1173            Self::CoinM(s) => s.quantity_precision,
1174        }
1175    }
1176
1177    /// Returns the Nautilus-formatted instrument ID.
1178    #[must_use]
1179    pub fn id(&self) -> InstrumentId {
1180        match self {
1181            Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1182            Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1183        }
1184    }
1185
1186    /// Returns the quote currency for the instrument.
1187    #[must_use]
1188    pub fn quote_currency(&self) -> Currency {
1189        let quote_asset = match self {
1190            Self::UsdM(s) => &s.quote_asset,
1191            Self::CoinM(s) => &s.quote_asset,
1192        };
1193        Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1194    }
1195}
1196
1197/// Binance Futures HTTP client for USD-M and COIN-M perpetuals.
1198#[derive(Debug, Clone)]
1199pub struct BinanceFuturesHttpClient {
1200    inner: Arc<BinanceRawFuturesHttpClient>,
1201    product_type: BinanceProductType,
1202    clock: &'static AtomicTime,
1203    instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1204    treat_expired_as_canceled: bool,
1205}
1206
1207impl BinanceFuturesHttpClient {
1208    /// Creates a new [`BinanceFuturesHttpClient`] instance.
1209    ///
1210    /// # Errors
1211    ///
1212    /// Returns an error if the product type is invalid or HTTP client creation fails.
1213    #[expect(clippy::too_many_arguments)]
1214    pub fn new(
1215        product_type: BinanceProductType,
1216        environment: BinanceEnvironment,
1217        clock: &'static AtomicTime,
1218        api_key: Option<String>,
1219        api_secret: Option<String>,
1220        base_url_override: Option<String>,
1221        recv_window: Option<u64>,
1222        timeout_secs: Option<u64>,
1223        proxy_url: Option<String>,
1224        treat_expired_as_canceled: bool,
1225    ) -> BinanceFuturesHttpResult<Self> {
1226        match product_type {
1227            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1228            _ => {
1229                return Err(BinanceFuturesHttpError::ValidationError(format!(
1230                    "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1231                )));
1232            }
1233        }
1234
1235        let raw = BinanceRawFuturesHttpClient::new(
1236            product_type,
1237            environment,
1238            api_key,
1239            api_secret,
1240            base_url_override,
1241            recv_window,
1242            timeout_secs,
1243            proxy_url,
1244        )?;
1245
1246        Ok(Self {
1247            inner: Arc::new(raw),
1248            product_type,
1249            clock,
1250            instruments: Arc::new(DashMap::new()),
1251            treat_expired_as_canceled,
1252        })
1253    }
1254
1255    /// Returns the product type (UsdM or CoinM).
1256    #[must_use]
1257    pub const fn product_type(&self) -> BinanceProductType {
1258        self.product_type
1259    }
1260
1261    /// Returns a reference to the inner raw HTTP client.
1262    #[must_use]
1263    pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1264        &self.inner
1265    }
1266
1267    /// Returns a clone of the instruments cache Arc.
1268    #[must_use]
1269    pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1270        Arc::clone(&self.instruments)
1271    }
1272
1273    /// Returns server time.
1274    ///
1275    /// # Errors
1276    ///
1277    /// Returns an error if the request fails.
1278    pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1279        self.inner
1280            .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1281            .await
1282    }
1283
1284    /// Sets leverage for a symbol.
1285    ///
1286    /// # Errors
1287    ///
1288    /// Returns an error if the request fails.
1289    pub async fn set_leverage(
1290        &self,
1291        params: &BinanceSetLeverageParams,
1292    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1293        self.inner.set_leverage(params).await
1294    }
1295
1296    /// Sets margin type for a symbol.
1297    ///
1298    /// # Errors
1299    ///
1300    /// Returns an error if the request fails.
1301    pub async fn set_margin_type(
1302        &self,
1303        params: &BinanceSetMarginTypeParams,
1304    ) -> BinanceFuturesHttpResult<serde_json::Value> {
1305        self.inner.set_margin_type(params).await
1306    }
1307
1308    /// Queries hedge mode (dual side position) setting.
1309    ///
1310    /// # Errors
1311    ///
1312    /// Returns an error if the request fails.
1313    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1314        self.inner.query_hedge_mode().await
1315    }
1316
1317    /// Creates a listen key for user data stream.
1318    ///
1319    /// # Errors
1320    ///
1321    /// Returns an error if the request fails.
1322    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1323        self.inner.create_listen_key().await
1324    }
1325
1326    /// Keeps alive an existing listen key.
1327    ///
1328    /// # Errors
1329    ///
1330    /// Returns an error if the request fails.
1331    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1332        self.inner.keepalive_listen_key(listen_key).await
1333    }
1334
1335    /// Closes an existing listen key.
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns an error if the request fails.
1340    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1341        self.inner.close_listen_key(listen_key).await
1342    }
1343
1344    /// Fetches exchange information and populates the instrument cache.
1345    ///
1346    /// # Errors
1347    ///
1348    /// Returns an error if the request fails or the product type is invalid.
1349    pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1350        match self.product_type {
1351            BinanceProductType::UsdM => {
1352                let info: BinanceFuturesUsdExchangeInfo = self
1353                    .inner
1354                    .get("exchangeInfo", None::<&()>, false, false)
1355                    .await?;
1356
1357                for symbol in info.symbols {
1358                    self.instruments
1359                        .insert(symbol.symbol, BinanceFuturesInstrument::UsdM(symbol));
1360                }
1361            }
1362            BinanceProductType::CoinM => {
1363                let info: BinanceFuturesCoinExchangeInfo = self
1364                    .inner
1365                    .get("exchangeInfo", None::<&()>, false, false)
1366                    .await?;
1367
1368                for symbol in info.symbols {
1369                    self.instruments
1370                        .insert(symbol.symbol, BinanceFuturesInstrument::CoinM(symbol));
1371                }
1372            }
1373            _ => {
1374                return Err(BinanceFuturesHttpError::ValidationError(
1375                    "Invalid product type for futures".to_string(),
1376                ));
1377            }
1378        }
1379
1380        Ok(())
1381    }
1382
1383    /// Fetches exchange info and returns the current status of each symbol.
1384    ///
1385    /// Builds a fresh status snapshot from the response without disturbing the
1386    /// shared instruments cache, so a transient failure does not break other
1387    /// HTTP operations that depend on cached precision data.
1388    ///
1389    /// # Errors
1390    ///
1391    /// Returns an error if the request fails or the product type is invalid.
1392    pub async fn request_symbol_statuses(
1393        &self,
1394    ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1395        let mut statuses = AHashMap::new();
1396
1397        match self.product_type {
1398            BinanceProductType::UsdM => {
1399                let info: BinanceFuturesUsdExchangeInfo = self
1400                    .inner
1401                    .get("exchangeInfo", None::<&()>, false, false)
1402                    .await?;
1403
1404                for symbol in &info.symbols {
1405                    statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1406                }
1407            }
1408            BinanceProductType::CoinM => {
1409                let info: BinanceFuturesCoinExchangeInfo = self
1410                    .inner
1411                    .get("exchangeInfo", None::<&()>, false, false)
1412                    .await?;
1413
1414                for symbol in &info.symbols {
1415                    let action = symbol
1416                        .contract_status
1417                        .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1418                    statuses.insert(symbol.symbol, action);
1419                }
1420            }
1421            _ => {
1422                return Err(BinanceFuturesHttpError::ValidationError(
1423                    "Invalid product type for futures".to_string(),
1424                ));
1425            }
1426        }
1427
1428        Ok(statuses)
1429    }
1430
1431    /// Fetches exchange information and returns parsed Nautilus instruments.
1432    ///
1433    /// # Errors
1434    ///
1435    /// Returns an error if the request fails or the product type is invalid.
1436    pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1437        let ts_init = UnixNanos::default();
1438
1439        let instruments = match self.product_type {
1440            BinanceProductType::UsdM => {
1441                let info: BinanceFuturesUsdExchangeInfo = self
1442                    .inner
1443                    .get("exchangeInfo", None::<&()>, false, false)
1444                    .await?;
1445
1446                let mut instruments = Vec::with_capacity(info.symbols.len());
1447
1448                for symbol in info.symbols {
1449                    // Cache symbol for precision lookups
1450                    self.instruments.insert(
1451                        symbol.symbol,
1452                        BinanceFuturesInstrument::UsdM(symbol.clone()),
1453                    );
1454
1455                    match parse_usdm_instrument(&symbol, ts_init, ts_init) {
1456                        Ok(instrument) => instruments.push(instrument),
1457                        Err(e) => {
1458                            log::debug!(
1459                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1460                                symbol.symbol
1461                            );
1462                        }
1463                    }
1464                }
1465
1466                log::debug!(
1467                    "Loaded USD-M perpetual instruments: count={}",
1468                    instruments.len()
1469                );
1470                instruments
1471            }
1472            BinanceProductType::CoinM => {
1473                let info: BinanceFuturesCoinExchangeInfo = self
1474                    .inner
1475                    .get("exchangeInfo", None::<&()>, false, false)
1476                    .await?;
1477
1478                let mut instruments = Vec::with_capacity(info.symbols.len());
1479                for symbol in info.symbols {
1480                    // Cache symbol for precision lookups
1481                    self.instruments.insert(
1482                        symbol.symbol,
1483                        BinanceFuturesInstrument::CoinM(symbol.clone()),
1484                    );
1485
1486                    match parse_coinm_instrument(&symbol, ts_init, ts_init) {
1487                        Ok(instrument) => instruments.push(instrument),
1488                        Err(e) => {
1489                            log::debug!(
1490                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1491                                symbol.symbol
1492                            );
1493                        }
1494                    }
1495                }
1496
1497                log::debug!(
1498                    "Loaded COIN-M perpetual instruments: count={}",
1499                    instruments.len()
1500                );
1501                instruments
1502            }
1503            _ => {
1504                return Err(BinanceFuturesHttpError::ValidationError(
1505                    "Invalid product type for futures".to_string(),
1506                ));
1507            }
1508        };
1509
1510        Ok(instruments)
1511    }
1512
1513    /// Fetches 24hr ticker statistics.
1514    ///
1515    /// # Errors
1516    ///
1517    /// Returns an error if the request fails.
1518    pub async fn ticker_24h(
1519        &self,
1520        params: &BinanceTicker24hrParams,
1521    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1522        self.inner.ticker_24h(params).await
1523    }
1524
1525    /// Fetches best bid/ask prices.
1526    ///
1527    /// # Errors
1528    ///
1529    /// Returns an error if the request fails.
1530    pub async fn book_ticker(
1531        &self,
1532        params: &BinanceBookTickerParams,
1533    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1534        self.inner.book_ticker(params).await
1535    }
1536
1537    /// Fetches price ticker.
1538    ///
1539    /// # Errors
1540    ///
1541    /// Returns an error if the request fails.
1542    pub async fn price_ticker(
1543        &self,
1544        symbol: Option<&str>,
1545    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1546        self.inner.price_ticker(symbol).await
1547    }
1548
1549    /// Fetches order book depth.
1550    ///
1551    /// # Errors
1552    ///
1553    /// Returns an error if the request fails.
1554    pub async fn depth(
1555        &self,
1556        params: &BinanceDepthParams,
1557    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1558        self.inner.depth(params).await
1559    }
1560
1561    /// Fetches mark price and funding rate.
1562    ///
1563    /// # Errors
1564    ///
1565    /// Returns an error if the request fails.
1566    pub async fn mark_price(
1567        &self,
1568        params: &BinanceMarkPriceParams,
1569    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
1570        self.inner.mark_price(params).await
1571    }
1572
1573    /// Fetches funding rate history.
1574    ///
1575    /// # Errors
1576    ///
1577    /// Returns an error if the request fails.
1578    pub async fn funding_rate(
1579        &self,
1580        params: &BinanceFundingRateParams,
1581    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
1582        self.inner.funding_rate(params).await
1583    }
1584
1585    /// Fetches current open interest for a symbol.
1586    ///
1587    /// # Errors
1588    ///
1589    /// Returns an error if the request fails.
1590    pub async fn open_interest(
1591        &self,
1592        params: &BinanceOpenInterestParams,
1593    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
1594        self.inner.open_interest(params).await
1595    }
1596
1597    /// Fetches historical open interest statistics for a symbol or pair.
1598    ///
1599    /// # Errors
1600    ///
1601    /// Returns an error if the request fails.
1602    pub async fn open_interest_hist(
1603        &self,
1604        params: &BinanceOpenInterestHistParams,
1605    ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
1606        self.inner.open_interest_hist(params).await
1607    }
1608
1609    /// Queries a single order by order ID or client order ID.
1610    ///
1611    /// # Errors
1612    ///
1613    /// Returns an error if the request fails.
1614    pub async fn query_order(
1615        &self,
1616        params: &BinanceOrderQueryParams,
1617    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1618        self.inner.query_order(params).await
1619    }
1620
1621    /// Queries all open orders.
1622    ///
1623    /// # Errors
1624    ///
1625    /// Returns an error if the request fails.
1626    pub async fn query_open_orders(
1627        &self,
1628        params: &BinanceOpenOrdersParams,
1629    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1630        self.inner.query_open_orders(params).await
1631    }
1632
1633    /// Queries all orders (including historical).
1634    ///
1635    /// # Errors
1636    ///
1637    /// Returns an error if the request fails.
1638    pub async fn query_all_orders(
1639        &self,
1640        params: &BinanceAllOrdersParams,
1641    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1642        self.inner.query_all_orders(params).await
1643    }
1644
1645    /// Fetches account information including balances and positions.
1646    ///
1647    /// # Errors
1648    ///
1649    /// Returns an error if the request fails.
1650    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1651        self.inner.query_account().await
1652    }
1653
1654    /// Fetches position risk information.
1655    ///
1656    /// # Errors
1657    ///
1658    /// Returns an error if the request fails.
1659    pub async fn query_positions(
1660        &self,
1661        params: &BinancePositionRiskParams,
1662    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1663        self.inner.query_positions(params).await
1664    }
1665
1666    /// Fetches user trades for a symbol.
1667    ///
1668    /// # Errors
1669    ///
1670    /// Returns an error if the request fails.
1671    pub async fn query_user_trades(
1672        &self,
1673        params: &BinanceUserTradesParams,
1674    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1675        self.inner.query_user_trades(params).await
1676    }
1677
1678    /// Submits a new order.
1679    ///
1680    /// # Errors
1681    ///
1682    /// Returns an error if:
1683    /// - The instrument is not cached.
1684    /// - The order type or time-in-force is unsupported.
1685    /// - Stop orders are submitted without a trigger price.
1686    /// - The request fails.
1687    #[expect(clippy::too_many_arguments)]
1688    pub async fn submit_order(
1689        &self,
1690        account_id: AccountId,
1691        instrument_id: InstrumentId,
1692        client_order_id: ClientOrderId,
1693        order_side: OrderSide,
1694        order_type: OrderType,
1695        quantity: Quantity,
1696        time_in_force: TimeInForce,
1697        price: Option<Price>,
1698        trigger_price: Option<Price>,
1699        reduce_only: bool,
1700        post_only: bool,
1701        position_side: Option<BinancePositionSide>,
1702        price_match: Option<BinancePriceMatch>,
1703    ) -> anyhow::Result<OrderStatusReport> {
1704        let symbol = format_binance_symbol(&instrument_id);
1705        let size_precision = self.get_size_precision(&symbol)?;
1706
1707        let binance_side = BinanceSide::try_from(order_side)?;
1708        let binance_order_type = order_type_to_binance_futures(order_type)?;
1709        let binance_tif = if post_only {
1710            BinanceTimeInForce::Gtx
1711        } else {
1712            BinanceTimeInForce::try_from(time_in_force)?
1713        };
1714
1715        let requires_trigger_price = matches!(
1716            order_type,
1717            OrderType::StopMarket
1718                | OrderType::StopLimit
1719                | OrderType::TrailingStopMarket
1720                | OrderType::MarketIfTouched
1721                | OrderType::LimitIfTouched
1722        );
1723
1724        if requires_trigger_price && trigger_price.is_none() {
1725            anyhow::bail!("Order type {order_type:?} requires a trigger price");
1726        }
1727
1728        // MARKET and STOP_MARKET orders don't accept timeInForce
1729        let requires_time_in_force = matches!(
1730            order_type,
1731            OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
1732        );
1733
1734        let qty_str = quantity.to_string();
1735        let price_str = if price_match.is_some() {
1736            None
1737        } else {
1738            price.map(|p| p.to_string())
1739        };
1740        let stop_price_str = trigger_price.map(|p| p.to_string());
1741        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1742
1743        let params = BinanceNewOrderParams {
1744            symbol,
1745            side: binance_side,
1746            order_type: binance_order_type,
1747            time_in_force: if requires_time_in_force {
1748                Some(binance_tif)
1749            } else {
1750                None
1751            },
1752            quantity: Some(qty_str),
1753            price: price_str,
1754            new_client_order_id: Some(client_id_str),
1755            stop_price: stop_price_str,
1756            reduce_only: reduce_only_param(reduce_only, position_side),
1757            position_side,
1758            close_position: None,
1759            activation_price: None,
1760            callback_rate: None,
1761            working_type: None,
1762            price_protect: None,
1763            new_order_resp_type: None,
1764            good_till_date: None,
1765            recv_window: None,
1766            price_match,
1767            self_trade_prevention_mode: None,
1768        };
1769
1770        let order = self.inner.submit_order(&params).await?;
1771        let ts_init = self.clock.get_time_ns();
1772        order.to_order_status_report(
1773            account_id,
1774            instrument_id,
1775            size_precision,
1776            self.treat_expired_as_canceled,
1777            ts_init,
1778        )
1779    }
1780
1781    /// Submits an algo order (conditional order) to the Binance Algo Service.
1782    ///
1783    /// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
1784    /// This method handles StopMarket, StopLimit, MarketIfTouched, LimitIfTouched,
1785    /// and TrailingStopMarket orders.
1786    ///
1787    /// # Errors
1788    ///
1789    /// Returns an error if:
1790    /// - The order type requires a trigger price but none is provided.
1791    /// - The instrument is not cached.
1792    /// - The request fails.
1793    #[expect(clippy::too_many_arguments)]
1794    pub async fn submit_algo_order(
1795        &self,
1796        account_id: AccountId,
1797        instrument_id: InstrumentId,
1798        client_order_id: ClientOrderId,
1799        order_side: OrderSide,
1800        order_type: OrderType,
1801        quantity: Quantity,
1802        time_in_force: TimeInForce,
1803        price: Option<Price>,
1804        trigger_price: Option<Price>,
1805        reduce_only: bool,
1806        close_position: bool,
1807        position_side: Option<BinancePositionSide>,
1808        activation_price: Option<Price>,
1809        callback_rate: Option<String>,
1810        working_type: Option<BinanceWorkingType>,
1811    ) -> anyhow::Result<OrderStatusReport> {
1812        let symbol = format_binance_symbol(&instrument_id);
1813        let size_precision = self.get_size_precision(&symbol)?;
1814
1815        let binance_side = BinanceSide::try_from(order_side)?;
1816        let binance_order_type = order_type_to_binance_futures(order_type)?;
1817        let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1818
1819        let requires_trigger_price = matches!(
1820            order_type,
1821            OrderType::StopMarket
1822                | OrderType::StopLimit
1823                | OrderType::MarketIfTouched
1824                | OrderType::LimitIfTouched
1825        );
1826        anyhow::ensure!(
1827            !requires_trigger_price || trigger_price.is_some(),
1828            "Algo order type {order_type:?} requires a trigger price"
1829        );
1830
1831        // Limit orders require time in force
1832        let requires_time_in_force =
1833            matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
1834
1835        let price_str = price.map(|p| p.to_string());
1836        let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
1837            None
1838        } else {
1839            trigger_price.map(|p| p.to_string())
1840        };
1841        let reduce_only = reduce_only_param(reduce_only, position_side);
1842        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1843
1844        // closePosition is mutually exclusive with quantity and reduceOnly
1845        let params = if close_position {
1846            BinanceNewAlgoOrderParams {
1847                symbol,
1848                side: binance_side,
1849                order_type: binance_order_type,
1850                algo_type: BinanceAlgoType::Conditional,
1851                position_side,
1852                quantity: None,
1853                price: price_str,
1854                trigger_price: trigger_price_str,
1855                time_in_force: if requires_time_in_force {
1856                    Some(binance_tif)
1857                } else {
1858                    None
1859                },
1860                working_type,
1861                close_position: Some(true),
1862                price_protect: None,
1863                reduce_only: None,
1864                activation_price: activation_price.map(|p| p.to_string()),
1865                callback_rate,
1866                client_algo_id: Some(client_id_str),
1867                good_till_date: None,
1868                recv_window: None,
1869            }
1870        } else {
1871            let qty_str = quantity.to_string();
1872            BinanceNewAlgoOrderParams {
1873                symbol,
1874                side: binance_side,
1875                order_type: binance_order_type,
1876                algo_type: BinanceAlgoType::Conditional,
1877                position_side,
1878                quantity: Some(qty_str),
1879                price: price_str,
1880                trigger_price: trigger_price_str,
1881                time_in_force: if requires_time_in_force {
1882                    Some(binance_tif)
1883                } else {
1884                    None
1885                },
1886                working_type,
1887                close_position: None,
1888                price_protect: None,
1889                reduce_only,
1890                activation_price: activation_price.map(|p| p.to_string()),
1891                callback_rate,
1892                client_algo_id: Some(client_id_str),
1893                good_till_date: None,
1894                recv_window: None,
1895            }
1896        };
1897
1898        let order = self.inner.submit_algo_order(&params).await?;
1899        let ts_init = self.clock.get_time_ns();
1900        order.to_order_status_report(account_id, instrument_id, size_precision, ts_init)
1901    }
1902
1903    /// Submits multiple orders in a single request (up to 5 orders).
1904    ///
1905    /// Each order in the batch is processed independently. The response contains
1906    /// the result for each order, which can be either a success or an error.
1907    ///
1908    /// # Errors
1909    ///
1910    /// Returns an error if the batch exceeds 5 orders or the request fails.
1911    pub async fn submit_order_list(
1912        &self,
1913        orders: &[BatchOrderItem],
1914    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1915        self.inner.submit_order_list(orders).await
1916    }
1917
1918    /// Modifies an existing order (price and quantity only).
1919    ///
1920    /// Either `venue_order_id` or `client_order_id` must be provided.
1921    ///
1922    /// # Errors
1923    ///
1924    /// Returns an error if:
1925    /// - Neither venue_order_id nor client_order_id is provided.
1926    /// - The instrument is not cached.
1927    /// - The request fails.
1928    #[expect(clippy::too_many_arguments)]
1929    pub async fn modify_order(
1930        &self,
1931        account_id: AccountId,
1932        instrument_id: InstrumentId,
1933        venue_order_id: Option<VenueOrderId>,
1934        client_order_id: Option<ClientOrderId>,
1935        order_side: OrderSide,
1936        quantity: Quantity,
1937        price: Price,
1938    ) -> anyhow::Result<OrderStatusReport> {
1939        anyhow::ensure!(
1940            venue_order_id.is_some() || client_order_id.is_some(),
1941            "Either venue_order_id or client_order_id must be provided"
1942        );
1943
1944        let symbol = format_binance_symbol(&instrument_id);
1945        let size_precision = self.get_size_precision(&symbol)?;
1946
1947        let binance_side = BinanceSide::try_from(order_side)?;
1948
1949        let order_id = venue_order_id
1950            .map(|id| id.inner().parse::<i64>())
1951            .transpose()
1952            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1953
1954        let params = BinanceModifyOrderParams {
1955            symbol,
1956            order_id,
1957            orig_client_order_id: client_order_id
1958                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1959            side: binance_side,
1960            quantity: quantity.to_string(),
1961            price: price.to_string(),
1962            recv_window: None,
1963        };
1964
1965        let order = self.inner.modify_order(&params).await?;
1966        let ts_init = self.clock.get_time_ns();
1967        order.to_order_status_report(
1968            account_id,
1969            instrument_id,
1970            size_precision,
1971            self.treat_expired_as_canceled,
1972            ts_init,
1973        )
1974    }
1975
1976    /// Modifies multiple orders in a single request (up to 5 orders).
1977    ///
1978    /// Each modify in the batch is processed independently. The response contains
1979    /// the result for each modify, which can be either a success or an error.
1980    ///
1981    /// # Errors
1982    ///
1983    /// Returns an error if the batch exceeds 5 orders or the request fails.
1984    pub async fn batch_modify_orders(
1985        &self,
1986        modifies: &[BatchModifyItem],
1987    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1988        self.inner.batch_modify_orders(modifies).await
1989    }
1990
1991    /// Cancels an order by venue order ID or client order ID.
1992    ///
1993    /// Either `venue_order_id` or `client_order_id` must be provided.
1994    ///
1995    /// # Errors
1996    ///
1997    /// Returns an error if:
1998    /// - Neither venue_order_id nor client_order_id is provided.
1999    /// - The request fails.
2000    pub async fn cancel_order(
2001        &self,
2002        instrument_id: InstrumentId,
2003        venue_order_id: Option<VenueOrderId>,
2004        client_order_id: Option<ClientOrderId>,
2005    ) -> anyhow::Result<VenueOrderId> {
2006        anyhow::ensure!(
2007            venue_order_id.is_some() || client_order_id.is_some(),
2008            "Either venue_order_id or client_order_id must be provided"
2009        );
2010
2011        let symbol = format_binance_symbol(&instrument_id);
2012
2013        let order_id = match venue_order_id {
2014            Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2015                Ok(order_id) => Some(order_id),
2016                Err(e) if client_order_id.is_some() => {
2017                    log::warn!(
2018                        "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2019                    );
2020                    None
2021                }
2022                Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2023            },
2024            None => None,
2025        };
2026
2027        let params = BinanceCancelOrderParams {
2028            symbol,
2029            order_id,
2030            orig_client_order_id: client_order_id
2031                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2032            recv_window: None,
2033        };
2034
2035        let order = self.inner.cancel_order(&params).await?;
2036        Ok(VenueOrderId::new(order.order_id.to_string()))
2037    }
2038
2039    /// Cancels an algo order (conditional order) via the Binance Algo Service.
2040    ///
2041    /// Use the `client_algo_id` which corresponds to the `client_order_id` used
2042    /// when submitting the algo order.
2043    ///
2044    /// # Errors
2045    ///
2046    /// Returns an error if the request fails.
2047    pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2048        let params = BinanceAlgoOrderQueryParams {
2049            algo_id: None,
2050            client_algo_id: Some(encode_broker_id(
2051                &client_order_id,
2052                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2053            )),
2054            recv_window: None,
2055        };
2056
2057        let response = self.inner.cancel_algo_order(&params).await?;
2058        if response.code.parse::<i32>().unwrap_or(0) == 200 {
2059            Ok(())
2060        } else {
2061            anyhow::bail!(
2062                "Cancel algo order failed: code={}, msg={}",
2063                response.code,
2064                response.msg
2065            )
2066        }
2067    }
2068
2069    /// Cancels all open orders for a symbol.
2070    ///
2071    /// # Errors
2072    ///
2073    /// Returns an error if the request fails.
2074    pub async fn cancel_all_orders(
2075        &self,
2076        instrument_id: InstrumentId,
2077    ) -> anyhow::Result<Vec<VenueOrderId>> {
2078        let symbol = format_binance_symbol(&instrument_id);
2079
2080        let params = BinanceCancelAllOrdersParams {
2081            symbol,
2082            recv_window: None,
2083        };
2084
2085        let response = self.inner.cancel_all_orders(&params).await?;
2086        if response.code == 200 {
2087            Ok(vec![])
2088        } else {
2089            anyhow::bail!("Cancel all orders failed: {}", response.msg);
2090        }
2091    }
2092
2093    /// Cancels all open algo orders for a symbol.
2094    ///
2095    /// # Errors
2096    ///
2097    /// Returns an error if the request fails.
2098    pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2099        let symbol = format_binance_symbol(&instrument_id);
2100
2101        let params = BinanceCancelAllAlgoOrdersParams {
2102            symbol,
2103            recv_window: None,
2104        };
2105
2106        let response = self.inner.cancel_all_algo_orders(&params).await?;
2107        if response.code == 200 {
2108            Ok(())
2109        } else {
2110            anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2111        }
2112    }
2113
2114    /// Cancels multiple orders in a single request (up to 10 orders).
2115    ///
2116    /// Each cancel in the batch is processed independently. The response contains
2117    /// the result for each cancel, which can be either a success or an error.
2118    ///
2119    /// # Errors
2120    ///
2121    /// Returns an error if the batch exceeds 10 orders or the request fails.
2122    pub async fn batch_cancel_orders(
2123        &self,
2124        cancels: &[BatchCancelItem],
2125    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2126        self.inner.batch_cancel_orders(cancels).await
2127    }
2128
2129    /// Queries open algo orders (conditional orders).
2130    ///
2131    /// Returns all open algo orders, optionally filtered by symbol.
2132    ///
2133    /// # Errors
2134    ///
2135    /// Returns an error if the request fails.
2136    pub async fn query_open_algo_orders(
2137        &self,
2138        instrument_id: Option<InstrumentId>,
2139    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2140        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2141
2142        let params = BinanceOpenAlgoOrdersParams {
2143            symbol,
2144            recv_window: None,
2145        };
2146
2147        self.inner.query_open_algo_orders(&params).await
2148    }
2149
2150    /// Queries a single algo order by client_order_id.
2151    ///
2152    /// # Errors
2153    ///
2154    /// Returns an error if the request fails.
2155    pub async fn query_algo_order(
2156        &self,
2157        client_order_id: ClientOrderId,
2158    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2159        let params = BinanceAlgoOrderQueryParams {
2160            algo_id: None,
2161            client_algo_id: Some(encode_broker_id(
2162                &client_order_id,
2163                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2164            )),
2165            recv_window: None,
2166        };
2167
2168        self.inner.query_algo_order(&params).await
2169    }
2170
2171    /// Returns the size precision for an instrument from the cache.
2172    fn get_size_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2173        let instrument = self
2174            .instruments
2175            .get(&Ustr::from(symbol))
2176            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2177
2178        let precision = match instrument.value() {
2179            BinanceFuturesInstrument::UsdM(s) => s.quantity_precision,
2180            BinanceFuturesInstrument::CoinM(s) => s.quantity_precision,
2181        };
2182
2183        Ok(precision as u8)
2184    }
2185
2186    /// Returns the price precision for an instrument from the cache.
2187    fn get_price_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2188        let instrument = self
2189            .instruments
2190            .get(&Ustr::from(symbol))
2191            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2192
2193        let precision = match instrument.value() {
2194            BinanceFuturesInstrument::UsdM(s) => s.price_precision,
2195            BinanceFuturesInstrument::CoinM(s) => s.price_precision,
2196        };
2197
2198        Ok(precision as u8)
2199    }
2200
2201    /// Requests the current account state.
2202    ///
2203    /// # Errors
2204    ///
2205    /// Returns an error if the request fails or parsing fails.
2206    pub async fn request_account_state(
2207        &self,
2208        account_id: AccountId,
2209    ) -> anyhow::Result<AccountState> {
2210        let ts_init = UnixNanos::default();
2211        let account_info = self.inner.query_account().await?;
2212        account_info.to_account_state(account_id, ts_init)
2213    }
2214
2215    /// Requests a single order status report.
2216    ///
2217    /// Either `venue_order_id` or `client_order_id` must be provided.
2218    ///
2219    /// # Errors
2220    ///
2221    /// Returns an error if the request fails or parsing fails.
2222    pub async fn request_order_status_report(
2223        &self,
2224        account_id: AccountId,
2225        instrument_id: InstrumentId,
2226        venue_order_id: Option<VenueOrderId>,
2227        client_order_id: Option<ClientOrderId>,
2228    ) -> anyhow::Result<OrderStatusReport> {
2229        anyhow::ensure!(
2230            venue_order_id.is_some() || client_order_id.is_some(),
2231            "Either venue_order_id or client_order_id must be provided"
2232        );
2233
2234        let symbol = format_binance_symbol(&instrument_id);
2235        let size_precision = self.get_size_precision(&symbol)?;
2236
2237        let order_id = venue_order_id
2238            .map(|id| id.inner().parse::<i64>())
2239            .transpose()
2240            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2241
2242        let orig_client_order_id =
2243            client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2244
2245        let params = BinanceOrderQueryParams {
2246            symbol,
2247            order_id,
2248            orig_client_order_id,
2249            recv_window: None,
2250        };
2251
2252        let order = self.inner.query_order(&params).await?;
2253        let ts_init = self.clock.get_time_ns();
2254        order.to_order_status_report(
2255            account_id,
2256            instrument_id,
2257            size_precision,
2258            self.treat_expired_as_canceled,
2259            ts_init,
2260        )
2261    }
2262
2263    /// Requests order status reports for open orders.
2264    ///
2265    /// If `instrument_id` is None, returns all open orders.
2266    ///
2267    /// # Errors
2268    ///
2269    /// Returns an error if the request fails or parsing fails.
2270    pub async fn request_order_status_reports(
2271        &self,
2272        account_id: AccountId,
2273        instrument_id: Option<InstrumentId>,
2274        open_only: bool,
2275    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2276        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2277
2278        let orders = if open_only {
2279            let params = BinanceOpenOrdersParams {
2280                symbol: symbol.clone(),
2281                recv_window: None,
2282            };
2283            self.inner.query_open_orders(&params).await?
2284        } else {
2285            // For historical orders, symbol is required
2286            let symbol = symbol.ok_or_else(|| {
2287                anyhow::anyhow!("instrument_id is required for historical orders")
2288            })?;
2289            let params = BinanceAllOrdersParams {
2290                symbol,
2291                order_id: None,
2292                start_time: None,
2293                end_time: None,
2294                limit: None,
2295                recv_window: None,
2296            };
2297            self.inner.query_all_orders(&params).await?
2298        };
2299
2300        let ts_init = self.clock.get_time_ns();
2301        let mut reports = Vec::with_capacity(orders.len());
2302
2303        for order in orders {
2304            let order_instrument_id = instrument_id.unwrap_or_else(|| {
2305                // Build instrument ID from order symbol
2306                let suffix = self.product_type.suffix();
2307                InstrumentId::from(format!("{}{}.BINANCE", order.symbol, suffix))
2308            });
2309
2310            let size_precision = self.get_size_precision(&order.symbol).unwrap_or(8); // Default precision if not in cache
2311
2312            match order.to_order_status_report(
2313                account_id,
2314                order_instrument_id,
2315                size_precision,
2316                self.treat_expired_as_canceled,
2317                ts_init,
2318            ) {
2319                Ok(report) => reports.push(report),
2320                Err(e) => {
2321                    log::warn!("Failed to parse order status report: {e}");
2322                }
2323            }
2324        }
2325
2326        Ok(reports)
2327    }
2328
2329    /// Requests fill reports for a symbol.
2330    ///
2331    /// # Errors
2332    ///
2333    /// Returns an error if the request fails or parsing fails.
2334    #[expect(clippy::too_many_arguments)]
2335    pub async fn request_fill_reports(
2336        &self,
2337        account_id: AccountId,
2338        instrument_id: InstrumentId,
2339        venue_order_id: Option<VenueOrderId>,
2340        start: Option<i64>,
2341        end: Option<i64>,
2342        limit: Option<u32>,
2343        bnfcr_currency: Currency,
2344    ) -> anyhow::Result<Vec<FillReport>> {
2345        let symbol = format_binance_symbol(&instrument_id);
2346        let size_precision = self.get_size_precision(&symbol)?;
2347        let price_precision = self.get_price_precision(&symbol)?;
2348
2349        let order_id = venue_order_id
2350            .map(|id| id.inner().parse::<i64>())
2351            .transpose()
2352            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2353
2354        let params = BinanceUserTradesParams {
2355            symbol,
2356            order_id,
2357            start_time: start,
2358            end_time: end,
2359            from_id: None,
2360            limit,
2361            recv_window: None,
2362        };
2363
2364        let trades = self.inner.query_user_trades(&params).await?;
2365
2366        let ts_init = self.clock.get_time_ns();
2367        let mut reports = Vec::with_capacity(trades.len());
2368
2369        for trade in trades {
2370            match trade.to_fill_report(
2371                account_id,
2372                instrument_id,
2373                price_precision,
2374                size_precision,
2375                bnfcr_currency,
2376                ts_init,
2377            ) {
2378                Ok(report) => reports.push(report),
2379                Err(e) => {
2380                    log::warn!("Failed to parse fill report: {e}");
2381                }
2382            }
2383        }
2384
2385        Ok(reports)
2386    }
2387
2388    /// Requests recent public trades for an instrument.
2389    ///
2390    /// # Errors
2391    ///
2392    /// Returns an error if the request fails, instrument is not cached, or parsing fails.
2393    pub async fn request_trades(
2394        &self,
2395        instrument_id: InstrumentId,
2396        limit: Option<u32>,
2397    ) -> anyhow::Result<Vec<TradeTick>> {
2398        let (symbol, price_precision, size_precision) =
2399            self.cached_precisions_by_id(instrument_id)?;
2400
2401        let params = BinanceTradesParams { symbol, limit };
2402
2403        let trades = self.inner.trades(&params).await?;
2404        let ts_init = UnixNanos::default();
2405
2406        let mut result = Vec::with_capacity(trades.len());
2407        for trade in trades {
2408            let tick = parse_futures_trade_tick(
2409                &trade,
2410                instrument_id,
2411                price_precision,
2412                size_precision,
2413                ts_init,
2414            )?;
2415            result.push(tick);
2416        }
2417
2418        Ok(result)
2419    }
2420
2421    /// Requests bar (kline/candlestick) data for an instrument.
2422    ///
2423    /// # Errors
2424    ///
2425    /// Returns an error if the bar type is not supported, instrument is not cached,
2426    /// or the request fails.
2427    pub async fn request_bars(
2428        &self,
2429        bar_type: BarType,
2430        start: Option<DateTime<Utc>>,
2431        end: Option<DateTime<Utc>>,
2432        limit: Option<u32>,
2433    ) -> anyhow::Result<Vec<Bar>> {
2434        anyhow::ensure!(
2435            bar_type.aggregation_source() == AggregationSource::External,
2436            "Only EXTERNAL aggregation is supported"
2437        );
2438
2439        let spec = bar_type.spec();
2440        let step = spec.step.get();
2441        let interval = match spec.aggregation {
2442            BarAggregation::Second => {
2443                anyhow::bail!("Binance Futures does not support second-level kline intervals")
2444            }
2445            BarAggregation::Minute => format!("{step}m"),
2446            BarAggregation::Hour => format!("{step}h"),
2447            BarAggregation::Day => format!("{step}d"),
2448            BarAggregation::Week => format!("{step}w"),
2449            BarAggregation::Month => format!("{step}M"),
2450            a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
2451        };
2452
2453        let instrument_id = bar_type.instrument_id();
2454        let (symbol, price_precision, size_precision) =
2455            self.cached_precisions_by_id(instrument_id)?;
2456
2457        let params = BinanceKlinesParams {
2458            symbol,
2459            interval,
2460            start_time: start.map(|dt| dt.timestamp_millis()),
2461            end_time: end.map(|dt| dt.timestamp_millis()),
2462            limit,
2463        };
2464
2465        let klines = self.inner.klines(&params).await?;
2466        let ts_init = UnixNanos::default();
2467
2468        let mut result = Vec::with_capacity(klines.len());
2469        for kline in klines {
2470            let bar = parse_futures_kline_bar(
2471                &kline,
2472                bar_type,
2473                price_precision,
2474                size_precision,
2475                ts_init,
2476            )?;
2477            result.push(bar);
2478        }
2479
2480        Ok(result)
2481    }
2482
2483    fn cached_precisions_by_id(
2484        &self,
2485        instrument_id: InstrumentId,
2486    ) -> anyhow::Result<(String, u8, u8)> {
2487        let symbol = format_binance_symbol(&instrument_id);
2488        let instrument = self
2489            .instruments
2490            .get(&Ustr::from(symbol.as_str()))
2491            .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
2492
2493        let (price_precision, size_precision) = match instrument.value() {
2494            BinanceFuturesInstrument::UsdM(s) => (s.price_precision, s.quantity_precision),
2495            BinanceFuturesInstrument::CoinM(s) => (s.price_precision, s.quantity_precision),
2496        };
2497
2498        Ok((symbol, price_precision as u8, size_precision as u8))
2499    }
2500
2501    /// Requests historical funding rates for an instrument.
2502    ///
2503    /// # Errors
2504    ///
2505    /// Returns an error if the request fails or parsing fails.
2506    pub async fn request_funding_rates(
2507        &self,
2508        instrument_id: InstrumentId,
2509        start: Option<DateTime<Utc>>,
2510        end: Option<DateTime<Utc>>,
2511        limit: Option<u32>,
2512    ) -> anyhow::Result<Vec<FundingRateUpdate>> {
2513        let params = BinanceFundingRateParams {
2514            symbol: Some(format_binance_symbol(&instrument_id)),
2515            start_time: start.map(|dt| dt.timestamp_millis()),
2516            end_time: end.map(|dt| dt.timestamp_millis()),
2517            limit,
2518        };
2519
2520        let rates = self.inner.funding_rate(&params).await?;
2521        let ts_init = UnixNanos::default();
2522
2523        let mut result = Vec::with_capacity(rates.len());
2524        for rate in rates {
2525            result.push(parse_futures_funding_rate_update(
2526                &rate,
2527                instrument_id,
2528                ts_init,
2529            )?);
2530        }
2531
2532        Ok(result)
2533    }
2534}
2535
2536fn parse_futures_trade_tick(
2537    trade: &BinanceFuturesTrade,
2538    instrument_id: InstrumentId,
2539    price_precision: u8,
2540    size_precision: u8,
2541    ts_init: UnixNanos,
2542) -> anyhow::Result<TradeTick> {
2543    let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
2544        .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
2545    let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
2546        .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
2547    let ts_event = UnixNanos::from_millis(trade.time as u64);
2548
2549    let aggressor_side = if trade.is_buyer_maker {
2550        AggressorSide::Seller
2551    } else {
2552        AggressorSide::Buyer
2553    };
2554
2555    Ok(TradeTick::new(
2556        instrument_id,
2557        price,
2558        size,
2559        aggressor_side,
2560        TradeId::new(trade.id.to_string()),
2561        ts_event,
2562        ts_init,
2563    ))
2564}
2565
2566fn parse_futures_kline_bar(
2567    kline: &BinanceFuturesKline,
2568    bar_type: BarType,
2569    price_precision: u8,
2570    size_precision: u8,
2571    ts_init: UnixNanos,
2572) -> anyhow::Result<Bar> {
2573    let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
2574        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2575    let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
2576        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2577    let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
2578        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2579    let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
2580        .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2581    let volume =
2582        parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
2583            .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2584    let ts_event = UnixNanos::from_millis(kline.close_time as u64);
2585
2586    Ok(Bar::new(
2587        bar_type, open, high, low, close, volume, ts_event, ts_init,
2588    ))
2589}
2590
2591fn parse_futures_funding_rate_update(
2592    rate: &BinanceFundingRate,
2593    instrument_id: InstrumentId,
2594    ts_init: UnixNanos,
2595) -> anyhow::Result<FundingRateUpdate> {
2596    let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
2597        anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
2598    })?;
2599    let ts_event = UnixNanos::from_millis(rate.funding_time as u64);
2600
2601    Ok(FundingRateUpdate::new(
2602        instrument_id,
2603        funding_rate,
2604        None, // Funding interval is not provided by the history endpoint
2605        None, // Next funding time is not provided by the history endpoint
2606        ts_event,
2607        ts_init,
2608    ))
2609}
2610
2611/// Checks if an order type requires the Binance Algo Service API.
2612///
2613/// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
2614/// The traditional `/fapi/v1/order` endpoint returns error `-4120` for these types.
2615#[must_use]
2616pub fn is_algo_order_type(order_type: OrderType) -> bool {
2617    matches!(
2618        order_type,
2619        OrderType::StopMarket
2620            | OrderType::StopLimit
2621            | OrderType::MarketIfTouched
2622            | OrderType::LimitIfTouched
2623            | OrderType::TrailingStopMarket
2624    )
2625}
2626
2627/// Converts a Nautilus order type to a Binance Futures order type.
2628pub(crate) fn order_type_to_binance_futures(
2629    order_type: OrderType,
2630) -> anyhow::Result<BinanceFuturesOrderType> {
2631    match order_type {
2632        OrderType::Market => Ok(BinanceFuturesOrderType::Market),
2633        OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
2634        OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
2635        OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
2636        OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
2637        OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
2638        OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
2639        _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
2640    }
2641}
2642
2643#[cfg(test)]
2644mod tests {
2645    use nautilus_core::time::get_atomic_clock_realtime;
2646    use nautilus_network::http::{HttpStatus, StatusCode};
2647    use rstest::rstest;
2648    use tokio_util::bytes::Bytes;
2649
2650    use super::*;
2651    use crate::common::enums::BinanceTradingStatus;
2652
2653    #[rstest]
2654    fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
2655        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
2656
2657        assert!(config.default_quota.is_some());
2658        assert_eq!(config.order_keys.len(), 2);
2659        assert!(config.order_keys.iter().any(|k| k.contains("Second")));
2660        assert!(config.order_keys.iter().any(|k| k.contains("Minute")));
2661    }
2662
2663    #[rstest]
2664    fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
2665        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
2666
2667        assert!(config.default_quota.is_some());
2668        assert_eq!(config.order_keys.len(), 2);
2669    }
2670
2671    #[rstest]
2672    fn test_quota_from_unknown_interval_returns_none() {
2673        let quota = BinanceRateLimitQuota {
2674            rate_limit_type: BinanceRateLimitType::Orders,
2675            interval: BinanceRateLimitInterval::Unknown,
2676            interval_num: 1,
2677            limit: 10,
2678        };
2679
2680        assert!(BinanceRawFuturesHttpClient::quota_from(&quota).is_none());
2681    }
2682
2683    #[rstest]
2684    fn test_create_client_rejects_spot_product_type() {
2685        let result = BinanceFuturesHttpClient::new(
2686            BinanceProductType::Spot,
2687            BinanceEnvironment::Live,
2688            get_atomic_clock_realtime(),
2689            None,
2690            None,
2691            None,
2692            None,
2693            None,
2694            None,
2695            false,
2696        );
2697
2698        result.unwrap_err();
2699    }
2700
2701    #[rstest]
2702    fn test_parse_futures_trade_tick_rejects_invalid_price() {
2703        let trade = BinanceFuturesTrade {
2704            id: 100,
2705            price: "not-a-number".to_string(),
2706            qty: "0.001".to_string(),
2707            quote_qty: "50.00".to_string(),
2708            time: 1_625_474_304_000,
2709            is_buyer_maker: false,
2710        };
2711
2712        let result = parse_futures_trade_tick(
2713            &trade,
2714            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2715            2,
2716            3,
2717            UnixNanos::from(1_000_000_000u64),
2718        );
2719
2720        let error = result.unwrap_err().to_string();
2721        assert!(error.contains("trade.price"));
2722        assert!(error.contains("100"));
2723    }
2724
2725    #[rstest]
2726    fn test_parse_futures_kline_bar_rejects_invalid_volume() {
2727        let kline = BinanceFuturesKline {
2728            open_time: 1_625_474_304_000,
2729            open: "50000.00".to_string(),
2730            high: "51000.00".to_string(),
2731            low: "49000.00".to_string(),
2732            close: "50500.00".to_string(),
2733            volume: "not-a-number".to_string(),
2734            close_time: 1_625_474_364_000,
2735            quote_volume: "631250.00".to_string(),
2736            num_trades: 100,
2737            taker_buy_base_volume: "6.2".to_string(),
2738            taker_buy_quote_volume: "313100.00".to_string(),
2739        };
2740
2741        let result = parse_futures_kline_bar(
2742            &kline,
2743            BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
2744            2,
2745            3,
2746            UnixNanos::from(1_000_000_000u64),
2747        );
2748
2749        let error = result.unwrap_err().to_string();
2750        assert!(error.contains("kline.volume"));
2751        assert!(error.contains("1625474304000"));
2752    }
2753
2754    fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
2755        BinanceRawFuturesHttpClient::new(
2756            BinanceProductType::UsdM,
2757            BinanceEnvironment::Live,
2758            None,
2759            None,
2760            None,
2761            None,
2762            None,
2763            None,
2764        )
2765        .expect("Failed to create test client")
2766    }
2767
2768    fn create_test_client() -> BinanceFuturesHttpClient {
2769        BinanceFuturesHttpClient::new(
2770            BinanceProductType::UsdM,
2771            BinanceEnvironment::Live,
2772            get_atomic_clock_realtime(),
2773            None,
2774            None,
2775            Some("http://127.0.0.1:1".to_string()),
2776            None,
2777            Some(1),
2778            None,
2779            false,
2780        )
2781        .expect("Failed to create test client")
2782    }
2783
2784    fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
2785        BinanceFuturesUsdSymbol {
2786            symbol: Ustr::from("BTCUSDT"),
2787            pair: Ustr::from("BTCUSDT"),
2788            contract_type: "PERPETUAL".to_string(),
2789            delivery_date: 4_133_404_800_000,
2790            onboard_date: 1_569_398_400_000,
2791            status: BinanceTradingStatus::Trading,
2792            maint_margin_percent: "2.5000".to_string(),
2793            required_margin_percent: "5.0000".to_string(),
2794            base_asset: Ustr::from("BTC"),
2795            quote_asset: Ustr::from("USDT"),
2796            margin_asset: Ustr::from("USDT"),
2797            price_precision: 2,
2798            quantity_precision: 3,
2799            base_asset_precision: 8,
2800            quote_precision: 8,
2801            underlying_type: None,
2802            underlying_sub_type: Vec::new(),
2803            settle_plan: None,
2804            trigger_protect: None,
2805            liquidation_fee: None,
2806            market_take_bound: None,
2807            order_types: Vec::new(),
2808            time_in_force: Vec::new(),
2809            filters: Vec::new(),
2810        }
2811    }
2812
2813    #[rstest]
2814    fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
2815        let client = create_test_client();
2816        client.instruments_cache().insert(
2817            Ustr::from("BTCUSDT"),
2818            BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
2819        );
2820
2821        let (symbol, price_precision, size_precision) = client
2822            .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
2823            .unwrap();
2824
2825        assert_eq!(symbol, "BTCUSDT");
2826        assert_eq!(price_precision, 2);
2827        assert_eq!(size_precision, 3);
2828    }
2829
2830    #[rstest]
2831    #[tokio::test]
2832    async fn test_submit_algo_order_stop_market_requires_trigger_price() {
2833        let client = create_test_client();
2834        client.instruments_cache().insert(
2835            Ustr::from("BTCUSDT"),
2836            BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
2837        );
2838
2839        let result = client
2840            .submit_algo_order(
2841                AccountId::from("BINANCE-001"),
2842                InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2843                ClientOrderId::new("missing-trigger-test-001"),
2844                OrderSide::Sell,
2845                OrderType::StopMarket,
2846                Quantity::from("0.001"),
2847                TimeInForce::Gtc,
2848                None,
2849                None,
2850                false,
2851                false,
2852                None,
2853                None,
2854                None,
2855                None,
2856            )
2857            .await;
2858
2859        let error = result.unwrap_err().to_string();
2860        assert_eq!(error, "Algo order type StopMarket requires a trigger price");
2861    }
2862
2863    #[rstest]
2864    fn test_batch_cancel_params_builds_order_id_list() {
2865        let items = vec![
2866            BatchCancelItem::by_order_id("BTCUSDT", 123),
2867            BatchCancelItem::by_order_id("BTCUSDT", 456),
2868        ];
2869
2870        let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
2871
2872        assert_eq!(params.symbol, "BTCUSDT");
2873        assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
2874        assert_eq!(params.orig_client_order_id_list, None);
2875    }
2876
2877    #[rstest]
2878    fn test_batch_cancel_params_builds_client_order_id_list() {
2879        let items = vec![
2880            BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
2881            BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
2882        ];
2883
2884        let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
2885
2886        assert_eq!(params.symbol, "BTCUSDT");
2887        assert_eq!(params.order_id_list, None);
2888        assert_eq!(
2889            params.orig_client_order_id_list.as_deref(),
2890            Some("[\"first-order\",\"second-order\"]"),
2891        );
2892    }
2893
2894    #[rstest]
2895    fn test_batch_cancel_params_rejects_mixed_symbols() {
2896        let items = vec![
2897            BatchCancelItem::by_order_id("BTCUSDT", 123),
2898            BatchCancelItem::by_order_id("ETHUSDT", 456),
2899        ];
2900
2901        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2902
2903        assert_validation_error(result, "same symbol");
2904    }
2905
2906    #[rstest]
2907    fn test_batch_cancel_params_rejects_mixed_id_types() {
2908        let items = vec![
2909            BatchCancelItem::by_order_id("BTCUSDT", 123),
2910            BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
2911        ];
2912
2913        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2914
2915        assert_validation_error(result, "not both");
2916    }
2917
2918    #[rstest]
2919    fn test_batch_cancel_params_rejects_items_without_ids() {
2920        let items = vec![BatchCancelItem {
2921            symbol: "BTCUSDT".to_string(),
2922            order_id: None,
2923            orig_client_order_id: None,
2924        }];
2925
2926        let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2927
2928        assert_validation_error(result, "at least one order ID or client order ID");
2929    }
2930
2931    #[rstest]
2932    #[tokio::test]
2933    async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
2934        let client = create_test_raw_client();
2935        let items = (0..11)
2936            .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
2937            .collect::<Vec<_>>();
2938
2939        let result = client.batch_cancel_orders(&items).await;
2940
2941        match result {
2942            Err(BinanceFuturesHttpError::ValidationError(message)) => {
2943                assert!(message.contains("10 orders maximum"));
2944            }
2945            other => panic!("Expected ValidationError, was {other:?}"),
2946        }
2947    }
2948
2949    fn assert_validation_error(
2950        result: BinanceFuturesHttpResult<BatchCancelParams>,
2951        expected_message: &str,
2952    ) {
2953        match result {
2954            Err(BinanceFuturesHttpError::ValidationError(message)) => {
2955                assert!(message.contains(expected_message));
2956            }
2957            other => panic!("Expected ValidationError, was {other:?}"),
2958        }
2959    }
2960
2961    #[rstest]
2962    fn test_parse_error_response_binance_error() {
2963        let client = create_test_raw_client();
2964        let response = HttpResponse {
2965            status: HttpStatus::new(StatusCode::BAD_REQUEST),
2966            headers: HashMap::new(),
2967            body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
2968        };
2969
2970        let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
2971
2972        match result {
2973            Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
2974                assert_eq!(code, -1121);
2975                assert_eq!(message, "Invalid symbol.");
2976            }
2977            other => panic!("Expected BinanceError, was {other:?}"),
2978        }
2979    }
2980}