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::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
47pub 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, );
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 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#[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#[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, ))
225}
226
227pub 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, );
284
285 Some(report)
286}
287
288pub 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![], true, UUID4::new(),
322 ts_event,
323 ts_init,
324 None, ))
326}
327
328pub 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
336pub 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 #[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" )]
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}