Skip to main content

nautilus_binance/spot/http/
models.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 response models.
17//!
18//! These models represent Binance venue-specific response types decoded from SBE.
19
20use std::fmt::Debug;
21
22use anyhow::Context;
23use nautilus_core::{UUID4, nanos::UnixNanos, string::secret::SecretString};
24use nautilus_model::{
25    enums::AccountType,
26    events::AccountState,
27    identifiers::AccountId,
28    types::{AccountBalance, Currency, Money},
29};
30use rust_decimal::Decimal;
31use zeroize::{Zeroize, ZeroizeOnDrop};
32
33use crate::{
34    common::{
35        enums::{
36            BinanceOrderStatus, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
37        },
38        parse::{account_balance_from_free_locked, parse_micros_or_init, sbe_mantissa_to_decimal},
39    },
40    spot::sbe::spot::{
41        contingency_type::ContingencyType, list_order_status::ListOrderStatus,
42        list_status_type::ListStatusType, order_side::OrderSide, order_status::OrderStatus,
43        order_type::OrderType, self_trade_prevention_mode::SelfTradePreventionMode,
44        time_in_force::TimeInForce,
45    },
46};
47
48/// Price/quantity level in an order book.
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct BinancePriceLevel {
51    /// Price mantissa (multiply by 10^exponent to get actual price).
52    pub price_mantissa: i64,
53    /// Quantity mantissa (multiply by 10^exponent to get actual quantity).
54    pub qty_mantissa: i64,
55}
56
57/// Binance order book depth response.
58#[derive(Debug, Clone, PartialEq)]
59pub struct BinanceDepth {
60    /// Last update ID for this depth snapshot.
61    pub last_update_id: i64,
62    /// Price exponent for all price levels.
63    pub price_exponent: i8,
64    /// Quantity exponent for all quantity values.
65    pub qty_exponent: i8,
66    /// Bid price levels (best bid first).
67    pub bids: Vec<BinancePriceLevel>,
68    /// Ask price levels (best ask first).
69    pub asks: Vec<BinancePriceLevel>,
70}
71
72/// A single trade from Binance.
73#[derive(Debug, Clone, PartialEq)]
74pub struct BinanceTrade {
75    /// Trade ID.
76    pub id: i64,
77    /// Price mantissa.
78    pub price_mantissa: i64,
79    /// Quantity mantissa.
80    pub qty_mantissa: i64,
81    /// Quote quantity mantissa (price * qty).
82    pub quote_qty_mantissa: i64,
83    /// Trade timestamp in microseconds (SBE precision).
84    pub time: i64,
85    /// Whether the buyer is the maker.
86    pub is_buyer_maker: bool,
87    /// Whether this trade is the best price match.
88    pub is_best_match: bool,
89}
90
91/// Binance trades response.
92#[derive(Debug, Clone, PartialEq)]
93pub struct BinanceTrades {
94    /// Price exponent for all trades.
95    pub price_exponent: i8,
96    /// Quantity exponent for all trades.
97    pub qty_exponent: i8,
98    /// List of trades.
99    pub trades: Vec<BinanceTrade>,
100}
101
102/// A single aggregate trade from Binance Spot.
103#[derive(Debug, Clone, PartialEq)]
104pub struct BinanceAggTrade {
105    /// Aggregate trade ID.
106    pub id: i64,
107    /// Price mantissa.
108    pub price_mantissa: i64,
109    /// Quantity mantissa.
110    pub qty_mantissa: i64,
111    /// First raw trade ID represented by this aggregate.
112    pub first_trade_id: i64,
113    /// Last raw trade ID represented by this aggregate.
114    pub last_trade_id: i64,
115    /// Trade timestamp in microseconds.
116    pub time: i64,
117    /// Whether the buyer is the maker.
118    pub is_buyer_maker: bool,
119    /// Whether this trade was the best price match.
120    pub is_best_match: bool,
121}
122
123/// Binance Spot aggregate trades response.
124#[derive(Debug, Clone, PartialEq)]
125pub struct BinanceAggTrades {
126    /// Price exponent for all trades.
127    pub price_exponent: i8,
128    /// Quantity exponent for all trades.
129    pub qty_exponent: i8,
130    /// Aggregate trades in chronological order.
131    pub trades: Vec<BinanceAggTrade>,
132}
133
134/// A fill from an order execution.
135#[derive(Debug, Clone, PartialEq)]
136pub struct BinanceOrderFill {
137    /// Fill price mantissa.
138    pub price_mantissa: i64,
139    /// Fill quantity mantissa.
140    pub qty_mantissa: i64,
141    /// Commission mantissa.
142    pub commission_mantissa: i64,
143    /// Commission exponent.
144    pub commission_exponent: i8,
145    /// Commission asset.
146    pub commission_asset: String,
147    /// Trade ID (if available).
148    pub trade_id: Option<i64>,
149}
150
151/// New order response (FULL response type).
152#[derive(Debug, Clone, PartialEq)]
153pub struct BinanceNewOrderResponse {
154    /// Price exponent for this response.
155    pub price_exponent: i8,
156    /// Quantity exponent for this response.
157    pub qty_exponent: i8,
158    /// Exchange order ID.
159    pub order_id: i64,
160    /// Order list ID (for OCO orders).
161    pub order_list_id: Option<i64>,
162    /// Transaction time in microseconds.
163    pub transact_time: i64,
164    /// Order price mantissa.
165    pub price_mantissa: i64,
166    /// Original order quantity mantissa.
167    pub orig_qty_mantissa: i64,
168    /// Executed quantity mantissa.
169    pub executed_qty_mantissa: i64,
170    /// Cumulative quote quantity mantissa.
171    pub cummulative_quote_qty_mantissa: i64,
172    /// Order status.
173    pub status: OrderStatus,
174    /// Time in force.
175    pub time_in_force: TimeInForce,
176    /// Order type.
177    pub order_type: OrderType,
178    /// Order side.
179    pub side: OrderSide,
180    /// Stop price mantissa (for stop orders).
181    pub stop_price_mantissa: Option<i64>,
182    /// Working time in microseconds.
183    pub working_time: Option<i64>,
184    /// Self-trade prevention mode.
185    pub self_trade_prevention_mode: SelfTradePreventionMode,
186    /// Client order ID.
187    pub client_order_id: String,
188    /// Symbol.
189    pub symbol: String,
190    /// Order fills.
191    pub fills: Vec<BinanceOrderFill>,
192    /// Expiry reason (schema 3:4; `None` when null/absent).
193    pub expiry_reason: Option<u8>,
194}
195
196/// Cancel order response.
197#[derive(Debug, Clone, PartialEq)]
198pub struct BinanceCancelOrderResponse {
199    /// Price exponent for this response.
200    pub price_exponent: i8,
201    /// Quantity exponent for this response.
202    pub qty_exponent: i8,
203    /// Exchange order ID.
204    pub order_id: i64,
205    /// Order list ID (for OCO orders).
206    pub order_list_id: Option<i64>,
207    /// Transaction time in microseconds.
208    pub transact_time: i64,
209    /// Order price mantissa.
210    pub price_mantissa: i64,
211    /// Original order quantity mantissa.
212    pub orig_qty_mantissa: i64,
213    /// Executed quantity mantissa.
214    pub executed_qty_mantissa: i64,
215    /// Cumulative quote quantity mantissa.
216    pub cummulative_quote_qty_mantissa: i64,
217    /// Order status.
218    pub status: OrderStatus,
219    /// Time in force.
220    pub time_in_force: TimeInForce,
221    /// Order type.
222    pub order_type: OrderType,
223    /// Order side.
224    pub side: OrderSide,
225    /// Self-trade prevention mode.
226    pub self_trade_prevention_mode: SelfTradePreventionMode,
227    /// Client order ID.
228    pub client_order_id: String,
229    /// Original client order ID.
230    pub orig_client_order_id: String,
231    /// Symbol.
232    pub symbol: String,
233}
234
235/// One order identity in a canceled order list.
236#[derive(Debug, Clone, PartialEq)]
237pub struct BinanceCancelOrderListOrder {
238    /// Trading pair symbol.
239    pub symbol: String,
240    /// Exchange order ID.
241    pub order_id: i64,
242    /// Original client order ID.
243    pub client_order_id: String,
244}
245
246/// Cancel order-list response.
247#[derive(Debug, Clone, PartialEq)]
248pub struct BinanceCancelOrderListResponse {
249    /// Exchange order-list ID.
250    pub order_list_id: i64,
251    /// Contingency type.
252    pub contingency_type: ContingencyType,
253    /// List status type.
254    pub list_status_type: ListStatusType,
255    /// Aggregate list order status.
256    pub list_order_status: ListOrderStatus,
257    /// Transaction time in microseconds.
258    pub transaction_time: i64,
259    /// Client order ID for the order list.
260    pub list_client_order_id: String,
261    /// Trading pair symbol.
262    pub symbol: String,
263    /// Orders in the list.
264    pub orders: Vec<BinanceCancelOrderListOrder>,
265    /// Canceled child order reports.
266    pub order_reports: Vec<BinanceCancelOrderResponse>,
267}
268
269/// One item returned by canceling all open orders.
270#[derive(Debug, Clone, PartialEq)]
271pub enum BinanceCancelOpenOrdersResponse {
272    /// An ordinary canceled order.
273    Order(BinanceCancelOrderResponse),
274    /// A canceled order list and its child reports.
275    OrderList(BinanceCancelOrderListResponse),
276}
277
278/// Query order response.
279#[derive(Debug, Clone, PartialEq)]
280pub struct BinanceOrderResponse {
281    /// Price exponent for this response.
282    pub price_exponent: i8,
283    /// Quantity exponent for this response.
284    pub qty_exponent: i8,
285    /// Exchange order ID.
286    pub order_id: i64,
287    /// Order list ID (for OCO orders).
288    pub order_list_id: Option<i64>,
289    /// Order price mantissa.
290    pub price_mantissa: i64,
291    /// Original order quantity mantissa.
292    pub orig_qty_mantissa: i64,
293    /// Executed quantity mantissa.
294    pub executed_qty_mantissa: i64,
295    /// Cumulative quote quantity mantissa.
296    pub cummulative_quote_qty_mantissa: i64,
297    /// Order status.
298    pub status: OrderStatus,
299    /// Time in force.
300    pub time_in_force: TimeInForce,
301    /// Order type.
302    pub order_type: OrderType,
303    /// Order side.
304    pub side: OrderSide,
305    /// Stop price mantissa (for stop orders).
306    pub stop_price_mantissa: Option<i64>,
307    /// Iceberg quantity mantissa.
308    pub iceberg_qty_mantissa: Option<i64>,
309    /// Order creation time in microseconds.
310    pub time: i64,
311    /// Last update time in microseconds.
312    pub update_time: i64,
313    /// Whether the order is working.
314    pub is_working: bool,
315    /// Working time in microseconds.
316    pub working_time: Option<i64>,
317    /// Original quote order quantity mantissa.
318    pub orig_quote_order_qty_mantissa: i64,
319    /// Self-trade prevention mode.
320    pub self_trade_prevention_mode: SelfTradePreventionMode,
321    /// Client order ID.
322    pub client_order_id: String,
323    /// Symbol.
324    pub symbol: String,
325    /// Expiry reason (schema 3:4; `None` when null/absent).
326    pub expiry_reason: Option<u8>,
327}
328
329/// Account balance for a single asset.
330#[derive(Debug, Clone, PartialEq)]
331pub struct BinanceBalance {
332    /// Asset symbol.
333    pub asset: String,
334    /// Free (available) balance mantissa.
335    pub free_mantissa: i64,
336    /// Locked balance mantissa.
337    pub locked_mantissa: i64,
338    /// Balance exponent.
339    pub exponent: i8,
340}
341
342/// Account information response.
343#[derive(Debug, Clone, PartialEq)]
344pub struct BinanceAccountInfo {
345    /// Commission exponent.
346    pub commission_exponent: i8,
347    /// Maker commission rate mantissa.
348    pub maker_commission_mantissa: i64,
349    /// Taker commission rate mantissa.
350    pub taker_commission_mantissa: i64,
351    /// Buyer commission rate mantissa.
352    pub buyer_commission_mantissa: i64,
353    /// Seller commission rate mantissa.
354    pub seller_commission_mantissa: i64,
355    /// Whether trading is enabled.
356    pub can_trade: bool,
357    /// Whether withdrawals are enabled.
358    pub can_withdraw: bool,
359    /// Whether deposits are enabled.
360    pub can_deposit: bool,
361    /// Whether the account requires self-trade prevention.
362    pub require_self_trade_prevention: bool,
363    /// Whether to prevent self-trade by quote order ID.
364    pub prevent_sor: bool,
365    /// Account update time in microseconds.
366    pub update_time: i64,
367    /// Account type.
368    pub account_type: String,
369    /// Account balances.
370    pub balances: Vec<BinanceBalance>,
371}
372
373impl BinanceAccountInfo {
374    /// Converts this Binance account info to a Nautilus [`AccountState`].
375    ///
376    /// An empty venue balance list still returns one zero USDT balance. A conversion error does
377    /// not use that placeholder.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if a balance mantissa and exponent cannot be converted to a decimal, if
382    /// free and locked overflow when added, or if an amount cannot be represented as money.
383    pub fn to_account_state(
384        &self,
385        account_id: AccountId,
386        ts_init: UnixNanos,
387    ) -> anyhow::Result<AccountState> {
388        let mut balances = Vec::with_capacity(self.balances.len());
389
390        for asset in &self.balances {
391            let free = sbe_mantissa_to_decimal(asset.free_mantissa, asset.exponent)
392                .with_context(|| format!("failed to convert free balance for {}", asset.asset))?;
393            let locked = sbe_mantissa_to_decimal(asset.locked_mantissa, asset.exponent)
394                .with_context(|| format!("failed to convert locked balance for {}", asset.asset))?;
395            let currency =
396                Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
397            let balance = account_balance_from_free_locked(&asset.asset, free, locked, currency)?;
398            balances.push(balance);
399        }
400
401        // Ensure at least one balance exists when the venue reported none.
402        if balances.is_empty() {
403            let zero_currency = Currency::USDT();
404            let zero_money = Money::zero(zero_currency);
405            let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
406            balances.push(zero_balance);
407        }
408
409        let ts_event =
410            parse_micros_or_init(self.update_time, "Spot SBE account update time", ts_init);
411
412        Ok(AccountState::new(
413            account_id,
414            AccountType::Cash,
415            balances,
416            vec![], // No margins for spot
417            true,   // is_reported
418            UUID4::new(),
419            ts_event,
420            ts_init,
421            None, // No base currency for spot
422        ))
423    }
424}
425
426/// Price filter from SBE response.
427#[derive(Debug, Clone, PartialEq)]
428pub struct BinancePriceFilterSbe {
429    /// Price exponent for mantissa conversion.
430    pub price_exponent: i8,
431    /// Minimum price mantissa.
432    pub min_price: i64,
433    /// Maximum price mantissa.
434    pub max_price: i64,
435    /// Tick size mantissa.
436    pub tick_size: i64,
437}
438
439/// Lot size filter from SBE response.
440#[derive(Debug, Clone, PartialEq)]
441pub struct BinanceLotSizeFilterSbe {
442    /// Quantity exponent for mantissa conversion.
443    pub qty_exponent: i8,
444    /// Minimum quantity mantissa.
445    pub min_qty: i64,
446    /// Maximum quantity mantissa.
447    pub max_qty: i64,
448    /// Step size mantissa.
449    pub step_size: i64,
450}
451
452/// Decoded Binance Spot notional filter.
453#[derive(Debug, Clone, PartialEq)]
454pub struct BinanceNotionalFilter {
455    /// Minimum quote-currency amount.
456    pub min: Decimal,
457    /// Maximum quote-currency amount, when specified.
458    pub max: Option<Decimal>,
459    /// Whether the minimum applies to market orders.
460    pub apply_min_to_market: bool,
461    /// Whether the maximum applies to market orders.
462    pub apply_max_to_market: bool,
463    /// Venue average-price window in minutes.
464    pub avg_price_mins: u32,
465}
466
467/// Symbol filters from SBE response.
468#[derive(Debug, Clone, Default, PartialEq)]
469pub struct BinanceSymbolFiltersSbe {
470    /// Price filter (required for trading).
471    pub price_filter: Option<BinancePriceFilterSbe>,
472    /// Lot size filter (required for trading).
473    pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
474    /// Exact notional rules and their market applicability.
475    pub notional_filters: Vec<BinanceNotionalFilter>,
476}
477
478/// Symbol information from SBE exchange info response.
479#[derive(Debug, Clone, PartialEq)]
480pub struct BinanceSymbolSbe {
481    /// Symbol name (e.g., "BTCUSDT").
482    pub symbol: String,
483    /// Base asset (e.g., "BTC").
484    pub base_asset: String,
485    /// Quote asset (e.g., "USDT").
486    pub quote_asset: String,
487    /// Base asset precision.
488    pub base_asset_precision: u8,
489    /// Quote asset precision.
490    pub quote_asset_precision: u8,
491    /// Symbol status.
492    pub status: u8,
493    /// Order types bitset.
494    pub order_types: u16,
495    /// Whether iceberg orders are allowed.
496    pub iceberg_allowed: bool,
497    /// Whether OCO orders are allowed.
498    pub oco_allowed: bool,
499    /// Whether OTO orders are allowed.
500    pub oto_allowed: bool,
501    /// Whether quote order quantity market orders are allowed.
502    pub quote_order_qty_market_allowed: bool,
503    /// Whether trailing stop is allowed.
504    pub allow_trailing_stop: bool,
505    /// Whether cancel-replace is allowed.
506    pub cancel_replace_allowed: bool,
507    /// Whether amend is allowed.
508    pub amend_allowed: bool,
509    /// Whether spot trading is allowed.
510    pub is_spot_trading_allowed: bool,
511    /// Whether margin trading is allowed.
512    pub is_margin_trading_allowed: bool,
513    /// Symbol filters decoded from SBE.
514    pub filters: BinanceSymbolFiltersSbe,
515    /// Permission sets.
516    pub permissions: Vec<Vec<String>>,
517}
518
519/// Exchange information from SBE response.
520#[derive(Debug, Clone, PartialEq)]
521pub struct BinanceExchangeInfoSbe {
522    /// List of symbols.
523    pub symbols: Vec<BinanceSymbolSbe>,
524}
525
526/// Exchange information returned as JSON by Binance US.
527#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
528pub struct BinanceExchangeInfoJson {
529    /// Symbol definitions.
530    pub symbols: Vec<BinanceSymbolJson>,
531}
532
533/// Spot symbol definition returned by JSON exchange info.
534#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct BinanceSymbolJson {
537    /// Raw venue symbol.
538    pub symbol: String,
539    /// Venue trading status.
540    pub status: String,
541    /// Base asset code.
542    pub base_asset: String,
543    /// Quote asset code.
544    pub quote_asset: String,
545    /// Base asset precision.
546    pub base_asset_precision: u8,
547    /// Quote asset precision.
548    pub quote_asset_precision: u8,
549    /// Symbol filters.
550    pub filters: Vec<BinanceSymbolFilterJson>,
551}
552
553/// Spot JSON symbol filter fields used for instrument construction.
554#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct BinanceSymbolFilterJson {
557    /// Venue filter type.
558    pub filter_type: String,
559    /// Minimum price.
560    pub min_price: Option<String>,
561    /// Maximum price.
562    pub max_price: Option<String>,
563    /// Tick size.
564    pub tick_size: Option<String>,
565    /// Minimum quantity.
566    pub min_qty: Option<String>,
567    /// Maximum quantity.
568    pub max_qty: Option<String>,
569    /// Quantity step size.
570    pub step_size: Option<String>,
571    /// Minimum quote notional.
572    pub min_notional: Option<String>,
573    /// Maximum quote notional.
574    pub max_notional: Option<String>,
575    /// Legacy minimum market applicability.
576    pub apply_to_market: Option<bool>,
577    /// Range minimum market applicability.
578    pub apply_min_to_market: Option<bool>,
579    /// Range maximum market applicability.
580    pub apply_max_to_market: Option<bool>,
581    /// Venue average-price window in minutes.
582    pub avg_price_mins: Option<u32>,
583}
584
585/// Account-specific Spot commission response.
586#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct BinanceAccountCommission {
589    /// Venue symbol.
590    pub symbol: String,
591    /// Standard maker and taker commission rates from the venue.
592    pub standard_commission: BinanceCommissionRates,
593}
594
595/// Maker and taker commission rates.
596#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
597pub struct BinanceCommissionRates {
598    /// Maker rate.
599    pub maker: String,
600    /// Taker rate.
601    pub taker: String,
602}
603
604/// Minimal JSON account response used for Binance US commission fallback.
605#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct BinanceAccountRatesJson {
608    /// Account-wide commission rates.
609    pub commission_rates: BinanceCommissionRates,
610}
611
612/// Account trade history entry.
613#[derive(Debug, Clone, PartialEq)]
614pub struct BinanceAccountTrade {
615    /// Price exponent.
616    pub price_exponent: i8,
617    /// Quantity exponent.
618    pub qty_exponent: i8,
619    /// Commission exponent.
620    pub commission_exponent: i8,
621    /// Trade ID.
622    pub id: i64,
623    /// Order ID.
624    pub order_id: i64,
625    /// Order list ID (for OCO).
626    pub order_list_id: Option<i64>,
627    /// Trade price mantissa.
628    pub price_mantissa: i64,
629    /// Trade quantity mantissa.
630    pub qty_mantissa: i64,
631    /// Quote quantity mantissa.
632    pub quote_qty_mantissa: i64,
633    /// Commission mantissa.
634    pub commission_mantissa: i64,
635    /// Trade time in microseconds.
636    pub time: i64,
637    /// Whether the trade was as buyer.
638    pub is_buyer: bool,
639    /// Whether the trade was as maker.
640    pub is_maker: bool,
641    /// Whether this is the best price match.
642    pub is_best_match: bool,
643    /// Symbol.
644    pub symbol: String,
645    /// Commission asset.
646    pub commission_asset: String,
647}
648
649/// Kline (candlestick) data response.
650#[derive(Debug, Clone, PartialEq)]
651pub struct BinanceKlines {
652    /// Price exponent for all klines.
653    pub price_exponent: i8,
654    /// Quantity exponent for all klines.
655    pub qty_exponent: i8,
656    /// List of klines.
657    pub klines: Vec<BinanceKline>,
658}
659
660/// Listen key response for user data stream.
661#[derive(Debug, Clone, PartialEq, serde::Deserialize, Zeroize, ZeroizeOnDrop)]
662#[serde(rename_all = "camelCase")]
663pub struct ListenKeyResponse {
664    /// The listen key for WebSocket user data stream.
665    pub listen_key: SecretString,
666}
667
668impl ListenKeyResponse {
669    /// Consumes the response and returns the listen key.
670    #[must_use]
671    pub fn into_listen_key(mut self) -> SecretString {
672        std::mem::take(&mut self.listen_key)
673    }
674}
675
676/// 24-hour ticker statistics response.
677#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct Ticker24hr {
680    /// Trading pair symbol.
681    pub symbol: String,
682    /// Price change in the last 24 hours.
683    pub price_change: String,
684    /// Price change percentage in the last 24 hours.
685    pub price_change_percent: String,
686    /// Weighted average price.
687    pub weighted_avg_price: String,
688    /// Previous close price.
689    pub prev_close_price: String,
690    /// Last price.
691    pub last_price: String,
692    /// Last quantity.
693    pub last_qty: String,
694    /// Best bid price.
695    pub bid_price: String,
696    /// Best bid quantity.
697    pub bid_qty: String,
698    /// Best ask price.
699    pub ask_price: String,
700    /// Best ask quantity.
701    pub ask_qty: String,
702    /// Open price.
703    pub open_price: String,
704    /// High price.
705    pub high_price: String,
706    /// Low price.
707    pub low_price: String,
708    /// Total traded base asset volume.
709    pub volume: String,
710    /// Total traded quote asset volume.
711    pub quote_volume: String,
712    /// Statistics open time in milliseconds.
713    pub open_time: i64,
714    /// Statistics close time in milliseconds.
715    pub close_time: i64,
716    /// First trade ID.
717    pub first_id: i64,
718    /// Last trade ID.
719    pub last_id: i64,
720    /// Number of trades.
721    pub count: i64,
722}
723
724/// Symbol price ticker response.
725#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
726pub struct TickerPrice {
727    /// Trading pair symbol.
728    pub symbol: String,
729    /// Latest price.
730    pub price: String,
731}
732
733/// Book ticker response (best bid/ask).
734#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
735#[serde(rename_all = "camelCase")]
736pub struct BookTicker {
737    /// Trading pair symbol.
738    pub symbol: String,
739    /// Best bid price.
740    pub bid_price: String,
741    /// Best bid quantity.
742    pub bid_qty: String,
743    /// Best ask price.
744    pub ask_price: String,
745    /// Best ask quantity.
746    pub ask_qty: String,
747}
748
749/// Average price response.
750#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
751pub struct AvgPrice {
752    /// Average price interval in minutes.
753    pub mins: i64,
754    /// Average price.
755    pub price: String,
756    /// Close time in milliseconds.
757    #[serde(rename = "closeTime")]
758    pub close_time: i64,
759}
760
761/// Trade fee information for a symbol.
762#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct TradeFee {
765    /// Trading pair symbol.
766    pub symbol: String,
767    /// Maker commission rate.
768    pub maker_commission: String,
769    /// Taker commission rate.
770    pub taker_commission: String,
771}
772
773/// Response from a new OCO order-list request.
774#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
775#[serde(rename_all = "camelCase")]
776pub struct NewOcoOrderListResponse {
777    /// Exchange order list ID.
778    pub order_list_id: i64,
779    /// Contingency type.
780    pub contingency_type: String,
781    /// List status type.
782    pub list_status_type: String,
783    /// List order status.
784    pub list_order_status: String,
785    /// Client order ID for the order list.
786    pub list_client_order_id: String,
787    /// Transaction time in milliseconds.
788    pub transaction_time: i64,
789    /// Trading pair symbol.
790    pub symbol: String,
791    /// Orders in the list.
792    pub orders: Vec<OrderListOrder>,
793}
794
795/// Order summary inside an order-list response.
796#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub struct OrderListOrder {
799    /// Trading pair symbol.
800    pub symbol: String,
801    /// Exchange order ID.
802    pub order_id: i64,
803    /// Client order ID.
804    pub client_order_id: String,
805}
806
807/// Result of a single order in a batch operation.
808///
809/// Each item in a batch response can be either a success or an error.
810#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
811#[serde(untagged)]
812pub enum BatchOrderResult {
813    /// Successful order placement.
814    Success(Box<BatchOrderSuccess>),
815    /// Failed order placement.
816    Error(BatchOrderError),
817}
818
819/// Successful order in a batch response.
820#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
821#[serde(rename_all = "camelCase")]
822pub struct BatchOrderSuccess {
823    /// Trading pair symbol.
824    pub symbol: String,
825    /// Exchange order ID.
826    pub order_id: i64,
827    /// Order list ID (for OCO orders).
828    #[serde(default)]
829    pub order_list_id: Option<i64>,
830    /// Client order ID.
831    pub client_order_id: String,
832    /// Transaction time in milliseconds.
833    pub transact_time: i64,
834    /// Order price.
835    pub price: String,
836    /// Original order quantity.
837    pub orig_qty: String,
838    /// Executed quantity.
839    pub executed_qty: String,
840    /// Cumulative quote quantity.
841    #[serde(rename = "cummulativeQuoteQty")]
842    pub cummulative_quote_qty: String,
843    /// Order status.
844    pub status: BinanceOrderStatus,
845    /// Time in force.
846    pub time_in_force: BinanceTimeInForce,
847    /// Order type.
848    #[serde(rename = "type")]
849    pub order_type: String,
850    /// Order side.
851    pub side: BinanceSide,
852    /// Working time in milliseconds.
853    #[serde(default)]
854    pub working_time: Option<i64>,
855    /// Self-trade prevention mode.
856    #[serde(default)]
857    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
858}
859
860/// Error in a batch order response.
861#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
862pub struct BatchOrderError {
863    /// Error code from Binance.
864    pub code: i64,
865    /// Error message.
866    pub msg: String,
867}
868
869/// Result of a single cancel in a batch cancel operation.
870#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
871#[serde(untagged)]
872pub enum BatchCancelResult {
873    /// Successful order cancellation.
874    Success(Box<BatchCancelSuccess>),
875    /// Failed order cancellation.
876    Error(BatchOrderError),
877}
878
879/// Successful cancel in a batch response.
880#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct BatchCancelSuccess {
883    /// Trading pair symbol.
884    pub symbol: String,
885    /// Original client order ID.
886    pub orig_client_order_id: String,
887    /// Exchange order ID.
888    pub order_id: i64,
889    /// Order list ID (for OCO orders).
890    #[serde(default)]
891    pub order_list_id: Option<i64>,
892    /// Client order ID.
893    pub client_order_id: String,
894    /// Transaction time in milliseconds.
895    #[serde(default)]
896    pub transact_time: Option<i64>,
897    /// Order price.
898    pub price: String,
899    /// Original order quantity.
900    pub orig_qty: String,
901    /// Executed quantity.
902    pub executed_qty: String,
903    /// Cumulative quote quantity.
904    #[serde(rename = "cummulativeQuoteQty")]
905    pub cummulative_quote_qty: String,
906    /// Order status.
907    pub status: BinanceOrderStatus,
908    /// Time in force.
909    pub time_in_force: BinanceTimeInForce,
910    /// Order type.
911    #[serde(rename = "type")]
912    pub order_type: String,
913    /// Order side.
914    pub side: BinanceSide,
915    /// Self-trade prevention mode.
916    #[serde(default)]
917    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
918}
919
920/// A single kline (candlestick) from Binance.
921#[derive(Debug, Clone, PartialEq)]
922pub struct BinanceKline {
923    /// Kline open time in microseconds.
924    pub open_time: i64,
925    /// Open price mantissa.
926    pub open_price: i64,
927    /// High price mantissa.
928    pub high_price: i64,
929    /// Low price mantissa.
930    pub low_price: i64,
931    /// Close price mantissa.
932    pub close_price: i64,
933    /// Volume (base asset) as 128-bit bytes.
934    pub volume: [u8; 16],
935    /// Kline close time in microseconds.
936    pub close_time: i64,
937    /// Quote volume as 128-bit bytes.
938    pub quote_volume: [u8; 16],
939    /// Number of trades.
940    pub num_trades: i64,
941    /// Taker buy base volume as 128-bit bytes.
942    pub taker_buy_base_volume: [u8; 16],
943    /// Taker buy quote volume as 128-bit bytes.
944    pub taker_buy_quote_volume: [u8; 16],
945}
946
947#[cfg(test)]
948mod tests {
949    use rstest::rstest;
950    use zeroize::Zeroize;
951
952    use super::*;
953    use crate::common::testing::load_fixture_string;
954
955    fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
956
957    #[rstest]
958    fn test_listen_key_response_deserialize() {
959        assert_zeroize_on_drop::<ListenKeyResponse>();
960
961        let json = r#"{"listenKey": "abc123xyz"}"#;
962        let mut response: ListenKeyResponse = serde_json::from_str(json).unwrap();
963
964        let debug = format!("{response:?}");
965        assert_eq!(response.listen_key.expose_secret(), "abc123xyz");
966        assert_eq!(debug, "ListenKeyResponse { listen_key: <redacted> }");
967        assert!(!debug.contains(response.listen_key.expose_secret()));
968
969        response.zeroize();
970        assert!(response.listen_key.expose_secret().is_empty());
971    }
972
973    #[rstest]
974    fn test_ticker_price_deserialize() {
975        let json = load_fixture_string("spot/http_json/ticker_price_response.json");
976        let response: TickerPrice = serde_json::from_str(&json).unwrap();
977        assert_eq!(response.symbol, "LTCBTC");
978        assert_eq!(response.price, "4.00000200");
979    }
980
981    #[rstest]
982    fn test_book_ticker_deserialize() {
983        let json = load_fixture_string("spot/http_json/book_ticker_response.json");
984        let response: BookTicker = serde_json::from_str(&json).unwrap();
985        assert_eq!(response.symbol, "LTCBTC");
986        assert_eq!(response.bid_price, "4.00000000");
987        assert_eq!(response.ask_price, "4.00000200");
988    }
989
990    #[rstest]
991    fn test_avg_price_deserialize() {
992        let json = load_fixture_string("spot/http_json/avg_price_response.json");
993        let response: AvgPrice = serde_json::from_str(&json).unwrap();
994        assert_eq!(response.mins, 5);
995        assert_eq!(response.price, "9.35751834");
996        assert_eq!(response.close_time, 1694061154503);
997    }
998
999    #[rstest]
1000    fn test_trade_fee_deserialize() {
1001        let json = r#"{
1002            "symbol": "BTCUSDT",
1003            "makerCommission": "0.001",
1004            "takerCommission": "0.001"
1005        }"#;
1006        let response: TradeFee = serde_json::from_str(json).unwrap();
1007        assert_eq!(response.symbol, "BTCUSDT");
1008        assert_eq!(response.maker_commission, "0.001");
1009        assert_eq!(response.taker_commission, "0.001");
1010    }
1011
1012    #[rstest]
1013    fn test_batch_order_result_success() {
1014        let json = load_fixture_string("spot/http_json/new_order_full_response.json");
1015        let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
1016        match result {
1017            BatchOrderResult::Success(order) => {
1018                assert_eq!(order.symbol, "BTCUSDT");
1019                assert_eq!(order.order_id, 28);
1020                assert_eq!(order.status, BinanceOrderStatus::Filled);
1021                assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
1022                assert_eq!(order.order_type, "MARKET");
1023                assert_eq!(order.side, BinanceSide::Sell);
1024                assert_eq!(
1025                    order.self_trade_prevention_mode,
1026                    Some(BinanceSelfTradePreventionMode::None)
1027                );
1028            }
1029            BatchOrderResult::Error(_) => panic!("Expected Success"),
1030        }
1031    }
1032
1033    #[rstest]
1034    fn test_batch_order_result_error() {
1035        let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
1036        let result: BatchOrderResult = serde_json::from_str(json).unwrap();
1037        match result {
1038            BatchOrderResult::Success(_) => panic!("Expected Error"),
1039            BatchOrderResult::Error(error) => {
1040                assert_eq!(error.code, -1013);
1041                assert_eq!(error.msg, "Invalid quantity.");
1042            }
1043        }
1044    }
1045
1046    #[rstest]
1047    fn test_batch_cancel_result_success() {
1048        let json = load_fixture_string("spot/http_json/cancel_order_response.json");
1049        let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
1050        match result {
1051            BatchCancelResult::Success(cancel) => {
1052                assert_eq!(cancel.symbol, "LTCBTC");
1053                assert_eq!(cancel.order_id, 4);
1054                assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
1055                assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
1056                assert_eq!(cancel.order_type, "LIMIT");
1057                assert_eq!(cancel.side, BinanceSide::Buy);
1058                assert_eq!(
1059                    cancel.self_trade_prevention_mode,
1060                    Some(BinanceSelfTradePreventionMode::None)
1061                );
1062            }
1063            BatchCancelResult::Error(_) => panic!("Expected Success"),
1064        }
1065    }
1066
1067    #[rstest]
1068    fn test_batch_cancel_result_error() {
1069        let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
1070        let result: BatchCancelResult = serde_json::from_str(json).unwrap();
1071        match result {
1072            BatchCancelResult::Success(_) => panic!("Expected Error"),
1073            BatchCancelResult::Error(error) => {
1074                assert_eq!(error.code, -2011);
1075                assert_eq!(error.msg, "Unknown order sent.");
1076            }
1077        }
1078    }
1079
1080    #[rstest]
1081    fn test_ticker_24hr_deserialize() {
1082        let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
1083        let response: Ticker24hr = serde_json::from_str(&json).unwrap();
1084        assert_eq!(response.symbol, "BNBBTC");
1085        assert_eq!(response.last_price, "4.00000200");
1086        assert_eq!(response.count, 76);
1087    }
1088
1089    fn account_info(balances: Vec<BinanceBalance>) -> BinanceAccountInfo {
1090        BinanceAccountInfo {
1091            commission_exponent: -8,
1092            maker_commission_mantissa: 10,
1093            taker_commission_mantissa: 10,
1094            buyer_commission_mantissa: 0,
1095            seller_commission_mantissa: 0,
1096            can_trade: true,
1097            can_withdraw: true,
1098            can_deposit: true,
1099            require_self_trade_prevention: false,
1100            prevent_sor: false,
1101            update_time: 1_700_000_000_000_000,
1102            account_type: "SPOT".to_string(),
1103            balances,
1104        }
1105    }
1106
1107    fn balance(asset: &str, exponent: i8, free: i64, locked: i64) -> BinanceBalance {
1108        BinanceBalance {
1109            asset: asset.to_string(),
1110            free_mantissa: free,
1111            locked_mantissa: locked,
1112            exponent,
1113        }
1114    }
1115
1116    #[rstest]
1117    fn test_account_info_to_account_state_keeps_exact_balance() {
1118        let info = account_info(vec![balance("BTC", -8, 100_000_000, 50_000_000)]);
1119
1120        let state = info
1121            .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1122            .unwrap();
1123
1124        assert_eq!(state.balances.len(), 1);
1125        assert_eq!(
1126            state.balances[0].total.as_decimal(),
1127            Decimal::from_str_exact("1.5").unwrap()
1128        );
1129        assert_eq!(
1130            state.balances[0].locked.as_decimal(),
1131            Decimal::from_str_exact("0.5").unwrap()
1132        );
1133    }
1134
1135    #[rstest]
1136    fn test_account_info_to_account_state_positive_exponent_is_exact() {
1137        let info = account_info(vec![balance("BTC", 2, 42, 7)]);
1138
1139        let state = info
1140            .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1141            .unwrap();
1142
1143        assert_eq!(state.balances[0].total.as_decimal(), Decimal::from(4_900));
1144        assert_eq!(state.balances[0].locked.as_decimal(), Decimal::from(700));
1145    }
1146
1147    #[rstest]
1148    #[case::scale_above_max(-29_i8, 1_i64)]
1149    #[case::min_exponent(i8::MIN, 1_i64)]
1150    #[case::positive_overflow(29_i8, 1_i64)]
1151    #[case::product_overflow(18_i8, i64::MAX)]
1152    fn test_account_info_to_account_state_rejects_unrepresentable_balance(
1153        #[case] exponent: i8,
1154        #[case] mantissa: i64,
1155    ) {
1156        let info = account_info(vec![
1157            balance("BTC", -8, 100_000_000, 0),
1158            balance("ETH", exponent, mantissa, mantissa),
1159        ]);
1160
1161        let err = info
1162            .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1163            .unwrap_err();
1164        let message = format!("{err:#}");
1165
1166        assert!(message.contains("ETH"), "{message}");
1167        assert!(message.contains(&exponent.to_string()), "{message}");
1168    }
1169
1170    #[rstest]
1171    #[case::free_overflow(i64::MAX, 1_i64)]
1172    #[case::locked_overflow(1_i64, i64::MAX)]
1173    fn test_account_info_to_account_state_rejects_one_sided_overflow(
1174        #[case] free: i64,
1175        #[case] locked: i64,
1176    ) {
1177        let info = account_info(vec![balance("ETH", 10, free, locked)]);
1178
1179        let err = info
1180            .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1181            .unwrap_err();
1182        let message = format!("{err:#}");
1183
1184        assert!(message.contains("ETH"), "{message}");
1185        assert!(message.contains("10"), "{message}");
1186    }
1187
1188    #[rstest]
1189    fn test_account_info_to_account_state_empty_list_uses_zero_usdt() {
1190        let info = account_info(vec![]);
1191
1192        let state = info
1193            .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1194            .unwrap();
1195
1196        assert_eq!(state.balances.len(), 1);
1197        assert_eq!(state.balances[0].currency, Currency::USDT());
1198        assert!(state.balances[0].total.is_zero());
1199    }
1200}