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 chrono::{DateTime, Utc};
21use dashmap::DashMap;
22use nautilus_core::{consts::NAUTILUS_USER_AGENT, nanos::UnixNanos};
23use nautilus_model::{
24    data::{Bar, BarType, TradeTick},
25    enums::{AggregationSource, AggressorSide, BarAggregation, OrderSide, OrderType, TimeInForce},
26    events::AccountState,
27    identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
28    instruments::any::InstrumentAny,
29    reports::{FillReport, OrderStatusReport},
30    types::{Price, Quantity},
31};
32use nautilus_network::{
33    http::{HttpClient, HttpResponse, Method},
34    ratelimiter::quota::Quota,
35};
36use serde::{Deserialize, Serialize, de::DeserializeOwned};
37use ustr::Ustr;
38
39use super::{
40    error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
41    models::{
42        BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
43        BinanceFuturesAccountInfo, BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
44        BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesKline,
45        BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
46        BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
47        BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest, BinanceOrderBook,
48        BinancePositionRisk, BinancePriceTicker, BinanceServerTime, BinanceUserTrade,
49        ListenKeyResponse,
50    },
51    query::{
52        BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAlgoOrderQueryParams,
53        BinanceAllAlgoOrdersParams, BinanceAllOrdersParams, BinanceBookTickerParams,
54        BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams, BinanceCancelOrderParams,
55        BinanceDepthParams, BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
56        BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
57        BinanceOpenAlgoOrdersParams, BinanceOpenInterestParams, BinanceOpenOrdersParams,
58        BinanceOrderQueryParams, BinancePositionRiskParams, BinanceSetLeverageParams,
59        BinanceSetMarginTypeParams, BinanceTicker24hrParams, BinanceTradesParams,
60        BinanceUserTradesParams, ListenKeyParams,
61    },
62};
63use crate::common::{
64    consts::{
65        BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH, BINANCE_FAPI_RATE_LIMITS,
66        BinanceRateLimitQuota,
67    },
68    credential::Credential,
69    enums::{
70        BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
71        BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType, BinanceSide,
72        BinanceTimeInForce,
73    },
74    models::BinanceErrorResponse,
75    parse::{parse_coinm_instrument, parse_usdm_instrument},
76    symbol::{format_binance_symbol, format_instrument_id},
77    urls::get_http_base_url,
78};
79
80const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
81const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
82
83/// Raw HTTP client for Binance Futures REST API.
84#[derive(Debug, Clone)]
85pub struct BinanceRawFuturesHttpClient {
86    client: HttpClient,
87    base_url: String,
88    api_path: &'static str,
89    credential: Option<Credential>,
90    recv_window: Option<u64>,
91    order_rate_keys: Vec<String>,
92}
93
94impl BinanceRawFuturesHttpClient {
95    /// Returns a reference to the underlying HTTP client.
96    #[must_use]
97    pub fn http_client(&self) -> &HttpClient {
98        &self.client
99    }
100
101    /// Creates a new Binance raw futures HTTP client.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if credentials are incomplete or the HTTP client fails to build.
106    #[allow(clippy::too_many_arguments)]
107    pub fn new(
108        product_type: BinanceProductType,
109        environment: BinanceEnvironment,
110        api_key: Option<String>,
111        api_secret: Option<String>,
112        base_url_override: Option<String>,
113        recv_window: Option<u64>,
114        timeout_secs: Option<u64>,
115        proxy_url: Option<String>,
116    ) -> BinanceFuturesHttpResult<Self> {
117        let RateLimitConfig {
118            default_quota,
119            keyed_quotas,
120            order_keys,
121        } = Self::rate_limit_config(product_type);
122
123        let credential = match (api_key, api_secret) {
124            (Some(key), Some(secret)) => Some(Credential::new(key, secret)),
125            (None, None) => None,
126            _ => return Err(BinanceFuturesHttpError::MissingCredentials),
127        };
128
129        let base_url = base_url_override
130            .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
131
132        let api_path = Self::resolve_api_path(product_type);
133        let headers = Self::default_headers(&credential);
134
135        let client = HttpClient::new(
136            headers,
137            vec!["X-MBX-APIKEY".to_string()],
138            keyed_quotas,
139            default_quota,
140            timeout_secs,
141            proxy_url,
142        )?;
143
144        Ok(Self {
145            client,
146            base_url,
147            api_path,
148            credential,
149            recv_window,
150            order_rate_keys: order_keys,
151        })
152    }
153
154    /// Performs a GET request and deserializes the response body.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the request fails or response deserialization fails.
159    pub async fn get<P, T>(
160        &self,
161        path: &str,
162        params: Option<&P>,
163        signed: bool,
164        use_order_quota: bool,
165    ) -> BinanceFuturesHttpResult<T>
166    where
167        P: Serialize + ?Sized,
168        T: DeserializeOwned,
169    {
170        self.request(Method::GET, path, params, signed, use_order_quota, None)
171            .await
172    }
173
174    /// Performs a POST request with optional body and signed query.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the request fails or response deserialization fails.
179    pub async fn post<P, T>(
180        &self,
181        path: &str,
182        params: Option<&P>,
183        body: Option<Vec<u8>>,
184        signed: bool,
185        use_order_quota: bool,
186    ) -> BinanceFuturesHttpResult<T>
187    where
188        P: Serialize + ?Sized,
189        T: DeserializeOwned,
190    {
191        self.request(Method::POST, path, params, signed, use_order_quota, body)
192            .await
193    }
194
195    /// Performs a PUT request with signed query.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the request fails or response deserialization fails.
200    pub async fn request_put<P, T>(
201        &self,
202        path: &str,
203        params: Option<&P>,
204        signed: bool,
205        use_order_quota: bool,
206    ) -> BinanceFuturesHttpResult<T>
207    where
208        P: Serialize + ?Sized,
209        T: DeserializeOwned,
210    {
211        self.request(Method::PUT, path, params, signed, use_order_quota, None)
212            .await
213    }
214
215    /// Performs a DELETE request with signed query.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the request fails or response deserialization fails.
220    pub async fn request_delete<P, T>(
221        &self,
222        path: &str,
223        params: Option<&P>,
224        signed: bool,
225        use_order_quota: bool,
226    ) -> BinanceFuturesHttpResult<T>
227    where
228        P: Serialize + ?Sized,
229        T: DeserializeOwned,
230    {
231        self.request(Method::DELETE, path, params, signed, use_order_quota, None)
232            .await
233    }
234
235    /// Performs a batch POST request with batchOrders parameter.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
240    pub async fn batch_request<T: Serialize>(
241        &self,
242        path: &str,
243        items: &[T],
244        use_order_quota: bool,
245    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
246        self.batch_request_method(Method::POST, path, items, use_order_quota)
247            .await
248    }
249
250    /// Performs a batch DELETE request with batchOrders parameter.
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
255    pub async fn batch_request_delete<T: Serialize>(
256        &self,
257        path: &str,
258        items: &[T],
259        use_order_quota: bool,
260    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
261        self.batch_request_method(Method::DELETE, path, items, use_order_quota)
262            .await
263    }
264
265    /// Performs a batch PUT request with batchOrders parameter.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
270    pub async fn batch_request_put<T: Serialize>(
271        &self,
272        path: &str,
273        items: &[T],
274        use_order_quota: bool,
275    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
276        self.batch_request_method(Method::PUT, path, items, use_order_quota)
277            .await
278    }
279
280    async fn batch_request_method<T: Serialize>(
281        &self,
282        method: Method,
283        path: &str,
284        items: &[T],
285        use_order_quota: bool,
286    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
287        let cred = self
288            .credential
289            .as_ref()
290            .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
291
292        let batch_json = serde_json::to_string(items)
293            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
294
295        let encoded_batch = Self::percent_encode(&batch_json);
296        let timestamp = Utc::now().timestamp_millis();
297        let mut query = format!("batchOrders={encoded_batch}&timestamp={timestamp}");
298
299        if let Some(recv_window) = self.recv_window {
300            query.push_str(&format!("&recvWindow={recv_window}"));
301        }
302
303        let signature = cred.sign(&query);
304        query.push_str(&format!("&signature={signature}"));
305
306        let url = self.build_url(path, &query);
307
308        let mut headers = HashMap::new();
309        headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
310
311        let keys = self.rate_limit_keys(use_order_quota);
312
313        let response = self
314            .client
315            .request(
316                method,
317                url,
318                None::<&HashMap<String, Vec<String>>>,
319                Some(headers),
320                None,
321                None,
322                Some(keys),
323            )
324            .await?;
325
326        if !response.status.is_success() {
327            return self.parse_error_response(response);
328        }
329
330        serde_json::from_slice(&response.body)
331            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
332    }
333
334    /// Percent-encodes a string for use in URL query parameters.
335    fn percent_encode(input: &str) -> String {
336        let mut result = String::with_capacity(input.len() * 3);
337        for byte in input.bytes() {
338            match byte {
339                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
340                    result.push(byte as char);
341                }
342                _ => {
343                    result.push('%');
344                    result.push_str(&format!("{byte:02X}"));
345                }
346            }
347        }
348        result
349    }
350
351    async fn request<P, T>(
352        &self,
353        method: Method,
354        path: &str,
355        params: Option<&P>,
356        signed: bool,
357        use_order_quota: bool,
358        body: Option<Vec<u8>>,
359    ) -> BinanceFuturesHttpResult<T>
360    where
361        P: Serialize + ?Sized,
362        T: DeserializeOwned,
363    {
364        let mut query = params
365            .map(serde_urlencoded::to_string)
366            .transpose()
367            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
368            .unwrap_or_default();
369
370        let mut headers = HashMap::new();
371        if signed {
372            let cred = self
373                .credential
374                .as_ref()
375                .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
376
377            if !query.is_empty() {
378                query.push('&');
379            }
380
381            let timestamp = Utc::now().timestamp_millis();
382            query.push_str(&format!("timestamp={timestamp}"));
383
384            if let Some(recv_window) = self.recv_window {
385                query.push_str(&format!("&recvWindow={recv_window}"));
386            }
387
388            let signature = cred.sign(&query);
389            query.push_str(&format!("&signature={signature}"));
390            headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
391        }
392
393        let url = self.build_url(path, &query);
394        let keys = self.rate_limit_keys(use_order_quota);
395
396        let response = self
397            .client
398            .request(
399                method,
400                url,
401                None::<&HashMap<String, Vec<String>>>,
402                Some(headers),
403                body,
404                None,
405                Some(keys),
406            )
407            .await?;
408
409        if !response.status.is_success() {
410            return self.parse_error_response(response);
411        }
412
413        serde_json::from_slice::<T>(&response.body)
414            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
415    }
416
417    fn build_url(&self, path: &str, query: &str) -> String {
418        // Full API paths (e.g., /fapi/v2/account) bypass the default api_path
419        let url_path = if path.starts_with("/fapi/") || path.starts_with("/dapi/") {
420            path.to_string()
421        } else if path.starts_with('/') {
422            format!("{}{}", self.api_path, path)
423        } else {
424            format!("{}/{}", self.api_path, path)
425        };
426
427        let mut url = format!("{}{}", self.base_url, url_path);
428        if !query.is_empty() {
429            url.push('?');
430            url.push_str(query);
431        }
432        url
433    }
434
435    fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
436        if use_orders {
437            let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
438            keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
439            keys.extend(self.order_rate_keys.iter().cloned());
440            keys
441        } else {
442            vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
443        }
444    }
445
446    fn parse_error_response<T>(&self, response: HttpResponse) -> BinanceFuturesHttpResult<T> {
447        let status = response.status.as_u16();
448        let body = String::from_utf8_lossy(&response.body).to_string();
449
450        if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
451            return Err(BinanceFuturesHttpError::BinanceError {
452                code: err.code,
453                message: err.msg,
454            });
455        }
456
457        Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
458    }
459
460    fn default_headers(credential: &Option<Credential>) -> HashMap<String, String> {
461        let mut headers = HashMap::new();
462        headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
463        if let Some(cred) = credential {
464            headers.insert("X-MBX-APIKEY".to_string(), cred.api_key().to_string());
465        }
466        headers
467    }
468
469    fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
470        match product_type {
471            BinanceProductType::UsdM => BINANCE_FAPI_PATH,
472            BinanceProductType::CoinM => BINANCE_DAPI_PATH,
473            _ => BINANCE_FAPI_PATH, // Default to USD-M
474        }
475    }
476
477    fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
478        let quotas = match product_type {
479            BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
480            BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
481            _ => BINANCE_FAPI_RATE_LIMITS,
482        };
483
484        let mut keyed = Vec::new();
485        let mut order_keys = Vec::new();
486        let mut default = None;
487
488        for quota in quotas {
489            if let Some(q) = Self::quota_from(quota) {
490                match quota.rate_limit_type {
491                    BinanceRateLimitType::RequestWeight if default.is_none() => {
492                        default = Some(q);
493                    }
494                    BinanceRateLimitType::Orders => {
495                        let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
496                        order_keys.push(key.clone());
497                        keyed.push((key, q));
498                    }
499                    _ => {}
500                }
501            }
502        }
503
504        let default_quota =
505            default.unwrap_or_else(|| Quota::per_second(NonZeroU32::new(10).unwrap()));
506
507        keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
508
509        RateLimitConfig {
510            default_quota: Some(default_quota),
511            keyed_quotas: keyed,
512            order_keys,
513        }
514    }
515
516    fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
517        let burst = NonZeroU32::new(quota.limit)?;
518        match quota.interval {
519            BinanceRateLimitInterval::Second => Some(Quota::per_second(burst)),
520            BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
521            BinanceRateLimitInterval::Day => {
522                Quota::with_period(Duration::from_secs(86_400)).map(|q| q.allow_burst(burst))
523            }
524        }
525    }
526
527    /// Fetches 24hr ticker statistics.
528    ///
529    /// # Errors
530    ///
531    /// Returns an error if the request fails.
532    pub async fn ticker_24h(
533        &self,
534        params: &BinanceTicker24hrParams,
535    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
536        self.get("ticker/24hr", Some(params), false, false).await
537    }
538
539    /// Fetches best bid/ask prices.
540    ///
541    /// # Errors
542    ///
543    /// Returns an error if the request fails.
544    pub async fn book_ticker(
545        &self,
546        params: &BinanceBookTickerParams,
547    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
548        self.get("ticker/bookTicker", Some(params), false, false)
549            .await
550    }
551
552    /// Fetches price ticker.
553    ///
554    /// # Errors
555    ///
556    /// Returns an error if the request fails.
557    pub async fn price_ticker(
558        &self,
559        symbol: Option<&str>,
560    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
561        #[derive(Serialize)]
562        struct Params<'a> {
563            #[serde(skip_serializing_if = "Option::is_none")]
564            symbol: Option<&'a str>,
565        }
566        self.get("ticker/price", Some(&Params { symbol }), false, false)
567            .await
568    }
569
570    /// Fetches order book depth.
571    ///
572    /// # Errors
573    ///
574    /// Returns an error if the request fails.
575    pub async fn depth(
576        &self,
577        params: &BinanceDepthParams,
578    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
579        self.get("depth", Some(params), false, false).await
580    }
581
582    /// Fetches mark price and funding rate.
583    ///
584    /// # Errors
585    ///
586    /// Returns an error if the request fails.
587    pub async fn mark_price(
588        &self,
589        params: &BinanceMarkPriceParams,
590    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
591        let response: MarkPriceResponse =
592            self.get("premiumIndex", Some(params), false, false).await?;
593        Ok(response.into())
594    }
595
596    /// Fetches funding rate history.
597    ///
598    /// # Errors
599    ///
600    /// Returns an error if the request fails.
601    pub async fn funding_rate(
602        &self,
603        params: &BinanceFundingRateParams,
604    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
605        self.get("fundingRate", Some(params), false, false).await
606    }
607
608    /// Fetches current open interest for a symbol.
609    ///
610    /// # Errors
611    ///
612    /// Returns an error if the request fails.
613    pub async fn open_interest(
614        &self,
615        params: &BinanceOpenInterestParams,
616    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
617        self.get("openInterest", Some(params), false, false).await
618    }
619
620    /// Fetches recent public trades for a symbol.
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if the request fails.
625    pub async fn trades(
626        &self,
627        params: &BinanceTradesParams,
628    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
629        self.get("trades", Some(params), false, false).await
630    }
631
632    /// Fetches kline/candlestick data for a symbol.
633    ///
634    /// # Errors
635    ///
636    /// Returns an error if the request fails.
637    pub async fn klines(
638        &self,
639        params: &BinanceKlinesParams,
640    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
641        self.get("klines", Some(params), false, false).await
642    }
643
644    /// Sets leverage for a symbol.
645    ///
646    /// # Errors
647    ///
648    /// Returns an error if the request fails.
649    pub async fn set_leverage(
650        &self,
651        params: &BinanceSetLeverageParams,
652    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
653        self.post("leverage", Some(params), None, true, false).await
654    }
655
656    /// Sets margin type for a symbol.
657    ///
658    /// # Errors
659    ///
660    /// Returns an error if the request fails.
661    pub async fn set_margin_type(
662        &self,
663        params: &BinanceSetMarginTypeParams,
664    ) -> BinanceFuturesHttpResult<serde_json::Value> {
665        self.post("marginType", Some(params), None, true, false)
666            .await
667    }
668
669    /// Queries hedge mode (dual side position) setting.
670    ///
671    /// # Errors
672    ///
673    /// Returns an error if the request fails.
674    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
675        self.get::<(), _>("positionSide/dual", None, true, false)
676            .await
677    }
678
679    /// Creates a listen key for user data stream.
680    ///
681    /// # Errors
682    ///
683    /// Returns an error if the request fails.
684    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
685        self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
686            .await
687    }
688
689    /// Keeps alive an existing listen key.
690    ///
691    /// # Errors
692    ///
693    /// Returns an error if the request fails.
694    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
695        let params = ListenKeyParams {
696            listen_key: listen_key.to_string(),
697        };
698        let _: serde_json::Value = self
699            .request_put("listenKey", Some(&params), true, false)
700            .await?;
701        Ok(())
702    }
703
704    /// Closes an existing listen key.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if the request fails.
709    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
710        let params = ListenKeyParams {
711            listen_key: listen_key.to_string(),
712        };
713        let _: serde_json::Value = self
714            .request_delete("listenKey", Some(&params), true, false)
715            .await?;
716        Ok(())
717    }
718
719    /// Fetches account information including balances and positions.
720    ///
721    /// # Errors
722    ///
723    /// Returns an error if the request fails.
724    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
725        // USD-M uses /fapi/v2/account, COIN-M uses /dapi/v1/account
726        let path = if self.api_path.starts_with("/fapi") {
727            "/fapi/v2/account"
728        } else {
729            "/dapi/v1/account"
730        };
731        self.get::<(), _>(path, None, true, false).await
732    }
733
734    /// Fetches position risk information.
735    ///
736    /// # Errors
737    ///
738    /// Returns an error if the request fails.
739    pub async fn query_positions(
740        &self,
741        params: &BinancePositionRiskParams,
742    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
743        // USD-M uses /fapi/v2/positionRisk, COIN-M uses /dapi/v1/positionRisk
744        let path = if self.api_path.starts_with("/fapi") {
745            "/fapi/v2/positionRisk"
746        } else {
747            "/dapi/v1/positionRisk"
748        };
749        self.get(path, Some(params), true, false).await
750    }
751
752    /// Fetches user trades for a symbol.
753    ///
754    /// # Errors
755    ///
756    /// Returns an error if the request fails.
757    pub async fn query_user_trades(
758        &self,
759        params: &BinanceUserTradesParams,
760    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
761        self.get("userTrades", Some(params), true, false).await
762    }
763
764    /// Queries a single order by order ID or client order ID.
765    ///
766    /// # Errors
767    ///
768    /// Returns an error if the request fails.
769    pub async fn query_order(
770        &self,
771        params: &BinanceOrderQueryParams,
772    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
773        self.get("order", Some(params), true, false).await
774    }
775
776    /// Queries all open orders.
777    ///
778    /// # Errors
779    ///
780    /// Returns an error if the request fails.
781    pub async fn query_open_orders(
782        &self,
783        params: &BinanceOpenOrdersParams,
784    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
785        self.get("openOrders", Some(params), true, false).await
786    }
787
788    /// Queries all orders (including historical).
789    ///
790    /// # Errors
791    ///
792    /// Returns an error if the request fails.
793    pub async fn query_all_orders(
794        &self,
795        params: &BinanceAllOrdersParams,
796    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
797        self.get("allOrders", Some(params), true, false).await
798    }
799
800    /// Submits a new order.
801    ///
802    /// # Errors
803    ///
804    /// Returns an error if the request fails.
805    pub async fn submit_order(
806        &self,
807        params: &BinanceNewOrderParams,
808    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
809        self.post("order", Some(params), None, true, true).await
810    }
811
812    /// Submits multiple orders in a single request (up to 5 orders).
813    ///
814    /// # Errors
815    ///
816    /// Returns an error if the batch exceeds 5 orders or the request fails.
817    pub async fn submit_order_list(
818        &self,
819        orders: &[BatchOrderItem],
820    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
821        if orders.is_empty() {
822            return Ok(Vec::new());
823        }
824
825        if orders.len() > 5 {
826            return Err(BinanceFuturesHttpError::ValidationError(
827                "Batch order limit is 5 orders maximum".to_string(),
828            ));
829        }
830
831        self.batch_request("batchOrders", orders, true).await
832    }
833
834    /// Modifies an existing order (price and quantity only).
835    ///
836    /// # Errors
837    ///
838    /// Returns an error if the request fails.
839    pub async fn modify_order(
840        &self,
841        params: &BinanceModifyOrderParams,
842    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
843        self.request_put("order", Some(params), true, true).await
844    }
845
846    /// Modifies multiple orders in a single request (up to 5 orders).
847    ///
848    /// # Errors
849    ///
850    /// Returns an error if the batch exceeds 5 orders or the request fails.
851    pub async fn batch_modify_orders(
852        &self,
853        modifies: &[BatchModifyItem],
854    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
855        if modifies.is_empty() {
856            return Ok(Vec::new());
857        }
858
859        if modifies.len() > 5 {
860            return Err(BinanceFuturesHttpError::ValidationError(
861                "Batch modify limit is 5 orders maximum".to_string(),
862            ));
863        }
864
865        self.batch_request_put("batchOrders", modifies, true).await
866    }
867
868    /// Cancels an existing order.
869    ///
870    /// # Errors
871    ///
872    /// Returns an error if the request fails.
873    pub async fn cancel_order(
874        &self,
875        params: &BinanceCancelOrderParams,
876    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
877        self.request_delete("order", Some(params), true, true).await
878    }
879
880    /// Cancels all open orders for a symbol.
881    ///
882    /// # Errors
883    ///
884    /// Returns an error if the request fails.
885    pub async fn cancel_all_orders(
886        &self,
887        params: &BinanceCancelAllOrdersParams,
888    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
889        self.request_delete("allOpenOrders", Some(params), true, true)
890            .await
891    }
892
893    /// Cancels multiple orders in a single request (up to 5 orders).
894    ///
895    /// # Errors
896    ///
897    /// Returns an error if the batch exceeds 5 orders or the request fails.
898    pub async fn batch_cancel_orders(
899        &self,
900        cancels: &[BatchCancelItem],
901    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
902        if cancels.is_empty() {
903            return Ok(Vec::new());
904        }
905
906        if cancels.len() > 5 {
907            return Err(BinanceFuturesHttpError::ValidationError(
908                "Batch cancel limit is 5 orders maximum".to_string(),
909            ));
910        }
911
912        self.batch_request_delete("batchOrders", cancels, true)
913            .await
914    }
915
916    /// Submits a new algo order (conditional order).
917    ///
918    /// Algo orders include STOP_MARKET, STOP (stop-limit), TAKE_PROFIT, TAKE_PROFIT_MARKET,
919    /// and TRAILING_STOP_MARKET order types.
920    ///
921    /// # Errors
922    ///
923    /// Returns an error if the request fails.
924    pub async fn submit_algo_order(
925        &self,
926        params: &BinanceNewAlgoOrderParams,
927    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
928        self.post("algoOrder", Some(params), None, true, true).await
929    }
930
931    /// Cancels an algo order.
932    ///
933    /// Must provide either `algo_id` or `client_algo_id`.
934    ///
935    /// # Errors
936    ///
937    /// Returns an error if the request fails.
938    pub async fn cancel_algo_order(
939        &self,
940        params: &BinanceAlgoOrderQueryParams,
941    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
942        self.request_delete("algoOrder", Some(params), true, true)
943            .await
944    }
945
946    /// Queries a single algo order.
947    ///
948    /// Must provide either `algo_id` or `client_algo_id`.
949    ///
950    /// # Errors
951    ///
952    /// Returns an error if the request fails.
953    pub async fn query_algo_order(
954        &self,
955        params: &BinanceAlgoOrderQueryParams,
956    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
957        self.get("algoOrder", Some(params), true, false).await
958    }
959
960    /// Queries all open algo orders.
961    ///
962    /// # Errors
963    ///
964    /// Returns an error if the request fails.
965    pub async fn query_open_algo_orders(
966        &self,
967        params: &BinanceOpenAlgoOrdersParams,
968    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
969        self.get("openAlgoOrders", Some(params), true, false).await
970    }
971
972    /// Queries all algo orders including historical (7-day limit).
973    ///
974    /// # Errors
975    ///
976    /// Returns an error if the request fails.
977    pub async fn query_all_algo_orders(
978        &self,
979        params: &BinanceAllAlgoOrdersParams,
980    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
981        self.get("allAlgoOrders", Some(params), true, false).await
982    }
983
984    /// Cancels all open algo orders for a symbol.
985    ///
986    /// # Errors
987    ///
988    /// Returns an error if the request fails.
989    pub async fn cancel_all_algo_orders(
990        &self,
991        params: &BinanceCancelAllAlgoOrdersParams,
992    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
993        self.request_delete("algoOpenOrders", Some(params), true, true)
994            .await
995    }
996}
997
998/// Response wrapper for mark price endpoint.
999#[derive(Debug, Deserialize)]
1000#[serde(untagged)]
1001enum MarkPriceResponse {
1002    Single(BinanceFuturesMarkPrice),
1003    Multiple(Vec<BinanceFuturesMarkPrice>),
1004}
1005
1006impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1007    fn from(response: MarkPriceResponse) -> Self {
1008        match response {
1009            MarkPriceResponse::Single(price) => vec![price],
1010            MarkPriceResponse::Multiple(prices) => prices,
1011        }
1012    }
1013}
1014
1015struct RateLimitConfig {
1016    default_quota: Option<Quota>,
1017    keyed_quotas: Vec<(String, Quota)>,
1018    order_keys: Vec<String>,
1019}
1020
1021/// In-memory cache entry for Binance Futures instruments.
1022#[derive(Clone, Debug)]
1023pub enum BinanceFuturesInstrument {
1024    /// USD-M futures symbol.
1025    UsdM(BinanceFuturesUsdSymbol),
1026    /// COIN-M futures symbol.
1027    CoinM(BinanceFuturesCoinSymbol),
1028}
1029
1030impl BinanceFuturesInstrument {
1031    /// Returns the symbol name for the instrument.
1032    #[must_use]
1033    pub const fn symbol(&self) -> Ustr {
1034        match self {
1035            Self::UsdM(s) => s.symbol,
1036            Self::CoinM(s) => s.symbol,
1037        }
1038    }
1039
1040    /// Returns the price precision for the instrument.
1041    #[must_use]
1042    pub const fn price_precision(&self) -> i32 {
1043        match self {
1044            Self::UsdM(s) => s.price_precision,
1045            Self::CoinM(s) => s.price_precision,
1046        }
1047    }
1048
1049    /// Returns the quantity precision for the instrument.
1050    #[must_use]
1051    pub const fn quantity_precision(&self) -> i32 {
1052        match self {
1053            Self::UsdM(s) => s.quantity_precision,
1054            Self::CoinM(s) => s.quantity_precision,
1055        }
1056    }
1057
1058    /// Returns the Nautilus-formatted instrument ID.
1059    #[must_use]
1060    pub fn id(&self) -> InstrumentId {
1061        match self {
1062            Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1063            Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1064        }
1065    }
1066}
1067
1068/// Binance Futures HTTP client for USD-M and COIN-M perpetuals.
1069#[derive(Debug, Clone)]
1070#[cfg_attr(
1071    feature = "python",
1072    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance")
1073)]
1074pub struct BinanceFuturesHttpClient {
1075    raw: BinanceRawFuturesHttpClient,
1076    product_type: BinanceProductType,
1077    instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1078}
1079
1080impl BinanceFuturesHttpClient {
1081    /// Creates a new [`BinanceFuturesHttpClient`] instance.
1082    ///
1083    /// # Errors
1084    ///
1085    /// Returns an error if the product type is invalid or HTTP client creation fails.
1086    #[allow(clippy::too_many_arguments)]
1087    pub fn new(
1088        product_type: BinanceProductType,
1089        environment: BinanceEnvironment,
1090        api_key: Option<String>,
1091        api_secret: Option<String>,
1092        base_url_override: Option<String>,
1093        recv_window: Option<u64>,
1094        timeout_secs: Option<u64>,
1095        proxy_url: Option<String>,
1096    ) -> BinanceFuturesHttpResult<Self> {
1097        match product_type {
1098            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1099            _ => {
1100                return Err(BinanceFuturesHttpError::ValidationError(format!(
1101                    "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1102                )));
1103            }
1104        }
1105
1106        let raw = BinanceRawFuturesHttpClient::new(
1107            product_type,
1108            environment,
1109            api_key,
1110            api_secret,
1111            base_url_override,
1112            recv_window,
1113            timeout_secs,
1114            proxy_url,
1115        )?;
1116
1117        Ok(Self {
1118            raw,
1119            product_type,
1120            instruments: Arc::new(DashMap::new()),
1121        })
1122    }
1123
1124    /// Returns the product type (UsdM or CoinM).
1125    #[must_use]
1126    pub const fn product_type(&self) -> BinanceProductType {
1127        self.product_type
1128    }
1129
1130    /// Returns a reference to the underlying raw HTTP client.
1131    #[must_use]
1132    pub const fn raw(&self) -> &BinanceRawFuturesHttpClient {
1133        &self.raw
1134    }
1135
1136    /// Returns a clone of the instruments cache Arc.
1137    #[must_use]
1138    pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1139        Arc::clone(&self.instruments)
1140    }
1141
1142    /// Returns server time.
1143    ///
1144    /// # Errors
1145    ///
1146    /// Returns an error if the request fails.
1147    pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1148        self.raw
1149            .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1150            .await
1151    }
1152
1153    /// Sets leverage for a symbol.
1154    ///
1155    /// # Errors
1156    ///
1157    /// Returns an error if the request fails.
1158    pub async fn set_leverage(
1159        &self,
1160        params: &BinanceSetLeverageParams,
1161    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1162        self.raw.set_leverage(params).await
1163    }
1164
1165    /// Sets margin type for a symbol.
1166    ///
1167    /// # Errors
1168    ///
1169    /// Returns an error if the request fails.
1170    pub async fn set_margin_type(
1171        &self,
1172        params: &BinanceSetMarginTypeParams,
1173    ) -> BinanceFuturesHttpResult<serde_json::Value> {
1174        self.raw.set_margin_type(params).await
1175    }
1176
1177    /// Queries hedge mode (dual side position) setting.
1178    ///
1179    /// # Errors
1180    ///
1181    /// Returns an error if the request fails.
1182    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1183        self.raw.query_hedge_mode().await
1184    }
1185
1186    /// Creates a listen key for user data stream.
1187    ///
1188    /// # Errors
1189    ///
1190    /// Returns an error if the request fails.
1191    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1192        self.raw.create_listen_key().await
1193    }
1194
1195    /// Keeps alive an existing listen key.
1196    ///
1197    /// # Errors
1198    ///
1199    /// Returns an error if the request fails.
1200    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1201        self.raw.keepalive_listen_key(listen_key).await
1202    }
1203
1204    /// Closes an existing listen key.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns an error if the request fails.
1209    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1210        self.raw.close_listen_key(listen_key).await
1211    }
1212
1213    /// Fetches exchange information and populates the instrument cache.
1214    ///
1215    /// # Errors
1216    ///
1217    /// Returns an error if the request fails or the product type is invalid.
1218    pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1219        match self.product_type {
1220            BinanceProductType::UsdM => {
1221                let info: BinanceFuturesUsdExchangeInfo = self
1222                    .raw
1223                    .get("exchangeInfo", None::<&()>, false, false)
1224                    .await?;
1225                for symbol in info.symbols {
1226                    self.instruments
1227                        .insert(symbol.symbol, BinanceFuturesInstrument::UsdM(symbol));
1228                }
1229            }
1230            BinanceProductType::CoinM => {
1231                let info: BinanceFuturesCoinExchangeInfo = self
1232                    .raw
1233                    .get("exchangeInfo", None::<&()>, false, false)
1234                    .await?;
1235                for symbol in info.symbols {
1236                    self.instruments
1237                        .insert(symbol.symbol, BinanceFuturesInstrument::CoinM(symbol));
1238                }
1239            }
1240            _ => {
1241                return Err(BinanceFuturesHttpError::ValidationError(
1242                    "Invalid product type for futures".to_string(),
1243                ));
1244            }
1245        }
1246
1247        Ok(())
1248    }
1249
1250    /// Fetches exchange information and returns parsed Nautilus instruments.
1251    ///
1252    /// # Errors
1253    ///
1254    /// Returns an error if the request fails or the product type is invalid.
1255    pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1256        let ts_init = UnixNanos::default();
1257
1258        let instruments = match self.product_type {
1259            BinanceProductType::UsdM => {
1260                let info: BinanceFuturesUsdExchangeInfo = self
1261                    .raw
1262                    .get("exchangeInfo", None::<&()>, false, false)
1263                    .await?;
1264
1265                let mut instruments = Vec::with_capacity(info.symbols.len());
1266
1267                for symbol in info.symbols {
1268                    // Cache symbol for precision lookups
1269                    self.instruments.insert(
1270                        symbol.symbol,
1271                        BinanceFuturesInstrument::UsdM(symbol.clone()),
1272                    );
1273
1274                    match parse_usdm_instrument(&symbol, ts_init, ts_init) {
1275                        Ok(instrument) => instruments.push(instrument),
1276                        Err(e) => {
1277                            log::debug!(
1278                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1279                                symbol.symbol
1280                            );
1281                        }
1282                    }
1283                }
1284
1285                log::info!(
1286                    "Loaded USD-M perpetual instruments: count={}",
1287                    instruments.len()
1288                );
1289                instruments
1290            }
1291            BinanceProductType::CoinM => {
1292                let info: BinanceFuturesCoinExchangeInfo = self
1293                    .raw
1294                    .get("exchangeInfo", None::<&()>, false, false)
1295                    .await?;
1296
1297                let mut instruments = Vec::with_capacity(info.symbols.len());
1298                for symbol in info.symbols {
1299                    // Cache symbol for precision lookups
1300                    self.instruments.insert(
1301                        symbol.symbol,
1302                        BinanceFuturesInstrument::CoinM(symbol.clone()),
1303                    );
1304
1305                    match parse_coinm_instrument(&symbol, ts_init, ts_init) {
1306                        Ok(instrument) => instruments.push(instrument),
1307                        Err(e) => {
1308                            log::debug!(
1309                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1310                                symbol.symbol
1311                            );
1312                        }
1313                    }
1314                }
1315
1316                log::info!(
1317                    "Loaded COIN-M perpetual instruments: count={}",
1318                    instruments.len()
1319                );
1320                instruments
1321            }
1322            _ => {
1323                return Err(BinanceFuturesHttpError::ValidationError(
1324                    "Invalid product type for futures".to_string(),
1325                ));
1326            }
1327        };
1328
1329        Ok(instruments)
1330    }
1331
1332    /// Fetches 24hr ticker statistics.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns an error if the request fails.
1337    pub async fn ticker_24h(
1338        &self,
1339        params: &BinanceTicker24hrParams,
1340    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1341        self.raw.ticker_24h(params).await
1342    }
1343
1344    /// Fetches best bid/ask prices.
1345    ///
1346    /// # Errors
1347    ///
1348    /// Returns an error if the request fails.
1349    pub async fn book_ticker(
1350        &self,
1351        params: &BinanceBookTickerParams,
1352    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1353        self.raw.book_ticker(params).await
1354    }
1355
1356    /// Fetches price ticker.
1357    ///
1358    /// # Errors
1359    ///
1360    /// Returns an error if the request fails.
1361    pub async fn price_ticker(
1362        &self,
1363        symbol: Option<&str>,
1364    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1365        self.raw.price_ticker(symbol).await
1366    }
1367
1368    /// Fetches order book depth.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns an error if the request fails.
1373    pub async fn depth(
1374        &self,
1375        params: &BinanceDepthParams,
1376    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1377        self.raw.depth(params).await
1378    }
1379
1380    /// Fetches mark price and funding rate.
1381    ///
1382    /// # Errors
1383    ///
1384    /// Returns an error if the request fails.
1385    pub async fn mark_price(
1386        &self,
1387        params: &BinanceMarkPriceParams,
1388    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
1389        self.raw.mark_price(params).await
1390    }
1391
1392    /// Fetches funding rate history.
1393    ///
1394    /// # Errors
1395    ///
1396    /// Returns an error if the request fails.
1397    pub async fn funding_rate(
1398        &self,
1399        params: &BinanceFundingRateParams,
1400    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
1401        self.raw.funding_rate(params).await
1402    }
1403
1404    /// Fetches current open interest for a symbol.
1405    ///
1406    /// # Errors
1407    ///
1408    /// Returns an error if the request fails.
1409    pub async fn open_interest(
1410        &self,
1411        params: &BinanceOpenInterestParams,
1412    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
1413        self.raw.open_interest(params).await
1414    }
1415
1416    /// Queries a single order by order ID or client order ID.
1417    ///
1418    /// # Errors
1419    ///
1420    /// Returns an error if the request fails.
1421    pub async fn query_order(
1422        &self,
1423        params: &BinanceOrderQueryParams,
1424    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1425        self.raw.query_order(params).await
1426    }
1427
1428    /// Queries all open orders.
1429    ///
1430    /// # Errors
1431    ///
1432    /// Returns an error if the request fails.
1433    pub async fn query_open_orders(
1434        &self,
1435        params: &BinanceOpenOrdersParams,
1436    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1437        self.raw.query_open_orders(params).await
1438    }
1439
1440    /// Queries all orders (including historical).
1441    ///
1442    /// # Errors
1443    ///
1444    /// Returns an error if the request fails.
1445    pub async fn query_all_orders(
1446        &self,
1447        params: &BinanceAllOrdersParams,
1448    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1449        self.raw.query_all_orders(params).await
1450    }
1451
1452    /// Fetches account information including balances and positions.
1453    ///
1454    /// # Errors
1455    ///
1456    /// Returns an error if the request fails.
1457    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1458        self.raw.query_account().await
1459    }
1460
1461    /// Fetches position risk information.
1462    ///
1463    /// # Errors
1464    ///
1465    /// Returns an error if the request fails.
1466    pub async fn query_positions(
1467        &self,
1468        params: &BinancePositionRiskParams,
1469    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1470        self.raw.query_positions(params).await
1471    }
1472
1473    /// Fetches user trades for a symbol.
1474    ///
1475    /// # Errors
1476    ///
1477    /// Returns an error if the request fails.
1478    pub async fn query_user_trades(
1479        &self,
1480        params: &BinanceUserTradesParams,
1481    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1482        self.raw.query_user_trades(params).await
1483    }
1484
1485    /// Submits a new order.
1486    ///
1487    /// # Errors
1488    ///
1489    /// Returns an error if:
1490    /// - The instrument is not cached.
1491    /// - The order type or time-in-force is unsupported.
1492    /// - Stop orders are submitted without a trigger price.
1493    /// - The request fails.
1494    #[allow(clippy::too_many_arguments)]
1495    pub async fn submit_order(
1496        &self,
1497        account_id: AccountId,
1498        instrument_id: InstrumentId,
1499        client_order_id: ClientOrderId,
1500        order_side: OrderSide,
1501        order_type: OrderType,
1502        quantity: Quantity,
1503        time_in_force: TimeInForce,
1504        price: Option<Price>,
1505        trigger_price: Option<Price>,
1506        reduce_only: bool,
1507        position_side: Option<BinancePositionSide>,
1508    ) -> anyhow::Result<OrderStatusReport> {
1509        let symbol = format_binance_symbol(&instrument_id);
1510        let size_precision = self.get_size_precision(&symbol)?;
1511
1512        let binance_side = BinanceSide::try_from(order_side)?;
1513        let binance_order_type = order_type_to_binance_futures(order_type)?;
1514        let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1515
1516        let requires_trigger_price = matches!(
1517            order_type,
1518            OrderType::StopMarket
1519                | OrderType::StopLimit
1520                | OrderType::TrailingStopMarket
1521                | OrderType::MarketIfTouched
1522                | OrderType::LimitIfTouched
1523        );
1524        if requires_trigger_price && trigger_price.is_none() {
1525            anyhow::bail!("Order type {order_type:?} requires a trigger price");
1526        }
1527
1528        // MARKET and STOP_MARKET orders don't accept timeInForce
1529        let requires_time_in_force = matches!(
1530            order_type,
1531            OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
1532        );
1533
1534        let qty_str = quantity.to_string();
1535        let price_str = price.map(|p| p.to_string());
1536        let stop_price_str = trigger_price.map(|p| p.to_string());
1537        let client_id_str = client_order_id.to_string();
1538
1539        let params = BinanceNewOrderParams {
1540            symbol,
1541            side: binance_side,
1542            order_type: binance_order_type,
1543            time_in_force: if requires_time_in_force {
1544                Some(binance_tif)
1545            } else {
1546                None
1547            },
1548            quantity: Some(qty_str),
1549            price: price_str,
1550            new_client_order_id: Some(client_id_str),
1551            stop_price: stop_price_str,
1552            reduce_only: if reduce_only { Some(true) } else { None },
1553            position_side,
1554            close_position: None,
1555            activation_price: None,
1556            callback_rate: None,
1557            working_type: None,
1558            price_protect: None,
1559            new_order_resp_type: None,
1560            good_till_date: None,
1561            recv_window: None,
1562            price_match: None,
1563            self_trade_prevention_mode: None,
1564        };
1565
1566        let order = self.raw.submit_order(&params).await?;
1567        order.to_order_status_report(account_id, instrument_id, size_precision)
1568    }
1569
1570    /// Submits an algo order (conditional order) to the Binance Algo Service.
1571    ///
1572    /// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
1573    /// This method handles StopMarket, StopLimit, MarketIfTouched, LimitIfTouched,
1574    /// and TrailingStopMarket orders.
1575    ///
1576    /// # Errors
1577    ///
1578    /// Returns an error if:
1579    /// - The order type requires a trigger price but none is provided.
1580    /// - The instrument is not cached.
1581    /// - The request fails.
1582    #[allow(clippy::too_many_arguments)]
1583    pub async fn submit_algo_order(
1584        &self,
1585        account_id: AccountId,
1586        instrument_id: InstrumentId,
1587        client_order_id: ClientOrderId,
1588        order_side: OrderSide,
1589        order_type: OrderType,
1590        quantity: Quantity,
1591        time_in_force: TimeInForce,
1592        price: Option<Price>,
1593        trigger_price: Option<Price>,
1594        reduce_only: bool,
1595        position_side: Option<BinancePositionSide>,
1596    ) -> anyhow::Result<OrderStatusReport> {
1597        let symbol = format_binance_symbol(&instrument_id);
1598        let size_precision = self.get_size_precision(&symbol)?;
1599
1600        let binance_side = BinanceSide::try_from(order_side)?;
1601        let binance_order_type = order_type_to_binance_futures(order_type)?;
1602        let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1603
1604        anyhow::ensure!(
1605            trigger_price.is_some(),
1606            "Algo order type {order_type:?} requires a trigger price"
1607        );
1608
1609        // Limit orders require time in force
1610        let requires_time_in_force =
1611            matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
1612
1613        let qty_str = quantity.to_string();
1614        let price_str = price.map(|p| p.to_string());
1615        let trigger_price_str = trigger_price.map(|p| p.to_string());
1616        let client_id_str = client_order_id.to_string();
1617
1618        let params = BinanceNewAlgoOrderParams {
1619            symbol,
1620            side: binance_side,
1621            order_type: binance_order_type,
1622            algo_type: BinanceAlgoType::Conditional,
1623            position_side,
1624            quantity: Some(qty_str),
1625            price: price_str,
1626            trigger_price: trigger_price_str,
1627            time_in_force: if requires_time_in_force {
1628                Some(binance_tif)
1629            } else {
1630                None
1631            },
1632            working_type: None,
1633            close_position: None,
1634            price_protect: None,
1635            reduce_only: if reduce_only { Some(true) } else { None },
1636            activation_price: None,
1637            callback_rate: None,
1638            client_algo_id: Some(client_id_str),
1639            good_till_date: None,
1640            recv_window: None,
1641        };
1642
1643        let order = self.raw.submit_algo_order(&params).await?;
1644        order.to_order_status_report(account_id, instrument_id, size_precision)
1645    }
1646
1647    /// Submits multiple orders in a single request (up to 5 orders).
1648    ///
1649    /// Each order in the batch is processed independently. The response contains
1650    /// the result for each order, which can be either a success or an error.
1651    ///
1652    /// # Errors
1653    ///
1654    /// Returns an error if the batch exceeds 5 orders or the request fails.
1655    pub async fn submit_order_list(
1656        &self,
1657        orders: &[BatchOrderItem],
1658    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1659        self.raw.submit_order_list(orders).await
1660    }
1661
1662    /// Modifies an existing order (price and quantity only).
1663    ///
1664    /// Either `venue_order_id` or `client_order_id` must be provided.
1665    ///
1666    /// # Errors
1667    ///
1668    /// Returns an error if:
1669    /// - Neither venue_order_id nor client_order_id is provided.
1670    /// - The instrument is not cached.
1671    /// - The request fails.
1672    #[allow(clippy::too_many_arguments)]
1673    pub async fn modify_order(
1674        &self,
1675        account_id: AccountId,
1676        instrument_id: InstrumentId,
1677        venue_order_id: Option<VenueOrderId>,
1678        client_order_id: Option<ClientOrderId>,
1679        order_side: OrderSide,
1680        quantity: Quantity,
1681        price: Price,
1682    ) -> anyhow::Result<OrderStatusReport> {
1683        anyhow::ensure!(
1684            venue_order_id.is_some() || client_order_id.is_some(),
1685            "Either venue_order_id or client_order_id must be provided"
1686        );
1687
1688        let symbol = format_binance_symbol(&instrument_id);
1689        let size_precision = self.get_size_precision(&symbol)?;
1690
1691        let binance_side = BinanceSide::try_from(order_side)?;
1692
1693        let order_id = venue_order_id
1694            .map(|id| id.inner().parse::<i64>())
1695            .transpose()
1696            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1697
1698        let params = BinanceModifyOrderParams {
1699            symbol,
1700            order_id,
1701            orig_client_order_id: client_order_id.map(|id| id.to_string()),
1702            side: binance_side,
1703            quantity: quantity.to_string(),
1704            price: price.to_string(),
1705            recv_window: None,
1706        };
1707
1708        let order = self.raw.modify_order(&params).await?;
1709        order.to_order_status_report(account_id, instrument_id, size_precision)
1710    }
1711
1712    /// Modifies multiple orders in a single request (up to 5 orders).
1713    ///
1714    /// Each modify in the batch is processed independently. The response contains
1715    /// the result for each modify, which can be either a success or an error.
1716    ///
1717    /// # Errors
1718    ///
1719    /// Returns an error if the batch exceeds 5 orders or the request fails.
1720    pub async fn batch_modify_orders(
1721        &self,
1722        modifies: &[BatchModifyItem],
1723    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1724        self.raw.batch_modify_orders(modifies).await
1725    }
1726
1727    /// Cancels an order by venue order ID or client order ID.
1728    ///
1729    /// Either `venue_order_id` or `client_order_id` must be provided.
1730    ///
1731    /// # Errors
1732    ///
1733    /// Returns an error if:
1734    /// - Neither venue_order_id nor client_order_id is provided.
1735    /// - The request fails.
1736    pub async fn cancel_order(
1737        &self,
1738        instrument_id: InstrumentId,
1739        venue_order_id: Option<VenueOrderId>,
1740        client_order_id: Option<ClientOrderId>,
1741    ) -> anyhow::Result<VenueOrderId> {
1742        anyhow::ensure!(
1743            venue_order_id.is_some() || client_order_id.is_some(),
1744            "Either venue_order_id or client_order_id must be provided"
1745        );
1746
1747        let symbol = format_binance_symbol(&instrument_id);
1748
1749        let order_id = venue_order_id
1750            .map(|id| id.inner().parse::<i64>())
1751            .transpose()
1752            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1753
1754        let params = BinanceCancelOrderParams {
1755            symbol,
1756            order_id,
1757            orig_client_order_id: client_order_id.map(|id| id.to_string()),
1758            recv_window: None,
1759        };
1760
1761        let order = self.raw.cancel_order(&params).await?;
1762        Ok(VenueOrderId::new(order.order_id.to_string()))
1763    }
1764
1765    /// Cancels an algo order (conditional order) via the Binance Algo Service.
1766    ///
1767    /// Use the `client_algo_id` which corresponds to the `client_order_id` used
1768    /// when submitting the algo order.
1769    ///
1770    /// # Errors
1771    ///
1772    /// Returns an error if the request fails.
1773    pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
1774        let params = BinanceAlgoOrderQueryParams {
1775            algo_id: None,
1776            client_algo_id: Some(client_order_id.to_string()),
1777            recv_window: None,
1778        };
1779
1780        let response = self.raw.cancel_algo_order(&params).await?;
1781        if response.code == 200 {
1782            Ok(())
1783        } else {
1784            anyhow::bail!(
1785                "Cancel algo order failed: code={}, msg={}",
1786                response.code,
1787                response.msg
1788            )
1789        }
1790    }
1791
1792    /// Cancels all open orders for a symbol.
1793    ///
1794    /// # Errors
1795    ///
1796    /// Returns an error if the request fails.
1797    pub async fn cancel_all_orders(
1798        &self,
1799        instrument_id: InstrumentId,
1800    ) -> anyhow::Result<Vec<VenueOrderId>> {
1801        let symbol = format_binance_symbol(&instrument_id);
1802
1803        let params = BinanceCancelAllOrdersParams {
1804            symbol,
1805            recv_window: None,
1806        };
1807
1808        let response = self.raw.cancel_all_orders(&params).await?;
1809        if response.code == 200 {
1810            Ok(vec![])
1811        } else {
1812            anyhow::bail!("Cancel all orders failed: {}", response.msg);
1813        }
1814    }
1815
1816    /// Cancels all open algo orders for a symbol.
1817    ///
1818    /// # Errors
1819    ///
1820    /// Returns an error if the request fails.
1821    pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
1822        let symbol = format_binance_symbol(&instrument_id);
1823
1824        let params = BinanceCancelAllAlgoOrdersParams {
1825            symbol,
1826            recv_window: None,
1827        };
1828
1829        let response = self.raw.cancel_all_algo_orders(&params).await?;
1830        if response.code == 200 {
1831            Ok(())
1832        } else {
1833            anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
1834        }
1835    }
1836
1837    /// Cancels multiple orders in a single request (up to 5 orders).
1838    ///
1839    /// Each cancel in the batch is processed independently. The response contains
1840    /// the result for each cancel, which can be either a success or an error.
1841    ///
1842    /// # Errors
1843    ///
1844    /// Returns an error if the batch exceeds 5 orders or the request fails.
1845    pub async fn batch_cancel_orders(
1846        &self,
1847        cancels: &[BatchCancelItem],
1848    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1849        self.raw.batch_cancel_orders(cancels).await
1850    }
1851
1852    /// Queries open algo orders (conditional orders).
1853    ///
1854    /// Returns all open algo orders, optionally filtered by symbol.
1855    ///
1856    /// # Errors
1857    ///
1858    /// Returns an error if the request fails.
1859    pub async fn query_open_algo_orders(
1860        &self,
1861        instrument_id: Option<InstrumentId>,
1862    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1863        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
1864
1865        let params = BinanceOpenAlgoOrdersParams {
1866            symbol,
1867            recv_window: None,
1868        };
1869
1870        self.raw.query_open_algo_orders(&params).await
1871    }
1872
1873    /// Queries a single algo order by client_order_id.
1874    ///
1875    /// # Errors
1876    ///
1877    /// Returns an error if the request fails.
1878    pub async fn query_algo_order(
1879        &self,
1880        client_order_id: ClientOrderId,
1881    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1882        let params = BinanceAlgoOrderQueryParams {
1883            algo_id: None,
1884            client_algo_id: Some(client_order_id.to_string()),
1885            recv_window: None,
1886        };
1887
1888        self.raw.query_algo_order(&params).await
1889    }
1890
1891    /// Returns the size precision for an instrument from the cache.
1892    fn get_size_precision(&self, symbol: &str) -> anyhow::Result<u8> {
1893        let instrument = self
1894            .instruments
1895            .get(&Ustr::from(symbol))
1896            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
1897
1898        let precision = match instrument.value() {
1899            BinanceFuturesInstrument::UsdM(s) => s.quantity_precision,
1900            BinanceFuturesInstrument::CoinM(s) => s.quantity_precision,
1901        };
1902
1903        Ok(precision as u8)
1904    }
1905
1906    /// Returns the price precision for an instrument from the cache.
1907    fn get_price_precision(&self, symbol: &str) -> anyhow::Result<u8> {
1908        let instrument = self
1909            .instruments
1910            .get(&Ustr::from(symbol))
1911            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
1912
1913        let precision = match instrument.value() {
1914            BinanceFuturesInstrument::UsdM(s) => s.price_precision,
1915            BinanceFuturesInstrument::CoinM(s) => s.price_precision,
1916        };
1917
1918        Ok(precision as u8)
1919    }
1920
1921    /// Requests the current account state.
1922    ///
1923    /// # Errors
1924    ///
1925    /// Returns an error if the request fails or parsing fails.
1926    pub async fn request_account_state(
1927        &self,
1928        account_id: AccountId,
1929    ) -> anyhow::Result<AccountState> {
1930        let ts_init = UnixNanos::default();
1931        let account_info = self.raw.query_account().await?;
1932        account_info.to_account_state(account_id, ts_init)
1933    }
1934
1935    /// Requests a single order status report.
1936    ///
1937    /// Either `venue_order_id` or `client_order_id` must be provided.
1938    ///
1939    /// # Errors
1940    ///
1941    /// Returns an error if the request fails or parsing fails.
1942    pub async fn request_order_status_report(
1943        &self,
1944        account_id: AccountId,
1945        instrument_id: InstrumentId,
1946        venue_order_id: Option<VenueOrderId>,
1947        client_order_id: Option<ClientOrderId>,
1948    ) -> anyhow::Result<OrderStatusReport> {
1949        anyhow::ensure!(
1950            venue_order_id.is_some() || client_order_id.is_some(),
1951            "Either venue_order_id or client_order_id must be provided"
1952        );
1953
1954        let symbol = format_binance_symbol(&instrument_id);
1955        let size_precision = self.get_size_precision(&symbol)?;
1956
1957        let order_id = venue_order_id
1958            .map(|id| id.inner().parse::<i64>())
1959            .transpose()
1960            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1961
1962        let orig_client_order_id = client_order_id.map(|id| id.to_string());
1963
1964        let params = BinanceOrderQueryParams {
1965            symbol,
1966            order_id,
1967            orig_client_order_id,
1968            recv_window: None,
1969        };
1970
1971        let order = self.raw.query_order(&params).await?;
1972        order.to_order_status_report(account_id, instrument_id, size_precision)
1973    }
1974
1975    /// Requests order status reports for open orders.
1976    ///
1977    /// If `instrument_id` is None, returns all open orders.
1978    ///
1979    /// # Errors
1980    ///
1981    /// Returns an error if the request fails or parsing fails.
1982    pub async fn request_order_status_reports(
1983        &self,
1984        account_id: AccountId,
1985        instrument_id: Option<InstrumentId>,
1986        open_only: bool,
1987    ) -> anyhow::Result<Vec<OrderStatusReport>> {
1988        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
1989
1990        let orders = if open_only {
1991            let params = BinanceOpenOrdersParams {
1992                symbol: symbol.clone(),
1993                recv_window: None,
1994            };
1995            self.raw.query_open_orders(&params).await?
1996        } else {
1997            // For historical orders, symbol is required
1998            let symbol = symbol.ok_or_else(|| {
1999                anyhow::anyhow!("instrument_id is required for historical orders")
2000            })?;
2001            let params = BinanceAllOrdersParams {
2002                symbol,
2003                order_id: None,
2004                start_time: None,
2005                end_time: None,
2006                limit: None,
2007                recv_window: None,
2008            };
2009            self.raw.query_all_orders(&params).await?
2010        };
2011
2012        let mut reports = Vec::with_capacity(orders.len());
2013
2014        for order in orders {
2015            let order_instrument_id = instrument_id.unwrap_or_else(|| {
2016                // Build instrument ID from order symbol
2017                let suffix = self.product_type.suffix();
2018                InstrumentId::from(format!("{}{}.BINANCE", order.symbol, suffix))
2019            });
2020
2021            let size_precision = self.get_size_precision(&order.symbol).unwrap_or(8); // Default precision if not in cache
2022
2023            match order.to_order_status_report(account_id, order_instrument_id, size_precision) {
2024                Ok(report) => reports.push(report),
2025                Err(e) => {
2026                    log::warn!("Failed to parse order status report: {e}");
2027                }
2028            }
2029        }
2030
2031        Ok(reports)
2032    }
2033
2034    /// Requests fill reports for a symbol.
2035    ///
2036    /// # Errors
2037    ///
2038    /// Returns an error if the request fails or parsing fails.
2039    pub async fn request_fill_reports(
2040        &self,
2041        account_id: AccountId,
2042        instrument_id: InstrumentId,
2043        venue_order_id: Option<VenueOrderId>,
2044        start: Option<i64>,
2045        end: Option<i64>,
2046        limit: Option<u32>,
2047    ) -> anyhow::Result<Vec<FillReport>> {
2048        let symbol = format_binance_symbol(&instrument_id);
2049        let size_precision = self.get_size_precision(&symbol)?;
2050        let price_precision = self.get_price_precision(&symbol)?;
2051
2052        let order_id = venue_order_id
2053            .map(|id| id.inner().parse::<i64>())
2054            .transpose()
2055            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2056
2057        let params = BinanceUserTradesParams {
2058            symbol,
2059            order_id,
2060            start_time: start,
2061            end_time: end,
2062            from_id: None,
2063            limit,
2064            recv_window: None,
2065        };
2066
2067        let trades = self.raw.query_user_trades(&params).await?;
2068
2069        let mut reports = Vec::with_capacity(trades.len());
2070
2071        for trade in trades {
2072            match trade.to_fill_report(account_id, instrument_id, price_precision, size_precision) {
2073                Ok(report) => reports.push(report),
2074                Err(e) => {
2075                    log::warn!("Failed to parse fill report: {e}");
2076                }
2077            }
2078        }
2079
2080        Ok(reports)
2081    }
2082
2083    /// Requests recent public trades for an instrument.
2084    ///
2085    /// # Errors
2086    ///
2087    /// Returns an error if the request fails, instrument is not cached, or parsing fails.
2088    pub async fn request_trades(
2089        &self,
2090        instrument_id: InstrumentId,
2091        limit: Option<u32>,
2092    ) -> anyhow::Result<Vec<TradeTick>> {
2093        let symbol = format_binance_symbol(&instrument_id);
2094        let size_precision = self.get_size_precision(&symbol)?;
2095        let price_precision = self.get_price_precision(&symbol)?;
2096
2097        let params = BinanceTradesParams { symbol, limit };
2098
2099        let trades = self.raw.trades(&params).await?;
2100        let ts_init = UnixNanos::default();
2101
2102        let mut result = Vec::with_capacity(trades.len());
2103        for trade in trades {
2104            let price: f64 = trade.price.parse().unwrap_or(0.0);
2105            let size: f64 = trade.qty.parse().unwrap_or(0.0);
2106            let ts_event = UnixNanos::from((trade.time * 1_000_000) as u64);
2107
2108            let aggressor_side = if trade.is_buyer_maker {
2109                AggressorSide::Seller
2110            } else {
2111                AggressorSide::Buyer
2112            };
2113
2114            let tick = TradeTick::new(
2115                instrument_id,
2116                Price::new(price, price_precision),
2117                Quantity::new(size, size_precision),
2118                aggressor_side,
2119                TradeId::new(trade.id.to_string()),
2120                ts_event,
2121                ts_init,
2122            );
2123            result.push(tick);
2124        }
2125
2126        Ok(result)
2127    }
2128
2129    /// Requests bar (kline/candlestick) data for an instrument.
2130    ///
2131    /// # Errors
2132    ///
2133    /// Returns an error if the bar type is not supported, instrument is not cached,
2134    /// or the request fails.
2135    pub async fn request_bars(
2136        &self,
2137        bar_type: BarType,
2138        start: Option<DateTime<Utc>>,
2139        end: Option<DateTime<Utc>>,
2140        limit: Option<u32>,
2141    ) -> anyhow::Result<Vec<Bar>> {
2142        anyhow::ensure!(
2143            bar_type.aggregation_source() == AggregationSource::External,
2144            "Only EXTERNAL aggregation is supported"
2145        );
2146
2147        let spec = bar_type.spec();
2148        let step = spec.step.get();
2149        let interval = match spec.aggregation {
2150            BarAggregation::Second => {
2151                anyhow::bail!("Binance Futures does not support second-level kline intervals")
2152            }
2153            BarAggregation::Minute => format!("{step}m"),
2154            BarAggregation::Hour => format!("{step}h"),
2155            BarAggregation::Day => format!("{step}d"),
2156            BarAggregation::Week => format!("{step}w"),
2157            BarAggregation::Month => format!("{step}M"),
2158            a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
2159        };
2160
2161        let symbol = format_binance_symbol(&bar_type.instrument_id());
2162        let price_precision = self.get_price_precision(&symbol)?;
2163        let size_precision = self.get_size_precision(&symbol)?;
2164
2165        let params = BinanceKlinesParams {
2166            symbol,
2167            interval,
2168            start_time: start.map(|dt| dt.timestamp_millis()),
2169            end_time: end.map(|dt| dt.timestamp_millis()),
2170            limit,
2171        };
2172
2173        let klines = self.raw.klines(&params).await?;
2174        let ts_init = UnixNanos::default();
2175
2176        let mut result = Vec::with_capacity(klines.len());
2177        for kline in klines {
2178            let open: f64 = kline.open.parse().unwrap_or(0.0);
2179            let high: f64 = kline.high.parse().unwrap_or(0.0);
2180            let low: f64 = kline.low.parse().unwrap_or(0.0);
2181            let close: f64 = kline.close.parse().unwrap_or(0.0);
2182            let volume: f64 = kline.volume.parse().unwrap_or(0.0);
2183
2184            // close_time is end of interval, add 1ms for next bar's open
2185            let ts_event = UnixNanos::from((kline.close_time * 1_000_000) as u64);
2186
2187            let bar = Bar::new(
2188                bar_type,
2189                Price::new(open, price_precision),
2190                Price::new(high, price_precision),
2191                Price::new(low, price_precision),
2192                Price::new(close, price_precision),
2193                Quantity::new(volume, size_precision),
2194                ts_event,
2195                ts_init,
2196            );
2197            result.push(bar);
2198        }
2199
2200        Ok(result)
2201    }
2202}
2203
2204/// Checks if an order type requires the Binance Algo Service API.
2205///
2206/// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
2207/// The traditional `/fapi/v1/order` endpoint returns error `-4120` for these types.
2208#[must_use]
2209pub fn is_algo_order_type(order_type: OrderType) -> bool {
2210    matches!(
2211        order_type,
2212        OrderType::StopMarket
2213            | OrderType::StopLimit
2214            | OrderType::MarketIfTouched
2215            | OrderType::LimitIfTouched
2216            | OrderType::TrailingStopMarket
2217    )
2218}
2219
2220/// Converts a Nautilus order type to a Binance Futures order type.
2221fn order_type_to_binance_futures(order_type: OrderType) -> anyhow::Result<BinanceFuturesOrderType> {
2222    match order_type {
2223        OrderType::Market => Ok(BinanceFuturesOrderType::Market),
2224        OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
2225        OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
2226        OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
2227        OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
2228        OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
2229        OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
2230        _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
2231    }
2232}
2233
2234#[cfg(test)]
2235mod tests {
2236    use nautilus_network::http::{HttpStatus, StatusCode};
2237    use rstest::rstest;
2238    use tokio_util::bytes::Bytes;
2239
2240    use super::*;
2241
2242    #[rstest]
2243    fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
2244        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
2245
2246        assert!(config.default_quota.is_some());
2247        assert_eq!(config.order_keys.len(), 2);
2248        assert!(config.order_keys.iter().any(|k| k.contains("Second")));
2249        assert!(config.order_keys.iter().any(|k| k.contains("Minute")));
2250    }
2251
2252    #[rstest]
2253    fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
2254        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
2255
2256        assert!(config.default_quota.is_some());
2257        assert_eq!(config.order_keys.len(), 2);
2258    }
2259
2260    #[rstest]
2261    fn test_create_client_rejects_spot_product_type() {
2262        let result = BinanceFuturesHttpClient::new(
2263            BinanceProductType::Spot,
2264            BinanceEnvironment::Mainnet,
2265            None,
2266            None,
2267            None,
2268            None,
2269            None,
2270            None,
2271        );
2272
2273        assert!(result.is_err());
2274    }
2275
2276    fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
2277        BinanceRawFuturesHttpClient::new(
2278            BinanceProductType::UsdM,
2279            BinanceEnvironment::Mainnet,
2280            None,
2281            None,
2282            None,
2283            None,
2284            None,
2285            None,
2286        )
2287        .expect("Failed to create test client")
2288    }
2289
2290    #[rstest]
2291    fn test_parse_error_response_binance_error() {
2292        let client = create_test_raw_client();
2293        let response = HttpResponse {
2294            status: HttpStatus::new(StatusCode::BAD_REQUEST),
2295            headers: HashMap::new(),
2296            body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
2297        };
2298
2299        let result: BinanceFuturesHttpResult<()> = client.parse_error_response(response);
2300
2301        match result {
2302            Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
2303                assert_eq!(code, -1121);
2304                assert_eq!(message, "Invalid symbol.");
2305            }
2306            other => panic!("Expected BinanceError, was {other:?}"),
2307        }
2308    }
2309}