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