1use anyhow::Context;
19use nautilus_core::{
20 UUID4, UnixNanos,
21 serialization::{
22 deserialize_decimal_or_zero, deserialize_optional_decimal_from_str,
23 serialize_decimal_as_str, serialize_optional_decimal_as_str,
24 },
25};
26use nautilus_model::{
27 enums::{AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
28 events::AccountState,
29 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
30 reports::{FillReport, OrderStatusReport},
31 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
32};
33use rust_decimal::Decimal;
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36use ustr::Ustr;
37
38use crate::{
39 common::{
40 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
41 encoder::decode_broker_id,
42 enums::{
43 BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
44 BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
45 BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
46 BinanceTradingStatus, BinanceWorkingType,
47 },
48 models::BinanceRateLimit,
49 parse::parse_required_decimal,
50 },
51 futures::conversions::normalize_futures_asset,
52};
53
54#[derive(Clone, Debug, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct BinanceServerTime {
58 pub server_time: i64,
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct BinanceFuturesTrade {
66 pub id: i64,
68 pub price: String,
70 pub qty: String,
72 pub quote_qty: String,
74 pub time: i64,
76 pub is_buyer_maker: bool,
78}
79
80#[derive(Clone, Debug)]
82pub struct BinanceFuturesKline {
83 pub open_time: i64,
85 pub open: String,
87 pub high: String,
89 pub low: String,
91 pub close: String,
93 pub volume: String,
95 pub close_time: i64,
97 pub quote_volume: String,
99 pub num_trades: i64,
101 pub taker_buy_base_volume: String,
103 pub taker_buy_quote_volume: String,
105}
106
107impl<'de> Deserialize<'de> for BinanceFuturesKline {
108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109 where
110 D: serde::Deserializer<'de>,
111 {
112 let arr: Vec<Value> = Vec::deserialize(deserializer)?;
113 if arr.len() < 11 {
114 return Err(serde::de::Error::custom("Invalid kline array length"));
115 }
116
117 Ok(Self {
118 open_time: required_kline_i64::<D::Error>(&arr, 0, "open_time")?,
119 open: required_kline_string::<D::Error>(&arr, 1, "open")?,
120 high: required_kline_string::<D::Error>(&arr, 2, "high")?,
121 low: required_kline_string::<D::Error>(&arr, 3, "low")?,
122 close: required_kline_string::<D::Error>(&arr, 4, "close")?,
123 volume: required_kline_string::<D::Error>(&arr, 5, "volume")?,
124 close_time: required_kline_i64::<D::Error>(&arr, 6, "close_time")?,
125 quote_volume: required_kline_string::<D::Error>(&arr, 7, "quote_volume")?,
126 num_trades: required_kline_i64::<D::Error>(&arr, 8, "num_trades")?,
127 taker_buy_base_volume: required_kline_string::<D::Error>(
128 &arr,
129 9,
130 "taker_buy_base_volume",
131 )?,
132 taker_buy_quote_volume: required_kline_string::<D::Error>(
133 &arr,
134 10,
135 "taker_buy_quote_volume",
136 )?,
137 })
138 }
139}
140
141fn required_kline_i64<E>(arr: &[Value], index: usize, field: &str) -> Result<i64, E>
142where
143 E: serde::de::Error,
144{
145 arr[index]
146 .as_i64()
147 .ok_or_else(|| E::custom(format!("invalid kline {field}")))
148}
149
150fn required_kline_string<E>(arr: &[Value], index: usize, field: &str) -> Result<String, E>
151where
152 E: serde::de::Error,
153{
154 arr[index]
155 .as_str()
156 .map(ToString::to_string)
157 .ok_or_else(|| E::custom(format!("invalid kline {field}")))
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct BinanceFuturesUsdExchangeInfo {
164 pub timezone: String,
166 pub server_time: i64,
168 pub rate_limits: Vec<BinanceRateLimit>,
170 #[serde(default)]
172 pub exchange_filters: Vec<Value>,
173 #[serde(default)]
175 pub assets: Vec<BinanceFuturesAsset>,
176 pub symbols: Vec<BinanceFuturesUsdSymbol>,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct BinanceFuturesAsset {
184 pub asset: Ustr,
186 pub margin_available: bool,
188 #[serde(default)]
190 pub auto_asset_exchange: Option<String>,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct BinanceFuturesUsdSymbol {
197 pub symbol: Ustr,
199 pub pair: Ustr,
201 pub contract_type: String,
203 pub delivery_date: i64,
205 pub onboard_date: i64,
207 pub status: BinanceTradingStatus,
209 pub maint_margin_percent: String,
211 pub required_margin_percent: String,
213 pub base_asset: Ustr,
215 pub quote_asset: Ustr,
217 pub margin_asset: Ustr,
219 pub price_precision: i32,
221 pub quantity_precision: i32,
223 pub base_asset_precision: i32,
225 pub quote_precision: i32,
227 #[serde(default)]
229 pub underlying_type: Option<String>,
230 #[serde(default)]
232 pub underlying_sub_type: Vec<String>,
233 #[serde(default)]
235 pub settle_plan: Option<i64>,
236 #[serde(default)]
238 pub trigger_protect: Option<String>,
239 #[serde(default)]
241 pub liquidation_fee: Option<String>,
242 #[serde(default)]
244 pub market_take_bound: Option<String>,
245 pub order_types: Vec<String>,
247 pub time_in_force: Vec<String>,
249 pub filters: Vec<Value>,
251}
252
253#[derive(Clone, Debug, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct BinanceFuturesCoinExchangeInfo {
257 pub timezone: String,
259 pub server_time: i64,
261 pub rate_limits: Vec<BinanceRateLimit>,
263 #[serde(default)]
265 pub exchange_filters: Vec<Value>,
266 pub symbols: Vec<BinanceFuturesCoinSymbol>,
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct BinanceFuturesCoinSymbol {
274 pub symbol: Ustr,
276 pub pair: Ustr,
278 pub contract_type: String,
280 pub delivery_date: i64,
282 pub onboard_date: i64,
284 #[serde(default)]
286 pub contract_status: Option<BinanceContractStatus>,
287 pub contract_size: i64,
289 pub maint_margin_percent: String,
291 pub required_margin_percent: String,
293 pub base_asset: Ustr,
295 pub quote_asset: Ustr,
297 pub margin_asset: Ustr,
299 pub price_precision: i32,
301 pub quantity_precision: i32,
303 pub base_asset_precision: i32,
305 pub quote_precision: i32,
307 #[serde(default, rename = "equalQtyPrecision")]
309 pub equal_qty_precision: Option<i32>,
310 #[serde(default)]
312 pub trigger_protect: Option<String>,
313 #[serde(default)]
315 pub liquidation_fee: Option<String>,
316 #[serde(default)]
318 pub market_take_bound: Option<String>,
319 pub order_types: Vec<String>,
321 pub time_in_force: Vec<String>,
323 pub filters: Vec<Value>,
325}
326
327#[derive(Clone, Debug, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct BinanceFuturesTicker24hr {
331 pub symbol: Ustr,
333 pub price_change: String,
335 pub price_change_percent: String,
337 pub weighted_avg_price: String,
339 pub last_price: String,
341 #[serde(default)]
343 pub last_qty: Option<String>,
344 pub open_price: String,
346 pub high_price: String,
348 pub low_price: String,
350 pub volume: String,
352 pub quote_volume: String,
354 pub open_time: i64,
356 pub close_time: i64,
358 #[serde(default)]
360 pub first_id: Option<i64>,
361 #[serde(default)]
363 pub last_id: Option<i64>,
364 #[serde(default)]
366 pub count: Option<i64>,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct BinanceFuturesMarkPrice {
373 pub symbol: Ustr,
375 pub mark_price: String,
377 #[serde(default)]
379 pub index_price: Option<String>,
380 #[serde(default)]
382 pub estimated_settle_price: Option<String>,
383 #[serde(default)]
385 pub last_funding_rate: Option<String>,
386 #[serde(default)]
388 pub next_funding_time: Option<i64>,
389 #[serde(default)]
391 pub interest_rate: Option<String>,
392 pub time: i64,
394}
395
396#[derive(Clone, Debug, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct BinanceOrderBook {
400 pub last_update_id: i64,
402 pub bids: Vec<(String, String)>,
404 pub asks: Vec<(String, String)>,
406 #[serde(default, rename = "E")]
408 pub event_time: Option<i64>,
409 #[serde(default, rename = "T")]
411 pub transaction_time: Option<i64>,
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct BinanceBookTicker {
418 pub symbol: Ustr,
420 pub bid_price: String,
422 pub bid_qty: String,
424 pub ask_price: String,
426 pub ask_qty: String,
428 #[serde(default)]
430 pub time: Option<i64>,
431}
432
433#[derive(Clone, Debug, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct BinancePriceTicker {
437 pub symbol: Ustr,
439 pub price: String,
441 #[serde(default)]
443 pub time: Option<i64>,
444}
445
446#[derive(Clone, Debug, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct BinanceFundingRate {
450 pub symbol: Ustr,
452 pub funding_rate: String,
454 pub funding_time: i64,
456 #[serde(default)]
458 pub mark_price: Option<String>,
459 #[serde(default)]
461 pub index_price: Option<String>,
462}
463
464#[derive(Clone, Debug, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct BinanceOpenInterest {
468 pub symbol: Ustr,
470 pub open_interest: String,
472 pub time: i64,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct BinanceOpenInterestHistRecord {
480 #[serde(default)]
482 pub symbol: Option<Ustr>,
483 #[serde(default)]
485 pub pair: Option<Ustr>,
486 #[serde(default)]
488 pub contract_type: Option<String>,
489 pub sum_open_interest: String,
491 pub sum_open_interest_value: String,
493 pub timestamp: i64,
495 #[serde(default, rename = "CMCCirculatingSupply")]
497 pub cmc_circulating_supply: Option<String>,
498}
499
500#[derive(Clone, Debug, Serialize, Deserialize)]
502#[serde(rename_all = "camelCase")]
503pub struct BinanceFuturesBalance {
504 #[serde(default)]
506 pub account_alias: Option<String>,
507 pub asset: Ustr,
509 #[serde(
511 alias = "balance",
512 deserialize_with = "deserialize_decimal_or_zero",
513 serialize_with = "serialize_decimal_as_str"
514 )]
515 pub wallet_balance: Decimal,
516 #[serde(
518 default,
519 deserialize_with = "deserialize_optional_decimal_from_str",
520 serialize_with = "serialize_optional_decimal_as_str"
521 )]
522 pub unrealized_profit: Option<Decimal>,
523 #[serde(
525 default,
526 deserialize_with = "deserialize_optional_decimal_from_str",
527 serialize_with = "serialize_optional_decimal_as_str"
528 )]
529 pub margin_balance: Option<Decimal>,
530 #[serde(
532 default,
533 deserialize_with = "deserialize_optional_decimal_from_str",
534 serialize_with = "serialize_optional_decimal_as_str"
535 )]
536 pub maint_margin: Option<Decimal>,
537 #[serde(
539 default,
540 deserialize_with = "deserialize_optional_decimal_from_str",
541 serialize_with = "serialize_optional_decimal_as_str"
542 )]
543 pub initial_margin: Option<Decimal>,
544 #[serde(
546 default,
547 deserialize_with = "deserialize_optional_decimal_from_str",
548 serialize_with = "serialize_optional_decimal_as_str"
549 )]
550 pub position_initial_margin: Option<Decimal>,
551 #[serde(
553 default,
554 deserialize_with = "deserialize_optional_decimal_from_str",
555 serialize_with = "serialize_optional_decimal_as_str"
556 )]
557 pub open_order_initial_margin: Option<Decimal>,
558 #[serde(
560 default,
561 deserialize_with = "deserialize_optional_decimal_from_str",
562 serialize_with = "serialize_optional_decimal_as_str"
563 )]
564 pub cross_wallet_balance: Option<Decimal>,
565 #[serde(
567 default,
568 deserialize_with = "deserialize_optional_decimal_from_str",
569 serialize_with = "serialize_optional_decimal_as_str"
570 )]
571 pub cross_un_pnl: Option<Decimal>,
572 #[serde(
574 deserialize_with = "deserialize_decimal_or_zero",
575 serialize_with = "serialize_decimal_as_str"
576 )]
577 pub available_balance: Decimal,
578 #[serde(
580 default,
581 deserialize_with = "deserialize_optional_decimal_from_str",
582 serialize_with = "serialize_optional_decimal_as_str"
583 )]
584 pub max_withdraw_amount: Option<Decimal>,
585 #[serde(default)]
587 pub margin_available: Option<bool>,
588 pub update_time: i64,
590 #[serde(
592 default,
593 deserialize_with = "deserialize_optional_decimal_from_str",
594 serialize_with = "serialize_optional_decimal_as_str"
595 )]
596 pub withdraw_available: Option<Decimal>,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
601#[serde(rename_all = "camelCase")]
602pub struct BinanceAccountPosition {
603 pub symbol: Ustr,
605 #[serde(default)]
607 pub initial_margin: Option<String>,
608 #[serde(default)]
610 pub maint_margin: Option<String>,
611 #[serde(default)]
613 pub unrealized_profit: Option<String>,
614 #[serde(default)]
616 pub position_initial_margin: Option<String>,
617 #[serde(default)]
619 pub open_order_initial_margin: Option<String>,
620 #[serde(default)]
622 pub leverage: Option<String>,
623 #[serde(default)]
625 pub isolated: Option<bool>,
626 #[serde(default)]
628 pub entry_price: Option<String>,
629 #[serde(default)]
631 pub max_notional: Option<String>,
632 #[serde(default)]
634 pub bid_notional: Option<String>,
635 #[serde(default)]
637 pub ask_notional: Option<String>,
638 #[serde(default)]
640 pub position_side: Option<BinancePositionSide>,
641 #[serde(default)]
643 pub position_amt: Option<String>,
644 #[serde(default)]
646 pub update_time: Option<i64>,
647}
648
649#[derive(Clone, Debug, Serialize, Deserialize)]
651#[serde(rename_all = "camelCase")]
652pub struct BinancePositionRisk {
653 pub symbol: Ustr,
655 pub position_amt: String,
657 pub entry_price: String,
659 pub mark_price: String,
661 #[serde(default)]
663 pub un_realized_profit: Option<String>,
664 #[serde(default)]
666 pub liquidation_price: Option<String>,
667 pub leverage: String,
669 #[serde(default)]
671 pub max_notional_value: Option<String>,
672 #[serde(default)]
674 pub margin_type: Option<BinanceMarginType>,
675 #[serde(default)]
677 pub isolated_margin: Option<String>,
678 #[serde(default)]
680 pub is_auto_add_margin: Option<String>,
681 #[serde(default)]
683 pub position_side: Option<BinancePositionSide>,
684 #[serde(default)]
686 pub notional: Option<String>,
687 #[serde(default)]
689 pub isolated_wallet: Option<String>,
690 #[serde(default)]
692 pub adl_quantile: Option<u8>,
693 #[serde(default)]
695 pub update_time: Option<i64>,
696 #[serde(default)]
698 pub break_even_price: Option<String>,
699 #[serde(default)]
701 pub bust_price: Option<String>,
702}
703
704#[derive(Clone, Debug, Serialize, Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct BinanceIncomeRecord {
708 #[serde(default)]
710 pub symbol: Option<Ustr>,
711 pub income_type: BinanceIncomeType,
713 pub income: String,
715 pub asset: Ustr,
717 pub time: i64,
719 #[serde(default)]
721 pub info: Option<String>,
722 #[serde(default)]
724 pub tran_id: Option<i64>,
725 #[serde(default)]
727 pub trade_id: Option<i64>,
728}
729
730#[derive(Clone, Debug, Serialize, Deserialize)]
732#[serde(rename_all = "camelCase")]
733pub struct BinanceUserTrade {
734 pub symbol: Ustr,
736 pub id: i64,
738 pub order_id: i64,
740 pub price: String,
742 pub qty: String,
744 #[serde(default)]
746 pub quote_qty: Option<String>,
747 pub realized_pnl: String,
749 pub side: BinanceSide,
751 #[serde(default)]
753 pub position_side: Option<BinancePositionSide>,
754 pub time: i64,
756 pub buyer: bool,
758 pub maker: bool,
760 #[serde(default)]
762 pub commission: Option<String>,
763 #[serde(default)]
765 pub commission_asset: Option<Ustr>,
766 #[serde(default)]
768 pub margin_asset: Option<Ustr>,
769}
770
771#[derive(Clone, Debug, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct BinanceFuturesAccountInfo {
775 #[serde(
777 default,
778 deserialize_with = "deserialize_optional_decimal_from_str",
779 serialize_with = "serialize_optional_decimal_as_str"
780 )]
781 pub total_initial_margin: Option<Decimal>,
782 #[serde(
784 default,
785 deserialize_with = "deserialize_optional_decimal_from_str",
786 serialize_with = "serialize_optional_decimal_as_str"
787 )]
788 pub total_maint_margin: Option<Decimal>,
789 #[serde(
791 default,
792 deserialize_with = "deserialize_optional_decimal_from_str",
793 serialize_with = "serialize_optional_decimal_as_str"
794 )]
795 pub total_wallet_balance: Option<Decimal>,
796 #[serde(
798 default,
799 deserialize_with = "deserialize_optional_decimal_from_str",
800 serialize_with = "serialize_optional_decimal_as_str"
801 )]
802 pub total_unrealized_profit: Option<Decimal>,
803 #[serde(
805 default,
806 deserialize_with = "deserialize_optional_decimal_from_str",
807 serialize_with = "serialize_optional_decimal_as_str"
808 )]
809 pub total_margin_balance: Option<Decimal>,
810 #[serde(
812 default,
813 deserialize_with = "deserialize_optional_decimal_from_str",
814 serialize_with = "serialize_optional_decimal_as_str"
815 )]
816 pub total_position_initial_margin: Option<Decimal>,
817 #[serde(
819 default,
820 deserialize_with = "deserialize_optional_decimal_from_str",
821 serialize_with = "serialize_optional_decimal_as_str"
822 )]
823 pub total_open_order_initial_margin: Option<Decimal>,
824 #[serde(
826 default,
827 deserialize_with = "deserialize_optional_decimal_from_str",
828 serialize_with = "serialize_optional_decimal_as_str"
829 )]
830 pub total_cross_wallet_balance: Option<Decimal>,
831 #[serde(
833 default,
834 deserialize_with = "deserialize_optional_decimal_from_str",
835 serialize_with = "serialize_optional_decimal_as_str"
836 )]
837 pub total_cross_un_pnl: Option<Decimal>,
838 #[serde(
840 default,
841 deserialize_with = "deserialize_optional_decimal_from_str",
842 serialize_with = "serialize_optional_decimal_as_str"
843 )]
844 pub available_balance: Option<Decimal>,
845 #[serde(
847 default,
848 deserialize_with = "deserialize_optional_decimal_from_str",
849 serialize_with = "serialize_optional_decimal_as_str"
850 )]
851 pub max_withdraw_amount: Option<Decimal>,
852 #[serde(default)]
854 pub can_deposit: Option<bool>,
855 #[serde(default)]
857 pub can_trade: Option<bool>,
858 #[serde(default)]
860 pub can_withdraw: Option<bool>,
861 #[serde(default)]
863 pub multi_assets_margin: Option<bool>,
864 #[serde(default)]
866 pub update_time: Option<i64>,
867 #[serde(default)]
869 pub assets: Vec<BinanceFuturesBalance>,
870 #[serde(default)]
872 pub positions: Vec<BinanceAccountPosition>,
873}
874
875impl BinanceFuturesAccountInfo {
876 pub fn to_account_state(
882 &self,
883 account_id: AccountId,
884 ts_init: UnixNanos,
885 ) -> anyhow::Result<AccountState> {
886 let mut balances = Vec::with_capacity(self.assets.len());
887
888 for asset in &self.assets {
889 let currency = Currency::get_or_create_crypto_with_context(
890 asset.asset.as_str(),
891 Some("futures balance"),
892 );
893
894 let balance = AccountBalance::from_total_and_free(
895 asset.wallet_balance,
896 asset.available_balance,
897 currency,
898 )
899 .context("failed to build account balance")?;
900 balances.push(balance);
901 }
902
903 if balances.is_empty() {
905 let zero_currency = Currency::USDT();
906 let zero_money = Money::zero(zero_currency);
907 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
908 balances.push(zero_balance);
909 }
910
911 let mut margins = Vec::new();
916
917 for asset in &self.assets {
918 let initial_dec = asset.initial_margin.unwrap_or_default();
919 let maint_dec = asset.maint_margin.unwrap_or_default();
920
921 if initial_dec.is_zero() && maint_dec.is_zero() {
922 continue;
923 }
924
925 let currency = Currency::get_or_create_crypto_with_context(
926 asset.asset.as_str(),
927 Some("futures margin"),
928 );
929 let initial = Money::from_decimal(initial_dec, currency)
930 .unwrap_or_else(|_| Money::zero(currency));
931 let maintenance =
932 Money::from_decimal(maint_dec, currency).unwrap_or_else(|_| Money::zero(currency));
933 margins.push(MarginBalance::new(initial, maintenance, None));
934 }
935
936 let ts_event = self
937 .update_time
938 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
939
940 Ok(AccountState::new(
941 account_id,
942 AccountType::Margin,
943 balances,
944 margins,
945 true, UUID4::new(),
947 ts_event,
948 ts_init,
949 None,
950 ))
951 }
952}
953
954#[derive(Clone, Debug, Serialize, Deserialize)]
956#[serde(rename_all = "camelCase")]
957pub struct BinanceHedgeModeResponse {
958 pub dual_side_position: bool,
960}
961
962#[derive(Clone, Debug, Serialize, Deserialize)]
964#[serde(rename_all = "camelCase")]
965pub struct BinanceLeverageResponse {
966 pub symbol: Ustr,
968 pub leverage: u32,
970 #[serde(default)]
972 pub max_notional_value: Option<String>,
973}
974
975#[derive(Clone, Debug, Serialize, Deserialize)]
977#[serde(rename_all = "camelCase")]
978pub struct BinanceCancelAllOrdersResponse {
979 pub code: i32,
981 pub msg: String,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize)]
987#[serde(rename_all = "camelCase")]
988pub struct BinanceFuturesOrder {
989 pub symbol: Ustr,
991 pub order_id: i64,
993 pub client_order_id: String,
995 pub orig_qty: String,
997 pub executed_qty: String,
999 #[serde(default = "zero_decimal_string")]
1001 pub cum_quote: String,
1002 pub price: String,
1004 #[serde(default)]
1006 pub avg_price: Option<String>,
1007 #[serde(default)]
1009 pub stop_price: Option<String>,
1010 pub status: BinanceOrderStatus,
1012 pub time_in_force: BinanceTimeInForce,
1014 #[serde(rename = "type")]
1016 pub order_type: BinanceFuturesOrderType,
1017 #[serde(default)]
1019 pub orig_type: Option<BinanceFuturesOrderType>,
1020 pub side: BinanceSide,
1022 #[serde(default)]
1024 pub position_side: Option<BinancePositionSide>,
1025 #[serde(default)]
1027 pub reduce_only: Option<bool>,
1028 #[serde(default)]
1030 pub close_position: Option<bool>,
1031 #[serde(default)]
1033 pub activate_price: Option<String>,
1034 #[serde(default)]
1036 pub price_rate: Option<String>,
1037 #[serde(default)]
1039 pub working_type: Option<BinanceWorkingType>,
1040 #[serde(default)]
1042 pub price_protect: Option<bool>,
1043 #[serde(default)]
1045 pub is_isolated: Option<bool>,
1046 #[serde(default)]
1048 pub good_till_date: Option<i64>,
1049 #[serde(default)]
1051 pub price_match: Option<BinancePriceMatch>,
1052 #[serde(default)]
1054 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
1055 #[serde(default)]
1057 pub update_time: Option<i64>,
1058 #[serde(default)]
1060 pub working_type_id: Option<i64>,
1061}
1062
1063fn zero_decimal_string() -> String {
1064 "0".to_string()
1065}
1066
1067impl BinanceFuturesOrder {
1068 pub fn to_order_status_report(
1074 &self,
1075 account_id: AccountId,
1076 instrument_id: InstrumentId,
1077 size_precision: u8,
1078 treat_expired_as_canceled: bool,
1079 ts_init: UnixNanos,
1080 ) -> anyhow::Result<OrderStatusReport> {
1081 let ts_event = self
1082 .update_time
1083 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
1084
1085 let client_order_id = ClientOrderId::new(decode_broker_id(
1086 &self.client_order_id,
1087 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1088 ));
1089 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1090
1091 let order_side = match self.side {
1092 BinanceSide::Buy => OrderSide::Buy,
1093 BinanceSide::Sell => OrderSide::Sell,
1094 };
1095
1096 let order_type = self.order_type.to_nautilus_order_type();
1097 let time_in_force = self.time_in_force.to_nautilus_time_in_force();
1098 let order_status = self
1099 .status
1100 .to_nautilus_order_status(treat_expired_as_canceled);
1101
1102 let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
1103 let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
1104
1105 Ok(OrderStatusReport::new(
1106 account_id,
1107 instrument_id,
1108 Some(client_order_id),
1109 venue_order_id,
1110 order_side,
1111 order_type,
1112 time_in_force,
1113 order_status,
1114 Quantity::from_decimal_dp(quantity, size_precision)
1115 .context("invalid orig_qty precision")?,
1116 Quantity::from_decimal_dp(filled_qty, size_precision)
1117 .context("invalid executed_qty precision")?,
1118 ts_event,
1119 ts_event,
1120 ts_init,
1121 Some(UUID4::new()),
1122 ))
1123 }
1124}
1125
1126impl BinanceFuturesOrderType {
1127 #[must_use]
1129 pub fn is_post_only(&self) -> bool {
1130 false }
1132
1133 #[must_use]
1135 pub fn to_nautilus_order_type(&self) -> OrderType {
1136 match self {
1137 Self::Market => OrderType::Market,
1138 Self::Limit => OrderType::Limit,
1139 Self::Stop => OrderType::StopLimit,
1140 Self::StopMarket => OrderType::StopMarket,
1141 Self::TakeProfit => OrderType::LimitIfTouched,
1142 Self::TakeProfitMarket => OrderType::MarketIfTouched,
1143 Self::TrailingStopMarket => OrderType::TrailingStopMarket,
1144 Self::Liquidation | Self::Adl => OrderType::Market, Self::Unknown => OrderType::Market,
1146 }
1147 }
1148}
1149
1150impl BinanceTimeInForce {
1151 #[must_use]
1153 pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1154 match self {
1155 Self::Gtc => TimeInForce::Gtc,
1156 Self::Ioc => TimeInForce::Ioc,
1157 Self::Fok => TimeInForce::Fok,
1158 Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
1160 Self::Rpi => TimeInForce::Ioc, Self::Unknown => TimeInForce::Gtc, }
1163 }
1164}
1165
1166impl BinanceOrderStatus {
1167 #[must_use]
1169 pub fn to_nautilus_order_status(&self, treat_expired_as_canceled: bool) -> OrderStatus {
1170 match self {
1171 Self::New | Self::PendingNew => OrderStatus::Accepted,
1172 Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1173 Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
1174 Self::Canceled => OrderStatus::Canceled,
1175 Self::PendingCancel => OrderStatus::PendingCancel,
1176 Self::Rejected => OrderStatus::Rejected,
1177 Self::Expired | Self::ExpiredInMatch => {
1178 if treat_expired_as_canceled {
1179 OrderStatus::Canceled
1180 } else {
1181 OrderStatus::Expired
1182 }
1183 }
1184 Self::Unknown => OrderStatus::Initialized,
1185 }
1186 }
1187}
1188
1189impl BinanceUserTrade {
1190 pub fn to_fill_report(
1196 &self,
1197 account_id: AccountId,
1198 instrument_id: InstrumentId,
1199 price_precision: u8,
1200 size_precision: u8,
1201 bnfcr_currency: Currency,
1202 ts_init: UnixNanos,
1203 ) -> anyhow::Result<FillReport> {
1204 let ts_event = UnixNanos::from_millis(self.time as u64);
1205
1206 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1207 let trade_id = TradeId::new(self.id.to_string());
1208
1209 let order_side = match self.side {
1210 BinanceSide::Buy => OrderSide::Buy,
1211 BinanceSide::Sell => OrderSide::Sell,
1212 };
1213
1214 let liquidity_side = if self.maker {
1215 LiquiditySide::Maker
1216 } else {
1217 LiquiditySide::Taker
1218 };
1219
1220 let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1221 let last_px: Decimal = self.price.parse().context("invalid price")?;
1222
1223 let commission_currency = self
1224 .commission_asset
1225 .as_ref()
1226 .map_or(bnfcr_currency, |asset| {
1227 normalize_futures_asset(asset, bnfcr_currency)
1228 });
1229 let commission = match self.commission.as_ref() {
1230 Some(raw) => {
1231 let decimal = parse_required_decimal(raw, "commission")?;
1232 Money::from_decimal(decimal, commission_currency)?
1233 }
1234 None => Money::zero(commission_currency),
1235 };
1236
1237 Ok(FillReport::new(
1238 account_id,
1239 instrument_id,
1240 venue_order_id,
1241 trade_id,
1242 order_side,
1243 Quantity::from_decimal_dp(last_qty, size_precision).context("invalid qty precision")?,
1244 Price::from_decimal_dp(last_px, price_precision).context("invalid price precision")?,
1245 commission,
1246 liquidity_side,
1247 None, None, ts_event,
1250 ts_init,
1251 Some(UUID4::new()),
1252 ))
1253 }
1254}
1255
1256#[derive(Clone, Debug, Deserialize)]
1260#[serde(untagged)]
1261pub enum BatchOrderResult {
1262 Success(Box<BinanceFuturesOrder>),
1264 Error(BatchOrderError),
1266}
1267
1268#[derive(Clone, Debug, Deserialize)]
1270pub struct BatchOrderError {
1271 pub code: i64,
1273 pub msg: String,
1275}
1276
1277#[derive(Debug, Clone, Deserialize)]
1279#[serde(rename_all = "camelCase")]
1280pub struct ListenKeyResponse {
1281 pub listen_key: String,
1283}
1284
1285#[derive(Clone, Debug, Serialize, Deserialize)]
1295#[serde(rename_all = "camelCase")]
1296pub struct BinanceFuturesAlgoOrder {
1297 pub algo_id: i64,
1299 pub client_algo_id: String,
1301 pub algo_type: BinanceAlgoType,
1303 #[serde(rename = "orderType", alias = "type")]
1305 pub order_type: BinanceFuturesOrderType,
1306 pub symbol: Ustr,
1308 pub side: BinanceSide,
1310 #[serde(default)]
1312 pub position_side: Option<BinancePositionSide>,
1313 #[serde(default)]
1315 pub time_in_force: Option<BinanceTimeInForce>,
1316 #[serde(default)]
1318 pub quantity: Option<String>,
1319 #[serde(default)]
1321 pub algo_status: Option<BinanceAlgoStatus>,
1322 #[serde(default)]
1324 pub trigger_price: Option<String>,
1325 #[serde(default)]
1327 pub price: Option<String>,
1328 #[serde(default)]
1330 pub working_type: Option<BinanceWorkingType>,
1331 #[serde(default)]
1333 pub close_position: Option<bool>,
1334 #[serde(default)]
1336 pub price_protect: Option<bool>,
1337 #[serde(default)]
1339 pub reduce_only: Option<bool>,
1340 #[serde(default)]
1342 pub activate_price: Option<String>,
1343 #[serde(default)]
1345 pub callback_rate: Option<String>,
1346 #[serde(default)]
1348 pub create_time: Option<i64>,
1349 #[serde(default)]
1351 pub update_time: Option<i64>,
1352 #[serde(default)]
1354 pub trigger_time: Option<i64>,
1355 #[serde(default)]
1357 pub actual_order_id: Option<String>,
1358 #[serde(default)]
1360 pub executed_qty: Option<String>,
1361 #[serde(default)]
1363 pub avg_price: Option<String>,
1364}
1365
1366impl BinanceFuturesAlgoOrder {
1367 pub fn to_order_status_report(
1373 &self,
1374 account_id: AccountId,
1375 instrument_id: InstrumentId,
1376 size_precision: u8,
1377 ts_init: UnixNanos,
1378 ) -> anyhow::Result<OrderStatusReport> {
1379 let ts_event = self
1380 .update_time
1381 .or(self.create_time)
1382 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
1383
1384 let client_order_id = ClientOrderId::new(decode_broker_id(
1385 &self.client_algo_id,
1386 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1387 ));
1388 let venue_order_id = self
1389 .actual_order_id
1390 .as_ref()
1391 .filter(|id| !id.is_empty())
1392 .map_or_else(
1393 || VenueOrderId::new(self.algo_id.to_string()),
1394 |id| VenueOrderId::new(id.clone()),
1395 );
1396
1397 let order_side = match self.side {
1398 BinanceSide::Buy => OrderSide::Buy,
1399 BinanceSide::Sell => OrderSide::Sell,
1400 };
1401
1402 let order_type = self.parse_order_type();
1403 let time_in_force = self
1404 .time_in_force
1405 .as_ref()
1406 .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1407 let order_status = self.parse_order_status();
1408
1409 let quantity: Decimal = self
1410 .quantity
1411 .as_ref()
1412 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1413 .context("invalid quantity")?;
1414 let filled_qty: Decimal = self
1415 .executed_qty
1416 .as_ref()
1417 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1418 .context("invalid executed_qty")?;
1419
1420 Ok(OrderStatusReport::new(
1421 account_id,
1422 instrument_id,
1423 Some(client_order_id),
1424 venue_order_id,
1425 order_side,
1426 order_type,
1427 time_in_force,
1428 order_status,
1429 Quantity::from_decimal_dp(quantity, size_precision)
1430 .context("invalid quantity precision")?,
1431 Quantity::from_decimal_dp(filled_qty, size_precision)
1432 .context("invalid executed_qty precision")?,
1433 ts_event,
1434 ts_event,
1435 ts_init,
1436 Some(UUID4::new()),
1437 ))
1438 }
1439
1440 fn parse_order_type(&self) -> OrderType {
1441 self.order_type.into()
1442 }
1443
1444 fn parse_order_status(&self) -> OrderStatus {
1445 match self.algo_status {
1446 Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1447 Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1448 Some(BinanceAlgoStatus::Triggered) => OrderStatus::Accepted,
1449 Some(BinanceAlgoStatus::Finished) => {
1450 if let Some(qty) = &self.executed_qty
1452 && let Ok(dec) = qty.parse::<Decimal>()
1453 && !dec.is_zero()
1454 {
1455 return OrderStatus::Filled;
1456 }
1457 OrderStatus::Canceled
1458 }
1459 Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1460 Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1461 Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1462 Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1463 }
1464 }
1465}
1466
1467#[derive(Clone, Debug, Deserialize)]
1469#[serde(rename_all = "camelCase")]
1470pub struct BinanceFuturesAlgoOrderCancelResponse {
1471 pub algo_id: i64,
1473 pub client_algo_id: String,
1475 pub code: String,
1477 pub msg: String,
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use rstest::rstest;
1484
1485 use super::*;
1486 use crate::common::testing::load_fixture_string;
1487
1488 #[rstest]
1489 fn test_parse_account_info_v2() {
1490 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1491 let account: BinanceFuturesAccountInfo =
1492 serde_json::from_str(&json).expect("Failed to parse account info");
1493
1494 assert_eq!(
1495 account.total_wallet_balance,
1496 Some(Decimal::from_str_exact("23.72469206").unwrap())
1497 );
1498 assert_eq!(account.assets.len(), 1);
1499 assert_eq!(account.assets[0].asset.as_str(), "USDT");
1500 assert_eq!(
1501 account.assets[0].wallet_balance,
1502 Decimal::from_str_exact("23.72469206").unwrap()
1503 );
1504 assert_eq!(account.positions.len(), 1);
1505 assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1506 assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1507 }
1508
1509 #[rstest]
1510 fn test_account_info_to_account_state_zero_margins() {
1511 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1512 let account: BinanceFuturesAccountInfo =
1513 serde_json::from_str(&json).expect("Failed to parse account info");
1514
1515 let account_id = AccountId::from("BINANCE-001");
1516 let ts_init = UnixNanos::from(1_000_000_000u64);
1517 let state = account.to_account_state(account_id, ts_init).unwrap();
1518
1519 assert_eq!(state.account_id, account_id);
1520 assert_eq!(state.account_type, AccountType::Margin);
1521 assert!(!state.balances.is_empty());
1522 assert_eq!(state.margins.len(), 0);
1523 }
1524
1525 #[rstest]
1526 fn test_account_info_to_account_state_with_margins() {
1527 let json = r#"{
1528 "totalInitialMargin": "500.25000000",
1529 "totalMaintMargin": "250.75000000",
1530 "totalWalletBalance": "10000.00000000",
1531 "assets": [{
1532 "asset": "USDT",
1533 "walletBalance": "10000.00000000",
1534 "availableBalance": "9500.00000000",
1535 "initialMargin": "500.25000000",
1536 "maintMargin": "250.75000000",
1537 "updateTime": 1617939110373
1538 }],
1539 "positions": []
1540 }"#;
1541 let account: BinanceFuturesAccountInfo =
1542 serde_json::from_str(json).expect("Failed to parse account info");
1543
1544 let account_id = AccountId::from("BINANCE-001");
1545 let ts_init = UnixNanos::from(1_000_000_000u64);
1546 let state = account.to_account_state(account_id, ts_init).unwrap();
1547
1548 assert_eq!(state.margins.len(), 1);
1549 let margin = &state.margins[0];
1550 assert!(margin.instrument_id.is_none());
1551 assert_eq!(margin.currency.code.as_str(), "USDT");
1552 assert_eq!(margin.initial.as_f64(), 500.25);
1553 assert_eq!(margin.maintenance.as_f64(), 250.75);
1554 }
1555
1556 #[rstest]
1557 fn test_account_info_to_account_state_coin_margined_per_base_coin() {
1558 let json = r#"{
1559 "totalWalletBalance": "0.00000000",
1560 "assets": [
1561 {
1562 "asset": "BTC",
1563 "walletBalance": "1.50000000",
1564 "availableBalance": "1.40000000",
1565 "initialMargin": "0.05000000",
1566 "maintMargin": "0.02500000",
1567 "updateTime": 1617939110373
1568 },
1569 {
1570 "asset": "ETH",
1571 "walletBalance": "10.00000000",
1572 "availableBalance": "9.00000000",
1573 "initialMargin": "0.80000000",
1574 "maintMargin": "0.40000000",
1575 "updateTime": 1617939110373
1576 }
1577 ],
1578 "positions": []
1579 }"#;
1580 let account: BinanceFuturesAccountInfo =
1581 serde_json::from_str(json).expect("Failed to parse account info");
1582
1583 let account_id = AccountId::from("BINANCE-001");
1584 let ts_init = UnixNanos::from(1_000_000_000u64);
1585 let state = account.to_account_state(account_id, ts_init).unwrap();
1586
1587 assert_eq!(state.margins.len(), 2);
1588 assert!(state.margins.iter().all(|m| m.instrument_id.is_none()));
1589 let btc = state
1590 .margins
1591 .iter()
1592 .find(|m| m.currency.code.as_str() == "BTC")
1593 .expect("BTC margin missing");
1594 assert_eq!(btc.initial.as_f64(), 0.05);
1595 assert_eq!(btc.maintenance.as_f64(), 0.025);
1596 let eth = state
1597 .margins
1598 .iter()
1599 .find(|m| m.currency.code.as_str() == "ETH")
1600 .expect("ETH margin missing");
1601 assert_eq!(eth.initial.as_f64(), 0.8);
1602 assert_eq!(eth.maintenance.as_f64(), 0.4);
1603 }
1604
1605 #[rstest]
1610 fn test_account_info_to_account_state_precision_drift() {
1611 let json = r#"{
1612 "assets": [{
1613 "asset": "USDT",
1614 "walletBalance": "10.000000034999",
1615 "availableBalance": "9.999999994999",
1616 "updateTime": 1617939110373
1617 }],
1618 "positions": []
1619 }"#;
1620 let account: BinanceFuturesAccountInfo =
1621 serde_json::from_str(json).expect("Failed to parse account info");
1622
1623 let account_id = AccountId::from("BINANCE-001");
1624 let ts_init = UnixNanos::from(1_000_000_000u64);
1625 let state = account.to_account_state(account_id, ts_init).unwrap();
1626
1627 assert_eq!(state.balances.len(), 1);
1628 let balance = &state.balances[0];
1629 assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
1630 }
1631
1632 #[rstest]
1633 fn test_account_info_to_account_state_empty_balance() {
1634 let json = r#"{
1636 "assets": [{
1637 "asset": "USDT",
1638 "walletBalance": "",
1639 "availableBalance": "",
1640 "updateTime": 0
1641 }],
1642 "positions": []
1643 }"#;
1644 let account: BinanceFuturesAccountInfo =
1645 serde_json::from_str(json).expect("Failed to parse account info");
1646
1647 let account_id = AccountId::from("BINANCE-001");
1648 let ts_init = UnixNanos::from(1_000_000_000u64);
1649 let state = account.to_account_state(account_id, ts_init).unwrap();
1650
1651 assert_eq!(state.balances.len(), 1);
1652 let balance = &state.balances[0];
1653 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1654 assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
1655 assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
1656 }
1657
1658 #[rstest]
1659 fn test_account_info_to_account_state_empty_assets() {
1660 let json = r#"{
1662 "assets": [],
1663 "positions": []
1664 }"#;
1665 let account: BinanceFuturesAccountInfo =
1666 serde_json::from_str(json).expect("Failed to parse account info");
1667
1668 let account_id = AccountId::from("BINANCE-001");
1669 let ts_init = UnixNanos::from(1_000_000_000u64);
1670 let state = account.to_account_state(account_id, ts_init).unwrap();
1671
1672 assert_eq!(state.balances.len(), 1);
1673 let balance = &state.balances[0];
1674 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1675 }
1676
1677 #[rstest]
1678 fn test_parse_position_risk() {
1679 let json = load_fixture_string("futures/http_json/position_risk.json");
1680 let positions: Vec<BinancePositionRisk> =
1681 serde_json::from_str(&json).expect("Failed to parse position risk");
1682
1683 assert_eq!(positions.len(), 1);
1684 assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
1685 assert_eq!(positions[0].position_amt, "0.001");
1686 assert_eq!(positions[0].mark_price, "51000.0");
1687 assert_eq!(positions[0].leverage, "20");
1688 }
1689
1690 #[rstest]
1691 fn test_parse_balance_with_v1_field() {
1692 let json = load_fixture_string("futures/http_json/balance.json");
1694 let balances: Vec<BinanceFuturesBalance> =
1695 serde_json::from_str(&json).expect("Failed to parse balance");
1696
1697 assert_eq!(balances.len(), 1);
1698 assert_eq!(balances[0].asset.as_str(), "USDT");
1699 assert_eq!(
1701 balances[0].wallet_balance,
1702 Decimal::from_str_exact("122.12345678").unwrap()
1703 );
1704 assert_eq!(
1705 balances[0].available_balance,
1706 Decimal::from_str_exact("122.12345678").unwrap()
1707 );
1708 }
1709
1710 #[rstest]
1711 fn test_parse_balance_with_v2_field() {
1712 let json = r#"{
1714 "asset": "USDT",
1715 "walletBalance": "100.00000000",
1716 "availableBalance": "100.00000000",
1717 "updateTime": 1617939110373
1718 }"#;
1719
1720 let balance: BinanceFuturesBalance =
1721 serde_json::from_str(json).expect("Failed to parse balance");
1722
1723 assert_eq!(balance.asset.as_str(), "USDT");
1724 assert_eq!(
1725 balance.wallet_balance,
1726 Decimal::from_str_exact("100.00000000").unwrap()
1727 );
1728 }
1729
1730 #[rstest]
1731 fn test_parse_order() {
1732 let json = load_fixture_string("futures/http_json/order_response.json");
1733 let order: BinanceFuturesOrder =
1734 serde_json::from_str(&json).expect("Failed to parse order");
1735
1736 assert_eq!(order.order_id, 12345678);
1737 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1738 assert_eq!(order.status, BinanceOrderStatus::New);
1739 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
1740 assert_eq!(order.side, BinanceSide::Buy);
1741 assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
1742 assert_eq!(order.price_match, Some(BinancePriceMatch::None));
1743 assert_eq!(
1744 order.self_trade_prevention_mode,
1745 Some(BinanceSelfTradePreventionMode::None)
1746 );
1747 }
1748
1749 #[rstest]
1750 fn test_parse_order_defaults_missing_cum_quote_to_zero() {
1751 let json = load_fixture_string("futures/http_json/order_response.json");
1752 let mut value: Value = serde_json::from_str(&json).expect("Failed to parse order fixture");
1753
1754 value
1755 .as_object_mut()
1756 .expect("Order fixture should be a JSON object")
1757 .remove("cumQuote");
1758
1759 let order: BinanceFuturesOrder =
1760 serde_json::from_value(value).expect("Failed to parse order");
1761
1762 assert_eq!(order.cum_quote, "0");
1763 }
1764
1765 #[rstest]
1766 fn test_parse_kline_rejects_non_string_price() {
1767 let value = serde_json::json!([
1768 1_625_474_304_000_i64,
1769 50000.00,
1770 "51000.00",
1771 "49000.00",
1772 "50500.00",
1773 "12.5",
1774 1_625_474_364_000_i64,
1775 "631250.00",
1776 100_i64,
1777 "6.2",
1778 "313100.00"
1779 ]);
1780
1781 let error = serde_json::from_value::<BinanceFuturesKline>(value)
1782 .unwrap_err()
1783 .to_string();
1784
1785 assert!(error.contains("open"));
1786 }
1787
1788 #[rstest]
1789 fn test_parse_hedge_mode_response() {
1790 let json = r#"{"dualSidePosition": true}"#;
1791 let response: BinanceHedgeModeResponse =
1792 serde_json::from_str(json).expect("Failed to parse hedge mode");
1793 assert!(response.dual_side_position);
1794 }
1795
1796 #[rstest]
1797 fn test_parse_leverage_response() {
1798 let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
1799 let response: BinanceLeverageResponse =
1800 serde_json::from_str(json).expect("Failed to parse leverage");
1801 assert_eq!(response.symbol.as_str(), "BTCUSDT");
1802 assert_eq!(response.leverage, 20);
1803 }
1804
1805 #[rstest]
1806 fn test_parse_listen_key_response() {
1807 let json =
1808 r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
1809 let response: ListenKeyResponse =
1810 serde_json::from_str(json).expect("Failed to parse listen key");
1811 assert!(!response.listen_key.is_empty());
1812 }
1813
1814 #[rstest]
1815 fn test_parse_account_position() {
1816 let json = r#"{
1817 "symbol": "ETHUSDT",
1818 "initialMargin": "100.00",
1819 "maintMargin": "50.00",
1820 "unrealizedProfit": "10.00",
1821 "positionInitialMargin": "100.00",
1822 "openOrderInitialMargin": "0",
1823 "leverage": "10",
1824 "isolated": true,
1825 "entryPrice": "2000.00",
1826 "maxNotional": "100000",
1827 "bidNotional": "0",
1828 "askNotional": "0",
1829 "positionSide": "LONG",
1830 "positionAmt": "0.5",
1831 "updateTime": 1625474304765
1832 }"#;
1833
1834 let position: BinanceAccountPosition =
1835 serde_json::from_str(json).expect("Failed to parse account position");
1836
1837 assert_eq!(position.symbol.as_str(), "ETHUSDT");
1838 assert_eq!(position.leverage, Some("10".to_string()));
1839 assert_eq!(position.isolated, Some(true));
1840 assert_eq!(position.position_side, Some(BinancePositionSide::Long));
1841 }
1842
1843 #[rstest]
1844 fn test_parse_algo_order() {
1845 let json = r#"{
1846 "algoId": 123456789,
1847 "clientAlgoId": "test-algo-order-1",
1848 "algoType": "CONDITIONAL",
1849 "type": "STOP_MARKET",
1850 "symbol": "BTCUSDT",
1851 "side": "BUY",
1852 "positionSide": "BOTH",
1853 "timeInForce": "GTC",
1854 "quantity": "0.001",
1855 "algoStatus": "NEW",
1856 "triggerPrice": "45000.00",
1857 "workingType": "MARK_PRICE",
1858 "reduceOnly": false,
1859 "createTime": 1625474304765,
1860 "updateTime": 1625474304765
1861 }"#;
1862
1863 let order: BinanceFuturesAlgoOrder =
1864 serde_json::from_str(json).expect("Failed to parse algo order");
1865
1866 assert_eq!(order.algo_id, 123456789);
1867 assert_eq!(order.client_algo_id, "test-algo-order-1");
1868 assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
1869 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
1870 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1871 assert_eq!(order.side, BinanceSide::Buy);
1872 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
1873 assert_eq!(order.trigger_price, Some("45000.00".to_string()));
1874 }
1875
1876 #[rstest]
1877 fn test_parse_algo_order_triggered() {
1878 let json = r#"{
1879 "algoId": 123456789,
1880 "clientAlgoId": "test-algo-order-2",
1881 "algoType": "CONDITIONAL",
1882 "type": "TAKE_PROFIT",
1883 "symbol": "ETHUSDT",
1884 "side": "SELL",
1885 "algoStatus": "TRIGGERED",
1886 "triggerPrice": "2500.00",
1887 "price": "2500.00",
1888 "actualOrderId": "987654321",
1889 "executedQty": "0.5",
1890 "avgPrice": "2499.50"
1891 }"#;
1892
1893 let order: BinanceFuturesAlgoOrder =
1894 serde_json::from_str(json).expect("Failed to parse triggered algo order");
1895
1896 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Triggered));
1897 assert_eq!(order.order_type, BinanceFuturesOrderType::TakeProfit);
1898 assert_eq!(order.actual_order_id, Some("987654321".to_string()));
1899 assert_eq!(order.executed_qty, Some("0.5".to_string()));
1900 }
1901
1902 #[rstest]
1903 fn test_parse_algo_order_cancel_response() {
1904 let json = r#"{
1905 "algoId": 123456789,
1906 "clientAlgoId": "test-algo-order-1",
1907 "code": "200",
1908 "msg": "success"
1909 }"#;
1910
1911 let response: BinanceFuturesAlgoOrderCancelResponse =
1912 serde_json::from_str(json).expect("Failed to parse algo cancel response");
1913
1914 assert_eq!(response.algo_id, 123456789);
1915 assert_eq!(response.client_algo_id, "test-algo-order-1");
1916 assert_eq!(response.code, "200");
1917 assert_eq!(response.msg, "success");
1918 }
1919
1920 #[rstest]
1921 fn test_order_to_report_decodes_broker_id() {
1922 let json = r#"{
1923 "orderId": 12345678,
1924 "symbol": "BTCUSDT",
1925 "status": "NEW",
1926 "clientOrderId": "x-aHRE4BCj-T0000000000000",
1927 "price": "50000.00",
1928 "avgPrice": "0.00",
1929 "origQty": "0.001",
1930 "executedQty": "0.000",
1931 "cumQuote": "0.00",
1932 "timeInForce": "GTC",
1933 "type": "LIMIT",
1934 "reduceOnly": false,
1935 "closePosition": false,
1936 "side": "BUY",
1937 "positionSide": "BOTH",
1938 "stopPrice": "0.00",
1939 "workingType": "CONTRACT_PRICE",
1940 "priceProtect": false,
1941 "origType": "LIMIT",
1942 "priceMatch": "NONE",
1943 "selfTradePreventionMode": "NONE",
1944 "goodTillDate": 0,
1945 "time": 1625474304765,
1946 "updateTime": 1625474304765
1947 }"#;
1948
1949 let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
1950 let account_id = AccountId::from("BINANCE-FUTURES-001");
1951 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1952 let ts_init = UnixNanos::from(1_000_000_000u64);
1953
1954 let report = order
1955 .to_order_status_report(account_id, instrument_id, 3, false, ts_init)
1956 .unwrap();
1957
1958 assert_eq!(
1959 report.client_order_id,
1960 Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
1961 );
1962 }
1963
1964 #[rstest]
1965 fn test_user_trade_to_fill_report_rejects_invalid_commission() {
1966 let trade = BinanceUserTrade {
1967 symbol: Ustr::from("BTCUSDT"),
1968 id: 100,
1969 order_id: 200,
1970 price: "50000.00".to_string(),
1971 qty: "0.001".to_string(),
1972 quote_qty: None,
1973 realized_pnl: "0".to_string(),
1974 side: BinanceSide::Buy,
1975 position_side: None,
1976 time: 1_625_474_304_000,
1977 buyer: true,
1978 maker: false,
1979 commission: Some("not-a-number".to_string()),
1980 commission_asset: Some(Ustr::from("USDT")),
1981 margin_asset: None,
1982 };
1983
1984 let result = trade.to_fill_report(
1985 AccountId::from("BINANCE-FUTURES-001"),
1986 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1987 2,
1988 3,
1989 Currency::USDT(),
1990 UnixNanos::from(1_000_000_000u64),
1991 );
1992
1993 let error = result.unwrap_err().to_string();
1994 assert!(error.contains("commission"));
1995 }
1996
1997 #[rstest]
1998 fn test_algo_order_to_report_decodes_broker_id() {
1999 let json = r#"{
2000 "algoId": 123456789,
2001 "clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
2002 "algoType": "CONDITIONAL",
2003 "type": "STOP_MARKET",
2004 "symbol": "BTCUSDT",
2005 "side": "BUY",
2006 "positionSide": "BOTH",
2007 "timeInForce": "GTC",
2008 "quantity": "0.001",
2009 "algoStatus": "NEW",
2010 "triggerPrice": "45000.00",
2011 "workingType": "MARK_PRICE",
2012 "reduceOnly": false,
2013 "createTime": 1625474304765,
2014 "updateTime": 1625474304765
2015 }"#;
2016
2017 let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
2018 let account_id = AccountId::from("BINANCE-FUTURES-001");
2019 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2020 let ts_init = UnixNanos::from(1_000_000_000u64);
2021
2022 let report = order
2023 .to_order_status_report(account_id, instrument_id, 3, ts_init)
2024 .unwrap();
2025
2026 assert_eq!(
2027 report.client_order_id,
2028 Some(ClientOrderId::from("my-algo-order-1")),
2029 );
2030 }
2031
2032 #[rstest]
2033 #[case(None, "123456789")]
2034 #[case(Some(""), "123456789")]
2035 #[case(Some("987654321"), "987654321")]
2036 fn test_algo_order_to_report_selects_valid_venue_order_id(
2037 #[case] actual_order_id: Option<&str>,
2038 #[case] expected_venue_order_id: &str,
2039 ) {
2040 let order = BinanceFuturesAlgoOrder {
2041 algo_id: 123456789,
2042 client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2043 algo_type: BinanceAlgoType::Conditional,
2044 order_type: BinanceFuturesOrderType::StopMarket,
2045 symbol: Ustr::from("BTCUSDT"),
2046 side: BinanceSide::Buy,
2047 position_side: Some(BinancePositionSide::Both),
2048 time_in_force: Some(BinanceTimeInForce::Gtc),
2049 quantity: Some("0.001".to_string()),
2050 algo_status: Some(BinanceAlgoStatus::New),
2051 trigger_price: Some("45000.00".to_string()),
2052 price: None,
2053 working_type: Some(BinanceWorkingType::MarkPrice),
2054 close_position: Some(false),
2055 price_protect: None,
2056 reduce_only: Some(false),
2057 activate_price: None,
2058 callback_rate: None,
2059 create_time: Some(1_625_474_304_765),
2060 update_time: Some(1_625_474_304_765),
2061 trigger_time: None,
2062 actual_order_id: actual_order_id.map(str::to_string),
2063 executed_qty: None,
2064 avg_price: None,
2065 };
2066 let account_id = AccountId::from("BINANCE-FUTURES-001");
2067 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2068 let ts_init = UnixNanos::from(1_000_000_000u64);
2069
2070 let report = order
2071 .to_order_status_report(account_id, instrument_id, 3, ts_init)
2072 .unwrap();
2073
2074 assert_eq!(
2075 report.venue_order_id,
2076 VenueOrderId::new(expected_venue_order_id)
2077 );
2078 }
2079
2080 #[rstest]
2081 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
2082 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
2083 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
2084 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
2085 fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
2086 #[case] status: BinanceOrderStatus,
2087 #[case] treat_expired_as_canceled: bool,
2088 #[case] expected: OrderStatus,
2089 ) {
2090 let result = status.to_nautilus_order_status(treat_expired_as_canceled);
2091 assert_eq!(result, expected);
2092 }
2093}