1use nautilus_core::{UUID4, UnixNanos};
22use nautilus_model::{
23 enums::{
24 AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
25 TrailingOffsetType, TriggerType,
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::{
39 common::{
40 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
41 encoder::decode_client_order_id,
42 enums::{
43 BinanceAlgoStatus, BinanceFuturesOrderType, BinanceOrderStatus, BinanceSide,
44 BinanceTimeInForce, BinanceWorkingType,
45 },
46 parse::{
47 parse_millis_or_init, parse_required_decimal, parse_required_price_at_precision,
48 parse_required_quantity_at_precision,
49 },
50 },
51 futures::conversions::{normalize_futures_asset, parse_good_till_date},
52};
53
54pub fn parse_futures_order_update_to_order_status(
60 msg: &BinanceFuturesOrderUpdateMsg,
61 instrument_id: InstrumentId,
62 price_precision: u8,
63 size_precision: u8,
64 account_id: AccountId,
65 treat_expired_as_canceled: bool,
66 ts_init: UnixNanos,
67) -> anyhow::Result<OrderStatusReport> {
68 let order = &msg.order;
69 let ts_event = parse_millis_or_init(msg.event_time, "Futures order update event time", ts_init);
70
71 let client_order_id = decode_order_client_id(order)?;
72 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
73
74 let order_side = parse_side(order.side);
75 let order_status = parse_order_status(order.order_status, treat_expired_as_canceled);
76 let order_type = parse_futures_order_type(order.order_type);
77 let time_in_force = parse_time_in_force(order.time_in_force);
78
79 let quantity =
80 parse_required_quantity_at_precision(&order.original_qty, size_precision, "original_qty")?;
81 let filled_qty = parse_required_quantity_at_precision(
82 &order.cumulative_filled_qty,
83 size_precision,
84 "cumulative_filled_qty",
85 )?;
86 let price = parse_required_price_at_precision(
87 &order.original_price,
88 price_precision,
89 "original_price",
90 )?;
91
92 let avg_px = if filled_qty.as_decimal() > Decimal::ZERO {
93 parse_optional_positive_price_at_precision(&order.average_price, price_precision)
94 } else {
95 None
96 };
97
98 let mut report = OrderStatusReport::new(
99 account_id,
100 instrument_id,
101 Some(client_order_id),
102 venue_order_id,
103 order_side.into(),
104 order_type,
105 time_in_force,
106 order_status,
107 quantity,
108 filled_qty,
109 ts_event,
110 ts_event,
111 ts_init,
112 None, );
114
115 report.price = Some(price);
116 report.post_only = order.order_type == BinanceFuturesOrderType::Limit
117 && order.time_in_force == BinanceTimeInForce::Gtx;
118
119 match parse_good_till_date(order.good_till_date) {
120 Ok(expire_time) => report.expire_time = expire_time,
121 Err(e) => log::warn!("{e}; omitting Futures order expiry"),
122 }
123
124 if let Some(stop_price) =
125 parse_optional_positive_price_at_precision(&order.stop_price, price_precision)
126 {
127 report.trigger_price = Some(stop_price);
128 }
129
130 if let Some(offset) = order
131 .callback_rate
132 .as_deref()
133 .and_then(parse_trailing_offset_basis_points)
134 {
135 report.trailing_offset = Some(offset);
136 report.trailing_offset_type = Some(TrailingOffsetType::BasisPoints);
137 }
138
139 if let Some(activation_price) = order
140 .activation_price
141 .as_deref()
142 .and_then(|raw| parse_optional_positive_price_at_precision(raw, price_precision))
143 {
144 report.activation_price = Some(activation_price);
145 }
146
147 if let Some(avg) = avg_px {
148 report.avg_px = Some(avg.as_decimal());
149 }
150
151 Ok(report)
152}
153
154pub fn resolve_commission(
166 order: &OrderUpdateData,
167 last_qty: Quantity,
168 last_px: Price,
169 taker_fee: Option<Decimal>,
170 quote_currency: Option<Currency>,
171 bnfcr_currency: Currency,
172) -> anyhow::Result<Money> {
173 if order.commission.is_some() || order.commission_asset.is_some() {
174 let raw_commission = order.commission.as_deref().unwrap_or("0");
175 let amount = parse_required_decimal(raw_commission, "commission")?;
176 let currency = order.commission_asset.as_ref().map_or(bnfcr_currency, |a| {
177 normalize_futures_asset(a.as_str(), bnfcr_currency)
178 });
179 Money::from_decimal(amount, currency)
180 .map_err(|e| anyhow::anyhow!("invalid commission='{raw_commission}': {e}"))
181 } else if let Some(fee) = taker_fee {
182 let currency = quote_currency.unwrap_or_else(Currency::USDT);
183 let notional = last_qty
184 .as_decimal()
185 .checked_mul(last_px.as_decimal())
186 .ok_or_else(|| {
187 anyhow::anyhow!(
188 "invalid fee notional for last_qty='{last_qty}' and last_px='{last_px}': multiplication overflow",
189 )
190 })?;
191 let amount = fee.checked_mul(notional).ok_or_else(|| {
192 anyhow::anyhow!(
193 "invalid fee amount for taker_fee='{fee}' and notional='{notional}': multiplication overflow"
194 )
195 })?;
196 Money::from_decimal(amount, currency)
197 .map_err(|e| anyhow::anyhow!("invalid fee amount='{amount}': {e}"))
198 } else {
199 Ok(Money::zero(Currency::USDT()))
200 }
201}
202
203#[expect(clippy::too_many_arguments)]
209pub fn parse_futures_order_update_to_fill(
210 msg: &BinanceFuturesOrderUpdateMsg,
211 account_id: AccountId,
212 instrument_id: InstrumentId,
213 price_precision: u8,
214 size_precision: u8,
215 taker_fee: Option<Decimal>,
216 quote_currency: Option<Currency>,
217 bnfcr_currency: Currency,
218 venue_position_id: Option<PositionId>,
219 ts_init: UnixNanos,
220) -> anyhow::Result<FillReport> {
221 let order = &msg.order;
222 let ts_event = parse_millis_or_init(msg.event_time, "Futures fill event time", ts_init);
223
224 let client_order_id = decode_order_client_id(order)?;
225 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
226 let trade_id = TradeId::new(order.trade_id.to_string());
227
228 let order_side = parse_side(order.side);
229
230 let liquidity_side = if order.is_maker {
231 LiquiditySide::Maker
232 } else {
233 LiquiditySide::Taker
234 };
235
236 let last_qty = parse_required_quantity_at_precision(
237 &order.last_filled_qty,
238 size_precision,
239 "last_filled_qty",
240 )?;
241 let last_px = parse_required_price_at_precision(
242 &order.last_filled_price,
243 price_precision,
244 "last_filled_price",
245 )?;
246 let commission = resolve_commission(
247 order,
248 last_qty,
249 last_px,
250 taker_fee,
251 quote_currency,
252 bnfcr_currency,
253 )?;
254
255 Ok(FillReport::new(
256 account_id,
257 instrument_id,
258 venue_order_id,
259 trade_id,
260 order_side,
261 last_qty,
262 last_px,
263 commission,
264 liquidity_side,
265 Some(client_order_id),
266 venue_position_id,
267 ts_event,
268 ts_init,
269 None, ))
271}
272
273pub fn parse_futures_algo_update_to_order_status(
283 algo_data: &AlgoOrderUpdateData,
284 event_time: i64,
285 instrument_id: InstrumentId,
286 price_precision: u8,
287 size_precision: u8,
288 account_id: AccountId,
289 ts_init: UnixNanos,
290) -> anyhow::Result<Option<OrderStatusReport>> {
291 let ts_event =
292 parse_millis_or_init(event_time, "Futures algo order update event time", ts_init);
293
294 let client_order_id = decode_algo_client_id(algo_data)?;
295
296 let venue_order_id = algo_data
297 .actual_order_id
298 .as_ref()
299 .filter(|id| !id.is_empty())
300 .map_or_else(
301 || VenueOrderId::new(algo_data.algo_id.to_string()),
302 |id| VenueOrderId::new(id.clone()),
303 );
304
305 let order_status = match algo_data.algo_status {
306 BinanceAlgoStatus::Canceled | BinanceAlgoStatus::Expired => OrderStatus::Canceled,
307 BinanceAlgoStatus::Rejected => OrderStatus::Rejected,
308 _ => return Ok(None),
309 };
310
311 let order_side = parse_side(algo_data.side);
312 let order_type = parse_futures_order_type(algo_data.order_type);
313 let time_in_force = parse_time_in_force(algo_data.time_in_force);
314
315 let quantity =
316 parse_required_quantity_at_precision(&algo_data.quantity, size_precision, "quantity")?;
317 let trigger_price = parse_algo_trigger_price(algo_data, price_precision)?;
318 let price = parse_algo_limit_price(algo_data, price_precision)?;
319
320 let mut report = OrderStatusReport::new(
321 account_id,
322 instrument_id,
323 Some(client_order_id),
324 venue_order_id,
325 order_side.into(),
326 order_type,
327 time_in_force,
328 order_status,
329 quantity,
330 Quantity::zero(size_precision),
331 ts_event,
332 ts_event,
333 ts_init,
334 None, );
336
337 if let Some(price) = price {
338 report.price = Some(price);
339 }
340
341 if let Some(trigger_price) = trigger_price {
342 report.trigger_price = Some(trigger_price);
343 report.trigger_type = Some(parse_working_type(algo_data.working_type));
344 }
345
346 match parse_good_till_date(algo_data.good_till_date) {
347 Ok(expire_time) => report.expire_time = expire_time,
348 Err(e) => log::warn!("{e}; omitting Futures algo order expiry"),
349 }
350
351 Ok(Some(report))
352}
353
354pub fn parse_futures_account_update(
356 msg: &BinanceFuturesAccountUpdateMsg,
357 account_id: AccountId,
358 bnfcr_currency: Currency,
359 ts_init: UnixNanos,
360) -> Option<AccountState> {
361 let ts_event =
362 parse_millis_or_init(msg.event_time, "Futures account update event time", ts_init);
363
364 let balances: Vec<AccountBalance> = msg
365 .account
366 .balances
367 .iter()
368 .filter_map(|b| {
369 if b.wallet_balance.is_zero() {
370 return None;
371 }
372
373 let currency = normalize_futures_asset(b.asset, bnfcr_currency);
374 AccountBalance::from_total_and_free(b.wallet_balance, b.cross_wallet_balance, currency)
375 .ok()
376 })
377 .collect();
378
379 if balances.is_empty() {
380 return None;
381 }
382
383 Some(AccountState::new(
384 account_id,
385 AccountType::Margin,
386 balances,
387 vec![], true, UUID4::new(),
390 ts_event,
391 ts_init,
392 None, ))
394}
395
396pub fn decode_order_client_id(order: &OrderUpdateData) -> anyhow::Result<ClientOrderId> {
402 decode_client_order_id(&order.client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)
403}
404
405pub fn decode_algo_client_id(algo: &AlgoOrderUpdateData) -> anyhow::Result<ClientOrderId> {
411 decode_client_order_id(&algo.client_algo_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)
412}
413
414fn parse_optional_positive_price_at_precision(raw: &str, precision: u8) -> Option<Price> {
415 let decimal = parse_required_decimal(raw, "optional_price").ok()?;
416 if decimal <= Decimal::ZERO {
417 return None;
418 }
419
420 Price::from_decimal_dp(decimal, precision).ok()
421}
422
423fn parse_positive_price_at_precision(
424 raw: &str,
425 precision: u8,
426 field: &str,
427) -> anyhow::Result<Option<Price>> {
428 let decimal = parse_required_decimal(raw, field)?;
429 if decimal <= Decimal::ZERO {
430 return Ok(None);
431 }
432
433 Price::from_decimal_dp(decimal, precision)
434 .map(Some)
435 .map_err(|e| anyhow::anyhow!("invalid {field} precision: {e}"))
436}
437
438fn parse_algo_trigger_price(
439 algo_data: &AlgoOrderUpdateData,
440 price_precision: u8,
441) -> anyhow::Result<Option<Price>> {
442 let trigger_price = parse_positive_price_at_precision(
443 &algo_data.trigger_price,
444 price_precision,
445 "trigger_price",
446 )?;
447
448 if trigger_price.is_none() && requires_algo_trigger_price(algo_data.order_type) {
449 anyhow::bail!(
450 "missing positive trigger_price for Binance algo order type {:?}",
451 algo_data.order_type
452 );
453 }
454
455 Ok(trigger_price)
456}
457
458fn parse_algo_limit_price(
459 algo_data: &AlgoOrderUpdateData,
460 price_precision: u8,
461) -> anyhow::Result<Option<Price>> {
462 let price = parse_positive_price_at_precision(&algo_data.price, price_precision, "price")?;
463
464 if price.is_none() && requires_algo_limit_price(algo_data.order_type) {
465 anyhow::bail!(
466 "missing positive price for Binance algo order type {:?}",
467 algo_data.order_type
468 );
469 }
470
471 Ok(price)
472}
473
474fn parse_trailing_offset_basis_points(raw: &str) -> Option<Decimal> {
475 let rate = parse_required_decimal(raw, "callback_rate").ok()?;
476 if rate <= Decimal::ZERO {
477 return None;
478 }
479
480 rate.checked_mul(Decimal::from(100))
481}
482
483fn parse_working_type(working_type: BinanceWorkingType) -> TriggerType {
484 match working_type {
485 BinanceWorkingType::ContractPrice => TriggerType::LastPrice,
486 BinanceWorkingType::MarkPrice => TriggerType::MarkPrice,
487 BinanceWorkingType::Unknown => TriggerType::Default,
488 }
489}
490
491fn requires_algo_trigger_price(order_type: BinanceFuturesOrderType) -> bool {
492 matches!(
493 order_type,
494 BinanceFuturesOrderType::Stop
495 | BinanceFuturesOrderType::StopMarket
496 | BinanceFuturesOrderType::TakeProfit
497 | BinanceFuturesOrderType::TakeProfitMarket
498 )
499}
500
501fn requires_algo_limit_price(order_type: BinanceFuturesOrderType) -> bool {
502 matches!(
503 order_type,
504 BinanceFuturesOrderType::Stop | BinanceFuturesOrderType::TakeProfit
505 )
506}
507
508fn parse_side(side: BinanceSide) -> OrderSide {
509 match side {
510 BinanceSide::Buy => OrderSide::Buy,
511 BinanceSide::Sell => OrderSide::Sell,
512 }
513}
514
515fn parse_order_status(status: BinanceOrderStatus, treat_expired_as_canceled: bool) -> OrderStatus {
516 match status {
517 BinanceOrderStatus::New | BinanceOrderStatus::PendingNew => OrderStatus::Accepted,
518 BinanceOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
519 BinanceOrderStatus::Filled
520 | BinanceOrderStatus::NewAdl
521 | BinanceOrderStatus::NewInsurance => OrderStatus::Filled,
522 BinanceOrderStatus::Canceled | BinanceOrderStatus::PendingCancel => OrderStatus::Canceled,
523 BinanceOrderStatus::Rejected => OrderStatus::Rejected,
524 BinanceOrderStatus::Expired | BinanceOrderStatus::ExpiredInMatch => {
525 if treat_expired_as_canceled {
526 OrderStatus::Canceled
527 } else {
528 OrderStatus::Expired
529 }
530 }
531 BinanceOrderStatus::Unknown => OrderStatus::Accepted,
532 }
533}
534
535fn parse_futures_order_type(order_type: BinanceFuturesOrderType) -> OrderType {
536 match order_type {
537 BinanceFuturesOrderType::Limit => OrderType::Limit,
538 BinanceFuturesOrderType::Market => OrderType::Market,
539 BinanceFuturesOrderType::Stop => OrderType::StopLimit,
540 BinanceFuturesOrderType::StopMarket => OrderType::StopMarket,
541 BinanceFuturesOrderType::TakeProfit => OrderType::LimitIfTouched,
542 BinanceFuturesOrderType::TakeProfitMarket => OrderType::MarketIfTouched,
543 BinanceFuturesOrderType::TrailingStopMarket => OrderType::TrailingStopMarket,
544 BinanceFuturesOrderType::Liquidation
545 | BinanceFuturesOrderType::Adl
546 | BinanceFuturesOrderType::Unknown => OrderType::Market,
547 }
548}
549
550fn parse_time_in_force(tif: BinanceTimeInForce) -> TimeInForce {
551 match tif {
552 BinanceTimeInForce::Gtc | BinanceTimeInForce::Gtx => TimeInForce::Gtc,
553 BinanceTimeInForce::Ioc | BinanceTimeInForce::Rpi => TimeInForce::Ioc,
554 BinanceTimeInForce::Fok => TimeInForce::Fok,
555 BinanceTimeInForce::Gtd => TimeInForce::Gtd,
556 BinanceTimeInForce::Unknown => TimeInForce::Gtc,
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use nautilus_model::enums::OrderSide;
563 use rstest::rstest;
564 use serde::de::DeserializeOwned;
565
566 use super::*;
567 use crate::{
568 common::{
569 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
570 encoder::encode_broker_id,
571 enums::{BinancePriceMatch, BinanceSelfTradePreventionMode},
572 testing::load_fixture_string,
573 },
574 futures::websocket::streams::messages::{
575 BinanceFuturesAccountUpdateMsg, BinanceFuturesAlgoUpdateMsg,
576 BinanceFuturesOrderUpdateMsg,
577 },
578 };
579
580 const PRICE_PRECISION: u8 = 2;
581 const SIZE_PRECISION: u8 = 3;
582
583 fn instrument_id() -> InstrumentId {
584 InstrumentId::from("ETHUSDT-PERP.BINANCE")
585 }
586
587 fn account_id() -> AccountId {
588 AccountId::from("BINANCE-FUTURES-001")
589 }
590
591 fn load_user_data_fixture<T: DeserializeOwned>(filename: &str) -> T {
592 let path = format!("futures/user_data_json/{filename}");
593 serde_json::from_str(&load_fixture_string(&path))
594 .unwrap_or_else(|e| panic!("Failed to parse fixture {path}: {e}"))
595 }
596
597 #[rstest]
598 fn test_parse_order_update_to_order_status_new() {
599 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
600 let ts_init = UnixNanos::from(1_000_000_000u64);
601
602 let report = parse_futures_order_update_to_order_status(
603 &msg,
604 instrument_id(),
605 PRICE_PRECISION,
606 SIZE_PRECISION,
607 account_id(),
608 false,
609 ts_init,
610 )
611 .unwrap();
612
613 assert_eq!(report.account_id, account_id());
614 assert_eq!(report.instrument_id, instrument_id());
615 assert_eq!(report.order_side, OrderSide::Buy.into());
616 assert_eq!(report.order_status, OrderStatus::Accepted);
617 assert_eq!(report.order_type, OrderType::TrailingStopMarket);
618 assert_eq!(report.venue_order_id, VenueOrderId::new("8886774"));
619 assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
620 }
621
622 #[rstest]
623 #[case::negative(-1)]
624 #[case::overflow(i64::MAX)]
625 fn test_parse_order_update_to_order_status_falls_back_for_invalid_timestamp(
626 #[case] event_time: i64,
627 ) {
628 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
629 msg.event_time = event_time;
630
631 let ts_init = UnixNanos::from(1);
632 let report = parse_futures_order_update_to_order_status(
633 &msg,
634 instrument_id(),
635 PRICE_PRECISION,
636 SIZE_PRECISION,
637 account_id(),
638 false,
639 ts_init,
640 )
641 .unwrap();
642
643 assert_eq!(report.ts_accepted, ts_init);
644 assert_eq!(report.ts_last, ts_init);
645 assert_eq!(report.ts_init, ts_init);
646 }
647
648 #[rstest]
649 fn test_parse_order_update_to_order_status_captures_activation_price() {
650 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
651 msg.order.activation_price = Some("1650.50".to_string());
652 let ts_init = UnixNanos::from(1_000_000_000u64);
653
654 let report = parse_futures_order_update_to_order_status(
655 &msg,
656 instrument_id(),
657 PRICE_PRECISION,
658 SIZE_PRECISION,
659 account_id(),
660 false,
661 ts_init,
662 )
663 .unwrap();
664
665 assert_eq!(report.activation_price, Some(Price::from("1650.50")));
666 }
667
668 #[rstest]
669 fn test_parse_order_update_to_order_status_preserves_good_till_date() {
670 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
671 msg.order.time_in_force = BinanceTimeInForce::Gtd;
672 msg.order.good_till_date = Some(1_700_000_601_000);
673 let ts_init = UnixNanos::from(1_000_000_000u64);
674
675 let report = parse_futures_order_update_to_order_status(
676 &msg,
677 instrument_id(),
678 PRICE_PRECISION,
679 SIZE_PRECISION,
680 account_id(),
681 false,
682 ts_init,
683 )
684 .unwrap();
685
686 assert_eq!(report.time_in_force, TimeInForce::Gtd);
687 assert_eq!(
688 report.expire_time,
689 Some(UnixNanos::from_millis(1_700_000_601_000)),
690 );
691 }
692
693 #[rstest]
694 fn test_parse_order_update_to_order_status_rejects_invalid_quantity() {
695 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
696 msg.order.original_qty = "not-a-number".to_string();
697 let ts_init = UnixNanos::from(1_000_000_000u64);
698
699 let result = parse_futures_order_update_to_order_status(
700 &msg,
701 instrument_id(),
702 PRICE_PRECISION,
703 SIZE_PRECISION,
704 account_id(),
705 false,
706 ts_init,
707 );
708
709 let error = result.unwrap_err().to_string();
710 assert!(error.contains("original_qty"));
711 }
712
713 #[rstest]
714 fn test_parse_order_update_to_order_status_rejects_invalid_filled_quantity() {
715 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
716 msg.order.cumulative_filled_qty = "not-a-number".to_string();
717 let ts_init = UnixNanos::from(1_000_000_000u64);
718
719 let result = parse_futures_order_update_to_order_status(
720 &msg,
721 instrument_id(),
722 PRICE_PRECISION,
723 SIZE_PRECISION,
724 account_id(),
725 false,
726 ts_init,
727 );
728
729 let error = result.unwrap_err().to_string();
730 assert!(error.contains("cumulative_filled_qty"));
731 }
732
733 #[rstest]
734 fn test_parse_order_update_to_order_status_rejects_invalid_price() {
735 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
736 msg.order.original_price = "not-a-number".to_string();
737 let ts_init = UnixNanos::from(1_000_000_000u64);
738
739 let result = parse_futures_order_update_to_order_status(
740 &msg,
741 instrument_id(),
742 PRICE_PRECISION,
743 SIZE_PRECISION,
744 account_id(),
745 false,
746 ts_init,
747 );
748
749 let error = result.unwrap_err().to_string();
750 assert!(error.contains("original_price"));
751 }
752
753 #[rstest]
754 fn test_parse_order_update_to_order_status_skips_invalid_optional_fields() {
755 let mut msg: BinanceFuturesOrderUpdateMsg =
756 load_user_data_fixture("order_update_trade.json");
757 msg.order.average_price = "not-a-number".to_string();
758 msg.order.stop_price = "not-a-number".to_string();
759 msg.order.callback_rate = Some("not-a-number".to_string());
760 let ts_init = UnixNanos::from(1_000_000_000u64);
761
762 let report = parse_futures_order_update_to_order_status(
763 &msg,
764 instrument_id(),
765 PRICE_PRECISION,
766 SIZE_PRECISION,
767 account_id(),
768 false,
769 ts_init,
770 )
771 .unwrap();
772
773 assert!(report.avg_px.is_none());
774 assert!(report.trigger_price.is_none());
775 assert_eq!(report.trailing_offset, None);
776 }
777
778 #[rstest]
779 fn test_parse_order_update_to_fill_report() {
780 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
781 let ts_init = UnixNanos::from(1_000_000_000u64);
782
783 assert_eq!(
784 msg.order.stp_mode,
785 Some(BinanceSelfTradePreventionMode::ExpireTaker),
786 );
787
788 let report = parse_futures_order_update_to_fill(
789 &msg,
790 account_id(),
791 instrument_id(),
792 PRICE_PRECISION,
793 SIZE_PRECISION,
794 None,
795 None,
796 Currency::USDT(),
797 None,
798 ts_init,
799 )
800 .unwrap();
801
802 assert_eq!(report.account_id, account_id());
803 assert_eq!(report.instrument_id, instrument_id());
804 assert_eq!(report.order_side, OrderSide::Buy);
805 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
806 assert_eq!(report.trade_id, TradeId::new("12345678"));
807 assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
808 assert_eq!(report.last_qty, Quantity::new(0.001, SIZE_PRECISION));
809 assert_eq!(report.last_px, Price::new(7100.50, PRICE_PRECISION));
810 }
811
812 #[rstest]
813 fn test_parse_order_update_to_fill_rejects_invalid_price() {
814 let mut msg: BinanceFuturesOrderUpdateMsg =
815 load_user_data_fixture("order_update_trade.json");
816 msg.order.last_filled_price = "not-a-number".to_string();
817 let ts_init = UnixNanos::from(1_000_000_000u64);
818
819 let result = parse_futures_order_update_to_fill(
820 &msg,
821 account_id(),
822 instrument_id(),
823 PRICE_PRECISION,
824 SIZE_PRECISION,
825 None,
826 None,
827 Currency::USDT(),
828 None,
829 ts_init,
830 );
831
832 let error = result.unwrap_err().to_string();
833 assert!(error.contains("last_filled_price"));
834 }
835
836 #[rstest]
837 fn test_parse_order_update_to_fill_rejects_invalid_quantity() {
838 let mut msg: BinanceFuturesOrderUpdateMsg =
839 load_user_data_fixture("order_update_trade.json");
840 msg.order.last_filled_qty = "not-a-number".to_string();
841 let ts_init = UnixNanos::from(1_000_000_000u64);
842
843 let result = parse_futures_order_update_to_fill(
844 &msg,
845 account_id(),
846 instrument_id(),
847 PRICE_PRECISION,
848 SIZE_PRECISION,
849 None,
850 None,
851 Currency::USDT(),
852 None,
853 ts_init,
854 );
855
856 let error = result.unwrap_err().to_string();
857 assert!(error.contains("last_filled_qty"));
858 }
859
860 #[rstest]
861 fn test_parse_order_update_to_fill_rejects_invalid_commission() {
862 let mut msg: BinanceFuturesOrderUpdateMsg =
863 load_user_data_fixture("order_update_trade.json");
864 msg.order.commission = Some("not-a-number".to_string());
865 let ts_init = UnixNanos::from(1_000_000_000u64);
866
867 let result = parse_futures_order_update_to_fill(
868 &msg,
869 account_id(),
870 instrument_id(),
871 PRICE_PRECISION,
872 SIZE_PRECISION,
873 None,
874 None,
875 Currency::USDT(),
876 None,
877 ts_init,
878 );
879
880 let error = result.unwrap_err().to_string();
881 assert!(error.contains("commission"));
882 }
883
884 #[rstest]
885 fn test_parse_account_update() {
886 let msg: BinanceFuturesAccountUpdateMsg = load_user_data_fixture("account_update.json");
887 let ts_init = UnixNanos::from(1_000_000_000u64);
888
889 let state =
890 parse_futures_account_update(&msg, account_id(), Currency::USDT(), ts_init).unwrap();
891
892 assert_eq!(state.account_id, account_id());
893 assert_eq!(state.account_type, AccountType::Margin);
894 assert!(state.is_reported);
895 assert_eq!(state.balances.len(), 1);
896 }
897
898 #[rstest]
902 #[case(Currency::USDT())]
903 #[case(Currency::USDC())]
904 fn test_parse_account_update_maps_bnfcr_to_configured_currency(
905 #[case] bnfcr_currency: Currency,
906 ) {
907 let msg: BinanceFuturesAccountUpdateMsg =
908 load_user_data_fixture("account_update_bnfcr.json");
909 let ts_init = UnixNanos::from(1_000_000_000u64);
910
911 let state =
912 parse_futures_account_update(&msg, account_id(), bnfcr_currency, ts_init).unwrap();
913
914 assert_eq!(state.balances.len(), 1);
915 assert_eq!(state.balances[0].total.currency, bnfcr_currency);
916 assert_eq!(
917 state.balances[0].total.as_decimal(),
918 Decimal::from_str_exact("5001.28983031").unwrap()
919 );
920 }
921
922 #[rstest]
926 fn test_parse_account_update_precision_drift() {
927 let json = r#"{
928 "e": "ACCOUNT_UPDATE",
929 "E": 1700000000000,
930 "T": 1700000000000,
931 "a": {
932 "m": "ORDER",
933 "B": [{
934 "a": "USDT",
935 "wb": "10.000000034999",
936 "cw": "9.999999994999"
937 }],
938 "P": []
939 }
940 }"#;
941 let msg: BinanceFuturesAccountUpdateMsg = serde_json::from_str(json).unwrap();
942 let ts_init = UnixNanos::from(1_000_000_000u64);
943
944 let state =
945 parse_futures_account_update(&msg, account_id(), Currency::USDT(), ts_init).unwrap();
946
947 assert_eq!(state.balances.len(), 1);
948 let balance = &state.balances[0];
949 assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
950 }
951
952 #[rstest]
953 fn test_parse_algo_update_to_order_status_canceled() {
954 let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_canceled.json");
955 let ts_init = UnixNanos::from(1_000_000_000u64);
956
957 assert_eq!(
958 msg.algo_order.stp_mode,
959 Some(BinanceSelfTradePreventionMode::ExpireMaker),
960 );
961 assert_eq!(msg.algo_order.price_match, Some(BinancePriceMatch::None));
962
963 let report = parse_futures_algo_update_to_order_status(
964 &msg.algo_order,
965 msg.event_time,
966 instrument_id(),
967 PRICE_PRECISION,
968 SIZE_PRECISION,
969 account_id(),
970 ts_init,
971 )
972 .unwrap()
973 .unwrap();
974
975 assert_eq!(report.account_id, account_id());
976 assert_eq!(report.instrument_id, instrument_id());
977 assert_eq!(
978 report.client_order_id,
979 Some(ClientOrderId::new("Q5xaq5EGKgXXa0fD7fs0Ip")),
980 );
981 assert_eq!(report.venue_order_id, VenueOrderId::new("2148719"));
982 assert_eq!(report.order_side, OrderSide::Sell.into());
983 assert_eq!(report.order_type, OrderType::LimitIfTouched);
984 assert_eq!(report.time_in_force, TimeInForce::Gtc);
985 assert_eq!(report.order_status, OrderStatus::Canceled);
986 assert_eq!(report.quantity, Quantity::new(0.01, SIZE_PRECISION));
987 assert_eq!(report.filled_qty, Quantity::new(0.0, SIZE_PRECISION));
988 assert_eq!(report.price, Some(Price::from("750.00")));
989 assert_eq!(report.trigger_price, Some(Price::from("750.00")));
990 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
991 assert_eq!(
992 report.ts_accepted,
993 UnixNanos::from(1_750_515_742_303_000_000u64)
994 );
995 assert_eq!(
996 report.ts_last,
997 UnixNanos::from(1_750_515_742_303_000_000u64)
998 );
999 assert_eq!(report.ts_init, ts_init);
1000 }
1001
1002 #[rstest]
1003 fn test_parse_algo_update_to_order_status_preserves_good_till_date() {
1004 let mut msg: BinanceFuturesAlgoUpdateMsg =
1005 load_user_data_fixture("algo_update_canceled.json");
1006 msg.algo_order.time_in_force = BinanceTimeInForce::Gtd;
1007 msg.algo_order.good_till_date = Some(1_700_000_601_000);
1008 let ts_init = UnixNanos::from(1_000_000_000u64);
1009
1010 let report = parse_futures_algo_update_to_order_status(
1011 &msg.algo_order,
1012 msg.event_time,
1013 instrument_id(),
1014 PRICE_PRECISION,
1015 SIZE_PRECISION,
1016 account_id(),
1017 ts_init,
1018 )
1019 .unwrap()
1020 .unwrap();
1021
1022 assert_eq!(report.time_in_force, TimeInForce::Gtd);
1023 assert_eq!(
1024 report.expire_time,
1025 Some(UnixNanos::from_millis(1_700_000_601_000)),
1026 );
1027 }
1028
1029 #[rstest]
1030 fn test_parse_algo_update_to_order_status_new_returns_none() {
1031 let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_new.json");
1032 let report = parse_futures_algo_update_to_order_status(
1033 &msg.algo_order,
1034 msg.event_time,
1035 instrument_id(),
1036 PRICE_PRECISION,
1037 SIZE_PRECISION,
1038 account_id(),
1039 UnixNanos::default(),
1040 );
1041
1042 assert!(report.unwrap().is_none());
1043 }
1044
1045 #[rstest]
1046 fn test_parse_algo_update_to_order_status_rejects_invalid_trigger_price() {
1047 let mut msg: BinanceFuturesAlgoUpdateMsg =
1048 load_user_data_fixture("algo_update_canceled.json");
1049 msg.algo_order.trigger_price = "not-a-number".to_string();
1050
1051 let result = parse_futures_algo_update_to_order_status(
1052 &msg.algo_order,
1053 msg.event_time,
1054 instrument_id(),
1055 PRICE_PRECISION,
1056 SIZE_PRECISION,
1057 account_id(),
1058 UnixNanos::default(),
1059 );
1060
1061 let error = result.unwrap_err().to_string();
1062 assert!(error.contains("trigger_price"));
1063 }
1064
1065 #[rstest]
1066 fn test_parse_algo_update_to_order_status_rejects_missing_trigger_price() {
1067 let mut msg: BinanceFuturesAlgoUpdateMsg =
1068 load_user_data_fixture("algo_update_canceled.json");
1069 msg.algo_order.trigger_price = "0".to_string();
1070
1071 let result = parse_futures_algo_update_to_order_status(
1072 &msg.algo_order,
1073 msg.event_time,
1074 instrument_id(),
1075 PRICE_PRECISION,
1076 SIZE_PRECISION,
1077 account_id(),
1078 UnixNanos::default(),
1079 );
1080
1081 let error = result.unwrap_err().to_string();
1082 assert!(error.contains("missing positive trigger_price"));
1083 }
1084
1085 #[rstest]
1086 fn test_parse_algo_update_to_order_status_rejects_missing_limit_price() {
1087 let mut msg: BinanceFuturesAlgoUpdateMsg =
1088 load_user_data_fixture("algo_update_canceled.json");
1089 msg.algo_order.price = "0".to_string();
1090
1091 let result = parse_futures_algo_update_to_order_status(
1092 &msg.algo_order,
1093 msg.event_time,
1094 instrument_id(),
1095 PRICE_PRECISION,
1096 SIZE_PRECISION,
1097 account_id(),
1098 UnixNanos::default(),
1099 );
1100
1101 let error = result.unwrap_err().to_string();
1102 assert!(error.contains("missing positive price"));
1103 }
1104
1105 #[rstest]
1106 fn test_parse_algo_update_to_order_status_rejects_invalid_quantity() {
1107 let mut msg: BinanceFuturesAlgoUpdateMsg =
1108 load_user_data_fixture("algo_update_canceled.json");
1109 msg.algo_order.quantity = "not-a-number".to_string();
1110
1111 let result = parse_futures_algo_update_to_order_status(
1112 &msg.algo_order,
1113 msg.event_time,
1114 instrument_id(),
1115 PRICE_PRECISION,
1116 SIZE_PRECISION,
1117 account_id(),
1118 UnixNanos::default(),
1119 );
1120
1121 let error = result.unwrap_err().to_string();
1122 assert!(error.contains("quantity"));
1123 }
1124
1125 #[rstest]
1126 fn test_decode_order_client_id() {
1127 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
1128 let original = ClientOrderId::from("O-20200101-000000-000-000-1");
1129 msg.order.client_order_id = encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1130
1131 let decoded = decode_order_client_id(&msg.order).unwrap();
1132
1133 assert_eq!(decoded, original);
1134 }
1135
1136 #[rstest]
1137 #[case::empty("", "invalid Binance client order ID ''")]
1138 #[case::whitespace(" ", "invalid Binance client order ID ' '")]
1139 #[case::non_ascii("client-é", "invalid Binance client order ID 'client-é'")]
1140 #[case::malformed_prefixed("x-aHRE4BCj-R", "missing raw broker client order ID payload")]
1141 fn test_decode_order_client_id_rejects_invalid_input(
1142 #[case] client_order_id: &str,
1143 #[case] expected: &str,
1144 ) {
1145 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
1146 msg.order.client_order_id = client_order_id.to_string();
1147
1148 let result = decode_order_client_id(&msg.order);
1149
1150 assert_eq!(result.unwrap_err().to_string(), expected);
1151 }
1152
1153 #[rstest]
1154 fn test_decode_algo_client_id() {
1155 let mut msg: BinanceFuturesAlgoUpdateMsg =
1156 load_user_data_fixture("algo_update_canceled.json");
1157 let original = ClientOrderId::from("O-20200101-000000-000-000-2");
1158 msg.algo_order.client_algo_id =
1159 encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1160
1161 let decoded = decode_algo_client_id(&msg.algo_order).unwrap();
1162
1163 assert_eq!(decoded, original);
1164 }
1165
1166 #[rstest]
1167 fn test_decode_algo_client_id_rejects_malformed_prefixed_input() {
1168 let mut msg: BinanceFuturesAlgoUpdateMsg =
1169 load_user_data_fixture("algo_update_canceled.json");
1170 msg.algo_order.client_algo_id = "x-aHRE4BCj-Tinvalid".to_string();
1171
1172 let result = decode_algo_client_id(&msg.algo_order);
1173
1174 assert_eq!(
1175 result.unwrap_err().to_string(),
1176 "invalid O-format broker client order ID payload length"
1177 );
1178 }
1179
1180 #[rstest]
1181 fn test_parse_liquidation_fill() {
1182 let msg: BinanceFuturesOrderUpdateMsg =
1183 load_user_data_fixture("order_update_calculated.json");
1184 let ts_init = UnixNanos::from(1_000_000_000u64);
1185
1186 assert!(msg.order.is_liquidation());
1187 assert!(msg.order.is_exchange_generated());
1188
1189 let fill = parse_futures_order_update_to_fill(
1190 &msg,
1191 account_id(),
1192 instrument_id(),
1193 PRICE_PRECISION,
1194 SIZE_PRECISION,
1195 None,
1196 None,
1197 Currency::USDT(),
1198 None,
1199 ts_init,
1200 )
1201 .unwrap();
1202
1203 assert_eq!(fill.account_id, account_id());
1204 assert_eq!(fill.instrument_id, instrument_id());
1205 assert_eq!(
1206 fill.client_order_id,
1207 Some(ClientOrderId::new("autoclose-1234567890"))
1208 );
1209 assert_eq!(fill.venue_order_id, VenueOrderId::new("8886999"));
1210 assert_eq!(fill.trade_id, TradeId::new("12345999"));
1211 assert_eq!(fill.order_side, OrderSide::Sell);
1212 assert_eq!(fill.last_qty, Quantity::new(0.014, SIZE_PRECISION));
1213 assert_eq!(fill.last_px, Price::new(9910.12, PRICE_PRECISION));
1214 assert_eq!(
1215 fill.commission,
1216 Money::new(0.06937084, Currency::from("USDT"))
1217 );
1218 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
1219 }
1220
1221 #[rstest]
1222 fn test_parse_liquidation_status_report() {
1223 let msg: BinanceFuturesOrderUpdateMsg =
1224 load_user_data_fixture("order_update_calculated.json");
1225 let ts_init = UnixNanos::from(1_000_000_000u64);
1226
1227 let status = parse_futures_order_update_to_order_status(
1228 &msg,
1229 instrument_id(),
1230 PRICE_PRECISION,
1231 SIZE_PRECISION,
1232 account_id(),
1233 false,
1234 ts_init,
1235 )
1236 .unwrap();
1237
1238 assert_eq!(status.account_id, account_id());
1239 assert_eq!(status.instrument_id, instrument_id());
1240 assert_eq!(
1241 status.client_order_id,
1242 Some(ClientOrderId::new("autoclose-1234567890"))
1243 );
1244 assert_eq!(status.venue_order_id, VenueOrderId::new("8886999"));
1245 assert_eq!(status.order_side, OrderSide::Sell.into());
1246 assert_eq!(status.order_status, OrderStatus::Filled);
1247 assert_eq!(status.quantity, Quantity::new(0.014, SIZE_PRECISION));
1248 assert_eq!(status.filled_qty, Quantity::new(0.014, SIZE_PRECISION));
1249 }
1250
1251 #[rstest]
1252 fn test_parse_adl_fill_with_new_adl_status() {
1253 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
1254 let ts_init = UnixNanos::from(1_000_000_000u64);
1255
1256 assert!(msg.order.is_adl());
1257 assert!(msg.order.is_exchange_generated());
1258 assert!(!msg.order.is_liquidation());
1259
1260 let fill = parse_futures_order_update_to_fill(
1261 &msg,
1262 account_id(),
1263 instrument_id(),
1264 PRICE_PRECISION,
1265 SIZE_PRECISION,
1266 None,
1267 None,
1268 Currency::USDT(),
1269 None,
1270 ts_init,
1271 )
1272 .unwrap();
1273
1274 assert_eq!(
1275 fill.client_order_id,
1276 Some(ClientOrderId::new("adl_autoclose_12345"))
1277 );
1278 assert_eq!(fill.venue_order_id, VenueOrderId::new("8887001"));
1279 assert_eq!(fill.order_side, OrderSide::Buy);
1280 assert_eq!(fill.last_qty, Quantity::new(0.005, SIZE_PRECISION));
1281 assert_eq!(fill.last_px, Price::new(42000.00, PRICE_PRECISION));
1282 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
1283 }
1284
1285 #[rstest]
1286 fn test_parse_adl_status_report_maps_new_adl_to_filled() {
1287 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
1288 let ts_init = UnixNanos::from(1_000_000_000u64);
1289
1290 let status = parse_futures_order_update_to_order_status(
1291 &msg,
1292 instrument_id(),
1293 PRICE_PRECISION,
1294 SIZE_PRECISION,
1295 account_id(),
1296 false,
1297 ts_init,
1298 )
1299 .unwrap();
1300
1301 assert_eq!(status.order_status, OrderStatus::Filled);
1302 assert_eq!(status.filled_qty, Quantity::new(0.005, SIZE_PRECISION));
1303 }
1304
1305 #[rstest]
1306 fn test_parse_settlement_fill_with_trade_exec_type() {
1307 let msg: BinanceFuturesOrderUpdateMsg =
1308 load_user_data_fixture("order_update_settlement.json");
1309 let ts_init = UnixNanos::from(1_000_000_000u64);
1310
1311 assert!(msg.order.is_settlement());
1312 assert!(msg.order.is_exchange_generated());
1313 assert!(!msg.order.is_liquidation());
1314 assert!(!msg.order.is_adl());
1315
1316 let fill = parse_futures_order_update_to_fill(
1317 &msg,
1318 account_id(),
1319 instrument_id(),
1320 PRICE_PRECISION,
1321 SIZE_PRECISION,
1322 None,
1323 None,
1324 Currency::USDT(),
1325 None,
1326 ts_init,
1327 )
1328 .unwrap();
1329
1330 assert_eq!(
1331 fill.client_order_id,
1332 Some(ClientOrderId::new("settlement_autoclose-9999"))
1333 );
1334 assert_eq!(fill.venue_order_id, VenueOrderId::new("8887002"));
1335 assert_eq!(fill.order_side, OrderSide::Sell);
1336 assert_eq!(fill.last_qty, Quantity::new(0.010, SIZE_PRECISION));
1337 assert_eq!(fill.last_px, Price::new(50000.00, PRICE_PRECISION));
1338 }
1339
1340 #[rstest]
1341 fn test_parse_order_status_new_adl_maps_to_filled() {
1342 let result = parse_order_status(BinanceOrderStatus::NewAdl, false);
1343 assert_eq!(result, OrderStatus::Filled);
1344 }
1345
1346 #[rstest]
1347 fn test_parse_order_status_new_insurance_maps_to_filled() {
1348 let result = parse_order_status(BinanceOrderStatus::NewInsurance, false);
1349 assert_eq!(result, OrderStatus::Filled);
1350 }
1351
1352 #[rstest]
1353 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
1354 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
1355 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
1356 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
1357 fn test_parse_order_status_expired_respects_treat_as_canceled(
1358 #[case] status: BinanceOrderStatus,
1359 #[case] treat_expired_as_canceled: bool,
1360 #[case] expected: OrderStatus,
1361 ) {
1362 let result = parse_order_status(status, treat_expired_as_canceled);
1363 assert_eq!(result, expected);
1364 }
1365
1366 #[rstest]
1367 fn test_is_exchange_generated_autoclose() {
1368 let msg: BinanceFuturesOrderUpdateMsg =
1369 load_user_data_fixture("order_update_calculated.json");
1370 assert!(msg.order.is_exchange_generated());
1371 assert!(msg.order.is_liquidation());
1372 }
1373
1374 #[rstest]
1375 fn test_is_exchange_generated_adl_autoclose() {
1376 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
1377 assert!(msg.order.is_exchange_generated());
1378 assert!(msg.order.is_adl());
1379 }
1380
1381 #[rstest]
1382 fn test_is_exchange_generated_settlement_autoclose() {
1383 let msg: BinanceFuturesOrderUpdateMsg =
1384 load_user_data_fixture("order_update_settlement.json");
1385 assert!(msg.order.is_exchange_generated());
1386 assert!(msg.order.is_settlement());
1387 }
1388
1389 #[rstest]
1390 fn test_is_exchange_generated_delivery_autoclose() {
1391 let msg: BinanceFuturesOrderUpdateMsg =
1392 load_user_data_fixture("order_update_delivery.json");
1393 assert!(msg.order.is_exchange_generated());
1394 assert!(msg.order.is_settlement());
1395 assert!(!msg.order.is_liquidation());
1396 assert!(!msg.order.is_adl());
1397 }
1398
1399 #[rstest]
1400 fn test_normal_order_is_not_exchange_generated() {
1401 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
1402 assert!(!msg.order.is_exchange_generated());
1403 assert!(!msg.order.is_liquidation());
1404 assert!(!msg.order.is_adl());
1405 assert!(!msg.order.is_settlement());
1406 }
1407
1408 #[rstest]
1409 fn test_parse_insurance_fill_with_new_insurance_status() {
1410 let msg: BinanceFuturesOrderUpdateMsg =
1411 load_user_data_fixture("order_update_insurance.json");
1412
1413 assert!(msg.order.is_liquidation());
1414 assert!(msg.order.is_exchange_generated());
1415 assert_eq!(msg.order.order_status, BinanceOrderStatus::NewInsurance);
1416
1417 let fill = parse_futures_order_update_to_fill(
1418 &msg,
1419 account_id(),
1420 instrument_id(),
1421 PRICE_PRECISION,
1422 SIZE_PRECISION,
1423 None,
1424 None,
1425 Currency::USDT(),
1426 None,
1427 UnixNanos::from(1_000_000_000u64),
1428 )
1429 .unwrap();
1430
1431 assert_eq!(
1432 fill.client_order_id,
1433 Some(ClientOrderId::new("autoclose-insurance-5678"))
1434 );
1435 assert_eq!(fill.order_side, OrderSide::Sell);
1436 assert_eq!(fill.last_qty, Quantity::new(0.020, SIZE_PRECISION));
1437 assert_eq!(fill.last_px, Price::new(45000.00, PRICE_PRECISION));
1438 }
1439
1440 #[rstest]
1441 fn test_parse_insurance_status_maps_new_insurance_to_filled() {
1442 let msg: BinanceFuturesOrderUpdateMsg =
1443 load_user_data_fixture("order_update_insurance.json");
1444
1445 let status = parse_futures_order_update_to_order_status(
1446 &msg,
1447 instrument_id(),
1448 PRICE_PRECISION,
1449 SIZE_PRECISION,
1450 account_id(),
1451 false,
1452 UnixNanos::from(1_000_000_000u64),
1453 )
1454 .unwrap();
1455
1456 assert_eq!(status.order_status, OrderStatus::Filled);
1457 }
1458
1459 #[rstest]
1460 fn test_parse_settlement_status_report() {
1461 let msg: BinanceFuturesOrderUpdateMsg =
1462 load_user_data_fixture("order_update_settlement.json");
1463
1464 let status = parse_futures_order_update_to_order_status(
1465 &msg,
1466 instrument_id(),
1467 PRICE_PRECISION,
1468 SIZE_PRECISION,
1469 account_id(),
1470 false,
1471 UnixNanos::from(1_000_000_000u64),
1472 )
1473 .unwrap();
1474
1475 assert_eq!(status.order_status, OrderStatus::Filled);
1476 assert_eq!(status.order_side, OrderSide::Sell.into());
1477 assert_eq!(status.quantity, Quantity::new(0.010, SIZE_PRECISION));
1478 assert_eq!(status.filled_qty, Quantity::new(0.010, SIZE_PRECISION));
1479 }
1480
1481 #[rstest]
1482 fn test_pending_liquidation_has_zero_fill_qty() {
1483 let msg: BinanceFuturesOrderUpdateMsg =
1484 load_user_data_fixture("order_update_calculated_pending.json");
1485
1486 assert!(msg.order.is_exchange_generated());
1487 assert!(msg.order.is_liquidation());
1488
1489 let last_qty = parse_required_decimal(&msg.order.last_filled_qty, "last_filled_qty")
1490 .expect("last_filled_qty should parse");
1491 assert!(last_qty.is_zero());
1492 }
1493
1494 #[rstest]
1495 #[case::venue_provided(Some("USDT"), Some("0.06937084"), None, None, 0.06937084, "USDT")]
1496 #[case::fallback_from_taker_fee(
1497 None, None,
1498 Some("0.0004"), Some("USDT"),
1499 0.055496, "USDT" )]
1501 #[case::no_commission_no_fee(None, None, None, None, 0.0, "USDT")]
1502 fn test_resolve_commission(
1503 #[case] commission_asset: Option<&str>,
1504 #[case] commission_amount: Option<&str>,
1505 #[case] taker_fee_str: Option<&str>,
1506 #[case] quote_currency_str: Option<&str>,
1507 #[case] expected_amount: f64,
1508 #[case] expected_currency: &str,
1509 ) {
1510 let mut msg: BinanceFuturesOrderUpdateMsg =
1511 load_user_data_fixture("order_update_calculated.json");
1512 msg.order.commission_asset = commission_asset.map(ustr::Ustr::from);
1513 msg.order.commission = commission_amount.map(String::from);
1514
1515 let last_qty = Quantity::from_decimal_dp(
1516 Decimal::from_str_exact(&msg.order.last_filled_qty).unwrap(),
1517 SIZE_PRECISION,
1518 )
1519 .unwrap();
1520 let last_px = Price::from_decimal_dp(
1521 Decimal::from_str_exact(&msg.order.last_filled_price).unwrap(),
1522 PRICE_PRECISION,
1523 )
1524 .unwrap();
1525 let taker_fee = taker_fee_str.map(|s| Decimal::from_str_exact(s).unwrap());
1526 let quote_currency = quote_currency_str.map(Currency::from);
1527
1528 let commission = resolve_commission(
1529 &msg.order,
1530 last_qty,
1531 last_px,
1532 taker_fee,
1533 quote_currency,
1534 Currency::USDT(),
1535 )
1536 .unwrap();
1537
1538 assert_eq!(commission.currency, Currency::from(expected_currency));
1539 let diff = (commission.as_f64() - expected_amount).abs();
1540 assert!(
1541 diff < 1e-4,
1542 "expected {expected_amount}, was {}",
1543 commission.as_f64()
1544 );
1545 }
1546
1547 #[rstest]
1548 #[case::with_venue_position_id(
1549 Some(Decimal::from_str_exact("0.0004").unwrap()),
1550 Some(Currency::from("USDT")),
1551 Some(PositionId::new("ETHUSDT-PERP.BINANCE-LONG")),
1552 )]
1553 #[case::without_extras(None, None, None)]
1554 fn test_parse_fill_with_optional_params(
1555 #[case] taker_fee: Option<Decimal>,
1556 #[case] quote_currency: Option<Currency>,
1557 #[case] venue_position_id: Option<PositionId>,
1558 ) {
1559 let msg: BinanceFuturesOrderUpdateMsg =
1560 load_user_data_fixture("order_update_calculated.json");
1561 let ts_init = UnixNanos::from(1_000_000_000u64);
1562
1563 let fill = parse_futures_order_update_to_fill(
1564 &msg,
1565 account_id(),
1566 instrument_id(),
1567 PRICE_PRECISION,
1568 SIZE_PRECISION,
1569 taker_fee,
1570 quote_currency,
1571 Currency::USDT(),
1572 venue_position_id,
1573 ts_init,
1574 )
1575 .unwrap();
1576
1577 assert_eq!(fill.venue_position_id, venue_position_id);
1578 assert_eq!(fill.account_id, account_id());
1579 assert_eq!(fill.instrument_id, instrument_id());
1580 }
1581}