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