Skip to main content

nautilus_binance/futures/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 Futures HTTP response models.
17
18use anyhow::Context;
19use nautilus_core::{UUID4, UnixNanos, time::get_atomic_clock_realtime};
20use nautilus_model::{
21    enums::{AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
22    events::AccountState,
23    identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, Venue, VenueOrderId},
24    reports::{FillReport, OrderStatusReport},
25    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
26};
27use rust_decimal::Decimal;
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use ustr::Ustr;
31
32use crate::common::{
33    enums::{
34        BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
35        BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
36        BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
37        BinanceTradingStatus, BinanceWorkingType,
38    },
39    models::BinanceRateLimit,
40};
41
42/// Server time response from `GET /fapi/v1/time`.
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct BinanceServerTime {
46    /// Server timestamp in milliseconds.
47    pub server_time: i64,
48}
49
50/// Public trade from `GET /fapi/v1/trades`.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BinanceFuturesTrade {
54    /// Trade ID.
55    pub id: i64,
56    /// Trade price.
57    pub price: String,
58    /// Trade quantity.
59    pub qty: String,
60    /// Quote asset quantity.
61    pub quote_qty: String,
62    /// Trade timestamp in milliseconds.
63    pub time: i64,
64    /// Whether the buyer is the maker.
65    pub is_buyer_maker: bool,
66}
67
68/// Kline/candlestick data from `GET /fapi/v1/klines`.
69#[derive(Clone, Debug)]
70pub struct BinanceFuturesKline {
71    /// Open time in milliseconds.
72    pub open_time: i64,
73    /// Open price.
74    pub open: String,
75    /// High price.
76    pub high: String,
77    /// Low price.
78    pub low: String,
79    /// Close price.
80    pub close: String,
81    /// Volume.
82    pub volume: String,
83    /// Close time in milliseconds.
84    pub close_time: i64,
85    /// Quote asset volume.
86    pub quote_volume: String,
87    /// Number of trades.
88    pub num_trades: i64,
89    /// Taker buy base volume.
90    pub taker_buy_base_volume: String,
91    /// Taker buy quote volume.
92    pub taker_buy_quote_volume: String,
93}
94
95impl<'de> Deserialize<'de> for BinanceFuturesKline {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: serde::Deserializer<'de>,
99    {
100        let arr: Vec<Value> = Vec::deserialize(deserializer)?;
101        if arr.len() < 11 {
102            return Err(serde::de::Error::custom("Invalid kline array length"));
103        }
104
105        Ok(Self {
106            open_time: arr[0].as_i64().unwrap_or(0),
107            open: arr[1].as_str().unwrap_or("0").to_string(),
108            high: arr[2].as_str().unwrap_or("0").to_string(),
109            low: arr[3].as_str().unwrap_or("0").to_string(),
110            close: arr[4].as_str().unwrap_or("0").to_string(),
111            volume: arr[5].as_str().unwrap_or("0").to_string(),
112            close_time: arr[6].as_i64().unwrap_or(0),
113            quote_volume: arr[7].as_str().unwrap_or("0").to_string(),
114            num_trades: arr[8].as_i64().unwrap_or(0),
115            taker_buy_base_volume: arr[9].as_str().unwrap_or("0").to_string(),
116            taker_buy_quote_volume: arr[10].as_str().unwrap_or("0").to_string(),
117        })
118    }
119}
120
121/// USD-M Futures exchange information response from `GET /fapi/v1/exchangeInfo`.
122#[derive(Clone, Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct BinanceFuturesUsdExchangeInfo {
125    /// Server timezone.
126    pub timezone: String,
127    /// Server timestamp in milliseconds.
128    pub server_time: i64,
129    /// Rate limit definitions.
130    pub rate_limits: Vec<BinanceRateLimit>,
131    /// Exchange-level filters.
132    #[serde(default)]
133    pub exchange_filters: Vec<Value>,
134    /// Asset definitions.
135    #[serde(default)]
136    pub assets: Vec<BinanceFuturesAsset>,
137    /// Trading symbols.
138    pub symbols: Vec<BinanceFuturesUsdSymbol>,
139}
140
141/// Futures asset definition.
142#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct BinanceFuturesAsset {
145    /// Asset name.
146    pub asset: Ustr,
147    /// Whether margin is available.
148    pub margin_available: bool,
149    /// Auto asset exchange threshold.
150    #[serde(default)]
151    pub auto_asset_exchange: Option<String>,
152}
153
154/// USD-M Futures symbol definition.
155#[derive(Clone, Debug, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct BinanceFuturesUsdSymbol {
158    /// Symbol name (e.g., "BTCUSDT").
159    pub symbol: Ustr,
160    /// Trading pair (e.g., "BTCUSDT").
161    pub pair: Ustr,
162    /// Contract type (PERPETUAL, CURRENT_QUARTER, NEXT_QUARTER).
163    pub contract_type: String,
164    /// Delivery date timestamp.
165    pub delivery_date: i64,
166    /// Onboard date timestamp.
167    pub onboard_date: i64,
168    /// Trading status.
169    pub status: BinanceTradingStatus,
170    /// Maintenance margin percent.
171    pub maint_margin_percent: String,
172    /// Required margin percent.
173    pub required_margin_percent: String,
174    /// Base asset.
175    pub base_asset: Ustr,
176    /// Quote asset.
177    pub quote_asset: Ustr,
178    /// Margin asset.
179    pub margin_asset: Ustr,
180    /// Price precision.
181    pub price_precision: i32,
182    /// Quantity precision.
183    pub quantity_precision: i32,
184    /// Base asset precision.
185    pub base_asset_precision: i32,
186    /// Quote precision.
187    pub quote_precision: i32,
188    /// Underlying type.
189    #[serde(default)]
190    pub underlying_type: Option<String>,
191    /// Underlying sub type.
192    #[serde(default)]
193    pub underlying_sub_type: Vec<String>,
194    /// Settle plan.
195    #[serde(default)]
196    pub settle_plan: Option<i64>,
197    /// Trigger protect threshold.
198    #[serde(default)]
199    pub trigger_protect: Option<String>,
200    /// Liquidation fee.
201    #[serde(default)]
202    pub liquidation_fee: Option<String>,
203    /// Market take bound.
204    #[serde(default)]
205    pub market_take_bound: Option<String>,
206    /// Allowed order types.
207    pub order_types: Vec<String>,
208    /// Time in force options.
209    pub time_in_force: Vec<String>,
210    /// Symbol filters.
211    pub filters: Vec<Value>,
212}
213
214/// COIN-M Futures exchange information response from `GET /dapi/v1/exchangeInfo`.
215#[derive(Clone, Debug, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct BinanceFuturesCoinExchangeInfo {
218    /// Server timezone.
219    pub timezone: String,
220    /// Server timestamp in milliseconds.
221    pub server_time: i64,
222    /// Rate limit definitions.
223    pub rate_limits: Vec<BinanceRateLimit>,
224    /// Exchange-level filters.
225    #[serde(default)]
226    pub exchange_filters: Vec<Value>,
227    /// Trading symbols.
228    pub symbols: Vec<BinanceFuturesCoinSymbol>,
229}
230
231/// COIN-M Futures symbol definition.
232#[derive(Clone, Debug, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct BinanceFuturesCoinSymbol {
235    /// Symbol name (e.g., "BTCUSD_PERP").
236    pub symbol: Ustr,
237    /// Trading pair (e.g., "BTCUSD").
238    pub pair: Ustr,
239    /// Contract type (PERPETUAL, CURRENT_QUARTER, NEXT_QUARTER).
240    pub contract_type: String,
241    /// Delivery date timestamp.
242    pub delivery_date: i64,
243    /// Onboard date timestamp.
244    pub onboard_date: i64,
245    /// Trading status.
246    #[serde(default)]
247    pub contract_status: Option<BinanceContractStatus>,
248    /// Contract size.
249    pub contract_size: i64,
250    /// Maintenance margin percent.
251    pub maint_margin_percent: String,
252    /// Required margin percent.
253    pub required_margin_percent: String,
254    /// Base asset.
255    pub base_asset: Ustr,
256    /// Quote asset.
257    pub quote_asset: Ustr,
258    /// Margin asset.
259    pub margin_asset: Ustr,
260    /// Price precision.
261    pub price_precision: i32,
262    /// Quantity precision.
263    pub quantity_precision: i32,
264    /// Base asset precision.
265    pub base_asset_precision: i32,
266    /// Quote precision.
267    pub quote_precision: i32,
268    /// Equal quantity precision.
269    #[serde(default, rename = "equalQtyPrecision")]
270    pub equal_qty_precision: Option<i32>,
271    /// Trigger protect threshold.
272    #[serde(default)]
273    pub trigger_protect: Option<String>,
274    /// Liquidation fee.
275    #[serde(default)]
276    pub liquidation_fee: Option<String>,
277    /// Market take bound.
278    #[serde(default)]
279    pub market_take_bound: Option<String>,
280    /// Allowed order types.
281    pub order_types: Vec<String>,
282    /// Time in force options.
283    pub time_in_force: Vec<String>,
284    /// Symbol filters.
285    pub filters: Vec<Value>,
286}
287
288/// 24hr ticker price change statistics for futures.
289#[derive(Clone, Debug, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct BinanceFuturesTicker24hr {
292    /// Symbol name.
293    pub symbol: Ustr,
294    /// Price change in quote asset.
295    pub price_change: String,
296    /// Price change percentage.
297    pub price_change_percent: String,
298    /// Weighted average price.
299    pub weighted_avg_price: String,
300    /// Last traded price.
301    pub last_price: String,
302    /// Last traded quantity.
303    #[serde(default)]
304    pub last_qty: Option<String>,
305    /// Opening price.
306    pub open_price: String,
307    /// Highest price.
308    pub high_price: String,
309    /// Lowest price.
310    pub low_price: String,
311    /// Total traded base asset volume.
312    pub volume: String,
313    /// Total traded quote asset volume.
314    pub quote_volume: String,
315    /// Statistics open time.
316    pub open_time: i64,
317    /// Statistics close time.
318    pub close_time: i64,
319    /// First trade ID.
320    #[serde(default)]
321    pub first_id: Option<i64>,
322    /// Last trade ID.
323    #[serde(default)]
324    pub last_id: Option<i64>,
325    /// Total number of trades.
326    #[serde(default)]
327    pub count: Option<i64>,
328}
329
330/// Mark price and funding rate for futures.
331#[derive(Clone, Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct BinanceFuturesMarkPrice {
334    /// Symbol name.
335    pub symbol: Ustr,
336    /// Mark price.
337    pub mark_price: String,
338    /// Index price.
339    #[serde(default)]
340    pub index_price: Option<String>,
341    /// Estimated settle price (only for delivery contracts).
342    #[serde(default)]
343    pub estimated_settle_price: Option<String>,
344    /// Last funding rate.
345    #[serde(default)]
346    pub last_funding_rate: Option<String>,
347    /// Next funding time.
348    #[serde(default)]
349    pub next_funding_time: Option<i64>,
350    /// Interest rate.
351    #[serde(default)]
352    pub interest_rate: Option<String>,
353    /// Timestamp.
354    pub time: i64,
355}
356
357/// Order book depth snapshot.
358#[derive(Clone, Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct BinanceOrderBook {
361    /// Last update ID.
362    pub last_update_id: i64,
363    /// Bid levels as `[price, quantity]` arrays.
364    pub bids: Vec<(String, String)>,
365    /// Ask levels as `[price, quantity]` arrays.
366    pub asks: Vec<(String, String)>,
367    /// Message output time.
368    #[serde(default, rename = "E")]
369    pub event_time: Option<i64>,
370    /// Transaction time.
371    #[serde(default, rename = "T")]
372    pub transaction_time: Option<i64>,
373}
374
375/// Best bid/ask from book ticker endpoint.
376#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct BinanceBookTicker {
379    /// Symbol name.
380    pub symbol: Ustr,
381    /// Best bid price.
382    pub bid_price: String,
383    /// Best bid quantity.
384    pub bid_qty: String,
385    /// Best ask price.
386    pub ask_price: String,
387    /// Best ask quantity.
388    pub ask_qty: String,
389    /// Event time.
390    #[serde(default)]
391    pub time: Option<i64>,
392}
393
394/// Price ticker.
395#[derive(Clone, Debug, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct BinancePriceTicker {
398    /// Symbol name.
399    pub symbol: Ustr,
400    /// Current price.
401    pub price: String,
402    /// Event time.
403    #[serde(default)]
404    pub time: Option<i64>,
405}
406
407/// Funding rate history record.
408#[derive(Clone, Debug, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct BinanceFundingRate {
411    /// Symbol name.
412    pub symbol: Ustr,
413    /// Funding rate value.
414    pub funding_rate: String,
415    /// Funding time in milliseconds.
416    pub funding_time: i64,
417    /// Mark price at the funding time.
418    #[serde(default)]
419    pub mark_price: Option<String>,
420    /// Index price at the funding time.
421    #[serde(default)]
422    pub index_price: Option<String>,
423}
424
425/// Open interest record.
426#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(rename_all = "camelCase")]
428pub struct BinanceOpenInterest {
429    /// Symbol name.
430    pub symbol: Ustr,
431    /// Total open interest.
432    pub open_interest: String,
433    /// Timestamp in milliseconds.
434    pub time: i64,
435}
436
437/// Futures account balance entry.
438#[derive(Clone, Debug, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct BinanceFuturesBalance {
441    /// Account alias (only USD-M).
442    #[serde(default)]
443    pub account_alias: Option<String>,
444    /// Asset code (e.g., "USDT").
445    pub asset: Ustr,
446    /// Wallet balance (v2 uses walletBalance, v1 uses balance).
447    #[serde(alias = "balance")]
448    pub wallet_balance: String,
449    /// Unrealized profit.
450    #[serde(default)]
451    pub unrealized_profit: Option<String>,
452    /// Margin balance.
453    #[serde(default)]
454    pub margin_balance: Option<String>,
455    /// Maintenance margin required.
456    #[serde(default)]
457    pub maint_margin: Option<String>,
458    /// Initial margin required.
459    #[serde(default)]
460    pub initial_margin: Option<String>,
461    /// Position initial margin.
462    #[serde(default)]
463    pub position_initial_margin: Option<String>,
464    /// Open order initial margin.
465    #[serde(default)]
466    pub open_order_initial_margin: Option<String>,
467    /// Cross wallet balance.
468    #[serde(default)]
469    pub cross_wallet_balance: Option<String>,
470    /// Unrealized PnL for cross positions.
471    #[serde(default)]
472    pub cross_un_pnl: Option<String>,
473    /// Available balance.
474    pub available_balance: String,
475    /// Maximum withdrawable amount.
476    #[serde(default)]
477    pub max_withdraw_amount: Option<String>,
478    /// Whether margin trading is available.
479    #[serde(default)]
480    pub margin_available: Option<bool>,
481    /// Timestamp of last update in milliseconds.
482    pub update_time: i64,
483    /// Withdrawable amount (COIN-M specific).
484    #[serde(default)]
485    pub withdraw_available: Option<String>,
486}
487
488/// Account position from `GET /fapi/v2/account` positions array.
489#[derive(Clone, Debug, Serialize, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub struct BinanceAccountPosition {
492    /// Symbol name.
493    pub symbol: Ustr,
494    /// Initial margin.
495    #[serde(default)]
496    pub initial_margin: Option<String>,
497    /// Maintenance margin.
498    #[serde(default)]
499    pub maint_margin: Option<String>,
500    /// Unrealized profit.
501    #[serde(default)]
502    pub unrealized_profit: Option<String>,
503    /// Position initial margin.
504    #[serde(default)]
505    pub position_initial_margin: Option<String>,
506    /// Open order initial margin.
507    #[serde(default)]
508    pub open_order_initial_margin: Option<String>,
509    /// Leverage.
510    #[serde(default)]
511    pub leverage: Option<String>,
512    /// Isolated margin mode.
513    #[serde(default)]
514    pub isolated: Option<bool>,
515    /// Entry price.
516    #[serde(default)]
517    pub entry_price: Option<String>,
518    /// Max notional value.
519    #[serde(default)]
520    pub max_notional: Option<String>,
521    /// Bid notional.
522    #[serde(default)]
523    pub bid_notional: Option<String>,
524    /// Ask notional.
525    #[serde(default)]
526    pub ask_notional: Option<String>,
527    /// Position side (BOTH, LONG, SHORT).
528    #[serde(default)]
529    pub position_side: Option<BinancePositionSide>,
530    /// Position amount.
531    #[serde(default)]
532    pub position_amt: Option<String>,
533    /// Update time.
534    #[serde(default)]
535    pub update_time: Option<i64>,
536}
537
538/// Position risk from `GET /fapi/v2/positionRisk`.
539#[derive(Clone, Debug, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct BinancePositionRisk {
542    /// Symbol name.
543    pub symbol: Ustr,
544    /// Position quantity.
545    pub position_amt: String,
546    /// Entry price.
547    pub entry_price: String,
548    /// Mark price.
549    pub mark_price: String,
550    /// Unrealized profit and loss.
551    #[serde(default)]
552    pub un_realized_profit: Option<String>,
553    /// Liquidation price.
554    #[serde(default)]
555    pub liquidation_price: Option<String>,
556    /// Applied leverage.
557    pub leverage: String,
558    /// Max notional value.
559    #[serde(default)]
560    pub max_notional_value: Option<String>,
561    /// Margin type (CROSSED or ISOLATED).
562    #[serde(default)]
563    pub margin_type: Option<BinanceMarginType>,
564    /// Isolated margin amount.
565    #[serde(default)]
566    pub isolated_margin: Option<String>,
567    /// Auto add margin flag (as string from API).
568    #[serde(default)]
569    pub is_auto_add_margin: Option<String>,
570    /// Position side (BOTH, LONG, SHORT).
571    #[serde(default)]
572    pub position_side: Option<BinancePositionSide>,
573    /// Notional position value.
574    #[serde(default)]
575    pub notional: Option<String>,
576    /// Isolated wallet balance.
577    #[serde(default)]
578    pub isolated_wallet: Option<String>,
579    /// ADL quantile indicator.
580    #[serde(default)]
581    pub adl_quantile: Option<u8>,
582    /// Last update time.
583    #[serde(default)]
584    pub update_time: Option<i64>,
585    /// Break-even price.
586    #[serde(default)]
587    pub break_even_price: Option<String>,
588    /// Bankruptcy price.
589    #[serde(default)]
590    pub bust_price: Option<String>,
591}
592
593/// Income history record.
594#[derive(Clone, Debug, Serialize, Deserialize)]
595#[serde(rename_all = "camelCase")]
596pub struct BinanceIncomeRecord {
597    /// Symbol name (may be empty for transfers).
598    #[serde(default)]
599    pub symbol: Option<Ustr>,
600    /// Income type (e.g., FUNDING_FEE, COMMISSION).
601    pub income_type: BinanceIncomeType,
602    /// Income amount.
603    pub income: String,
604    /// Asset code.
605    pub asset: Ustr,
606    /// Event time in milliseconds.
607    pub time: i64,
608    /// Additional info field.
609    #[serde(default)]
610    pub info: Option<String>,
611    /// Transaction ID.
612    #[serde(default)]
613    pub tran_id: Option<i64>,
614    /// Related trade ID.
615    #[serde(default)]
616    pub trade_id: Option<i64>,
617}
618
619/// User trade record.
620#[derive(Clone, Debug, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct BinanceUserTrade {
623    /// Symbol name.
624    pub symbol: Ustr,
625    /// Trade ID.
626    pub id: i64,
627    /// Order ID.
628    pub order_id: i64,
629    /// Trade price.
630    pub price: String,
631    /// Executed quantity.
632    pub qty: String,
633    /// Quote quantity.
634    #[serde(default)]
635    pub quote_qty: Option<String>,
636    /// Realized PnL for the trade.
637    pub realized_pnl: String,
638    /// Buy/sell side.
639    pub side: BinanceSide,
640    /// Position side (BOTH, LONG, SHORT).
641    #[serde(default)]
642    pub position_side: Option<BinancePositionSide>,
643    /// Trade time in milliseconds.
644    pub time: i64,
645    /// Was the buyer the maker?
646    pub buyer: bool,
647    /// Was the trade maker liquidity?
648    pub maker: bool,
649    /// Commission paid.
650    #[serde(default)]
651    pub commission: Option<String>,
652    /// Commission asset.
653    #[serde(default)]
654    pub commission_asset: Option<Ustr>,
655    /// Margin asset (if provided).
656    #[serde(default)]
657    pub margin_asset: Option<Ustr>,
658}
659
660/// Futures account information from `GET /fapi/v2/account` or `GET /dapi/v1/account`.
661#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct BinanceFuturesAccountInfo {
664    /// Total initial margin required.
665    #[serde(default)]
666    pub total_initial_margin: Option<String>,
667    /// Total maintenance margin required.
668    #[serde(default)]
669    pub total_maint_margin: Option<String>,
670    /// Total wallet balance.
671    #[serde(default)]
672    pub total_wallet_balance: Option<String>,
673    /// Total unrealized profit.
674    #[serde(default)]
675    pub total_unrealized_profit: Option<String>,
676    /// Total margin balance.
677    #[serde(default)]
678    pub total_margin_balance: Option<String>,
679    /// Total position initial margin.
680    #[serde(default)]
681    pub total_position_initial_margin: Option<String>,
682    /// Total open order initial margin.
683    #[serde(default)]
684    pub total_open_order_initial_margin: Option<String>,
685    /// Total cross wallet balance.
686    #[serde(default)]
687    pub total_cross_wallet_balance: Option<String>,
688    /// Total cross unrealized PnL.
689    #[serde(default)]
690    pub total_cross_un_pnl: Option<String>,
691    /// Available balance.
692    #[serde(default)]
693    pub available_balance: Option<String>,
694    /// Max withdraw amount.
695    #[serde(default)]
696    pub max_withdraw_amount: Option<String>,
697    /// Can deposit.
698    #[serde(default)]
699    pub can_deposit: Option<bool>,
700    /// Can trade.
701    #[serde(default)]
702    pub can_trade: Option<bool>,
703    /// Can withdraw.
704    #[serde(default)]
705    pub can_withdraw: Option<bool>,
706    /// Multi-assets margin mode.
707    #[serde(default)]
708    pub multi_assets_margin: Option<bool>,
709    /// Update time.
710    #[serde(default)]
711    pub update_time: Option<i64>,
712    /// Account balances.
713    #[serde(default)]
714    pub assets: Vec<BinanceFuturesBalance>,
715    /// Account positions.
716    #[serde(default)]
717    pub positions: Vec<BinanceAccountPosition>,
718}
719
720impl BinanceFuturesAccountInfo {
721    /// Converts this Binance account info to a Nautilus [`AccountState`].
722    ///
723    /// # Errors
724    ///
725    /// Returns an error if balance parsing fails.
726    pub fn to_account_state(
727        &self,
728        account_id: AccountId,
729        ts_init: UnixNanos,
730    ) -> anyhow::Result<AccountState> {
731        let mut balances = Vec::with_capacity(self.assets.len());
732
733        for asset in &self.assets {
734            let currency = Currency::get_or_create_crypto_with_context(
735                asset.asset.as_str(),
736                Some("futures balance"),
737            );
738
739            let total: Decimal = asset.wallet_balance.parse().context("invalid balance")?;
740            let available: Decimal = asset
741                .available_balance
742                .parse()
743                .context("invalid available_balance")?;
744            let locked = total - available;
745
746            let total_money = Money::from_decimal(total, currency)
747                .unwrap_or_else(|_| Money::new(total.to_string().parse().unwrap_or(0.0), currency));
748            let locked_money = Money::from_decimal(locked, currency).unwrap_or_else(|_| {
749                Money::new(locked.to_string().parse().unwrap_or(0.0), currency)
750            });
751            let free_money = Money::from_decimal(available, currency).unwrap_or_else(|_| {
752                Money::new(available.to_string().parse().unwrap_or(0.0), currency)
753            });
754
755            let balance = AccountBalance::new(total_money, locked_money, free_money);
756            balances.push(balance);
757        }
758
759        // Ensure at least one balance exists
760        if balances.is_empty() {
761            let zero_currency = Currency::USDT();
762            let zero_money = Money::new(0.0, zero_currency);
763            let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
764            balances.push(zero_balance);
765        }
766
767        // Parse margin requirements
768        let mut margins = Vec::new();
769
770        let initial_margin_dec = self
771            .total_initial_margin
772            .as_ref()
773            .and_then(|s| Decimal::from_str_exact(s).ok());
774        let maint_margin_dec = self
775            .total_maint_margin
776            .as_ref()
777            .and_then(|s| Decimal::from_str_exact(s).ok());
778
779        if let (Some(initial_margin_dec), Some(maint_margin_dec)) =
780            (initial_margin_dec, maint_margin_dec)
781        {
782            let has_margin = !initial_margin_dec.is_zero() || !maint_margin_dec.is_zero();
783            if has_margin {
784                let margin_currency = Currency::USDT();
785                let margin_instrument_id =
786                    InstrumentId::new(Symbol::new("ACCOUNT"), Venue::new("BINANCE"));
787
788                let initial_margin = Money::from_decimal(initial_margin_dec, margin_currency)
789                    .unwrap_or_else(|_| Money::zero(margin_currency));
790                let maintenance_margin = Money::from_decimal(maint_margin_dec, margin_currency)
791                    .unwrap_or_else(|_| Money::zero(margin_currency));
792
793                let margin_balance =
794                    MarginBalance::new(initial_margin, maintenance_margin, margin_instrument_id);
795                margins.push(margin_balance);
796            }
797        }
798
799        let ts_event = self
800            .update_time
801            .map_or(ts_init, |t| UnixNanos::from((t * 1_000_000) as u64));
802
803        Ok(AccountState::new(
804            account_id,
805            AccountType::Margin,
806            balances,
807            margins,
808            true, // is_reported
809            UUID4::new(),
810            ts_event,
811            ts_init,
812            None,
813        ))
814    }
815}
816
817/// Hedge mode (dual side position) response.
818#[derive(Clone, Debug, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct BinanceHedgeModeResponse {
821    /// Whether dual side position mode is enabled.
822    pub dual_side_position: bool,
823}
824
825/// Leverage change response.
826#[derive(Clone, Debug, Serialize, Deserialize)]
827#[serde(rename_all = "camelCase")]
828pub struct BinanceLeverageResponse {
829    /// Symbol.
830    pub symbol: Ustr,
831    /// New leverage value.
832    pub leverage: u32,
833    /// Max notional value at this leverage.
834    #[serde(default)]
835    pub max_notional_value: Option<String>,
836}
837
838/// Cancel all orders response.
839#[derive(Clone, Debug, Serialize, Deserialize)]
840#[serde(rename_all = "camelCase")]
841pub struct BinanceCancelAllOrdersResponse {
842    /// Response code (200 = success).
843    pub code: i32,
844    /// Response message.
845    pub msg: String,
846}
847
848/// Futures order information.
849#[derive(Clone, Debug, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct BinanceFuturesOrder {
852    /// Symbol name.
853    pub symbol: Ustr,
854    /// Order ID.
855    pub order_id: i64,
856    /// Client order ID.
857    pub client_order_id: String,
858    /// Original order quantity.
859    pub orig_qty: String,
860    /// Executed quantity.
861    pub executed_qty: String,
862    /// Cumulative quote asset transacted.
863    pub cum_quote: String,
864    /// Original limit price.
865    pub price: String,
866    /// Average execution price.
867    #[serde(default)]
868    pub avg_price: Option<String>,
869    /// Stop price.
870    #[serde(default)]
871    pub stop_price: Option<String>,
872    /// Order status.
873    pub status: BinanceOrderStatus,
874    /// Time in force.
875    pub time_in_force: BinanceTimeInForce,
876    /// Order type.
877    #[serde(rename = "type")]
878    pub order_type: BinanceFuturesOrderType,
879    /// Original order type.
880    #[serde(default)]
881    pub orig_type: Option<BinanceFuturesOrderType>,
882    /// Order side (BUY/SELL).
883    pub side: BinanceSide,
884    /// Position side (BOTH/LONG/SHORT).
885    #[serde(default)]
886    pub position_side: Option<BinancePositionSide>,
887    /// Reduce-only flag.
888    #[serde(default)]
889    pub reduce_only: Option<bool>,
890    /// Close position flag (for stop orders).
891    #[serde(default)]
892    pub close_position: Option<bool>,
893    /// Trailing delta activation price.
894    #[serde(default)]
895    pub activate_price: Option<String>,
896    /// Trailing callback rate.
897    #[serde(default)]
898    pub price_rate: Option<String>,
899    /// Working type (CONTRACT_PRICE or MARK_PRICE).
900    #[serde(default)]
901    pub working_type: Option<BinanceWorkingType>,
902    /// Whether price protection is enabled.
903    #[serde(default)]
904    pub price_protect: Option<bool>,
905    /// Whether order uses isolated margin.
906    #[serde(default)]
907    pub is_isolated: Option<bool>,
908    /// Good till date (for GTD orders).
909    #[serde(default)]
910    pub good_till_date: Option<i64>,
911    /// Price match mode.
912    #[serde(default)]
913    pub price_match: Option<BinancePriceMatch>,
914    /// Self-trade prevention mode.
915    #[serde(default)]
916    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
917    /// Last update time.
918    #[serde(default)]
919    pub update_time: Option<i64>,
920    /// Working order ID for tracking.
921    #[serde(default)]
922    pub working_type_id: Option<i64>,
923}
924
925impl BinanceFuturesOrder {
926    /// Converts this Binance order to a Nautilus [`OrderStatusReport`].
927    ///
928    /// # Errors
929    ///
930    /// Returns an error if quantity parsing fails.
931    pub fn to_order_status_report(
932        &self,
933        account_id: AccountId,
934        instrument_id: InstrumentId,
935        size_precision: u8,
936    ) -> anyhow::Result<OrderStatusReport> {
937        let ts_now = get_atomic_clock_realtime().get_time_ns();
938        let ts_event = self
939            .update_time
940            .map_or(ts_now, |t| UnixNanos::from((t * 1_000_000) as u64));
941
942        let client_order_id = ClientOrderId::new(&self.client_order_id);
943        let venue_order_id = VenueOrderId::new(self.order_id.to_string());
944
945        let order_side = match self.side {
946            BinanceSide::Buy => OrderSide::Buy,
947            BinanceSide::Sell => OrderSide::Sell,
948        };
949
950        let order_type = self.order_type.to_nautilus_order_type();
951        let time_in_force = self.time_in_force.to_nautilus_time_in_force();
952        let order_status = self.status.to_nautilus_order_status();
953
954        let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
955        let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
956
957        Ok(OrderStatusReport::new(
958            account_id,
959            instrument_id,
960            Some(client_order_id),
961            venue_order_id,
962            order_side,
963            order_type,
964            time_in_force,
965            order_status,
966            Quantity::new(quantity.to_string().parse()?, size_precision),
967            Quantity::new(filled_qty.to_string().parse()?, size_precision),
968            ts_event,
969            ts_event,
970            ts_now,
971            Some(UUID4::new()),
972        ))
973    }
974}
975
976impl BinanceFuturesOrderType {
977    /// Returns whether this order type is post-only.
978    #[must_use]
979    pub fn is_post_only(&self) -> bool {
980        false // Binance Futures doesn't have a dedicated post-only type
981    }
982
983    /// Converts to Nautilus order type.
984    #[must_use]
985    pub fn to_nautilus_order_type(&self) -> OrderType {
986        match self {
987            Self::Market => OrderType::Market,
988            Self::Limit => OrderType::Limit,
989            Self::Stop => OrderType::StopLimit,
990            Self::StopMarket => OrderType::StopMarket,
991            Self::TakeProfit => OrderType::LimitIfTouched,
992            Self::TakeProfitMarket => OrderType::MarketIfTouched,
993            Self::TrailingStopMarket => OrderType::TrailingStopMarket,
994            Self::Liquidation | Self::Adl => OrderType::Market, // Forced closes
995            Self::Unknown => OrderType::Market,
996        }
997    }
998}
999
1000impl BinanceTimeInForce {
1001    /// Converts to Nautilus time in force.
1002    #[must_use]
1003    pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1004        match self {
1005            Self::Gtc => TimeInForce::Gtc,
1006            Self::Ioc => TimeInForce::Ioc,
1007            Self::Fok => TimeInForce::Fok,
1008            Self::Gtx => TimeInForce::Gtc, // GTX is GTC with post-only
1009            Self::Gtd => TimeInForce::Gtd,
1010            Self::Unknown => TimeInForce::Gtc, // default
1011        }
1012    }
1013}
1014
1015impl BinanceOrderStatus {
1016    /// Converts to Nautilus order status.
1017    #[must_use]
1018    pub fn to_nautilus_order_status(&self) -> OrderStatus {
1019        match self {
1020            Self::New => OrderStatus::Accepted,
1021            Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1022            Self::Filled => OrderStatus::Filled,
1023            Self::Canceled => OrderStatus::Canceled,
1024            Self::PendingCancel => OrderStatus::PendingCancel,
1025            Self::Rejected => OrderStatus::Rejected,
1026            Self::Expired => OrderStatus::Expired,
1027            Self::ExpiredInMatch => OrderStatus::Expired,
1028            Self::Unknown => OrderStatus::Initialized,
1029        }
1030    }
1031}
1032
1033impl BinanceUserTrade {
1034    /// Converts this Binance trade to a Nautilus [`FillReport`].
1035    ///
1036    /// # Errors
1037    ///
1038    /// Returns an error if quantity or price parsing fails.
1039    pub fn to_fill_report(
1040        &self,
1041        account_id: AccountId,
1042        instrument_id: InstrumentId,
1043        price_precision: u8,
1044        size_precision: u8,
1045    ) -> anyhow::Result<FillReport> {
1046        let ts_now = get_atomic_clock_realtime().get_time_ns();
1047        let ts_event = UnixNanos::from((self.time * 1_000_000) as u64);
1048
1049        let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1050        let trade_id = TradeId::new(self.id.to_string());
1051
1052        let order_side = match self.side {
1053            BinanceSide::Buy => OrderSide::Buy,
1054            BinanceSide::Sell => OrderSide::Sell,
1055        };
1056
1057        let liquidity_side = if self.maker {
1058            LiquiditySide::Maker
1059        } else {
1060            LiquiditySide::Taker
1061        };
1062
1063        let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1064        let last_px: Decimal = self.price.parse().context("invalid price")?;
1065
1066        let commission = {
1067            let comm_val: f64 = self
1068                .commission
1069                .as_ref()
1070                .and_then(|c| c.parse().ok())
1071                .unwrap_or(0.0);
1072            let comm_asset = self
1073                .commission_asset
1074                .as_ref()
1075                .map_or_else(Currency::USDT, Currency::from);
1076            Money::new(comm_val, comm_asset)
1077        };
1078
1079        Ok(FillReport::new(
1080            account_id,
1081            instrument_id,
1082            venue_order_id,
1083            trade_id,
1084            order_side,
1085            Quantity::new(last_qty.to_string().parse()?, size_precision),
1086            Price::new(last_px.to_string().parse()?, price_precision),
1087            commission,
1088            liquidity_side,
1089            None, // client_order_id
1090            None, // venue_position_id
1091            ts_event,
1092            ts_now,
1093            Some(UUID4::new()),
1094        ))
1095    }
1096}
1097
1098/// Result of a single order in a batch operation.
1099///
1100/// Each item in a batch response can be either a success or an error.
1101#[derive(Clone, Debug, Deserialize)]
1102#[serde(untagged)]
1103pub enum BatchOrderResult {
1104    /// Successful order operation.
1105    Success(Box<BinanceFuturesOrder>),
1106    /// Failed order operation.
1107    Error(BatchOrderError),
1108}
1109
1110/// Error in a batch order response.
1111#[derive(Clone, Debug, Deserialize)]
1112pub struct BatchOrderError {
1113    /// Error code from Binance.
1114    pub code: i64,
1115    /// Error message.
1116    pub msg: String,
1117}
1118
1119/// Listen key response from user data stream endpoints.
1120#[derive(Debug, Clone, Deserialize)]
1121#[serde(rename_all = "camelCase")]
1122pub struct ListenKeyResponse {
1123    /// The listen key for WebSocket user data stream.
1124    pub listen_key: String,
1125}
1126
1127/// Algo order response from Binance Futures Algo Service API.
1128///
1129/// Algo orders are conditional orders (STOP_MARKET, STOP_LIMIT, TAKE_PROFIT,
1130/// TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET) that are managed by Binance's
1131/// Algo Service rather than the traditional order matching engine.
1132///
1133/// # References
1134///
1135/// - <https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Algo-Order>
1136#[derive(Clone, Debug, Serialize, Deserialize)]
1137#[serde(rename_all = "camelCase")]
1138pub struct BinanceFuturesAlgoOrder {
1139    /// Unique algo order ID assigned by Binance.
1140    pub algo_id: i64,
1141    /// Client-specified algo order ID for idempotency.
1142    pub client_algo_id: String,
1143    /// Algo type (currently only `Conditional` is supported).
1144    pub algo_type: BinanceAlgoType,
1145    /// Order type (STOP_MARKET, STOP, TAKE_PROFIT, TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET).
1146    #[serde(rename = "type")]
1147    pub order_type: BinanceFuturesOrderType,
1148    /// Trading symbol.
1149    pub symbol: Ustr,
1150    /// Order side (BUY/SELL).
1151    pub side: BinanceSide,
1152    /// Position side (BOTH, LONG, SHORT).
1153    #[serde(default)]
1154    pub position_side: Option<BinancePositionSide>,
1155    /// Time in force.
1156    #[serde(default)]
1157    pub time_in_force: Option<BinanceTimeInForce>,
1158    /// Order quantity.
1159    #[serde(default)]
1160    pub quantity: Option<String>,
1161    /// Algo order status.
1162    #[serde(default)]
1163    pub algo_status: Option<BinanceAlgoStatus>,
1164    /// Trigger price for the conditional order.
1165    #[serde(default)]
1166    pub trigger_price: Option<String>,
1167    /// Limit price (for STOP/TAKE_PROFIT limit orders).
1168    #[serde(default)]
1169    pub price: Option<String>,
1170    /// Working type for trigger price calculation (CONTRACT_PRICE or MARK_PRICE).
1171    #[serde(default)]
1172    pub working_type: Option<BinanceWorkingType>,
1173    /// Close all position flag.
1174    #[serde(default)]
1175    pub close_position: Option<bool>,
1176    /// Price protection enabled.
1177    #[serde(default)]
1178    pub price_protect: Option<bool>,
1179    /// Reduce-only flag.
1180    #[serde(default)]
1181    pub reduce_only: Option<bool>,
1182    /// Activation price for TRAILING_STOP_MARKET orders.
1183    #[serde(default)]
1184    pub activate_price: Option<String>,
1185    /// Callback rate for TRAILING_STOP_MARKET orders (0.1 to 10, where 1 = 1%).
1186    #[serde(default)]
1187    pub callback_rate: Option<String>,
1188    /// Order creation time in milliseconds.
1189    #[serde(default)]
1190    pub create_time: Option<i64>,
1191    /// Last update time in milliseconds.
1192    #[serde(default)]
1193    pub update_time: Option<i64>,
1194    /// Trigger time in milliseconds (when the algo order triggered).
1195    #[serde(default)]
1196    pub trigger_time: Option<i64>,
1197    /// Order ID in matching engine (populated when algo order is triggered).
1198    #[serde(default)]
1199    pub actual_order_id: Option<String>,
1200    /// Executed quantity in matching engine (populated when algo order is triggered).
1201    #[serde(default)]
1202    pub executed_qty: Option<String>,
1203    /// Average fill price in matching engine (populated when algo order is triggered).
1204    #[serde(default)]
1205    pub avg_price: Option<String>,
1206}
1207
1208impl BinanceFuturesAlgoOrder {
1209    /// Converts this Binance algo order to a Nautilus [`OrderStatusReport`].
1210    ///
1211    /// # Errors
1212    ///
1213    /// Returns an error if quantity parsing fails.
1214    pub fn to_order_status_report(
1215        &self,
1216        account_id: AccountId,
1217        instrument_id: InstrumentId,
1218        size_precision: u8,
1219    ) -> anyhow::Result<OrderStatusReport> {
1220        let ts_now = get_atomic_clock_realtime().get_time_ns();
1221        let ts_event = self
1222            .update_time
1223            .or(self.create_time)
1224            .map_or(ts_now, |t| UnixNanos::from((t * 1_000_000) as u64));
1225
1226        let client_order_id = ClientOrderId::new(&self.client_algo_id);
1227        let venue_order_id = self.actual_order_id.as_ref().map_or_else(
1228            || VenueOrderId::new(self.algo_id.to_string()),
1229            |id| VenueOrderId::new(id.clone()),
1230        );
1231
1232        let order_side = match self.side {
1233            BinanceSide::Buy => OrderSide::Buy,
1234            BinanceSide::Sell => OrderSide::Sell,
1235        };
1236
1237        let order_type = self.parse_order_type();
1238        let time_in_force = self
1239            .time_in_force
1240            .as_ref()
1241            .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1242        let order_status = self.parse_order_status();
1243
1244        let quantity: Decimal = self
1245            .quantity
1246            .as_ref()
1247            .map_or(Ok(Decimal::ZERO), |q| q.parse())
1248            .context("invalid quantity")?;
1249        let filled_qty: Decimal = self
1250            .executed_qty
1251            .as_ref()
1252            .map_or(Ok(Decimal::ZERO), |q| q.parse())
1253            .context("invalid executed_qty")?;
1254
1255        Ok(OrderStatusReport::new(
1256            account_id,
1257            instrument_id,
1258            Some(client_order_id),
1259            venue_order_id,
1260            order_side,
1261            order_type,
1262            time_in_force,
1263            order_status,
1264            Quantity::new(quantity.to_string().parse()?, size_precision),
1265            Quantity::new(filled_qty.to_string().parse()?, size_precision),
1266            ts_event,
1267            ts_event,
1268            ts_now,
1269            Some(UUID4::new()),
1270        ))
1271    }
1272
1273    fn parse_order_type(&self) -> OrderType {
1274        self.order_type.into()
1275    }
1276
1277    fn parse_order_status(&self) -> OrderStatus {
1278        match self.algo_status {
1279            Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1280            Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1281            Some(BinanceAlgoStatus::Triggered) => OrderStatus::Accepted,
1282            Some(BinanceAlgoStatus::Finished) => {
1283                // Check executed_qty to determine if filled or canceled
1284                if let Some(qty) = &self.executed_qty
1285                    && let Ok(dec) = qty.parse::<Decimal>()
1286                    && !dec.is_zero()
1287                {
1288                    return OrderStatus::Filled;
1289                }
1290                OrderStatus::Canceled
1291            }
1292            Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1293            Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1294            Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1295            Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1296        }
1297    }
1298}
1299
1300/// Cancel response for algo orders from Binance Futures Algo Service API.
1301#[derive(Clone, Debug, Deserialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct BinanceFuturesAlgoOrderCancelResponse {
1304    /// Algo order ID that was canceled.
1305    pub algo_id: i64,
1306    /// Client algo order ID.
1307    pub client_algo_id: String,
1308    /// Response code (200 for success).
1309    pub code: i32,
1310    /// Response message.
1311    pub msg: String,
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use rstest::rstest;
1317
1318    use super::*;
1319
1320    /// Test fixture from Binance API docs for GET /fapi/v2/account
1321    const ACCOUNT_INFO_V2_JSON: &str = r#"{
1322        "feeTier": 0,
1323        "canTrade": true,
1324        "canDeposit": true,
1325        "canWithdraw": true,
1326        "updateTime": 0,
1327        "multiAssetsMargin": false,
1328        "tradeGroupId": -1,
1329        "totalInitialMargin": "0.00000000",
1330        "totalMaintMargin": "0.00000000",
1331        "totalWalletBalance": "23.72469206",
1332        "totalUnrealizedProfit": "0.00000000",
1333        "totalMarginBalance": "23.72469206",
1334        "totalPositionInitialMargin": "0.00000000",
1335        "totalOpenOrderInitialMargin": "0.00000000",
1336        "totalCrossWalletBalance": "23.72469206",
1337        "totalCrossUnPnl": "0.00000000",
1338        "availableBalance": "23.72469206",
1339        "maxWithdrawAmount": "23.72469206",
1340        "assets": [
1341            {
1342                "asset": "USDT",
1343                "walletBalance": "23.72469206",
1344                "unrealizedProfit": "0.00000000",
1345                "marginBalance": "23.72469206",
1346                "maintMargin": "0.00000000",
1347                "initialMargin": "0.00000000",
1348                "positionInitialMargin": "0.00000000",
1349                "openOrderInitialMargin": "0.00000000",
1350                "crossWalletBalance": "23.72469206",
1351                "crossUnPnl": "0.00000000",
1352                "availableBalance": "23.72469206",
1353                "maxWithdrawAmount": "23.72469206",
1354                "marginAvailable": true,
1355                "updateTime": 1625474304765
1356            }
1357        ],
1358        "positions": [
1359            {
1360                "symbol": "BTCUSDT",
1361                "initialMargin": "0",
1362                "maintMargin": "0",
1363                "unrealizedProfit": "0.00000000",
1364                "positionInitialMargin": "0",
1365                "openOrderInitialMargin": "0",
1366                "leverage": "100",
1367                "isolated": false,
1368                "entryPrice": "0.00000",
1369                "maxNotional": "250000",
1370                "bidNotional": "0",
1371                "askNotional": "0",
1372                "positionSide": "BOTH",
1373                "positionAmt": "0",
1374                "updateTime": 0
1375            }
1376        ]
1377    }"#;
1378
1379    /// Test fixture for GET /fapi/v2/positionRisk
1380    const POSITION_RISK_JSON: &str = r#"[
1381        {
1382            "symbol": "BTCUSDT",
1383            "positionAmt": "0.001",
1384            "entryPrice": "50000.0",
1385            "markPrice": "51000.0",
1386            "unRealizedProfit": "1.00000000",
1387            "liquidationPrice": "45000.0",
1388            "leverage": "20",
1389            "maxNotionalValue": "250000",
1390            "marginType": "cross",
1391            "isolatedMargin": "0.00000000",
1392            "isAutoAddMargin": "false",
1393            "positionSide": "BOTH",
1394            "notional": "51.0",
1395            "isolatedWallet": "0",
1396            "updateTime": 1625474304765,
1397            "breakEvenPrice": "50100.0"
1398        }
1399    ]"#;
1400
1401    /// Test fixture for balance endpoint
1402    const BALANCE_JSON: &str = r#"[
1403        {
1404            "accountAlias": "SgsR",
1405            "asset": "USDT",
1406            "balance": "122.12345678",
1407            "crossWalletBalance": "122.12345678",
1408            "crossUnPnl": "0.00000000",
1409            "availableBalance": "122.12345678",
1410            "maxWithdrawAmount": "122.12345678",
1411            "marginAvailable": true,
1412            "updateTime": 1617939110373
1413        }
1414    ]"#;
1415
1416    /// Test fixture for order response
1417    const ORDER_JSON: &str = r#"{
1418        "orderId": 12345678,
1419        "symbol": "BTCUSDT",
1420        "status": "NEW",
1421        "clientOrderId": "testOrder123",
1422        "price": "50000.00",
1423        "avgPrice": "0.00",
1424        "origQty": "0.001",
1425        "executedQty": "0.000",
1426        "cumQuote": "0.00",
1427        "timeInForce": "GTC",
1428        "type": "LIMIT",
1429        "reduceOnly": false,
1430        "closePosition": false,
1431        "side": "BUY",
1432        "positionSide": "BOTH",
1433        "stopPrice": "0.00",
1434        "workingType": "CONTRACT_PRICE",
1435        "priceProtect": false,
1436        "origType": "LIMIT",
1437        "priceMatch": "NONE",
1438        "selfTradePreventionMode": "NONE",
1439        "goodTillDate": 0,
1440        "time": 1625474304765,
1441        "updateTime": 1625474304765
1442    }"#;
1443
1444    #[rstest]
1445    fn test_parse_account_info_v2() {
1446        let account: BinanceFuturesAccountInfo =
1447            serde_json::from_str(ACCOUNT_INFO_V2_JSON).expect("Failed to parse account info");
1448
1449        assert_eq!(
1450            account.total_wallet_balance,
1451            Some("23.72469206".to_string())
1452        );
1453        assert_eq!(account.assets.len(), 1);
1454        assert_eq!(account.assets[0].asset.as_str(), "USDT");
1455        assert_eq!(account.assets[0].wallet_balance, "23.72469206");
1456        assert_eq!(account.positions.len(), 1);
1457        assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1458        assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1459    }
1460
1461    #[rstest]
1462    fn test_parse_position_risk() {
1463        let positions: Vec<BinancePositionRisk> =
1464            serde_json::from_str(POSITION_RISK_JSON).expect("Failed to parse position risk");
1465
1466        assert_eq!(positions.len(), 1);
1467        assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
1468        assert_eq!(positions[0].position_amt, "0.001");
1469        assert_eq!(positions[0].mark_price, "51000.0");
1470        assert_eq!(positions[0].leverage, "20");
1471    }
1472
1473    #[rstest]
1474    fn test_parse_balance_with_v1_field() {
1475        // V1 uses 'balance' field
1476        let balances: Vec<BinanceFuturesBalance> =
1477            serde_json::from_str(BALANCE_JSON).expect("Failed to parse balance");
1478
1479        assert_eq!(balances.len(), 1);
1480        assert_eq!(balances[0].asset.as_str(), "USDT");
1481        // Uses alias to parse 'balance' into wallet_balance
1482        assert_eq!(balances[0].wallet_balance, "122.12345678");
1483        assert_eq!(balances[0].available_balance, "122.12345678");
1484    }
1485
1486    #[rstest]
1487    fn test_parse_balance_with_v2_field() {
1488        // V2 uses 'walletBalance' field
1489        let json = r#"{
1490            "asset": "USDT",
1491            "walletBalance": "100.00000000",
1492            "availableBalance": "100.00000000",
1493            "updateTime": 1617939110373
1494        }"#;
1495
1496        let balance: BinanceFuturesBalance =
1497            serde_json::from_str(json).expect("Failed to parse balance");
1498
1499        assert_eq!(balance.asset.as_str(), "USDT");
1500        assert_eq!(balance.wallet_balance, "100.00000000");
1501    }
1502
1503    #[rstest]
1504    fn test_parse_order() {
1505        let order: BinanceFuturesOrder =
1506            serde_json::from_str(ORDER_JSON).expect("Failed to parse order");
1507
1508        assert_eq!(order.order_id, 12345678);
1509        assert_eq!(order.symbol.as_str(), "BTCUSDT");
1510        assert_eq!(order.status, BinanceOrderStatus::New);
1511        assert_eq!(order.side, BinanceSide::Buy);
1512        assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
1513    }
1514
1515    #[rstest]
1516    fn test_parse_hedge_mode_response() {
1517        let json = r#"{"dualSidePosition": true}"#;
1518        let response: BinanceHedgeModeResponse =
1519            serde_json::from_str(json).expect("Failed to parse hedge mode");
1520        assert!(response.dual_side_position);
1521    }
1522
1523    #[rstest]
1524    fn test_parse_leverage_response() {
1525        let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
1526        let response: BinanceLeverageResponse =
1527            serde_json::from_str(json).expect("Failed to parse leverage");
1528        assert_eq!(response.symbol.as_str(), "BTCUSDT");
1529        assert_eq!(response.leverage, 20);
1530    }
1531
1532    #[rstest]
1533    fn test_parse_listen_key_response() {
1534        let json =
1535            r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
1536        let response: ListenKeyResponse =
1537            serde_json::from_str(json).expect("Failed to parse listen key");
1538        assert!(!response.listen_key.is_empty());
1539    }
1540
1541    #[rstest]
1542    fn test_parse_account_position() {
1543        let json = r#"{
1544            "symbol": "ETHUSDT",
1545            "initialMargin": "100.00",
1546            "maintMargin": "50.00",
1547            "unrealizedProfit": "10.00",
1548            "positionInitialMargin": "100.00",
1549            "openOrderInitialMargin": "0",
1550            "leverage": "10",
1551            "isolated": true,
1552            "entryPrice": "2000.00",
1553            "maxNotional": "100000",
1554            "bidNotional": "0",
1555            "askNotional": "0",
1556            "positionSide": "LONG",
1557            "positionAmt": "0.5",
1558            "updateTime": 1625474304765
1559        }"#;
1560
1561        let position: BinanceAccountPosition =
1562            serde_json::from_str(json).expect("Failed to parse account position");
1563
1564        assert_eq!(position.symbol.as_str(), "ETHUSDT");
1565        assert_eq!(position.leverage, Some("10".to_string()));
1566        assert_eq!(position.isolated, Some(true));
1567        assert_eq!(position.position_side, Some(BinancePositionSide::Long));
1568    }
1569
1570    #[rstest]
1571    fn test_parse_algo_order() {
1572        let json = r#"{
1573            "algoId": 123456789,
1574            "clientAlgoId": "test-algo-order-1",
1575            "algoType": "CONDITIONAL",
1576            "type": "STOP_MARKET",
1577            "symbol": "BTCUSDT",
1578            "side": "BUY",
1579            "positionSide": "BOTH",
1580            "timeInForce": "GTC",
1581            "quantity": "0.001",
1582            "algoStatus": "NEW",
1583            "triggerPrice": "45000.00",
1584            "workingType": "MARK_PRICE",
1585            "reduceOnly": false,
1586            "createTime": 1625474304765,
1587            "updateTime": 1625474304765
1588        }"#;
1589
1590        let order: BinanceFuturesAlgoOrder =
1591            serde_json::from_str(json).expect("Failed to parse algo order");
1592
1593        assert_eq!(order.algo_id, 123456789);
1594        assert_eq!(order.client_algo_id, "test-algo-order-1");
1595        assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
1596        assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
1597        assert_eq!(order.symbol.as_str(), "BTCUSDT");
1598        assert_eq!(order.side, BinanceSide::Buy);
1599        assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
1600        assert_eq!(order.trigger_price, Some("45000.00".to_string()));
1601    }
1602
1603    #[rstest]
1604    fn test_parse_algo_order_triggered() {
1605        let json = r#"{
1606            "algoId": 123456789,
1607            "clientAlgoId": "test-algo-order-2",
1608            "algoType": "CONDITIONAL",
1609            "type": "TAKE_PROFIT",
1610            "symbol": "ETHUSDT",
1611            "side": "SELL",
1612            "algoStatus": "TRIGGERED",
1613            "triggerPrice": "2500.00",
1614            "price": "2500.00",
1615            "actualOrderId": "987654321",
1616            "executedQty": "0.5",
1617            "avgPrice": "2499.50"
1618        }"#;
1619
1620        let order: BinanceFuturesAlgoOrder =
1621            serde_json::from_str(json).expect("Failed to parse triggered algo order");
1622
1623        assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Triggered));
1624        assert_eq!(order.order_type, BinanceFuturesOrderType::TakeProfit);
1625        assert_eq!(order.actual_order_id, Some("987654321".to_string()));
1626        assert_eq!(order.executed_qty, Some("0.5".to_string()));
1627    }
1628
1629    #[rstest]
1630    fn test_parse_algo_order_cancel_response() {
1631        let json = r#"{
1632            "algoId": 123456789,
1633            "clientAlgoId": "test-algo-order-1",
1634            "code": 200,
1635            "msg": "success"
1636        }"#;
1637
1638        let response: BinanceFuturesAlgoOrderCancelResponse =
1639            serde_json::from_str(json).expect("Failed to parse algo cancel response");
1640
1641        assert_eq!(response.algo_id, 123456789);
1642        assert_eq!(response.client_algo_id, "test-algo-order-1");
1643        assert_eq!(response.code, 200);
1644        assert_eq!(response.msg, "success");
1645    }
1646}