Skip to main content

nautilus_binance/futures/websocket/streams/
parse_exec.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//! Parse functions for converting Binance Futures venue types to Nautilus reports.
17//!
18//! Pure functions that take venue message + precision + account_id + ts_init
19//! and return Nautilus report types.
20
21use nautilus_core::{UUID4, UnixNanos};
22use nautilus_model::{
23    enums::{
24        AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
25        TrailingOffsetType,
26    },
27    events::AccountState,
28    identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
29    reports::{FillReport, OrderStatusReport},
30    types::{AccountBalance, Currency, Money, Price, Quantity},
31};
32use rust_decimal::Decimal;
33
34use super::messages::{
35    AlgoOrderUpdateData, BinanceFuturesAccountUpdateMsg, BinanceFuturesOrderUpdateMsg,
36    OrderUpdateData,
37};
38use crate::common::{
39    consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
40    encoder::decode_broker_id,
41    enums::{
42        BinanceAlgoStatus, BinanceFuturesOrderType, BinanceOrderStatus, BinanceSide,
43        BinanceTimeInForce,
44    },
45};
46
47/// Converts a Binance Futures order update to a Nautilus order status report.
48///
49/// # Errors
50///
51/// Returns an error if report construction fails.
52pub fn parse_futures_order_update_to_order_status(
53    msg: &BinanceFuturesOrderUpdateMsg,
54    instrument_id: InstrumentId,
55    price_precision: u8,
56    size_precision: u8,
57    account_id: AccountId,
58    treat_expired_as_canceled: bool,
59    ts_init: UnixNanos,
60) -> anyhow::Result<OrderStatusReport> {
61    let order = &msg.order;
62    let ts_event = UnixNanos::from_millis(msg.event_time as u64);
63
64    let client_order_id = ClientOrderId::new(decode_broker_id(
65        &order.client_order_id,
66        BINANCE_NAUTILUS_FUTURES_BROKER_ID,
67    ));
68    let venue_order_id = VenueOrderId::new(order.order_id.to_string());
69
70    let order_side = parse_side(order.side);
71    let order_status = parse_order_status(order.order_status, treat_expired_as_canceled);
72    let order_type = parse_futures_order_type(order.order_type);
73    let time_in_force = parse_time_in_force(order.time_in_force);
74
75    let quantity: f64 = order.original_qty.parse().unwrap_or(0.0);
76    let filled_qty: f64 = order.cumulative_filled_qty.parse().unwrap_or(0.0);
77    let price: f64 = order.original_price.parse().unwrap_or(0.0);
78
79    let avg_px = if filled_qty > 0.0 {
80        let avg: f64 = order.average_price.parse().unwrap_or(0.0);
81        if avg > 0.0 {
82            Some(Price::new(avg, price_precision))
83        } else {
84            None
85        }
86    } else {
87        None
88    };
89
90    let mut report = OrderStatusReport::new(
91        account_id,
92        instrument_id,
93        Some(client_order_id),
94        venue_order_id,
95        order_side,
96        order_type,
97        time_in_force,
98        order_status,
99        Quantity::new(quantity, size_precision),
100        Quantity::new(filled_qty, size_precision),
101        ts_event,
102        ts_event,
103        ts_init,
104        None, // report_id
105    );
106
107    report.price = Some(Price::new(price, price_precision));
108    report.post_only = order.order_type == BinanceFuturesOrderType::Limit
109        && order.time_in_force == BinanceTimeInForce::Gtx;
110
111    let stop_price: f64 = order.stop_price.parse().unwrap_or(0.0);
112    if stop_price > 0.0 {
113        report.trigger_price = Some(Price::new(stop_price, price_precision));
114    }
115
116    if let Some(ref cr) = order.callback_rate {
117        let rate: f64 = cr.parse().unwrap_or(0.0);
118        if rate > 0.0 {
119            // Binance callbackRate is percentage (1 = 1%), convert to basis points (100 = 1%)
120            report.trailing_offset = Some(
121                rust_decimal::Decimal::from_f64_retain(rate * 100.0)
122                    .unwrap_or(rust_decimal::Decimal::ZERO),
123            );
124            report.trailing_offset_type = TrailingOffsetType::BasisPoints;
125        }
126    }
127
128    if let Some(avg) = avg_px {
129        report.avg_px = Some(avg.as_decimal());
130    }
131
132    Ok(report)
133}
134
135/// Resolves the commission for a Binance fill event.
136///
137/// Uses the venue-provided commission fields (N/n) when present. Falls back to
138/// estimating `taker_fee * qty * price` when the venue omits them, matching the
139/// Python adapter behavior for exchange-generated fills (liquidation, ADL).
140/// Returns zero USDT when neither source is available.
141#[must_use]
142pub fn resolve_commission(
143    order: &OrderUpdateData,
144    last_qty: f64,
145    last_px: f64,
146    taker_fee: Option<Decimal>,
147    quote_currency: Option<Currency>,
148) -> Money {
149    if order.commission_asset.is_some() {
150        let amount: f64 = order
151            .commission
152            .as_deref()
153            .unwrap_or("0")
154            .parse()
155            .unwrap_or(0.0);
156        let currency = order
157            .commission_asset
158            .as_ref()
159            .map_or_else(Currency::USDT, |a| Currency::from(a.as_str()));
160        Money::new(amount, currency)
161    } else if let Some(fee) = taker_fee {
162        let currency = quote_currency.unwrap_or_else(Currency::USDT);
163        let notional = Decimal::try_from(last_qty * last_px).unwrap_or_default();
164        Money::from_decimal(fee * notional, currency).unwrap_or_else(|_| Money::new(0.0, currency))
165    } else {
166        Money::new(0.0, Currency::USDT())
167    }
168}
169
170/// Converts a Binance Futures order update (Trade type) to a Nautilus fill report.
171///
172/// # Errors
173///
174/// Returns an error if report construction fails.
175#[expect(clippy::too_many_arguments)]
176pub fn parse_futures_order_update_to_fill(
177    msg: &BinanceFuturesOrderUpdateMsg,
178    account_id: AccountId,
179    instrument_id: InstrumentId,
180    price_precision: u8,
181    size_precision: u8,
182    taker_fee: Option<Decimal>,
183    quote_currency: Option<Currency>,
184    venue_position_id: Option<PositionId>,
185    ts_init: UnixNanos,
186) -> anyhow::Result<FillReport> {
187    let order = &msg.order;
188    let ts_event = UnixNanos::from_millis(msg.event_time as u64);
189
190    let client_order_id = ClientOrderId::new(decode_broker_id(
191        &order.client_order_id,
192        BINANCE_NAUTILUS_FUTURES_BROKER_ID,
193    ));
194    let venue_order_id = VenueOrderId::new(order.order_id.to_string());
195    let trade_id = TradeId::new(order.trade_id.to_string());
196
197    let order_side = parse_side(order.side);
198
199    let liquidity_side = if order.is_maker {
200        LiquiditySide::Maker
201    } else {
202        LiquiditySide::Taker
203    };
204
205    let last_qty: f64 = order.last_filled_qty.parse().unwrap_or(0.0);
206    let last_px: f64 = order.last_filled_price.parse().unwrap_or(0.0);
207    let commission = resolve_commission(order, last_qty, last_px, taker_fee, quote_currency);
208
209    Ok(FillReport::new(
210        account_id,
211        instrument_id,
212        venue_order_id,
213        trade_id,
214        order_side,
215        Quantity::new(last_qty, size_precision),
216        Price::new(last_px, price_precision),
217        commission,
218        liquidity_side,
219        Some(client_order_id),
220        venue_position_id,
221        ts_event,
222        ts_init,
223        None, // report_id
224    ))
225}
226
227/// Converts a Binance Futures algo order update to a Nautilus order status report.
228///
229/// Returns `None` for algo statuses that don't map to an order status report
230/// (e.g. New, Triggering, Triggered, Finished, Unknown).
231pub fn parse_futures_algo_update_to_order_status(
232    algo_data: &AlgoOrderUpdateData,
233    event_time: i64,
234    instrument_id: InstrumentId,
235    _price_precision: u8,
236    size_precision: u8,
237    account_id: AccountId,
238    ts_init: UnixNanos,
239) -> Option<OrderStatusReport> {
240    let ts_event = UnixNanos::from_millis(event_time as u64);
241
242    let client_order_id = ClientOrderId::new(decode_broker_id(
243        &algo_data.client_algo_id,
244        BINANCE_NAUTILUS_FUTURES_BROKER_ID,
245    ));
246
247    let venue_order_id = algo_data
248        .actual_order_id
249        .as_ref()
250        .filter(|id| !id.is_empty())
251        .map_or_else(
252            || VenueOrderId::new(algo_data.algo_id.to_string()),
253            |id| VenueOrderId::new(id.clone()),
254        );
255
256    let order_status = match algo_data.algo_status {
257        BinanceAlgoStatus::Canceled | BinanceAlgoStatus::Expired => OrderStatus::Canceled,
258        BinanceAlgoStatus::Rejected => OrderStatus::Rejected,
259        _ => return None,
260    };
261
262    let order_side = parse_side(algo_data.side);
263    let order_type = parse_futures_order_type(algo_data.order_type);
264    let time_in_force = parse_time_in_force(algo_data.time_in_force);
265
266    let quantity: f64 = algo_data.quantity.parse().unwrap_or(0.0);
267
268    let report = OrderStatusReport::new(
269        account_id,
270        instrument_id,
271        Some(client_order_id),
272        venue_order_id,
273        order_side,
274        order_type,
275        time_in_force,
276        order_status,
277        Quantity::new(quantity, size_precision),
278        Quantity::new(0.0, size_precision),
279        ts_event,
280        ts_event,
281        ts_init,
282        None, // report_id
283    );
284
285    Some(report)
286}
287
288/// Converts a Binance Futures account update to a Nautilus account state.
289pub fn parse_futures_account_update(
290    msg: &BinanceFuturesAccountUpdateMsg,
291    account_id: AccountId,
292    ts_init: UnixNanos,
293) -> Option<AccountState> {
294    let ts_event = UnixNanos::from_millis(msg.event_time as u64);
295
296    let balances: Vec<AccountBalance> = msg
297        .account
298        .balances
299        .iter()
300        .filter_map(|b| {
301            if b.wallet_balance.is_zero() {
302                return None;
303            }
304
305            let currency = Currency::from(&b.asset);
306            AccountBalance::from_total_and_free(b.wallet_balance, b.cross_wallet_balance, currency)
307                .ok()
308        })
309        .collect();
310
311    if balances.is_empty() {
312        return None;
313    }
314
315    Some(AccountState::new(
316        account_id,
317        AccountType::Margin,
318        balances,
319        vec![], // Margins handled separately
320        true,   // is_reported
321        UUID4::new(),
322        ts_event,
323        ts_init,
324        None, // base_currency
325    ))
326}
327
328/// Returns the decoded client order ID from an [`OrderUpdateData`].
329pub fn decode_order_client_id(order: &OrderUpdateData) -> ClientOrderId {
330    ClientOrderId::new(decode_broker_id(
331        &order.client_order_id,
332        BINANCE_NAUTILUS_FUTURES_BROKER_ID,
333    ))
334}
335
336/// Returns the decoded client order ID from an [`AlgoOrderUpdateData`].
337pub fn decode_algo_client_id(algo: &AlgoOrderUpdateData) -> ClientOrderId {
338    ClientOrderId::new(decode_broker_id(
339        &algo.client_algo_id,
340        BINANCE_NAUTILUS_FUTURES_BROKER_ID,
341    ))
342}
343
344fn parse_side(side: BinanceSide) -> OrderSide {
345    match side {
346        BinanceSide::Buy => OrderSide::Buy,
347        BinanceSide::Sell => OrderSide::Sell,
348    }
349}
350
351fn parse_order_status(status: BinanceOrderStatus, treat_expired_as_canceled: bool) -> OrderStatus {
352    match status {
353        BinanceOrderStatus::New | BinanceOrderStatus::PendingNew => OrderStatus::Accepted,
354        BinanceOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
355        BinanceOrderStatus::Filled
356        | BinanceOrderStatus::NewAdl
357        | BinanceOrderStatus::NewInsurance => OrderStatus::Filled,
358        BinanceOrderStatus::Canceled | BinanceOrderStatus::PendingCancel => OrderStatus::Canceled,
359        BinanceOrderStatus::Rejected => OrderStatus::Rejected,
360        BinanceOrderStatus::Expired | BinanceOrderStatus::ExpiredInMatch => {
361            if treat_expired_as_canceled {
362                OrderStatus::Canceled
363            } else {
364                OrderStatus::Expired
365            }
366        }
367        BinanceOrderStatus::Unknown => OrderStatus::Accepted,
368    }
369}
370
371fn parse_futures_order_type(order_type: BinanceFuturesOrderType) -> OrderType {
372    match order_type {
373        BinanceFuturesOrderType::Limit => OrderType::Limit,
374        BinanceFuturesOrderType::Market => OrderType::Market,
375        BinanceFuturesOrderType::Stop => OrderType::StopLimit,
376        BinanceFuturesOrderType::StopMarket => OrderType::StopMarket,
377        BinanceFuturesOrderType::TakeProfit => OrderType::LimitIfTouched,
378        BinanceFuturesOrderType::TakeProfitMarket => OrderType::MarketIfTouched,
379        BinanceFuturesOrderType::TrailingStopMarket => OrderType::TrailingStopMarket,
380        BinanceFuturesOrderType::Liquidation
381        | BinanceFuturesOrderType::Adl
382        | BinanceFuturesOrderType::Unknown => OrderType::Market,
383    }
384}
385
386fn parse_time_in_force(tif: BinanceTimeInForce) -> TimeInForce {
387    match tif {
388        BinanceTimeInForce::Gtc | BinanceTimeInForce::Gtx => TimeInForce::Gtc,
389        BinanceTimeInForce::Ioc | BinanceTimeInForce::Rpi => TimeInForce::Ioc,
390        BinanceTimeInForce::Fok => TimeInForce::Fok,
391        BinanceTimeInForce::Gtd => TimeInForce::Gtd,
392        BinanceTimeInForce::Unknown => TimeInForce::Gtc,
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use rstest::rstest;
399    use serde::de::DeserializeOwned;
400
401    use super::*;
402    use crate::{
403        common::{
404            consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
405            encoder::encode_broker_id,
406            enums::{BinancePriceMatch, BinanceSelfTradePreventionMode},
407            testing::load_fixture_string,
408        },
409        futures::websocket::streams::messages::{
410            BinanceFuturesAccountUpdateMsg, BinanceFuturesAlgoUpdateMsg,
411            BinanceFuturesOrderUpdateMsg,
412        },
413    };
414
415    const PRICE_PRECISION: u8 = 2;
416    const SIZE_PRECISION: u8 = 3;
417
418    fn instrument_id() -> InstrumentId {
419        InstrumentId::from("ETHUSDT-PERP.BINANCE")
420    }
421
422    fn account_id() -> AccountId {
423        AccountId::from("BINANCE-FUTURES-001")
424    }
425
426    fn load_user_data_fixture<T: DeserializeOwned>(filename: &str) -> T {
427        let path = format!("futures/user_data_json/{filename}");
428        serde_json::from_str(&load_fixture_string(&path))
429            .unwrap_or_else(|e| panic!("Failed to parse fixture {path}: {e}"))
430    }
431
432    #[rstest]
433    fn test_parse_order_update_to_order_status_new() {
434        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
435        let ts_init = UnixNanos::from(1_000_000_000u64);
436
437        let report = parse_futures_order_update_to_order_status(
438            &msg,
439            instrument_id(),
440            PRICE_PRECISION,
441            SIZE_PRECISION,
442            account_id(),
443            false,
444            ts_init,
445        )
446        .unwrap();
447
448        assert_eq!(report.account_id, account_id());
449        assert_eq!(report.instrument_id, instrument_id());
450        assert_eq!(report.order_side, OrderSide::Buy);
451        assert_eq!(report.order_status, OrderStatus::Accepted);
452        assert_eq!(report.order_type, OrderType::TrailingStopMarket);
453        assert_eq!(report.venue_order_id, VenueOrderId::new("8886774"));
454        assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
455    }
456
457    #[rstest]
458    fn test_parse_order_update_to_fill_report() {
459        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
460        let ts_init = UnixNanos::from(1_000_000_000u64);
461
462        assert_eq!(
463            msg.order.stp_mode,
464            Some(BinanceSelfTradePreventionMode::ExpireTaker),
465        );
466
467        let report = parse_futures_order_update_to_fill(
468            &msg,
469            account_id(),
470            instrument_id(),
471            PRICE_PRECISION,
472            SIZE_PRECISION,
473            None,
474            None,
475            None,
476            ts_init,
477        )
478        .unwrap();
479
480        assert_eq!(report.account_id, account_id());
481        assert_eq!(report.instrument_id, instrument_id());
482        assert_eq!(report.order_side, OrderSide::Buy);
483        assert_eq!(report.liquidity_side, LiquiditySide::Maker);
484        assert_eq!(report.trade_id, TradeId::new("12345678"));
485        assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
486        assert_eq!(report.last_qty, Quantity::new(0.001, SIZE_PRECISION));
487        assert_eq!(report.last_px, Price::new(7100.50, PRICE_PRECISION));
488    }
489
490    #[rstest]
491    fn test_parse_account_update() {
492        let msg: BinanceFuturesAccountUpdateMsg = load_user_data_fixture("account_update.json");
493        let ts_init = UnixNanos::from(1_000_000_000u64);
494
495        let state = parse_futures_account_update(&msg, account_id(), ts_init).unwrap();
496
497        assert_eq!(state.account_id, account_id());
498        assert_eq!(state.account_type, AccountType::Margin);
499        assert!(state.is_reported);
500        assert_eq!(state.balances.len(), 1);
501    }
502
503    // Regression for the #3867 bug class: WS balances whose `wb` and `cw` have more decimal
504    // places than the asset's currency precision used to trip the invariant when Money::new
505    // rounded each side independently.
506    #[rstest]
507    fn test_parse_account_update_precision_drift() {
508        let json = r#"{
509            "e": "ACCOUNT_UPDATE",
510            "E": 1700000000000,
511            "T": 1700000000000,
512            "a": {
513                "m": "ORDER",
514                "B": [{
515                    "a": "USDT",
516                    "wb": "10.000000034999",
517                    "cw": "9.999999994999"
518                }],
519                "P": []
520            }
521        }"#;
522        let msg: BinanceFuturesAccountUpdateMsg = serde_json::from_str(json).unwrap();
523        let ts_init = UnixNanos::from(1_000_000_000u64);
524
525        let state = parse_futures_account_update(&msg, account_id(), ts_init).unwrap();
526
527        assert_eq!(state.balances.len(), 1);
528        let balance = &state.balances[0];
529        assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
530    }
531
532    #[rstest]
533    fn test_parse_algo_update_to_order_status_canceled() {
534        let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_canceled.json");
535        let ts_init = UnixNanos::from(1_000_000_000u64);
536
537        assert_eq!(
538            msg.algo_order.stp_mode,
539            Some(BinanceSelfTradePreventionMode::ExpireMaker),
540        );
541        assert_eq!(msg.algo_order.price_match, Some(BinancePriceMatch::None));
542
543        let report = parse_futures_algo_update_to_order_status(
544            &msg.algo_order,
545            msg.event_time,
546            instrument_id(),
547            PRICE_PRECISION,
548            SIZE_PRECISION,
549            account_id(),
550            ts_init,
551        )
552        .unwrap();
553
554        assert_eq!(report.account_id, account_id());
555        assert_eq!(report.instrument_id, instrument_id());
556        assert_eq!(
557            report.client_order_id,
558            Some(ClientOrderId::new("Q5xaq5EGKgXXa0fD7fs0Ip")),
559        );
560        assert_eq!(report.venue_order_id, VenueOrderId::new("2148719"));
561        assert_eq!(report.order_side, OrderSide::Sell);
562        assert_eq!(report.order_type, OrderType::LimitIfTouched);
563        assert_eq!(report.time_in_force, TimeInForce::Gtc);
564        assert_eq!(report.order_status, OrderStatus::Canceled);
565        assert_eq!(report.quantity, Quantity::new(0.01, SIZE_PRECISION));
566        assert_eq!(report.filled_qty, Quantity::new(0.0, SIZE_PRECISION));
567        assert_eq!(
568            report.ts_accepted,
569            UnixNanos::from(1_750_515_742_303_000_000u64)
570        );
571        assert_eq!(
572            report.ts_last,
573            UnixNanos::from(1_750_515_742_303_000_000u64)
574        );
575        assert_eq!(report.ts_init, ts_init);
576    }
577
578    #[rstest]
579    fn test_parse_algo_update_to_order_status_new_returns_none() {
580        let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_new.json");
581        let report = parse_futures_algo_update_to_order_status(
582            &msg.algo_order,
583            msg.event_time,
584            instrument_id(),
585            PRICE_PRECISION,
586            SIZE_PRECISION,
587            account_id(),
588            UnixNanos::default(),
589        );
590
591        assert!(report.is_none());
592    }
593
594    #[rstest]
595    fn test_decode_order_client_id() {
596        let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
597        let original = ClientOrderId::from("O-20200101-000000-000-000-1");
598        msg.order.client_order_id = encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
599
600        let decoded = decode_order_client_id(&msg.order);
601
602        assert_eq!(decoded, original);
603    }
604
605    #[rstest]
606    fn test_decode_algo_client_id() {
607        let mut msg: BinanceFuturesAlgoUpdateMsg =
608            load_user_data_fixture("algo_update_canceled.json");
609        let original = ClientOrderId::from("O-20200101-000000-000-000-2");
610        msg.algo_order.client_algo_id =
611            encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
612
613        let decoded = decode_algo_client_id(&msg.algo_order);
614
615        assert_eq!(decoded, original);
616    }
617
618    #[rstest]
619    fn test_parse_liquidation_fill() {
620        let msg: BinanceFuturesOrderUpdateMsg =
621            load_user_data_fixture("order_update_calculated.json");
622        let ts_init = UnixNanos::from(1_000_000_000u64);
623
624        assert!(msg.order.is_liquidation());
625        assert!(msg.order.is_exchange_generated());
626
627        let fill = parse_futures_order_update_to_fill(
628            &msg,
629            account_id(),
630            instrument_id(),
631            PRICE_PRECISION,
632            SIZE_PRECISION,
633            None,
634            None,
635            None,
636            ts_init,
637        )
638        .unwrap();
639
640        assert_eq!(fill.account_id, account_id());
641        assert_eq!(fill.instrument_id, instrument_id());
642        assert_eq!(
643            fill.client_order_id,
644            Some(ClientOrderId::new("autoclose-1234567890"))
645        );
646        assert_eq!(fill.venue_order_id, VenueOrderId::new("8886999"));
647        assert_eq!(fill.trade_id, TradeId::new("12345999"));
648        assert_eq!(fill.order_side, OrderSide::Sell);
649        assert_eq!(fill.last_qty, Quantity::new(0.014, SIZE_PRECISION));
650        assert_eq!(fill.last_px, Price::new(9910.12, PRICE_PRECISION));
651        assert_eq!(
652            fill.commission,
653            Money::new(0.06937084, Currency::from("USDT"))
654        );
655        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
656    }
657
658    #[rstest]
659    fn test_parse_liquidation_status_report() {
660        let msg: BinanceFuturesOrderUpdateMsg =
661            load_user_data_fixture("order_update_calculated.json");
662        let ts_init = UnixNanos::from(1_000_000_000u64);
663
664        let status = parse_futures_order_update_to_order_status(
665            &msg,
666            instrument_id(),
667            PRICE_PRECISION,
668            SIZE_PRECISION,
669            account_id(),
670            false,
671            ts_init,
672        )
673        .unwrap();
674
675        assert_eq!(status.account_id, account_id());
676        assert_eq!(status.instrument_id, instrument_id());
677        assert_eq!(
678            status.client_order_id,
679            Some(ClientOrderId::new("autoclose-1234567890"))
680        );
681        assert_eq!(status.venue_order_id, VenueOrderId::new("8886999"));
682        assert_eq!(status.order_side, OrderSide::Sell);
683        assert_eq!(status.order_status, OrderStatus::Filled);
684        assert_eq!(status.quantity, Quantity::new(0.014, SIZE_PRECISION));
685        assert_eq!(status.filled_qty, Quantity::new(0.014, SIZE_PRECISION));
686    }
687
688    #[rstest]
689    fn test_parse_adl_fill_with_new_adl_status() {
690        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
691        let ts_init = UnixNanos::from(1_000_000_000u64);
692
693        assert!(msg.order.is_adl());
694        assert!(msg.order.is_exchange_generated());
695        assert!(!msg.order.is_liquidation());
696
697        let fill = parse_futures_order_update_to_fill(
698            &msg,
699            account_id(),
700            instrument_id(),
701            PRICE_PRECISION,
702            SIZE_PRECISION,
703            None,
704            None,
705            None,
706            ts_init,
707        )
708        .unwrap();
709
710        assert_eq!(
711            fill.client_order_id,
712            Some(ClientOrderId::new("adl_autoclose_12345"))
713        );
714        assert_eq!(fill.venue_order_id, VenueOrderId::new("8887001"));
715        assert_eq!(fill.order_side, OrderSide::Buy);
716        assert_eq!(fill.last_qty, Quantity::new(0.005, SIZE_PRECISION));
717        assert_eq!(fill.last_px, Price::new(42000.00, PRICE_PRECISION));
718        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
719    }
720
721    #[rstest]
722    fn test_parse_adl_status_report_maps_new_adl_to_filled() {
723        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
724        let ts_init = UnixNanos::from(1_000_000_000u64);
725
726        let status = parse_futures_order_update_to_order_status(
727            &msg,
728            instrument_id(),
729            PRICE_PRECISION,
730            SIZE_PRECISION,
731            account_id(),
732            false,
733            ts_init,
734        )
735        .unwrap();
736
737        assert_eq!(status.order_status, OrderStatus::Filled);
738        assert_eq!(status.filled_qty, Quantity::new(0.005, SIZE_PRECISION));
739    }
740
741    #[rstest]
742    fn test_parse_settlement_fill_with_trade_exec_type() {
743        let msg: BinanceFuturesOrderUpdateMsg =
744            load_user_data_fixture("order_update_settlement.json");
745        let ts_init = UnixNanos::from(1_000_000_000u64);
746
747        assert!(msg.order.is_settlement());
748        assert!(msg.order.is_exchange_generated());
749        assert!(!msg.order.is_liquidation());
750        assert!(!msg.order.is_adl());
751
752        let fill = parse_futures_order_update_to_fill(
753            &msg,
754            account_id(),
755            instrument_id(),
756            PRICE_PRECISION,
757            SIZE_PRECISION,
758            None,
759            None,
760            None,
761            ts_init,
762        )
763        .unwrap();
764
765        assert_eq!(
766            fill.client_order_id,
767            Some(ClientOrderId::new("settlement_autoclose-9999"))
768        );
769        assert_eq!(fill.venue_order_id, VenueOrderId::new("8887002"));
770        assert_eq!(fill.order_side, OrderSide::Sell);
771        assert_eq!(fill.last_qty, Quantity::new(0.010, SIZE_PRECISION));
772        assert_eq!(fill.last_px, Price::new(50000.00, PRICE_PRECISION));
773    }
774
775    #[rstest]
776    fn test_parse_order_status_new_adl_maps_to_filled() {
777        let result = parse_order_status(BinanceOrderStatus::NewAdl, false);
778        assert_eq!(result, OrderStatus::Filled);
779    }
780
781    #[rstest]
782    fn test_parse_order_status_new_insurance_maps_to_filled() {
783        let result = parse_order_status(BinanceOrderStatus::NewInsurance, false);
784        assert_eq!(result, OrderStatus::Filled);
785    }
786
787    #[rstest]
788    #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
789    #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
790    #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
791    #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
792    fn test_parse_order_status_expired_respects_treat_as_canceled(
793        #[case] status: BinanceOrderStatus,
794        #[case] treat_expired_as_canceled: bool,
795        #[case] expected: OrderStatus,
796    ) {
797        let result = parse_order_status(status, treat_expired_as_canceled);
798        assert_eq!(result, expected);
799    }
800
801    #[rstest]
802    fn test_is_exchange_generated_autoclose() {
803        let msg: BinanceFuturesOrderUpdateMsg =
804            load_user_data_fixture("order_update_calculated.json");
805        assert!(msg.order.is_exchange_generated());
806        assert!(msg.order.is_liquidation());
807    }
808
809    #[rstest]
810    fn test_is_exchange_generated_adl_autoclose() {
811        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
812        assert!(msg.order.is_exchange_generated());
813        assert!(msg.order.is_adl());
814    }
815
816    #[rstest]
817    fn test_is_exchange_generated_settlement_autoclose() {
818        let msg: BinanceFuturesOrderUpdateMsg =
819            load_user_data_fixture("order_update_settlement.json");
820        assert!(msg.order.is_exchange_generated());
821        assert!(msg.order.is_settlement());
822    }
823
824    #[rstest]
825    fn test_is_exchange_generated_delivery_autoclose() {
826        let msg: BinanceFuturesOrderUpdateMsg =
827            load_user_data_fixture("order_update_delivery.json");
828        assert!(msg.order.is_exchange_generated());
829        assert!(msg.order.is_settlement());
830        assert!(!msg.order.is_liquidation());
831        assert!(!msg.order.is_adl());
832    }
833
834    #[rstest]
835    fn test_normal_order_is_not_exchange_generated() {
836        let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
837        assert!(!msg.order.is_exchange_generated());
838        assert!(!msg.order.is_liquidation());
839        assert!(!msg.order.is_adl());
840        assert!(!msg.order.is_settlement());
841    }
842
843    #[rstest]
844    fn test_parse_insurance_fill_with_new_insurance_status() {
845        let msg: BinanceFuturesOrderUpdateMsg =
846            load_user_data_fixture("order_update_insurance.json");
847
848        assert!(msg.order.is_liquidation());
849        assert!(msg.order.is_exchange_generated());
850        assert_eq!(msg.order.order_status, BinanceOrderStatus::NewInsurance);
851
852        let fill = parse_futures_order_update_to_fill(
853            &msg,
854            account_id(),
855            instrument_id(),
856            PRICE_PRECISION,
857            SIZE_PRECISION,
858            None,
859            None,
860            None,
861            UnixNanos::from(1_000_000_000u64),
862        )
863        .unwrap();
864
865        assert_eq!(
866            fill.client_order_id,
867            Some(ClientOrderId::new("autoclose-insurance-5678"))
868        );
869        assert_eq!(fill.order_side, OrderSide::Sell);
870        assert_eq!(fill.last_qty, Quantity::new(0.020, SIZE_PRECISION));
871        assert_eq!(fill.last_px, Price::new(45000.00, PRICE_PRECISION));
872    }
873
874    #[rstest]
875    fn test_parse_insurance_status_maps_new_insurance_to_filled() {
876        let msg: BinanceFuturesOrderUpdateMsg =
877            load_user_data_fixture("order_update_insurance.json");
878
879        let status = parse_futures_order_update_to_order_status(
880            &msg,
881            instrument_id(),
882            PRICE_PRECISION,
883            SIZE_PRECISION,
884            account_id(),
885            false,
886            UnixNanos::from(1_000_000_000u64),
887        )
888        .unwrap();
889
890        assert_eq!(status.order_status, OrderStatus::Filled);
891    }
892
893    #[rstest]
894    fn test_parse_settlement_status_report() {
895        let msg: BinanceFuturesOrderUpdateMsg =
896            load_user_data_fixture("order_update_settlement.json");
897
898        let status = parse_futures_order_update_to_order_status(
899            &msg,
900            instrument_id(),
901            PRICE_PRECISION,
902            SIZE_PRECISION,
903            account_id(),
904            false,
905            UnixNanos::from(1_000_000_000u64),
906        )
907        .unwrap();
908
909        assert_eq!(status.order_status, OrderStatus::Filled);
910        assert_eq!(status.order_side, OrderSide::Sell);
911        assert_eq!(status.quantity, Quantity::new(0.010, SIZE_PRECISION));
912        assert_eq!(status.filled_qty, Quantity::new(0.010, SIZE_PRECISION));
913    }
914
915    #[rstest]
916    fn test_pending_liquidation_has_zero_fill_qty() {
917        let msg: BinanceFuturesOrderUpdateMsg =
918            load_user_data_fixture("order_update_calculated_pending.json");
919
920        assert!(msg.order.is_exchange_generated());
921        assert!(msg.order.is_liquidation());
922
923        let last_qty: f64 = msg.order.last_filled_qty.parse().unwrap_or(0.0);
924        assert_eq!(last_qty, 0.0);
925    }
926
927    #[rstest]
928    #[case::venue_provided(Some("USDT"), Some("0.06937084"), None, None, 0.06937084, "USDT")]
929    #[case::fallback_from_taker_fee(
930        None, None,
931        Some("0.0004"), Some("USDT"),
932        0.055496, "USDT"  // 0.0004 * 0.014 * 9910.12 ≈ 0.05549...
933    )]
934    #[case::no_commission_no_fee(None, None, None, None, 0.0, "USDT")]
935    fn test_resolve_commission(
936        #[case] commission_asset: Option<&str>,
937        #[case] commission_amount: Option<&str>,
938        #[case] taker_fee_str: Option<&str>,
939        #[case] quote_currency_str: Option<&str>,
940        #[case] expected_amount: f64,
941        #[case] expected_currency: &str,
942    ) {
943        let mut msg: BinanceFuturesOrderUpdateMsg =
944            load_user_data_fixture("order_update_calculated.json");
945        msg.order.commission_asset = commission_asset.map(ustr::Ustr::from);
946        msg.order.commission = commission_amount.map(String::from);
947
948        let last_qty: f64 = msg.order.last_filled_qty.parse().unwrap();
949        let last_px: f64 = msg.order.last_filled_price.parse().unwrap();
950        let taker_fee = taker_fee_str.map(|s| Decimal::from_str_exact(s).unwrap());
951        let quote_currency = quote_currency_str.map(Currency::from);
952
953        let commission =
954            resolve_commission(&msg.order, last_qty, last_px, taker_fee, quote_currency);
955
956        assert_eq!(commission.currency, Currency::from(expected_currency));
957        let diff = (commission.as_f64() - expected_amount).abs();
958        assert!(
959            diff < 1e-4,
960            "expected {expected_amount}, was {}",
961            commission.as_f64()
962        );
963    }
964
965    #[rstest]
966    #[case::with_venue_position_id(
967        Some(Decimal::from_str_exact("0.0004").unwrap()),
968        Some(Currency::from("USDT")),
969        Some(PositionId::new("ETHUSDT-PERP.BINANCE-LONG")),
970    )]
971    #[case::without_extras(None, None, None)]
972    fn test_parse_fill_with_optional_params(
973        #[case] taker_fee: Option<Decimal>,
974        #[case] quote_currency: Option<Currency>,
975        #[case] venue_position_id: Option<PositionId>,
976    ) {
977        let msg: BinanceFuturesOrderUpdateMsg =
978            load_user_data_fixture("order_update_calculated.json");
979        let ts_init = UnixNanos::from(1_000_000_000u64);
980
981        let fill = parse_futures_order_update_to_fill(
982            &msg,
983            account_id(),
984            instrument_id(),
985            PRICE_PRECISION,
986            SIZE_PRECISION,
987            taker_fee,
988            quote_currency,
989            venue_position_id,
990            ts_init,
991        )
992        .unwrap();
993
994        assert_eq!(fill.venue_position_id, venue_position_id);
995        assert_eq!(fill.account_id, account_id());
996        assert_eq!(fill.instrument_id, instrument_id());
997    }
998}