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