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 update_time: Option<i64>,
1109 #[serde(default)]
1111 pub working_type_id: Option<i64>,
1112}
1113
1114fn zero_decimal_string() -> String {
1115 "0".to_string()
1116}
1117
1118impl BinanceFuturesOrder {
1119 pub fn to_order_status_report(
1125 &self,
1126 account_id: AccountId,
1127 instrument_id: InstrumentId,
1128 price_precision: u8,
1129 size_precision: u8,
1130 treat_expired_as_canceled: bool,
1131 ts_init: UnixNanos,
1132 ) -> anyhow::Result<OrderStatusReport> {
1133 let ts_event = self
1134 .update_time
1135 .map(|value| parse_millis(value, "Futures order update time"))
1136 .transpose()?
1137 .unwrap_or(ts_init);
1138
1139 let client_order_id =
1140 decode_client_order_id(&self.client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1141 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1142
1143 let order_side = match self.side {
1144 BinanceSide::Buy => OrderSide::Buy,
1145 BinanceSide::Sell => OrderSide::Sell,
1146 };
1147
1148 let order_type = self.order_type.to_nautilus_order_type()?;
1149 let time_in_force = self.time_in_force.to_nautilus_time_in_force()?;
1150 let order_status = self
1151 .status
1152 .to_nautilus_order_status(treat_expired_as_canceled)?;
1153
1154 let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
1155 let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
1156 let price = if self.price.is_empty() {
1157 None
1158 } else {
1159 let price: Decimal = self.price.parse().context("invalid price")?;
1160 if price == Decimal::ZERO {
1161 None
1162 } else {
1163 Some(
1164 Price::from_decimal_dp(price, price_precision)
1165 .context("invalid price precision")?,
1166 )
1167 }
1168 };
1169 let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1170
1171 let mut report = OrderStatusReport::new(
1172 account_id,
1173 instrument_id,
1174 Some(client_order_id),
1175 venue_order_id,
1176 order_side.into(),
1177 order_type,
1178 time_in_force,
1179 order_status,
1180 Quantity::from_decimal_dp(quantity, size_precision)
1181 .context("invalid orig_qty precision")?,
1182 Quantity::from_decimal_dp(filled_qty, size_precision)
1183 .context("invalid executed_qty precision")?,
1184 ts_event,
1185 ts_event,
1186 ts_init,
1187 Some(UUID4::new()),
1188 );
1189
1190 report.post_only = self.order_type == BinanceFuturesOrderType::Limit
1191 && matches!(
1192 self.time_in_force,
1193 BinanceTimeInForce::Gtx | BinanceTimeInForce::Rpi
1194 );
1195
1196 if let Some(price) = price {
1197 report = report.with_price(price);
1198 }
1199
1200 if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1201 report = report.with_expire_time(expire_time);
1202 }
1203
1204 report.avg_px = avg_px;
1205
1206 Ok(report)
1207 }
1208}
1209
1210impl BinanceFuturesOrderType {
1211 #[must_use]
1213 pub fn is_post_only(&self) -> bool {
1214 false }
1216
1217 pub fn to_nautilus_order_type(&self) -> anyhow::Result<OrderType> {
1223 (*self).try_into()
1224 }
1225}
1226
1227impl BinanceTimeInForce {
1228 pub fn to_nautilus_time_in_force(&self) -> anyhow::Result<TimeInForce> {
1234 Ok(match self {
1235 Self::Gtc => TimeInForce::Gtc,
1236 Self::Ioc => TimeInForce::Ioc,
1237 Self::Fok => TimeInForce::Fok,
1238 Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
1240 Self::Rpi => TimeInForce::Gtc,
1241 Self::Unknown => anyhow::bail!("unknown Binance time in force"),
1242 })
1243 }
1244}
1245
1246impl BinanceOrderStatus {
1247 pub fn to_nautilus_order_status(
1253 &self,
1254 treat_expired_as_canceled: bool,
1255 ) -> anyhow::Result<OrderStatus> {
1256 Ok(match self {
1257 Self::New | Self::PendingNew => OrderStatus::Accepted,
1258 Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1259 Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
1260 Self::Canceled => OrderStatus::Canceled,
1261 Self::PendingCancel => OrderStatus::PendingCancel,
1262 Self::Rejected => OrderStatus::Rejected,
1263 Self::Expired | Self::ExpiredInMatch => {
1264 if treat_expired_as_canceled {
1265 OrderStatus::Canceled
1266 } else {
1267 OrderStatus::Expired
1268 }
1269 }
1270 Self::Unknown => anyhow::bail!("unknown Binance order status"),
1271 })
1272 }
1273}
1274
1275impl BinanceUserTrade {
1276 pub fn to_fill_report(
1282 &self,
1283 account_id: AccountId,
1284 instrument_id: InstrumentId,
1285 price_precision: u8,
1286 size_precision: u8,
1287 bnfcr_currency: Currency,
1288 ts_init: UnixNanos,
1289 ) -> anyhow::Result<FillReport> {
1290 let ts_event = parse_millis(self.time, "Futures user trade time")?;
1291
1292 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1293 let trade_id = TradeId::new(self.id.to_string());
1294
1295 let order_side = match self.side {
1296 BinanceSide::Buy => OrderSide::Buy,
1297 BinanceSide::Sell => OrderSide::Sell,
1298 };
1299
1300 let liquidity_side = if self.maker {
1301 LiquiditySide::Maker
1302 } else {
1303 LiquiditySide::Taker
1304 };
1305
1306 let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1307 let last_px: Decimal = self.price.parse().context("invalid price")?;
1308
1309 let commission_currency = self
1310 .commission_asset
1311 .as_ref()
1312 .map_or(bnfcr_currency, |asset| {
1313 normalize_futures_asset(asset, bnfcr_currency)
1314 });
1315 let commission = match self.commission.as_ref() {
1316 Some(raw) => {
1317 let decimal = parse_required_decimal(raw, "commission")?;
1318 Money::from_decimal(decimal, commission_currency)?
1319 }
1320 None => Money::zero(commission_currency),
1321 };
1322
1323 Ok(FillReport::new(
1324 account_id,
1325 instrument_id,
1326 venue_order_id,
1327 trade_id,
1328 order_side,
1329 Quantity::from_decimal_dp(last_qty, size_precision).context("invalid qty precision")?,
1330 Price::from_decimal_dp(last_px, price_precision).context("invalid price precision")?,
1331 commission,
1332 liquidity_side,
1333 None, None, ts_event,
1336 ts_init,
1337 Some(UUID4::new()),
1338 ))
1339 }
1340}
1341
1342#[derive(Clone, Debug, Deserialize)]
1346#[serde(untagged)]
1347pub enum BatchOrderResult {
1348 Success(Box<BinanceFuturesOrder>),
1350 Error(BatchOrderError),
1352}
1353
1354#[derive(Clone, Debug, Deserialize)]
1356pub struct BatchOrderError {
1357 pub code: i64,
1359 pub msg: String,
1361}
1362
1363#[derive(Debug, Clone, Deserialize, Zeroize, ZeroizeOnDrop)]
1365#[serde(rename_all = "camelCase")]
1366pub struct ListenKeyResponse {
1367 pub listen_key: SecretString,
1369}
1370
1371impl ListenKeyResponse {
1372 #[must_use]
1374 pub fn into_listen_key(mut self) -> SecretString {
1375 std::mem::take(&mut self.listen_key)
1376 }
1377}
1378
1379#[derive(Clone, Debug, Serialize, Deserialize)]
1389#[serde(rename_all = "camelCase")]
1390pub struct BinanceFuturesAlgoOrder {
1391 pub algo_id: i64,
1393 pub client_algo_id: String,
1395 pub algo_type: BinanceAlgoType,
1397 #[serde(rename = "orderType", alias = "type")]
1399 pub order_type: BinanceFuturesOrderType,
1400 pub symbol: Ustr,
1402 pub side: BinanceSide,
1404 #[serde(default)]
1406 pub position_side: Option<BinancePositionSide>,
1407 #[serde(default)]
1409 pub time_in_force: Option<BinanceTimeInForce>,
1410 #[serde(default)]
1412 pub quantity: Option<String>,
1413 #[serde(default)]
1415 pub algo_status: Option<BinanceAlgoStatus>,
1416 #[serde(default)]
1418 pub trigger_price: Option<String>,
1419 #[serde(default)]
1421 pub price: Option<String>,
1422 #[serde(default)]
1424 pub working_type: Option<BinanceWorkingType>,
1425 #[serde(default)]
1427 pub close_position: Option<bool>,
1428 #[serde(default)]
1430 pub price_protect: Option<bool>,
1431 #[serde(default)]
1433 pub reduce_only: Option<bool>,
1434 #[serde(default)]
1436 pub activate_price: Option<String>,
1437 #[serde(default)]
1439 pub callback_rate: Option<String>,
1440 #[serde(default)]
1442 pub good_till_date: Option<i64>,
1443 #[serde(default)]
1445 pub create_time: Option<i64>,
1446 #[serde(default)]
1448 pub update_time: Option<i64>,
1449 #[serde(default)]
1451 pub trigger_time: Option<i64>,
1452 #[serde(default)]
1454 pub actual_order_id: Option<String>,
1455 #[serde(default, rename = "actualQty", alias = "executedQty")]
1457 pub executed_qty: Option<String>,
1458 #[serde(default, rename = "actualPrice", alias = "avgPrice")]
1460 pub avg_price: Option<String>,
1461}
1462
1463impl BinanceFuturesAlgoOrder {
1464 pub fn to_order_status_report(
1471 &self,
1472 account_id: AccountId,
1473 instrument_id: InstrumentId,
1474 price_precision: u8,
1475 size_precision: u8,
1476 ts_init: UnixNanos,
1477 ) -> anyhow::Result<OrderStatusReport> {
1478 let ts_event = self
1479 .update_time
1480 .or(self.create_time)
1481 .map(|value| parse_millis(value, "Futures algo order time"))
1482 .transpose()?
1483 .unwrap_or(ts_init);
1484
1485 let client_order_id =
1486 decode_client_order_id(&self.client_algo_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1487 let venue_order_id = self
1488 .actual_order_id
1489 .as_ref()
1490 .filter(|id| !id.is_empty())
1491 .map_or_else(
1492 || VenueOrderId::new(self.algo_id.to_string()),
1493 |id| VenueOrderId::new(id.clone()),
1494 );
1495
1496 let order_side = match self.side {
1497 BinanceSide::Buy => OrderSide::Buy,
1498 BinanceSide::Sell => OrderSide::Sell,
1499 };
1500
1501 let order_type = self.order_type.to_nautilus_order_type()?;
1502 let time_in_force = self
1503 .time_in_force
1504 .as_ref()
1505 .map(BinanceTimeInForce::to_nautilus_time_in_force)
1506 .transpose()?
1507 .unwrap_or(TimeInForce::Gtc);
1508 let order_status = self.parse_order_status()?;
1509
1510 let quantity: Decimal = self
1511 .quantity
1512 .as_ref()
1513 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1514 .context("invalid quantity")?;
1515 let filled_qty: Decimal = self
1516 .executed_qty
1517 .as_ref()
1518 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1519 .context("invalid executed_qty")?;
1520 let price = if let Some(price) = self.price.as_ref().filter(|price| !price.is_empty()) {
1521 let price: Decimal = price.parse().context("invalid price")?;
1522 if price == Decimal::ZERO {
1523 None
1524 } else {
1525 Some(
1526 Price::from_decimal_dp(price, price_precision)
1527 .context("invalid price precision")?,
1528 )
1529 }
1530 } else {
1531 None
1532 };
1533 let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1534 let trigger_price = self.parse_trigger_price(price_precision)?;
1535 let trailing_offset = self.parse_trailing_offset()?;
1536
1537 let mut report = OrderStatusReport::new(
1538 account_id,
1539 instrument_id,
1540 Some(client_order_id),
1541 venue_order_id,
1542 order_side.into(),
1543 order_type,
1544 time_in_force,
1545 order_status,
1546 Quantity::from_decimal_dp(quantity, size_precision)
1547 .context("invalid quantity precision")?,
1548 Quantity::from_decimal_dp(filled_qty, size_precision)
1549 .context("invalid executed_qty precision")?,
1550 ts_event,
1551 ts_event,
1552 ts_init,
1553 Some(UUID4::new()),
1554 );
1555
1556 if let Some(price) = price {
1557 report = report.with_price(price);
1558 }
1559
1560 report.avg_px = avg_px;
1561
1562 if let Some(trigger_price) = trigger_price {
1563 report = report
1564 .with_trigger_price(trigger_price)
1565 .with_trigger_type(parse_working_type(self.working_type));
1566 }
1567
1568 if let Some(trailing_offset) = trailing_offset {
1569 report = report
1570 .with_trailing_offset(trailing_offset)
1571 .with_trailing_offset_type(TrailingOffsetType::BasisPoints);
1572 }
1573
1574 if let Some(activation_price) = self
1575 .activate_price
1576 .as_deref()
1577 .map(|price| {
1578 parse_positive_price_at_precision(price, price_precision, "activate_price")
1579 })
1580 .transpose()?
1581 .flatten()
1582 {
1583 report = report.with_activation_price(activation_price);
1584 }
1585
1586 if self.reduce_only == Some(true) || self.close_position == Some(true) {
1587 report = report.with_reduce_only(true);
1588 }
1589
1590 if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1591 report = report.with_expire_time(expire_time);
1592 }
1593
1594 if let Some(trigger_time) = self.trigger_time {
1595 report =
1596 report.with_ts_triggered(parse_millis(trigger_time, "Futures algo trigger time")?);
1597 }
1598
1599 Ok(report)
1600 }
1601
1602 #[expect(clippy::too_many_arguments)]
1613 pub fn to_order_status_report_with_actual(
1614 &self,
1615 actual: &BinanceFuturesOrder,
1616 account_id: AccountId,
1617 instrument_id: InstrumentId,
1618 price_precision: u8,
1619 size_precision: u8,
1620 treat_expired_as_canceled: bool,
1621 ts_init: UnixNanos,
1622 ) -> anyhow::Result<OrderStatusReport> {
1623 let expected_actual_order_id = self
1624 .actual_order_id
1625 .as_deref()
1626 .filter(|id| !id.is_empty())
1627 .context("algo order has no actual_order_id")?;
1628
1629 if expected_actual_order_id != actual.order_id.to_string() {
1630 anyhow::bail!(
1631 "actual order ID mismatch: expected {expected_actual_order_id}, was {}",
1632 actual.order_id
1633 );
1634 }
1635
1636 if self.symbol != actual.symbol {
1637 anyhow::bail!(
1638 "actual order symbol mismatch: expected {}, was {}",
1639 self.symbol,
1640 actual.symbol
1641 );
1642 }
1643
1644 if self.side != actual.side {
1645 anyhow::bail!(
1646 "actual order side mismatch: expected {:?}, was {:?}",
1647 self.side,
1648 actual.side
1649 );
1650 }
1651
1652 let mut report = self.to_order_status_report(
1653 account_id,
1654 instrument_id,
1655 price_precision,
1656 size_precision,
1657 ts_init,
1658 )?;
1659 let actual_report = actual.to_order_status_report(
1660 account_id,
1661 instrument_id,
1662 price_precision,
1663 size_precision,
1664 treat_expired_as_canceled,
1665 ts_init,
1666 )?;
1667 report.venue_order_id = actual_report.venue_order_id;
1668 report.order_status = actual_report.order_status;
1669 report.quantity = actual_report.quantity;
1670 report.filled_qty = actual_report.filled_qty;
1671 report.avg_px = actual_report.avg_px.or(report.avg_px);
1672 report.expire_time = report.expire_time.or(actual_report.expire_time);
1673 report.ts_last = actual_report.ts_last;
1674
1675 Ok(report)
1676 }
1677
1678 fn parse_trigger_price(&self, price_precision: u8) -> anyhow::Result<Option<Price>> {
1679 let raw_trigger_price = match self.order_type {
1680 BinanceFuturesOrderType::TrailingStopMarket => self
1681 .trigger_price
1682 .as_deref()
1683 .or(self.activate_price.as_deref()),
1684 _ => self.trigger_price.as_deref(),
1685 };
1686 let trigger_price = raw_trigger_price
1687 .map(|price| parse_positive_price_at_precision(price, price_precision, "trigger_price"))
1688 .transpose()?
1689 .flatten();
1690
1691 if trigger_price.is_none() && requires_algo_trigger_price(self.order_type) {
1692 anyhow::bail!(
1693 "missing positive trigger_price for Binance algo order type {:?}",
1694 self.order_type
1695 );
1696 }
1697
1698 Ok(trigger_price)
1699 }
1700
1701 fn parse_trailing_offset(&self) -> anyhow::Result<Option<Decimal>> {
1702 if self.order_type != BinanceFuturesOrderType::TrailingStopMarket {
1703 return Ok(None);
1704 }
1705
1706 self.callback_rate
1707 .as_deref()
1708 .map(parse_callback_rate_basis_points)
1709 .transpose()
1710 .map(Option::flatten)
1711 }
1712
1713 fn parse_order_status(&self) -> anyhow::Result<OrderStatus> {
1714 Ok(match self.algo_status {
1715 Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1716 Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1717 Some(BinanceAlgoStatus::Triggered) => self
1718 .executed_qty
1719 .as_deref()
1720 .and_then(|qty| qty.parse::<Decimal>().ok())
1721 .filter(|qty| *qty > Decimal::ZERO)
1722 .map_or(OrderStatus::Accepted, |_| OrderStatus::PartiallyFilled),
1723 Some(BinanceAlgoStatus::Finished) => {
1724 let executed_qty = self
1725 .executed_qty
1726 .as_deref()
1727 .and_then(|qty| qty.parse::<Decimal>().ok());
1728 let quantity = self
1729 .quantity
1730 .as_deref()
1731 .and_then(|qty| qty.parse::<Decimal>().ok());
1732 match (executed_qty, quantity) {
1733 (Some(actual), Some(total)) if total > Decimal::ZERO && actual >= total => {
1734 OrderStatus::Filled
1735 }
1736 _ => OrderStatus::Canceled,
1737 }
1738 }
1739 Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1740 Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1741 Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1742 Some(BinanceAlgoStatus::Unknown) => anyhow::bail!("unknown Binance algo order status"),
1743 None => OrderStatus::Initialized,
1744 })
1745 }
1746}
1747
1748fn parse_avg_px(
1749 raw: Option<&str>,
1750 filled_qty: Decimal,
1751 price_precision: u8,
1752) -> anyhow::Result<Option<Decimal>> {
1753 if filled_qty <= Decimal::ZERO {
1754 return Ok(None);
1755 }
1756
1757 raw.filter(|price| !price.is_empty())
1758 .map(|price| parse_positive_price_at_precision(price, price_precision, "avg_price"))
1759 .transpose()
1760 .map(|price| price.flatten().map(|price| price.as_decimal()))
1761}
1762
1763fn parse_positive_price_at_precision(
1764 raw: &str,
1765 precision: u8,
1766 field: &str,
1767) -> anyhow::Result<Option<Price>> {
1768 let decimal = parse_required_decimal(raw, field)?;
1769 if decimal <= Decimal::ZERO {
1770 return Ok(None);
1771 }
1772
1773 Price::from_decimal_dp(decimal, precision)
1774 .map(Some)
1775 .map_err(|e| anyhow::anyhow!("invalid {field} precision: {e}"))
1776}
1777
1778fn parse_callback_rate_basis_points(raw: &str) -> anyhow::Result<Option<Decimal>> {
1779 let rate = parse_required_decimal(raw, "callback_rate")?;
1780 if rate <= Decimal::ZERO {
1781 return Ok(None);
1782 }
1783
1784 rate.checked_mul(Decimal::from(100))
1785 .map(Some)
1786 .ok_or_else(|| anyhow::anyhow!("invalid callback_rate='{raw}': multiplication overflow"))
1787}
1788
1789fn parse_working_type(working_type: Option<BinanceWorkingType>) -> TriggerType {
1790 match working_type {
1791 Some(BinanceWorkingType::ContractPrice) => TriggerType::LastPrice,
1792 Some(BinanceWorkingType::MarkPrice) => TriggerType::MarkPrice,
1793 Some(BinanceWorkingType::Unknown) | None => TriggerType::Default,
1794 }
1795}
1796
1797fn requires_algo_trigger_price(order_type: BinanceFuturesOrderType) -> bool {
1798 matches!(
1799 order_type,
1800 BinanceFuturesOrderType::Stop
1801 | BinanceFuturesOrderType::StopMarket
1802 | BinanceFuturesOrderType::TakeProfit
1803 | BinanceFuturesOrderType::TakeProfitMarket
1804 | BinanceFuturesOrderType::TrailingStopMarket
1805 )
1806}
1807
1808#[derive(Clone, Debug, Deserialize)]
1810#[serde(rename_all = "camelCase")]
1811pub struct BinanceFuturesAlgoOrderCancelResponse {
1812 pub algo_id: i64,
1814 pub client_algo_id: String,
1816 pub code: String,
1818 pub msg: String,
1820}
1821
1822#[cfg(test)]
1823mod tests {
1824 use nautilus_model::identifiers::ClientOrderId;
1825 use rstest::rstest;
1826 use rust_decimal_macros::dec;
1827 use zeroize::Zeroize;
1828
1829 use super::*;
1830 use crate::common::testing::load_fixture_string;
1831
1832 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
1833
1834 #[rstest]
1835 fn test_unknown_order_enums_reject_conversion() {
1836 assert_eq!(
1837 BinanceFuturesOrderType::Unknown
1838 .to_nautilus_order_type()
1839 .unwrap_err()
1840 .to_string(),
1841 "unknown Binance Futures order type",
1842 );
1843 assert_eq!(
1844 BinanceTimeInForce::Unknown
1845 .to_nautilus_time_in_force()
1846 .unwrap_err()
1847 .to_string(),
1848 "unknown Binance time in force",
1849 );
1850 assert_eq!(
1851 BinanceOrderStatus::Unknown
1852 .to_nautilus_order_status(false)
1853 .unwrap_err()
1854 .to_string(),
1855 "unknown Binance order status",
1856 );
1857 }
1858
1859 #[rstest]
1860 fn test_parse_account_info_v2() {
1861 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1862 let account: BinanceFuturesAccountInfo =
1863 serde_json::from_str(&json).expect("Failed to parse account info");
1864
1865 assert_eq!(
1866 account.total_wallet_balance,
1867 Some(Decimal::from_str_exact("23.72469206").unwrap())
1868 );
1869 assert_eq!(account.assets.len(), 1);
1870 assert_eq!(account.assets[0].asset.as_str(), "USDT");
1871 assert_eq!(
1872 account.assets[0].wallet_balance,
1873 Decimal::from_str_exact("23.72469206").unwrap()
1874 );
1875 assert_eq!(account.positions.len(), 1);
1876 assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1877 assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1878 }
1879
1880 #[rstest]
1881 fn test_account_info_to_account_state_zero_margins() {
1882 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1883 let account: BinanceFuturesAccountInfo =
1884 serde_json::from_str(&json).expect("Failed to parse account info");
1885
1886 let account_id = AccountId::from("BINANCE-001");
1887 let ts_init = UnixNanos::from(1_000_000_000u64);
1888 let state = account.to_account_state(account_id, ts_init).unwrap();
1889
1890 assert_eq!(state.account_id, account_id);
1891 assert_eq!(state.account_type, AccountType::Margin);
1892 assert!(!state.balances.is_empty());
1893 assert_eq!(state.margins.len(), 0);
1894 }
1895
1896 #[rstest]
1897 fn test_account_info_to_account_state_with_margins() {
1898 let json = r#"{
1899 "totalInitialMargin": "500.25000000",
1900 "totalMaintMargin": "250.75000000",
1901 "totalWalletBalance": "10000.00000000",
1902 "assets": [{
1903 "asset": "USDT",
1904 "walletBalance": "10000.00000000",
1905 "availableBalance": "9500.00000000",
1906 "initialMargin": "500.25000000",
1907 "maintMargin": "250.75000000",
1908 "updateTime": 1617939110373
1909 }],
1910 "positions": []
1911 }"#;
1912 let account: BinanceFuturesAccountInfo =
1913 serde_json::from_str(json).expect("Failed to parse account info");
1914
1915 let account_id = AccountId::from("BINANCE-001");
1916 let ts_init = UnixNanos::from(1_000_000_000u64);
1917 let state = account.to_account_state(account_id, ts_init).unwrap();
1918
1919 assert_eq!(state.margins.len(), 1);
1920 let margin = &state.margins[0];
1921 assert!(margin.instrument_id.is_none());
1922 assert_eq!(margin.currency.code, "USDT");
1923 assert_eq!(margin.initial.as_f64(), 500.25);
1924 assert_eq!(margin.maintenance.as_f64(), 250.75);
1925 }
1926
1927 #[rstest]
1928 fn test_account_info_to_account_state_coin_margined_per_base_coin() {
1929 let json = r#"{
1930 "totalWalletBalance": "0.00000000",
1931 "assets": [
1932 {
1933 "asset": "BTC",
1934 "walletBalance": "1.50000000",
1935 "availableBalance": "1.40000000",
1936 "initialMargin": "0.05000000",
1937 "maintMargin": "0.02500000",
1938 "updateTime": 1617939110373
1939 },
1940 {
1941 "asset": "ETH",
1942 "walletBalance": "10.00000000",
1943 "availableBalance": "9.00000000",
1944 "initialMargin": "0.80000000",
1945 "maintMargin": "0.40000000",
1946 "updateTime": 1617939110373
1947 }
1948 ],
1949 "positions": []
1950 }"#;
1951 let account: BinanceFuturesAccountInfo =
1952 serde_json::from_str(json).expect("Failed to parse account info");
1953
1954 let account_id = AccountId::from("BINANCE-001");
1955 let ts_init = UnixNanos::from(1_000_000_000u64);
1956 let state = account.to_account_state(account_id, ts_init).unwrap();
1957
1958 assert_eq!(state.margins.len(), 2);
1959 assert!(state.margins.iter().all(|m| m.instrument_id.is_none()));
1960 let btc = state
1961 .margins
1962 .iter()
1963 .find(|m| m.currency.code == "BTC")
1964 .expect("BTC margin missing");
1965 assert_eq!(btc.initial.as_f64(), 0.05);
1966 assert_eq!(btc.maintenance.as_f64(), 0.025);
1967 let eth = state
1968 .margins
1969 .iter()
1970 .find(|m| m.currency.code == "ETH")
1971 .expect("ETH margin missing");
1972 assert_eq!(eth.initial.as_f64(), 0.8);
1973 assert_eq!(eth.maintenance.as_f64(), 0.4);
1974 }
1975
1976 #[rstest]
1981 fn test_account_info_to_account_state_precision_drift() {
1982 let json = r#"{
1983 "assets": [{
1984 "asset": "USDT",
1985 "walletBalance": "10.000000034999",
1986 "availableBalance": "9.999999994999",
1987 "updateTime": 1617939110373
1988 }],
1989 "positions": []
1990 }"#;
1991 let account: BinanceFuturesAccountInfo =
1992 serde_json::from_str(json).expect("Failed to parse account info");
1993
1994 let account_id = AccountId::from("BINANCE-001");
1995 let ts_init = UnixNanos::from(1_000_000_000u64);
1996 let state = account.to_account_state(account_id, ts_init).unwrap();
1997
1998 assert_eq!(state.balances.len(), 1);
1999 let balance = &state.balances[0];
2000 assert_eq!(balance.total, balance.locked + balance.free);
2001 }
2002
2003 #[rstest]
2004 fn test_account_info_to_account_state_empty_balance() {
2005 let json = r#"{
2007 "assets": [{
2008 "asset": "USDT",
2009 "walletBalance": "",
2010 "availableBalance": "",
2011 "updateTime": 0
2012 }],
2013 "positions": []
2014 }"#;
2015 let account: BinanceFuturesAccountInfo =
2016 serde_json::from_str(json).expect("Failed to parse account info");
2017
2018 let account_id = AccountId::from("BINANCE-001");
2019 let ts_init = UnixNanos::from(1_000_000_000u64);
2020 let state = account.to_account_state(account_id, ts_init).unwrap();
2021
2022 assert_eq!(state.balances.len(), 1);
2023 let balance = &state.balances[0];
2024 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
2025 assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
2026 assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
2027 }
2028
2029 #[rstest]
2030 fn test_account_info_to_account_state_empty_assets() {
2031 let json = r#"{
2033 "assets": [],
2034 "positions": []
2035 }"#;
2036 let account: BinanceFuturesAccountInfo =
2037 serde_json::from_str(json).expect("Failed to parse account info");
2038
2039 let account_id = AccountId::from("BINANCE-001");
2040 let ts_init = UnixNanos::from(1_000_000_000u64);
2041 let state = account.to_account_state(account_id, ts_init).unwrap();
2042
2043 assert_eq!(state.balances.len(), 1);
2044 let balance = &state.balances[0];
2045 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
2046 }
2047
2048 #[rstest]
2049 fn test_parse_position_risk() {
2050 let json = load_fixture_string("futures/http_json/position_risk.json");
2051 let positions: Vec<BinancePositionRisk> =
2052 serde_json::from_str(&json).expect("Failed to parse position risk");
2053
2054 assert_eq!(positions.len(), 1);
2055 assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
2056 assert_eq!(positions[0].position_amt, "0.001");
2057 assert_eq!(positions[0].mark_price, "51000.0");
2058 assert_eq!(positions[0].leverage, "20");
2059 }
2060
2061 #[rstest]
2062 fn test_parse_balance_with_v1_field() {
2063 let json = load_fixture_string("futures/http_json/balance.json");
2065 let balances: Vec<BinanceFuturesBalance> =
2066 serde_json::from_str(&json).expect("Failed to parse balance");
2067
2068 assert_eq!(balances.len(), 1);
2069 assert_eq!(balances[0].asset.as_str(), "USDT");
2070 assert_eq!(
2072 balances[0].wallet_balance,
2073 Decimal::from_str_exact("122.12345678").unwrap()
2074 );
2075 assert_eq!(
2076 balances[0].available_balance,
2077 Decimal::from_str_exact("122.12345678").unwrap()
2078 );
2079 }
2080
2081 #[rstest]
2082 fn test_parse_balance_with_v2_field() {
2083 let json = r#"{
2085 "asset": "USDT",
2086 "walletBalance": "100.00000000",
2087 "availableBalance": "100.00000000",
2088 "updateTime": 1617939110373
2089 }"#;
2090
2091 let balance: BinanceFuturesBalance =
2092 serde_json::from_str(json).expect("Failed to parse balance");
2093
2094 assert_eq!(balance.asset.as_str(), "USDT");
2095 assert_eq!(
2096 balance.wallet_balance,
2097 Decimal::from_str_exact("100.00000000").unwrap()
2098 );
2099 }
2100
2101 #[rstest]
2102 fn test_parse_order() {
2103 let json = load_fixture_string("futures/http_json/order_response.json");
2104 let order: BinanceFuturesOrder =
2105 serde_json::from_str(&json).expect("Failed to parse order");
2106
2107 assert_eq!(order.order_id, 12345678);
2108 assert_eq!(order.symbol.as_str(), "BTCUSDT");
2109 assert_eq!(order.status, BinanceOrderStatus::New);
2110 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
2111 assert_eq!(order.side, BinanceSide::Buy);
2112 assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
2113 assert_eq!(order.price_match, Some(BinancePriceMatch::None));
2114 assert_eq!(
2115 order.self_trade_prevention_mode,
2116 Some(BinanceSelfTradePreventionMode::None)
2117 );
2118 }
2119
2120 #[rstest]
2121 fn test_order_to_report_maps_rpi_to_gtc_post_only() {
2122 let mut order = order_with_price("50000.00");
2123 order.order_type = BinanceFuturesOrderType::Limit;
2124 order.time_in_force = BinanceTimeInForce::Rpi;
2125
2126 let report = order
2127 .to_order_status_report(
2128 AccountId::from("BINANCE-FUTURES-001"),
2129 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2130 2,
2131 3,
2132 false,
2133 UnixNanos::from(1_000_000_000u64),
2134 )
2135 .unwrap();
2136
2137 assert_eq!(report.time_in_force, TimeInForce::Gtc);
2138 assert!(report.post_only);
2139 }
2140
2141 #[rstest]
2142 fn test_parse_order_defaults_missing_cum_quote_to_zero() {
2143 let json = load_fixture_string("futures/http_json/order_response.json");
2144 let mut value: Value = serde_json::from_str(&json).expect("Failed to parse order fixture");
2145
2146 value
2147 .as_object_mut()
2148 .expect("Order fixture should be a JSON object")
2149 .remove("cumQuote");
2150
2151 let order: BinanceFuturesOrder =
2152 serde_json::from_value(value).expect("Failed to parse order");
2153
2154 assert_eq!(order.cum_quote, "0");
2155 }
2156
2157 #[rstest]
2158 fn test_parse_kline_rejects_non_string_price() {
2159 let value = serde_json::json!([
2160 1_625_474_304_000_i64,
2161 50000.00,
2162 "51000.00",
2163 "49000.00",
2164 "50500.00",
2165 "12.5",
2166 1_625_474_364_000_i64,
2167 "631250.00",
2168 100_i64,
2169 "6.2",
2170 "313100.00"
2171 ]);
2172
2173 let error = serde_json::from_value::<BinanceFuturesKline>(value)
2174 .unwrap_err()
2175 .to_string();
2176
2177 assert!(error.contains("open"));
2178 }
2179
2180 #[rstest]
2181 fn test_parse_hedge_mode_response() {
2182 let json = r#"{"dualSidePosition": true}"#;
2183 let response: BinanceHedgeModeResponse =
2184 serde_json::from_str(json).expect("Failed to parse hedge mode");
2185 assert!(response.dual_side_position);
2186 }
2187
2188 #[rstest]
2189 fn test_parse_leverage_response() {
2190 let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
2191 let response: BinanceLeverageResponse =
2192 serde_json::from_str(json).expect("Failed to parse leverage");
2193 assert_eq!(response.symbol.as_str(), "BTCUSDT");
2194 assert_eq!(response.leverage, 20);
2195 }
2196
2197 #[rstest]
2198 fn test_parse_listen_key_response() {
2199 assert_zeroize_on_drop::<ListenKeyResponse>();
2200
2201 let json =
2202 r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
2203 let mut response: ListenKeyResponse =
2204 serde_json::from_str(json).expect("Failed to parse listen key");
2205
2206 let debug = format!("{response:?}");
2207 assert_eq!(
2208 response.listen_key.expose_secret(),
2209 "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"
2210 );
2211 assert_eq!(debug, "ListenKeyResponse { listen_key: <redacted> }");
2212 assert!(!debug.contains(response.listen_key.expose_secret()));
2213
2214 response.zeroize();
2215 assert!(response.listen_key.expose_secret().is_empty());
2216 }
2217
2218 #[rstest]
2219 fn test_parse_account_position() {
2220 let json = r#"{
2221 "symbol": "ETHUSDT",
2222 "initialMargin": "100.00",
2223 "maintMargin": "50.00",
2224 "unrealizedProfit": "10.00",
2225 "positionInitialMargin": "100.00",
2226 "openOrderInitialMargin": "0",
2227 "leverage": "10",
2228 "isolated": true,
2229 "entryPrice": "2000.00",
2230 "maxNotional": "100000",
2231 "bidNotional": "0",
2232 "askNotional": "0",
2233 "positionSide": "LONG",
2234 "positionAmt": "0.5",
2235 "updateTime": 1625474304765
2236 }"#;
2237
2238 let position: BinanceAccountPosition =
2239 serde_json::from_str(json).expect("Failed to parse account position");
2240
2241 assert_eq!(position.symbol.as_str(), "ETHUSDT");
2242 assert_eq!(position.leverage, Some("10".to_string()));
2243 assert_eq!(position.isolated, Some(true));
2244 assert_eq!(position.position_side, Some(BinancePositionSide::Long));
2245 }
2246
2247 #[rstest]
2248 fn test_parse_algo_order() {
2249 let json = r#"{
2250 "algoId": 123456789,
2251 "clientAlgoId": "test-algo-order-1",
2252 "algoType": "CONDITIONAL",
2253 "type": "STOP_MARKET",
2254 "symbol": "BTCUSDT",
2255 "side": "BUY",
2256 "positionSide": "BOTH",
2257 "timeInForce": "GTC",
2258 "quantity": "0.001",
2259 "algoStatus": "NEW",
2260 "triggerPrice": "45000.00",
2261 "workingType": "MARK_PRICE",
2262 "reduceOnly": false,
2263 "createTime": 1625474304765,
2264 "updateTime": 1625474304765
2265 }"#;
2266
2267 let order: BinanceFuturesAlgoOrder =
2268 serde_json::from_str(json).expect("Failed to parse algo order");
2269
2270 assert_eq!(order.algo_id, 123456789);
2271 assert_eq!(order.client_algo_id, "test-algo-order-1");
2272 assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
2273 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2274 assert_eq!(order.symbol.as_str(), "BTCUSDT");
2275 assert_eq!(order.side, BinanceSide::Buy);
2276 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
2277 assert_eq!(order.trigger_price, Some("45000.00".to_string()));
2278 }
2279
2280 #[rstest]
2281 #[case("actualQty", "actualPrice")]
2282 #[case("executedQty", "avgPrice")]
2283 fn test_parse_algo_order_finished(#[case] quantity_field: &str, #[case] price_field: &str) {
2284 let json = load_fixture_string("futures/http_json/algo_order_response.json")
2285 .replace("actualQty", quantity_field)
2286 .replace("actualPrice", price_field);
2287
2288 let order: BinanceFuturesAlgoOrder =
2289 serde_json::from_str(&json).expect("Failed to parse finished algo order");
2290
2291 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Finished));
2292 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2293 assert_eq!(order.actual_order_id, Some("987654321".to_string()));
2294 assert_eq!(order.executed_qty, Some("0.001".to_string()));
2295 assert_eq!(order.avg_price, Some("50000.00".to_string()));
2296 }
2297
2298 #[rstest]
2299 fn test_parse_algo_order_cancel_response() {
2300 let json = r#"{
2301 "algoId": 123456789,
2302 "clientAlgoId": "test-algo-order-1",
2303 "code": "200",
2304 "msg": "success"
2305 }"#;
2306
2307 let response: BinanceFuturesAlgoOrderCancelResponse =
2308 serde_json::from_str(json).expect("Failed to parse algo cancel response");
2309
2310 assert_eq!(response.algo_id, 123456789);
2311 assert_eq!(response.client_algo_id, "test-algo-order-1");
2312 assert_eq!(response.code, "200");
2313 assert_eq!(response.msg, "success");
2314 }
2315
2316 #[rstest]
2317 fn test_order_to_report_decodes_broker_id() {
2318 let json = r#"{
2319 "orderId": 12345678,
2320 "symbol": "BTCUSDT",
2321 "status": "NEW",
2322 "clientOrderId": "x-aHRE4BCj-T0000000000000",
2323 "price": "50000.00",
2324 "avgPrice": "0.00",
2325 "origQty": "0.001",
2326 "executedQty": "0.000",
2327 "cumQuote": "0.00",
2328 "timeInForce": "GTC",
2329 "type": "LIMIT",
2330 "reduceOnly": false,
2331 "closePosition": false,
2332 "side": "BUY",
2333 "positionSide": "BOTH",
2334 "stopPrice": "0.00",
2335 "workingType": "CONTRACT_PRICE",
2336 "priceProtect": false,
2337 "origType": "LIMIT",
2338 "priceMatch": "NONE",
2339 "selfTradePreventionMode": "NONE",
2340 "goodTillDate": 0,
2341 "time": 1625474304765,
2342 "updateTime": 1625474304765
2343 }"#;
2344
2345 let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
2346 let account_id = AccountId::from("BINANCE-FUTURES-001");
2347 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2348 let ts_init = UnixNanos::from(1_000_000_000u64);
2349
2350 let report = order
2351 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2352 .unwrap();
2353
2354 assert_eq!(
2355 report.client_order_id,
2356 Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
2357 );
2358 assert_eq!(report.price, Some(Price::from("50000.00")));
2359 }
2360
2361 #[rstest]
2362 fn test_order_to_report_rejects_invalid_client_order_id() {
2363 let mut order = order_with_price("50000.00");
2364 order.client_order_id = String::new();
2365
2366 let result = order.to_order_status_report(
2367 AccountId::from("BINANCE-FUTURES-001"),
2368 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2369 2,
2370 3,
2371 false,
2372 UnixNanos::from(1_000_000_000u64),
2373 );
2374
2375 assert_eq!(
2376 result.unwrap_err().to_string(),
2377 "invalid Binance client order ID ''"
2378 );
2379 }
2380
2381 #[rstest]
2382 #[case("0")]
2383 #[case("")]
2384 fn test_order_to_report_omits_missing_price(#[case] price: &str) {
2385 let order = order_with_price(price);
2386 let account_id = AccountId::from("BINANCE-FUTURES-001");
2387 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2388 let ts_init = UnixNanos::from(1_000_000_000u64);
2389
2390 let report = order
2391 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2392 .unwrap();
2393
2394 assert_eq!(report.price, None);
2395 }
2396
2397 #[rstest]
2398 fn test_order_to_report_sets_avg_px_for_filled_market_order() {
2399 let mut order = order_with_price("0");
2400 order.status = BinanceOrderStatus::Filled;
2401 order.executed_qty = "0.001".to_string();
2402 order.cum_quote = "50.00".to_string();
2403 order.avg_price = Some("50000.00".to_string());
2404 let account_id = AccountId::from("BINANCE-FUTURES-001");
2405 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2406 let ts_init = UnixNanos::from(1_000_000_000u64);
2407
2408 let report = order
2409 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2410 .unwrap();
2411
2412 assert_eq!(report.price, None);
2413 assert_eq!(
2414 report.avg_px,
2415 Some(Decimal::from_str_exact("50000.00").unwrap())
2416 );
2417 }
2418
2419 #[rstest]
2420 fn test_order_to_report_omits_avg_px_without_fills() {
2421 let mut order = order_with_price("0");
2422 order.avg_price = Some("50000.00".to_string());
2423 let account_id = AccountId::from("BINANCE-FUTURES-001");
2424 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2425 let ts_init = UnixNanos::from(1_000_000_000u64);
2426
2427 let report = order
2428 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2429 .unwrap();
2430
2431 assert_eq!(report.avg_px, None);
2432 }
2433
2434 #[rstest]
2435 fn test_order_to_report_rejects_invalid_avg_px_for_filled_order() {
2436 let mut order = order_with_price("0");
2437 order.executed_qty = "0.001".to_string();
2438 order.avg_price = Some("not-a-number".to_string());
2439 let account_id = AccountId::from("BINANCE-FUTURES-001");
2440 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2441 let ts_init = UnixNanos::from(1_000_000_000u64);
2442
2443 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2444
2445 let error = result.unwrap_err().to_string();
2446 assert!(error.contains("avg_price"));
2447 }
2448
2449 #[rstest]
2450 fn test_close_all_algo_report_with_actual_uses_matching_engine_quantity() {
2451 let mut algo = algo_order_with_price(None);
2452 algo.order_type = BinanceFuturesOrderType::StopMarket;
2453 algo.quantity = None;
2454 algo.close_position = Some(true);
2455 algo.algo_status = Some(BinanceAlgoStatus::Finished);
2456 algo.actual_order_id = Some("987654321".to_string());
2457 algo.executed_qty = Some("0.002".to_string());
2458 algo.avg_price = Some("49000.00".to_string());
2459 algo.reduce_only = Some(true);
2460 algo.trigger_time = Some(1_625_474_305_000);
2461 algo.time_in_force = Some(BinanceTimeInForce::Gtd);
2462 algo.good_till_date = Some(1_700_000_601_000);
2463
2464 let mut actual = order_with_price("0");
2465 actual.order_id = 987654321;
2466 actual.orig_qty = "0.002".to_string();
2467 actual.executed_qty = "0.001".to_string();
2468 actual.avg_price = Some("50000.00".to_string());
2469 actual.status = BinanceOrderStatus::PartiallyFilled;
2470 actual.order_type = BinanceFuturesOrderType::Market;
2471 actual.side = BinanceSide::Sell;
2472 actual.update_time = Some(1_625_474_306_000);
2473
2474 let account_id = AccountId::from("BINANCE-FUTURES-001");
2475 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2476 let ts_init = UnixNanos::from(1_000_000_000u64);
2477 let report = algo
2478 .to_order_status_report_with_actual(
2479 &actual,
2480 account_id,
2481 instrument_id,
2482 2,
2483 3,
2484 false,
2485 ts_init,
2486 )
2487 .unwrap();
2488
2489 assert_eq!(
2490 report.client_order_id,
2491 Some(ClientOrderId::from("my-algo-order-1"))
2492 );
2493 assert_eq!(report.venue_order_id, VenueOrderId::from("987654321"));
2494 assert_eq!(report.order_type, OrderType::StopMarket);
2495 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
2496 assert_eq!(report.quantity, Quantity::from("0.002"));
2497 assert_eq!(report.filled_qty, Quantity::from("0.001"));
2498 assert_eq!(report.avg_px, Some(Decimal::from(50000)));
2499 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2500 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2501 assert!(report.reduce_only);
2502 assert_eq!(
2503 report.expire_time,
2504 Some(UnixNanos::from_millis(1_700_000_601_000)),
2505 );
2506 assert_eq!(
2507 report.ts_triggered,
2508 Some(UnixNanos::from_millis(1_625_474_305_000))
2509 );
2510 assert_eq!(report.ts_last, UnixNanos::from_millis(1_625_474_306_000));
2511 }
2512
2513 #[rstest]
2514 #[case(BinanceAlgoStatus::Finished, Some("0.001"), OrderStatus::Filled)]
2515 #[case(BinanceAlgoStatus::Finished, Some("0.0005"), OrderStatus::Canceled)]
2516 #[case(
2517 BinanceAlgoStatus::Triggered,
2518 Some("0.0005"),
2519 OrderStatus::PartiallyFilled
2520 )]
2521 #[case(BinanceAlgoStatus::Triggered, None, OrderStatus::Accepted)]
2522 fn test_algo_order_status_uses_actual_quantity_conservatively(
2523 #[case] algo_status: BinanceAlgoStatus,
2524 #[case] executed_qty: Option<&str>,
2525 #[case] expected: OrderStatus,
2526 ) {
2527 let mut order = algo_order_with_price(None);
2528 order.algo_status = Some(algo_status);
2529 order.executed_qty = executed_qty.map(str::to_string);
2530
2531 assert_eq!(order.parse_order_status().unwrap(), expected);
2532 }
2533
2534 #[rstest]
2535 fn test_algo_order_to_report_sets_price() {
2536 let order = algo_order_with_price(Some("50000.00"));
2537 let account_id = AccountId::from("BINANCE-FUTURES-001");
2538 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2539 let ts_init = UnixNanos::from(1_000_000_000u64);
2540
2541 let report = order
2542 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2543 .unwrap();
2544
2545 assert_eq!(report.price, Some(Price::from("50000.00")));
2546 }
2547
2548 #[rstest]
2549 fn test_algo_order_to_report_sets_actual_fill_fields() {
2550 let json = load_fixture_string("futures/http_json/algo_order_response.json");
2551 let order: BinanceFuturesAlgoOrder = serde_json::from_str(&json).unwrap();
2552 let account_id = AccountId::from("BINANCE-FUTURES-001");
2553 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2554 let ts_init = UnixNanos::from(1_000_000_000u64);
2555
2556 let report = order
2557 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2558 .unwrap();
2559
2560 assert_eq!(report.order_status, OrderStatus::Filled);
2561 assert_eq!(report.filled_qty.as_decimal(), dec!(0.001));
2562 assert_eq!(report.price, None);
2563 assert_eq!(report.avg_px, Some(dec!(50000.00)));
2564 }
2565
2566 #[rstest]
2567 fn test_algo_order_to_report_omits_avg_price_without_fills() {
2568 let mut order = algo_order_with_price(None);
2569 order.executed_qty = Some("0".to_string());
2570 order.avg_price = Some("not-a-number".to_string());
2571 let account_id = AccountId::from("BINANCE-FUTURES-001");
2572 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2573 let ts_init = UnixNanos::from(1_000_000_000u64);
2574
2575 let report = order
2576 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2577 .unwrap();
2578
2579 assert_eq!(report.filled_qty.as_decimal(), Decimal::ZERO);
2580 assert_eq!(report.avg_px, None);
2581 }
2582
2583 #[rstest]
2584 fn test_algo_order_to_report_sets_trigger_fields() {
2585 let order = algo_order_with_price(Some("44000.00"));
2586 let account_id = AccountId::from("BINANCE-FUTURES-001");
2587 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2588 let ts_init = UnixNanos::from(1_000_000_000u64);
2589
2590 let report = order
2591 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2592 .unwrap();
2593
2594 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2595 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2596 }
2597
2598 #[rstest]
2599 fn test_algo_order_to_report_sets_trailing_fields() {
2600 let mut order = algo_order_with_price(None);
2601 order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2602 order.trigger_price = None;
2603 order.activate_price = Some("45000.00".to_string());
2604 order.callback_rate = Some("0.25".to_string());
2605 let account_id = AccountId::from("BINANCE-FUTURES-001");
2606 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2607 let ts_init = UnixNanos::from(1_000_000_000u64);
2608
2609 let report = order
2610 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2611 .unwrap();
2612
2613 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2614 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2615 assert_eq!(report.trailing_offset, Some(Decimal::from(25)));
2616 assert_eq!(
2617 report.trailing_offset_type,
2618 Some(TrailingOffsetType::BasisPoints),
2619 );
2620 }
2621
2622 #[rstest]
2623 #[case(None)]
2624 #[case(Some("0"))]
2625 #[case(Some(""))]
2626 fn test_algo_order_to_report_omits_missing_price(#[case] price: Option<&str>) {
2627 let order = algo_order_with_price(price);
2628 let account_id = AccountId::from("BINANCE-FUTURES-001");
2629 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2630 let ts_init = UnixNanos::from(1_000_000_000u64);
2631
2632 let report = order
2633 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2634 .unwrap();
2635
2636 assert_eq!(report.price, None);
2637 }
2638
2639 #[rstest]
2640 fn test_order_to_report_rejects_invalid_price() {
2641 let order = order_with_price("not-a-number");
2642 let account_id = AccountId::from("BINANCE-FUTURES-001");
2643 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2644 let ts_init = UnixNanos::from(1_000_000_000u64);
2645
2646 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2647
2648 let error = result.unwrap_err().to_string();
2649 assert!(error.contains("invalid price"));
2650 }
2651
2652 #[rstest]
2653 fn test_algo_order_to_report_rejects_invalid_trigger_price() {
2654 let mut order = algo_order_with_price(Some("50000.00"));
2655 order.trigger_price = Some("not-a-number".to_string());
2656 let account_id = AccountId::from("BINANCE-FUTURES-001");
2657 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2658 let ts_init = UnixNanos::from(1_000_000_000u64);
2659
2660 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2661
2662 let error = result.unwrap_err().to_string();
2663 assert!(error.contains("trigger_price"));
2664 }
2665
2666 #[rstest]
2667 fn test_algo_order_to_report_rejects_missing_trigger_price() {
2668 let mut order = algo_order_with_price(None);
2669 order.order_type = BinanceFuturesOrderType::StopMarket;
2670 order.trigger_price = None;
2671 let account_id = AccountId::from("BINANCE-FUTURES-001");
2672 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2673 let ts_init = UnixNanos::from(1_000_000_000u64);
2674
2675 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2676
2677 let error = result.unwrap_err().to_string();
2678 assert!(error.contains("missing positive trigger_price"));
2679 }
2680
2681 #[rstest]
2682 fn test_algo_order_to_report_rejects_invalid_callback_rate() {
2683 let mut order = algo_order_with_price(None);
2684 order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2685 order.trigger_price = None;
2686 order.activate_price = Some("45000.00".to_string());
2687 order.callback_rate = Some("not-a-number".to_string());
2688 let account_id = AccountId::from("BINANCE-FUTURES-001");
2689 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2690 let ts_init = UnixNanos::from(1_000_000_000u64);
2691
2692 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2693
2694 let error = result.unwrap_err().to_string();
2695 assert!(error.contains("callback_rate"));
2696 }
2697
2698 #[rstest]
2699 fn test_algo_order_to_report_rejects_invalid_price() {
2700 let order = algo_order_with_price(Some("not-a-number"));
2701 let account_id = AccountId::from("BINANCE-FUTURES-001");
2702 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2703 let ts_init = UnixNanos::from(1_000_000_000u64);
2704
2705 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2706
2707 let error = result.unwrap_err().to_string();
2708 assert!(error.contains("invalid price"));
2709 }
2710
2711 #[rstest]
2712 fn test_order_to_report_preserves_good_till_date() {
2713 let mut order = order_with_price("50000.00");
2714 order.time_in_force = BinanceTimeInForce::Gtd;
2715 order.good_till_date = Some(1_700_000_601_000);
2716 let account_id = AccountId::from("BINANCE-FUTURES-001");
2717 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2718 let ts_init = UnixNanos::from(1_000_000_000u64);
2719
2720 let report = order
2721 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2722 .unwrap();
2723
2724 assert_eq!(report.time_in_force, TimeInForce::Gtd);
2725 assert_eq!(
2726 report.expire_time,
2727 Some(UnixNanos::from_millis(1_700_000_601_000)),
2728 );
2729 }
2730
2731 #[rstest]
2732 fn test_algo_order_to_report_preserves_good_till_date() {
2733 let mut order = algo_order_with_price(Some("50000.00"));
2734 order.time_in_force = Some(BinanceTimeInForce::Gtd);
2735 order.good_till_date = Some(1_700_000_601_000);
2736 let account_id = AccountId::from("BINANCE-FUTURES-001");
2737 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2738 let ts_init = UnixNanos::from(1_000_000_000u64);
2739
2740 let report = order
2741 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2742 .unwrap();
2743
2744 assert_eq!(report.time_in_force, TimeInForce::Gtd);
2745 assert_eq!(
2746 report.expire_time,
2747 Some(UnixNanos::from_millis(1_700_000_601_000)),
2748 );
2749 }
2750
2751 fn order_with_price(price: &str) -> BinanceFuturesOrder {
2752 BinanceFuturesOrder {
2753 symbol: Ustr::from("BTCUSDT"),
2754 order_id: 12345678,
2755 client_order_id: "external-order".to_string(),
2756 orig_qty: "0.001".to_string(),
2757 executed_qty: "0.000".to_string(),
2758 cum_quote: "0.00".to_string(),
2759 price: price.to_string(),
2760 avg_price: Some("0.00".to_string()),
2761 stop_price: Some("0.00".to_string()),
2762 status: BinanceOrderStatus::New,
2763 time_in_force: BinanceTimeInForce::Gtc,
2764 order_type: BinanceFuturesOrderType::Market,
2765 orig_type: Some(BinanceFuturesOrderType::Market),
2766 side: BinanceSide::Buy,
2767 position_side: Some(BinancePositionSide::Both),
2768 reduce_only: Some(false),
2769 close_position: Some(false),
2770 activate_price: None,
2771 price_rate: None,
2772 working_type: Some(BinanceWorkingType::ContractPrice),
2773 price_protect: Some(false),
2774 is_isolated: None,
2775 good_till_date: Some(0),
2776 price_match: Some(BinancePriceMatch::None),
2777 self_trade_prevention_mode: Some(BinanceSelfTradePreventionMode::None),
2778 update_time: Some(1_625_474_304_765),
2779 working_type_id: None,
2780 }
2781 }
2782
2783 fn algo_order_with_price(price: Option<&str>) -> BinanceFuturesAlgoOrder {
2784 BinanceFuturesAlgoOrder {
2785 algo_id: 123456789,
2786 client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2787 algo_type: BinanceAlgoType::Conditional,
2788 order_type: BinanceFuturesOrderType::TakeProfit,
2789 symbol: Ustr::from("BTCUSDT"),
2790 side: BinanceSide::Sell,
2791 position_side: Some(BinancePositionSide::Both),
2792 time_in_force: Some(BinanceTimeInForce::Gtc),
2793 quantity: Some("0.001".to_string()),
2794 algo_status: Some(BinanceAlgoStatus::New),
2795 trigger_price: Some("45000.00".to_string()),
2796 price: price.map(str::to_string),
2797 working_type: Some(BinanceWorkingType::MarkPrice),
2798 close_position: Some(false),
2799 price_protect: None,
2800 reduce_only: Some(false),
2801 activate_price: None,
2802 callback_rate: None,
2803 good_till_date: Some(0),
2804 create_time: Some(1_625_474_304_765),
2805 update_time: Some(1_625_474_304_765),
2806 trigger_time: None,
2807 actual_order_id: None,
2808 executed_qty: None,
2809 avg_price: None,
2810 }
2811 }
2812
2813 #[rstest]
2814 fn test_user_trade_to_fill_report_rejects_invalid_commission() {
2815 let trade = BinanceUserTrade {
2816 symbol: Ustr::from("BTCUSDT"),
2817 id: 100,
2818 order_id: 200,
2819 price: "50000.00".to_string(),
2820 qty: "0.001".to_string(),
2821 quote_qty: None,
2822 realized_pnl: "0".to_string(),
2823 side: BinanceSide::Buy,
2824 position_side: None,
2825 time: 1_625_474_304_000,
2826 buyer: true,
2827 maker: false,
2828 commission: Some("not-a-number".to_string()),
2829 commission_asset: Some(Ustr::from("USDT")),
2830 margin_asset: None,
2831 };
2832
2833 let result = trade.to_fill_report(
2834 AccountId::from("BINANCE-FUTURES-001"),
2835 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2836 2,
2837 3,
2838 Currency::USDT(),
2839 UnixNanos::from(1_000_000_000u64),
2840 );
2841
2842 let error = result.unwrap_err().to_string();
2843 assert!(error.contains("commission"));
2844 }
2845
2846 #[rstest]
2847 fn test_algo_order_to_report_decodes_broker_id() {
2848 let json = r#"{
2849 "algoId": 123456789,
2850 "clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
2851 "algoType": "CONDITIONAL",
2852 "type": "STOP_MARKET",
2853 "symbol": "BTCUSDT",
2854 "side": "BUY",
2855 "positionSide": "BOTH",
2856 "timeInForce": "GTC",
2857 "quantity": "0.001",
2858 "algoStatus": "NEW",
2859 "triggerPrice": "45000.00",
2860 "workingType": "MARK_PRICE",
2861 "reduceOnly": false,
2862 "createTime": 1625474304765,
2863 "updateTime": 1625474304765
2864 }"#;
2865
2866 let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
2867 let account_id = AccountId::from("BINANCE-FUTURES-001");
2868 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2869 let ts_init = UnixNanos::from(1_000_000_000u64);
2870
2871 let report = order
2872 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2873 .unwrap();
2874
2875 assert_eq!(
2876 report.client_order_id,
2877 Some(ClientOrderId::from("my-algo-order-1")),
2878 );
2879 }
2880
2881 #[rstest]
2882 fn test_algo_order_to_report_rejects_invalid_client_order_id() {
2883 let mut order = algo_order_with_price(None);
2884 order.client_algo_id = "x-aHRE4BCj-R".to_string();
2885
2886 let result = order.to_order_status_report(
2887 AccountId::from("BINANCE-FUTURES-001"),
2888 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2889 2,
2890 3,
2891 UnixNanos::from(1_000_000_000u64),
2892 );
2893
2894 assert_eq!(
2895 result.unwrap_err().to_string(),
2896 "missing raw broker client order ID payload"
2897 );
2898 }
2899
2900 #[rstest]
2901 #[case(None, "123456789")]
2902 #[case(Some(""), "123456789")]
2903 #[case(Some("987654321"), "987654321")]
2904 fn test_algo_order_to_report_selects_valid_venue_order_id(
2905 #[case] actual_order_id: Option<&str>,
2906 #[case] expected_venue_order_id: &str,
2907 ) {
2908 let order = BinanceFuturesAlgoOrder {
2909 algo_id: 123456789,
2910 client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2911 algo_type: BinanceAlgoType::Conditional,
2912 order_type: BinanceFuturesOrderType::StopMarket,
2913 symbol: Ustr::from("BTCUSDT"),
2914 side: BinanceSide::Buy,
2915 position_side: Some(BinancePositionSide::Both),
2916 time_in_force: Some(BinanceTimeInForce::Gtc),
2917 quantity: Some("0.001".to_string()),
2918 algo_status: Some(BinanceAlgoStatus::New),
2919 trigger_price: Some("45000.00".to_string()),
2920 price: None,
2921 working_type: Some(BinanceWorkingType::MarkPrice),
2922 close_position: Some(false),
2923 price_protect: None,
2924 reduce_only: Some(false),
2925 activate_price: None,
2926 callback_rate: None,
2927 good_till_date: Some(0),
2928 create_time: Some(1_625_474_304_765),
2929 update_time: Some(1_625_474_304_765),
2930 trigger_time: None,
2931 actual_order_id: actual_order_id.map(str::to_string),
2932 executed_qty: None,
2933 avg_price: None,
2934 };
2935 let account_id = AccountId::from("BINANCE-FUTURES-001");
2936 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2937 let ts_init = UnixNanos::from(1_000_000_000u64);
2938
2939 let report = order
2940 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2941 .unwrap();
2942
2943 assert_eq!(
2944 report.venue_order_id,
2945 VenueOrderId::new(expected_venue_order_id)
2946 );
2947 }
2948
2949 #[rstest]
2950 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
2951 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
2952 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
2953 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
2954 fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
2955 #[case] status: BinanceOrderStatus,
2956 #[case] treat_expired_as_canceled: bool,
2957 #[case] expected: OrderStatus,
2958 ) {
2959 let result = status
2960 .to_nautilus_order_status(treat_expired_as_canceled)
2961 .unwrap();
2962 assert_eq!(result, expected);
2963 }
2964}