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::{
28        AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
29        TrailingOffsetType, TriggerType,
30    },
31    events::AccountState,
32    identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
33    reports::{FillReport, OrderStatusReport},
34    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
35};
36use rust_decimal::Decimal;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39use ustr::Ustr;
40
41use crate::{
42    common::{
43        consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
44        encoder::decode_client_order_id,
45        enums::{
46            BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
47            BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
48            BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
49            BinanceTradingStatus, BinanceWorkingType,
50        },
51        models::BinanceRateLimit,
52        parse::{parse_millis, parse_required_decimal},
53    },
54    futures::conversions::{normalize_futures_asset, parse_good_till_date},
55};
56
57/// Server time response from `GET /fapi/v1/time`.
58#[derive(Clone, Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct BinanceServerTime {
61    /// Server timestamp in milliseconds.
62    pub server_time: i64,
63}
64
65/// Public trade from `GET /fapi/v1/trades`.
66#[derive(Clone, Debug, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct BinanceFuturesTrade {
69    /// Trade ID.
70    pub id: i64,
71    /// Trade price.
72    pub price: String,
73    /// Trade quantity.
74    pub qty: String,
75    /// Quote asset quantity.
76    pub quote_qty: String,
77    /// Trade timestamp in milliseconds.
78    pub time: i64,
79    /// Whether the buyer is the maker.
80    pub is_buyer_maker: bool,
81}
82
83/// Aggregate public trade from `GET /fapi/v1/aggTrades` or `GET /dapi/v1/aggTrades`.
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct BinanceFuturesAggTrade {
86    /// Aggregate trade ID.
87    #[serde(rename = "a")]
88    pub id: i64,
89    /// Trade price.
90    #[serde(rename = "p")]
91    pub price: String,
92    /// Trade quantity.
93    #[serde(rename = "q")]
94    pub qty: String,
95    /// First raw trade ID represented by this aggregate.
96    #[serde(rename = "f")]
97    pub first_trade_id: i64,
98    /// Last raw trade ID represented by this aggregate.
99    #[serde(rename = "l")]
100    pub last_trade_id: i64,
101    /// Trade timestamp in milliseconds.
102    #[serde(rename = "T")]
103    pub time: i64,
104    /// Whether the buyer is the maker.
105    #[serde(rename = "m")]
106    pub is_buyer_maker: bool,
107}
108
109/// Kline/candlestick data from `GET /fapi/v1/klines`.
110#[derive(Clone, Debug)]
111pub struct BinanceFuturesKline {
112    /// Open time in milliseconds.
113    pub open_time: i64,
114    /// Open price.
115    pub open: String,
116    /// High price.
117    pub high: String,
118    /// Low price.
119    pub low: String,
120    /// Close price.
121    pub close: String,
122    /// Volume.
123    pub volume: String,
124    /// Close time in milliseconds.
125    pub close_time: i64,
126    /// Quote asset volume.
127    pub quote_volume: String,
128    /// Number of trades.
129    pub num_trades: i64,
130    /// Taker buy base volume.
131    pub taker_buy_base_volume: String,
132    /// Taker buy quote volume.
133    pub taker_buy_quote_volume: String,
134}
135
136impl<'de> Deserialize<'de> for BinanceFuturesKline {
137    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138    where
139        D: serde::Deserializer<'de>,
140    {
141        let arr: Vec<Value> = Vec::deserialize(deserializer)?;
142        if arr.len() < 11 {
143            return Err(serde::de::Error::custom("Invalid kline array length"));
144        }
145
146        Ok(Self {
147            open_time: required_kline_i64::<D::Error>(&arr, 0, "open_time")?,
148            open: required_kline_string::<D::Error>(&arr, 1, "open")?,
149            high: required_kline_string::<D::Error>(&arr, 2, "high")?,
150            low: required_kline_string::<D::Error>(&arr, 3, "low")?,
151            close: required_kline_string::<D::Error>(&arr, 4, "close")?,
152            volume: required_kline_string::<D::Error>(&arr, 5, "volume")?,
153            close_time: required_kline_i64::<D::Error>(&arr, 6, "close_time")?,
154            quote_volume: required_kline_string::<D::Error>(&arr, 7, "quote_volume")?,
155            num_trades: required_kline_i64::<D::Error>(&arr, 8, "num_trades")?,
156            taker_buy_base_volume: required_kline_string::<D::Error>(
157                &arr,
158                9,
159                "taker_buy_base_volume",
160            )?,
161            taker_buy_quote_volume: required_kline_string::<D::Error>(
162                &arr,
163                10,
164                "taker_buy_quote_volume",
165            )?,
166        })
167    }
168}
169
170fn required_kline_i64<E>(arr: &[Value], index: usize, field: &str) -> Result<i64, E>
171where
172    E: serde::de::Error,
173{
174    arr[index]
175        .as_i64()
176        .ok_or_else(|| E::custom(format!("invalid kline {field}")))
177}
178
179fn required_kline_string<E>(arr: &[Value], index: usize, field: &str) -> Result<String, E>
180where
181    E: serde::de::Error,
182{
183    arr[index]
184        .as_str()
185        .map(ToString::to_string)
186        .ok_or_else(|| E::custom(format!("invalid kline {field}")))
187}
188
189/// USD-M Futures exchange information response from `GET /fapi/v1/exchangeInfo`.
190#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct BinanceFuturesUsdExchangeInfo {
193    /// Server timezone.
194    pub timezone: String,
195    /// Server timestamp in milliseconds.
196    pub server_time: i64,
197    /// Rate limit definitions.
198    pub rate_limits: Vec<BinanceRateLimit>,
199    /// Exchange-level filters.
200    #[serde(default)]
201    pub exchange_filters: Vec<Value>,
202    /// Asset definitions.
203    #[serde(default)]
204    pub assets: Vec<BinanceFuturesAsset>,
205    /// Trading symbols.
206    pub symbols: Vec<BinanceFuturesUsdSymbol>,
207}
208
209/// Futures asset definition.
210#[derive(Clone, Debug, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct BinanceFuturesAsset {
213    /// Asset name.
214    pub asset: Ustr,
215    /// Whether margin is available.
216    pub margin_available: bool,
217    /// Auto asset exchange threshold.
218    #[serde(default)]
219    pub auto_asset_exchange: Option<String>,
220}
221
222/// USD-M Futures symbol definition.
223#[derive(Clone, Debug, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct BinanceFuturesUsdSymbol {
226    /// Symbol name (e.g., "BTCUSDT").
227    pub symbol: Ustr,
228    /// Trading pair (e.g., "BTCUSDT").
229    pub pair: Ustr,
230    /// Contract type (PERPETUAL, TRADIFI_PERPETUAL, CURRENT_MONTH, NEXT_MONTH,
231    /// CURRENT_QUARTER, NEXT_QUARTER).
232    pub contract_type: String,
233    /// Delivery date timestamp.
234    pub delivery_date: i64,
235    /// Onboard date timestamp.
236    pub onboard_date: i64,
237    /// Trading status.
238    pub status: BinanceTradingStatus,
239    /// Maintenance margin percent.
240    pub maint_margin_percent: String,
241    /// Required margin percent.
242    pub required_margin_percent: String,
243    /// Base asset.
244    pub base_asset: Ustr,
245    /// Quote asset.
246    pub quote_asset: Ustr,
247    /// Margin asset.
248    pub margin_asset: Ustr,
249    /// Price precision.
250    pub price_precision: i32,
251    /// Quantity precision.
252    pub quantity_precision: i32,
253    /// Base asset precision.
254    pub base_asset_precision: i32,
255    /// Quote precision.
256    pub quote_precision: i32,
257    /// Underlying type.
258    #[serde(default)]
259    pub underlying_type: Option<String>,
260    /// Underlying sub type.
261    #[serde(default)]
262    pub underlying_sub_type: Vec<String>,
263    /// Settle plan.
264    #[serde(default)]
265    pub settle_plan: Option<i64>,
266    /// Trigger protect threshold.
267    #[serde(default)]
268    pub trigger_protect: Option<String>,
269    /// Liquidation fee.
270    #[serde(default)]
271    pub liquidation_fee: Option<String>,
272    /// Market take bound.
273    #[serde(default)]
274    pub market_take_bound: Option<String>,
275    /// Allowed order types.
276    pub order_types: Vec<String>,
277    /// Time in force options.
278    pub time_in_force: Vec<String>,
279    /// Symbol filters.
280    pub filters: Vec<Value>,
281}
282
283/// COIN-M Futures exchange information response from `GET /dapi/v1/exchangeInfo`.
284#[derive(Clone, Debug, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct BinanceFuturesCoinExchangeInfo {
287    /// Server timezone.
288    pub timezone: String,
289    /// Server timestamp in milliseconds.
290    pub server_time: i64,
291    /// Rate limit definitions.
292    pub rate_limits: Vec<BinanceRateLimit>,
293    /// Exchange-level filters.
294    #[serde(default)]
295    pub exchange_filters: Vec<Value>,
296    /// Trading symbols.
297    pub symbols: Vec<BinanceFuturesCoinSymbol>,
298}
299
300/// COIN-M Futures symbol definition.
301#[derive(Clone, Debug, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct BinanceFuturesCoinSymbol {
304    /// Symbol name (e.g., "BTCUSD_PERP").
305    pub symbol: Ustr,
306    /// Trading pair (e.g., "BTCUSD").
307    pub pair: Ustr,
308    /// Contract type (PERPETUAL, CURRENT_QUARTER, NEXT_QUARTER).
309    pub contract_type: String,
310    /// Delivery date timestamp.
311    pub delivery_date: i64,
312    /// Onboard date timestamp.
313    pub onboard_date: i64,
314    /// Trading status.
315    #[serde(default)]
316    pub contract_status: Option<BinanceContractStatus>,
317    /// Contract size.
318    pub contract_size: i64,
319    /// Maintenance margin percent.
320    pub maint_margin_percent: String,
321    /// Required margin percent.
322    pub required_margin_percent: String,
323    /// Base asset.
324    pub base_asset: Ustr,
325    /// Quote asset.
326    pub quote_asset: Ustr,
327    /// Margin asset.
328    pub margin_asset: Ustr,
329    /// Price precision.
330    pub price_precision: i32,
331    /// Quantity precision.
332    pub quantity_precision: i32,
333    /// Base asset precision.
334    pub base_asset_precision: i32,
335    /// Quote precision.
336    pub quote_precision: i32,
337    /// Equal quantity precision.
338    #[serde(default, rename = "equalQtyPrecision")]
339    pub equal_qty_precision: Option<i32>,
340    /// Trigger protect threshold.
341    #[serde(default)]
342    pub trigger_protect: Option<String>,
343    /// Liquidation fee.
344    #[serde(default)]
345    pub liquidation_fee: Option<String>,
346    /// Market take bound.
347    #[serde(default)]
348    pub market_take_bound: Option<String>,
349    /// Allowed order types.
350    pub order_types: Vec<String>,
351    /// Time in force options.
352    pub time_in_force: Vec<String>,
353    /// Symbol filters.
354    pub filters: Vec<Value>,
355}
356
357/// 24hr ticker price change statistics for futures.
358#[derive(Clone, Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct BinanceFuturesTicker24hr {
361    /// Symbol name.
362    pub symbol: Ustr,
363    /// Price change in quote asset.
364    pub price_change: String,
365    /// Price change percentage.
366    pub price_change_percent: String,
367    /// Weighted average price.
368    pub weighted_avg_price: String,
369    /// Last traded price.
370    pub last_price: String,
371    /// Last traded quantity.
372    #[serde(default)]
373    pub last_qty: Option<String>,
374    /// Opening price.
375    pub open_price: String,
376    /// Highest price.
377    pub high_price: String,
378    /// Lowest price.
379    pub low_price: String,
380    /// Total traded base asset volume.
381    pub volume: String,
382    /// Total traded quote asset volume.
383    pub quote_volume: String,
384    /// Statistics open time.
385    pub open_time: i64,
386    /// Statistics close time.
387    pub close_time: i64,
388    /// First trade ID.
389    #[serde(default)]
390    pub first_id: Option<i64>,
391    /// Last trade ID.
392    #[serde(default)]
393    pub last_id: Option<i64>,
394    /// Total number of trades.
395    #[serde(default)]
396    pub count: Option<i64>,
397}
398
399/// Mark price and funding rate for futures.
400#[derive(Clone, Debug, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct BinanceFuturesMarkPrice {
403    /// Symbol name.
404    pub symbol: Ustr,
405    /// Mark price.
406    pub mark_price: String,
407    /// Index price.
408    #[serde(default)]
409    pub index_price: Option<String>,
410    /// Estimated settle price (only for delivery contracts).
411    #[serde(default)]
412    pub estimated_settle_price: Option<String>,
413    /// Last funding rate.
414    #[serde(default)]
415    pub last_funding_rate: Option<String>,
416    /// Next funding time.
417    #[serde(default)]
418    pub next_funding_time: Option<i64>,
419    /// Interest rate.
420    #[serde(default)]
421    pub interest_rate: Option<String>,
422    /// Timestamp.
423    pub time: i64,
424}
425
426/// Order book depth snapshot.
427#[derive(Clone, Debug, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct BinanceOrderBook {
430    /// Last update ID.
431    pub last_update_id: i64,
432    /// Bid levels as `[price, quantity]` arrays.
433    pub bids: Vec<(String, String)>,
434    /// Ask levels as `[price, quantity]` arrays.
435    pub asks: Vec<(String, String)>,
436    /// Message output time.
437    #[serde(default, rename = "E")]
438    pub event_time: Option<i64>,
439    /// Transaction time.
440    #[serde(default, rename = "T")]
441    pub transaction_time: Option<i64>,
442}
443
444/// Best bid/ask from book ticker endpoint.
445#[derive(Clone, Debug, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct BinanceBookTicker {
448    /// Symbol name.
449    pub symbol: Ustr,
450    /// Best bid price.
451    pub bid_price: String,
452    /// Best bid quantity.
453    pub bid_qty: String,
454    /// Best ask price.
455    pub ask_price: String,
456    /// Best ask quantity.
457    pub ask_qty: String,
458    /// Event time.
459    #[serde(default)]
460    pub time: Option<i64>,
461}
462
463/// Price ticker.
464#[derive(Clone, Debug, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct BinancePriceTicker {
467    /// Symbol name.
468    pub symbol: Ustr,
469    /// Current price.
470    pub price: String,
471    /// Event time.
472    #[serde(default)]
473    pub time: Option<i64>,
474}
475
476/// Funding rate history record.
477#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct BinanceFundingRate {
480    /// Symbol name.
481    pub symbol: Ustr,
482    /// Funding rate value.
483    pub funding_rate: String,
484    /// Funding time in milliseconds.
485    pub funding_time: i64,
486    /// Mark price at the funding time.
487    #[serde(default)]
488    pub mark_price: Option<String>,
489    /// Index price at the funding time.
490    #[serde(default)]
491    pub index_price: Option<String>,
492}
493
494/// Open interest record.
495#[derive(Clone, Debug, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct BinanceOpenInterest {
498    /// Symbol name.
499    pub symbol: Ustr,
500    /// Total open interest.
501    pub open_interest: String,
502    /// Timestamp in milliseconds.
503    pub time: i64,
504}
505
506/// Historical open interest record from `GET /futures/data/openInterestHist`.
507#[derive(Clone, Debug, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct BinanceOpenInterestHistRecord {
510    /// Symbol name for USD-M responses.
511    #[serde(default)]
512    pub symbol: Option<Ustr>,
513    /// Trading pair for COIN-M responses.
514    #[serde(default)]
515    pub pair: Option<Ustr>,
516    /// Contract type for COIN-M responses.
517    #[serde(default)]
518    pub contract_type: Option<String>,
519    /// Total open interest for the bucket.
520    pub sum_open_interest: String,
521    /// Total open interest notional value for the bucket.
522    pub sum_open_interest_value: String,
523    /// Bucket timestamp in milliseconds.
524    pub timestamp: i64,
525    /// USD-M-specific optional circulating supply field.
526    #[serde(default, rename = "CMCCirculatingSupply")]
527    pub cmc_circulating_supply: Option<String>,
528}
529
530/// Futures account balance entry.
531#[derive(Clone, Debug, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub struct BinanceFuturesBalance {
534    /// Account alias (only USD-M).
535    #[serde(default)]
536    pub account_alias: Option<String>,
537    /// Asset code (e.g., "USDT").
538    pub asset: Ustr,
539    /// Wallet balance (v2 uses walletBalance, v1 uses balance).
540    #[serde(
541        alias = "balance",
542        deserialize_with = "deserialize_decimal_or_zero",
543        serialize_with = "serialize_decimal_as_str"
544    )]
545    pub wallet_balance: Decimal,
546    /// Unrealized profit.
547    #[serde(
548        default,
549        deserialize_with = "deserialize_optional_decimal_from_str",
550        serialize_with = "serialize_optional_decimal_as_str"
551    )]
552    pub unrealized_profit: Option<Decimal>,
553    /// Margin balance.
554    #[serde(
555        default,
556        deserialize_with = "deserialize_optional_decimal_from_str",
557        serialize_with = "serialize_optional_decimal_as_str"
558    )]
559    pub margin_balance: Option<Decimal>,
560    /// Maintenance margin required.
561    #[serde(
562        default,
563        deserialize_with = "deserialize_optional_decimal_from_str",
564        serialize_with = "serialize_optional_decimal_as_str"
565    )]
566    pub maint_margin: Option<Decimal>,
567    /// Initial margin required.
568    #[serde(
569        default,
570        deserialize_with = "deserialize_optional_decimal_from_str",
571        serialize_with = "serialize_optional_decimal_as_str"
572    )]
573    pub initial_margin: Option<Decimal>,
574    /// Position initial margin.
575    #[serde(
576        default,
577        deserialize_with = "deserialize_optional_decimal_from_str",
578        serialize_with = "serialize_optional_decimal_as_str"
579    )]
580    pub position_initial_margin: Option<Decimal>,
581    /// Open order initial margin.
582    #[serde(
583        default,
584        deserialize_with = "deserialize_optional_decimal_from_str",
585        serialize_with = "serialize_optional_decimal_as_str"
586    )]
587    pub open_order_initial_margin: Option<Decimal>,
588    /// Cross wallet balance.
589    #[serde(
590        default,
591        deserialize_with = "deserialize_optional_decimal_from_str",
592        serialize_with = "serialize_optional_decimal_as_str"
593    )]
594    pub cross_wallet_balance: Option<Decimal>,
595    /// Unrealized PnL for cross positions.
596    #[serde(
597        default,
598        deserialize_with = "deserialize_optional_decimal_from_str",
599        serialize_with = "serialize_optional_decimal_as_str"
600    )]
601    pub cross_un_pnl: Option<Decimal>,
602    /// Available balance.
603    #[serde(
604        deserialize_with = "deserialize_decimal_or_zero",
605        serialize_with = "serialize_decimal_as_str"
606    )]
607    pub available_balance: Decimal,
608    /// Maximum withdrawable amount.
609    #[serde(
610        default,
611        deserialize_with = "deserialize_optional_decimal_from_str",
612        serialize_with = "serialize_optional_decimal_as_str"
613    )]
614    pub max_withdraw_amount: Option<Decimal>,
615    /// Whether margin trading is available.
616    #[serde(default)]
617    pub margin_available: Option<bool>,
618    /// Timestamp of last update in milliseconds.
619    pub update_time: i64,
620    /// Withdrawable amount (COIN-M specific).
621    #[serde(
622        default,
623        deserialize_with = "deserialize_optional_decimal_from_str",
624        serialize_with = "serialize_optional_decimal_as_str"
625    )]
626    pub withdraw_available: Option<Decimal>,
627}
628
629/// Account position from `GET /fapi/v2/account` positions array.
630#[derive(Clone, Debug, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct BinanceAccountPosition {
633    /// Symbol name.
634    pub symbol: Ustr,
635    /// Initial margin.
636    #[serde(default)]
637    pub initial_margin: Option<String>,
638    /// Maintenance margin.
639    #[serde(default)]
640    pub maint_margin: Option<String>,
641    /// Unrealized profit.
642    #[serde(default)]
643    pub unrealized_profit: Option<String>,
644    /// Position initial margin.
645    #[serde(default)]
646    pub position_initial_margin: Option<String>,
647    /// Open order initial margin.
648    #[serde(default)]
649    pub open_order_initial_margin: Option<String>,
650    /// Leverage.
651    #[serde(default)]
652    pub leverage: Option<String>,
653    /// Isolated margin mode.
654    #[serde(default)]
655    pub isolated: Option<bool>,
656    /// Entry price.
657    #[serde(default)]
658    pub entry_price: Option<String>,
659    /// Max notional value.
660    #[serde(default)]
661    pub max_notional: Option<String>,
662    /// Bid notional.
663    #[serde(default)]
664    pub bid_notional: Option<String>,
665    /// Ask notional.
666    #[serde(default)]
667    pub ask_notional: Option<String>,
668    /// Position side (BOTH, LONG, SHORT).
669    #[serde(default)]
670    pub position_side: Option<BinancePositionSide>,
671    /// Position amount.
672    #[serde(default)]
673    pub position_amt: Option<String>,
674    /// Update time.
675    #[serde(default)]
676    pub update_time: Option<i64>,
677}
678
679/// Position risk from `GET /fapi/v2/positionRisk`.
680#[derive(Clone, Debug, Serialize, Deserialize)]
681#[serde(rename_all = "camelCase")]
682pub struct BinancePositionRisk {
683    /// Symbol name.
684    pub symbol: Ustr,
685    /// Position quantity.
686    pub position_amt: String,
687    /// Entry price.
688    pub entry_price: String,
689    /// Mark price.
690    pub mark_price: String,
691    /// Unrealized profit and loss.
692    #[serde(default)]
693    pub un_realized_profit: Option<String>,
694    /// Liquidation price.
695    #[serde(default)]
696    pub liquidation_price: Option<String>,
697    /// Applied leverage.
698    pub leverage: String,
699    /// Max notional value.
700    #[serde(default)]
701    pub max_notional_value: Option<String>,
702    /// Margin type (CROSSED or ISOLATED).
703    #[serde(default)]
704    pub margin_type: Option<BinanceMarginType>,
705    /// Isolated margin amount.
706    #[serde(default)]
707    pub isolated_margin: Option<String>,
708    /// Auto add margin flag (as string from API).
709    #[serde(default)]
710    pub is_auto_add_margin: Option<String>,
711    /// Position side (BOTH, LONG, SHORT).
712    #[serde(default)]
713    pub position_side: Option<BinancePositionSide>,
714    /// Notional position value.
715    #[serde(default)]
716    pub notional: Option<String>,
717    /// Isolated wallet balance.
718    #[serde(default)]
719    pub isolated_wallet: Option<String>,
720    /// ADL quantile indicator.
721    #[serde(default)]
722    pub adl_quantile: Option<u8>,
723    /// Last update time.
724    #[serde(default)]
725    pub update_time: Option<i64>,
726    /// Break-even price.
727    #[serde(default)]
728    pub break_even_price: Option<String>,
729    /// Bankruptcy price.
730    #[serde(default)]
731    pub bust_price: Option<String>,
732}
733
734/// Income history record.
735#[derive(Clone, Debug, Serialize, Deserialize)]
736#[serde(rename_all = "camelCase")]
737pub struct BinanceIncomeRecord {
738    /// Symbol name (may be empty for transfers).
739    #[serde(default)]
740    pub symbol: Option<Ustr>,
741    /// Income type (e.g., FUNDING_FEE, COMMISSION).
742    pub income_type: BinanceIncomeType,
743    /// Income amount.
744    pub income: String,
745    /// Asset code.
746    pub asset: Ustr,
747    /// Event time in milliseconds.
748    pub time: i64,
749    /// Additional info field.
750    #[serde(default)]
751    pub info: Option<String>,
752    /// Transaction ID.
753    #[serde(default)]
754    pub tran_id: Option<i64>,
755    /// Related trade ID.
756    #[serde(default)]
757    pub trade_id: Option<i64>,
758}
759
760/// User trade record.
761#[derive(Clone, Debug, Serialize, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct BinanceUserTrade {
764    /// Symbol name.
765    pub symbol: Ustr,
766    /// Trade ID.
767    pub id: i64,
768    /// Order ID.
769    pub order_id: i64,
770    /// Trade price.
771    pub price: String,
772    /// Executed quantity.
773    pub qty: String,
774    /// Quote quantity.
775    #[serde(default)]
776    pub quote_qty: Option<String>,
777    /// Realized PnL for the trade.
778    pub realized_pnl: String,
779    /// Buy/sell side.
780    pub side: BinanceSide,
781    /// Position side (BOTH, LONG, SHORT).
782    #[serde(default)]
783    pub position_side: Option<BinancePositionSide>,
784    /// Trade time in milliseconds.
785    pub time: i64,
786    /// Was the buyer the maker?
787    pub buyer: bool,
788    /// Was the trade maker liquidity?
789    pub maker: bool,
790    /// Commission paid.
791    #[serde(default)]
792    pub commission: Option<String>,
793    /// Commission asset.
794    #[serde(default)]
795    pub commission_asset: Option<Ustr>,
796    /// Margin asset (if provided).
797    #[serde(default)]
798    pub margin_asset: Option<Ustr>,
799}
800
801/// Futures account information from `GET /fapi/v2/account` or `GET /dapi/v1/account`.
802#[derive(Clone, Debug, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804pub struct BinanceFuturesAccountInfo {
805    /// Futures VIP fee tier.
806    #[serde(default)]
807    pub fee_tier: u8,
808    /// Total initial margin required.
809    #[serde(
810        default,
811        deserialize_with = "deserialize_optional_decimal_from_str",
812        serialize_with = "serialize_optional_decimal_as_str"
813    )]
814    pub total_initial_margin: Option<Decimal>,
815    /// Total maintenance margin required.
816    #[serde(
817        default,
818        deserialize_with = "deserialize_optional_decimal_from_str",
819        serialize_with = "serialize_optional_decimal_as_str"
820    )]
821    pub total_maint_margin: Option<Decimal>,
822    /// Total wallet balance.
823    #[serde(
824        default,
825        deserialize_with = "deserialize_optional_decimal_from_str",
826        serialize_with = "serialize_optional_decimal_as_str"
827    )]
828    pub total_wallet_balance: Option<Decimal>,
829    /// Total unrealized profit.
830    #[serde(
831        default,
832        deserialize_with = "deserialize_optional_decimal_from_str",
833        serialize_with = "serialize_optional_decimal_as_str"
834    )]
835    pub total_unrealized_profit: Option<Decimal>,
836    /// Total margin balance.
837    #[serde(
838        default,
839        deserialize_with = "deserialize_optional_decimal_from_str",
840        serialize_with = "serialize_optional_decimal_as_str"
841    )]
842    pub total_margin_balance: Option<Decimal>,
843    /// Total position initial margin.
844    #[serde(
845        default,
846        deserialize_with = "deserialize_optional_decimal_from_str",
847        serialize_with = "serialize_optional_decimal_as_str"
848    )]
849    pub total_position_initial_margin: Option<Decimal>,
850    /// Total open order initial margin.
851    #[serde(
852        default,
853        deserialize_with = "deserialize_optional_decimal_from_str",
854        serialize_with = "serialize_optional_decimal_as_str"
855    )]
856    pub total_open_order_initial_margin: Option<Decimal>,
857    /// Total cross wallet balance.
858    #[serde(
859        default,
860        deserialize_with = "deserialize_optional_decimal_from_str",
861        serialize_with = "serialize_optional_decimal_as_str"
862    )]
863    pub total_cross_wallet_balance: Option<Decimal>,
864    /// Total cross unrealized PnL.
865    #[serde(
866        default,
867        deserialize_with = "deserialize_optional_decimal_from_str",
868        serialize_with = "serialize_optional_decimal_as_str"
869    )]
870    pub total_cross_un_pnl: Option<Decimal>,
871    /// Available balance.
872    #[serde(
873        default,
874        deserialize_with = "deserialize_optional_decimal_from_str",
875        serialize_with = "serialize_optional_decimal_as_str"
876    )]
877    pub available_balance: Option<Decimal>,
878    /// Max withdraw amount.
879    #[serde(
880        default,
881        deserialize_with = "deserialize_optional_decimal_from_str",
882        serialize_with = "serialize_optional_decimal_as_str"
883    )]
884    pub max_withdraw_amount: Option<Decimal>,
885    /// Can deposit.
886    #[serde(default)]
887    pub can_deposit: Option<bool>,
888    /// Can trade.
889    #[serde(default)]
890    pub can_trade: Option<bool>,
891    /// Can withdraw.
892    #[serde(default)]
893    pub can_withdraw: Option<bool>,
894    /// Multi-assets margin mode.
895    #[serde(default)]
896    pub multi_assets_margin: Option<bool>,
897    /// Update time.
898    #[serde(default)]
899    pub update_time: Option<i64>,
900    /// Account balances.
901    #[serde(default)]
902    pub assets: Vec<BinanceFuturesBalance>,
903    /// Account positions.
904    #[serde(default)]
905    pub positions: Vec<BinanceAccountPosition>,
906}
907
908/// Account-specific Futures commission rates for one symbol.
909#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct BinanceFuturesCommissionRate {
912    /// Venue symbol.
913    pub symbol: Ustr,
914    /// Maker commission rate.
915    pub maker_commission_rate: String,
916    /// Taker commission rate.
917    pub taker_commission_rate: String,
918}
919
920impl BinanceFuturesAccountInfo {
921    /// Converts this Binance account info to a Nautilus [`AccountState`].
922    ///
923    /// # Errors
924    ///
925    /// Returns an error if balance parsing fails.
926    pub fn to_account_state(
927        &self,
928        account_id: AccountId,
929        ts_init: UnixNanos,
930    ) -> anyhow::Result<AccountState> {
931        let mut balances = Vec::with_capacity(self.assets.len());
932
933        for asset in &self.assets {
934            let currency = Currency::get_or_create_crypto_with_context(
935                asset.asset.as_str(),
936                Some("futures balance"),
937            );
938
939            let balance = AccountBalance::from_total_and_free(
940                asset.wallet_balance,
941                asset.available_balance,
942                currency,
943            )
944            .context("failed to build account balance")?;
945            balances.push(balance);
946        }
947
948        // Ensure at least one balance exists
949        if balances.is_empty() {
950            let zero_currency = Currency::USDT();
951            let zero_money = Money::zero(zero_currency);
952            let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
953            balances.push(zero_balance);
954        }
955
956        // Emit account-wide (cross-margin) margin balances per collateral asset.
957        // Binance reports per-asset `initialMargin` / `maintMargin` which covers both
958        // USDT-M (single collateral, typically USDT or BNB under multi-assets mode) and
959        // COIN-M (one entry per base coin, e.g. BTC / ETH).
960        let mut margins = Vec::new();
961
962        for asset in &self.assets {
963            let initial_dec = asset.initial_margin.unwrap_or_default();
964            let maint_dec = asset.maint_margin.unwrap_or_default();
965
966            if initial_dec.is_zero() && maint_dec.is_zero() {
967                continue;
968            }
969
970            let currency = Currency::get_or_create_crypto_with_context(
971                asset.asset.as_str(),
972                Some("futures margin"),
973            );
974            let initial = Money::from_decimal(initial_dec, currency)
975                .unwrap_or_else(|_| Money::zero(currency));
976            let maintenance =
977                Money::from_decimal(maint_dec, currency).unwrap_or_else(|_| Money::zero(currency));
978            margins.push(MarginBalance::new(initial, maintenance, None));
979        }
980
981        let ts_event = self
982            .update_time
983            .map(|value| parse_millis(value, "Futures account update time"))
984            .transpose()?
985            .unwrap_or(ts_init);
986
987        Ok(AccountState::new(
988            account_id,
989            AccountType::Margin,
990            balances,
991            margins,
992            true, // is_reported
993            UUID4::new(),
994            ts_event,
995            ts_init,
996            None,
997        ))
998    }
999}
1000
1001/// Hedge mode (dual side position) response.
1002#[derive(Clone, Debug, Serialize, Deserialize)]
1003#[serde(rename_all = "camelCase")]
1004pub struct BinanceHedgeModeResponse {
1005    /// Whether dual side position mode is enabled.
1006    pub dual_side_position: bool,
1007}
1008
1009/// Leverage change response.
1010#[derive(Clone, Debug, Serialize, Deserialize)]
1011#[serde(rename_all = "camelCase")]
1012pub struct BinanceLeverageResponse {
1013    /// Symbol.
1014    pub symbol: Ustr,
1015    /// New leverage value.
1016    pub leverage: u32,
1017    /// Max notional value at this leverage.
1018    #[serde(default)]
1019    pub max_notional_value: Option<String>,
1020}
1021
1022/// Cancel all orders response.
1023#[derive(Clone, Debug, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct BinanceCancelAllOrdersResponse {
1026    /// Response code (200 = success).
1027    pub code: i32,
1028    /// Response message.
1029    pub msg: String,
1030}
1031
1032/// Futures order information.
1033#[derive(Clone, Debug, Serialize, Deserialize)]
1034#[serde(rename_all = "camelCase")]
1035pub struct BinanceFuturesOrder {
1036    /// Symbol name.
1037    pub symbol: Ustr,
1038    /// Order ID.
1039    pub order_id: i64,
1040    /// Client order ID.
1041    pub client_order_id: String,
1042    /// Original order quantity.
1043    pub orig_qty: String,
1044    /// Executed quantity.
1045    pub executed_qty: String,
1046    /// Cumulative quote asset transacted.
1047    #[serde(default = "zero_decimal_string")]
1048    pub cum_quote: String,
1049    /// Original limit price.
1050    pub price: String,
1051    /// Average execution price.
1052    #[serde(default)]
1053    pub avg_price: Option<String>,
1054    /// Stop price.
1055    #[serde(default)]
1056    pub stop_price: Option<String>,
1057    /// Order status.
1058    pub status: BinanceOrderStatus,
1059    /// Time in force.
1060    pub time_in_force: BinanceTimeInForce,
1061    /// Order type.
1062    #[serde(rename = "type")]
1063    pub order_type: BinanceFuturesOrderType,
1064    /// Original order type.
1065    #[serde(default)]
1066    pub orig_type: Option<BinanceFuturesOrderType>,
1067    /// Order side (BUY/SELL).
1068    pub side: BinanceSide,
1069    /// Position side (BOTH/LONG/SHORT).
1070    #[serde(default)]
1071    pub position_side: Option<BinancePositionSide>,
1072    /// Reduce-only flag.
1073    #[serde(default)]
1074    pub reduce_only: Option<bool>,
1075    /// Close position flag (for stop orders).
1076    #[serde(default)]
1077    pub close_position: Option<bool>,
1078    /// Trailing delta activation price.
1079    #[serde(default)]
1080    pub activate_price: Option<String>,
1081    /// Trailing callback rate.
1082    #[serde(default)]
1083    pub price_rate: Option<String>,
1084    /// Working type (CONTRACT_PRICE or MARK_PRICE).
1085    #[serde(default)]
1086    pub working_type: Option<BinanceWorkingType>,
1087    /// Whether price protection is enabled.
1088    #[serde(default)]
1089    pub price_protect: Option<bool>,
1090    /// Whether order uses isolated margin.
1091    #[serde(default)]
1092    pub is_isolated: Option<bool>,
1093    /// Good till date (for GTD orders).
1094    #[serde(default)]
1095    pub good_till_date: Option<i64>,
1096    /// Price match mode.
1097    #[serde(default)]
1098    pub price_match: Option<BinancePriceMatch>,
1099    /// Self-trade prevention mode.
1100    #[serde(default)]
1101    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
1102    /// Last update time.
1103    #[serde(default)]
1104    pub update_time: Option<i64>,
1105    /// Working order ID for tracking.
1106    #[serde(default)]
1107    pub working_type_id: Option<i64>,
1108}
1109
1110fn zero_decimal_string() -> String {
1111    "0".to_string()
1112}
1113
1114impl BinanceFuturesOrder {
1115    /// Converts this Binance order to a Nautilus [`OrderStatusReport`].
1116    ///
1117    /// # Errors
1118    ///
1119    /// Returns an error if client order ID, quantity, or price parsing fails.
1120    pub fn to_order_status_report(
1121        &self,
1122        account_id: AccountId,
1123        instrument_id: InstrumentId,
1124        price_precision: u8,
1125        size_precision: u8,
1126        treat_expired_as_canceled: bool,
1127        ts_init: UnixNanos,
1128    ) -> anyhow::Result<OrderStatusReport> {
1129        let ts_event = self
1130            .update_time
1131            .map(|value| parse_millis(value, "Futures order update time"))
1132            .transpose()?
1133            .unwrap_or(ts_init);
1134
1135        let client_order_id =
1136            decode_client_order_id(&self.client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1137        let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1138
1139        let order_side = match self.side {
1140            BinanceSide::Buy => OrderSide::Buy,
1141            BinanceSide::Sell => OrderSide::Sell,
1142        };
1143
1144        let order_type = self.order_type.to_nautilus_order_type();
1145        let time_in_force = self.time_in_force.to_nautilus_time_in_force();
1146        let order_status = self
1147            .status
1148            .to_nautilus_order_status(treat_expired_as_canceled);
1149
1150        let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
1151        let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
1152        let price = if self.price.is_empty() {
1153            None
1154        } else {
1155            let price: Decimal = self.price.parse().context("invalid price")?;
1156            if price == Decimal::ZERO {
1157                None
1158            } else {
1159                Some(
1160                    Price::from_decimal_dp(price, price_precision)
1161                        .context("invalid price precision")?,
1162                )
1163            }
1164        };
1165        let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1166
1167        let mut report = OrderStatusReport::new(
1168            account_id,
1169            instrument_id,
1170            Some(client_order_id),
1171            venue_order_id,
1172            order_side.into(),
1173            order_type,
1174            time_in_force,
1175            order_status,
1176            Quantity::from_decimal_dp(quantity, size_precision)
1177                .context("invalid orig_qty precision")?,
1178            Quantity::from_decimal_dp(filled_qty, size_precision)
1179                .context("invalid executed_qty precision")?,
1180            ts_event,
1181            ts_event,
1182            ts_init,
1183            Some(UUID4::new()),
1184        );
1185
1186        if let Some(price) = price {
1187            report = report.with_price(price);
1188        }
1189
1190        if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1191            report = report.with_expire_time(expire_time);
1192        }
1193
1194        report.avg_px = avg_px;
1195
1196        Ok(report)
1197    }
1198}
1199
1200impl BinanceFuturesOrderType {
1201    /// Returns whether this order type is post-only.
1202    #[must_use]
1203    pub fn is_post_only(&self) -> bool {
1204        false // Binance Futures doesn't have a dedicated post-only type
1205    }
1206
1207    /// Converts to Nautilus order type.
1208    #[must_use]
1209    pub fn to_nautilus_order_type(&self) -> OrderType {
1210        match self {
1211            Self::Market => OrderType::Market,
1212            Self::Limit => OrderType::Limit,
1213            Self::Stop => OrderType::StopLimit,
1214            Self::StopMarket => OrderType::StopMarket,
1215            Self::TakeProfit => OrderType::LimitIfTouched,
1216            Self::TakeProfitMarket => OrderType::MarketIfTouched,
1217            Self::TrailingStopMarket => OrderType::TrailingStopMarket,
1218            Self::Liquidation | Self::Adl => OrderType::Market, // Forced closes
1219            Self::Unknown => OrderType::Market,
1220        }
1221    }
1222}
1223
1224impl BinanceTimeInForce {
1225    /// Converts to Nautilus time in force.
1226    #[must_use]
1227    pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1228        match self {
1229            Self::Gtc => TimeInForce::Gtc,
1230            Self::Ioc => TimeInForce::Ioc,
1231            Self::Fok => TimeInForce::Fok,
1232            Self::Gtx => TimeInForce::Gtc, // GTX is GTC with post-only
1233            Self::Gtd => TimeInForce::Gtd,
1234            Self::Rpi => TimeInForce::Ioc, // RPI behaves as immediate
1235            Self::Unknown => TimeInForce::Gtc, // default
1236        }
1237    }
1238}
1239
1240impl BinanceOrderStatus {
1241    /// Converts to Nautilus order status.
1242    #[must_use]
1243    pub fn to_nautilus_order_status(&self, treat_expired_as_canceled: bool) -> OrderStatus {
1244        match self {
1245            Self::New | Self::PendingNew => OrderStatus::Accepted,
1246            Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1247            Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
1248            Self::Canceled => OrderStatus::Canceled,
1249            Self::PendingCancel => OrderStatus::PendingCancel,
1250            Self::Rejected => OrderStatus::Rejected,
1251            Self::Expired | Self::ExpiredInMatch => {
1252                if treat_expired_as_canceled {
1253                    OrderStatus::Canceled
1254                } else {
1255                    OrderStatus::Expired
1256                }
1257            }
1258            Self::Unknown => OrderStatus::Initialized,
1259        }
1260    }
1261}
1262
1263impl BinanceUserTrade {
1264    /// Converts this Binance trade to a Nautilus [`FillReport`].
1265    ///
1266    /// # Errors
1267    ///
1268    /// Returns an error if quantity or price parsing fails.
1269    pub fn to_fill_report(
1270        &self,
1271        account_id: AccountId,
1272        instrument_id: InstrumentId,
1273        price_precision: u8,
1274        size_precision: u8,
1275        bnfcr_currency: Currency,
1276        ts_init: UnixNanos,
1277    ) -> anyhow::Result<FillReport> {
1278        let ts_event = parse_millis(self.time, "Futures user trade time")?;
1279
1280        let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1281        let trade_id = TradeId::new(self.id.to_string());
1282
1283        let order_side = match self.side {
1284            BinanceSide::Buy => OrderSide::Buy,
1285            BinanceSide::Sell => OrderSide::Sell,
1286        };
1287
1288        let liquidity_side = if self.maker {
1289            LiquiditySide::Maker
1290        } else {
1291            LiquiditySide::Taker
1292        };
1293
1294        let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1295        let last_px: Decimal = self.price.parse().context("invalid price")?;
1296
1297        let commission_currency = self
1298            .commission_asset
1299            .as_ref()
1300            .map_or(bnfcr_currency, |asset| {
1301                normalize_futures_asset(asset, bnfcr_currency)
1302            });
1303        let commission = match self.commission.as_ref() {
1304            Some(raw) => {
1305                let decimal = parse_required_decimal(raw, "commission")?;
1306                Money::from_decimal(decimal, commission_currency)?
1307            }
1308            None => Money::zero(commission_currency),
1309        };
1310
1311        Ok(FillReport::new(
1312            account_id,
1313            instrument_id,
1314            venue_order_id,
1315            trade_id,
1316            order_side,
1317            Quantity::from_decimal_dp(last_qty, size_precision).context("invalid qty precision")?,
1318            Price::from_decimal_dp(last_px, price_precision).context("invalid price precision")?,
1319            commission,
1320            liquidity_side,
1321            None, // client_order_id
1322            None, // venue_position_id
1323            ts_event,
1324            ts_init,
1325            Some(UUID4::new()),
1326        ))
1327    }
1328}
1329
1330/// Result of a single order in a batch operation.
1331///
1332/// Each item in a batch response can be either a success or an error.
1333#[derive(Clone, Debug, Deserialize)]
1334#[serde(untagged)]
1335pub enum BatchOrderResult {
1336    /// Successful order operation.
1337    Success(Box<BinanceFuturesOrder>),
1338    /// Failed order operation.
1339    Error(BatchOrderError),
1340}
1341
1342/// Error in a batch order response.
1343#[derive(Clone, Debug, Deserialize)]
1344pub struct BatchOrderError {
1345    /// Error code from Binance.
1346    pub code: i64,
1347    /// Error message.
1348    pub msg: String,
1349}
1350
1351/// Listen key response from user data stream endpoints.
1352#[derive(Debug, Clone, Deserialize)]
1353#[serde(rename_all = "camelCase")]
1354pub struct ListenKeyResponse {
1355    /// The listen key for WebSocket user data stream.
1356    pub listen_key: String,
1357}
1358
1359/// Algo order response from Binance Futures Algo Service API.
1360///
1361/// Algo orders are conditional orders (STOP_MARKET, STOP_LIMIT, TAKE_PROFIT,
1362/// TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET) that are managed by Binance's
1363/// Algo Service rather than the traditional order matching engine.
1364///
1365/// # References
1366///
1367/// - <https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Algo-Order>
1368#[derive(Clone, Debug, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct BinanceFuturesAlgoOrder {
1371    /// Unique algo order ID assigned by Binance.
1372    pub algo_id: i64,
1373    /// Client-specified algo order ID for idempotency.
1374    pub client_algo_id: String,
1375    /// Algo type (currently only `Conditional` is supported).
1376    pub algo_type: BinanceAlgoType,
1377    /// Order type (STOP_MARKET, STOP, TAKE_PROFIT, TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET).
1378    #[serde(rename = "orderType", alias = "type")]
1379    pub order_type: BinanceFuturesOrderType,
1380    /// Trading symbol.
1381    pub symbol: Ustr,
1382    /// Order side (BUY/SELL).
1383    pub side: BinanceSide,
1384    /// Position side (BOTH, LONG, SHORT).
1385    #[serde(default)]
1386    pub position_side: Option<BinancePositionSide>,
1387    /// Time in force.
1388    #[serde(default)]
1389    pub time_in_force: Option<BinanceTimeInForce>,
1390    /// Order quantity.
1391    #[serde(default)]
1392    pub quantity: Option<String>,
1393    /// Algo order status.
1394    #[serde(default)]
1395    pub algo_status: Option<BinanceAlgoStatus>,
1396    /// Trigger price for the conditional order.
1397    #[serde(default)]
1398    pub trigger_price: Option<String>,
1399    /// Limit price (for STOP/TAKE_PROFIT limit orders).
1400    #[serde(default)]
1401    pub price: Option<String>,
1402    /// Working type for trigger price calculation (CONTRACT_PRICE or MARK_PRICE).
1403    #[serde(default)]
1404    pub working_type: Option<BinanceWorkingType>,
1405    /// Close all position flag.
1406    #[serde(default)]
1407    pub close_position: Option<bool>,
1408    /// Price protection enabled.
1409    #[serde(default)]
1410    pub price_protect: Option<bool>,
1411    /// Reduce-only flag.
1412    #[serde(default)]
1413    pub reduce_only: Option<bool>,
1414    /// Activation price for TRAILING_STOP_MARKET orders.
1415    #[serde(default)]
1416    pub activate_price: Option<String>,
1417    /// Callback rate for TRAILING_STOP_MARKET orders (0.1 to 10, where 1 = 1%).
1418    #[serde(default)]
1419    pub callback_rate: Option<String>,
1420    /// Good till date in milliseconds.
1421    #[serde(default)]
1422    pub good_till_date: Option<i64>,
1423    /// Order creation time in milliseconds.
1424    #[serde(default)]
1425    pub create_time: Option<i64>,
1426    /// Last update time in milliseconds.
1427    #[serde(default)]
1428    pub update_time: Option<i64>,
1429    /// Trigger time in milliseconds (when the algo order triggered).
1430    #[serde(default)]
1431    pub trigger_time: Option<i64>,
1432    /// Order ID in matching engine (populated when algo order is triggered).
1433    #[serde(default)]
1434    pub actual_order_id: Option<String>,
1435    /// Executed quantity in matching engine (populated when algo order is triggered).
1436    #[serde(default, rename = "actualQty", alias = "executedQty")]
1437    pub executed_qty: Option<String>,
1438    /// Average fill price in matching engine (populated when algo order is triggered).
1439    #[serde(default, rename = "actualPrice", alias = "avgPrice")]
1440    pub avg_price: Option<String>,
1441}
1442
1443impl BinanceFuturesAlgoOrder {
1444    /// Converts this Binance algo order to a Nautilus [`OrderStatusReport`].
1445    ///
1446    /// # Errors
1447    ///
1448    /// Returns an error if client order ID, quantity, price, trigger, or trailing fields cannot be
1449    /// parsed.
1450    pub fn to_order_status_report(
1451        &self,
1452        account_id: AccountId,
1453        instrument_id: InstrumentId,
1454        price_precision: u8,
1455        size_precision: u8,
1456        ts_init: UnixNanos,
1457    ) -> anyhow::Result<OrderStatusReport> {
1458        let ts_event = self
1459            .update_time
1460            .or(self.create_time)
1461            .map(|value| parse_millis(value, "Futures algo order time"))
1462            .transpose()?
1463            .unwrap_or(ts_init);
1464
1465        let client_order_id =
1466            decode_client_order_id(&self.client_algo_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1467        let venue_order_id = self
1468            .actual_order_id
1469            .as_ref()
1470            .filter(|id| !id.is_empty())
1471            .map_or_else(
1472                || VenueOrderId::new(self.algo_id.to_string()),
1473                |id| VenueOrderId::new(id.clone()),
1474            );
1475
1476        let order_side = match self.side {
1477            BinanceSide::Buy => OrderSide::Buy,
1478            BinanceSide::Sell => OrderSide::Sell,
1479        };
1480
1481        let order_type = self.parse_order_type();
1482        let time_in_force = self
1483            .time_in_force
1484            .as_ref()
1485            .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1486        let order_status = self.parse_order_status();
1487
1488        let quantity: Decimal = self
1489            .quantity
1490            .as_ref()
1491            .map_or(Ok(Decimal::ZERO), |q| q.parse())
1492            .context("invalid quantity")?;
1493        let filled_qty: Decimal = self
1494            .executed_qty
1495            .as_ref()
1496            .map_or(Ok(Decimal::ZERO), |q| q.parse())
1497            .context("invalid executed_qty")?;
1498        let price = if let Some(price) = self.price.as_ref().filter(|price| !price.is_empty()) {
1499            let price: Decimal = price.parse().context("invalid price")?;
1500            if price == Decimal::ZERO {
1501                None
1502            } else {
1503                Some(
1504                    Price::from_decimal_dp(price, price_precision)
1505                        .context("invalid price precision")?,
1506                )
1507            }
1508        } else {
1509            None
1510        };
1511        let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1512        let trigger_price = self.parse_trigger_price(price_precision)?;
1513        let trailing_offset = self.parse_trailing_offset()?;
1514
1515        let mut report = OrderStatusReport::new(
1516            account_id,
1517            instrument_id,
1518            Some(client_order_id),
1519            venue_order_id,
1520            order_side.into(),
1521            order_type,
1522            time_in_force,
1523            order_status,
1524            Quantity::from_decimal_dp(quantity, size_precision)
1525                .context("invalid quantity precision")?,
1526            Quantity::from_decimal_dp(filled_qty, size_precision)
1527                .context("invalid executed_qty precision")?,
1528            ts_event,
1529            ts_event,
1530            ts_init,
1531            Some(UUID4::new()),
1532        );
1533
1534        if let Some(price) = price {
1535            report = report.with_price(price);
1536        }
1537
1538        report.avg_px = avg_px;
1539
1540        if let Some(trigger_price) = trigger_price {
1541            report = report
1542                .with_trigger_price(trigger_price)
1543                .with_trigger_type(parse_working_type(self.working_type));
1544        }
1545
1546        if let Some(trailing_offset) = trailing_offset {
1547            report = report
1548                .with_trailing_offset(trailing_offset)
1549                .with_trailing_offset_type(TrailingOffsetType::BasisPoints);
1550        }
1551
1552        if let Some(activation_price) = self
1553            .activate_price
1554            .as_deref()
1555            .map(|price| {
1556                parse_positive_price_at_precision(price, price_precision, "activate_price")
1557            })
1558            .transpose()?
1559            .flatten()
1560        {
1561            report = report.with_activation_price(activation_price);
1562        }
1563
1564        if self.reduce_only == Some(true) || self.close_position == Some(true) {
1565            report = report.with_reduce_only(true);
1566        }
1567
1568        if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1569            report = report.with_expire_time(expire_time);
1570        }
1571
1572        if let Some(trigger_time) = self.trigger_time {
1573            report =
1574                report.with_ts_triggered(parse_millis(trigger_time, "Futures algo trigger time")?);
1575        }
1576
1577        Ok(report)
1578    }
1579
1580    /// Converts this algo order to a report enriched with matching-engine execution details.
1581    ///
1582    /// The algo order remains the source of client identity and conditional-order metadata.
1583    /// The matching-engine order is authoritative for status, quantity, fills, average price,
1584    /// venue order identity, and the last update time.
1585    ///
1586    /// # Errors
1587    ///
1588    /// Returns an error if the matching-engine order does not match this algo order or either
1589    /// response contains invalid report data.
1590    #[expect(clippy::too_many_arguments)]
1591    pub fn to_order_status_report_with_actual(
1592        &self,
1593        actual: &BinanceFuturesOrder,
1594        account_id: AccountId,
1595        instrument_id: InstrumentId,
1596        price_precision: u8,
1597        size_precision: u8,
1598        treat_expired_as_canceled: bool,
1599        ts_init: UnixNanos,
1600    ) -> anyhow::Result<OrderStatusReport> {
1601        let expected_actual_order_id = self
1602            .actual_order_id
1603            .as_deref()
1604            .filter(|id| !id.is_empty())
1605            .context("algo order has no actual_order_id")?;
1606
1607        if expected_actual_order_id != actual.order_id.to_string() {
1608            anyhow::bail!(
1609                "actual order ID mismatch: expected {expected_actual_order_id}, was {}",
1610                actual.order_id
1611            );
1612        }
1613
1614        if self.symbol != actual.symbol {
1615            anyhow::bail!(
1616                "actual order symbol mismatch: expected {}, was {}",
1617                self.symbol,
1618                actual.symbol
1619            );
1620        }
1621
1622        if self.side != actual.side {
1623            anyhow::bail!(
1624                "actual order side mismatch: expected {:?}, was {:?}",
1625                self.side,
1626                actual.side
1627            );
1628        }
1629
1630        let mut report = self.to_order_status_report(
1631            account_id,
1632            instrument_id,
1633            price_precision,
1634            size_precision,
1635            ts_init,
1636        )?;
1637        let actual_report = actual.to_order_status_report(
1638            account_id,
1639            instrument_id,
1640            price_precision,
1641            size_precision,
1642            treat_expired_as_canceled,
1643            ts_init,
1644        )?;
1645        report.venue_order_id = actual_report.venue_order_id;
1646        report.order_status = actual_report.order_status;
1647        report.quantity = actual_report.quantity;
1648        report.filled_qty = actual_report.filled_qty;
1649        report.avg_px = actual_report.avg_px.or(report.avg_px);
1650        report.expire_time = report.expire_time.or(actual_report.expire_time);
1651        report.ts_last = actual_report.ts_last;
1652
1653        Ok(report)
1654    }
1655
1656    fn parse_trigger_price(&self, price_precision: u8) -> anyhow::Result<Option<Price>> {
1657        let raw_trigger_price = match self.order_type {
1658            BinanceFuturesOrderType::TrailingStopMarket => self
1659                .trigger_price
1660                .as_deref()
1661                .or(self.activate_price.as_deref()),
1662            _ => self.trigger_price.as_deref(),
1663        };
1664        let trigger_price = raw_trigger_price
1665            .map(|price| parse_positive_price_at_precision(price, price_precision, "trigger_price"))
1666            .transpose()?
1667            .flatten();
1668
1669        if trigger_price.is_none() && requires_algo_trigger_price(self.order_type) {
1670            anyhow::bail!(
1671                "missing positive trigger_price for Binance algo order type {:?}",
1672                self.order_type
1673            );
1674        }
1675
1676        Ok(trigger_price)
1677    }
1678
1679    fn parse_trailing_offset(&self) -> anyhow::Result<Option<Decimal>> {
1680        if self.order_type != BinanceFuturesOrderType::TrailingStopMarket {
1681            return Ok(None);
1682        }
1683
1684        self.callback_rate
1685            .as_deref()
1686            .map(parse_callback_rate_basis_points)
1687            .transpose()
1688            .map(Option::flatten)
1689    }
1690
1691    fn parse_order_type(&self) -> OrderType {
1692        self.order_type.into()
1693    }
1694
1695    fn parse_order_status(&self) -> OrderStatus {
1696        match self.algo_status {
1697            Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1698            Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1699            Some(BinanceAlgoStatus::Triggered) => self
1700                .executed_qty
1701                .as_deref()
1702                .and_then(|qty| qty.parse::<Decimal>().ok())
1703                .filter(|qty| *qty > Decimal::ZERO)
1704                .map_or(OrderStatus::Accepted, |_| OrderStatus::PartiallyFilled),
1705            Some(BinanceAlgoStatus::Finished) => {
1706                let executed_qty = self
1707                    .executed_qty
1708                    .as_deref()
1709                    .and_then(|qty| qty.parse::<Decimal>().ok());
1710                let quantity = self
1711                    .quantity
1712                    .as_deref()
1713                    .and_then(|qty| qty.parse::<Decimal>().ok());
1714                match (executed_qty, quantity) {
1715                    (Some(actual), Some(total)) if total > Decimal::ZERO && actual >= total => {
1716                        OrderStatus::Filled
1717                    }
1718                    _ => OrderStatus::Canceled,
1719                }
1720            }
1721            Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1722            Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1723            Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1724            Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1725        }
1726    }
1727}
1728
1729fn parse_avg_px(
1730    raw: Option<&str>,
1731    filled_qty: Decimal,
1732    price_precision: u8,
1733) -> anyhow::Result<Option<Decimal>> {
1734    if filled_qty <= Decimal::ZERO {
1735        return Ok(None);
1736    }
1737
1738    raw.filter(|price| !price.is_empty())
1739        .map(|price| parse_positive_price_at_precision(price, price_precision, "avg_price"))
1740        .transpose()
1741        .map(|price| price.flatten().map(|price| price.as_decimal()))
1742}
1743
1744fn parse_positive_price_at_precision(
1745    raw: &str,
1746    precision: u8,
1747    field: &str,
1748) -> anyhow::Result<Option<Price>> {
1749    let decimal = parse_required_decimal(raw, field)?;
1750    if decimal <= Decimal::ZERO {
1751        return Ok(None);
1752    }
1753
1754    Price::from_decimal_dp(decimal, precision)
1755        .map(Some)
1756        .map_err(|e| anyhow::anyhow!("invalid {field} precision: {e}"))
1757}
1758
1759fn parse_callback_rate_basis_points(raw: &str) -> anyhow::Result<Option<Decimal>> {
1760    let rate = parse_required_decimal(raw, "callback_rate")?;
1761    if rate <= Decimal::ZERO {
1762        return Ok(None);
1763    }
1764
1765    rate.checked_mul(Decimal::from(100))
1766        .map(Some)
1767        .ok_or_else(|| anyhow::anyhow!("invalid callback_rate='{raw}': multiplication overflow"))
1768}
1769
1770fn parse_working_type(working_type: Option<BinanceWorkingType>) -> TriggerType {
1771    match working_type {
1772        Some(BinanceWorkingType::ContractPrice) => TriggerType::LastPrice,
1773        Some(BinanceWorkingType::MarkPrice) => TriggerType::MarkPrice,
1774        Some(BinanceWorkingType::Unknown) | None => TriggerType::Default,
1775    }
1776}
1777
1778fn requires_algo_trigger_price(order_type: BinanceFuturesOrderType) -> bool {
1779    matches!(
1780        order_type,
1781        BinanceFuturesOrderType::Stop
1782            | BinanceFuturesOrderType::StopMarket
1783            | BinanceFuturesOrderType::TakeProfit
1784            | BinanceFuturesOrderType::TakeProfitMarket
1785            | BinanceFuturesOrderType::TrailingStopMarket
1786    )
1787}
1788
1789/// Cancel response for algo orders from Binance Futures Algo Service API.
1790#[derive(Clone, Debug, Deserialize)]
1791#[serde(rename_all = "camelCase")]
1792pub struct BinanceFuturesAlgoOrderCancelResponse {
1793    /// Algo order ID that was canceled.
1794    pub algo_id: i64,
1795    /// Client algo order ID.
1796    pub client_algo_id: String,
1797    /// Response code (200 for success).
1798    pub code: String,
1799    /// Response message.
1800    pub msg: String,
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805    use nautilus_model::identifiers::ClientOrderId;
1806    use rstest::rstest;
1807    use rust_decimal_macros::dec;
1808
1809    use super::*;
1810    use crate::common::testing::load_fixture_string;
1811
1812    #[rstest]
1813    fn test_parse_account_info_v2() {
1814        let json = load_fixture_string("futures/http_json/account_info_v2.json");
1815        let account: BinanceFuturesAccountInfo =
1816            serde_json::from_str(&json).expect("Failed to parse account info");
1817
1818        assert_eq!(
1819            account.total_wallet_balance,
1820            Some(Decimal::from_str_exact("23.72469206").unwrap())
1821        );
1822        assert_eq!(account.assets.len(), 1);
1823        assert_eq!(account.assets[0].asset.as_str(), "USDT");
1824        assert_eq!(
1825            account.assets[0].wallet_balance,
1826            Decimal::from_str_exact("23.72469206").unwrap()
1827        );
1828        assert_eq!(account.positions.len(), 1);
1829        assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1830        assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1831    }
1832
1833    #[rstest]
1834    fn test_account_info_to_account_state_zero_margins() {
1835        let json = load_fixture_string("futures/http_json/account_info_v2.json");
1836        let account: BinanceFuturesAccountInfo =
1837            serde_json::from_str(&json).expect("Failed to parse account info");
1838
1839        let account_id = AccountId::from("BINANCE-001");
1840        let ts_init = UnixNanos::from(1_000_000_000u64);
1841        let state = account.to_account_state(account_id, ts_init).unwrap();
1842
1843        assert_eq!(state.account_id, account_id);
1844        assert_eq!(state.account_type, AccountType::Margin);
1845        assert!(!state.balances.is_empty());
1846        assert_eq!(state.margins.len(), 0);
1847    }
1848
1849    #[rstest]
1850    fn test_account_info_to_account_state_with_margins() {
1851        let json = r#"{
1852            "totalInitialMargin": "500.25000000",
1853            "totalMaintMargin": "250.75000000",
1854            "totalWalletBalance": "10000.00000000",
1855            "assets": [{
1856                "asset": "USDT",
1857                "walletBalance": "10000.00000000",
1858                "availableBalance": "9500.00000000",
1859                "initialMargin": "500.25000000",
1860                "maintMargin": "250.75000000",
1861                "updateTime": 1617939110373
1862            }],
1863            "positions": []
1864        }"#;
1865        let account: BinanceFuturesAccountInfo =
1866            serde_json::from_str(json).expect("Failed to parse account info");
1867
1868        let account_id = AccountId::from("BINANCE-001");
1869        let ts_init = UnixNanos::from(1_000_000_000u64);
1870        let state = account.to_account_state(account_id, ts_init).unwrap();
1871
1872        assert_eq!(state.margins.len(), 1);
1873        let margin = &state.margins[0];
1874        assert!(margin.instrument_id.is_none());
1875        assert_eq!(margin.currency.code.as_str(), "USDT");
1876        assert_eq!(margin.initial.as_f64(), 500.25);
1877        assert_eq!(margin.maintenance.as_f64(), 250.75);
1878    }
1879
1880    #[rstest]
1881    fn test_account_info_to_account_state_coin_margined_per_base_coin() {
1882        let json = r#"{
1883            "totalWalletBalance": "0.00000000",
1884            "assets": [
1885                {
1886                    "asset": "BTC",
1887                    "walletBalance": "1.50000000",
1888                    "availableBalance": "1.40000000",
1889                    "initialMargin": "0.05000000",
1890                    "maintMargin": "0.02500000",
1891                    "updateTime": 1617939110373
1892                },
1893                {
1894                    "asset": "ETH",
1895                    "walletBalance": "10.00000000",
1896                    "availableBalance": "9.00000000",
1897                    "initialMargin": "0.80000000",
1898                    "maintMargin": "0.40000000",
1899                    "updateTime": 1617939110373
1900                }
1901            ],
1902            "positions": []
1903        }"#;
1904        let account: BinanceFuturesAccountInfo =
1905            serde_json::from_str(json).expect("Failed to parse account info");
1906
1907        let account_id = AccountId::from("BINANCE-001");
1908        let ts_init = UnixNanos::from(1_000_000_000u64);
1909        let state = account.to_account_state(account_id, ts_init).unwrap();
1910
1911        assert_eq!(state.margins.len(), 2);
1912        assert!(state.margins.iter().all(|m| m.instrument_id.is_none()));
1913        let btc = state
1914            .margins
1915            .iter()
1916            .find(|m| m.currency.code.as_str() == "BTC")
1917            .expect("BTC margin missing");
1918        assert_eq!(btc.initial.as_f64(), 0.05);
1919        assert_eq!(btc.maintenance.as_f64(), 0.025);
1920        let eth = state
1921            .margins
1922            .iter()
1923            .find(|m| m.currency.code.as_str() == "ETH")
1924            .expect("ETH margin missing");
1925        assert_eq!(eth.initial.as_f64(), 0.8);
1926        assert_eq!(eth.maintenance.as_f64(), 0.4);
1927    }
1928
1929    // Regression for the #3867 bug class: wire values with more decimal places
1930    // than the currency precision (USDT=8) previously tripped the
1931    // `total == locked + free` invariant when Money::new rounded each side
1932    // independently. The `from_total_and_free` helper must keep the invariant.
1933    #[rstest]
1934    fn test_account_info_to_account_state_precision_drift() {
1935        let json = r#"{
1936            "assets": [{
1937                "asset": "USDT",
1938                "walletBalance": "10.000000034999",
1939                "availableBalance": "9.999999994999",
1940                "updateTime": 1617939110373
1941            }],
1942            "positions": []
1943        }"#;
1944        let account: BinanceFuturesAccountInfo =
1945            serde_json::from_str(json).expect("Failed to parse account info");
1946
1947        let account_id = AccountId::from("BINANCE-001");
1948        let ts_init = UnixNanos::from(1_000_000_000u64);
1949        let state = account.to_account_state(account_id, ts_init).unwrap();
1950
1951        assert_eq!(state.balances.len(), 1);
1952        let balance = &state.balances[0];
1953        assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
1954    }
1955
1956    #[rstest]
1957    fn test_account_info_to_account_state_empty_balance() {
1958        // Empty strings for balance fields (inactive/zero-balance accounts)
1959        let json = r#"{
1960            "assets": [{
1961                "asset": "USDT",
1962                "walletBalance": "",
1963                "availableBalance": "",
1964                "updateTime": 0
1965            }],
1966            "positions": []
1967        }"#;
1968        let account: BinanceFuturesAccountInfo =
1969            serde_json::from_str(json).expect("Failed to parse account info");
1970
1971        let account_id = AccountId::from("BINANCE-001");
1972        let ts_init = UnixNanos::from(1_000_000_000u64);
1973        let state = account.to_account_state(account_id, ts_init).unwrap();
1974
1975        assert_eq!(state.balances.len(), 1);
1976        let balance = &state.balances[0];
1977        assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1978        assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
1979        assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
1980    }
1981
1982    #[rstest]
1983    fn test_account_info_to_account_state_empty_assets() {
1984        // No assets at all (completely empty account)
1985        let json = r#"{
1986            "assets": [],
1987            "positions": []
1988        }"#;
1989        let account: BinanceFuturesAccountInfo =
1990            serde_json::from_str(json).expect("Failed to parse account info");
1991
1992        let account_id = AccountId::from("BINANCE-001");
1993        let ts_init = UnixNanos::from(1_000_000_000u64);
1994        let state = account.to_account_state(account_id, ts_init).unwrap();
1995
1996        assert_eq!(state.balances.len(), 1);
1997        let balance = &state.balances[0];
1998        assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1999    }
2000
2001    #[rstest]
2002    fn test_parse_position_risk() {
2003        let json = load_fixture_string("futures/http_json/position_risk.json");
2004        let positions: Vec<BinancePositionRisk> =
2005            serde_json::from_str(&json).expect("Failed to parse position risk");
2006
2007        assert_eq!(positions.len(), 1);
2008        assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
2009        assert_eq!(positions[0].position_amt, "0.001");
2010        assert_eq!(positions[0].mark_price, "51000.0");
2011        assert_eq!(positions[0].leverage, "20");
2012    }
2013
2014    #[rstest]
2015    fn test_parse_balance_with_v1_field() {
2016        // V1 uses 'balance' field
2017        let json = load_fixture_string("futures/http_json/balance.json");
2018        let balances: Vec<BinanceFuturesBalance> =
2019            serde_json::from_str(&json).expect("Failed to parse balance");
2020
2021        assert_eq!(balances.len(), 1);
2022        assert_eq!(balances[0].asset.as_str(), "USDT");
2023        // Uses alias to parse 'balance' into wallet_balance
2024        assert_eq!(
2025            balances[0].wallet_balance,
2026            Decimal::from_str_exact("122.12345678").unwrap()
2027        );
2028        assert_eq!(
2029            balances[0].available_balance,
2030            Decimal::from_str_exact("122.12345678").unwrap()
2031        );
2032    }
2033
2034    #[rstest]
2035    fn test_parse_balance_with_v2_field() {
2036        // V2 uses 'walletBalance' field
2037        let json = r#"{
2038            "asset": "USDT",
2039            "walletBalance": "100.00000000",
2040            "availableBalance": "100.00000000",
2041            "updateTime": 1617939110373
2042        }"#;
2043
2044        let balance: BinanceFuturesBalance =
2045            serde_json::from_str(json).expect("Failed to parse balance");
2046
2047        assert_eq!(balance.asset.as_str(), "USDT");
2048        assert_eq!(
2049            balance.wallet_balance,
2050            Decimal::from_str_exact("100.00000000").unwrap()
2051        );
2052    }
2053
2054    #[rstest]
2055    fn test_parse_order() {
2056        let json = load_fixture_string("futures/http_json/order_response.json");
2057        let order: BinanceFuturesOrder =
2058            serde_json::from_str(&json).expect("Failed to parse order");
2059
2060        assert_eq!(order.order_id, 12345678);
2061        assert_eq!(order.symbol.as_str(), "BTCUSDT");
2062        assert_eq!(order.status, BinanceOrderStatus::New);
2063        assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
2064        assert_eq!(order.side, BinanceSide::Buy);
2065        assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
2066        assert_eq!(order.price_match, Some(BinancePriceMatch::None));
2067        assert_eq!(
2068            order.self_trade_prevention_mode,
2069            Some(BinanceSelfTradePreventionMode::None)
2070        );
2071    }
2072
2073    #[rstest]
2074    fn test_parse_order_defaults_missing_cum_quote_to_zero() {
2075        let json = load_fixture_string("futures/http_json/order_response.json");
2076        let mut value: Value = serde_json::from_str(&json).expect("Failed to parse order fixture");
2077
2078        value
2079            .as_object_mut()
2080            .expect("Order fixture should be a JSON object")
2081            .remove("cumQuote");
2082
2083        let order: BinanceFuturesOrder =
2084            serde_json::from_value(value).expect("Failed to parse order");
2085
2086        assert_eq!(order.cum_quote, "0");
2087    }
2088
2089    #[rstest]
2090    fn test_parse_kline_rejects_non_string_price() {
2091        let value = serde_json::json!([
2092            1_625_474_304_000_i64,
2093            50000.00,
2094            "51000.00",
2095            "49000.00",
2096            "50500.00",
2097            "12.5",
2098            1_625_474_364_000_i64,
2099            "631250.00",
2100            100_i64,
2101            "6.2",
2102            "313100.00"
2103        ]);
2104
2105        let error = serde_json::from_value::<BinanceFuturesKline>(value)
2106            .unwrap_err()
2107            .to_string();
2108
2109        assert!(error.contains("open"));
2110    }
2111
2112    #[rstest]
2113    fn test_parse_hedge_mode_response() {
2114        let json = r#"{"dualSidePosition": true}"#;
2115        let response: BinanceHedgeModeResponse =
2116            serde_json::from_str(json).expect("Failed to parse hedge mode");
2117        assert!(response.dual_side_position);
2118    }
2119
2120    #[rstest]
2121    fn test_parse_leverage_response() {
2122        let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
2123        let response: BinanceLeverageResponse =
2124            serde_json::from_str(json).expect("Failed to parse leverage");
2125        assert_eq!(response.symbol.as_str(), "BTCUSDT");
2126        assert_eq!(response.leverage, 20);
2127    }
2128
2129    #[rstest]
2130    fn test_parse_listen_key_response() {
2131        let json =
2132            r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
2133        let response: ListenKeyResponse =
2134            serde_json::from_str(json).expect("Failed to parse listen key");
2135        assert!(!response.listen_key.is_empty());
2136    }
2137
2138    #[rstest]
2139    fn test_parse_account_position() {
2140        let json = r#"{
2141            "symbol": "ETHUSDT",
2142            "initialMargin": "100.00",
2143            "maintMargin": "50.00",
2144            "unrealizedProfit": "10.00",
2145            "positionInitialMargin": "100.00",
2146            "openOrderInitialMargin": "0",
2147            "leverage": "10",
2148            "isolated": true,
2149            "entryPrice": "2000.00",
2150            "maxNotional": "100000",
2151            "bidNotional": "0",
2152            "askNotional": "0",
2153            "positionSide": "LONG",
2154            "positionAmt": "0.5",
2155            "updateTime": 1625474304765
2156        }"#;
2157
2158        let position: BinanceAccountPosition =
2159            serde_json::from_str(json).expect("Failed to parse account position");
2160
2161        assert_eq!(position.symbol.as_str(), "ETHUSDT");
2162        assert_eq!(position.leverage, Some("10".to_string()));
2163        assert_eq!(position.isolated, Some(true));
2164        assert_eq!(position.position_side, Some(BinancePositionSide::Long));
2165    }
2166
2167    #[rstest]
2168    fn test_parse_algo_order() {
2169        let json = r#"{
2170            "algoId": 123456789,
2171            "clientAlgoId": "test-algo-order-1",
2172            "algoType": "CONDITIONAL",
2173            "type": "STOP_MARKET",
2174            "symbol": "BTCUSDT",
2175            "side": "BUY",
2176            "positionSide": "BOTH",
2177            "timeInForce": "GTC",
2178            "quantity": "0.001",
2179            "algoStatus": "NEW",
2180            "triggerPrice": "45000.00",
2181            "workingType": "MARK_PRICE",
2182            "reduceOnly": false,
2183            "createTime": 1625474304765,
2184            "updateTime": 1625474304765
2185        }"#;
2186
2187        let order: BinanceFuturesAlgoOrder =
2188            serde_json::from_str(json).expect("Failed to parse algo order");
2189
2190        assert_eq!(order.algo_id, 123456789);
2191        assert_eq!(order.client_algo_id, "test-algo-order-1");
2192        assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
2193        assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2194        assert_eq!(order.symbol.as_str(), "BTCUSDT");
2195        assert_eq!(order.side, BinanceSide::Buy);
2196        assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
2197        assert_eq!(order.trigger_price, Some("45000.00".to_string()));
2198    }
2199
2200    #[rstest]
2201    #[case("actualQty", "actualPrice")]
2202    #[case("executedQty", "avgPrice")]
2203    fn test_parse_algo_order_finished(#[case] quantity_field: &str, #[case] price_field: &str) {
2204        let json = load_fixture_string("futures/http_json/algo_order_response.json")
2205            .replace("actualQty", quantity_field)
2206            .replace("actualPrice", price_field);
2207
2208        let order: BinanceFuturesAlgoOrder =
2209            serde_json::from_str(&json).expect("Failed to parse finished algo order");
2210
2211        assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Finished));
2212        assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2213        assert_eq!(order.actual_order_id, Some("987654321".to_string()));
2214        assert_eq!(order.executed_qty, Some("0.001".to_string()));
2215        assert_eq!(order.avg_price, Some("50000.00".to_string()));
2216    }
2217
2218    #[rstest]
2219    fn test_parse_algo_order_cancel_response() {
2220        let json = r#"{
2221            "algoId": 123456789,
2222            "clientAlgoId": "test-algo-order-1",
2223            "code": "200",
2224            "msg": "success"
2225        }"#;
2226
2227        let response: BinanceFuturesAlgoOrderCancelResponse =
2228            serde_json::from_str(json).expect("Failed to parse algo cancel response");
2229
2230        assert_eq!(response.algo_id, 123456789);
2231        assert_eq!(response.client_algo_id, "test-algo-order-1");
2232        assert_eq!(response.code, "200");
2233        assert_eq!(response.msg, "success");
2234    }
2235
2236    #[rstest]
2237    fn test_order_to_report_decodes_broker_id() {
2238        let json = r#"{
2239            "orderId": 12345678,
2240            "symbol": "BTCUSDT",
2241            "status": "NEW",
2242            "clientOrderId": "x-aHRE4BCj-T0000000000000",
2243            "price": "50000.00",
2244            "avgPrice": "0.00",
2245            "origQty": "0.001",
2246            "executedQty": "0.000",
2247            "cumQuote": "0.00",
2248            "timeInForce": "GTC",
2249            "type": "LIMIT",
2250            "reduceOnly": false,
2251            "closePosition": false,
2252            "side": "BUY",
2253            "positionSide": "BOTH",
2254            "stopPrice": "0.00",
2255            "workingType": "CONTRACT_PRICE",
2256            "priceProtect": false,
2257            "origType": "LIMIT",
2258            "priceMatch": "NONE",
2259            "selfTradePreventionMode": "NONE",
2260            "goodTillDate": 0,
2261            "time": 1625474304765,
2262            "updateTime": 1625474304765
2263        }"#;
2264
2265        let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
2266        let account_id = AccountId::from("BINANCE-FUTURES-001");
2267        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2268        let ts_init = UnixNanos::from(1_000_000_000u64);
2269
2270        let report = order
2271            .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2272            .unwrap();
2273
2274        assert_eq!(
2275            report.client_order_id,
2276            Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
2277        );
2278        assert_eq!(report.price, Some(Price::from("50000.00")));
2279    }
2280
2281    #[rstest]
2282    fn test_order_to_report_rejects_invalid_client_order_id() {
2283        let mut order = order_with_price("50000.00");
2284        order.client_order_id = String::new();
2285
2286        let result = order.to_order_status_report(
2287            AccountId::from("BINANCE-FUTURES-001"),
2288            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2289            2,
2290            3,
2291            false,
2292            UnixNanos::from(1_000_000_000u64),
2293        );
2294
2295        assert_eq!(
2296            result.unwrap_err().to_string(),
2297            "invalid Binance client order ID ''"
2298        );
2299    }
2300
2301    #[rstest]
2302    #[case("0")]
2303    #[case("")]
2304    fn test_order_to_report_omits_missing_price(#[case] price: &str) {
2305        let order = order_with_price(price);
2306        let account_id = AccountId::from("BINANCE-FUTURES-001");
2307        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2308        let ts_init = UnixNanos::from(1_000_000_000u64);
2309
2310        let report = order
2311            .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2312            .unwrap();
2313
2314        assert_eq!(report.price, None);
2315    }
2316
2317    #[rstest]
2318    fn test_order_to_report_sets_avg_px_for_filled_market_order() {
2319        let mut order = order_with_price("0");
2320        order.status = BinanceOrderStatus::Filled;
2321        order.executed_qty = "0.001".to_string();
2322        order.cum_quote = "50.00".to_string();
2323        order.avg_price = Some("50000.00".to_string());
2324        let account_id = AccountId::from("BINANCE-FUTURES-001");
2325        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2326        let ts_init = UnixNanos::from(1_000_000_000u64);
2327
2328        let report = order
2329            .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2330            .unwrap();
2331
2332        assert_eq!(report.price, None);
2333        assert_eq!(
2334            report.avg_px,
2335            Some(Decimal::from_str_exact("50000.00").unwrap())
2336        );
2337    }
2338
2339    #[rstest]
2340    fn test_order_to_report_omits_avg_px_without_fills() {
2341        let mut order = order_with_price("0");
2342        order.avg_price = Some("50000.00".to_string());
2343        let account_id = AccountId::from("BINANCE-FUTURES-001");
2344        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2345        let ts_init = UnixNanos::from(1_000_000_000u64);
2346
2347        let report = order
2348            .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2349            .unwrap();
2350
2351        assert_eq!(report.avg_px, None);
2352    }
2353
2354    #[rstest]
2355    fn test_order_to_report_rejects_invalid_avg_px_for_filled_order() {
2356        let mut order = order_with_price("0");
2357        order.executed_qty = "0.001".to_string();
2358        order.avg_price = Some("not-a-number".to_string());
2359        let account_id = AccountId::from("BINANCE-FUTURES-001");
2360        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2361        let ts_init = UnixNanos::from(1_000_000_000u64);
2362
2363        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2364
2365        let error = result.unwrap_err().to_string();
2366        assert!(error.contains("avg_price"));
2367    }
2368
2369    #[rstest]
2370    fn test_close_all_algo_report_with_actual_uses_matching_engine_quantity() {
2371        let mut algo = algo_order_with_price(None);
2372        algo.order_type = BinanceFuturesOrderType::StopMarket;
2373        algo.quantity = None;
2374        algo.close_position = Some(true);
2375        algo.algo_status = Some(BinanceAlgoStatus::Finished);
2376        algo.actual_order_id = Some("987654321".to_string());
2377        algo.executed_qty = Some("0.002".to_string());
2378        algo.avg_price = Some("49000.00".to_string());
2379        algo.reduce_only = Some(true);
2380        algo.trigger_time = Some(1_625_474_305_000);
2381        algo.time_in_force = Some(BinanceTimeInForce::Gtd);
2382        algo.good_till_date = Some(1_700_000_601_000);
2383
2384        let mut actual = order_with_price("0");
2385        actual.order_id = 987654321;
2386        actual.orig_qty = "0.002".to_string();
2387        actual.executed_qty = "0.001".to_string();
2388        actual.avg_price = Some("50000.00".to_string());
2389        actual.status = BinanceOrderStatus::PartiallyFilled;
2390        actual.order_type = BinanceFuturesOrderType::Market;
2391        actual.side = BinanceSide::Sell;
2392        actual.update_time = Some(1_625_474_306_000);
2393
2394        let account_id = AccountId::from("BINANCE-FUTURES-001");
2395        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2396        let ts_init = UnixNanos::from(1_000_000_000u64);
2397        let report = algo
2398            .to_order_status_report_with_actual(
2399                &actual,
2400                account_id,
2401                instrument_id,
2402                2,
2403                3,
2404                false,
2405                ts_init,
2406            )
2407            .unwrap();
2408
2409        assert_eq!(
2410            report.client_order_id,
2411            Some(ClientOrderId::from("my-algo-order-1"))
2412        );
2413        assert_eq!(report.venue_order_id, VenueOrderId::from("987654321"));
2414        assert_eq!(report.order_type, OrderType::StopMarket);
2415        assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
2416        assert_eq!(report.quantity, Quantity::from("0.002"));
2417        assert_eq!(report.filled_qty, Quantity::from("0.001"));
2418        assert_eq!(report.avg_px, Some(Decimal::from(50000)));
2419        assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2420        assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2421        assert!(report.reduce_only);
2422        assert_eq!(
2423            report.expire_time,
2424            Some(UnixNanos::from_millis(1_700_000_601_000)),
2425        );
2426        assert_eq!(
2427            report.ts_triggered,
2428            Some(UnixNanos::from_millis(1_625_474_305_000))
2429        );
2430        assert_eq!(report.ts_last, UnixNanos::from_millis(1_625_474_306_000));
2431    }
2432
2433    #[rstest]
2434    #[case(BinanceAlgoStatus::Finished, Some("0.001"), OrderStatus::Filled)]
2435    #[case(BinanceAlgoStatus::Finished, Some("0.0005"), OrderStatus::Canceled)]
2436    #[case(
2437        BinanceAlgoStatus::Triggered,
2438        Some("0.0005"),
2439        OrderStatus::PartiallyFilled
2440    )]
2441    #[case(BinanceAlgoStatus::Triggered, None, OrderStatus::Accepted)]
2442    fn test_algo_order_status_uses_actual_quantity_conservatively(
2443        #[case] algo_status: BinanceAlgoStatus,
2444        #[case] executed_qty: Option<&str>,
2445        #[case] expected: OrderStatus,
2446    ) {
2447        let mut order = algo_order_with_price(None);
2448        order.algo_status = Some(algo_status);
2449        order.executed_qty = executed_qty.map(str::to_string);
2450
2451        assert_eq!(order.parse_order_status(), expected);
2452    }
2453
2454    #[rstest]
2455    fn test_algo_order_to_report_sets_price() {
2456        let order = algo_order_with_price(Some("50000.00"));
2457        let account_id = AccountId::from("BINANCE-FUTURES-001");
2458        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2459        let ts_init = UnixNanos::from(1_000_000_000u64);
2460
2461        let report = order
2462            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2463            .unwrap();
2464
2465        assert_eq!(report.price, Some(Price::from("50000.00")));
2466    }
2467
2468    #[rstest]
2469    fn test_algo_order_to_report_sets_actual_fill_fields() {
2470        let json = load_fixture_string("futures/http_json/algo_order_response.json");
2471        let order: BinanceFuturesAlgoOrder = serde_json::from_str(&json).unwrap();
2472        let account_id = AccountId::from("BINANCE-FUTURES-001");
2473        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2474        let ts_init = UnixNanos::from(1_000_000_000u64);
2475
2476        let report = order
2477            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2478            .unwrap();
2479
2480        assert_eq!(report.order_status, OrderStatus::Filled);
2481        assert_eq!(report.filled_qty.as_decimal(), dec!(0.001));
2482        assert_eq!(report.price, None);
2483        assert_eq!(report.avg_px, Some(dec!(50000.00)));
2484    }
2485
2486    #[rstest]
2487    fn test_algo_order_to_report_omits_avg_price_without_fills() {
2488        let mut order = algo_order_with_price(None);
2489        order.executed_qty = Some("0".to_string());
2490        order.avg_price = Some("not-a-number".to_string());
2491        let account_id = AccountId::from("BINANCE-FUTURES-001");
2492        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2493        let ts_init = UnixNanos::from(1_000_000_000u64);
2494
2495        let report = order
2496            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2497            .unwrap();
2498
2499        assert_eq!(report.filled_qty.as_decimal(), Decimal::ZERO);
2500        assert_eq!(report.avg_px, None);
2501    }
2502
2503    #[rstest]
2504    fn test_algo_order_to_report_sets_trigger_fields() {
2505        let order = algo_order_with_price(Some("44000.00"));
2506        let account_id = AccountId::from("BINANCE-FUTURES-001");
2507        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2508        let ts_init = UnixNanos::from(1_000_000_000u64);
2509
2510        let report = order
2511            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2512            .unwrap();
2513
2514        assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2515        assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2516    }
2517
2518    #[rstest]
2519    fn test_algo_order_to_report_sets_trailing_fields() {
2520        let mut order = algo_order_with_price(None);
2521        order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2522        order.trigger_price = None;
2523        order.activate_price = Some("45000.00".to_string());
2524        order.callback_rate = Some("0.25".to_string());
2525        let account_id = AccountId::from("BINANCE-FUTURES-001");
2526        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2527        let ts_init = UnixNanos::from(1_000_000_000u64);
2528
2529        let report = order
2530            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2531            .unwrap();
2532
2533        assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2534        assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2535        assert_eq!(report.trailing_offset, Some(Decimal::from(25)));
2536        assert_eq!(
2537            report.trailing_offset_type,
2538            Some(TrailingOffsetType::BasisPoints),
2539        );
2540    }
2541
2542    #[rstest]
2543    #[case(None)]
2544    #[case(Some("0"))]
2545    #[case(Some(""))]
2546    fn test_algo_order_to_report_omits_missing_price(#[case] price: Option<&str>) {
2547        let order = algo_order_with_price(price);
2548        let account_id = AccountId::from("BINANCE-FUTURES-001");
2549        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2550        let ts_init = UnixNanos::from(1_000_000_000u64);
2551
2552        let report = order
2553            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2554            .unwrap();
2555
2556        assert_eq!(report.price, None);
2557    }
2558
2559    #[rstest]
2560    fn test_order_to_report_rejects_invalid_price() {
2561        let order = order_with_price("not-a-number");
2562        let account_id = AccountId::from("BINANCE-FUTURES-001");
2563        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2564        let ts_init = UnixNanos::from(1_000_000_000u64);
2565
2566        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2567
2568        let error = result.unwrap_err().to_string();
2569        assert!(error.contains("invalid price"));
2570    }
2571
2572    #[rstest]
2573    fn test_algo_order_to_report_rejects_invalid_trigger_price() {
2574        let mut order = algo_order_with_price(Some("50000.00"));
2575        order.trigger_price = Some("not-a-number".to_string());
2576        let account_id = AccountId::from("BINANCE-FUTURES-001");
2577        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2578        let ts_init = UnixNanos::from(1_000_000_000u64);
2579
2580        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2581
2582        let error = result.unwrap_err().to_string();
2583        assert!(error.contains("trigger_price"));
2584    }
2585
2586    #[rstest]
2587    fn test_algo_order_to_report_rejects_missing_trigger_price() {
2588        let mut order = algo_order_with_price(None);
2589        order.order_type = BinanceFuturesOrderType::StopMarket;
2590        order.trigger_price = None;
2591        let account_id = AccountId::from("BINANCE-FUTURES-001");
2592        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2593        let ts_init = UnixNanos::from(1_000_000_000u64);
2594
2595        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2596
2597        let error = result.unwrap_err().to_string();
2598        assert!(error.contains("missing positive trigger_price"));
2599    }
2600
2601    #[rstest]
2602    fn test_algo_order_to_report_rejects_invalid_callback_rate() {
2603        let mut order = algo_order_with_price(None);
2604        order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2605        order.trigger_price = None;
2606        order.activate_price = Some("45000.00".to_string());
2607        order.callback_rate = Some("not-a-number".to_string());
2608        let account_id = AccountId::from("BINANCE-FUTURES-001");
2609        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2610        let ts_init = UnixNanos::from(1_000_000_000u64);
2611
2612        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2613
2614        let error = result.unwrap_err().to_string();
2615        assert!(error.contains("callback_rate"));
2616    }
2617
2618    #[rstest]
2619    fn test_algo_order_to_report_rejects_invalid_price() {
2620        let order = algo_order_with_price(Some("not-a-number"));
2621        let account_id = AccountId::from("BINANCE-FUTURES-001");
2622        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2623        let ts_init = UnixNanos::from(1_000_000_000u64);
2624
2625        let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2626
2627        let error = result.unwrap_err().to_string();
2628        assert!(error.contains("invalid price"));
2629    }
2630
2631    #[rstest]
2632    fn test_order_to_report_preserves_good_till_date() {
2633        let mut order = order_with_price("50000.00");
2634        order.time_in_force = BinanceTimeInForce::Gtd;
2635        order.good_till_date = Some(1_700_000_601_000);
2636        let account_id = AccountId::from("BINANCE-FUTURES-001");
2637        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2638        let ts_init = UnixNanos::from(1_000_000_000u64);
2639
2640        let report = order
2641            .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2642            .unwrap();
2643
2644        assert_eq!(report.time_in_force, TimeInForce::Gtd);
2645        assert_eq!(
2646            report.expire_time,
2647            Some(UnixNanos::from_millis(1_700_000_601_000)),
2648        );
2649    }
2650
2651    #[rstest]
2652    fn test_algo_order_to_report_preserves_good_till_date() {
2653        let mut order = algo_order_with_price(Some("50000.00"));
2654        order.time_in_force = Some(BinanceTimeInForce::Gtd);
2655        order.good_till_date = Some(1_700_000_601_000);
2656        let account_id = AccountId::from("BINANCE-FUTURES-001");
2657        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2658        let ts_init = UnixNanos::from(1_000_000_000u64);
2659
2660        let report = order
2661            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2662            .unwrap();
2663
2664        assert_eq!(report.time_in_force, TimeInForce::Gtd);
2665        assert_eq!(
2666            report.expire_time,
2667            Some(UnixNanos::from_millis(1_700_000_601_000)),
2668        );
2669    }
2670
2671    fn order_with_price(price: &str) -> BinanceFuturesOrder {
2672        BinanceFuturesOrder {
2673            symbol: Ustr::from("BTCUSDT"),
2674            order_id: 12345678,
2675            client_order_id: "external-order".to_string(),
2676            orig_qty: "0.001".to_string(),
2677            executed_qty: "0.000".to_string(),
2678            cum_quote: "0.00".to_string(),
2679            price: price.to_string(),
2680            avg_price: Some("0.00".to_string()),
2681            stop_price: Some("0.00".to_string()),
2682            status: BinanceOrderStatus::New,
2683            time_in_force: BinanceTimeInForce::Gtc,
2684            order_type: BinanceFuturesOrderType::Market,
2685            orig_type: Some(BinanceFuturesOrderType::Market),
2686            side: BinanceSide::Buy,
2687            position_side: Some(BinancePositionSide::Both),
2688            reduce_only: Some(false),
2689            close_position: Some(false),
2690            activate_price: None,
2691            price_rate: None,
2692            working_type: Some(BinanceWorkingType::ContractPrice),
2693            price_protect: Some(false),
2694            is_isolated: None,
2695            good_till_date: Some(0),
2696            price_match: Some(BinancePriceMatch::None),
2697            self_trade_prevention_mode: Some(BinanceSelfTradePreventionMode::None),
2698            update_time: Some(1_625_474_304_765),
2699            working_type_id: None,
2700        }
2701    }
2702
2703    fn algo_order_with_price(price: Option<&str>) -> BinanceFuturesAlgoOrder {
2704        BinanceFuturesAlgoOrder {
2705            algo_id: 123456789,
2706            client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2707            algo_type: BinanceAlgoType::Conditional,
2708            order_type: BinanceFuturesOrderType::TakeProfit,
2709            symbol: Ustr::from("BTCUSDT"),
2710            side: BinanceSide::Sell,
2711            position_side: Some(BinancePositionSide::Both),
2712            time_in_force: Some(BinanceTimeInForce::Gtc),
2713            quantity: Some("0.001".to_string()),
2714            algo_status: Some(BinanceAlgoStatus::New),
2715            trigger_price: Some("45000.00".to_string()),
2716            price: price.map(str::to_string),
2717            working_type: Some(BinanceWorkingType::MarkPrice),
2718            close_position: Some(false),
2719            price_protect: None,
2720            reduce_only: Some(false),
2721            activate_price: None,
2722            callback_rate: None,
2723            good_till_date: Some(0),
2724            create_time: Some(1_625_474_304_765),
2725            update_time: Some(1_625_474_304_765),
2726            trigger_time: None,
2727            actual_order_id: None,
2728            executed_qty: None,
2729            avg_price: None,
2730        }
2731    }
2732
2733    #[rstest]
2734    fn test_user_trade_to_fill_report_rejects_invalid_commission() {
2735        let trade = BinanceUserTrade {
2736            symbol: Ustr::from("BTCUSDT"),
2737            id: 100,
2738            order_id: 200,
2739            price: "50000.00".to_string(),
2740            qty: "0.001".to_string(),
2741            quote_qty: None,
2742            realized_pnl: "0".to_string(),
2743            side: BinanceSide::Buy,
2744            position_side: None,
2745            time: 1_625_474_304_000,
2746            buyer: true,
2747            maker: false,
2748            commission: Some("not-a-number".to_string()),
2749            commission_asset: Some(Ustr::from("USDT")),
2750            margin_asset: None,
2751        };
2752
2753        let result = trade.to_fill_report(
2754            AccountId::from("BINANCE-FUTURES-001"),
2755            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2756            2,
2757            3,
2758            Currency::USDT(),
2759            UnixNanos::from(1_000_000_000u64),
2760        );
2761
2762        let error = result.unwrap_err().to_string();
2763        assert!(error.contains("commission"));
2764    }
2765
2766    #[rstest]
2767    fn test_algo_order_to_report_decodes_broker_id() {
2768        let json = r#"{
2769            "algoId": 123456789,
2770            "clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
2771            "algoType": "CONDITIONAL",
2772            "type": "STOP_MARKET",
2773            "symbol": "BTCUSDT",
2774            "side": "BUY",
2775            "positionSide": "BOTH",
2776            "timeInForce": "GTC",
2777            "quantity": "0.001",
2778            "algoStatus": "NEW",
2779            "triggerPrice": "45000.00",
2780            "workingType": "MARK_PRICE",
2781            "reduceOnly": false,
2782            "createTime": 1625474304765,
2783            "updateTime": 1625474304765
2784        }"#;
2785
2786        let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
2787        let account_id = AccountId::from("BINANCE-FUTURES-001");
2788        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2789        let ts_init = UnixNanos::from(1_000_000_000u64);
2790
2791        let report = order
2792            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2793            .unwrap();
2794
2795        assert_eq!(
2796            report.client_order_id,
2797            Some(ClientOrderId::from("my-algo-order-1")),
2798        );
2799    }
2800
2801    #[rstest]
2802    fn test_algo_order_to_report_rejects_invalid_client_order_id() {
2803        let mut order = algo_order_with_price(None);
2804        order.client_algo_id = "x-aHRE4BCj-R".to_string();
2805
2806        let result = order.to_order_status_report(
2807            AccountId::from("BINANCE-FUTURES-001"),
2808            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2809            2,
2810            3,
2811            UnixNanos::from(1_000_000_000u64),
2812        );
2813
2814        assert_eq!(
2815            result.unwrap_err().to_string(),
2816            "missing raw broker client order ID payload"
2817        );
2818    }
2819
2820    #[rstest]
2821    #[case(None, "123456789")]
2822    #[case(Some(""), "123456789")]
2823    #[case(Some("987654321"), "987654321")]
2824    fn test_algo_order_to_report_selects_valid_venue_order_id(
2825        #[case] actual_order_id: Option<&str>,
2826        #[case] expected_venue_order_id: &str,
2827    ) {
2828        let order = BinanceFuturesAlgoOrder {
2829            algo_id: 123456789,
2830            client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2831            algo_type: BinanceAlgoType::Conditional,
2832            order_type: BinanceFuturesOrderType::StopMarket,
2833            symbol: Ustr::from("BTCUSDT"),
2834            side: BinanceSide::Buy,
2835            position_side: Some(BinancePositionSide::Both),
2836            time_in_force: Some(BinanceTimeInForce::Gtc),
2837            quantity: Some("0.001".to_string()),
2838            algo_status: Some(BinanceAlgoStatus::New),
2839            trigger_price: Some("45000.00".to_string()),
2840            price: None,
2841            working_type: Some(BinanceWorkingType::MarkPrice),
2842            close_position: Some(false),
2843            price_protect: None,
2844            reduce_only: Some(false),
2845            activate_price: None,
2846            callback_rate: None,
2847            good_till_date: Some(0),
2848            create_time: Some(1_625_474_304_765),
2849            update_time: Some(1_625_474_304_765),
2850            trigger_time: None,
2851            actual_order_id: actual_order_id.map(str::to_string),
2852            executed_qty: None,
2853            avg_price: None,
2854        };
2855        let account_id = AccountId::from("BINANCE-FUTURES-001");
2856        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2857        let ts_init = UnixNanos::from(1_000_000_000u64);
2858
2859        let report = order
2860            .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2861            .unwrap();
2862
2863        assert_eq!(
2864            report.venue_order_id,
2865            VenueOrderId::new(expected_venue_order_id)
2866        );
2867    }
2868
2869    #[rstest]
2870    #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
2871    #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
2872    #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
2873    #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
2874    fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
2875        #[case] status: BinanceOrderStatus,
2876        #[case] treat_expired_as_canceled: bool,
2877        #[case] expected: OrderStatus,
2878    ) {
2879        let result = status.to_nautilus_order_status(treat_expired_as_canceled);
2880        assert_eq!(result, expected);
2881    }
2882}