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