1use 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::{
39 common::{
40 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
41 encoder::decode_broker_id,
42 enums::{
43 BinanceAlgoStatus, BinanceFuturesOrderType, BinanceOrderStatus, BinanceSide,
44 BinanceTimeInForce,
45 },
46 parse::{
47 parse_required_decimal, parse_required_price_at_precision,
48 parse_required_quantity_at_precision,
49 },
50 },
51 futures::conversions::normalize_futures_asset,
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 = UnixNanos::from_millis(msg.event_time as u64);
70
71 let client_order_id = ClientOrderId::new(decode_broker_id(
72 &order.client_order_id,
73 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
74 ));
75 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
76
77 let order_side = parse_side(order.side);
78 let order_status = parse_order_status(order.order_status, treat_expired_as_canceled);
79 let order_type = parse_futures_order_type(order.order_type);
80 let time_in_force = parse_time_in_force(order.time_in_force);
81
82 let quantity =
83 parse_required_quantity_at_precision(&order.original_qty, size_precision, "original_qty")?;
84 let filled_qty = parse_required_quantity_at_precision(
85 &order.cumulative_filled_qty,
86 size_precision,
87 "cumulative_filled_qty",
88 )?;
89 let price = parse_required_price_at_precision(
90 &order.original_price,
91 price_precision,
92 "original_price",
93 )?;
94
95 let avg_px = if filled_qty.as_decimal() > Decimal::ZERO {
96 parse_optional_positive_price_at_precision(&order.average_price, price_precision)
97 } else {
98 None
99 };
100
101 let mut report = OrderStatusReport::new(
102 account_id,
103 instrument_id,
104 Some(client_order_id),
105 venue_order_id,
106 order_side,
107 order_type,
108 time_in_force,
109 order_status,
110 quantity,
111 filled_qty,
112 ts_event,
113 ts_event,
114 ts_init,
115 None, );
117
118 report.price = Some(price);
119 report.post_only = order.order_type == BinanceFuturesOrderType::Limit
120 && order.time_in_force == BinanceTimeInForce::Gtx;
121
122 if let Some(stop_price) =
123 parse_optional_positive_price_at_precision(&order.stop_price, price_precision)
124 {
125 report.trigger_price = Some(stop_price);
126 }
127
128 if let Some(offset) = order
129 .callback_rate
130 .as_deref()
131 .and_then(parse_trailing_offset_basis_points)
132 {
133 report.trailing_offset = Some(offset);
134 report.trailing_offset_type = TrailingOffsetType::BasisPoints;
135 }
136
137 if let Some(avg) = avg_px {
138 report.avg_px = Some(avg.as_decimal());
139 }
140
141 Ok(report)
142}
143
144pub fn resolve_commission(
156 order: &OrderUpdateData,
157 last_qty: Quantity,
158 last_px: Price,
159 taker_fee: Option<Decimal>,
160 quote_currency: Option<Currency>,
161 bnfcr_currency: Currency,
162) -> anyhow::Result<Money> {
163 if order.commission.is_some() || order.commission_asset.is_some() {
164 let raw_commission = order.commission.as_deref().unwrap_or("0");
165 let amount = parse_required_decimal(raw_commission, "commission")?;
166 let currency = order.commission_asset.as_ref().map_or(bnfcr_currency, |a| {
167 normalize_futures_asset(a.as_str(), bnfcr_currency)
168 });
169 Money::from_decimal(amount, currency)
170 .map_err(|e| anyhow::anyhow!("invalid commission='{raw_commission}': {e}"))
171 } else if let Some(fee) = taker_fee {
172 let currency = quote_currency.unwrap_or_else(Currency::USDT);
173 let notional = last_qty
174 .as_decimal()
175 .checked_mul(last_px.as_decimal())
176 .ok_or_else(|| {
177 anyhow::anyhow!(
178 "invalid fee notional for last_qty='{last_qty}' and last_px='{last_px}': multiplication overflow",
179 )
180 })?;
181 let amount = fee.checked_mul(notional).ok_or_else(|| {
182 anyhow::anyhow!(
183 "invalid fee amount for taker_fee='{fee}' and notional='{notional}': multiplication overflow"
184 )
185 })?;
186 Money::from_decimal(amount, currency)
187 .map_err(|e| anyhow::anyhow!("invalid fee amount='{amount}': {e}"))
188 } else {
189 Ok(Money::zero(Currency::USDT()))
190 }
191}
192
193#[expect(clippy::too_many_arguments)]
199pub fn parse_futures_order_update_to_fill(
200 msg: &BinanceFuturesOrderUpdateMsg,
201 account_id: AccountId,
202 instrument_id: InstrumentId,
203 price_precision: u8,
204 size_precision: u8,
205 taker_fee: Option<Decimal>,
206 quote_currency: Option<Currency>,
207 bnfcr_currency: Currency,
208 venue_position_id: Option<PositionId>,
209 ts_init: UnixNanos,
210) -> anyhow::Result<FillReport> {
211 let order = &msg.order;
212 let ts_event = UnixNanos::from_millis(msg.event_time as u64);
213
214 let client_order_id = ClientOrderId::new(decode_broker_id(
215 &order.client_order_id,
216 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
217 ));
218 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
219 let trade_id = TradeId::new(order.trade_id.to_string());
220
221 let order_side = parse_side(order.side);
222
223 let liquidity_side = if order.is_maker {
224 LiquiditySide::Maker
225 } else {
226 LiquiditySide::Taker
227 };
228
229 let last_qty = parse_required_quantity_at_precision(
230 &order.last_filled_qty,
231 size_precision,
232 "last_filled_qty",
233 )?;
234 let last_px = parse_required_price_at_precision(
235 &order.last_filled_price,
236 price_precision,
237 "last_filled_price",
238 )?;
239 let commission = resolve_commission(
240 order,
241 last_qty,
242 last_px,
243 taker_fee,
244 quote_currency,
245 bnfcr_currency,
246 )?;
247
248 Ok(FillReport::new(
249 account_id,
250 instrument_id,
251 venue_order_id,
252 trade_id,
253 order_side,
254 last_qty,
255 last_px,
256 commission,
257 liquidity_side,
258 Some(client_order_id),
259 venue_position_id,
260 ts_event,
261 ts_init,
262 None, ))
264}
265
266pub fn parse_futures_algo_update_to_order_status(
275 algo_data: &AlgoOrderUpdateData,
276 event_time: i64,
277 instrument_id: InstrumentId,
278 _price_precision: u8,
279 size_precision: u8,
280 account_id: AccountId,
281 ts_init: UnixNanos,
282) -> anyhow::Result<Option<OrderStatusReport>> {
283 let ts_event = UnixNanos::from_millis(event_time as u64);
284
285 let client_order_id = ClientOrderId::new(decode_broker_id(
286 &algo_data.client_algo_id,
287 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
288 ));
289
290 let venue_order_id = algo_data
291 .actual_order_id
292 .as_ref()
293 .filter(|id| !id.is_empty())
294 .map_or_else(
295 || VenueOrderId::new(algo_data.algo_id.to_string()),
296 |id| VenueOrderId::new(id.clone()),
297 );
298
299 let order_status = match algo_data.algo_status {
300 BinanceAlgoStatus::Canceled | BinanceAlgoStatus::Expired => OrderStatus::Canceled,
301 BinanceAlgoStatus::Rejected => OrderStatus::Rejected,
302 _ => return Ok(None),
303 };
304
305 let order_side = parse_side(algo_data.side);
306 let order_type = parse_futures_order_type(algo_data.order_type);
307 let time_in_force = parse_time_in_force(algo_data.time_in_force);
308
309 let quantity =
310 parse_required_quantity_at_precision(&algo_data.quantity, size_precision, "quantity")?;
311
312 let report = OrderStatusReport::new(
313 account_id,
314 instrument_id,
315 Some(client_order_id),
316 venue_order_id,
317 order_side,
318 order_type,
319 time_in_force,
320 order_status,
321 quantity,
322 Quantity::zero(size_precision),
323 ts_event,
324 ts_event,
325 ts_init,
326 None, );
328
329 Ok(Some(report))
330}
331
332pub fn parse_futures_account_update(
334 msg: &BinanceFuturesAccountUpdateMsg,
335 account_id: AccountId,
336 bnfcr_currency: Currency,
337 ts_init: UnixNanos,
338) -> Option<AccountState> {
339 let ts_event = UnixNanos::from_millis(msg.event_time as u64);
340
341 let balances: Vec<AccountBalance> = msg
342 .account
343 .balances
344 .iter()
345 .filter_map(|b| {
346 if b.wallet_balance.is_zero() {
347 return None;
348 }
349
350 let currency = normalize_futures_asset(b.asset, bnfcr_currency);
351 AccountBalance::from_total_and_free(b.wallet_balance, b.cross_wallet_balance, currency)
352 .ok()
353 })
354 .collect();
355
356 if balances.is_empty() {
357 return None;
358 }
359
360 Some(AccountState::new(
361 account_id,
362 AccountType::Margin,
363 balances,
364 vec![], true, UUID4::new(),
367 ts_event,
368 ts_init,
369 None, ))
371}
372
373pub fn decode_order_client_id(order: &OrderUpdateData) -> ClientOrderId {
375 ClientOrderId::new(decode_broker_id(
376 &order.client_order_id,
377 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
378 ))
379}
380
381pub fn decode_algo_client_id(algo: &AlgoOrderUpdateData) -> ClientOrderId {
383 ClientOrderId::new(decode_broker_id(
384 &algo.client_algo_id,
385 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
386 ))
387}
388
389fn parse_optional_positive_price_at_precision(raw: &str, precision: u8) -> Option<Price> {
390 let decimal = parse_required_decimal(raw, "optional_price").ok()?;
391 if decimal <= Decimal::ZERO {
392 return None;
393 }
394
395 Price::from_decimal_dp(decimal, precision).ok()
396}
397
398fn parse_trailing_offset_basis_points(raw: &str) -> Option<Decimal> {
399 let rate = parse_required_decimal(raw, "callback_rate").ok()?;
400 if rate <= Decimal::ZERO {
401 return None;
402 }
403
404 rate.checked_mul(Decimal::from(100))
405}
406
407fn parse_side(side: BinanceSide) -> OrderSide {
408 match side {
409 BinanceSide::Buy => OrderSide::Buy,
410 BinanceSide::Sell => OrderSide::Sell,
411 }
412}
413
414fn parse_order_status(status: BinanceOrderStatus, treat_expired_as_canceled: bool) -> OrderStatus {
415 match status {
416 BinanceOrderStatus::New | BinanceOrderStatus::PendingNew => OrderStatus::Accepted,
417 BinanceOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
418 BinanceOrderStatus::Filled
419 | BinanceOrderStatus::NewAdl
420 | BinanceOrderStatus::NewInsurance => OrderStatus::Filled,
421 BinanceOrderStatus::Canceled | BinanceOrderStatus::PendingCancel => OrderStatus::Canceled,
422 BinanceOrderStatus::Rejected => OrderStatus::Rejected,
423 BinanceOrderStatus::Expired | BinanceOrderStatus::ExpiredInMatch => {
424 if treat_expired_as_canceled {
425 OrderStatus::Canceled
426 } else {
427 OrderStatus::Expired
428 }
429 }
430 BinanceOrderStatus::Unknown => OrderStatus::Accepted,
431 }
432}
433
434fn parse_futures_order_type(order_type: BinanceFuturesOrderType) -> OrderType {
435 match order_type {
436 BinanceFuturesOrderType::Limit => OrderType::Limit,
437 BinanceFuturesOrderType::Market => OrderType::Market,
438 BinanceFuturesOrderType::Stop => OrderType::StopLimit,
439 BinanceFuturesOrderType::StopMarket => OrderType::StopMarket,
440 BinanceFuturesOrderType::TakeProfit => OrderType::LimitIfTouched,
441 BinanceFuturesOrderType::TakeProfitMarket => OrderType::MarketIfTouched,
442 BinanceFuturesOrderType::TrailingStopMarket => OrderType::TrailingStopMarket,
443 BinanceFuturesOrderType::Liquidation
444 | BinanceFuturesOrderType::Adl
445 | BinanceFuturesOrderType::Unknown => OrderType::Market,
446 }
447}
448
449fn parse_time_in_force(tif: BinanceTimeInForce) -> TimeInForce {
450 match tif {
451 BinanceTimeInForce::Gtc | BinanceTimeInForce::Gtx => TimeInForce::Gtc,
452 BinanceTimeInForce::Ioc | BinanceTimeInForce::Rpi => TimeInForce::Ioc,
453 BinanceTimeInForce::Fok => TimeInForce::Fok,
454 BinanceTimeInForce::Gtd => TimeInForce::Gtd,
455 BinanceTimeInForce::Unknown => TimeInForce::Gtc,
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use rstest::rstest;
462 use serde::de::DeserializeOwned;
463
464 use super::*;
465 use crate::{
466 common::{
467 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
468 encoder::encode_broker_id,
469 enums::{BinancePriceMatch, BinanceSelfTradePreventionMode},
470 testing::load_fixture_string,
471 },
472 futures::websocket::streams::messages::{
473 BinanceFuturesAccountUpdateMsg, BinanceFuturesAlgoUpdateMsg,
474 BinanceFuturesOrderUpdateMsg,
475 },
476 };
477
478 const PRICE_PRECISION: u8 = 2;
479 const SIZE_PRECISION: u8 = 3;
480
481 fn instrument_id() -> InstrumentId {
482 InstrumentId::from("ETHUSDT-PERP.BINANCE")
483 }
484
485 fn account_id() -> AccountId {
486 AccountId::from("BINANCE-FUTURES-001")
487 }
488
489 fn load_user_data_fixture<T: DeserializeOwned>(filename: &str) -> T {
490 let path = format!("futures/user_data_json/{filename}");
491 serde_json::from_str(&load_fixture_string(&path))
492 .unwrap_or_else(|e| panic!("Failed to parse fixture {path}: {e}"))
493 }
494
495 #[rstest]
496 fn test_parse_order_update_to_order_status_new() {
497 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
498 let ts_init = UnixNanos::from(1_000_000_000u64);
499
500 let report = parse_futures_order_update_to_order_status(
501 &msg,
502 instrument_id(),
503 PRICE_PRECISION,
504 SIZE_PRECISION,
505 account_id(),
506 false,
507 ts_init,
508 )
509 .unwrap();
510
511 assert_eq!(report.account_id, account_id());
512 assert_eq!(report.instrument_id, instrument_id());
513 assert_eq!(report.order_side, OrderSide::Buy);
514 assert_eq!(report.order_status, OrderStatus::Accepted);
515 assert_eq!(report.order_type, OrderType::TrailingStopMarket);
516 assert_eq!(report.venue_order_id, VenueOrderId::new("8886774"));
517 assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
518 }
519
520 #[rstest]
521 fn test_parse_order_update_to_order_status_rejects_invalid_quantity() {
522 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
523 msg.order.original_qty = "not-a-number".to_string();
524 let ts_init = UnixNanos::from(1_000_000_000u64);
525
526 let result = parse_futures_order_update_to_order_status(
527 &msg,
528 instrument_id(),
529 PRICE_PRECISION,
530 SIZE_PRECISION,
531 account_id(),
532 false,
533 ts_init,
534 );
535
536 let error = result.unwrap_err().to_string();
537 assert!(error.contains("original_qty"));
538 }
539
540 #[rstest]
541 fn test_parse_order_update_to_order_status_rejects_invalid_filled_quantity() {
542 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
543 msg.order.cumulative_filled_qty = "not-a-number".to_string();
544 let ts_init = UnixNanos::from(1_000_000_000u64);
545
546 let result = parse_futures_order_update_to_order_status(
547 &msg,
548 instrument_id(),
549 PRICE_PRECISION,
550 SIZE_PRECISION,
551 account_id(),
552 false,
553 ts_init,
554 );
555
556 let error = result.unwrap_err().to_string();
557 assert!(error.contains("cumulative_filled_qty"));
558 }
559
560 #[rstest]
561 fn test_parse_order_update_to_order_status_rejects_invalid_price() {
562 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
563 msg.order.original_price = "not-a-number".to_string();
564 let ts_init = UnixNanos::from(1_000_000_000u64);
565
566 let result = parse_futures_order_update_to_order_status(
567 &msg,
568 instrument_id(),
569 PRICE_PRECISION,
570 SIZE_PRECISION,
571 account_id(),
572 false,
573 ts_init,
574 );
575
576 let error = result.unwrap_err().to_string();
577 assert!(error.contains("original_price"));
578 }
579
580 #[rstest]
581 fn test_parse_order_update_to_order_status_skips_invalid_optional_fields() {
582 let mut msg: BinanceFuturesOrderUpdateMsg =
583 load_user_data_fixture("order_update_trade.json");
584 msg.order.average_price = "not-a-number".to_string();
585 msg.order.stop_price = "not-a-number".to_string();
586 msg.order.callback_rate = Some("not-a-number".to_string());
587 let ts_init = UnixNanos::from(1_000_000_000u64);
588
589 let report = parse_futures_order_update_to_order_status(
590 &msg,
591 instrument_id(),
592 PRICE_PRECISION,
593 SIZE_PRECISION,
594 account_id(),
595 false,
596 ts_init,
597 )
598 .unwrap();
599
600 assert!(report.avg_px.is_none());
601 assert!(report.trigger_price.is_none());
602 assert_eq!(report.trailing_offset, None);
603 }
604
605 #[rstest]
606 fn test_parse_order_update_to_fill_report() {
607 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
608 let ts_init = UnixNanos::from(1_000_000_000u64);
609
610 assert_eq!(
611 msg.order.stp_mode,
612 Some(BinanceSelfTradePreventionMode::ExpireTaker),
613 );
614
615 let report = parse_futures_order_update_to_fill(
616 &msg,
617 account_id(),
618 instrument_id(),
619 PRICE_PRECISION,
620 SIZE_PRECISION,
621 None,
622 None,
623 Currency::USDT(),
624 None,
625 ts_init,
626 )
627 .unwrap();
628
629 assert_eq!(report.account_id, account_id());
630 assert_eq!(report.instrument_id, instrument_id());
631 assert_eq!(report.order_side, OrderSide::Buy);
632 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
633 assert_eq!(report.trade_id, TradeId::new("12345678"));
634 assert_eq!(report.client_order_id, Some(ClientOrderId::from("TEST")));
635 assert_eq!(report.last_qty, Quantity::new(0.001, SIZE_PRECISION));
636 assert_eq!(report.last_px, Price::new(7100.50, PRICE_PRECISION));
637 }
638
639 #[rstest]
640 fn test_parse_order_update_to_fill_rejects_invalid_price() {
641 let mut msg: BinanceFuturesOrderUpdateMsg =
642 load_user_data_fixture("order_update_trade.json");
643 msg.order.last_filled_price = "not-a-number".to_string();
644 let ts_init = UnixNanos::from(1_000_000_000u64);
645
646 let result = parse_futures_order_update_to_fill(
647 &msg,
648 account_id(),
649 instrument_id(),
650 PRICE_PRECISION,
651 SIZE_PRECISION,
652 None,
653 None,
654 Currency::USDT(),
655 None,
656 ts_init,
657 );
658
659 let error = result.unwrap_err().to_string();
660 assert!(error.contains("last_filled_price"));
661 }
662
663 #[rstest]
664 fn test_parse_order_update_to_fill_rejects_invalid_quantity() {
665 let mut msg: BinanceFuturesOrderUpdateMsg =
666 load_user_data_fixture("order_update_trade.json");
667 msg.order.last_filled_qty = "not-a-number".to_string();
668 let ts_init = UnixNanos::from(1_000_000_000u64);
669
670 let result = parse_futures_order_update_to_fill(
671 &msg,
672 account_id(),
673 instrument_id(),
674 PRICE_PRECISION,
675 SIZE_PRECISION,
676 None,
677 None,
678 Currency::USDT(),
679 None,
680 ts_init,
681 );
682
683 let error = result.unwrap_err().to_string();
684 assert!(error.contains("last_filled_qty"));
685 }
686
687 #[rstest]
688 fn test_parse_order_update_to_fill_rejects_invalid_commission() {
689 let mut msg: BinanceFuturesOrderUpdateMsg =
690 load_user_data_fixture("order_update_trade.json");
691 msg.order.commission = Some("not-a-number".to_string());
692 let ts_init = UnixNanos::from(1_000_000_000u64);
693
694 let result = parse_futures_order_update_to_fill(
695 &msg,
696 account_id(),
697 instrument_id(),
698 PRICE_PRECISION,
699 SIZE_PRECISION,
700 None,
701 None,
702 Currency::USDT(),
703 None,
704 ts_init,
705 );
706
707 let error = result.unwrap_err().to_string();
708 assert!(error.contains("commission"));
709 }
710
711 #[rstest]
712 fn test_parse_account_update() {
713 let msg: BinanceFuturesAccountUpdateMsg = load_user_data_fixture("account_update.json");
714 let ts_init = UnixNanos::from(1_000_000_000u64);
715
716 let state =
717 parse_futures_account_update(&msg, account_id(), Currency::USDT(), ts_init).unwrap();
718
719 assert_eq!(state.account_id, account_id());
720 assert_eq!(state.account_type, AccountType::Margin);
721 assert!(state.is_reported);
722 assert_eq!(state.balances.len(), 1);
723 }
724
725 #[rstest]
729 #[case(Currency::USDT())]
730 #[case(Currency::USDC())]
731 fn test_parse_account_update_maps_bnfcr_to_configured_currency(
732 #[case] bnfcr_currency: Currency,
733 ) {
734 let msg: BinanceFuturesAccountUpdateMsg =
735 load_user_data_fixture("account_update_bnfcr.json");
736 let ts_init = UnixNanos::from(1_000_000_000u64);
737
738 let state =
739 parse_futures_account_update(&msg, account_id(), bnfcr_currency, ts_init).unwrap();
740
741 assert_eq!(state.balances.len(), 1);
742 assert_eq!(state.balances[0].total.currency, bnfcr_currency);
743 assert_eq!(
744 state.balances[0].total.as_decimal(),
745 Decimal::from_str_exact("5001.28983031").unwrap()
746 );
747 }
748
749 #[rstest]
753 fn test_parse_account_update_precision_drift() {
754 let json = r#"{
755 "e": "ACCOUNT_UPDATE",
756 "E": 1700000000000,
757 "T": 1700000000000,
758 "a": {
759 "m": "ORDER",
760 "B": [{
761 "a": "USDT",
762 "wb": "10.000000034999",
763 "cw": "9.999999994999"
764 }],
765 "P": []
766 }
767 }"#;
768 let msg: BinanceFuturesAccountUpdateMsg = serde_json::from_str(json).unwrap();
769 let ts_init = UnixNanos::from(1_000_000_000u64);
770
771 let state =
772 parse_futures_account_update(&msg, account_id(), Currency::USDT(), ts_init).unwrap();
773
774 assert_eq!(state.balances.len(), 1);
775 let balance = &state.balances[0];
776 assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
777 }
778
779 #[rstest]
780 fn test_parse_algo_update_to_order_status_canceled() {
781 let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_canceled.json");
782 let ts_init = UnixNanos::from(1_000_000_000u64);
783
784 assert_eq!(
785 msg.algo_order.stp_mode,
786 Some(BinanceSelfTradePreventionMode::ExpireMaker),
787 );
788 assert_eq!(msg.algo_order.price_match, Some(BinancePriceMatch::None));
789
790 let report = parse_futures_algo_update_to_order_status(
791 &msg.algo_order,
792 msg.event_time,
793 instrument_id(),
794 PRICE_PRECISION,
795 SIZE_PRECISION,
796 account_id(),
797 ts_init,
798 )
799 .unwrap()
800 .unwrap();
801
802 assert_eq!(report.account_id, account_id());
803 assert_eq!(report.instrument_id, instrument_id());
804 assert_eq!(
805 report.client_order_id,
806 Some(ClientOrderId::new("Q5xaq5EGKgXXa0fD7fs0Ip")),
807 );
808 assert_eq!(report.venue_order_id, VenueOrderId::new("2148719"));
809 assert_eq!(report.order_side, OrderSide::Sell);
810 assert_eq!(report.order_type, OrderType::LimitIfTouched);
811 assert_eq!(report.time_in_force, TimeInForce::Gtc);
812 assert_eq!(report.order_status, OrderStatus::Canceled);
813 assert_eq!(report.quantity, Quantity::new(0.01, SIZE_PRECISION));
814 assert_eq!(report.filled_qty, Quantity::new(0.0, SIZE_PRECISION));
815 assert_eq!(
816 report.ts_accepted,
817 UnixNanos::from(1_750_515_742_303_000_000u64)
818 );
819 assert_eq!(
820 report.ts_last,
821 UnixNanos::from(1_750_515_742_303_000_000u64)
822 );
823 assert_eq!(report.ts_init, ts_init);
824 }
825
826 #[rstest]
827 fn test_parse_algo_update_to_order_status_new_returns_none() {
828 let msg: BinanceFuturesAlgoUpdateMsg = load_user_data_fixture("algo_update_new.json");
829 let report = parse_futures_algo_update_to_order_status(
830 &msg.algo_order,
831 msg.event_time,
832 instrument_id(),
833 PRICE_PRECISION,
834 SIZE_PRECISION,
835 account_id(),
836 UnixNanos::default(),
837 );
838
839 assert!(report.unwrap().is_none());
840 }
841
842 #[rstest]
843 fn test_parse_algo_update_to_order_status_rejects_invalid_quantity() {
844 let mut msg: BinanceFuturesAlgoUpdateMsg =
845 load_user_data_fixture("algo_update_canceled.json");
846 msg.algo_order.quantity = "not-a-number".to_string();
847
848 let result = parse_futures_algo_update_to_order_status(
849 &msg.algo_order,
850 msg.event_time,
851 instrument_id(),
852 PRICE_PRECISION,
853 SIZE_PRECISION,
854 account_id(),
855 UnixNanos::default(),
856 );
857
858 let error = result.unwrap_err().to_string();
859 assert!(error.contains("quantity"));
860 }
861
862 #[rstest]
863 fn test_decode_order_client_id() {
864 let mut msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_new.json");
865 let original = ClientOrderId::from("O-20200101-000000-000-000-1");
866 msg.order.client_order_id = encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
867
868 let decoded = decode_order_client_id(&msg.order);
869
870 assert_eq!(decoded, original);
871 }
872
873 #[rstest]
874 fn test_decode_algo_client_id() {
875 let mut msg: BinanceFuturesAlgoUpdateMsg =
876 load_user_data_fixture("algo_update_canceled.json");
877 let original = ClientOrderId::from("O-20200101-000000-000-000-2");
878 msg.algo_order.client_algo_id =
879 encode_broker_id(&original, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
880
881 let decoded = decode_algo_client_id(&msg.algo_order);
882
883 assert_eq!(decoded, original);
884 }
885
886 #[rstest]
887 fn test_parse_liquidation_fill() {
888 let msg: BinanceFuturesOrderUpdateMsg =
889 load_user_data_fixture("order_update_calculated.json");
890 let ts_init = UnixNanos::from(1_000_000_000u64);
891
892 assert!(msg.order.is_liquidation());
893 assert!(msg.order.is_exchange_generated());
894
895 let fill = parse_futures_order_update_to_fill(
896 &msg,
897 account_id(),
898 instrument_id(),
899 PRICE_PRECISION,
900 SIZE_PRECISION,
901 None,
902 None,
903 Currency::USDT(),
904 None,
905 ts_init,
906 )
907 .unwrap();
908
909 assert_eq!(fill.account_id, account_id());
910 assert_eq!(fill.instrument_id, instrument_id());
911 assert_eq!(
912 fill.client_order_id,
913 Some(ClientOrderId::new("autoclose-1234567890"))
914 );
915 assert_eq!(fill.venue_order_id, VenueOrderId::new("8886999"));
916 assert_eq!(fill.trade_id, TradeId::new("12345999"));
917 assert_eq!(fill.order_side, OrderSide::Sell);
918 assert_eq!(fill.last_qty, Quantity::new(0.014, SIZE_PRECISION));
919 assert_eq!(fill.last_px, Price::new(9910.12, PRICE_PRECISION));
920 assert_eq!(
921 fill.commission,
922 Money::new(0.06937084, Currency::from("USDT"))
923 );
924 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
925 }
926
927 #[rstest]
928 fn test_parse_liquidation_status_report() {
929 let msg: BinanceFuturesOrderUpdateMsg =
930 load_user_data_fixture("order_update_calculated.json");
931 let ts_init = UnixNanos::from(1_000_000_000u64);
932
933 let status = parse_futures_order_update_to_order_status(
934 &msg,
935 instrument_id(),
936 PRICE_PRECISION,
937 SIZE_PRECISION,
938 account_id(),
939 false,
940 ts_init,
941 )
942 .unwrap();
943
944 assert_eq!(status.account_id, account_id());
945 assert_eq!(status.instrument_id, instrument_id());
946 assert_eq!(
947 status.client_order_id,
948 Some(ClientOrderId::new("autoclose-1234567890"))
949 );
950 assert_eq!(status.venue_order_id, VenueOrderId::new("8886999"));
951 assert_eq!(status.order_side, OrderSide::Sell);
952 assert_eq!(status.order_status, OrderStatus::Filled);
953 assert_eq!(status.quantity, Quantity::new(0.014, SIZE_PRECISION));
954 assert_eq!(status.filled_qty, Quantity::new(0.014, SIZE_PRECISION));
955 }
956
957 #[rstest]
958 fn test_parse_adl_fill_with_new_adl_status() {
959 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
960 let ts_init = UnixNanos::from(1_000_000_000u64);
961
962 assert!(msg.order.is_adl());
963 assert!(msg.order.is_exchange_generated());
964 assert!(!msg.order.is_liquidation());
965
966 let fill = parse_futures_order_update_to_fill(
967 &msg,
968 account_id(),
969 instrument_id(),
970 PRICE_PRECISION,
971 SIZE_PRECISION,
972 None,
973 None,
974 Currency::USDT(),
975 None,
976 ts_init,
977 )
978 .unwrap();
979
980 assert_eq!(
981 fill.client_order_id,
982 Some(ClientOrderId::new("adl_autoclose_12345"))
983 );
984 assert_eq!(fill.venue_order_id, VenueOrderId::new("8887001"));
985 assert_eq!(fill.order_side, OrderSide::Buy);
986 assert_eq!(fill.last_qty, Quantity::new(0.005, SIZE_PRECISION));
987 assert_eq!(fill.last_px, Price::new(42000.00, PRICE_PRECISION));
988 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
989 }
990
991 #[rstest]
992 fn test_parse_adl_status_report_maps_new_adl_to_filled() {
993 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
994 let ts_init = UnixNanos::from(1_000_000_000u64);
995
996 let status = parse_futures_order_update_to_order_status(
997 &msg,
998 instrument_id(),
999 PRICE_PRECISION,
1000 SIZE_PRECISION,
1001 account_id(),
1002 false,
1003 ts_init,
1004 )
1005 .unwrap();
1006
1007 assert_eq!(status.order_status, OrderStatus::Filled);
1008 assert_eq!(status.filled_qty, Quantity::new(0.005, SIZE_PRECISION));
1009 }
1010
1011 #[rstest]
1012 fn test_parse_settlement_fill_with_trade_exec_type() {
1013 let msg: BinanceFuturesOrderUpdateMsg =
1014 load_user_data_fixture("order_update_settlement.json");
1015 let ts_init = UnixNanos::from(1_000_000_000u64);
1016
1017 assert!(msg.order.is_settlement());
1018 assert!(msg.order.is_exchange_generated());
1019 assert!(!msg.order.is_liquidation());
1020 assert!(!msg.order.is_adl());
1021
1022 let fill = parse_futures_order_update_to_fill(
1023 &msg,
1024 account_id(),
1025 instrument_id(),
1026 PRICE_PRECISION,
1027 SIZE_PRECISION,
1028 None,
1029 None,
1030 Currency::USDT(),
1031 None,
1032 ts_init,
1033 )
1034 .unwrap();
1035
1036 assert_eq!(
1037 fill.client_order_id,
1038 Some(ClientOrderId::new("settlement_autoclose-9999"))
1039 );
1040 assert_eq!(fill.venue_order_id, VenueOrderId::new("8887002"));
1041 assert_eq!(fill.order_side, OrderSide::Sell);
1042 assert_eq!(fill.last_qty, Quantity::new(0.010, SIZE_PRECISION));
1043 assert_eq!(fill.last_px, Price::new(50000.00, PRICE_PRECISION));
1044 }
1045
1046 #[rstest]
1047 fn test_parse_order_status_new_adl_maps_to_filled() {
1048 let result = parse_order_status(BinanceOrderStatus::NewAdl, false);
1049 assert_eq!(result, OrderStatus::Filled);
1050 }
1051
1052 #[rstest]
1053 fn test_parse_order_status_new_insurance_maps_to_filled() {
1054 let result = parse_order_status(BinanceOrderStatus::NewInsurance, false);
1055 assert_eq!(result, OrderStatus::Filled);
1056 }
1057
1058 #[rstest]
1059 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
1060 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
1061 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
1062 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
1063 fn test_parse_order_status_expired_respects_treat_as_canceled(
1064 #[case] status: BinanceOrderStatus,
1065 #[case] treat_expired_as_canceled: bool,
1066 #[case] expected: OrderStatus,
1067 ) {
1068 let result = parse_order_status(status, treat_expired_as_canceled);
1069 assert_eq!(result, expected);
1070 }
1071
1072 #[rstest]
1073 fn test_is_exchange_generated_autoclose() {
1074 let msg: BinanceFuturesOrderUpdateMsg =
1075 load_user_data_fixture("order_update_calculated.json");
1076 assert!(msg.order.is_exchange_generated());
1077 assert!(msg.order.is_liquidation());
1078 }
1079
1080 #[rstest]
1081 fn test_is_exchange_generated_adl_autoclose() {
1082 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_adl.json");
1083 assert!(msg.order.is_exchange_generated());
1084 assert!(msg.order.is_adl());
1085 }
1086
1087 #[rstest]
1088 fn test_is_exchange_generated_settlement_autoclose() {
1089 let msg: BinanceFuturesOrderUpdateMsg =
1090 load_user_data_fixture("order_update_settlement.json");
1091 assert!(msg.order.is_exchange_generated());
1092 assert!(msg.order.is_settlement());
1093 }
1094
1095 #[rstest]
1096 fn test_is_exchange_generated_delivery_autoclose() {
1097 let msg: BinanceFuturesOrderUpdateMsg =
1098 load_user_data_fixture("order_update_delivery.json");
1099 assert!(msg.order.is_exchange_generated());
1100 assert!(msg.order.is_settlement());
1101 assert!(!msg.order.is_liquidation());
1102 assert!(!msg.order.is_adl());
1103 }
1104
1105 #[rstest]
1106 fn test_normal_order_is_not_exchange_generated() {
1107 let msg: BinanceFuturesOrderUpdateMsg = load_user_data_fixture("order_update_trade.json");
1108 assert!(!msg.order.is_exchange_generated());
1109 assert!(!msg.order.is_liquidation());
1110 assert!(!msg.order.is_adl());
1111 assert!(!msg.order.is_settlement());
1112 }
1113
1114 #[rstest]
1115 fn test_parse_insurance_fill_with_new_insurance_status() {
1116 let msg: BinanceFuturesOrderUpdateMsg =
1117 load_user_data_fixture("order_update_insurance.json");
1118
1119 assert!(msg.order.is_liquidation());
1120 assert!(msg.order.is_exchange_generated());
1121 assert_eq!(msg.order.order_status, BinanceOrderStatus::NewInsurance);
1122
1123 let fill = parse_futures_order_update_to_fill(
1124 &msg,
1125 account_id(),
1126 instrument_id(),
1127 PRICE_PRECISION,
1128 SIZE_PRECISION,
1129 None,
1130 None,
1131 Currency::USDT(),
1132 None,
1133 UnixNanos::from(1_000_000_000u64),
1134 )
1135 .unwrap();
1136
1137 assert_eq!(
1138 fill.client_order_id,
1139 Some(ClientOrderId::new("autoclose-insurance-5678"))
1140 );
1141 assert_eq!(fill.order_side, OrderSide::Sell);
1142 assert_eq!(fill.last_qty, Quantity::new(0.020, SIZE_PRECISION));
1143 assert_eq!(fill.last_px, Price::new(45000.00, PRICE_PRECISION));
1144 }
1145
1146 #[rstest]
1147 fn test_parse_insurance_status_maps_new_insurance_to_filled() {
1148 let msg: BinanceFuturesOrderUpdateMsg =
1149 load_user_data_fixture("order_update_insurance.json");
1150
1151 let status = parse_futures_order_update_to_order_status(
1152 &msg,
1153 instrument_id(),
1154 PRICE_PRECISION,
1155 SIZE_PRECISION,
1156 account_id(),
1157 false,
1158 UnixNanos::from(1_000_000_000u64),
1159 )
1160 .unwrap();
1161
1162 assert_eq!(status.order_status, OrderStatus::Filled);
1163 }
1164
1165 #[rstest]
1166 fn test_parse_settlement_status_report() {
1167 let msg: BinanceFuturesOrderUpdateMsg =
1168 load_user_data_fixture("order_update_settlement.json");
1169
1170 let status = parse_futures_order_update_to_order_status(
1171 &msg,
1172 instrument_id(),
1173 PRICE_PRECISION,
1174 SIZE_PRECISION,
1175 account_id(),
1176 false,
1177 UnixNanos::from(1_000_000_000u64),
1178 )
1179 .unwrap();
1180
1181 assert_eq!(status.order_status, OrderStatus::Filled);
1182 assert_eq!(status.order_side, OrderSide::Sell);
1183 assert_eq!(status.quantity, Quantity::new(0.010, SIZE_PRECISION));
1184 assert_eq!(status.filled_qty, Quantity::new(0.010, SIZE_PRECISION));
1185 }
1186
1187 #[rstest]
1188 fn test_pending_liquidation_has_zero_fill_qty() {
1189 let msg: BinanceFuturesOrderUpdateMsg =
1190 load_user_data_fixture("order_update_calculated_pending.json");
1191
1192 assert!(msg.order.is_exchange_generated());
1193 assert!(msg.order.is_liquidation());
1194
1195 let last_qty = parse_required_decimal(&msg.order.last_filled_qty, "last_filled_qty")
1196 .expect("last_filled_qty should parse");
1197 assert!(last_qty.is_zero());
1198 }
1199
1200 #[rstest]
1201 #[case::venue_provided(Some("USDT"), Some("0.06937084"), None, None, 0.06937084, "USDT")]
1202 #[case::fallback_from_taker_fee(
1203 None, None,
1204 Some("0.0004"), Some("USDT"),
1205 0.055496, "USDT" )]
1207 #[case::no_commission_no_fee(None, None, None, None, 0.0, "USDT")]
1208 fn test_resolve_commission(
1209 #[case] commission_asset: Option<&str>,
1210 #[case] commission_amount: Option<&str>,
1211 #[case] taker_fee_str: Option<&str>,
1212 #[case] quote_currency_str: Option<&str>,
1213 #[case] expected_amount: f64,
1214 #[case] expected_currency: &str,
1215 ) {
1216 let mut msg: BinanceFuturesOrderUpdateMsg =
1217 load_user_data_fixture("order_update_calculated.json");
1218 msg.order.commission_asset = commission_asset.map(ustr::Ustr::from);
1219 msg.order.commission = commission_amount.map(String::from);
1220
1221 let last_qty = Quantity::from_decimal_dp(
1222 Decimal::from_str_exact(&msg.order.last_filled_qty).unwrap(),
1223 SIZE_PRECISION,
1224 )
1225 .unwrap();
1226 let last_px = Price::from_decimal_dp(
1227 Decimal::from_str_exact(&msg.order.last_filled_price).unwrap(),
1228 PRICE_PRECISION,
1229 )
1230 .unwrap();
1231 let taker_fee = taker_fee_str.map(|s| Decimal::from_str_exact(s).unwrap());
1232 let quote_currency = quote_currency_str.map(Currency::from);
1233
1234 let commission = resolve_commission(
1235 &msg.order,
1236 last_qty,
1237 last_px,
1238 taker_fee,
1239 quote_currency,
1240 Currency::USDT(),
1241 )
1242 .unwrap();
1243
1244 assert_eq!(commission.currency, Currency::from(expected_currency));
1245 let diff = (commission.as_f64() - expected_amount).abs();
1246 assert!(
1247 diff < 1e-4,
1248 "expected {expected_amount}, was {}",
1249 commission.as_f64()
1250 );
1251 }
1252
1253 #[rstest]
1254 #[case::with_venue_position_id(
1255 Some(Decimal::from_str_exact("0.0004").unwrap()),
1256 Some(Currency::from("USDT")),
1257 Some(PositionId::new("ETHUSDT-PERP.BINANCE-LONG")),
1258 )]
1259 #[case::without_extras(None, None, None)]
1260 fn test_parse_fill_with_optional_params(
1261 #[case] taker_fee: Option<Decimal>,
1262 #[case] quote_currency: Option<Currency>,
1263 #[case] venue_position_id: Option<PositionId>,
1264 ) {
1265 let msg: BinanceFuturesOrderUpdateMsg =
1266 load_user_data_fixture("order_update_calculated.json");
1267 let ts_init = UnixNanos::from(1_000_000_000u64);
1268
1269 let fill = parse_futures_order_update_to_fill(
1270 &msg,
1271 account_id(),
1272 instrument_id(),
1273 PRICE_PRECISION,
1274 SIZE_PRECISION,
1275 taker_fee,
1276 quote_currency,
1277 Currency::USDT(),
1278 venue_position_id,
1279 ts_init,
1280 )
1281 .unwrap();
1282
1283 assert_eq!(fill.venue_position_id, venue_position_id);
1284 assert_eq!(fill.account_id, account_id());
1285 assert_eq!(fill.instrument_id, instrument_id());
1286 }
1287}