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