Skip to main content

nautilus_binance/spot/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 Spot HTTP client with SBE encoding.
17//!
18//! This client communicates with Binance Spot REST API using SBE (Simple Binary
19//! Encoding) for all request/response payloads, providing microsecond timestamp
20//! precision and reduced latency compared to JSON.
21//!
22//! ## Architecture
23//!
24//! Two-layer client pattern:
25//! - [`BinanceRawSpotHttpClient`]: Low-level API methods returning raw bytes.
26//! - [`BinanceSpotHttpClient`]: High-level methods with SBE decoding.
27//!
28//! ## SBE Headers
29//!
30//! All requests include:
31//! - `Accept: application/sbe`
32//! - `X-MBX-SBE: 3:5` (schema ID:version)
33
34use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, sync::Arc};
35
36use ahash::AHashMap;
37use jiff::Timestamp;
38use nautilus_common::cache::InstrumentLookupError;
39use nautilus_core::{
40    collections::AtomicMap, consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, hex,
41    nanos::UnixNanos, time::AtomicTime,
42};
43use nautilus_model::{
44    data::{Bar, BarType, BookOrder, TradeTick},
45    enums::{
46        AggregationSource, BarAggregation, BookType, MarketStatusAction, OrderSide, OrderType,
47        TimeInForce,
48    },
49    events::AccountState,
50    identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
51    instruments::{Instrument, any::InstrumentAny},
52    orderbook::OrderBook,
53    reports::{FillReport, OrderStatusReport},
54    types::{Price, Quantity},
55};
56use nautilus_network::{
57    http::{HttpClient, HttpResponse, Method, USER_AGENT},
58    ratelimiter::quota::Quota,
59};
60use rust_decimal::Decimal;
61use serde::{Deserialize, Serialize};
62use ustr::Ustr;
63
64use super::{
65    error::{BinanceSpotHttpError, BinanceSpotHttpResult},
66    models::{
67        AvgPrice, BatchCancelResult, BatchOrderResult, BinanceAccountCommission,
68        BinanceAccountInfo, BinanceAccountRatesJson, BinanceAccountTrade, BinanceAggTrade,
69        BinanceAggTrades, BinanceBalance, BinanceCancelOrderResponse, BinanceDepth,
70        BinanceExchangeInfoJson, BinanceKline, BinanceKlines, BinanceNewOrderResponse,
71        BinanceOrderFill, BinanceOrderResponse, BinancePriceLevel, BinanceTrade, BinanceTrades,
72        BookTicker, ListenKeyResponse, NewOcoOrderListResponse, Ticker24hr, TickerPrice, TradeFee,
73    },
74    parse,
75    query::{
76        AccountCommissionParams, AccountInfoParams, AccountTradesParams, AggTradesParams,
77        AllOrdersParams, AvgPriceParams, BatchCancelItem, BatchOrderItem, CancelOpenOrdersParams,
78        CancelOrderParams, CancelReplaceOrderParams, DepthParams, KlinesParams, ListenKeyParams,
79        NewOcoOrderListParams, NewOrderParams, OpenOrdersParams, QueryOrderParams, TickerParams,
80        TradeFeeParams, TradesParams,
81    },
82};
83use crate::{
84    common::{
85        consts::{
86            BINANCE_API_KEY_HEADER, BINANCE_NAUTILUS_SPOT_BROKER_ID, BINANCE_NO_SUCH_ORDER_CODE,
87            BINANCE_SPOT_RATE_LIMITS, BINANCE_VENUE, BinanceRateLimitQuota,
88        },
89        credential::SigningCredential,
90        encoder::{decode_client_order_id, encode_broker_id},
91        enums::{
92            BinanceEnvironment, BinanceOrderStatus, BinanceProductType, BinanceRateLimitInterval,
93            BinanceRateLimitType, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
94        },
95        fees::BINANCE_SPOT_FEE_DEFAULT,
96        instruments::BinanceInstrumentSelector,
97        models::BinanceErrorResponse,
98        parse::{
99            get_currency, parse_fill_report_sbe, parse_klines_to_binance_bars,
100            parse_new_order_response_sbe, parse_order_status_report_sbe,
101            parse_spot_instrument_json_with_fees, parse_spot_instrument_sbe_with_fees,
102            parse_spot_trades_sbe,
103        },
104        urls::get_http_base_url,
105    },
106    config::BinanceInstrumentProviderConfig,
107    spot::{
108        enums::{
109            BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType,
110            order_type_to_binance_spot, time_in_force_to_binance_spot,
111        },
112        sbe::{
113            generated::symbol_status::SymbolStatus,
114            spot::{
115                ReadBuf, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
116                error_response_codec::{self, ErrorResponseDecoder},
117                message_header_codec::MessageHeaderDecoder,
118                order_side::OrderSide as SbeOrderSide,
119                order_status::OrderStatus as SbeOrderStatus,
120                order_type::OrderType as SbeOrderType,
121                self_trade_prevention_mode::SelfTradePreventionMode as SbeSelfTradePreventionMode,
122                time_in_force::TimeInForce as SbeTimeInForce,
123            },
124        },
125    },
126};
127
128/// SBE schema header value (`X-MBX-SBE`) sent on Spot API requests.
129///
130/// Requests the current `3:5` schema. The decoder accepts any version within schema ID `3`
131/// (see `parse::MessageHeader::validate`), so compatible responses continue to decode.
132pub const SBE_SCHEMA_HEADER: &str = "3:5";
133
134use crate::common::consts::{
135    BINANCE_SAPI_PATH as SAPI_PATH, BINANCE_SPOT_API_PATH as SPOT_API_PATH,
136};
137
138/// Global rate limit key.
139const BINANCE_GLOBAL_RATE_KEY: &str = "binance:spot:global";
140
141/// Orders rate limit key prefix.
142const BINANCE_ORDERS_RATE_KEY: &str = "binance:spot:orders";
143
144struct RateLimitConfig {
145    default_quota: Option<Quota>,
146    keyed_quotas: Vec<(String, Quota)>,
147    order_keys: Vec<String>,
148}
149
150#[derive(Debug, Deserialize)]
151#[serde(rename_all = "camelCase")]
152struct SpotAccountJson {
153    #[serde(default)]
154    maker_commission: i64,
155    #[serde(default)]
156    taker_commission: i64,
157    #[serde(default)]
158    buyer_commission: i64,
159    #[serde(default)]
160    seller_commission: i64,
161    #[serde(default)]
162    commission_rates: Option<SpotCommissionRatesJson>,
163    can_trade: bool,
164    can_withdraw: bool,
165    can_deposit: bool,
166    #[serde(default)]
167    require_self_trade_prevention: bool,
168    #[serde(default)]
169    prevent_sor: bool,
170    update_time: i64,
171    account_type: String,
172    balances: Vec<SpotBalanceJson>,
173}
174
175#[derive(Debug, Deserialize)]
176struct SpotCommissionRatesJson {
177    maker: String,
178    taker: String,
179    buyer: String,
180    seller: String,
181}
182
183#[derive(Debug, Deserialize)]
184struct SpotBalanceJson {
185    asset: String,
186    free: String,
187    locked: String,
188}
189
190#[derive(Debug, Deserialize)]
191#[serde(rename_all = "camelCase")]
192struct SpotOrderJson {
193    symbol: String,
194    order_id: i64,
195    #[serde(default)]
196    order_list_id: Option<i64>,
197    client_order_id: String,
198    #[serde(default)]
199    orig_client_order_id: String,
200    #[serde(default)]
201    transact_time: i64,
202    #[serde(default)]
203    price: String,
204    #[serde(default)]
205    orig_qty: String,
206    #[serde(default)]
207    executed_qty: String,
208    #[serde(default, rename = "cummulativeQuoteQty")]
209    cummulative_quote_qty: String,
210    status: BinanceOrderStatus,
211    time_in_force: BinanceTimeInForce,
212    #[serde(rename = "type")]
213    order_type: BinanceSpotOrderType,
214    side: BinanceSide,
215    #[serde(default)]
216    stop_price: String,
217    #[serde(default)]
218    iceberg_qty: String,
219    #[serde(default)]
220    time: i64,
221    #[serde(default)]
222    update_time: i64,
223    #[serde(default)]
224    is_working: bool,
225    #[serde(default)]
226    working_time: Option<i64>,
227    #[serde(default)]
228    orig_quote_order_qty: String,
229    #[serde(default)]
230    self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
231    #[serde(default)]
232    fills: Vec<SpotOrderFillJson>,
233}
234
235#[derive(Debug, Deserialize)]
236#[serde(rename_all = "camelCase")]
237struct SpotCancelReplaceJson {
238    new_order_response: SpotOrderJson,
239}
240
241#[derive(Debug, Deserialize)]
242#[serde(rename_all = "camelCase")]
243struct SpotOrderFillJson {
244    price: String,
245    qty: String,
246    commission: String,
247    commission_asset: String,
248    #[serde(default)]
249    trade_id: Option<i64>,
250}
251
252#[derive(Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254struct SpotAccountTradeJson {
255    symbol: String,
256    id: i64,
257    order_id: i64,
258    #[serde(default)]
259    order_list_id: Option<i64>,
260    price: String,
261    qty: String,
262    quote_qty: String,
263    commission: String,
264    commission_asset: String,
265    time: i64,
266    is_buyer: bool,
267    is_maker: bool,
268    is_best_match: bool,
269}
270
271#[derive(Debug, Deserialize)]
272#[serde(rename_all = "camelCase")]
273struct SpotDepthJson {
274    last_update_id: i64,
275    bids: Vec<[String; 2]>,
276    asks: Vec<[String; 2]>,
277}
278
279#[derive(Debug, Deserialize)]
280#[serde(rename_all = "camelCase")]
281struct SpotTradeJson {
282    id: i64,
283    price: String,
284    qty: String,
285    quote_qty: String,
286    time: i64,
287    is_buyer_maker: bool,
288    is_best_match: bool,
289}
290
291#[derive(Debug, Deserialize)]
292struct SpotAggTradeJson {
293    #[serde(rename = "a")]
294    id: i64,
295    #[serde(rename = "p")]
296    price: String,
297    #[serde(rename = "q")]
298    qty: String,
299    #[serde(rename = "f")]
300    first_trade_id: i64,
301    #[serde(rename = "l")]
302    last_trade_id: i64,
303    #[serde(rename = "T")]
304    time: i64,
305    #[serde(rename = "m")]
306    is_buyer_maker: bool,
307    #[serde(rename = "M")]
308    is_best_match: bool,
309}
310
311type SpotKlineJson = (
312    i64,
313    String,
314    String,
315    String,
316    String,
317    String,
318    i64,
319    String,
320    i64,
321    String,
322    String,
323    String,
324);
325
326/// Low-level HTTP client for Binance Spot REST API with SBE encoding.
327///
328/// Handles:
329/// - Base URL resolution by environment.
330/// - Optional HMAC SHA256 signing for private endpoints.
331/// - Rate limiting using Spot API quotas.
332/// - SBE decoding to Binance-specific response types.
333///
334/// Methods are named to match Binance API endpoints and return
335/// venue-specific types (decoded from SBE).
336#[derive(Debug, Clone)]
337pub struct BinanceRawSpotHttpClient {
338    client: HttpClient,
339    base_url: String,
340    credential: Option<SigningCredential>,
341    recv_window: Option<u64>,
342    order_rate_keys: Vec<String>,
343    json_responses: bool,
344}
345
346impl BinanceRawSpotHttpClient {
347    /// Returns whether signed requests can be made.
348    #[must_use]
349    pub fn has_credentials(&self) -> bool {
350        self.credential.is_some()
351    }
352
353    /// Creates a new Binance Spot raw HTTP client.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if the underlying [`HttpClient`] fails to build.
358    pub fn new(
359        environment: BinanceEnvironment,
360        api_key: Option<String>,
361        api_secret: Option<String>,
362        base_url_override: Option<String>,
363        recv_window: Option<u64>,
364        timeout_secs: Option<u64>,
365        proxy_url: Option<String>,
366    ) -> BinanceSpotHttpResult<Self> {
367        Self::new_with_json_responses(
368            environment,
369            api_key,
370            api_secret,
371            base_url_override,
372            recv_window,
373            timeout_secs,
374            proxy_url,
375            false,
376        )
377    }
378
379    /// Creates a raw Spot client with JSON responses instead of Global SBE responses.
380    ///
381    /// # Errors
382    ///
383    /// Returns an error if the underlying [`HttpClient`] fails to build.
384    #[expect(clippy::too_many_arguments)]
385    pub fn new_with_json_responses(
386        environment: BinanceEnvironment,
387        api_key: Option<String>,
388        api_secret: Option<String>,
389        base_url_override: Option<String>,
390        recv_window: Option<u64>,
391        timeout_secs: Option<u64>,
392        proxy_url: Option<String>,
393        json_responses: bool,
394    ) -> BinanceSpotHttpResult<Self> {
395        let RateLimitConfig {
396            default_quota,
397            keyed_quotas,
398            order_keys,
399        } = Self::rate_limit_config();
400
401        let credential = match (api_key, api_secret) {
402            (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
403            (None, None) => None,
404            _ => return Err(BinanceSpotHttpError::MissingCredentials),
405        };
406
407        let base_url = base_url_override.unwrap_or_else(|| {
408            get_http_base_url(BinanceProductType::Spot, environment).to_string()
409        });
410
411        let headers = Self::default_headers(&credential, json_responses);
412
413        let client = HttpClient::builder()
414            .headers(headers)
415            .header_keys(vec![BINANCE_API_KEY_HEADER.to_string()])
416            .keyed_quotas(keyed_quotas)
417            .maybe_default_quota(default_quota)
418            .maybe_timeout_secs(timeout_secs)
419            .maybe_proxy_url(proxy_url)
420            .build()?;
421
422        Ok(Self {
423            client,
424            base_url,
425            credential,
426            recv_window,
427            order_rate_keys: order_keys,
428            json_responses,
429        })
430    }
431
432    /// Returns the SBE schema ID.
433    #[must_use]
434    pub const fn schema_id() -> u16 {
435        SBE_SCHEMA_ID
436    }
437
438    /// Returns the SBE schema version.
439    #[must_use]
440    pub const fn schema_version() -> u16 {
441        SBE_SCHEMA_VERSION
442    }
443
444    /// Performs a GET request and returns raw response bytes.
445    ///
446    /// # Errors
447    ///
448    /// Returns an error if the request fails.
449    pub async fn get<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
450    where
451        P: Serialize + ?Sized,
452    {
453        self.request(Method::GET, path, params, false, false).await
454    }
455
456    /// Performs a signed GET request and returns raw response bytes.
457    ///
458    /// # Errors
459    ///
460    /// Returns an error if credentials are missing or the request fails.
461    pub async fn get_signed<P>(
462        &self,
463        path: &str,
464        params: Option<&P>,
465    ) -> BinanceSpotHttpResult<Vec<u8>>
466    where
467        P: Serialize + ?Sized,
468    {
469        self.request(Method::GET, path, params, true, false).await
470    }
471
472    /// Performs a signed GET request and requests a JSON response.
473    ///
474    /// # Errors
475    ///
476    /// Returns an error if credentials are missing or the request fails.
477    pub async fn get_signed_json<P>(
478        &self,
479        path: &str,
480        params: Option<&P>,
481    ) -> BinanceSpotHttpResult<Vec<u8>>
482    where
483        P: Serialize + ?Sized,
484    {
485        self.request_with_extra_headers(
486            Method::GET,
487            path,
488            params,
489            true,
490            false,
491            Some(HashMap::from([(
492                "Accept".to_string(),
493                "application/json".to_string(),
494            )])),
495        )
496        .await
497    }
498
499    /// Performs a signed POST request and returns raw response bytes.
500    ///
501    /// # Errors
502    ///
503    /// Returns an error if credentials are missing or the request fails.
504    pub async fn post_signed<P>(
505        &self,
506        path: &str,
507        params: Option<&P>,
508    ) -> BinanceSpotHttpResult<Vec<u8>>
509    where
510        P: Serialize + ?Sized,
511    {
512        self.request(Method::POST, path, params, true, true).await
513    }
514
515    /// Performs a signed POST request and requests a JSON response.
516    ///
517    /// # Errors
518    ///
519    /// Returns an error if credentials are missing or the request fails.
520    pub async fn post_signed_json<P>(
521        &self,
522        path: &str,
523        params: Option<&P>,
524    ) -> BinanceSpotHttpResult<Vec<u8>>
525    where
526        P: Serialize + ?Sized,
527    {
528        self.request_with_extra_headers(
529            Method::POST,
530            path,
531            params,
532            true,
533            true,
534            Some(HashMap::from([(
535                "Accept".to_string(),
536                "application/json".to_string(),
537            )])),
538        )
539        .await
540    }
541
542    /// Performs a signed DELETE request and returns raw response bytes.
543    ///
544    /// # Errors
545    ///
546    /// Returns an error if credentials are missing or the request fails.
547    pub async fn delete_signed<P>(
548        &self,
549        path: &str,
550        params: Option<&P>,
551    ) -> BinanceSpotHttpResult<Vec<u8>>
552    where
553        P: Serialize + ?Sized,
554    {
555        self.request(Method::DELETE, path, params, true, true).await
556    }
557
558    /// Performs a signed DELETE request and requests a JSON response.
559    ///
560    /// # Errors
561    ///
562    /// Returns an error if credentials are missing or the request fails.
563    pub async fn delete_signed_json<P>(
564        &self,
565        path: &str,
566        params: Option<&P>,
567    ) -> BinanceSpotHttpResult<Vec<u8>>
568    where
569        P: Serialize + ?Sized,
570    {
571        self.request_with_extra_headers(
572            Method::DELETE,
573            path,
574            params,
575            true,
576            true,
577            Some(HashMap::from([(
578                "Accept".to_string(),
579                "application/json".to_string(),
580            )])),
581        )
582        .await
583    }
584
585    async fn request<P>(
586        &self,
587        method: Method,
588        path: &str,
589        params: Option<&P>,
590        signed: bool,
591        use_order_quota: bool,
592    ) -> BinanceSpotHttpResult<Vec<u8>>
593    where
594        P: Serialize + ?Sized,
595    {
596        self.request_with_extra_headers(method, path, params, signed, use_order_quota, None)
597            .await
598    }
599
600    async fn request_with_extra_headers<P>(
601        &self,
602        method: Method,
603        path: &str,
604        params: Option<&P>,
605        signed: bool,
606        use_order_quota: bool,
607        extra_headers: Option<HashMap<String, String>>,
608    ) -> BinanceSpotHttpResult<Vec<u8>>
609    where
610        P: Serialize + ?Sized,
611    {
612        let mut query = params
613            .map(serde_urlencoded::to_string)
614            .transpose()
615            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
616            .unwrap_or_default();
617
618        let mut headers = extra_headers.unwrap_or_default();
619
620        if signed {
621            let cred = self
622                .credential
623                .as_ref()
624                .ok_or(BinanceSpotHttpError::MissingCredentials)?;
625
626            if !query.is_empty() {
627                query.push('&');
628            }
629
630            let timestamp = Timestamp::now().as_millisecond();
631            query.push_str(&format!("timestamp={timestamp}"));
632
633            if let Some(recv_window) = self.recv_window {
634                query.push_str(&format!("&recvWindow={recv_window}"));
635            }
636
637            let signature = Self::percent_encode(&cred.sign(&query));
638            query.push_str(&format!("&signature={signature}"));
639            headers.insert(
640                BINANCE_API_KEY_HEADER.to_string(),
641                cred.api_key().to_string(),
642            );
643        }
644
645        let url = self.build_url(path, &query);
646        let keys = self.rate_limit_keys(use_order_quota);
647
648        let response = self
649            .client
650            .request(
651                method,
652                url,
653                None::<&HashMap<String, Vec<String>>>,
654                Some(headers),
655                None,
656                None,
657                Some(keys),
658            )
659            .await?;
660
661        if !response.status.is_success() {
662            return self.parse_error_response(&response);
663        }
664
665        Ok(response.body.to_vec())
666    }
667
668    fn build_url(&self, path: &str, query: &str) -> String {
669        let normalized_path = if path.starts_with('/') {
670            path.to_string()
671        } else {
672            format!("/{path}")
673        };
674
675        let mut url = format!("{}{}{}", self.base_url, SPOT_API_PATH, normalized_path);
676
677        if !query.is_empty() {
678            url.push('?');
679            url.push_str(query);
680        }
681        url
682    }
683
684    fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
685        if use_orders {
686            let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
687            keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
688            keys.extend(self.order_rate_keys.iter().cloned());
689            keys
690        } else {
691            vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
692        }
693    }
694
695    fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceSpotHttpResult<T> {
696        let status = response.status.as_u16();
697        let body = &response.body;
698
699        // Binance may return JSON errors even when SBE was requested
700        if let Ok(body_str) = std::str::from_utf8(body)
701            && let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(body_str)
702        {
703            return Err(BinanceSpotHttpError::BinanceError {
704                code: err.code,
705                message: err.msg,
706            });
707        }
708
709        // Try to decode SBE error response
710        if let Some((code, message)) = Self::try_decode_sbe_error(body) {
711            return Err(BinanceSpotHttpError::BinanceError {
712                code: code.into(),
713                message,
714            });
715        }
716
717        Err(BinanceSpotHttpError::UnexpectedStatus {
718            status,
719            body: hex::encode(body),
720        })
721    }
722
723    /// Attempts to decode an SBE error response.
724    ///
725    /// Returns Some((code, message)) if successfully decoded, None otherwise.
726    fn try_decode_sbe_error(body: &[u8]) -> Option<(i16, String)> {
727        const HEADER_LEN: usize = 8;
728        if body.len() < HEADER_LEN + error_response_codec::SBE_BLOCK_LENGTH as usize {
729            return None;
730        }
731
732        let buf = ReadBuf::new(body);
733
734        // Decode message header
735        let header = MessageHeaderDecoder::default().wrap(buf, 0);
736        if header.template_id() != error_response_codec::SBE_TEMPLATE_ID {
737            return None;
738        }
739
740        // Decode error response
741        let mut decoder = ErrorResponseDecoder::default().header(header, 0);
742        let code = decoder.code();
743
744        // Decode the message string (VAR_DATA with 2-byte length prefix)
745        let msg_coords = decoder.msg_decoder();
746        let msg_bytes = decoder.msg_slice(msg_coords);
747        let message = String::from_utf8_lossy(msg_bytes).into_owned();
748
749        Some((code, message))
750    }
751
752    fn default_headers(
753        credential: &Option<SigningCredential>,
754        json_responses: bool,
755    ) -> HashMap<String, String> {
756        let mut headers = HashMap::new();
757        headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
758        if json_responses {
759            headers.insert("Accept".to_string(), "application/json".to_string());
760        } else {
761            headers.insert("Accept".to_string(), "application/sbe".to_string());
762            headers.insert("X-MBX-SBE".to_string(), SBE_SCHEMA_HEADER.to_string());
763        }
764
765        if let Some(cred) = credential {
766            headers.insert(
767                BINANCE_API_KEY_HEADER.to_string(),
768                cred.api_key().to_string(),
769            );
770        }
771        headers
772    }
773
774    fn rate_limit_config() -> RateLimitConfig {
775        let quotas = BINANCE_SPOT_RATE_LIMITS;
776        let mut keyed = Vec::new();
777        let mut order_keys = Vec::new();
778        let mut default = None;
779
780        for quota in quotas {
781            if let Some(q) = Self::quota_from(quota) {
782                match quota.rate_limit_type {
783                    BinanceRateLimitType::RequestWeight if default.is_none() => {
784                        default = Some(q);
785                    }
786                    BinanceRateLimitType::Orders => {
787                        let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
788                        order_keys.push(key.clone());
789                        keyed.push((key, q));
790                    }
791                    _ => {}
792                }
793            }
794        }
795
796        let default_quota = default.unwrap_or_else(|| {
797            Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
798        });
799
800        keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
801
802        RateLimitConfig {
803            default_quota: Some(default_quota),
804            keyed_quotas: keyed,
805            order_keys,
806        }
807    }
808
809    fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
810        let burst = NonZeroU32::new(quota.limit)?;
811        match quota.interval {
812            BinanceRateLimitInterval::Second => Quota::per_second(burst),
813            BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
814            BinanceRateLimitInterval::Day => {
815                Quota::with_period(std::time::Duration::from_secs(SECONDS_IN_DAY))
816                    .map(|q| q.allow_burst(burst))
817            }
818            BinanceRateLimitInterval::Unknown => None,
819        }
820    }
821
822    /// Tests connectivity to the API.
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if the request fails or SBE decoding fails.
827    pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
828        let bytes = self.get("ping", None::<&()>).await?;
829        parse::decode_ping(&bytes)?;
830        Ok(())
831    }
832
833    /// Returns the server time in **microseconds** since epoch.
834    ///
835    /// Note: SBE provides microsecond precision vs JSON's milliseconds.
836    ///
837    /// # Errors
838    ///
839    /// Returns an error if the request fails or SBE decoding fails.
840    pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
841        if self.json_responses {
842            #[derive(Deserialize)]
843            #[serde(rename_all = "camelCase")]
844            struct ServerTimeJson {
845                server_time: i64,
846            }
847
848            let bytes = self.get_json("time", None::<&()>).await?;
849            let response: ServerTimeJson = serde_json::from_slice(&bytes)
850                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
851            return millis_to_micros(response.server_time);
852        }
853
854        let bytes = self.get("time", None::<&()>).await?;
855        let timestamp = parse::decode_server_time(&bytes)?;
856        Ok(timestamp)
857    }
858
859    /// Returns exchange information including trading symbols.
860    ///
861    /// # Errors
862    ///
863    /// Returns an error if the request fails or SBE decoding fails.
864    pub async fn exchange_info(
865        &self,
866    ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
867        let bytes = self.get("exchangeInfo", None::<&()>).await?;
868        let info = parse::decode_exchange_info(&bytes)?;
869        Ok(info)
870    }
871
872    /// Returns JSON exchange information for endpoints that do not support SBE.
873    ///
874    /// # Errors
875    ///
876    /// Returns an error if the request or JSON decode fails.
877    pub async fn exchange_info_json(&self) -> BinanceSpotHttpResult<BinanceExchangeInfoJson> {
878        let bytes = self.get_json("exchangeInfo", None::<&()>).await?;
879        serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
880    }
881
882    /// Returns order book depth for a symbol.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error if the request fails or SBE decoding fails.
887    pub async fn depth(&self, params: &DepthParams) -> BinanceSpotHttpResult<BinanceDepth> {
888        if self.json_responses {
889            let bytes = self.get_json("depth", Some(params)).await?;
890            let response: SpotDepthJson = serde_json::from_slice(&bytes)
891                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
892            spot_depth_from_json(response)
893        } else {
894            let bytes = self.get("depth", Some(params)).await?;
895            Ok(parse::decode_depth(&bytes)?)
896        }
897    }
898
899    /// Returns recent trades for a symbol.
900    ///
901    /// # Errors
902    ///
903    /// Returns an error if the request fails or SBE decoding fails.
904    pub async fn trades(
905        &self,
906        symbol: &str,
907        limit: Option<u32>,
908    ) -> BinanceSpotHttpResult<BinanceTrades> {
909        let params = TradesParams {
910            symbol: symbol.to_string(),
911            limit,
912        };
913
914        if self.json_responses {
915            let bytes = self.get_json("trades", Some(&params)).await?;
916            let response: Vec<SpotTradeJson> = serde_json::from_slice(&bytes)
917                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
918            spot_trades_from_json(response)
919        } else {
920            let bytes = self.get("trades", Some(&params)).await?;
921            Ok(parse::decode_trades(&bytes)?)
922        }
923    }
924
925    /// Returns aggregate trades for a symbol.
926    ///
927    /// # Errors
928    ///
929    /// Returns an error if the bounds are invalid or the request cannot be decoded.
930    pub async fn agg_trades(
931        &self,
932        params: &AggTradesParams,
933    ) -> BinanceSpotHttpResult<BinanceAggTrades> {
934        if params.limit.is_some_and(|limit| limit > 1000) {
935            return Err(BinanceSpotHttpError::ValidationError(
936                "aggregate trade limit must not exceed 1000".to_string(),
937            ));
938        }
939
940        if matches!((params.start_time, params.end_time), (Some(start), Some(end)) if start > end) {
941            return Err(BinanceSpotHttpError::ValidationError(
942                "aggregate trade startTime must not exceed endTime".to_string(),
943            ));
944        }
945
946        if self.json_responses {
947            let bytes = self.get_json("aggTrades", Some(params)).await?;
948            let response: Vec<SpotAggTradeJson> = serde_json::from_slice(&bytes)
949                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
950            spot_agg_trades_from_json(response)
951        } else {
952            let bytes = self.get("aggTrades", Some(params)).await?;
953            Ok(parse::decode_agg_trades(&bytes)?)
954        }
955    }
956
957    /// Returns kline (candlestick) data for a symbol.
958    ///
959    /// # Errors
960    ///
961    /// Returns an error if the request fails or SBE decoding fails.
962    pub async fn klines(
963        &self,
964        symbol: &str,
965        interval: &str,
966        start_time: Option<i64>,
967        end_time: Option<i64>,
968        limit: Option<u32>,
969    ) -> BinanceSpotHttpResult<BinanceKlines> {
970        let params = KlinesParams {
971            symbol: symbol.to_string(),
972            interval: interval.to_string(),
973            start_time,
974            end_time,
975            time_zone: None,
976            limit,
977        };
978
979        if self.json_responses {
980            let bytes = self.get_json("klines", Some(&params)).await?;
981            let response: Vec<SpotKlineJson> = serde_json::from_slice(&bytes)
982                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
983            spot_klines_from_json(response)
984        } else {
985            let bytes = self.get("klines", Some(&params)).await?;
986            Ok(parse::decode_klines(&bytes)?)
987        }
988    }
989
990    /// Performs a public GET request that returns JSON.
991    async fn get_json<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
992    where
993        P: Serialize + ?Sized,
994    {
995        let query = params
996            .map(serde_urlencoded::to_string)
997            .transpose()
998            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
999            .unwrap_or_default();
1000
1001        let url = self.build_url(path, &query);
1002        let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1003
1004        let response = self
1005            .client
1006            .request(
1007                Method::GET,
1008                url,
1009                None::<&HashMap<String, Vec<String>>>,
1010                None,
1011                None,
1012                None,
1013                Some(keys),
1014            )
1015            .await?;
1016
1017        if !response.status.is_success() {
1018            return self.parse_error_response(&response);
1019        }
1020
1021        Ok(response.body.to_vec())
1022    }
1023
1024    /// Returns 24-hour ticker price change statistics.
1025    ///
1026    /// If `symbol` is None, returns statistics for all symbols.
1027    ///
1028    /// # Errors
1029    ///
1030    /// Returns an error if the request fails.
1031    pub async fn ticker_24hr(
1032        &self,
1033        symbol: Option<&str>,
1034    ) -> BinanceSpotHttpResult<Vec<Ticker24hr>> {
1035        let params = symbol.map(TickerParams::for_symbol);
1036        let bytes = self.get_json("ticker/24hr", params.as_ref()).await?;
1037
1038        // Single symbol returns object, multiple returns array
1039        if symbol.is_some() {
1040            let ticker: Ticker24hr = serde_json::from_slice(&bytes)
1041                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1042            Ok(vec![ticker])
1043        } else {
1044            let tickers: Vec<Ticker24hr> = serde_json::from_slice(&bytes)
1045                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1046            Ok(tickers)
1047        }
1048    }
1049
1050    /// Returns latest price for a symbol or all symbols.
1051    ///
1052    /// If `symbol` is None, returns prices for all symbols.
1053    ///
1054    /// # Errors
1055    ///
1056    /// Returns an error if the request fails.
1057    pub async fn ticker_price(
1058        &self,
1059        symbol: Option<&str>,
1060    ) -> BinanceSpotHttpResult<Vec<TickerPrice>> {
1061        let params = symbol.map(TickerParams::for_symbol);
1062        let bytes = self.get_json("ticker/price", params.as_ref()).await?;
1063
1064        // Single symbol returns object, multiple returns array
1065        if symbol.is_some() {
1066            let ticker: TickerPrice = serde_json::from_slice(&bytes)
1067                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1068            Ok(vec![ticker])
1069        } else {
1070            let tickers: Vec<TickerPrice> = serde_json::from_slice(&bytes)
1071                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1072            Ok(tickers)
1073        }
1074    }
1075
1076    /// Returns best bid/ask price for a symbol or all symbols.
1077    ///
1078    /// If `symbol` is None, returns book ticker for all symbols.
1079    ///
1080    /// # Errors
1081    ///
1082    /// Returns an error if the request fails.
1083    pub async fn ticker_book(
1084        &self,
1085        symbol: Option<&str>,
1086    ) -> BinanceSpotHttpResult<Vec<BookTicker>> {
1087        let params = symbol.map(TickerParams::for_symbol);
1088        let bytes = self.get_json("ticker/bookTicker", params.as_ref()).await?;
1089
1090        // Single symbol returns object, multiple returns array
1091        if symbol.is_some() {
1092            let ticker: BookTicker = serde_json::from_slice(&bytes)
1093                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1094            Ok(vec![ticker])
1095        } else {
1096            let tickers: Vec<BookTicker> = serde_json::from_slice(&bytes)
1097                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1098            Ok(tickers)
1099        }
1100    }
1101
1102    /// Returns current average price for a symbol.
1103    ///
1104    /// # Errors
1105    ///
1106    /// Returns an error if the request fails.
1107    pub async fn avg_price(&self, symbol: &str) -> BinanceSpotHttpResult<AvgPrice> {
1108        let params = AvgPriceParams::new(symbol);
1109        let bytes = self.get_json("avgPrice", Some(&params)).await?;
1110
1111        let avg_price: AvgPrice = serde_json::from_slice(&bytes)
1112            .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1113        Ok(avg_price)
1114    }
1115
1116    /// Returns trading fee rates for symbols.
1117    ///
1118    /// If `symbol` is None, returns fee rates for all symbols.
1119    /// Uses SAPI endpoint (requires authentication).
1120    ///
1121    /// # Errors
1122    ///
1123    /// Returns an error if credentials are missing or the request fails.
1124    pub async fn get_trade_fee(
1125        &self,
1126        symbol: Option<&str>,
1127    ) -> BinanceSpotHttpResult<Vec<TradeFee>> {
1128        let params = symbol.map(TradeFeeParams::for_symbol);
1129        let bytes = self
1130            .get_signed_sapi("asset/tradeFee", params.as_ref())
1131            .await?;
1132
1133        let fees: Vec<TradeFee> = serde_json::from_slice(&bytes)
1134            .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1135        Ok(fees)
1136    }
1137
1138    /// Performs a signed GET request to SAPI endpoints (returns JSON).
1139    async fn get_signed_sapi<P>(
1140        &self,
1141        path: &str,
1142        params: Option<&P>,
1143    ) -> BinanceSpotHttpResult<Vec<u8>>
1144    where
1145        P: Serialize + ?Sized,
1146    {
1147        let cred = self
1148            .credential
1149            .as_ref()
1150            .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1151
1152        let mut query = params
1153            .map(serde_urlencoded::to_string)
1154            .transpose()
1155            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1156            .unwrap_or_default();
1157
1158        if !query.is_empty() {
1159            query.push('&');
1160        }
1161
1162        let timestamp = Timestamp::now().as_millisecond();
1163        query.push_str(&format!("timestamp={timestamp}"));
1164
1165        if let Some(recv_window) = self.recv_window {
1166            query.push_str(&format!("&recvWindow={recv_window}"));
1167        }
1168
1169        let signature = Self::percent_encode(&cred.sign(&query));
1170        query.push_str(&format!("&signature={signature}"));
1171
1172        // Build SAPI URL (different from regular API path)
1173        let normalized_path = if path.starts_with('/') {
1174            path.to_string()
1175        } else {
1176            format!("/{path}")
1177        };
1178
1179        let mut url = format!("{}{}{}", self.base_url, SAPI_PATH, normalized_path);
1180
1181        if !query.is_empty() {
1182            url.push('?');
1183            url.push_str(&query);
1184        }
1185
1186        let mut headers = HashMap::new();
1187        headers.insert(
1188            BINANCE_API_KEY_HEADER.to_string(),
1189            cred.api_key().to_string(),
1190        );
1191
1192        let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1193
1194        let response = self
1195            .client
1196            .request(
1197                Method::GET,
1198                url,
1199                None::<&HashMap<String, Vec<String>>>,
1200                Some(headers),
1201                None,
1202                None,
1203                Some(keys),
1204            )
1205            .await?;
1206
1207        if !response.status.is_success() {
1208            return self.parse_error_response(&response);
1209        }
1210
1211        Ok(response.body.to_vec())
1212    }
1213
1214    /// Percent-encodes a string for use in URL query parameters.
1215    fn percent_encode(input: &str) -> String {
1216        let mut result = String::with_capacity(input.len() * 3);
1217        for byte in input.bytes() {
1218            match byte {
1219                // Unreserved characters (RFC 3986)
1220                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1221                    result.push(byte as char);
1222                }
1223                _ => {
1224                    result.push('%');
1225                    result.push_str(&format!("{byte:02X}"));
1226                }
1227            }
1228        }
1229        result
1230    }
1231
1232    /// Submits multiple orders in a single request (up to 5 orders).
1233    ///
1234    /// Each order in the batch is processed independently. The response contains
1235    /// the result for each order, which can be either a success or an error.
1236    ///
1237    /// # Errors
1238    ///
1239    /// Returns an error if credentials are missing, the request fails, or
1240    /// JSON parsing fails. Individual order failures are returned in the
1241    /// response array as `BatchOrderResult::Error`.
1242    pub async fn batch_submit_orders(
1243        &self,
1244        orders: &[BatchOrderItem],
1245    ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
1246        if orders.is_empty() {
1247            return Ok(Vec::new());
1248        }
1249
1250        if orders.len() > 5 {
1251            return Err(BinanceSpotHttpError::ValidationError(
1252                "Batch order limit is 5 orders maximum".to_string(),
1253            ));
1254        }
1255
1256        let batch_json = serde_json::to_string(orders)
1257            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1258
1259        let bytes = self
1260            .batch_request(Method::POST, "batchOrders", &batch_json)
1261            .await?;
1262
1263        let results: Vec<BatchOrderResult> = serde_json::from_slice(&bytes)
1264            .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1265
1266        Ok(results)
1267    }
1268
1269    /// Cancels multiple orders in a single request (up to 5 orders).
1270    ///
1271    /// Each cancel in the batch is processed independently. The response contains
1272    /// the result for each cancel, which can be either a success or an error.
1273    ///
1274    /// # Errors
1275    ///
1276    /// Returns an error if credentials are missing, the request fails, or
1277    /// JSON parsing fails. Individual cancel failures are returned in the
1278    /// response array as `BatchCancelResult::Error`.
1279    pub async fn batch_cancel_orders(
1280        &self,
1281        cancels: &[BatchCancelItem],
1282    ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
1283        if cancels.is_empty() {
1284            return Ok(Vec::new());
1285        }
1286
1287        if cancels.len() > 5 {
1288            return Err(BinanceSpotHttpError::ValidationError(
1289                "Batch cancel limit is 5 orders maximum".to_string(),
1290            ));
1291        }
1292
1293        let batch_json = serde_json::to_string(cancels)
1294            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
1295
1296        let bytes = self
1297            .batch_request(Method::DELETE, "batchOrders", &batch_json)
1298            .await?;
1299
1300        let results: Vec<BatchCancelResult> = serde_json::from_slice(&bytes)
1301            .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1302
1303        Ok(results)
1304    }
1305
1306    /// Performs a signed batch request with the batchOrders parameter.
1307    async fn batch_request(
1308        &self,
1309        method: Method,
1310        path: &str,
1311        batch_json: &str,
1312    ) -> BinanceSpotHttpResult<Vec<u8>> {
1313        let cred = self
1314            .credential
1315            .as_ref()
1316            .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1317
1318        let encoded_batch = Self::percent_encode(batch_json);
1319        let timestamp = Timestamp::now().as_millisecond();
1320        let mut query = format!("batchOrders={encoded_batch}&timestamp={timestamp}");
1321
1322        if let Some(recv_window) = self.recv_window {
1323            query.push_str(&format!("&recvWindow={recv_window}"));
1324        }
1325
1326        let signature = Self::percent_encode(&cred.sign(&query));
1327        query.push_str(&format!("&signature={signature}"));
1328
1329        let url = self.build_url(path, &query);
1330
1331        let mut headers = HashMap::new();
1332        headers.insert(
1333            BINANCE_API_KEY_HEADER.to_string(),
1334            cred.api_key().to_string(),
1335        );
1336
1337        let keys = self.rate_limit_keys(true);
1338
1339        let response = self
1340            .client
1341            .request(
1342                method,
1343                url,
1344                None::<&HashMap<String, Vec<String>>>,
1345                Some(headers),
1346                None,
1347                None,
1348                Some(keys),
1349            )
1350            .await?;
1351
1352        if !response.status.is_success() {
1353            return self.parse_error_response(&response);
1354        }
1355
1356        Ok(response.body.to_vec())
1357    }
1358
1359    /// Returns account information including balances.
1360    ///
1361    /// # Errors
1362    ///
1363    /// Returns an error if the request fails or SBE decoding fails.
1364    pub async fn account(
1365        &self,
1366        params: &AccountInfoParams,
1367    ) -> BinanceSpotHttpResult<BinanceAccountInfo> {
1368        if self.json_responses {
1369            let bytes = self.get_signed_json("account", Some(params)).await?;
1370            let response: SpotAccountJson = serde_json::from_slice(&bytes)
1371                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1372            spot_account_from_json(response)
1373        } else {
1374            let bytes = self.get_signed("account", Some(params)).await?;
1375            Ok(parse::decode_account(&bytes)?)
1376        }
1377    }
1378
1379    /// Returns account-specific commission rates for one symbol.
1380    ///
1381    /// # Errors
1382    ///
1383    /// Returns an error if credentials are missing, the request fails, or JSON is malformed.
1384    pub async fn account_commission(
1385        &self,
1386        symbol: &str,
1387    ) -> BinanceSpotHttpResult<BinanceAccountCommission> {
1388        let params = AccountCommissionParams::new(symbol);
1389        let bytes = self
1390            .get_signed_json("account/commission", Some(&params))
1391            .await?;
1392        serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1393    }
1394
1395    /// Returns the minimal JSON account commission view used by Binance US.
1396    ///
1397    /// # Errors
1398    ///
1399    /// Returns an error if credentials are missing, the request fails, or JSON is malformed.
1400    pub async fn account_rates_json(&self) -> BinanceSpotHttpResult<BinanceAccountRatesJson> {
1401        let params = AccountInfoParams::default();
1402        let bytes = self.get_signed_json("account", Some(&params)).await?;
1403        serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1404    }
1405
1406    /// Returns account trade history for a symbol.
1407    ///
1408    /// # Errors
1409    ///
1410    /// Returns an error if the request fails or SBE decoding fails.
1411    pub async fn account_trades(
1412        &self,
1413        symbol: &str,
1414        order_id: Option<i64>,
1415        start_time: Option<i64>,
1416        end_time: Option<i64>,
1417        limit: Option<u32>,
1418    ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1419        self.account_trades_with_cursor(symbol, order_id, start_time, end_time, None, limit)
1420            .await
1421    }
1422
1423    async fn account_trades_with_cursor(
1424        &self,
1425        symbol: &str,
1426        order_id: Option<i64>,
1427        start_time: Option<i64>,
1428        end_time: Option<i64>,
1429        from_id: Option<i64>,
1430        limit: Option<u32>,
1431    ) -> BinanceSpotHttpResult<Vec<BinanceAccountTrade>> {
1432        let params = AccountTradesParams {
1433            symbol: symbol.to_string(),
1434            order_id,
1435            start_time,
1436            end_time,
1437            from_id,
1438            limit,
1439        };
1440
1441        if self.json_responses {
1442            let bytes = self.get_signed_json("myTrades", Some(&params)).await?;
1443            let response: Vec<SpotAccountTradeJson> = serde_json::from_slice(&bytes)
1444                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1445            response
1446                .into_iter()
1447                .map(spot_account_trade_from_json)
1448                .collect()
1449        } else {
1450            let bytes = self.get_signed("myTrades", Some(&params)).await?;
1451            Ok(parse::decode_account_trades(&bytes)?)
1452        }
1453    }
1454
1455    /// Queries an order's status.
1456    ///
1457    /// Either `order_id` or `client_order_id` must be provided.
1458    ///
1459    /// # Errors
1460    ///
1461    /// Returns an error if the request fails or SBE decoding fails.
1462    pub async fn query_order(
1463        &self,
1464        symbol: &str,
1465        order_id: Option<i64>,
1466        client_order_id: Option<&str>,
1467    ) -> BinanceSpotHttpResult<BinanceOrderResponse> {
1468        let params = QueryOrderParams {
1469            symbol: symbol.to_string(),
1470            order_id,
1471            orig_client_order_id: client_order_id.map(|s| s.to_string()),
1472        };
1473
1474        if self.json_responses {
1475            let bytes = self.get_signed_json("order", Some(&params)).await?;
1476            let response: SpotOrderJson = serde_json::from_slice(&bytes)
1477                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1478            spot_order_from_json(response)
1479        } else {
1480            let bytes = self.get_signed("order", Some(&params)).await?;
1481            Ok(parse::decode_order(&bytes)?)
1482        }
1483    }
1484
1485    /// Returns all open orders for a symbol or all symbols.
1486    ///
1487    /// # Errors
1488    ///
1489    /// Returns an error if the request fails or SBE decoding fails.
1490    pub async fn open_orders(
1491        &self,
1492        symbol: Option<&str>,
1493    ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1494        let params = OpenOrdersParams {
1495            symbol: symbol.map(|s| s.to_string()),
1496        };
1497
1498        if self.json_responses {
1499            let bytes = self.get_signed_json("openOrders", Some(&params)).await?;
1500            let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1501                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1502            response.into_iter().map(spot_order_from_json).collect()
1503        } else {
1504            let bytes = self.get_signed("openOrders", Some(&params)).await?;
1505            Ok(parse::decode_orders(&bytes)?)
1506        }
1507    }
1508
1509    /// Returns all orders (including closed) for a symbol.
1510    ///
1511    /// # Errors
1512    ///
1513    /// Returns an error if the request fails or SBE decoding fails.
1514    pub async fn all_orders(
1515        &self,
1516        symbol: &str,
1517        start_time: Option<i64>,
1518        end_time: Option<i64>,
1519        limit: Option<u32>,
1520    ) -> BinanceSpotHttpResult<Vec<BinanceOrderResponse>> {
1521        let params = AllOrdersParams {
1522            symbol: symbol.to_string(),
1523            order_id: None,
1524            start_time,
1525            end_time,
1526            limit,
1527        };
1528
1529        if self.json_responses {
1530            let bytes = self.get_signed_json("allOrders", Some(&params)).await?;
1531            let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1532                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1533            response.into_iter().map(spot_order_from_json).collect()
1534        } else {
1535            let bytes = self.get_signed("allOrders", Some(&params)).await?;
1536            Ok(parse::decode_orders(&bytes)?)
1537        }
1538    }
1539
1540    /// Performs a signed POST request for order operations.
1541    async fn post_order<P>(&self, path: &str, params: Option<&P>) -> BinanceSpotHttpResult<Vec<u8>>
1542    where
1543        P: Serialize + ?Sized,
1544    {
1545        if self.json_responses {
1546            self.post_signed_json(path, params).await
1547        } else {
1548            self.post_signed(path, params).await
1549        }
1550    }
1551
1552    /// Performs a signed DELETE request for cancel operations.
1553    async fn delete_order<P>(
1554        &self,
1555        path: &str,
1556        params: Option<&P>,
1557    ) -> BinanceSpotHttpResult<Vec<u8>>
1558    where
1559        P: Serialize + ?Sized,
1560    {
1561        if self.json_responses {
1562            self.delete_signed_json(path, params).await
1563        } else {
1564            self.delete_signed(path, params).await
1565        }
1566    }
1567
1568    /// Creates a new order.
1569    ///
1570    /// # Errors
1571    ///
1572    /// Returns an error if the request fails or SBE decoding fails.
1573    #[expect(clippy::too_many_arguments)]
1574    pub async fn new_order(
1575        &self,
1576        symbol: &str,
1577        side: BinanceSide,
1578        order_type: BinanceSpotOrderType,
1579        time_in_force: Option<BinanceTimeInForce>,
1580        quantity: Option<&str>,
1581        price: Option<&str>,
1582        client_order_id: Option<&str>,
1583        stop_price: Option<&str>,
1584    ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1585        let params = NewOrderParams {
1586            symbol: symbol.to_string(),
1587            side,
1588            order_type,
1589            time_in_force,
1590            quantity: quantity.map(|s| s.to_string()),
1591            quote_order_qty: None,
1592            price: price.map(|s| s.to_string()),
1593            new_client_order_id: client_order_id.map(|s| s.to_string()),
1594            stop_price: stop_price.map(|s| s.to_string()),
1595            trailing_delta: None,
1596            iceberg_qty: None,
1597            new_order_resp_type: Some(BinanceOrderResponseType::Full),
1598            self_trade_prevention_mode: None,
1599            strategy_id: None,
1600            strategy_type: None,
1601        };
1602        let bytes = self.post_order("order", Some(&params)).await?;
1603        self.decode_new_order_response(&bytes)
1604    }
1605
1606    /// Creates a new order with full parameter support.
1607    ///
1608    /// Extends [`new_order`](Self::new_order) with `quote_order_qty` (for market
1609    /// orders denominated in quote currency) and `iceberg_qty` (display
1610    /// quantity for iceberg orders).
1611    ///
1612    /// # Errors
1613    ///
1614    /// Returns an error if the request fails or SBE decoding fails.
1615    #[expect(clippy::too_many_arguments)]
1616    pub async fn new_order_full(
1617        &self,
1618        symbol: &str,
1619        side: BinanceSide,
1620        order_type: BinanceSpotOrderType,
1621        time_in_force: Option<BinanceTimeInForce>,
1622        quantity: Option<&str>,
1623        quote_order_qty: Option<&str>,
1624        price: Option<&str>,
1625        client_order_id: Option<&str>,
1626        stop_price: Option<&str>,
1627        iceberg_qty: Option<&str>,
1628    ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1629        let params = NewOrderParams {
1630            symbol: symbol.to_string(),
1631            side,
1632            order_type,
1633            time_in_force,
1634            quantity: quantity.map(|s| s.to_string()),
1635            quote_order_qty: quote_order_qty.map(|s| s.to_string()),
1636            price: price.map(|s| s.to_string()),
1637            new_client_order_id: client_order_id.map(|s| s.to_string()),
1638            stop_price: stop_price.map(|s| s.to_string()),
1639            trailing_delta: None,
1640            iceberg_qty: iceberg_qty.map(|s| s.to_string()),
1641            new_order_resp_type: Some(BinanceOrderResponseType::Full),
1642            self_trade_prevention_mode: None,
1643            strategy_id: None,
1644            strategy_type: None,
1645        };
1646        let bytes = self.post_order("order", Some(&params)).await?;
1647        self.decode_new_order_response(&bytes)
1648    }
1649
1650    /// Creates a new OCO order list.
1651    ///
1652    /// # Errors
1653    ///
1654    /// Returns an error if the request fails or JSON decoding fails.
1655    pub async fn new_oco_order_list(
1656        &self,
1657        params: &NewOcoOrderListParams,
1658    ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
1659        let bytes = self.post_signed_json("orderList/oco", Some(params)).await?;
1660        serde_json::from_slice(&bytes).map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))
1661    }
1662
1663    /// Cancels an existing order and places a new order atomically.
1664    ///
1665    /// # Errors
1666    ///
1667    /// Returns an error if the request fails or SBE decoding fails.
1668    #[expect(clippy::too_many_arguments)]
1669    pub async fn cancel_replace_order(
1670        &self,
1671        symbol: &str,
1672        side: BinanceSide,
1673        order_type: BinanceSpotOrderType,
1674        time_in_force: Option<BinanceTimeInForce>,
1675        quantity: Option<&str>,
1676        price: Option<&str>,
1677        cancel_order_id: Option<i64>,
1678        cancel_client_order_id: Option<&str>,
1679        new_client_order_id: Option<&str>,
1680    ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1681        let params = CancelReplaceOrderParams {
1682            symbol: symbol.to_string(),
1683            side,
1684            order_type,
1685            cancel_replace_mode: BinanceCancelReplaceMode::StopOnFailure,
1686            time_in_force,
1687            quantity: quantity.map(|s| s.to_string()),
1688            quote_order_qty: None,
1689            price: price.map(|s| s.to_string()),
1690            cancel_order_id,
1691            cancel_orig_client_order_id: cancel_client_order_id.map(|s| s.to_string()),
1692            new_client_order_id: new_client_order_id.map(|s| s.to_string()),
1693            stop_price: None,
1694            trailing_delta: None,
1695            iceberg_qty: None,
1696            new_order_resp_type: Some(BinanceOrderResponseType::Full),
1697            self_trade_prevention_mode: None,
1698        };
1699        let bytes = self
1700            .post_order("order/cancelReplace", Some(&params))
1701            .await?;
1702
1703        if self.json_responses {
1704            let response: SpotCancelReplaceJson = serde_json::from_slice(&bytes)
1705                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1706            spot_new_order_from_json(response.new_order_response)
1707        } else {
1708            self.decode_new_order_response(&bytes)
1709        }
1710    }
1711
1712    /// Cancels an existing order.
1713    ///
1714    /// Either `order_id` or `client_order_id` must be provided.
1715    ///
1716    /// # Errors
1717    ///
1718    /// Returns an error if the request fails or SBE decoding fails.
1719    pub async fn cancel_order(
1720        &self,
1721        symbol: &str,
1722        order_id: Option<i64>,
1723        client_order_id: Option<&str>,
1724    ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1725        let params = match (order_id, client_order_id) {
1726            (Some(id), _) => CancelOrderParams::by_order_id(symbol, id),
1727            (None, Some(id)) => CancelOrderParams::by_client_order_id(symbol, id.to_string()),
1728            (None, None) => {
1729                return Err(BinanceSpotHttpError::ValidationError(
1730                    "Either order_id or client_order_id must be provided".to_string(),
1731                ));
1732            }
1733        };
1734        let bytes = self.delete_order("order", Some(&params)).await?;
1735        self.decode_cancel_order_response(&bytes)
1736    }
1737
1738    /// Cancels all open orders for a symbol.
1739    ///
1740    /// # Errors
1741    ///
1742    /// Returns an error if the request fails or SBE decoding fails.
1743    pub async fn cancel_open_orders(
1744        &self,
1745        symbol: &str,
1746    ) -> BinanceSpotHttpResult<Vec<BinanceCancelOrderResponse>> {
1747        let params = CancelOpenOrdersParams::new(symbol.to_string());
1748        let bytes = self.delete_order("openOrders", Some(&params)).await?;
1749        if self.json_responses {
1750            let response: Vec<SpotOrderJson> = serde_json::from_slice(&bytes)
1751                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1752            response
1753                .into_iter()
1754                .map(spot_cancel_order_from_json)
1755                .collect()
1756        } else {
1757            Ok(parse::decode_cancel_open_orders(&bytes)?)
1758        }
1759    }
1760
1761    fn decode_new_order_response(
1762        &self,
1763        bytes: &[u8],
1764    ) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
1765        if self.json_responses {
1766            let response: SpotOrderJson = serde_json::from_slice(bytes)
1767                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1768            spot_new_order_from_json(response)
1769        } else {
1770            Ok(parse::decode_new_order_full(bytes)?)
1771        }
1772    }
1773
1774    fn decode_cancel_order_response(
1775        &self,
1776        bytes: &[u8],
1777    ) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
1778        if self.json_responses {
1779            let response: SpotOrderJson = serde_json::from_slice(bytes)
1780                .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1781            spot_cancel_order_from_json(response)
1782        } else {
1783            Ok(parse::decode_cancel_order(bytes)?)
1784        }
1785    }
1786
1787    /// Performs an API-key authenticated request (no signature) that returns JSON.
1788    async fn request_with_api_key<P>(
1789        &self,
1790        method: Method,
1791        path: &str,
1792        params: Option<&P>,
1793    ) -> BinanceSpotHttpResult<Vec<u8>>
1794    where
1795        P: Serialize + ?Sized,
1796    {
1797        let cred = self
1798            .credential
1799            .as_ref()
1800            .ok_or(BinanceSpotHttpError::MissingCredentials)?;
1801
1802        let query = params
1803            .map(serde_urlencoded::to_string)
1804            .transpose()
1805            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?
1806            .unwrap_or_default();
1807
1808        let url = self.build_url(path, &query);
1809
1810        let mut headers = HashMap::new();
1811        headers.insert(
1812            BINANCE_API_KEY_HEADER.to_string(),
1813            cred.api_key().to_string(),
1814        );
1815
1816        let keys = vec![BINANCE_GLOBAL_RATE_KEY.to_string()];
1817
1818        let response = self
1819            .client
1820            .request(
1821                method,
1822                url,
1823                None::<&HashMap<String, Vec<String>>>,
1824                Some(headers),
1825                None,
1826                None,
1827                Some(keys),
1828            )
1829            .await?;
1830
1831        if !response.status.is_success() {
1832            return self.parse_error_response(&response);
1833        }
1834
1835        Ok(response.body.to_vec())
1836    }
1837
1838    /// Creates a new listen key for the user data stream.
1839    ///
1840    /// Listen keys are valid for 60 minutes. Use `extend_listen_key` to keep
1841    /// the stream alive.
1842    ///
1843    /// # Errors
1844    ///
1845    /// Returns an error if credentials are missing or the request fails.
1846    pub async fn create_listen_key(&self) -> BinanceSpotHttpResult<ListenKeyResponse> {
1847        let bytes = self
1848            .request_with_api_key(Method::POST, "userDataStream", None::<&()>)
1849            .await?;
1850
1851        let response: ListenKeyResponse = serde_json::from_slice(&bytes)
1852            .map_err(|e| BinanceSpotHttpError::JsonError(e.to_string()))?;
1853
1854        Ok(response)
1855    }
1856
1857    /// Extends the validity of a listen key by 60 minutes.
1858    ///
1859    /// Should be called periodically to keep the user data stream alive.
1860    ///
1861    /// # Errors
1862    ///
1863    /// Returns an error if credentials are missing or the request fails.
1864    pub async fn extend_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1865        let params = ListenKeyParams::new(listen_key);
1866        self.request_with_api_key(Method::PUT, "userDataStream", Some(&params))
1867            .await?;
1868        Ok(())
1869    }
1870
1871    /// Closes a listen key, terminating the user data stream.
1872    ///
1873    /// # Errors
1874    ///
1875    /// Returns an error if credentials are missing or the request fails.
1876    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceSpotHttpResult<()> {
1877        let params = ListenKeyParams::new(listen_key);
1878        self.request_with_api_key(Method::DELETE, "userDataStream", Some(&params))
1879            .await?;
1880        Ok(())
1881    }
1882}
1883
1884fn spot_depth_from_json(response: SpotDepthJson) -> BinanceSpotHttpResult<BinanceDepth> {
1885    let price_scale = decimal_common_scale(
1886        response
1887            .bids
1888            .iter()
1889            .chain(&response.asks)
1890            .map(|level| level[0].as_str()),
1891    )?;
1892    let qty_scale = decimal_common_scale(
1893        response
1894            .bids
1895            .iter()
1896            .chain(&response.asks)
1897            .map(|level| level[1].as_str()),
1898    )?;
1899    let parse_levels = |levels: Vec<[String; 2]>| {
1900        levels
1901            .into_iter()
1902            .map(|level| {
1903                Ok(BinancePriceLevel {
1904                    price_mantissa: decimal_mantissa(&level[0], price_scale)?,
1905                    qty_mantissa: decimal_mantissa(&level[1], qty_scale)?,
1906                })
1907            })
1908            .collect::<BinanceSpotHttpResult<Vec<_>>>()
1909    };
1910
1911    Ok(BinanceDepth {
1912        last_update_id: response.last_update_id,
1913        price_exponent: decimal_exponent(price_scale)?,
1914        qty_exponent: decimal_exponent(qty_scale)?,
1915        bids: parse_levels(response.bids)?,
1916        asks: parse_levels(response.asks)?,
1917    })
1918}
1919
1920fn spot_trades_from_json(response: Vec<SpotTradeJson>) -> BinanceSpotHttpResult<BinanceTrades> {
1921    let price_scale = decimal_common_scale(
1922        response
1923            .iter()
1924            .flat_map(|trade| [trade.price.as_str(), trade.quote_qty.as_str()]),
1925    )?;
1926    let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1927    let trades = response
1928        .into_iter()
1929        .map(|trade| {
1930            Ok(BinanceTrade {
1931                id: trade.id,
1932                price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1933                qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1934                quote_qty_mantissa: decimal_mantissa(&trade.quote_qty, price_scale)?,
1935                time: millis_to_micros(trade.time)?,
1936                is_buyer_maker: trade.is_buyer_maker,
1937                is_best_match: trade.is_best_match,
1938            })
1939        })
1940        .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
1941
1942    Ok(BinanceTrades {
1943        price_exponent: decimal_exponent(price_scale)?,
1944        qty_exponent: decimal_exponent(qty_scale)?,
1945        trades,
1946    })
1947}
1948
1949fn spot_agg_trades_from_json(
1950    response: Vec<SpotAggTradeJson>,
1951) -> BinanceSpotHttpResult<BinanceAggTrades> {
1952    let price_scale = decimal_common_scale(response.iter().map(|trade| trade.price.as_str()))?;
1953    let qty_scale = decimal_common_scale(response.iter().map(|trade| trade.qty.as_str()))?;
1954    let trades = response
1955        .into_iter()
1956        .map(|trade| {
1957            Ok(BinanceAggTrade {
1958                id: trade.id,
1959                price_mantissa: decimal_mantissa(&trade.price, price_scale)?,
1960                qty_mantissa: decimal_mantissa(&trade.qty, qty_scale)?,
1961                first_trade_id: trade.first_trade_id,
1962                last_trade_id: trade.last_trade_id,
1963                time: millis_to_micros(trade.time)?,
1964                is_buyer_maker: trade.is_buyer_maker,
1965                is_best_match: trade.is_best_match,
1966            })
1967        })
1968        .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
1969
1970    Ok(BinanceAggTrades {
1971        price_exponent: decimal_exponent(price_scale)?,
1972        qty_exponent: decimal_exponent(qty_scale)?,
1973        trades,
1974    })
1975}
1976
1977fn spot_klines_from_json(response: Vec<SpotKlineJson>) -> BinanceSpotHttpResult<BinanceKlines> {
1978    let price_scale = decimal_common_scale(response.iter().flat_map(|kline| {
1979        [
1980            kline.1.as_str(),
1981            kline.2.as_str(),
1982            kline.3.as_str(),
1983            kline.4.as_str(),
1984            kline.7.as_str(),
1985            kline.10.as_str(),
1986        ]
1987    }))?;
1988    let qty_scale = decimal_common_scale(
1989        response
1990            .iter()
1991            .flat_map(|kline| [kline.5.as_str(), kline.9.as_str()]),
1992    )?;
1993    let klines = response
1994        .into_iter()
1995        .map(|kline| {
1996            let volume = decimal_i128_bytes(&kline.5, qty_scale)?;
1997            let quote_volume = decimal_i128_bytes(&kline.7, price_scale)?;
1998            let taker_buy_base_volume = decimal_i128_bytes(&kline.9, qty_scale)?;
1999            let taker_buy_quote_volume = decimal_i128_bytes(&kline.10, price_scale)?;
2000            Ok(BinanceKline {
2001                open_time: millis_to_micros(kline.0)?,
2002                open_price: decimal_mantissa(&kline.1, price_scale)?,
2003                high_price: decimal_mantissa(&kline.2, price_scale)?,
2004                low_price: decimal_mantissa(&kline.3, price_scale)?,
2005                close_price: decimal_mantissa(&kline.4, price_scale)?,
2006                volume,
2007                close_time: millis_to_micros(kline.6)?,
2008                quote_volume,
2009                num_trades: kline.8,
2010                taker_buy_base_volume,
2011                taker_buy_quote_volume,
2012            })
2013        })
2014        .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2015
2016    Ok(BinanceKlines {
2017        price_exponent: decimal_exponent(price_scale)?,
2018        qty_exponent: decimal_exponent(qty_scale)?,
2019        klines,
2020    })
2021}
2022
2023fn decimal_i128_bytes(value: &str, scale: u32) -> BinanceSpotHttpResult<[u8; 16]> {
2024    let mut decimal = Decimal::from_str_exact(value)
2025        .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2026    decimal.rescale(scale);
2027    Ok(decimal.mantissa().to_le_bytes())
2028}
2029
2030fn spot_account_from_json(response: SpotAccountJson) -> BinanceSpotHttpResult<BinanceAccountInfo> {
2031    let commission_rates = response.commission_rates.map_or_else(
2032        || {
2033            [
2034                Decimal::new(response.maker_commission, 4).to_string(),
2035                Decimal::new(response.taker_commission, 4).to_string(),
2036                Decimal::new(response.buyer_commission, 4).to_string(),
2037                Decimal::new(response.seller_commission, 4).to_string(),
2038            ]
2039        },
2040        |rates| [rates.maker, rates.taker, rates.buyer, rates.seller],
2041    );
2042    let commission_scale = decimal_common_scale(commission_rates.iter().map(String::as_str))?;
2043    let balances = response
2044        .balances
2045        .into_iter()
2046        .map(|balance| {
2047            let scale = decimal_common_scale([balance.free.as_str(), balance.locked.as_str()])?;
2048            Ok(BinanceBalance {
2049                asset: balance.asset,
2050                free_mantissa: decimal_mantissa(&balance.free, scale)?,
2051                locked_mantissa: decimal_mantissa(&balance.locked, scale)?,
2052                exponent: decimal_exponent(scale)?,
2053            })
2054        })
2055        .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2056
2057    Ok(BinanceAccountInfo {
2058        commission_exponent: decimal_exponent(commission_scale)?,
2059        maker_commission_mantissa: decimal_mantissa(&commission_rates[0], commission_scale)?,
2060        taker_commission_mantissa: decimal_mantissa(&commission_rates[1], commission_scale)?,
2061        buyer_commission_mantissa: decimal_mantissa(&commission_rates[2], commission_scale)?,
2062        seller_commission_mantissa: decimal_mantissa(&commission_rates[3], commission_scale)?,
2063        can_trade: response.can_trade,
2064        can_withdraw: response.can_withdraw,
2065        can_deposit: response.can_deposit,
2066        require_self_trade_prevention: response.require_self_trade_prevention,
2067        prevent_sor: response.prevent_sor,
2068        update_time: millis_to_micros(response.update_time)?,
2069        account_type: response.account_type,
2070        balances,
2071    })
2072}
2073
2074fn spot_new_order_from_json(
2075    response: SpotOrderJson,
2076) -> BinanceSpotHttpResult<BinanceNewOrderResponse> {
2077    let (price_scale, qty_scale) = spot_order_scales(&response)?;
2078    let fills = response
2079        .fills
2080        .iter()
2081        .map(|fill| {
2082            let commission_scale = decimal_common_scale([fill.commission.as_str()])?;
2083            Ok(BinanceOrderFill {
2084                price_mantissa: decimal_mantissa(&fill.price, price_scale)?,
2085                qty_mantissa: decimal_mantissa(&fill.qty, qty_scale)?,
2086                commission_mantissa: decimal_mantissa(&fill.commission, commission_scale)?,
2087                commission_exponent: decimal_exponent(commission_scale)?,
2088                commission_asset: fill.commission_asset.clone(),
2089                trade_id: fill.trade_id,
2090            })
2091        })
2092        .collect::<BinanceSpotHttpResult<Vec<_>>>()?;
2093
2094    Ok(BinanceNewOrderResponse {
2095        price_exponent: decimal_exponent(price_scale)?,
2096        qty_exponent: decimal_exponent(qty_scale)?,
2097        order_id: response.order_id,
2098        order_list_id: valid_order_list_id(response.order_list_id),
2099        transact_time: millis_to_micros(response.transact_time)?,
2100        price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2101        orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2102        executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2103        cummulative_quote_qty_mantissa: decimal_mantissa(
2104            &response.cummulative_quote_qty,
2105            price_scale + qty_scale,
2106        )?,
2107        status: spot_sbe_order_status(response.status),
2108        time_in_force: spot_sbe_time_in_force(response.time_in_force),
2109        order_type: spot_sbe_order_type(response.order_type),
2110        side: spot_sbe_order_side(response.side),
2111        stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2112        working_time: response.working_time.map(millis_to_micros).transpose()?,
2113        self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2114        client_order_id: response.client_order_id,
2115        symbol: response.symbol,
2116        fills,
2117        expiry_reason: None,
2118    })
2119}
2120
2121fn spot_cancel_order_from_json(
2122    response: SpotOrderJson,
2123) -> BinanceSpotHttpResult<BinanceCancelOrderResponse> {
2124    let (price_scale, qty_scale) = spot_order_scales(&response)?;
2125    Ok(BinanceCancelOrderResponse {
2126        price_exponent: decimal_exponent(price_scale)?,
2127        qty_exponent: decimal_exponent(qty_scale)?,
2128        order_id: response.order_id,
2129        order_list_id: valid_order_list_id(response.order_list_id),
2130        transact_time: millis_to_micros(response.transact_time)?,
2131        price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2132        orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2133        executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2134        cummulative_quote_qty_mantissa: decimal_mantissa(
2135            &response.cummulative_quote_qty,
2136            price_scale + qty_scale,
2137        )?,
2138        status: spot_sbe_order_status(response.status),
2139        time_in_force: spot_sbe_time_in_force(response.time_in_force),
2140        order_type: spot_sbe_order_type(response.order_type),
2141        side: spot_sbe_order_side(response.side),
2142        self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2143        client_order_id: response.client_order_id,
2144        orig_client_order_id: response.orig_client_order_id,
2145        symbol: response.symbol,
2146    })
2147}
2148
2149fn spot_order_from_json(response: SpotOrderJson) -> BinanceSpotHttpResult<BinanceOrderResponse> {
2150    let (price_scale, qty_scale) = spot_order_scales(&response)?;
2151    Ok(BinanceOrderResponse {
2152        price_exponent: decimal_exponent(price_scale)?,
2153        qty_exponent: decimal_exponent(qty_scale)?,
2154        order_id: response.order_id,
2155        order_list_id: valid_order_list_id(response.order_list_id),
2156        price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2157        orig_qty_mantissa: decimal_mantissa(&response.orig_qty, qty_scale)?,
2158        executed_qty_mantissa: decimal_mantissa(&response.executed_qty, qty_scale)?,
2159        cummulative_quote_qty_mantissa: decimal_mantissa(
2160            &response.cummulative_quote_qty,
2161            price_scale + qty_scale,
2162        )?,
2163        status: spot_sbe_order_status(response.status),
2164        time_in_force: spot_sbe_time_in_force(response.time_in_force),
2165        order_type: spot_sbe_order_type(response.order_type),
2166        side: spot_sbe_order_side(response.side),
2167        stop_price_mantissa: decimal_optional_mantissa(&response.stop_price, price_scale)?,
2168        iceberg_qty_mantissa: decimal_optional_mantissa(&response.iceberg_qty, qty_scale)?,
2169        time: millis_to_micros(response.time)?,
2170        update_time: millis_to_micros(response.update_time)?,
2171        is_working: response.is_working,
2172        working_time: response.working_time.map(millis_to_micros).transpose()?,
2173        orig_quote_order_qty_mantissa: decimal_mantissa(
2174            &response.orig_quote_order_qty,
2175            price_scale + qty_scale,
2176        )?,
2177        self_trade_prevention_mode: spot_sbe_stp(response.self_trade_prevention_mode),
2178        client_order_id: response.client_order_id,
2179        symbol: response.symbol,
2180        expiry_reason: None,
2181    })
2182}
2183
2184fn spot_account_trade_from_json(
2185    response: SpotAccountTradeJson,
2186) -> BinanceSpotHttpResult<BinanceAccountTrade> {
2187    let price_scale = decimal_common_scale([response.price.as_str()])?;
2188    let qty_scale = decimal_common_scale([response.qty.as_str()])?;
2189    let commission_scale = decimal_common_scale([response.commission.as_str()])?;
2190    Ok(BinanceAccountTrade {
2191        price_exponent: decimal_exponent(price_scale)?,
2192        qty_exponent: decimal_exponent(qty_scale)?,
2193        commission_exponent: decimal_exponent(commission_scale)?,
2194        id: response.id,
2195        order_id: response.order_id,
2196        order_list_id: valid_order_list_id(response.order_list_id),
2197        price_mantissa: decimal_mantissa(&response.price, price_scale)?,
2198        qty_mantissa: decimal_mantissa(&response.qty, qty_scale)?,
2199        quote_qty_mantissa: decimal_mantissa(&response.quote_qty, price_scale + qty_scale)?,
2200        commission_mantissa: decimal_mantissa(&response.commission, commission_scale)?,
2201        time: millis_to_micros(response.time)?,
2202        is_buyer: response.is_buyer,
2203        is_maker: response.is_maker,
2204        is_best_match: response.is_best_match,
2205        symbol: response.symbol,
2206        commission_asset: response.commission_asset,
2207    })
2208}
2209
2210fn spot_order_scales(response: &SpotOrderJson) -> BinanceSpotHttpResult<(u32, u32)> {
2211    let price_scale = decimal_common_scale(
2212        [response.price.as_str(), response.stop_price.as_str()]
2213            .into_iter()
2214            .chain(response.fills.iter().map(|fill| fill.price.as_str())),
2215    )?;
2216    let qty_scale = decimal_common_scale(
2217        [
2218            response.orig_qty.as_str(),
2219            response.executed_qty.as_str(),
2220            response.iceberg_qty.as_str(),
2221        ]
2222        .into_iter()
2223        .chain(response.fills.iter().map(|fill| fill.qty.as_str())),
2224    )?;
2225    Ok((price_scale, qty_scale))
2226}
2227
2228fn decimal_common_scale<'a>(
2229    values: impl IntoIterator<Item = &'a str>,
2230) -> BinanceSpotHttpResult<u32> {
2231    values
2232        .into_iter()
2233        .filter(|value| !value.is_empty())
2234        .try_fold(0, |scale, value| {
2235            let decimal = Decimal::from_str_exact(value)
2236                .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2237            Ok(scale.max(decimal.scale()))
2238        })
2239}
2240
2241fn decimal_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<i64> {
2242    if value.is_empty() {
2243        return Ok(0);
2244    }
2245    let mut decimal = Decimal::from_str_exact(value)
2246        .map_err(|e| BinanceSpotHttpError::ResponseParseError(e.to_string()))?;
2247    decimal.rescale(scale);
2248    i64::try_from(decimal.mantissa()).map_err(|_| {
2249        BinanceSpotHttpError::ResponseParseError(format!(
2250            "decimal mantissa is outside i64 range: {value}"
2251        ))
2252    })
2253}
2254
2255fn decimal_optional_mantissa(value: &str, scale: u32) -> BinanceSpotHttpResult<Option<i64>> {
2256    let mantissa = decimal_mantissa(value, scale)?;
2257    Ok((mantissa != 0).then_some(mantissa))
2258}
2259
2260fn decimal_exponent(scale: u32) -> BinanceSpotHttpResult<i8> {
2261    i8::try_from(scale)
2262        .map(|scale| -scale)
2263        .map_err(|_| BinanceSpotHttpError::ResponseParseError("decimal scale exceeds i8".into()))
2264}
2265
2266fn millis_to_micros(timestamp: i64) -> BinanceSpotHttpResult<i64> {
2267    timestamp.checked_mul(1_000).ok_or_else(|| {
2268        BinanceSpotHttpError::ResponseParseError(format!(
2269            "timestamp overflows microseconds: {timestamp}"
2270        ))
2271    })
2272}
2273
2274fn valid_order_list_id(order_list_id: Option<i64>) -> Option<i64> {
2275    order_list_id.filter(|value| *value >= 0)
2276}
2277
2278const fn spot_sbe_order_status(status: BinanceOrderStatus) -> SbeOrderStatus {
2279    match status {
2280        BinanceOrderStatus::New => SbeOrderStatus::New,
2281        BinanceOrderStatus::PendingNew => SbeOrderStatus::PendingNew,
2282        BinanceOrderStatus::PartiallyFilled => SbeOrderStatus::PartiallyFilled,
2283        BinanceOrderStatus::Filled => SbeOrderStatus::Filled,
2284        BinanceOrderStatus::Canceled => SbeOrderStatus::Canceled,
2285        BinanceOrderStatus::PendingCancel => SbeOrderStatus::PendingCancel,
2286        BinanceOrderStatus::Rejected => SbeOrderStatus::Rejected,
2287        BinanceOrderStatus::Expired => SbeOrderStatus::Expired,
2288        BinanceOrderStatus::ExpiredInMatch => SbeOrderStatus::ExpiredInMatch,
2289        BinanceOrderStatus::NewInsurance
2290        | BinanceOrderStatus::NewAdl
2291        | BinanceOrderStatus::Unknown => SbeOrderStatus::Unknown,
2292    }
2293}
2294
2295const fn spot_sbe_time_in_force(time_in_force: BinanceTimeInForce) -> SbeTimeInForce {
2296    match time_in_force {
2297        BinanceTimeInForce::Gtc => SbeTimeInForce::Gtc,
2298        BinanceTimeInForce::Ioc => SbeTimeInForce::Ioc,
2299        BinanceTimeInForce::Fok => SbeTimeInForce::Fok,
2300        BinanceTimeInForce::Gtx
2301        | BinanceTimeInForce::Gtd
2302        | BinanceTimeInForce::Rpi
2303        | BinanceTimeInForce::Unknown => SbeTimeInForce::NonRepresentable,
2304    }
2305}
2306
2307const fn spot_sbe_order_type(order_type: BinanceSpotOrderType) -> SbeOrderType {
2308    match order_type {
2309        BinanceSpotOrderType::Market => SbeOrderType::Market,
2310        BinanceSpotOrderType::Limit => SbeOrderType::Limit,
2311        BinanceSpotOrderType::StopLoss => SbeOrderType::StopLoss,
2312        BinanceSpotOrderType::StopLossLimit => SbeOrderType::StopLossLimit,
2313        BinanceSpotOrderType::TakeProfit => SbeOrderType::TakeProfit,
2314        BinanceSpotOrderType::TakeProfitLimit => SbeOrderType::TakeProfitLimit,
2315        BinanceSpotOrderType::LimitMaker => SbeOrderType::LimitMaker,
2316        BinanceSpotOrderType::Unknown => SbeOrderType::NonRepresentable,
2317    }
2318}
2319
2320const fn spot_sbe_order_side(side: BinanceSide) -> SbeOrderSide {
2321    match side {
2322        BinanceSide::Buy => SbeOrderSide::Buy,
2323        BinanceSide::Sell => SbeOrderSide::Sell,
2324    }
2325}
2326
2327fn spot_sbe_stp(mode: Option<BinanceSelfTradePreventionMode>) -> SbeSelfTradePreventionMode {
2328    match mode.unwrap_or(BinanceSelfTradePreventionMode::None) {
2329        BinanceSelfTradePreventionMode::None => SbeSelfTradePreventionMode::None,
2330        BinanceSelfTradePreventionMode::ExpireMaker => SbeSelfTradePreventionMode::ExpireMaker,
2331        BinanceSelfTradePreventionMode::ExpireTaker => SbeSelfTradePreventionMode::ExpireTaker,
2332        BinanceSelfTradePreventionMode::ExpireBoth => SbeSelfTradePreventionMode::ExpireBoth,
2333        BinanceSelfTradePreventionMode::Decrement => SbeSelfTradePreventionMode::Decrement,
2334        BinanceSelfTradePreventionMode::Transfer => SbeSelfTradePreventionMode::Transfer,
2335        BinanceSelfTradePreventionMode::Unknown => SbeSelfTradePreventionMode::NonRepresentable,
2336    }
2337}
2338
2339/// High-level HTTP client for Binance Spot API.
2340///
2341/// Wraps [`BinanceRawSpotHttpClient`] and provides domain-level methods:
2342/// - Simple types (ping, server_time): Pass through from raw client.
2343/// - Complex types (instruments, orders): Transform to Nautilus domain types.
2344pub struct BinanceSpotHttpClient {
2345    inner: Arc<BinanceRawSpotHttpClient>,
2346    clock: &'static AtomicTime,
2347    instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
2348}
2349
2350impl Clone for BinanceSpotHttpClient {
2351    fn clone(&self) -> Self {
2352        Self {
2353            inner: self.inner.clone(),
2354            clock: self.clock,
2355            instruments_cache: self.instruments_cache.clone(),
2356        }
2357    }
2358}
2359
2360impl Debug for BinanceSpotHttpClient {
2361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2362        f.debug_struct(stringify!(BinanceSpotHttpClient))
2363            .field("inner", &self.inner)
2364            .field("instruments_cached", &self.instruments_cache.len())
2365            .finish()
2366    }
2367}
2368
2369impl BinanceSpotHttpClient {
2370    /// Creates a new Binance Spot HTTP client.
2371    ///
2372    /// # Errors
2373    ///
2374    /// Returns an error if the underlying HTTP client cannot be created.
2375    #[expect(clippy::too_many_arguments)]
2376    pub fn new(
2377        environment: BinanceEnvironment,
2378        clock: &'static AtomicTime,
2379        api_key: Option<String>,
2380        api_secret: Option<String>,
2381        base_url_override: Option<String>,
2382        recv_window: Option<u64>,
2383        timeout_secs: Option<u64>,
2384        proxy_url: Option<String>,
2385    ) -> BinanceSpotHttpResult<Self> {
2386        Self::new_with_json_responses(
2387            environment,
2388            clock,
2389            api_key,
2390            api_secret,
2391            base_url_override,
2392            recv_window,
2393            timeout_secs,
2394            proxy_url,
2395            false,
2396        )
2397    }
2398
2399    /// Creates a Spot client for an endpoint that returns JSON REST payloads.
2400    ///
2401    /// # Errors
2402    ///
2403    /// Returns an error if the underlying HTTP client cannot be created.
2404    #[expect(clippy::too_many_arguments)]
2405    pub fn new_with_json_responses(
2406        environment: BinanceEnvironment,
2407        clock: &'static AtomicTime,
2408        api_key: Option<String>,
2409        api_secret: Option<String>,
2410        base_url_override: Option<String>,
2411        recv_window: Option<u64>,
2412        timeout_secs: Option<u64>,
2413        proxy_url: Option<String>,
2414        json_responses: bool,
2415    ) -> BinanceSpotHttpResult<Self> {
2416        let inner = BinanceRawSpotHttpClient::new_with_json_responses(
2417            environment,
2418            api_key,
2419            api_secret,
2420            base_url_override,
2421            recv_window,
2422            timeout_secs,
2423            proxy_url,
2424            json_responses,
2425        )?;
2426
2427        Ok(Self {
2428            inner: Arc::new(inner),
2429            clock,
2430            instruments_cache: Arc::new(AtomicMap::new()),
2431        })
2432    }
2433
2434    /// Returns a reference to the inner raw client.
2435    #[must_use]
2436    pub fn inner(&self) -> &BinanceRawSpotHttpClient {
2437        &self.inner
2438    }
2439
2440    /// Returns whether signed requests can be made.
2441    #[must_use]
2442    pub fn has_credentials(&self) -> bool {
2443        self.inner.has_credentials()
2444    }
2445
2446    /// Returns the SBE schema ID.
2447    #[must_use]
2448    pub const fn schema_id() -> u16 {
2449        SBE_SCHEMA_ID
2450    }
2451
2452    /// Returns the SBE schema version.
2453    #[must_use]
2454    pub const fn schema_version() -> u16 {
2455        SBE_SCHEMA_VERSION
2456    }
2457
2458    /// Generates a timestamp for initialization.
2459    fn generate_ts_init(&self) -> UnixNanos {
2460        self.clock.get_time_ns()
2461    }
2462
2463    fn command_validation_error(message: impl Into<String>) -> anyhow::Error {
2464        anyhow::anyhow!(BinanceSpotHttpError::ValidationError(message.into()))
2465    }
2466
2467    fn response_parse_error(message: impl Into<String>) -> anyhow::Error {
2468        anyhow::anyhow!(BinanceSpotHttpError::ResponseParseError(message.into()))
2469    }
2470
2471    /// Retrieves an instrument from the cache.
2472    fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
2473        self.instruments_cache
2474            .get_cloned(&symbol)
2475            .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not in cache"))
2476    }
2477
2478    /// Caches multiple instruments.
2479    pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
2480        self.instruments_cache.rcu(move |cache| {
2481            for instrument in &instruments {
2482                cache.insert(instrument.raw_symbol().inner(), instrument.clone());
2483            }
2484        });
2485    }
2486
2487    /// Replaces the complete instrument cache.
2488    pub fn replace_instruments(&self, instruments: &[InstrumentAny]) {
2489        let cache = instruments
2490            .iter()
2491            .map(|instrument| (instrument.raw_symbol().inner(), instrument.clone()))
2492            .collect();
2493        self.instruments_cache.store(cache);
2494    }
2495
2496    /// Caches a single instrument.
2497    pub fn cache_instrument(&self, instrument: InstrumentAny) {
2498        self.instruments_cache
2499            .insert(instrument.raw_symbol().inner(), instrument);
2500    }
2501
2502    /// Gets an instrument from the cache by symbol.
2503    #[must_use]
2504    pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
2505        self.instruments_cache.get_cloned(symbol)
2506    }
2507
2508    /// Tests connectivity to the API.
2509    ///
2510    /// # Errors
2511    ///
2512    /// Returns an error if the request fails or SBE decoding fails.
2513    pub async fn ping(&self) -> BinanceSpotHttpResult<()> {
2514        self.inner.ping().await
2515    }
2516
2517    /// Returns the server time in **microseconds** since epoch.
2518    ///
2519    /// Note: SBE provides microsecond precision vs JSON's milliseconds.
2520    ///
2521    /// # Errors
2522    ///
2523    /// Returns an error if the request fails or SBE decoding fails.
2524    pub async fn server_time(&self) -> BinanceSpotHttpResult<i64> {
2525        self.inner.server_time().await
2526    }
2527
2528    /// Returns exchange information including trading symbols.
2529    ///
2530    /// # Errors
2531    ///
2532    /// Returns an error if the request fails or SBE decoding fails.
2533    pub async fn exchange_info(
2534        &self,
2535    ) -> BinanceSpotHttpResult<super::models::BinanceExchangeInfoSbe> {
2536        self.inner.exchange_info().await
2537    }
2538
2539    /// Returns a fresh status snapshot for Global or US Spot symbols.
2540    ///
2541    /// # Errors
2542    ///
2543    /// Returns an error if exchange info cannot be requested or decoded.
2544    pub async fn request_symbol_statuses(
2545        &self,
2546        us: bool,
2547    ) -> BinanceSpotHttpResult<AHashMap<InstrumentId, MarketStatusAction>> {
2548        let mut statuses = AHashMap::new();
2549
2550        if us {
2551            let info = self.inner.exchange_info_json().await?;
2552            for symbol in info.symbols {
2553                let instrument_id =
2554                    InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2555                statuses.insert(instrument_id, spot_json_market_status(&symbol.status));
2556            }
2557        } else {
2558            let info = self.exchange_info().await?;
2559            for symbol in info.symbols {
2560                let instrument_id =
2561                    InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2562                statuses.insert(
2563                    instrument_id,
2564                    MarketStatusAction::from(SymbolStatus::from(symbol.status)),
2565                );
2566            }
2567        }
2568        Ok(statuses)
2569    }
2570
2571    /// Requests Nautilus instruments for all trading symbols.
2572    ///
2573    /// Fetches exchange info via SBE and parses each symbol into a CurrencyPair.
2574    /// Non-trading symbols are skipped with a debug log.
2575    ///
2576    /// # Errors
2577    ///
2578    /// Returns an error if the request fails or SBE decoding fails.
2579    pub async fn request_instruments(&self) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2580        self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default(), false)
2581            .await
2582    }
2583
2584    /// Requests configured Nautilus instruments with populated maker and taker fees.
2585    ///
2586    /// # Errors
2587    ///
2588    /// Returns an error if configuration, exchange info, or required parsing fails.
2589    pub async fn request_instruments_with_config(
2590        &self,
2591        config: &BinanceInstrumentProviderConfig,
2592        us: bool,
2593    ) -> BinanceSpotHttpResult<Vec<InstrumentAny>> {
2594        config
2595            .validate(BinanceProductType::Spot)
2596            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2597        let selector = BinanceInstrumentSelector::new(config)
2598            .map_err(|e| BinanceSpotHttpError::ValidationError(e.to_string()))?;
2599        let ts_init = self.generate_ts_init();
2600        let fallback_fees = self.spot_fallback_fees(us).await;
2601
2602        let mut instruments = if us {
2603            if config.query_commission_rates {
2604                if config.log_warnings {
2605                    log::warn!(
2606                        "Binance US does not expose the Global account/commission endpoint; using account-wide commission rates"
2607                    );
2608                } else {
2609                    log::debug!(
2610                        "Binance US exact per-symbol commission query disabled; using account-wide rates"
2611                    );
2612                }
2613            }
2614            let info = self.inner.exchange_info_json().await?;
2615            let mut instruments = Vec::with_capacity(info.symbols.len());
2616            for symbol in &info.symbols {
2617                let instrument_id =
2618                    InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2619
2620                if !selector.includes(
2621                    instrument_id,
2622                    &symbol.symbol,
2623                    &symbol.base_asset,
2624                    &symbol.quote_asset,
2625                    None,
2626                ) {
2627                    continue;
2628                }
2629
2630                match parse_spot_instrument_json_with_fees(
2631                    symbol,
2632                    Some(fallback_fees.0),
2633                    Some(fallback_fees.1),
2634                    ts_init,
2635                    ts_init,
2636                ) {
2637                    Ok(instrument) => instruments.push(instrument),
2638                    Err(e) => log_instrument_parse_error(config, &symbol.symbol, &e),
2639                }
2640            }
2641            instruments
2642        } else {
2643            let info = self.exchange_info().await?;
2644            let mut instruments = Vec::with_capacity(info.symbols.len());
2645            for symbol in &info.symbols {
2646                let instrument_id =
2647                    InstrumentId::new(Symbol::from(symbol.symbol.as_str()), *BINANCE_VENUE);
2648
2649                if !selector.includes(
2650                    instrument_id,
2651                    &symbol.symbol,
2652                    &symbol.base_asset,
2653                    &symbol.quote_asset,
2654                    None,
2655                ) {
2656                    continue;
2657                }
2658
2659                let fees = self
2660                    .spot_symbol_fees(config, &symbol.symbol, fallback_fees)
2661                    .await;
2662
2663                match parse_spot_instrument_sbe_with_fees(
2664                    symbol,
2665                    Some(fees.0),
2666                    Some(fees.1),
2667                    ts_init,
2668                    ts_init,
2669                ) {
2670                    Ok(instrument) => instruments.push(instrument),
2671                    Err(e) => log_instrument_parse_error(config, &symbol.symbol, &e),
2672                }
2673            }
2674            instruments
2675        };
2676
2677        instruments.shrink_to_fit();
2678        self.replace_instruments(&instruments);
2679
2680        log::debug!("Loaded spot instruments: count={}", instruments.len());
2681        Ok(instruments)
2682    }
2683
2684    async fn spot_fallback_fees(&self, us: bool) -> (Decimal, Decimal) {
2685        if !self.has_credentials() {
2686            return (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT);
2687        }
2688
2689        let result = if us {
2690            self.inner.account_rates_json().await.map(|account| {
2691                parse_commission_rates(
2692                    &account.commission_rates.maker,
2693                    &account.commission_rates.taker,
2694                )
2695            })
2696        } else {
2697            self.inner
2698                .account(&AccountInfoParams::default())
2699                .await
2700                .map(|account| {
2701                    Ok((
2702                        decimal_from_mantissa_exponent(
2703                            account.maker_commission_mantissa,
2704                            account.commission_exponent,
2705                        ),
2706                        decimal_from_mantissa_exponent(
2707                            account.taker_commission_mantissa,
2708                            account.commission_exponent,
2709                        ),
2710                    ))
2711                })
2712        };
2713
2714        match result {
2715            Ok(Ok(fees)) => fees,
2716            Ok(Err(e)) => {
2717                log::warn!("Invalid Binance Spot account commission rates: {e}; using fallback");
2718                (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2719            }
2720            Err(e) => {
2721                log::warn!("Binance Spot account commission query failed: {e}; using fallback");
2722                (BINANCE_SPOT_FEE_DEFAULT, BINANCE_SPOT_FEE_DEFAULT)
2723            }
2724        }
2725    }
2726
2727    async fn spot_symbol_fees(
2728        &self,
2729        config: &BinanceInstrumentProviderConfig,
2730        symbol: &str,
2731        fallback: (Decimal, Decimal),
2732    ) -> (Decimal, Decimal) {
2733        if !config.query_commission_rates || !self.has_credentials() {
2734            return fallback;
2735        }
2736
2737        match self.inner.account_commission(symbol).await {
2738            Ok(response) => match parse_commission_rates(
2739                &response.standard_commission.maker,
2740                &response.standard_commission.taker,
2741            ) {
2742                Ok(fees) => fees,
2743                Err(e) => {
2744                    log::warn!(
2745                        "Invalid Binance Spot commission response for {symbol}: {e}; using fallback"
2746                    );
2747                    fallback
2748                }
2749            },
2750            Err(e) => {
2751                log::warn!(
2752                    "Binance Spot commission query failed for {symbol}: {e}; using fallback"
2753                );
2754                fallback
2755            }
2756        }
2757    }
2758
2759    /// Requests recent trades for an instrument.
2760    ///
2761    /// # Errors
2762    ///
2763    /// Returns an error if the request fails, the instrument is not cached,
2764    /// or trade parsing fails.
2765    pub async fn request_trades(
2766        &self,
2767        instrument_id: InstrumentId,
2768        limit: Option<u32>,
2769    ) -> anyhow::Result<Vec<TradeTick>> {
2770        let symbol = instrument_id.symbol.inner();
2771        let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2772        let ts_init = self.generate_ts_init();
2773
2774        let trades = self
2775            .inner
2776            .trades(symbol.as_str(), limit)
2777            .await
2778            .map_err(|e| anyhow::anyhow!(e))?;
2779
2780        parse_spot_trades_sbe(&trades, &instrument, ts_init)
2781    }
2782
2783    /// Requests bounded aggregate trades for an instrument.
2784    ///
2785    /// # Errors
2786    ///
2787    /// Returns an error if the request fails, the instrument is not cached, or parsing fails.
2788    pub async fn request_agg_trades(
2789        &self,
2790        instrument_id: InstrumentId,
2791        start: Option<Timestamp>,
2792        end: Option<Timestamp>,
2793        limit: Option<u32>,
2794    ) -> anyhow::Result<Vec<TradeTick>> {
2795        let symbol = instrument_id.symbol.inner();
2796        let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2797        let params = AggTradesParams {
2798            symbol: symbol.to_string(),
2799            from_id: None,
2800            start_time: start.map(|dt| dt.as_millisecond()),
2801            end_time: end.map(|dt| dt.as_millisecond()),
2802            limit,
2803        };
2804        let response = self.inner.agg_trades(&params).await?;
2805        let trades = BinanceTrades {
2806            price_exponent: response.price_exponent,
2807            qty_exponent: response.qty_exponent,
2808            trades: response
2809                .trades
2810                .into_iter()
2811                .map(|trade| super::models::BinanceTrade {
2812                    id: trade.id,
2813                    price_mantissa: trade.price_mantissa,
2814                    qty_mantissa: trade.qty_mantissa,
2815                    quote_qty_mantissa: 0,
2816                    time: trade.time,
2817                    is_buyer_maker: trade.is_buyer_maker,
2818                    is_best_match: trade.is_best_match,
2819                })
2820                .collect(),
2821        };
2822
2823        let mut parsed = parse_spot_trades_sbe(&trades, &instrument, UnixNanos::default())?;
2824        for trade in &mut parsed {
2825            trade.ts_init = trade.ts_event;
2826        }
2827        Ok(parsed)
2828    }
2829
2830    /// Requests bar (kline/candlestick) data.
2831    ///
2832    /// # Errors
2833    ///
2834    /// Returns an error if the bar type is not supported, instrument is not cached,
2835    /// or the request fails.
2836    pub async fn request_binance_bars(
2837        &self,
2838        bar_type: BarType,
2839        start: Option<Timestamp>,
2840        end: Option<Timestamp>,
2841        limit: Option<u32>,
2842    ) -> anyhow::Result<Vec<crate::common::bar::BinanceBar>> {
2843        anyhow::ensure!(
2844            bar_type.aggregation_source() == AggregationSource::External,
2845            "Only EXTERNAL aggregation is supported"
2846        );
2847
2848        let spec = bar_type.spec();
2849        let step = spec.step.get();
2850        let interval = match spec.aggregation {
2851            BarAggregation::Second if step == 1 => "1s".to_string(),
2852            BarAggregation::Second => {
2853                anyhow::bail!("Binance Spot supports only the 1s kline interval")
2854            }
2855            BarAggregation::Minute => format!("{step}m"),
2856            BarAggregation::Hour => format!("{step}h"),
2857            BarAggregation::Day => format!("{step}d"),
2858            BarAggregation::Week => format!("{step}w"),
2859            BarAggregation::Month => format!("{step}M"),
2860            a => anyhow::bail!("Binance does not support {a:?} aggregation"),
2861        };
2862
2863        let instrument_id = bar_type.instrument_id();
2864        let symbol = instrument_id.symbol;
2865        let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2866        let klines = self
2867            .inner
2868            .klines(
2869                symbol.as_str(),
2870                &interval,
2871                start.map(|dt| dt.as_millisecond()),
2872                end.map(|dt| dt.as_millisecond()),
2873                limit,
2874            )
2875            .await
2876            .map_err(|e| anyhow::anyhow!(e))?;
2877
2878        let mut bars =
2879            parse_klines_to_binance_bars(&klines, bar_type, &instrument, UnixNanos::default())?;
2880        let now = self.clock.get_time_ns();
2881        bars.retain(|bar| bar.ts_event < now);
2882        for bar in &mut bars {
2883            bar.ts_init = bar.ts_event;
2884        }
2885        Ok(bars)
2886    }
2887
2888    /// Requests core bars for an instrument.
2889    ///
2890    /// # Errors
2891    ///
2892    /// Returns an error if the bar type is unsupported or the request fails.
2893    pub async fn request_bars(
2894        &self,
2895        bar_type: BarType,
2896        start: Option<Timestamp>,
2897        end: Option<Timestamp>,
2898        limit: Option<u32>,
2899    ) -> anyhow::Result<Vec<Bar>> {
2900        Ok(self
2901            .request_binance_bars(bar_type, start, end, limit)
2902            .await?
2903            .into_iter()
2904            .map(|bar| bar.bar())
2905            .collect())
2906    }
2907
2908    /// Requests an explicit L2 order-book snapshot.
2909    ///
2910    /// # Errors
2911    ///
2912    /// Returns an error for an invalid depth, missing instrument, request failure, or invalid level.
2913    pub async fn request_book_snapshot(
2914        &self,
2915        instrument_id: InstrumentId,
2916        depth: Option<u32>,
2917    ) -> anyhow::Result<OrderBook> {
2918        if depth.is_some_and(|value| value == 0 || value > 5000) {
2919            anyhow::bail!("Binance Spot order-book depth must be between 1 and 5000");
2920        }
2921        let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2922        let params = DepthParams {
2923            symbol: instrument_id.symbol.to_string(),
2924            limit: depth,
2925        };
2926        let snapshot = self.inner.depth(&params).await?;
2927        let ts_event = self.generate_ts_init();
2928        Self::parse_book_snapshot_response(instrument_id, &instrument, &snapshot, ts_event)
2929    }
2930
2931    fn parse_book_snapshot_response(
2932        instrument_id: InstrumentId,
2933        instrument: &InstrumentAny,
2934        snapshot: &BinanceDepth,
2935        ts_event: UnixNanos,
2936    ) -> anyhow::Result<OrderBook> {
2937        let sequence = u64::try_from(snapshot.last_update_id)
2938            .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
2939        let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
2940        let mut add_level = |level: &super::models::BinancePriceLevel,
2941                             side: OrderSide,
2942                             order_id: usize,
2943                             name: &str|
2944         -> anyhow::Result<()> {
2945            let price = Price::from_mantissa_exponent_checked(
2946                level.price_mantissa,
2947                snapshot.price_exponent,
2948                instrument.price_precision(),
2949            )
2950            .map_err(|e| anyhow::anyhow!("invalid {name} price: {e}"))?;
2951            anyhow::ensure!(price.is_positive(), "invalid non-positive {name} price");
2952            let qty_mantissa = u64::try_from(level.qty_mantissa)
2953                .map_err(|_| anyhow::anyhow!("invalid negative {name} quantity"))?;
2954            let quantity = Quantity::from_mantissa_exponent_checked(
2955                qty_mantissa,
2956                snapshot.qty_exponent,
2957                instrument.size_precision(),
2958            )
2959            .map_err(|e| anyhow::anyhow!("invalid {name} quantity: {e}"))?;
2960            anyhow::ensure!(
2961                quantity.is_positive(),
2962                "invalid non-positive {name} quantity"
2963            );
2964            let order = BookOrder::new(
2965                side,
2966                price,
2967                quantity,
2968                u64::try_from(order_id)
2969                    .map_err(|_| anyhow::anyhow!("order-book level index overflow"))?,
2970            );
2971            book.add(order, 0, sequence, ts_event);
2972            Ok(())
2973        };
2974
2975        for (index, level) in snapshot.bids.iter().enumerate() {
2976            add_level(level, OrderSide::Buy, index, "bid")?;
2977        }
2978        let bid_count = snapshot.bids.len();
2979        for (index, level) in snapshot.asks.iter().enumerate() {
2980            let order_id = bid_count
2981                .checked_add(index)
2982                .ok_or_else(|| anyhow::anyhow!("order-book level index overflow"))?;
2983            add_level(level, OrderSide::Sell, order_id, "ask")?;
2984        }
2985        Ok(book)
2986    }
2987
2988    fn instrument_from_cache_by_id(
2989        &self,
2990        instrument_id: InstrumentId,
2991    ) -> anyhow::Result<InstrumentAny> {
2992        self.instruments_cache
2993            .get_cloned(&instrument_id.symbol.inner())
2994            .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
2995    }
2996
2997    /// Requests the account state with Nautilus types.
2998    ///
2999    /// # Errors
3000    ///
3001    /// Returns an error if the request fails or SBE decoding fails.
3002    pub async fn request_account_state(
3003        &self,
3004        account_id: AccountId,
3005    ) -> anyhow::Result<AccountState> {
3006        let ts_init = self.clock.get_time_ns();
3007        let params = AccountInfoParams::default();
3008        let account_info = self.inner.account(&params).await?;
3009        Ok(account_info.to_account_state(account_id, ts_init))
3010    }
3011
3012    /// Requests the status of a specific order.
3013    ///
3014    /// Either `venue_order_id` or `client_order_id` must be provided.
3015    ///
3016    /// # Errors
3017    ///
3018    /// Returns an error if neither identifier is provided, the request fails for any
3019    /// reason other than a missing order, instrument is not cached, or parsing fails.
3020    pub async fn request_order_status_report(
3021        &self,
3022        account_id: AccountId,
3023        instrument_id: InstrumentId,
3024        venue_order_id: Option<VenueOrderId>,
3025        client_order_id: Option<ClientOrderId>,
3026    ) -> anyhow::Result<Option<OrderStatusReport>> {
3027        anyhow::ensure!(
3028            venue_order_id.is_some() || client_order_id.is_some(),
3029            "Either venue_order_id or client_order_id must be provided"
3030        );
3031
3032        let symbol = instrument_id.symbol.inner();
3033        let instrument = self.instrument_from_cache(symbol)?;
3034        let ts_init = self.generate_ts_init();
3035
3036        let order_id = venue_order_id
3037            .map(|id| id.inner().parse::<i64>())
3038            .transpose()
3039            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3040
3041        let client_id_str =
3042            client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3043
3044        let order = match self
3045            .inner
3046            .query_order(symbol.as_str(), order_id, client_id_str.as_deref())
3047            .await
3048        {
3049            Ok(order) => order,
3050            Err(e) if Self::is_no_such_order_error(&e) => {
3051                log::debug!("Binance Spot order not found: instrument_id={instrument_id}");
3052                return Ok(None);
3053            }
3054            Err(e) => anyhow::bail!(e),
3055        };
3056
3057        parse_order_status_report_sbe(
3058            &order,
3059            account_id,
3060            &instrument,
3061            BINANCE_NAUTILUS_SPOT_BROKER_ID,
3062            ts_init,
3063        )
3064        .map(Some)
3065    }
3066
3067    const fn is_no_such_order_error(error: &BinanceSpotHttpError) -> bool {
3068        matches!(
3069            error,
3070            BinanceSpotHttpError::BinanceError { code, .. } if *code == BINANCE_NO_SUCH_ORDER_CODE
3071        )
3072    }
3073
3074    /// Requests order status reports.
3075    ///
3076    /// When `open_only` is true, returns only open orders (instrument_id optional).
3077    /// When `open_only` is false, returns order history (instrument_id required).
3078    ///
3079    /// # Errors
3080    ///
3081    /// Returns an error if the request fails, any order's instrument is not cached,
3082    /// or parsing fails.
3083    pub async fn request_order_status_reports(
3084        &self,
3085        account_id: AccountId,
3086        instrument_id: Option<InstrumentId>,
3087        start: Option<Timestamp>,
3088        end: Option<Timestamp>,
3089        open_only: bool,
3090        limit: Option<u32>,
3091    ) -> anyhow::Result<Vec<OrderStatusReport>> {
3092        let ts_init = self.generate_ts_init();
3093        let symbol = instrument_id.map(|id| id.symbol.to_string());
3094
3095        let orders = if open_only {
3096            self.inner
3097                .open_orders(symbol.as_deref())
3098                .await
3099                .map_err(|e| anyhow::anyhow!(e))?
3100        } else {
3101            let symbol = symbol
3102                .ok_or_else(|| anyhow::anyhow!("instrument_id is required when open_only=false"))?;
3103            self.inner
3104                .all_orders(
3105                    &symbol,
3106                    start.map(|dt| dt.as_millisecond()),
3107                    end.map(|dt| dt.as_millisecond()),
3108                    limit,
3109                )
3110                .await
3111                .map_err(|e| anyhow::anyhow!(e))?
3112        };
3113
3114        orders
3115            .iter()
3116            .map(|order| {
3117                let symbol = Ustr::from(&order.symbol);
3118                let instrument = self.instrument_from_cache(symbol)?;
3119                parse_order_status_report_sbe(
3120                    order,
3121                    account_id,
3122                    &instrument,
3123                    BINANCE_NAUTILUS_SPOT_BROKER_ID,
3124                    ts_init,
3125                )
3126            })
3127            .collect()
3128    }
3129
3130    /// Requests fill reports (trade history) for an instrument.
3131    ///
3132    /// # Errors
3133    ///
3134    /// Returns an error if the request fails, any trade's instrument is not cached,
3135    /// or parsing fails.
3136    pub async fn request_fill_reports(
3137        &self,
3138        account_id: AccountId,
3139        instrument_id: InstrumentId,
3140        venue_order_id: Option<VenueOrderId>,
3141        start: Option<Timestamp>,
3142        end: Option<Timestamp>,
3143        limit: Option<u32>,
3144    ) -> anyhow::Result<Vec<FillReport>> {
3145        self.request_fill_reports_with_cursor(
3146            account_id,
3147            instrument_id,
3148            venue_order_id,
3149            start,
3150            end,
3151            None,
3152            limit,
3153        )
3154        .await
3155    }
3156
3157    #[expect(clippy::too_many_arguments)]
3158    pub(crate) async fn request_fill_reports_with_cursor(
3159        &self,
3160        account_id: AccountId,
3161        instrument_id: InstrumentId,
3162        venue_order_id: Option<VenueOrderId>,
3163        start: Option<Timestamp>,
3164        end: Option<Timestamp>,
3165        from_id: Option<i64>,
3166        limit: Option<u32>,
3167    ) -> anyhow::Result<Vec<FillReport>> {
3168        let ts_init = self.generate_ts_init();
3169        let symbol = instrument_id.symbol.inner();
3170
3171        let order_id = venue_order_id
3172            .map(|id| id.inner().parse::<i64>())
3173            .transpose()
3174            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3175
3176        let trades = self
3177            .inner
3178            .account_trades_with_cursor(
3179                symbol.as_str(),
3180                order_id,
3181                start.map(|dt| dt.as_millisecond()),
3182                end.map(|dt| dt.as_millisecond()),
3183                from_id,
3184                limit,
3185            )
3186            .await
3187            .map_err(|e| anyhow::anyhow!(e))?;
3188
3189        trades
3190            .iter()
3191            .map(|trade| {
3192                let symbol = Ustr::from(&trade.symbol);
3193                let instrument = self.instrument_from_cache(symbol)?;
3194                let commission_currency = get_currency(&trade.commission_asset);
3195                parse_fill_report_sbe(trade, account_id, &instrument, commission_currency, ts_init)
3196            })
3197            .collect()
3198    }
3199
3200    /// Submits a new order to the venue.
3201    ///
3202    /// Converts Nautilus domain types to Binance-specific parameters
3203    /// and returns an `OrderStatusReport`.
3204    ///
3205    /// # Errors
3206    ///
3207    /// Returns an error if:
3208    /// - The instrument is not cached.
3209    /// - The order type or time-in-force is unsupported.
3210    /// - Stop orders are submitted without a trigger price.
3211    /// - The request fails or SBE decoding fails.
3212    #[expect(clippy::too_many_arguments)]
3213    pub async fn submit_order(
3214        &self,
3215        account_id: AccountId,
3216        instrument_id: InstrumentId,
3217        client_order_id: ClientOrderId,
3218        order_side: OrderSide,
3219        order_type: OrderType,
3220        quantity: Quantity,
3221        time_in_force: TimeInForce,
3222        price: Option<Price>,
3223        trigger_price: Option<Price>,
3224        post_only: bool,
3225        quote_quantity: bool,
3226        display_qty: Option<Quantity>,
3227        use_gtd: bool,
3228    ) -> anyhow::Result<OrderStatusReport> {
3229        let symbol = instrument_id.symbol.inner();
3230        let instrument = self
3231            .instrument_from_cache(symbol)
3232            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3233        let ts_init = self.generate_ts_init();
3234
3235        let binance_side = BinanceSide::try_from(order_side)
3236            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3237        let binance_order_type = order_type_to_binance_spot(order_type, post_only)
3238            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3239
3240        // Validate trigger price for conditional orders
3241        let requires_trigger = matches!(
3242            order_type,
3243            OrderType::StopMarket
3244                | OrderType::StopLimit
3245                | OrderType::MarketIfTouched
3246                | OrderType::LimitIfTouched
3247        );
3248
3249        if requires_trigger && trigger_price.is_none() {
3250            return Err(Self::command_validation_error(
3251                "Conditional orders require a trigger price",
3252            ));
3253        }
3254
3255        // Validate price for order types that require it
3256        let requires_price = matches!(
3257            binance_order_type,
3258            BinanceSpotOrderType::Limit
3259                | BinanceSpotOrderType::StopLossLimit
3260                | BinanceSpotOrderType::TakeProfitLimit
3261                | BinanceSpotOrderType::LimitMaker
3262        );
3263
3264        if requires_price && price.is_none() {
3265            return Err(Self::command_validation_error(format!(
3266                "{binance_order_type:?} orders require a price"
3267            )));
3268        }
3269
3270        // Only send TIF for order types that support it
3271        let supports_tif = matches!(
3272            binance_order_type,
3273            BinanceSpotOrderType::Limit
3274                | BinanceSpotOrderType::StopLossLimit
3275                | BinanceSpotOrderType::TakeProfitLimit
3276        );
3277        let binance_tif = if supports_tif {
3278            Some(
3279                time_in_force_to_binance_spot(time_in_force, use_gtd)
3280                    .map_err(|e| Self::command_validation_error(e.to_string()))?,
3281            )
3282        } else {
3283            None
3284        };
3285
3286        let qty_str = quantity.to_string();
3287        let price_str = price.map(|p| p.to_string());
3288        let stop_price_str = trigger_price.map(|p| p.to_string());
3289        let iceberg_qty_str = display_qty.map(|q| q.to_string());
3290        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3291
3292        if quote_quantity && binance_order_type != BinanceSpotOrderType::Market {
3293            return Err(Self::command_validation_error(
3294                "quoteOrderQty is only supported for MARKET orders",
3295            ));
3296        }
3297
3298        let (base_qty, quote_qty) = if quote_quantity {
3299            (None, Some(qty_str.as_str()))
3300        } else {
3301            (Some(qty_str.as_str()), None)
3302        };
3303
3304        let response = self
3305            .inner
3306            .new_order_full(
3307                symbol.as_str(),
3308                binance_side,
3309                binance_order_type,
3310                binance_tif,
3311                base_qty,
3312                quote_qty,
3313                price_str.as_deref(),
3314                Some(&client_id_str),
3315                stop_price_str.as_deref(),
3316                iceberg_qty_str.as_deref(),
3317            )
3318            .await?;
3319
3320        parse_new_order_response_sbe(
3321            &response,
3322            account_id,
3323            &instrument,
3324            BINANCE_NAUTILUS_SPOT_BROKER_ID,
3325            ts_init,
3326        )
3327        .map_err(|e| Self::response_parse_error(e.to_string()))
3328    }
3329
3330    /// Submits multiple orders in a single batch request.
3331    ///
3332    /// Binance limits batch submit to 5 orders maximum.
3333    ///
3334    /// # Errors
3335    ///
3336    /// Returns an error if the request fails or JSON parsing fails.
3337    pub async fn submit_order_list(
3338        &self,
3339        orders: &[BatchOrderItem],
3340    ) -> BinanceSpotHttpResult<Vec<BatchOrderResult>> {
3341        self.inner.batch_submit_orders(orders).await
3342    }
3343
3344    /// Submits a Spot OCO order list.
3345    ///
3346    /// # Errors
3347    ///
3348    /// Returns an error if the request fails or JSON parsing fails.
3349    pub async fn submit_oco_order_list(
3350        &self,
3351        params: &NewOcoOrderListParams,
3352    ) -> BinanceSpotHttpResult<NewOcoOrderListResponse> {
3353        self.inner.new_oco_order_list(params).await
3354    }
3355
3356    /// Modifies an existing order (cancel and replace atomically).
3357    ///
3358    /// # Errors
3359    ///
3360    /// Returns an error if:
3361    /// - The instrument is not cached.
3362    /// - The order type or time-in-force is unsupported.
3363    /// - The request fails or SBE decoding fails.
3364    #[expect(clippy::too_many_arguments)]
3365    pub async fn modify_order(
3366        &self,
3367        account_id: AccountId,
3368        instrument_id: InstrumentId,
3369        venue_order_id: VenueOrderId,
3370        client_order_id: ClientOrderId,
3371        order_side: OrderSide,
3372        order_type: OrderType,
3373        quantity: Quantity,
3374        time_in_force: TimeInForce,
3375        price: Option<Price>,
3376        use_gtd: bool,
3377    ) -> anyhow::Result<OrderStatusReport> {
3378        let symbol = instrument_id.symbol.inner();
3379        let instrument = self
3380            .instrument_from_cache(symbol)
3381            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3382        let ts_init = self.generate_ts_init();
3383
3384        let binance_side = BinanceSide::try_from(order_side)
3385            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3386        let binance_order_type = order_type_to_binance_spot(order_type, false)
3387            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3388        let binance_tif = time_in_force_to_binance_spot(time_in_force, use_gtd)
3389            .map_err(|e| Self::command_validation_error(e.to_string()))?;
3390
3391        let cancel_order_id: i64 = venue_order_id.inner().parse().map_err(|_| {
3392            Self::command_validation_error(format!("Invalid venue order ID: {venue_order_id}"))
3393        })?;
3394
3395        let qty_str = quantity.to_string();
3396        let price_str = price.map(|p| p.to_string());
3397        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_SPOT_BROKER_ID);
3398
3399        let response = self
3400            .inner
3401            .cancel_replace_order(
3402                symbol.as_str(),
3403                binance_side,
3404                binance_order_type,
3405                Some(binance_tif),
3406                Some(&qty_str),
3407                price_str.as_deref(),
3408                Some(cancel_order_id),
3409                None,
3410                Some(&client_id_str),
3411            )
3412            .await
3413            .map_err(|e| anyhow::anyhow!(e))?;
3414
3415        parse_new_order_response_sbe(
3416            &response,
3417            account_id,
3418            &instrument,
3419            BINANCE_NAUTILUS_SPOT_BROKER_ID,
3420            ts_init,
3421        )
3422        .map_err(|e| Self::response_parse_error(e.to_string()))
3423    }
3424
3425    /// Cancels an existing order on the venue.
3426    ///
3427    /// Either `venue_order_id` or `client_order_id` must be provided.
3428    ///
3429    /// # Errors
3430    ///
3431    /// Returns an error if the request fails or SBE decoding fails.
3432    pub async fn cancel_order(
3433        &self,
3434        instrument_id: InstrumentId,
3435        venue_order_id: Option<VenueOrderId>,
3436        client_order_id: Option<ClientOrderId>,
3437    ) -> anyhow::Result<VenueOrderId> {
3438        let symbol = instrument_id.symbol.inner();
3439
3440        let order_id = match venue_order_id {
3441            Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
3442                Ok(order_id) => Some(order_id),
3443                Err(e) if client_order_id.is_some() => {
3444                    log::warn!(
3445                        "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
3446                    );
3447                    None
3448                }
3449                Err(e) => {
3450                    return Err(Self::command_validation_error(format!(
3451                        "Invalid venue order ID: {e}"
3452                    )));
3453                }
3454            },
3455            None => None,
3456        };
3457
3458        let client_id_str =
3459            client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_SPOT_BROKER_ID));
3460
3461        let response = self
3462            .inner
3463            .cancel_order(symbol.as_str(), order_id, client_id_str.as_deref())
3464            .await
3465            .map_err(|e| anyhow::anyhow!(e))?;
3466
3467        Ok(VenueOrderId::new(response.order_id.to_string()))
3468    }
3469
3470    /// Cancels multiple orders in a single batch request.
3471    ///
3472    /// Binance limits batch cancel to 5 orders maximum.
3473    ///
3474    /// # Errors
3475    ///
3476    /// Returns an error if the request fails or JSON parsing fails.
3477    pub async fn batch_cancel_orders(
3478        &self,
3479        cancels: &[BatchCancelItem],
3480    ) -> BinanceSpotHttpResult<Vec<BatchCancelResult>> {
3481        self.inner.batch_cancel_orders(cancels).await
3482    }
3483
3484    /// Cancels all open orders for a symbol.
3485    ///
3486    /// Returns the venue order IDs of all canceled orders.
3487    ///
3488    /// # Errors
3489    ///
3490    /// Returns an error if the request fails or SBE decoding fails.
3491    pub async fn cancel_all_orders(
3492        &self,
3493        instrument_id: InstrumentId,
3494    ) -> anyhow::Result<Vec<(VenueOrderId, ClientOrderId)>> {
3495        let symbol = instrument_id.symbol.inner();
3496
3497        let responses = self
3498            .inner
3499            .cancel_open_orders(symbol.as_str())
3500            .await
3501            .map_err(|e| anyhow::anyhow!(e))?;
3502
3503        responses
3504            .into_iter()
3505            .map(|r| {
3506                Ok((
3507                    VenueOrderId::new(r.order_id.to_string()),
3508                    decode_client_order_id(
3509                        &r.orig_client_order_id,
3510                        BINANCE_NAUTILUS_SPOT_BROKER_ID,
3511                    )?,
3512                ))
3513            })
3514            .collect()
3515    }
3516}
3517
3518fn parse_commission_rates(maker: &str, taker: &str) -> anyhow::Result<(Decimal, Decimal)> {
3519    Ok((
3520        Decimal::from_str_exact(maker)?,
3521        Decimal::from_str_exact(taker)?,
3522    ))
3523}
3524
3525fn spot_json_market_status(status: &str) -> MarketStatusAction {
3526    match status {
3527        "TRADING" => MarketStatusAction::Trading,
3528        "BREAK" => MarketStatusAction::Pause,
3529        _ => MarketStatusAction::NotAvailableForTrading,
3530    }
3531}
3532
3533fn decimal_from_mantissa_exponent(mantissa: i64, exponent: i8) -> Decimal {
3534    if exponent >= 0 {
3535        Decimal::from(mantissa) * Decimal::from(10_i64.pow(exponent as u32))
3536    } else {
3537        Decimal::new(mantissa, (-exponent) as u32)
3538    }
3539}
3540
3541fn log_instrument_parse_error(
3542    config: &BinanceInstrumentProviderConfig,
3543    symbol: &str,
3544    error: &anyhow::Error,
3545) {
3546    if config.log_warnings {
3547        log::warn!("Skipping Binance Spot instrument {symbol}: {error}");
3548    } else {
3549        log::debug!("Skipping Binance Spot instrument {symbol}: {error}");
3550    }
3551}
3552
3553#[cfg(test)]
3554mod tests {
3555    use nautilus_model::instruments::stubs::currency_pair_btcusdt;
3556    use rstest::rstest;
3557
3558    use super::*;
3559    use crate::spot::http::models::BinancePriceLevel;
3560
3561    #[rstest]
3562    fn test_schema_constants() {
3563        assert_eq!(BinanceRawSpotHttpClient::schema_id(), 3);
3564        assert_eq!(BinanceRawSpotHttpClient::schema_version(), 5);
3565        assert_eq!(BinanceSpotHttpClient::schema_id(), 3);
3566        assert_eq!(BinanceSpotHttpClient::schema_version(), 5);
3567    }
3568
3569    #[rstest]
3570    fn test_sbe_schema_header() {
3571        assert_eq!(SBE_SCHEMA_HEADER, "3:5");
3572    }
3573
3574    #[rstest]
3575    fn test_parse_book_snapshot_response_rejects_negative_update_id() {
3576        let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3577        let snapshot = BinanceDepth {
3578            last_update_id: -1,
3579            price_exponent: -2,
3580            qty_exponent: -5,
3581            bids: vec![],
3582            asks: vec![],
3583        };
3584
3585        let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3586            InstrumentId::from("BTCUSDT.BINANCE"),
3587            &instrument,
3588            &snapshot,
3589            UnixNanos::from(1_700_000_000_000_000_001u64),
3590        )
3591        .unwrap_err();
3592
3593        assert_eq!(error.to_string(), "invalid negative order-book update ID");
3594    }
3595
3596    #[rstest]
3597    fn test_parse_book_snapshot_response_rejects_negative_quantity() {
3598        let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3599        let snapshot = BinanceDepth {
3600            last_update_id: 12345,
3601            price_exponent: -2,
3602            qty_exponent: -5,
3603            bids: vec![BinancePriceLevel {
3604                price_mantissa: 4_200_001,
3605                qty_mantissa: -1,
3606            }],
3607            asks: vec![],
3608        };
3609
3610        let error = BinanceSpotHttpClient::parse_book_snapshot_response(
3611            InstrumentId::from("BTCUSDT.BINANCE"),
3612            &instrument,
3613            &snapshot,
3614            UnixNanos::from(1_700_000_000_000_000_001u64),
3615        )
3616        .unwrap_err();
3617
3618        assert_eq!(error.to_string(), "invalid negative bid quantity");
3619    }
3620
3621    #[rstest]
3622    fn test_default_headers_include_sbe() {
3623        let headers = BinanceRawSpotHttpClient::default_headers(&None, false);
3624
3625        assert_eq!(headers.get("Accept"), Some(&"application/sbe".to_string()));
3626        assert_eq!(headers.get("X-MBX-SBE"), Some(&"3:5".to_string()));
3627    }
3628
3629    #[rstest]
3630    fn test_json_headers_exclude_sbe() {
3631        let headers = BinanceRawSpotHttpClient::default_headers(&None, true);
3632
3633        assert_eq!(headers.get("Accept"), Some(&"application/json".to_string()));
3634        assert_eq!(headers.get("X-MBX-SBE"), None);
3635    }
3636
3637    #[rstest]
3638    fn test_spot_trades_from_json_preserves_all_fields() {
3639        let response: Vec<SpotTradeJson> = serde_json::from_value(serde_json::json!([{
3640            "id": 17,
3641            "price": "123.45",
3642            "qty": "0.06789",
3643            "quoteQty": "8.3810205",
3644            "time": 1_700_000_000_123_i64,
3645            "isBuyerMaker": true,
3646            "isBestMatch": false
3647        }]))
3648        .unwrap();
3649
3650        let parsed = spot_trades_from_json(response).unwrap();
3651
3652        assert_eq!(parsed.price_exponent, -7);
3653        assert_eq!(parsed.qty_exponent, -5);
3654        assert_eq!(parsed.trades.len(), 1);
3655        assert_eq!(parsed.trades[0].id, 17);
3656        assert_eq!(parsed.trades[0].price_mantissa, 1_234_500_000);
3657        assert_eq!(parsed.trades[0].qty_mantissa, 6_789);
3658        assert_eq!(parsed.trades[0].quote_qty_mantissa, 83_810_205);
3659        assert_eq!(parsed.trades[0].time, 1_700_000_000_123_000);
3660        assert!(parsed.trades[0].is_buyer_maker);
3661        assert!(!parsed.trades[0].is_best_match);
3662    }
3663
3664    #[rstest]
3665    fn test_spot_agg_trades_from_json_preserves_all_fields() {
3666        let response: Vec<SpotAggTradeJson> = serde_json::from_value(serde_json::json!([{
3667            "a": 21,
3668            "p": "432.10",
3669            "q": "1.234",
3670            "f": 31,
3671            "l": 32,
3672            "T": 1_700_000_000_456_i64,
3673            "m": false,
3674            "M": true
3675        }]))
3676        .unwrap();
3677
3678        let parsed = spot_agg_trades_from_json(response).unwrap();
3679
3680        assert_eq!(parsed.price_exponent, -2);
3681        assert_eq!(parsed.qty_exponent, -3);
3682        assert_eq!(parsed.trades.len(), 1);
3683        assert_eq!(parsed.trades[0].id, 21);
3684        assert_eq!(parsed.trades[0].price_mantissa, 43_210);
3685        assert_eq!(parsed.trades[0].qty_mantissa, 1_234);
3686        assert_eq!(parsed.trades[0].first_trade_id, 31);
3687        assert_eq!(parsed.trades[0].last_trade_id, 32);
3688        assert_eq!(parsed.trades[0].time, 1_700_000_000_456_000);
3689        assert!(!parsed.trades[0].is_buyer_maker);
3690        assert!(parsed.trades[0].is_best_match);
3691    }
3692
3693    #[rstest]
3694    fn test_spot_klines_from_json_preserves_all_fields() {
3695        let response: Vec<SpotKlineJson> = serde_json::from_value(serde_json::json!([[
3696            1_700_000_000_000_i64,
3697            "10.10",
3698            "11.20",
3699            "9.30",
3700            "10.40",
3701            "12.345",
3702            1_700_000_059_999_i64,
3703            "128.765",
3704            37,
3705            "5.432",
3706            "56.789",
3707            "0"
3708        ]]))
3709        .unwrap();
3710
3711        let parsed = spot_klines_from_json(response).unwrap();
3712        let kline = &parsed.klines[0];
3713
3714        assert_eq!(parsed.price_exponent, -3);
3715        assert_eq!(parsed.qty_exponent, -3);
3716        assert_eq!(parsed.klines.len(), 1);
3717        assert_eq!(kline.open_time, 1_700_000_000_000_000);
3718        assert_eq!(kline.open_price, 10_100);
3719        assert_eq!(kline.high_price, 11_200);
3720        assert_eq!(kline.low_price, 9_300);
3721        assert_eq!(kline.close_price, 10_400);
3722        assert_eq!(i128::from_le_bytes(kline.volume), 12_345);
3723        assert_eq!(kline.close_time, 1_700_000_059_999_000);
3724        assert_eq!(i128::from_le_bytes(kline.quote_volume), 128_765);
3725        assert_eq!(kline.num_trades, 37);
3726        assert_eq!(i128::from_le_bytes(kline.taker_buy_base_volume), 5_432);
3727        assert_eq!(i128::from_le_bytes(kline.taker_buy_quote_volume), 56_789);
3728    }
3729
3730    #[rstest]
3731    fn test_spot_account_from_json_preserves_all_fields() {
3732        let response: SpotAccountJson = serde_json::from_value(serde_json::json!({
3733            "commissionRates": {
3734                "maker": "0.0008",
3735                "taker": "0.0011",
3736                "buyer": "0.0002",
3737                "seller": "0.0003"
3738            },
3739            "canTrade": true,
3740            "canWithdraw": false,
3741            "canDeposit": true,
3742            "requireSelfTradePrevention": true,
3743            "preventSor": false,
3744            "updateTime": 1_700_000_000_789_i64,
3745            "accountType": "SPOT",
3746            "balances": [{"asset": "USD", "free": "123.45", "locked": "6.789"}]
3747        }))
3748        .unwrap();
3749
3750        let account = spot_account_from_json(response).unwrap();
3751
3752        assert_eq!(account.commission_exponent, -4);
3753        assert_eq!(account.maker_commission_mantissa, 8);
3754        assert_eq!(account.taker_commission_mantissa, 11);
3755        assert_eq!(account.buyer_commission_mantissa, 2);
3756        assert_eq!(account.seller_commission_mantissa, 3);
3757        assert!(account.can_trade);
3758        assert!(!account.can_withdraw);
3759        assert!(account.can_deposit);
3760        assert!(account.require_self_trade_prevention);
3761        assert!(!account.prevent_sor);
3762        assert_eq!(account.update_time, 1_700_000_000_789_000);
3763        assert_eq!(account.account_type, "SPOT");
3764        assert_eq!(account.balances.len(), 1);
3765        assert_eq!(account.balances[0].asset, "USD");
3766        assert_eq!(account.balances[0].free_mantissa, 123_450);
3767        assert_eq!(account.balances[0].locked_mantissa, 6_789);
3768        assert_eq!(account.balances[0].exponent, -3);
3769    }
3770
3771    #[rstest]
3772    fn test_spot_cancel_replace_json_uses_nested_new_order_response() {
3773        let response: SpotCancelReplaceJson = serde_json::from_value(serde_json::json!({
3774            "cancelResult": "SUCCESS",
3775            "newOrderResult": "SUCCESS",
3776            "cancelResponse": {},
3777            "newOrderResponse": {
3778                "symbol": "ETHUSD",
3779                "orderId": 101,
3780                "orderListId": -1,
3781                "clientOrderId": "new-order",
3782                "transactTime": 1_700_000_000_123_i64,
3783                "price": "12.34",
3784                "origQty": "5.678",
3785                "executedQty": "1.234",
3786                "cummulativeQuoteQty": "15.22756",
3787                "status": "PARTIALLY_FILLED",
3788                "timeInForce": "GTC",
3789                "type": "LIMIT",
3790                "side": "BUY",
3791                "stopPrice": "11.11",
3792                "workingTime": 1_700_000_000_124_i64,
3793                "selfTradePreventionMode": "EXPIRE_MAKER",
3794                "fills": [{
3795                    "price": "12.34",
3796                    "qty": "1.234",
3797                    "commission": "0.001234",
3798                    "commissionAsset": "USD",
3799                    "tradeId": 44
3800                }]
3801            }
3802        }))
3803        .unwrap();
3804
3805        let order = spot_new_order_from_json(response.new_order_response).unwrap();
3806
3807        assert_eq!(order.price_exponent, -2);
3808        assert_eq!(order.qty_exponent, -3);
3809        assert_eq!(order.order_id, 101);
3810        assert_eq!(order.order_list_id, None);
3811        assert_eq!(order.transact_time, 1_700_000_000_123_000);
3812        assert_eq!(order.price_mantissa, 1_234);
3813        assert_eq!(order.orig_qty_mantissa, 5_678);
3814        assert_eq!(order.executed_qty_mantissa, 1_234);
3815        assert_eq!(order.cummulative_quote_qty_mantissa, 1_522_756);
3816        assert_eq!(order.status, SbeOrderStatus::PartiallyFilled);
3817        assert_eq!(order.time_in_force, SbeTimeInForce::Gtc);
3818        assert_eq!(order.order_type, SbeOrderType::Limit);
3819        assert_eq!(order.side, SbeOrderSide::Buy);
3820        assert_eq!(order.stop_price_mantissa, Some(1_111));
3821        assert_eq!(order.working_time, Some(1_700_000_000_124_000));
3822        assert_eq!(
3823            order.self_trade_prevention_mode,
3824            SbeSelfTradePreventionMode::ExpireMaker
3825        );
3826        assert_eq!(order.client_order_id, "new-order");
3827        assert_eq!(order.symbol, "ETHUSD");
3828        assert_eq!(order.fills.len(), 1);
3829        assert_eq!(order.fills[0].price_mantissa, 1_234);
3830        assert_eq!(order.fills[0].qty_mantissa, 1_234);
3831        assert_eq!(order.fills[0].commission_mantissa, 1_234);
3832        assert_eq!(order.fills[0].commission_exponent, -6);
3833        assert_eq!(order.fills[0].commission_asset, "USD");
3834        assert_eq!(order.fills[0].trade_id, Some(44));
3835        assert_eq!(order.expiry_reason, None);
3836    }
3837
3838    #[rstest]
3839    fn test_rate_limit_config() {
3840        let config = BinanceRawSpotHttpClient::rate_limit_config();
3841
3842        assert!(config.default_quota.is_some());
3843        // Spot has 2 ORDERS quotas (SECOND and DAY)
3844        assert_eq!(config.order_keys.len(), 2);
3845    }
3846
3847    #[rstest]
3848    fn test_quota_from_unknown_interval_returns_none() {
3849        let quota = BinanceRateLimitQuota {
3850            rate_limit_type: BinanceRateLimitType::Orders,
3851            interval: BinanceRateLimitInterval::Unknown,
3852            interval_num: 1,
3853            limit: 10,
3854        };
3855
3856        assert!(BinanceRawSpotHttpClient::quota_from(&quota).is_none());
3857    }
3858
3859    fn create_test_raw_client() -> BinanceRawSpotHttpClient {
3860        BinanceRawSpotHttpClient::new(
3861            BinanceEnvironment::Live,
3862            None,
3863            None,
3864            Some("http://127.0.0.1:1".to_string()),
3865            None,
3866            Some(1),
3867            None,
3868        )
3869        .unwrap()
3870    }
3871
3872    #[rstest]
3873    #[case::limit(
3874        AggTradesParams {
3875            symbol: "BTCUSDT".to_string(),
3876            from_id: None,
3877            start_time: None,
3878            end_time: None,
3879            limit: Some(1001),
3880        },
3881        "Validation error: aggregate trade limit must not exceed 1000"
3882    )]
3883    #[case::bounds(
3884        AggTradesParams {
3885            symbol: "BTCUSDT".to_string(),
3886            from_id: None,
3887            start_time: Some(2000),
3888            end_time: Some(1000),
3889            limit: Some(1000),
3890        },
3891        "Validation error: aggregate trade startTime must not exceed endTime"
3892    )]
3893    #[tokio::test]
3894    async fn test_agg_trades_rejects_invalid_bounds(
3895        #[case] params: AggTradesParams,
3896        #[case] expected: &str,
3897    ) {
3898        let error = create_test_raw_client()
3899            .agg_trades(&params)
3900            .await
3901            .unwrap_err();
3902
3903        assert_eq!(error.to_string(), expected);
3904    }
3905}