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::{
28 AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
29 TrailingOffsetType, TriggerType,
30 },
31 events::AccountState,
32 identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
33 reports::{FillReport, OrderStatusReport},
34 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
35};
36use rust_decimal::Decimal;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39use ustr::Ustr;
40
41use crate::{
42 common::{
43 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
44 encoder::decode_client_order_id,
45 enums::{
46 BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
47 BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
48 BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
49 BinanceTradingStatus, BinanceWorkingType,
50 },
51 models::BinanceRateLimit,
52 parse::{parse_millis, parse_required_decimal},
53 },
54 futures::conversions::{normalize_futures_asset, parse_good_till_date},
55};
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct BinanceServerTime {
61 pub server_time: i64,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct BinanceFuturesTrade {
69 pub id: i64,
71 pub price: String,
73 pub qty: String,
75 pub quote_qty: String,
77 pub time: i64,
79 pub is_buyer_maker: bool,
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct BinanceFuturesAggTrade {
86 #[serde(rename = "a")]
88 pub id: i64,
89 #[serde(rename = "p")]
91 pub price: String,
92 #[serde(rename = "q")]
94 pub qty: String,
95 #[serde(rename = "f")]
97 pub first_trade_id: i64,
98 #[serde(rename = "l")]
100 pub last_trade_id: i64,
101 #[serde(rename = "T")]
103 pub time: i64,
104 #[serde(rename = "m")]
106 pub is_buyer_maker: bool,
107}
108
109#[derive(Clone, Debug)]
111pub struct BinanceFuturesKline {
112 pub open_time: i64,
114 pub open: String,
116 pub high: String,
118 pub low: String,
120 pub close: String,
122 pub volume: String,
124 pub close_time: i64,
126 pub quote_volume: String,
128 pub num_trades: i64,
130 pub taker_buy_base_volume: String,
132 pub taker_buy_quote_volume: String,
134}
135
136impl<'de> Deserialize<'de> for BinanceFuturesKline {
137 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138 where
139 D: serde::Deserializer<'de>,
140 {
141 let arr: Vec<Value> = Vec::deserialize(deserializer)?;
142 if arr.len() < 11 {
143 return Err(serde::de::Error::custom("Invalid kline array length"));
144 }
145
146 Ok(Self {
147 open_time: required_kline_i64::<D::Error>(&arr, 0, "open_time")?,
148 open: required_kline_string::<D::Error>(&arr, 1, "open")?,
149 high: required_kline_string::<D::Error>(&arr, 2, "high")?,
150 low: required_kline_string::<D::Error>(&arr, 3, "low")?,
151 close: required_kline_string::<D::Error>(&arr, 4, "close")?,
152 volume: required_kline_string::<D::Error>(&arr, 5, "volume")?,
153 close_time: required_kline_i64::<D::Error>(&arr, 6, "close_time")?,
154 quote_volume: required_kline_string::<D::Error>(&arr, 7, "quote_volume")?,
155 num_trades: required_kline_i64::<D::Error>(&arr, 8, "num_trades")?,
156 taker_buy_base_volume: required_kline_string::<D::Error>(
157 &arr,
158 9,
159 "taker_buy_base_volume",
160 )?,
161 taker_buy_quote_volume: required_kline_string::<D::Error>(
162 &arr,
163 10,
164 "taker_buy_quote_volume",
165 )?,
166 })
167 }
168}
169
170fn required_kline_i64<E>(arr: &[Value], index: usize, field: &str) -> Result<i64, E>
171where
172 E: serde::de::Error,
173{
174 arr[index]
175 .as_i64()
176 .ok_or_else(|| E::custom(format!("invalid kline {field}")))
177}
178
179fn required_kline_string<E>(arr: &[Value], index: usize, field: &str) -> Result<String, E>
180where
181 E: serde::de::Error,
182{
183 arr[index]
184 .as_str()
185 .map(ToString::to_string)
186 .ok_or_else(|| E::custom(format!("invalid kline {field}")))
187}
188
189#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct BinanceFuturesUsdExchangeInfo {
193 pub timezone: String,
195 pub server_time: i64,
197 pub rate_limits: Vec<BinanceRateLimit>,
199 #[serde(default)]
201 pub exchange_filters: Vec<Value>,
202 #[serde(default)]
204 pub assets: Vec<BinanceFuturesAsset>,
205 pub symbols: Vec<BinanceFuturesUsdSymbol>,
207}
208
209#[derive(Clone, Debug, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct BinanceFuturesAsset {
213 pub asset: Ustr,
215 pub margin_available: bool,
217 #[serde(default)]
219 pub auto_asset_exchange: Option<String>,
220}
221
222#[derive(Clone, Debug, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct BinanceFuturesUsdSymbol {
226 pub symbol: Ustr,
228 pub pair: Ustr,
230 pub contract_type: String,
233 pub delivery_date: i64,
235 pub onboard_date: i64,
237 pub status: BinanceTradingStatus,
239 pub maint_margin_percent: String,
241 pub required_margin_percent: String,
243 pub base_asset: Ustr,
245 pub quote_asset: Ustr,
247 pub margin_asset: Ustr,
249 pub price_precision: i32,
251 pub quantity_precision: i32,
253 pub base_asset_precision: i32,
255 pub quote_precision: i32,
257 #[serde(default)]
259 pub underlying_type: Option<String>,
260 #[serde(default)]
262 pub underlying_sub_type: Vec<String>,
263 #[serde(default)]
265 pub settle_plan: Option<i64>,
266 #[serde(default)]
268 pub trigger_protect: Option<String>,
269 #[serde(default)]
271 pub liquidation_fee: Option<String>,
272 #[serde(default)]
274 pub market_take_bound: Option<String>,
275 pub order_types: Vec<String>,
277 pub time_in_force: Vec<String>,
279 pub filters: Vec<Value>,
281}
282
283#[derive(Clone, Debug, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct BinanceFuturesCoinExchangeInfo {
287 pub timezone: String,
289 pub server_time: i64,
291 pub rate_limits: Vec<BinanceRateLimit>,
293 #[serde(default)]
295 pub exchange_filters: Vec<Value>,
296 pub symbols: Vec<BinanceFuturesCoinSymbol>,
298}
299
300#[derive(Clone, Debug, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct BinanceFuturesCoinSymbol {
304 pub symbol: Ustr,
306 pub pair: Ustr,
308 pub contract_type: String,
310 pub delivery_date: i64,
312 pub onboard_date: i64,
314 #[serde(default)]
316 pub contract_status: Option<BinanceContractStatus>,
317 pub contract_size: i64,
319 pub maint_margin_percent: String,
321 pub required_margin_percent: String,
323 pub base_asset: Ustr,
325 pub quote_asset: Ustr,
327 pub margin_asset: Ustr,
329 pub price_precision: i32,
331 pub quantity_precision: i32,
333 pub base_asset_precision: i32,
335 pub quote_precision: i32,
337 #[serde(default, rename = "equalQtyPrecision")]
339 pub equal_qty_precision: Option<i32>,
340 #[serde(default)]
342 pub trigger_protect: Option<String>,
343 #[serde(default)]
345 pub liquidation_fee: Option<String>,
346 #[serde(default)]
348 pub market_take_bound: Option<String>,
349 pub order_types: Vec<String>,
351 pub time_in_force: Vec<String>,
353 pub filters: Vec<Value>,
355}
356
357#[derive(Clone, Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct BinanceFuturesTicker24hr {
361 pub symbol: Ustr,
363 pub price_change: String,
365 pub price_change_percent: String,
367 pub weighted_avg_price: String,
369 pub last_price: String,
371 #[serde(default)]
373 pub last_qty: Option<String>,
374 pub open_price: String,
376 pub high_price: String,
378 pub low_price: String,
380 pub volume: String,
382 pub quote_volume: String,
384 pub open_time: i64,
386 pub close_time: i64,
388 #[serde(default)]
390 pub first_id: Option<i64>,
391 #[serde(default)]
393 pub last_id: Option<i64>,
394 #[serde(default)]
396 pub count: Option<i64>,
397}
398
399#[derive(Clone, Debug, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct BinanceFuturesMarkPrice {
403 pub symbol: Ustr,
405 pub mark_price: String,
407 #[serde(default)]
409 pub index_price: Option<String>,
410 #[serde(default)]
412 pub estimated_settle_price: Option<String>,
413 #[serde(default)]
415 pub last_funding_rate: Option<String>,
416 #[serde(default)]
418 pub next_funding_time: Option<i64>,
419 #[serde(default)]
421 pub interest_rate: Option<String>,
422 pub time: i64,
424}
425
426#[derive(Clone, Debug, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct BinanceOrderBook {
430 pub last_update_id: i64,
432 pub bids: Vec<(String, String)>,
434 pub asks: Vec<(String, String)>,
436 #[serde(default, rename = "E")]
438 pub event_time: Option<i64>,
439 #[serde(default, rename = "T")]
441 pub transaction_time: Option<i64>,
442}
443
444#[derive(Clone, Debug, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct BinanceBookTicker {
448 pub symbol: Ustr,
450 pub bid_price: String,
452 pub bid_qty: String,
454 pub ask_price: String,
456 pub ask_qty: String,
458 #[serde(default)]
460 pub time: Option<i64>,
461}
462
463#[derive(Clone, Debug, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct BinancePriceTicker {
467 pub symbol: Ustr,
469 pub price: String,
471 #[serde(default)]
473 pub time: Option<i64>,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct BinanceFundingRate {
480 pub symbol: Ustr,
482 pub funding_rate: String,
484 pub funding_time: i64,
486 #[serde(default)]
488 pub mark_price: Option<String>,
489 #[serde(default)]
491 pub index_price: Option<String>,
492}
493
494#[derive(Clone, Debug, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct BinanceOpenInterest {
498 pub symbol: Ustr,
500 pub open_interest: String,
502 pub time: i64,
504}
505
506#[derive(Clone, Debug, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct BinanceOpenInterestHistRecord {
510 #[serde(default)]
512 pub symbol: Option<Ustr>,
513 #[serde(default)]
515 pub pair: Option<Ustr>,
516 #[serde(default)]
518 pub contract_type: Option<String>,
519 pub sum_open_interest: String,
521 pub sum_open_interest_value: String,
523 pub timestamp: i64,
525 #[serde(default, rename = "CMCCirculatingSupply")]
527 pub cmc_circulating_supply: Option<String>,
528}
529
530#[derive(Clone, Debug, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub struct BinanceFuturesBalance {
534 #[serde(default)]
536 pub account_alias: Option<String>,
537 pub asset: Ustr,
539 #[serde(
541 alias = "balance",
542 deserialize_with = "deserialize_decimal_or_zero",
543 serialize_with = "serialize_decimal_as_str"
544 )]
545 pub wallet_balance: Decimal,
546 #[serde(
548 default,
549 deserialize_with = "deserialize_optional_decimal_from_str",
550 serialize_with = "serialize_optional_decimal_as_str"
551 )]
552 pub unrealized_profit: Option<Decimal>,
553 #[serde(
555 default,
556 deserialize_with = "deserialize_optional_decimal_from_str",
557 serialize_with = "serialize_optional_decimal_as_str"
558 )]
559 pub margin_balance: Option<Decimal>,
560 #[serde(
562 default,
563 deserialize_with = "deserialize_optional_decimal_from_str",
564 serialize_with = "serialize_optional_decimal_as_str"
565 )]
566 pub maint_margin: Option<Decimal>,
567 #[serde(
569 default,
570 deserialize_with = "deserialize_optional_decimal_from_str",
571 serialize_with = "serialize_optional_decimal_as_str"
572 )]
573 pub initial_margin: Option<Decimal>,
574 #[serde(
576 default,
577 deserialize_with = "deserialize_optional_decimal_from_str",
578 serialize_with = "serialize_optional_decimal_as_str"
579 )]
580 pub position_initial_margin: Option<Decimal>,
581 #[serde(
583 default,
584 deserialize_with = "deserialize_optional_decimal_from_str",
585 serialize_with = "serialize_optional_decimal_as_str"
586 )]
587 pub open_order_initial_margin: Option<Decimal>,
588 #[serde(
590 default,
591 deserialize_with = "deserialize_optional_decimal_from_str",
592 serialize_with = "serialize_optional_decimal_as_str"
593 )]
594 pub cross_wallet_balance: Option<Decimal>,
595 #[serde(
597 default,
598 deserialize_with = "deserialize_optional_decimal_from_str",
599 serialize_with = "serialize_optional_decimal_as_str"
600 )]
601 pub cross_un_pnl: Option<Decimal>,
602 #[serde(
604 deserialize_with = "deserialize_decimal_or_zero",
605 serialize_with = "serialize_decimal_as_str"
606 )]
607 pub available_balance: Decimal,
608 #[serde(
610 default,
611 deserialize_with = "deserialize_optional_decimal_from_str",
612 serialize_with = "serialize_optional_decimal_as_str"
613 )]
614 pub max_withdraw_amount: Option<Decimal>,
615 #[serde(default)]
617 pub margin_available: Option<bool>,
618 pub update_time: i64,
620 #[serde(
622 default,
623 deserialize_with = "deserialize_optional_decimal_from_str",
624 serialize_with = "serialize_optional_decimal_as_str"
625 )]
626 pub withdraw_available: Option<Decimal>,
627}
628
629#[derive(Clone, Debug, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct BinanceAccountPosition {
633 pub symbol: Ustr,
635 #[serde(default)]
637 pub initial_margin: Option<String>,
638 #[serde(default)]
640 pub maint_margin: Option<String>,
641 #[serde(default)]
643 pub unrealized_profit: Option<String>,
644 #[serde(default)]
646 pub position_initial_margin: Option<String>,
647 #[serde(default)]
649 pub open_order_initial_margin: Option<String>,
650 #[serde(default)]
652 pub leverage: Option<String>,
653 #[serde(default)]
655 pub isolated: Option<bool>,
656 #[serde(default)]
658 pub entry_price: Option<String>,
659 #[serde(default)]
661 pub max_notional: Option<String>,
662 #[serde(default)]
664 pub bid_notional: Option<String>,
665 #[serde(default)]
667 pub ask_notional: Option<String>,
668 #[serde(default)]
670 pub position_side: Option<BinancePositionSide>,
671 #[serde(default)]
673 pub position_amt: Option<String>,
674 #[serde(default)]
676 pub update_time: Option<i64>,
677}
678
679#[derive(Clone, Debug, Serialize, Deserialize)]
681#[serde(rename_all = "camelCase")]
682pub struct BinancePositionRisk {
683 pub symbol: Ustr,
685 pub position_amt: String,
687 pub entry_price: String,
689 pub mark_price: String,
691 #[serde(default)]
693 pub un_realized_profit: Option<String>,
694 #[serde(default)]
696 pub liquidation_price: Option<String>,
697 pub leverage: String,
699 #[serde(default)]
701 pub max_notional_value: Option<String>,
702 #[serde(default)]
704 pub margin_type: Option<BinanceMarginType>,
705 #[serde(default)]
707 pub isolated_margin: Option<String>,
708 #[serde(default)]
710 pub is_auto_add_margin: Option<String>,
711 #[serde(default)]
713 pub position_side: Option<BinancePositionSide>,
714 #[serde(default)]
716 pub notional: Option<String>,
717 #[serde(default)]
719 pub isolated_wallet: Option<String>,
720 #[serde(default)]
722 pub adl_quantile: Option<u8>,
723 #[serde(default)]
725 pub update_time: Option<i64>,
726 #[serde(default)]
728 pub break_even_price: Option<String>,
729 #[serde(default)]
731 pub bust_price: Option<String>,
732}
733
734#[derive(Clone, Debug, Serialize, Deserialize)]
736#[serde(rename_all = "camelCase")]
737pub struct BinanceIncomeRecord {
738 #[serde(default)]
740 pub symbol: Option<Ustr>,
741 pub income_type: BinanceIncomeType,
743 pub income: String,
745 pub asset: Ustr,
747 pub time: i64,
749 #[serde(default)]
751 pub info: Option<String>,
752 #[serde(default)]
754 pub tran_id: Option<i64>,
755 #[serde(default)]
757 pub trade_id: Option<i64>,
758}
759
760#[derive(Clone, Debug, Serialize, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct BinanceUserTrade {
764 pub symbol: Ustr,
766 pub id: i64,
768 pub order_id: i64,
770 pub price: String,
772 pub qty: String,
774 #[serde(default)]
776 pub quote_qty: Option<String>,
777 pub realized_pnl: String,
779 pub side: BinanceSide,
781 #[serde(default)]
783 pub position_side: Option<BinancePositionSide>,
784 pub time: i64,
786 pub buyer: bool,
788 pub maker: bool,
790 #[serde(default)]
792 pub commission: Option<String>,
793 #[serde(default)]
795 pub commission_asset: Option<Ustr>,
796 #[serde(default)]
798 pub margin_asset: Option<Ustr>,
799}
800
801#[derive(Clone, Debug, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804pub struct BinanceFuturesAccountInfo {
805 #[serde(default)]
807 pub fee_tier: u8,
808 #[serde(
810 default,
811 deserialize_with = "deserialize_optional_decimal_from_str",
812 serialize_with = "serialize_optional_decimal_as_str"
813 )]
814 pub total_initial_margin: Option<Decimal>,
815 #[serde(
817 default,
818 deserialize_with = "deserialize_optional_decimal_from_str",
819 serialize_with = "serialize_optional_decimal_as_str"
820 )]
821 pub total_maint_margin: Option<Decimal>,
822 #[serde(
824 default,
825 deserialize_with = "deserialize_optional_decimal_from_str",
826 serialize_with = "serialize_optional_decimal_as_str"
827 )]
828 pub total_wallet_balance: Option<Decimal>,
829 #[serde(
831 default,
832 deserialize_with = "deserialize_optional_decimal_from_str",
833 serialize_with = "serialize_optional_decimal_as_str"
834 )]
835 pub total_unrealized_profit: Option<Decimal>,
836 #[serde(
838 default,
839 deserialize_with = "deserialize_optional_decimal_from_str",
840 serialize_with = "serialize_optional_decimal_as_str"
841 )]
842 pub total_margin_balance: Option<Decimal>,
843 #[serde(
845 default,
846 deserialize_with = "deserialize_optional_decimal_from_str",
847 serialize_with = "serialize_optional_decimal_as_str"
848 )]
849 pub total_position_initial_margin: Option<Decimal>,
850 #[serde(
852 default,
853 deserialize_with = "deserialize_optional_decimal_from_str",
854 serialize_with = "serialize_optional_decimal_as_str"
855 )]
856 pub total_open_order_initial_margin: Option<Decimal>,
857 #[serde(
859 default,
860 deserialize_with = "deserialize_optional_decimal_from_str",
861 serialize_with = "serialize_optional_decimal_as_str"
862 )]
863 pub total_cross_wallet_balance: Option<Decimal>,
864 #[serde(
866 default,
867 deserialize_with = "deserialize_optional_decimal_from_str",
868 serialize_with = "serialize_optional_decimal_as_str"
869 )]
870 pub total_cross_un_pnl: Option<Decimal>,
871 #[serde(
873 default,
874 deserialize_with = "deserialize_optional_decimal_from_str",
875 serialize_with = "serialize_optional_decimal_as_str"
876 )]
877 pub available_balance: Option<Decimal>,
878 #[serde(
880 default,
881 deserialize_with = "deserialize_optional_decimal_from_str",
882 serialize_with = "serialize_optional_decimal_as_str"
883 )]
884 pub max_withdraw_amount: Option<Decimal>,
885 #[serde(default)]
887 pub can_deposit: Option<bool>,
888 #[serde(default)]
890 pub can_trade: Option<bool>,
891 #[serde(default)]
893 pub can_withdraw: Option<bool>,
894 #[serde(default)]
896 pub multi_assets_margin: Option<bool>,
897 #[serde(default)]
899 pub update_time: Option<i64>,
900 #[serde(default)]
902 pub assets: Vec<BinanceFuturesBalance>,
903 #[serde(default)]
905 pub positions: Vec<BinanceAccountPosition>,
906}
907
908#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct BinanceFuturesCommissionRate {
912 pub symbol: Ustr,
914 pub maker_commission_rate: String,
916 pub taker_commission_rate: String,
918}
919
920impl BinanceFuturesAccountInfo {
921 pub fn to_account_state(
927 &self,
928 account_id: AccountId,
929 ts_init: UnixNanos,
930 ) -> anyhow::Result<AccountState> {
931 let mut balances = Vec::with_capacity(self.assets.len());
932
933 for asset in &self.assets {
934 let currency = Currency::get_or_create_crypto_with_context(
935 asset.asset.as_str(),
936 Some("futures balance"),
937 );
938
939 let balance = AccountBalance::from_total_and_free(
940 asset.wallet_balance,
941 asset.available_balance,
942 currency,
943 )
944 .context("failed to build account balance")?;
945 balances.push(balance);
946 }
947
948 if balances.is_empty() {
950 let zero_currency = Currency::USDT();
951 let zero_money = Money::zero(zero_currency);
952 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
953 balances.push(zero_balance);
954 }
955
956 let mut margins = Vec::new();
961
962 for asset in &self.assets {
963 let initial_dec = asset.initial_margin.unwrap_or_default();
964 let maint_dec = asset.maint_margin.unwrap_or_default();
965
966 if initial_dec.is_zero() && maint_dec.is_zero() {
967 continue;
968 }
969
970 let currency = Currency::get_or_create_crypto_with_context(
971 asset.asset.as_str(),
972 Some("futures margin"),
973 );
974 let initial = Money::from_decimal(initial_dec, currency)
975 .unwrap_or_else(|_| Money::zero(currency));
976 let maintenance =
977 Money::from_decimal(maint_dec, currency).unwrap_or_else(|_| Money::zero(currency));
978 margins.push(MarginBalance::new(initial, maintenance, None));
979 }
980
981 let ts_event = self
982 .update_time
983 .map(|value| parse_millis(value, "Futures account update time"))
984 .transpose()?
985 .unwrap_or(ts_init);
986
987 Ok(AccountState::new(
988 account_id,
989 AccountType::Margin,
990 balances,
991 margins,
992 true, UUID4::new(),
994 ts_event,
995 ts_init,
996 None,
997 ))
998 }
999}
1000
1001#[derive(Clone, Debug, Serialize, Deserialize)]
1003#[serde(rename_all = "camelCase")]
1004pub struct BinanceHedgeModeResponse {
1005 pub dual_side_position: bool,
1007}
1008
1009#[derive(Clone, Debug, Serialize, Deserialize)]
1011#[serde(rename_all = "camelCase")]
1012pub struct BinanceLeverageResponse {
1013 pub symbol: Ustr,
1015 pub leverage: u32,
1017 #[serde(default)]
1019 pub max_notional_value: Option<String>,
1020}
1021
1022#[derive(Clone, Debug, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct BinanceCancelAllOrdersResponse {
1026 pub code: i32,
1028 pub msg: String,
1030}
1031
1032#[derive(Clone, Debug, Serialize, Deserialize)]
1034#[serde(rename_all = "camelCase")]
1035pub struct BinanceFuturesOrder {
1036 pub symbol: Ustr,
1038 pub order_id: i64,
1040 pub client_order_id: String,
1042 pub orig_qty: String,
1044 pub executed_qty: String,
1046 #[serde(default = "zero_decimal_string")]
1048 pub cum_quote: String,
1049 pub price: String,
1051 #[serde(default)]
1053 pub avg_price: Option<String>,
1054 #[serde(default)]
1056 pub stop_price: Option<String>,
1057 pub status: BinanceOrderStatus,
1059 pub time_in_force: BinanceTimeInForce,
1061 #[serde(rename = "type")]
1063 pub order_type: BinanceFuturesOrderType,
1064 #[serde(default)]
1066 pub orig_type: Option<BinanceFuturesOrderType>,
1067 pub side: BinanceSide,
1069 #[serde(default)]
1071 pub position_side: Option<BinancePositionSide>,
1072 #[serde(default)]
1074 pub reduce_only: Option<bool>,
1075 #[serde(default)]
1077 pub close_position: Option<bool>,
1078 #[serde(default)]
1080 pub activate_price: Option<String>,
1081 #[serde(default)]
1083 pub price_rate: Option<String>,
1084 #[serde(default)]
1086 pub working_type: Option<BinanceWorkingType>,
1087 #[serde(default)]
1089 pub price_protect: Option<bool>,
1090 #[serde(default)]
1092 pub is_isolated: Option<bool>,
1093 #[serde(default)]
1095 pub good_till_date: Option<i64>,
1096 #[serde(default)]
1098 pub price_match: Option<BinancePriceMatch>,
1099 #[serde(default)]
1101 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
1102 #[serde(default)]
1104 pub update_time: Option<i64>,
1105 #[serde(default)]
1107 pub working_type_id: Option<i64>,
1108}
1109
1110fn zero_decimal_string() -> String {
1111 "0".to_string()
1112}
1113
1114impl BinanceFuturesOrder {
1115 pub fn to_order_status_report(
1121 &self,
1122 account_id: AccountId,
1123 instrument_id: InstrumentId,
1124 price_precision: u8,
1125 size_precision: u8,
1126 treat_expired_as_canceled: bool,
1127 ts_init: UnixNanos,
1128 ) -> anyhow::Result<OrderStatusReport> {
1129 let ts_event = self
1130 .update_time
1131 .map(|value| parse_millis(value, "Futures order update time"))
1132 .transpose()?
1133 .unwrap_or(ts_init);
1134
1135 let client_order_id =
1136 decode_client_order_id(&self.client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1137 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1138
1139 let order_side = match self.side {
1140 BinanceSide::Buy => OrderSide::Buy,
1141 BinanceSide::Sell => OrderSide::Sell,
1142 };
1143
1144 let order_type = self.order_type.to_nautilus_order_type();
1145 let time_in_force = self.time_in_force.to_nautilus_time_in_force();
1146 let order_status = self
1147 .status
1148 .to_nautilus_order_status(treat_expired_as_canceled);
1149
1150 let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
1151 let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
1152 let price = if self.price.is_empty() {
1153 None
1154 } else {
1155 let price: Decimal = self.price.parse().context("invalid price")?;
1156 if price == Decimal::ZERO {
1157 None
1158 } else {
1159 Some(
1160 Price::from_decimal_dp(price, price_precision)
1161 .context("invalid price precision")?,
1162 )
1163 }
1164 };
1165 let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1166
1167 let mut report = OrderStatusReport::new(
1168 account_id,
1169 instrument_id,
1170 Some(client_order_id),
1171 venue_order_id,
1172 order_side.into(),
1173 order_type,
1174 time_in_force,
1175 order_status,
1176 Quantity::from_decimal_dp(quantity, size_precision)
1177 .context("invalid orig_qty precision")?,
1178 Quantity::from_decimal_dp(filled_qty, size_precision)
1179 .context("invalid executed_qty precision")?,
1180 ts_event,
1181 ts_event,
1182 ts_init,
1183 Some(UUID4::new()),
1184 );
1185
1186 if let Some(price) = price {
1187 report = report.with_price(price);
1188 }
1189
1190 if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1191 report = report.with_expire_time(expire_time);
1192 }
1193
1194 report.avg_px = avg_px;
1195
1196 Ok(report)
1197 }
1198}
1199
1200impl BinanceFuturesOrderType {
1201 #[must_use]
1203 pub fn is_post_only(&self) -> bool {
1204 false }
1206
1207 #[must_use]
1209 pub fn to_nautilus_order_type(&self) -> OrderType {
1210 match self {
1211 Self::Market => OrderType::Market,
1212 Self::Limit => OrderType::Limit,
1213 Self::Stop => OrderType::StopLimit,
1214 Self::StopMarket => OrderType::StopMarket,
1215 Self::TakeProfit => OrderType::LimitIfTouched,
1216 Self::TakeProfitMarket => OrderType::MarketIfTouched,
1217 Self::TrailingStopMarket => OrderType::TrailingStopMarket,
1218 Self::Liquidation | Self::Adl => OrderType::Market, Self::Unknown => OrderType::Market,
1220 }
1221 }
1222}
1223
1224impl BinanceTimeInForce {
1225 #[must_use]
1227 pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1228 match self {
1229 Self::Gtc => TimeInForce::Gtc,
1230 Self::Ioc => TimeInForce::Ioc,
1231 Self::Fok => TimeInForce::Fok,
1232 Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
1234 Self::Rpi => TimeInForce::Ioc, Self::Unknown => TimeInForce::Gtc, }
1237 }
1238}
1239
1240impl BinanceOrderStatus {
1241 #[must_use]
1243 pub fn to_nautilus_order_status(&self, treat_expired_as_canceled: bool) -> OrderStatus {
1244 match self {
1245 Self::New | Self::PendingNew => OrderStatus::Accepted,
1246 Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1247 Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
1248 Self::Canceled => OrderStatus::Canceled,
1249 Self::PendingCancel => OrderStatus::PendingCancel,
1250 Self::Rejected => OrderStatus::Rejected,
1251 Self::Expired | Self::ExpiredInMatch => {
1252 if treat_expired_as_canceled {
1253 OrderStatus::Canceled
1254 } else {
1255 OrderStatus::Expired
1256 }
1257 }
1258 Self::Unknown => OrderStatus::Initialized,
1259 }
1260 }
1261}
1262
1263impl BinanceUserTrade {
1264 pub fn to_fill_report(
1270 &self,
1271 account_id: AccountId,
1272 instrument_id: InstrumentId,
1273 price_precision: u8,
1274 size_precision: u8,
1275 bnfcr_currency: Currency,
1276 ts_init: UnixNanos,
1277 ) -> anyhow::Result<FillReport> {
1278 let ts_event = parse_millis(self.time, "Futures user trade time")?;
1279
1280 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1281 let trade_id = TradeId::new(self.id.to_string());
1282
1283 let order_side = match self.side {
1284 BinanceSide::Buy => OrderSide::Buy,
1285 BinanceSide::Sell => OrderSide::Sell,
1286 };
1287
1288 let liquidity_side = if self.maker {
1289 LiquiditySide::Maker
1290 } else {
1291 LiquiditySide::Taker
1292 };
1293
1294 let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1295 let last_px: Decimal = self.price.parse().context("invalid price")?;
1296
1297 let commission_currency = self
1298 .commission_asset
1299 .as_ref()
1300 .map_or(bnfcr_currency, |asset| {
1301 normalize_futures_asset(asset, bnfcr_currency)
1302 });
1303 let commission = match self.commission.as_ref() {
1304 Some(raw) => {
1305 let decimal = parse_required_decimal(raw, "commission")?;
1306 Money::from_decimal(decimal, commission_currency)?
1307 }
1308 None => Money::zero(commission_currency),
1309 };
1310
1311 Ok(FillReport::new(
1312 account_id,
1313 instrument_id,
1314 venue_order_id,
1315 trade_id,
1316 order_side,
1317 Quantity::from_decimal_dp(last_qty, size_precision).context("invalid qty precision")?,
1318 Price::from_decimal_dp(last_px, price_precision).context("invalid price precision")?,
1319 commission,
1320 liquidity_side,
1321 None, None, ts_event,
1324 ts_init,
1325 Some(UUID4::new()),
1326 ))
1327 }
1328}
1329
1330#[derive(Clone, Debug, Deserialize)]
1334#[serde(untagged)]
1335pub enum BatchOrderResult {
1336 Success(Box<BinanceFuturesOrder>),
1338 Error(BatchOrderError),
1340}
1341
1342#[derive(Clone, Debug, Deserialize)]
1344pub struct BatchOrderError {
1345 pub code: i64,
1347 pub msg: String,
1349}
1350
1351#[derive(Debug, Clone, Deserialize)]
1353#[serde(rename_all = "camelCase")]
1354pub struct ListenKeyResponse {
1355 pub listen_key: String,
1357}
1358
1359#[derive(Clone, Debug, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct BinanceFuturesAlgoOrder {
1371 pub algo_id: i64,
1373 pub client_algo_id: String,
1375 pub algo_type: BinanceAlgoType,
1377 #[serde(rename = "orderType", alias = "type")]
1379 pub order_type: BinanceFuturesOrderType,
1380 pub symbol: Ustr,
1382 pub side: BinanceSide,
1384 #[serde(default)]
1386 pub position_side: Option<BinancePositionSide>,
1387 #[serde(default)]
1389 pub time_in_force: Option<BinanceTimeInForce>,
1390 #[serde(default)]
1392 pub quantity: Option<String>,
1393 #[serde(default)]
1395 pub algo_status: Option<BinanceAlgoStatus>,
1396 #[serde(default)]
1398 pub trigger_price: Option<String>,
1399 #[serde(default)]
1401 pub price: Option<String>,
1402 #[serde(default)]
1404 pub working_type: Option<BinanceWorkingType>,
1405 #[serde(default)]
1407 pub close_position: Option<bool>,
1408 #[serde(default)]
1410 pub price_protect: Option<bool>,
1411 #[serde(default)]
1413 pub reduce_only: Option<bool>,
1414 #[serde(default)]
1416 pub activate_price: Option<String>,
1417 #[serde(default)]
1419 pub callback_rate: Option<String>,
1420 #[serde(default)]
1422 pub good_till_date: Option<i64>,
1423 #[serde(default)]
1425 pub create_time: Option<i64>,
1426 #[serde(default)]
1428 pub update_time: Option<i64>,
1429 #[serde(default)]
1431 pub trigger_time: Option<i64>,
1432 #[serde(default)]
1434 pub actual_order_id: Option<String>,
1435 #[serde(default, rename = "actualQty", alias = "executedQty")]
1437 pub executed_qty: Option<String>,
1438 #[serde(default, rename = "actualPrice", alias = "avgPrice")]
1440 pub avg_price: Option<String>,
1441}
1442
1443impl BinanceFuturesAlgoOrder {
1444 pub fn to_order_status_report(
1451 &self,
1452 account_id: AccountId,
1453 instrument_id: InstrumentId,
1454 price_precision: u8,
1455 size_precision: u8,
1456 ts_init: UnixNanos,
1457 ) -> anyhow::Result<OrderStatusReport> {
1458 let ts_event = self
1459 .update_time
1460 .or(self.create_time)
1461 .map(|value| parse_millis(value, "Futures algo order time"))
1462 .transpose()?
1463 .unwrap_or(ts_init);
1464
1465 let client_order_id =
1466 decode_client_order_id(&self.client_algo_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)?;
1467 let venue_order_id = self
1468 .actual_order_id
1469 .as_ref()
1470 .filter(|id| !id.is_empty())
1471 .map_or_else(
1472 || VenueOrderId::new(self.algo_id.to_string()),
1473 |id| VenueOrderId::new(id.clone()),
1474 );
1475
1476 let order_side = match self.side {
1477 BinanceSide::Buy => OrderSide::Buy,
1478 BinanceSide::Sell => OrderSide::Sell,
1479 };
1480
1481 let order_type = self.parse_order_type();
1482 let time_in_force = self
1483 .time_in_force
1484 .as_ref()
1485 .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1486 let order_status = self.parse_order_status();
1487
1488 let quantity: Decimal = self
1489 .quantity
1490 .as_ref()
1491 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1492 .context("invalid quantity")?;
1493 let filled_qty: Decimal = self
1494 .executed_qty
1495 .as_ref()
1496 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1497 .context("invalid executed_qty")?;
1498 let price = if let Some(price) = self.price.as_ref().filter(|price| !price.is_empty()) {
1499 let price: Decimal = price.parse().context("invalid price")?;
1500 if price == Decimal::ZERO {
1501 None
1502 } else {
1503 Some(
1504 Price::from_decimal_dp(price, price_precision)
1505 .context("invalid price precision")?,
1506 )
1507 }
1508 } else {
1509 None
1510 };
1511 let avg_px = parse_avg_px(self.avg_price.as_deref(), filled_qty, price_precision)?;
1512 let trigger_price = self.parse_trigger_price(price_precision)?;
1513 let trailing_offset = self.parse_trailing_offset()?;
1514
1515 let mut report = OrderStatusReport::new(
1516 account_id,
1517 instrument_id,
1518 Some(client_order_id),
1519 venue_order_id,
1520 order_side.into(),
1521 order_type,
1522 time_in_force,
1523 order_status,
1524 Quantity::from_decimal_dp(quantity, size_precision)
1525 .context("invalid quantity precision")?,
1526 Quantity::from_decimal_dp(filled_qty, size_precision)
1527 .context("invalid executed_qty precision")?,
1528 ts_event,
1529 ts_event,
1530 ts_init,
1531 Some(UUID4::new()),
1532 );
1533
1534 if let Some(price) = price {
1535 report = report.with_price(price);
1536 }
1537
1538 report.avg_px = avg_px;
1539
1540 if let Some(trigger_price) = trigger_price {
1541 report = report
1542 .with_trigger_price(trigger_price)
1543 .with_trigger_type(parse_working_type(self.working_type));
1544 }
1545
1546 if let Some(trailing_offset) = trailing_offset {
1547 report = report
1548 .with_trailing_offset(trailing_offset)
1549 .with_trailing_offset_type(TrailingOffsetType::BasisPoints);
1550 }
1551
1552 if let Some(activation_price) = self
1553 .activate_price
1554 .as_deref()
1555 .map(|price| {
1556 parse_positive_price_at_precision(price, price_precision, "activate_price")
1557 })
1558 .transpose()?
1559 .flatten()
1560 {
1561 report = report.with_activation_price(activation_price);
1562 }
1563
1564 if self.reduce_only == Some(true) || self.close_position == Some(true) {
1565 report = report.with_reduce_only(true);
1566 }
1567
1568 if let Some(expire_time) = parse_good_till_date(self.good_till_date)? {
1569 report = report.with_expire_time(expire_time);
1570 }
1571
1572 if let Some(trigger_time) = self.trigger_time {
1573 report =
1574 report.with_ts_triggered(parse_millis(trigger_time, "Futures algo trigger time")?);
1575 }
1576
1577 Ok(report)
1578 }
1579
1580 #[expect(clippy::too_many_arguments)]
1591 pub fn to_order_status_report_with_actual(
1592 &self,
1593 actual: &BinanceFuturesOrder,
1594 account_id: AccountId,
1595 instrument_id: InstrumentId,
1596 price_precision: u8,
1597 size_precision: u8,
1598 treat_expired_as_canceled: bool,
1599 ts_init: UnixNanos,
1600 ) -> anyhow::Result<OrderStatusReport> {
1601 let expected_actual_order_id = self
1602 .actual_order_id
1603 .as_deref()
1604 .filter(|id| !id.is_empty())
1605 .context("algo order has no actual_order_id")?;
1606
1607 if expected_actual_order_id != actual.order_id.to_string() {
1608 anyhow::bail!(
1609 "actual order ID mismatch: expected {expected_actual_order_id}, was {}",
1610 actual.order_id
1611 );
1612 }
1613
1614 if self.symbol != actual.symbol {
1615 anyhow::bail!(
1616 "actual order symbol mismatch: expected {}, was {}",
1617 self.symbol,
1618 actual.symbol
1619 );
1620 }
1621
1622 if self.side != actual.side {
1623 anyhow::bail!(
1624 "actual order side mismatch: expected {:?}, was {:?}",
1625 self.side,
1626 actual.side
1627 );
1628 }
1629
1630 let mut report = self.to_order_status_report(
1631 account_id,
1632 instrument_id,
1633 price_precision,
1634 size_precision,
1635 ts_init,
1636 )?;
1637 let actual_report = actual.to_order_status_report(
1638 account_id,
1639 instrument_id,
1640 price_precision,
1641 size_precision,
1642 treat_expired_as_canceled,
1643 ts_init,
1644 )?;
1645 report.venue_order_id = actual_report.venue_order_id;
1646 report.order_status = actual_report.order_status;
1647 report.quantity = actual_report.quantity;
1648 report.filled_qty = actual_report.filled_qty;
1649 report.avg_px = actual_report.avg_px.or(report.avg_px);
1650 report.expire_time = report.expire_time.or(actual_report.expire_time);
1651 report.ts_last = actual_report.ts_last;
1652
1653 Ok(report)
1654 }
1655
1656 fn parse_trigger_price(&self, price_precision: u8) -> anyhow::Result<Option<Price>> {
1657 let raw_trigger_price = match self.order_type {
1658 BinanceFuturesOrderType::TrailingStopMarket => self
1659 .trigger_price
1660 .as_deref()
1661 .or(self.activate_price.as_deref()),
1662 _ => self.trigger_price.as_deref(),
1663 };
1664 let trigger_price = raw_trigger_price
1665 .map(|price| parse_positive_price_at_precision(price, price_precision, "trigger_price"))
1666 .transpose()?
1667 .flatten();
1668
1669 if trigger_price.is_none() && requires_algo_trigger_price(self.order_type) {
1670 anyhow::bail!(
1671 "missing positive trigger_price for Binance algo order type {:?}",
1672 self.order_type
1673 );
1674 }
1675
1676 Ok(trigger_price)
1677 }
1678
1679 fn parse_trailing_offset(&self) -> anyhow::Result<Option<Decimal>> {
1680 if self.order_type != BinanceFuturesOrderType::TrailingStopMarket {
1681 return Ok(None);
1682 }
1683
1684 self.callback_rate
1685 .as_deref()
1686 .map(parse_callback_rate_basis_points)
1687 .transpose()
1688 .map(Option::flatten)
1689 }
1690
1691 fn parse_order_type(&self) -> OrderType {
1692 self.order_type.into()
1693 }
1694
1695 fn parse_order_status(&self) -> OrderStatus {
1696 match self.algo_status {
1697 Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1698 Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1699 Some(BinanceAlgoStatus::Triggered) => self
1700 .executed_qty
1701 .as_deref()
1702 .and_then(|qty| qty.parse::<Decimal>().ok())
1703 .filter(|qty| *qty > Decimal::ZERO)
1704 .map_or(OrderStatus::Accepted, |_| OrderStatus::PartiallyFilled),
1705 Some(BinanceAlgoStatus::Finished) => {
1706 let executed_qty = self
1707 .executed_qty
1708 .as_deref()
1709 .and_then(|qty| qty.parse::<Decimal>().ok());
1710 let quantity = self
1711 .quantity
1712 .as_deref()
1713 .and_then(|qty| qty.parse::<Decimal>().ok());
1714 match (executed_qty, quantity) {
1715 (Some(actual), Some(total)) if total > Decimal::ZERO && actual >= total => {
1716 OrderStatus::Filled
1717 }
1718 _ => OrderStatus::Canceled,
1719 }
1720 }
1721 Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1722 Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1723 Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1724 Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1725 }
1726 }
1727}
1728
1729fn parse_avg_px(
1730 raw: Option<&str>,
1731 filled_qty: Decimal,
1732 price_precision: u8,
1733) -> anyhow::Result<Option<Decimal>> {
1734 if filled_qty <= Decimal::ZERO {
1735 return Ok(None);
1736 }
1737
1738 raw.filter(|price| !price.is_empty())
1739 .map(|price| parse_positive_price_at_precision(price, price_precision, "avg_price"))
1740 .transpose()
1741 .map(|price| price.flatten().map(|price| price.as_decimal()))
1742}
1743
1744fn parse_positive_price_at_precision(
1745 raw: &str,
1746 precision: u8,
1747 field: &str,
1748) -> anyhow::Result<Option<Price>> {
1749 let decimal = parse_required_decimal(raw, field)?;
1750 if decimal <= Decimal::ZERO {
1751 return Ok(None);
1752 }
1753
1754 Price::from_decimal_dp(decimal, precision)
1755 .map(Some)
1756 .map_err(|e| anyhow::anyhow!("invalid {field} precision: {e}"))
1757}
1758
1759fn parse_callback_rate_basis_points(raw: &str) -> anyhow::Result<Option<Decimal>> {
1760 let rate = parse_required_decimal(raw, "callback_rate")?;
1761 if rate <= Decimal::ZERO {
1762 return Ok(None);
1763 }
1764
1765 rate.checked_mul(Decimal::from(100))
1766 .map(Some)
1767 .ok_or_else(|| anyhow::anyhow!("invalid callback_rate='{raw}': multiplication overflow"))
1768}
1769
1770fn parse_working_type(working_type: Option<BinanceWorkingType>) -> TriggerType {
1771 match working_type {
1772 Some(BinanceWorkingType::ContractPrice) => TriggerType::LastPrice,
1773 Some(BinanceWorkingType::MarkPrice) => TriggerType::MarkPrice,
1774 Some(BinanceWorkingType::Unknown) | None => TriggerType::Default,
1775 }
1776}
1777
1778fn requires_algo_trigger_price(order_type: BinanceFuturesOrderType) -> bool {
1779 matches!(
1780 order_type,
1781 BinanceFuturesOrderType::Stop
1782 | BinanceFuturesOrderType::StopMarket
1783 | BinanceFuturesOrderType::TakeProfit
1784 | BinanceFuturesOrderType::TakeProfitMarket
1785 | BinanceFuturesOrderType::TrailingStopMarket
1786 )
1787}
1788
1789#[derive(Clone, Debug, Deserialize)]
1791#[serde(rename_all = "camelCase")]
1792pub struct BinanceFuturesAlgoOrderCancelResponse {
1793 pub algo_id: i64,
1795 pub client_algo_id: String,
1797 pub code: String,
1799 pub msg: String,
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805 use nautilus_model::identifiers::ClientOrderId;
1806 use rstest::rstest;
1807 use rust_decimal_macros::dec;
1808
1809 use super::*;
1810 use crate::common::testing::load_fixture_string;
1811
1812 #[rstest]
1813 fn test_parse_account_info_v2() {
1814 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1815 let account: BinanceFuturesAccountInfo =
1816 serde_json::from_str(&json).expect("Failed to parse account info");
1817
1818 assert_eq!(
1819 account.total_wallet_balance,
1820 Some(Decimal::from_str_exact("23.72469206").unwrap())
1821 );
1822 assert_eq!(account.assets.len(), 1);
1823 assert_eq!(account.assets[0].asset.as_str(), "USDT");
1824 assert_eq!(
1825 account.assets[0].wallet_balance,
1826 Decimal::from_str_exact("23.72469206").unwrap()
1827 );
1828 assert_eq!(account.positions.len(), 1);
1829 assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1830 assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1831 }
1832
1833 #[rstest]
1834 fn test_account_info_to_account_state_zero_margins() {
1835 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1836 let account: BinanceFuturesAccountInfo =
1837 serde_json::from_str(&json).expect("Failed to parse account info");
1838
1839 let account_id = AccountId::from("BINANCE-001");
1840 let ts_init = UnixNanos::from(1_000_000_000u64);
1841 let state = account.to_account_state(account_id, ts_init).unwrap();
1842
1843 assert_eq!(state.account_id, account_id);
1844 assert_eq!(state.account_type, AccountType::Margin);
1845 assert!(!state.balances.is_empty());
1846 assert_eq!(state.margins.len(), 0);
1847 }
1848
1849 #[rstest]
1850 fn test_account_info_to_account_state_with_margins() {
1851 let json = r#"{
1852 "totalInitialMargin": "500.25000000",
1853 "totalMaintMargin": "250.75000000",
1854 "totalWalletBalance": "10000.00000000",
1855 "assets": [{
1856 "asset": "USDT",
1857 "walletBalance": "10000.00000000",
1858 "availableBalance": "9500.00000000",
1859 "initialMargin": "500.25000000",
1860 "maintMargin": "250.75000000",
1861 "updateTime": 1617939110373
1862 }],
1863 "positions": []
1864 }"#;
1865 let account: BinanceFuturesAccountInfo =
1866 serde_json::from_str(json).expect("Failed to parse account info");
1867
1868 let account_id = AccountId::from("BINANCE-001");
1869 let ts_init = UnixNanos::from(1_000_000_000u64);
1870 let state = account.to_account_state(account_id, ts_init).unwrap();
1871
1872 assert_eq!(state.margins.len(), 1);
1873 let margin = &state.margins[0];
1874 assert!(margin.instrument_id.is_none());
1875 assert_eq!(margin.currency.code.as_str(), "USDT");
1876 assert_eq!(margin.initial.as_f64(), 500.25);
1877 assert_eq!(margin.maintenance.as_f64(), 250.75);
1878 }
1879
1880 #[rstest]
1881 fn test_account_info_to_account_state_coin_margined_per_base_coin() {
1882 let json = r#"{
1883 "totalWalletBalance": "0.00000000",
1884 "assets": [
1885 {
1886 "asset": "BTC",
1887 "walletBalance": "1.50000000",
1888 "availableBalance": "1.40000000",
1889 "initialMargin": "0.05000000",
1890 "maintMargin": "0.02500000",
1891 "updateTime": 1617939110373
1892 },
1893 {
1894 "asset": "ETH",
1895 "walletBalance": "10.00000000",
1896 "availableBalance": "9.00000000",
1897 "initialMargin": "0.80000000",
1898 "maintMargin": "0.40000000",
1899 "updateTime": 1617939110373
1900 }
1901 ],
1902 "positions": []
1903 }"#;
1904 let account: BinanceFuturesAccountInfo =
1905 serde_json::from_str(json).expect("Failed to parse account info");
1906
1907 let account_id = AccountId::from("BINANCE-001");
1908 let ts_init = UnixNanos::from(1_000_000_000u64);
1909 let state = account.to_account_state(account_id, ts_init).unwrap();
1910
1911 assert_eq!(state.margins.len(), 2);
1912 assert!(state.margins.iter().all(|m| m.instrument_id.is_none()));
1913 let btc = state
1914 .margins
1915 .iter()
1916 .find(|m| m.currency.code.as_str() == "BTC")
1917 .expect("BTC margin missing");
1918 assert_eq!(btc.initial.as_f64(), 0.05);
1919 assert_eq!(btc.maintenance.as_f64(), 0.025);
1920 let eth = state
1921 .margins
1922 .iter()
1923 .find(|m| m.currency.code.as_str() == "ETH")
1924 .expect("ETH margin missing");
1925 assert_eq!(eth.initial.as_f64(), 0.8);
1926 assert_eq!(eth.maintenance.as_f64(), 0.4);
1927 }
1928
1929 #[rstest]
1934 fn test_account_info_to_account_state_precision_drift() {
1935 let json = r#"{
1936 "assets": [{
1937 "asset": "USDT",
1938 "walletBalance": "10.000000034999",
1939 "availableBalance": "9.999999994999",
1940 "updateTime": 1617939110373
1941 }],
1942 "positions": []
1943 }"#;
1944 let account: BinanceFuturesAccountInfo =
1945 serde_json::from_str(json).expect("Failed to parse account info");
1946
1947 let account_id = AccountId::from("BINANCE-001");
1948 let ts_init = UnixNanos::from(1_000_000_000u64);
1949 let state = account.to_account_state(account_id, ts_init).unwrap();
1950
1951 assert_eq!(state.balances.len(), 1);
1952 let balance = &state.balances[0];
1953 assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
1954 }
1955
1956 #[rstest]
1957 fn test_account_info_to_account_state_empty_balance() {
1958 let json = r#"{
1960 "assets": [{
1961 "asset": "USDT",
1962 "walletBalance": "",
1963 "availableBalance": "",
1964 "updateTime": 0
1965 }],
1966 "positions": []
1967 }"#;
1968 let account: BinanceFuturesAccountInfo =
1969 serde_json::from_str(json).expect("Failed to parse account info");
1970
1971 let account_id = AccountId::from("BINANCE-001");
1972 let ts_init = UnixNanos::from(1_000_000_000u64);
1973 let state = account.to_account_state(account_id, ts_init).unwrap();
1974
1975 assert_eq!(state.balances.len(), 1);
1976 let balance = &state.balances[0];
1977 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1978 assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
1979 assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
1980 }
1981
1982 #[rstest]
1983 fn test_account_info_to_account_state_empty_assets() {
1984 let json = r#"{
1986 "assets": [],
1987 "positions": []
1988 }"#;
1989 let account: BinanceFuturesAccountInfo =
1990 serde_json::from_str(json).expect("Failed to parse account info");
1991
1992 let account_id = AccountId::from("BINANCE-001");
1993 let ts_init = UnixNanos::from(1_000_000_000u64);
1994 let state = account.to_account_state(account_id, ts_init).unwrap();
1995
1996 assert_eq!(state.balances.len(), 1);
1997 let balance = &state.balances[0];
1998 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1999 }
2000
2001 #[rstest]
2002 fn test_parse_position_risk() {
2003 let json = load_fixture_string("futures/http_json/position_risk.json");
2004 let positions: Vec<BinancePositionRisk> =
2005 serde_json::from_str(&json).expect("Failed to parse position risk");
2006
2007 assert_eq!(positions.len(), 1);
2008 assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
2009 assert_eq!(positions[0].position_amt, "0.001");
2010 assert_eq!(positions[0].mark_price, "51000.0");
2011 assert_eq!(positions[0].leverage, "20");
2012 }
2013
2014 #[rstest]
2015 fn test_parse_balance_with_v1_field() {
2016 let json = load_fixture_string("futures/http_json/balance.json");
2018 let balances: Vec<BinanceFuturesBalance> =
2019 serde_json::from_str(&json).expect("Failed to parse balance");
2020
2021 assert_eq!(balances.len(), 1);
2022 assert_eq!(balances[0].asset.as_str(), "USDT");
2023 assert_eq!(
2025 balances[0].wallet_balance,
2026 Decimal::from_str_exact("122.12345678").unwrap()
2027 );
2028 assert_eq!(
2029 balances[0].available_balance,
2030 Decimal::from_str_exact("122.12345678").unwrap()
2031 );
2032 }
2033
2034 #[rstest]
2035 fn test_parse_balance_with_v2_field() {
2036 let json = r#"{
2038 "asset": "USDT",
2039 "walletBalance": "100.00000000",
2040 "availableBalance": "100.00000000",
2041 "updateTime": 1617939110373
2042 }"#;
2043
2044 let balance: BinanceFuturesBalance =
2045 serde_json::from_str(json).expect("Failed to parse balance");
2046
2047 assert_eq!(balance.asset.as_str(), "USDT");
2048 assert_eq!(
2049 balance.wallet_balance,
2050 Decimal::from_str_exact("100.00000000").unwrap()
2051 );
2052 }
2053
2054 #[rstest]
2055 fn test_parse_order() {
2056 let json = load_fixture_string("futures/http_json/order_response.json");
2057 let order: BinanceFuturesOrder =
2058 serde_json::from_str(&json).expect("Failed to parse order");
2059
2060 assert_eq!(order.order_id, 12345678);
2061 assert_eq!(order.symbol.as_str(), "BTCUSDT");
2062 assert_eq!(order.status, BinanceOrderStatus::New);
2063 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
2064 assert_eq!(order.side, BinanceSide::Buy);
2065 assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
2066 assert_eq!(order.price_match, Some(BinancePriceMatch::None));
2067 assert_eq!(
2068 order.self_trade_prevention_mode,
2069 Some(BinanceSelfTradePreventionMode::None)
2070 );
2071 }
2072
2073 #[rstest]
2074 fn test_parse_order_defaults_missing_cum_quote_to_zero() {
2075 let json = load_fixture_string("futures/http_json/order_response.json");
2076 let mut value: Value = serde_json::from_str(&json).expect("Failed to parse order fixture");
2077
2078 value
2079 .as_object_mut()
2080 .expect("Order fixture should be a JSON object")
2081 .remove("cumQuote");
2082
2083 let order: BinanceFuturesOrder =
2084 serde_json::from_value(value).expect("Failed to parse order");
2085
2086 assert_eq!(order.cum_quote, "0");
2087 }
2088
2089 #[rstest]
2090 fn test_parse_kline_rejects_non_string_price() {
2091 let value = serde_json::json!([
2092 1_625_474_304_000_i64,
2093 50000.00,
2094 "51000.00",
2095 "49000.00",
2096 "50500.00",
2097 "12.5",
2098 1_625_474_364_000_i64,
2099 "631250.00",
2100 100_i64,
2101 "6.2",
2102 "313100.00"
2103 ]);
2104
2105 let error = serde_json::from_value::<BinanceFuturesKline>(value)
2106 .unwrap_err()
2107 .to_string();
2108
2109 assert!(error.contains("open"));
2110 }
2111
2112 #[rstest]
2113 fn test_parse_hedge_mode_response() {
2114 let json = r#"{"dualSidePosition": true}"#;
2115 let response: BinanceHedgeModeResponse =
2116 serde_json::from_str(json).expect("Failed to parse hedge mode");
2117 assert!(response.dual_side_position);
2118 }
2119
2120 #[rstest]
2121 fn test_parse_leverage_response() {
2122 let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
2123 let response: BinanceLeverageResponse =
2124 serde_json::from_str(json).expect("Failed to parse leverage");
2125 assert_eq!(response.symbol.as_str(), "BTCUSDT");
2126 assert_eq!(response.leverage, 20);
2127 }
2128
2129 #[rstest]
2130 fn test_parse_listen_key_response() {
2131 let json =
2132 r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
2133 let response: ListenKeyResponse =
2134 serde_json::from_str(json).expect("Failed to parse listen key");
2135 assert!(!response.listen_key.is_empty());
2136 }
2137
2138 #[rstest]
2139 fn test_parse_account_position() {
2140 let json = r#"{
2141 "symbol": "ETHUSDT",
2142 "initialMargin": "100.00",
2143 "maintMargin": "50.00",
2144 "unrealizedProfit": "10.00",
2145 "positionInitialMargin": "100.00",
2146 "openOrderInitialMargin": "0",
2147 "leverage": "10",
2148 "isolated": true,
2149 "entryPrice": "2000.00",
2150 "maxNotional": "100000",
2151 "bidNotional": "0",
2152 "askNotional": "0",
2153 "positionSide": "LONG",
2154 "positionAmt": "0.5",
2155 "updateTime": 1625474304765
2156 }"#;
2157
2158 let position: BinanceAccountPosition =
2159 serde_json::from_str(json).expect("Failed to parse account position");
2160
2161 assert_eq!(position.symbol.as_str(), "ETHUSDT");
2162 assert_eq!(position.leverage, Some("10".to_string()));
2163 assert_eq!(position.isolated, Some(true));
2164 assert_eq!(position.position_side, Some(BinancePositionSide::Long));
2165 }
2166
2167 #[rstest]
2168 fn test_parse_algo_order() {
2169 let json = r#"{
2170 "algoId": 123456789,
2171 "clientAlgoId": "test-algo-order-1",
2172 "algoType": "CONDITIONAL",
2173 "type": "STOP_MARKET",
2174 "symbol": "BTCUSDT",
2175 "side": "BUY",
2176 "positionSide": "BOTH",
2177 "timeInForce": "GTC",
2178 "quantity": "0.001",
2179 "algoStatus": "NEW",
2180 "triggerPrice": "45000.00",
2181 "workingType": "MARK_PRICE",
2182 "reduceOnly": false,
2183 "createTime": 1625474304765,
2184 "updateTime": 1625474304765
2185 }"#;
2186
2187 let order: BinanceFuturesAlgoOrder =
2188 serde_json::from_str(json).expect("Failed to parse algo order");
2189
2190 assert_eq!(order.algo_id, 123456789);
2191 assert_eq!(order.client_algo_id, "test-algo-order-1");
2192 assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
2193 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2194 assert_eq!(order.symbol.as_str(), "BTCUSDT");
2195 assert_eq!(order.side, BinanceSide::Buy);
2196 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
2197 assert_eq!(order.trigger_price, Some("45000.00".to_string()));
2198 }
2199
2200 #[rstest]
2201 #[case("actualQty", "actualPrice")]
2202 #[case("executedQty", "avgPrice")]
2203 fn test_parse_algo_order_finished(#[case] quantity_field: &str, #[case] price_field: &str) {
2204 let json = load_fixture_string("futures/http_json/algo_order_response.json")
2205 .replace("actualQty", quantity_field)
2206 .replace("actualPrice", price_field);
2207
2208 let order: BinanceFuturesAlgoOrder =
2209 serde_json::from_str(&json).expect("Failed to parse finished algo order");
2210
2211 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Finished));
2212 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
2213 assert_eq!(order.actual_order_id, Some("987654321".to_string()));
2214 assert_eq!(order.executed_qty, Some("0.001".to_string()));
2215 assert_eq!(order.avg_price, Some("50000.00".to_string()));
2216 }
2217
2218 #[rstest]
2219 fn test_parse_algo_order_cancel_response() {
2220 let json = r#"{
2221 "algoId": 123456789,
2222 "clientAlgoId": "test-algo-order-1",
2223 "code": "200",
2224 "msg": "success"
2225 }"#;
2226
2227 let response: BinanceFuturesAlgoOrderCancelResponse =
2228 serde_json::from_str(json).expect("Failed to parse algo cancel response");
2229
2230 assert_eq!(response.algo_id, 123456789);
2231 assert_eq!(response.client_algo_id, "test-algo-order-1");
2232 assert_eq!(response.code, "200");
2233 assert_eq!(response.msg, "success");
2234 }
2235
2236 #[rstest]
2237 fn test_order_to_report_decodes_broker_id() {
2238 let json = r#"{
2239 "orderId": 12345678,
2240 "symbol": "BTCUSDT",
2241 "status": "NEW",
2242 "clientOrderId": "x-aHRE4BCj-T0000000000000",
2243 "price": "50000.00",
2244 "avgPrice": "0.00",
2245 "origQty": "0.001",
2246 "executedQty": "0.000",
2247 "cumQuote": "0.00",
2248 "timeInForce": "GTC",
2249 "type": "LIMIT",
2250 "reduceOnly": false,
2251 "closePosition": false,
2252 "side": "BUY",
2253 "positionSide": "BOTH",
2254 "stopPrice": "0.00",
2255 "workingType": "CONTRACT_PRICE",
2256 "priceProtect": false,
2257 "origType": "LIMIT",
2258 "priceMatch": "NONE",
2259 "selfTradePreventionMode": "NONE",
2260 "goodTillDate": 0,
2261 "time": 1625474304765,
2262 "updateTime": 1625474304765
2263 }"#;
2264
2265 let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
2266 let account_id = AccountId::from("BINANCE-FUTURES-001");
2267 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2268 let ts_init = UnixNanos::from(1_000_000_000u64);
2269
2270 let report = order
2271 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2272 .unwrap();
2273
2274 assert_eq!(
2275 report.client_order_id,
2276 Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
2277 );
2278 assert_eq!(report.price, Some(Price::from("50000.00")));
2279 }
2280
2281 #[rstest]
2282 fn test_order_to_report_rejects_invalid_client_order_id() {
2283 let mut order = order_with_price("50000.00");
2284 order.client_order_id = String::new();
2285
2286 let result = order.to_order_status_report(
2287 AccountId::from("BINANCE-FUTURES-001"),
2288 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2289 2,
2290 3,
2291 false,
2292 UnixNanos::from(1_000_000_000u64),
2293 );
2294
2295 assert_eq!(
2296 result.unwrap_err().to_string(),
2297 "invalid Binance client order ID ''"
2298 );
2299 }
2300
2301 #[rstest]
2302 #[case("0")]
2303 #[case("")]
2304 fn test_order_to_report_omits_missing_price(#[case] price: &str) {
2305 let order = order_with_price(price);
2306 let account_id = AccountId::from("BINANCE-FUTURES-001");
2307 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2308 let ts_init = UnixNanos::from(1_000_000_000u64);
2309
2310 let report = order
2311 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2312 .unwrap();
2313
2314 assert_eq!(report.price, None);
2315 }
2316
2317 #[rstest]
2318 fn test_order_to_report_sets_avg_px_for_filled_market_order() {
2319 let mut order = order_with_price("0");
2320 order.status = BinanceOrderStatus::Filled;
2321 order.executed_qty = "0.001".to_string();
2322 order.cum_quote = "50.00".to_string();
2323 order.avg_price = Some("50000.00".to_string());
2324 let account_id = AccountId::from("BINANCE-FUTURES-001");
2325 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2326 let ts_init = UnixNanos::from(1_000_000_000u64);
2327
2328 let report = order
2329 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2330 .unwrap();
2331
2332 assert_eq!(report.price, None);
2333 assert_eq!(
2334 report.avg_px,
2335 Some(Decimal::from_str_exact("50000.00").unwrap())
2336 );
2337 }
2338
2339 #[rstest]
2340 fn test_order_to_report_omits_avg_px_without_fills() {
2341 let mut order = order_with_price("0");
2342 order.avg_price = Some("50000.00".to_string());
2343 let account_id = AccountId::from("BINANCE-FUTURES-001");
2344 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2345 let ts_init = UnixNanos::from(1_000_000_000u64);
2346
2347 let report = order
2348 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2349 .unwrap();
2350
2351 assert_eq!(report.avg_px, None);
2352 }
2353
2354 #[rstest]
2355 fn test_order_to_report_rejects_invalid_avg_px_for_filled_order() {
2356 let mut order = order_with_price("0");
2357 order.executed_qty = "0.001".to_string();
2358 order.avg_price = Some("not-a-number".to_string());
2359 let account_id = AccountId::from("BINANCE-FUTURES-001");
2360 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2361 let ts_init = UnixNanos::from(1_000_000_000u64);
2362
2363 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2364
2365 let error = result.unwrap_err().to_string();
2366 assert!(error.contains("avg_price"));
2367 }
2368
2369 #[rstest]
2370 fn test_close_all_algo_report_with_actual_uses_matching_engine_quantity() {
2371 let mut algo = algo_order_with_price(None);
2372 algo.order_type = BinanceFuturesOrderType::StopMarket;
2373 algo.quantity = None;
2374 algo.close_position = Some(true);
2375 algo.algo_status = Some(BinanceAlgoStatus::Finished);
2376 algo.actual_order_id = Some("987654321".to_string());
2377 algo.executed_qty = Some("0.002".to_string());
2378 algo.avg_price = Some("49000.00".to_string());
2379 algo.reduce_only = Some(true);
2380 algo.trigger_time = Some(1_625_474_305_000);
2381 algo.time_in_force = Some(BinanceTimeInForce::Gtd);
2382 algo.good_till_date = Some(1_700_000_601_000);
2383
2384 let mut actual = order_with_price("0");
2385 actual.order_id = 987654321;
2386 actual.orig_qty = "0.002".to_string();
2387 actual.executed_qty = "0.001".to_string();
2388 actual.avg_price = Some("50000.00".to_string());
2389 actual.status = BinanceOrderStatus::PartiallyFilled;
2390 actual.order_type = BinanceFuturesOrderType::Market;
2391 actual.side = BinanceSide::Sell;
2392 actual.update_time = Some(1_625_474_306_000);
2393
2394 let account_id = AccountId::from("BINANCE-FUTURES-001");
2395 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2396 let ts_init = UnixNanos::from(1_000_000_000u64);
2397 let report = algo
2398 .to_order_status_report_with_actual(
2399 &actual,
2400 account_id,
2401 instrument_id,
2402 2,
2403 3,
2404 false,
2405 ts_init,
2406 )
2407 .unwrap();
2408
2409 assert_eq!(
2410 report.client_order_id,
2411 Some(ClientOrderId::from("my-algo-order-1"))
2412 );
2413 assert_eq!(report.venue_order_id, VenueOrderId::from("987654321"));
2414 assert_eq!(report.order_type, OrderType::StopMarket);
2415 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
2416 assert_eq!(report.quantity, Quantity::from("0.002"));
2417 assert_eq!(report.filled_qty, Quantity::from("0.001"));
2418 assert_eq!(report.avg_px, Some(Decimal::from(50000)));
2419 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2420 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2421 assert!(report.reduce_only);
2422 assert_eq!(
2423 report.expire_time,
2424 Some(UnixNanos::from_millis(1_700_000_601_000)),
2425 );
2426 assert_eq!(
2427 report.ts_triggered,
2428 Some(UnixNanos::from_millis(1_625_474_305_000))
2429 );
2430 assert_eq!(report.ts_last, UnixNanos::from_millis(1_625_474_306_000));
2431 }
2432
2433 #[rstest]
2434 #[case(BinanceAlgoStatus::Finished, Some("0.001"), OrderStatus::Filled)]
2435 #[case(BinanceAlgoStatus::Finished, Some("0.0005"), OrderStatus::Canceled)]
2436 #[case(
2437 BinanceAlgoStatus::Triggered,
2438 Some("0.0005"),
2439 OrderStatus::PartiallyFilled
2440 )]
2441 #[case(BinanceAlgoStatus::Triggered, None, OrderStatus::Accepted)]
2442 fn test_algo_order_status_uses_actual_quantity_conservatively(
2443 #[case] algo_status: BinanceAlgoStatus,
2444 #[case] executed_qty: Option<&str>,
2445 #[case] expected: OrderStatus,
2446 ) {
2447 let mut order = algo_order_with_price(None);
2448 order.algo_status = Some(algo_status);
2449 order.executed_qty = executed_qty.map(str::to_string);
2450
2451 assert_eq!(order.parse_order_status(), expected);
2452 }
2453
2454 #[rstest]
2455 fn test_algo_order_to_report_sets_price() {
2456 let order = algo_order_with_price(Some("50000.00"));
2457 let account_id = AccountId::from("BINANCE-FUTURES-001");
2458 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2459 let ts_init = UnixNanos::from(1_000_000_000u64);
2460
2461 let report = order
2462 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2463 .unwrap();
2464
2465 assert_eq!(report.price, Some(Price::from("50000.00")));
2466 }
2467
2468 #[rstest]
2469 fn test_algo_order_to_report_sets_actual_fill_fields() {
2470 let json = load_fixture_string("futures/http_json/algo_order_response.json");
2471 let order: BinanceFuturesAlgoOrder = serde_json::from_str(&json).unwrap();
2472 let account_id = AccountId::from("BINANCE-FUTURES-001");
2473 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2474 let ts_init = UnixNanos::from(1_000_000_000u64);
2475
2476 let report = order
2477 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2478 .unwrap();
2479
2480 assert_eq!(report.order_status, OrderStatus::Filled);
2481 assert_eq!(report.filled_qty.as_decimal(), dec!(0.001));
2482 assert_eq!(report.price, None);
2483 assert_eq!(report.avg_px, Some(dec!(50000.00)));
2484 }
2485
2486 #[rstest]
2487 fn test_algo_order_to_report_omits_avg_price_without_fills() {
2488 let mut order = algo_order_with_price(None);
2489 order.executed_qty = Some("0".to_string());
2490 order.avg_price = Some("not-a-number".to_string());
2491 let account_id = AccountId::from("BINANCE-FUTURES-001");
2492 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2493 let ts_init = UnixNanos::from(1_000_000_000u64);
2494
2495 let report = order
2496 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2497 .unwrap();
2498
2499 assert_eq!(report.filled_qty.as_decimal(), Decimal::ZERO);
2500 assert_eq!(report.avg_px, None);
2501 }
2502
2503 #[rstest]
2504 fn test_algo_order_to_report_sets_trigger_fields() {
2505 let order = algo_order_with_price(Some("44000.00"));
2506 let account_id = AccountId::from("BINANCE-FUTURES-001");
2507 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2508 let ts_init = UnixNanos::from(1_000_000_000u64);
2509
2510 let report = order
2511 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2512 .unwrap();
2513
2514 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2515 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2516 }
2517
2518 #[rstest]
2519 fn test_algo_order_to_report_sets_trailing_fields() {
2520 let mut order = algo_order_with_price(None);
2521 order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2522 order.trigger_price = None;
2523 order.activate_price = Some("45000.00".to_string());
2524 order.callback_rate = Some("0.25".to_string());
2525 let account_id = AccountId::from("BINANCE-FUTURES-001");
2526 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2527 let ts_init = UnixNanos::from(1_000_000_000u64);
2528
2529 let report = order
2530 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2531 .unwrap();
2532
2533 assert_eq!(report.trigger_price, Some(Price::from("45000.00")));
2534 assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
2535 assert_eq!(report.trailing_offset, Some(Decimal::from(25)));
2536 assert_eq!(
2537 report.trailing_offset_type,
2538 Some(TrailingOffsetType::BasisPoints),
2539 );
2540 }
2541
2542 #[rstest]
2543 #[case(None)]
2544 #[case(Some("0"))]
2545 #[case(Some(""))]
2546 fn test_algo_order_to_report_omits_missing_price(#[case] price: Option<&str>) {
2547 let order = algo_order_with_price(price);
2548 let account_id = AccountId::from("BINANCE-FUTURES-001");
2549 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2550 let ts_init = UnixNanos::from(1_000_000_000u64);
2551
2552 let report = order
2553 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2554 .unwrap();
2555
2556 assert_eq!(report.price, None);
2557 }
2558
2559 #[rstest]
2560 fn test_order_to_report_rejects_invalid_price() {
2561 let order = order_with_price("not-a-number");
2562 let account_id = AccountId::from("BINANCE-FUTURES-001");
2563 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2564 let ts_init = UnixNanos::from(1_000_000_000u64);
2565
2566 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init);
2567
2568 let error = result.unwrap_err().to_string();
2569 assert!(error.contains("invalid price"));
2570 }
2571
2572 #[rstest]
2573 fn test_algo_order_to_report_rejects_invalid_trigger_price() {
2574 let mut order = algo_order_with_price(Some("50000.00"));
2575 order.trigger_price = Some("not-a-number".to_string());
2576 let account_id = AccountId::from("BINANCE-FUTURES-001");
2577 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2578 let ts_init = UnixNanos::from(1_000_000_000u64);
2579
2580 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2581
2582 let error = result.unwrap_err().to_string();
2583 assert!(error.contains("trigger_price"));
2584 }
2585
2586 #[rstest]
2587 fn test_algo_order_to_report_rejects_missing_trigger_price() {
2588 let mut order = algo_order_with_price(None);
2589 order.order_type = BinanceFuturesOrderType::StopMarket;
2590 order.trigger_price = None;
2591 let account_id = AccountId::from("BINANCE-FUTURES-001");
2592 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2593 let ts_init = UnixNanos::from(1_000_000_000u64);
2594
2595 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2596
2597 let error = result.unwrap_err().to_string();
2598 assert!(error.contains("missing positive trigger_price"));
2599 }
2600
2601 #[rstest]
2602 fn test_algo_order_to_report_rejects_invalid_callback_rate() {
2603 let mut order = algo_order_with_price(None);
2604 order.order_type = BinanceFuturesOrderType::TrailingStopMarket;
2605 order.trigger_price = None;
2606 order.activate_price = Some("45000.00".to_string());
2607 order.callback_rate = Some("not-a-number".to_string());
2608 let account_id = AccountId::from("BINANCE-FUTURES-001");
2609 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2610 let ts_init = UnixNanos::from(1_000_000_000u64);
2611
2612 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2613
2614 let error = result.unwrap_err().to_string();
2615 assert!(error.contains("callback_rate"));
2616 }
2617
2618 #[rstest]
2619 fn test_algo_order_to_report_rejects_invalid_price() {
2620 let order = algo_order_with_price(Some("not-a-number"));
2621 let account_id = AccountId::from("BINANCE-FUTURES-001");
2622 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2623 let ts_init = UnixNanos::from(1_000_000_000u64);
2624
2625 let result = order.to_order_status_report(account_id, instrument_id, 2, 3, ts_init);
2626
2627 let error = result.unwrap_err().to_string();
2628 assert!(error.contains("invalid price"));
2629 }
2630
2631 #[rstest]
2632 fn test_order_to_report_preserves_good_till_date() {
2633 let mut order = order_with_price("50000.00");
2634 order.time_in_force = BinanceTimeInForce::Gtd;
2635 order.good_till_date = Some(1_700_000_601_000);
2636 let account_id = AccountId::from("BINANCE-FUTURES-001");
2637 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2638 let ts_init = UnixNanos::from(1_000_000_000u64);
2639
2640 let report = order
2641 .to_order_status_report(account_id, instrument_id, 2, 3, false, ts_init)
2642 .unwrap();
2643
2644 assert_eq!(report.time_in_force, TimeInForce::Gtd);
2645 assert_eq!(
2646 report.expire_time,
2647 Some(UnixNanos::from_millis(1_700_000_601_000)),
2648 );
2649 }
2650
2651 #[rstest]
2652 fn test_algo_order_to_report_preserves_good_till_date() {
2653 let mut order = algo_order_with_price(Some("50000.00"));
2654 order.time_in_force = Some(BinanceTimeInForce::Gtd);
2655 order.good_till_date = Some(1_700_000_601_000);
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 report = order
2661 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2662 .unwrap();
2663
2664 assert_eq!(report.time_in_force, TimeInForce::Gtd);
2665 assert_eq!(
2666 report.expire_time,
2667 Some(UnixNanos::from_millis(1_700_000_601_000)),
2668 );
2669 }
2670
2671 fn order_with_price(price: &str) -> BinanceFuturesOrder {
2672 BinanceFuturesOrder {
2673 symbol: Ustr::from("BTCUSDT"),
2674 order_id: 12345678,
2675 client_order_id: "external-order".to_string(),
2676 orig_qty: "0.001".to_string(),
2677 executed_qty: "0.000".to_string(),
2678 cum_quote: "0.00".to_string(),
2679 price: price.to_string(),
2680 avg_price: Some("0.00".to_string()),
2681 stop_price: Some("0.00".to_string()),
2682 status: BinanceOrderStatus::New,
2683 time_in_force: BinanceTimeInForce::Gtc,
2684 order_type: BinanceFuturesOrderType::Market,
2685 orig_type: Some(BinanceFuturesOrderType::Market),
2686 side: BinanceSide::Buy,
2687 position_side: Some(BinancePositionSide::Both),
2688 reduce_only: Some(false),
2689 close_position: Some(false),
2690 activate_price: None,
2691 price_rate: None,
2692 working_type: Some(BinanceWorkingType::ContractPrice),
2693 price_protect: Some(false),
2694 is_isolated: None,
2695 good_till_date: Some(0),
2696 price_match: Some(BinancePriceMatch::None),
2697 self_trade_prevention_mode: Some(BinanceSelfTradePreventionMode::None),
2698 update_time: Some(1_625_474_304_765),
2699 working_type_id: None,
2700 }
2701 }
2702
2703 fn algo_order_with_price(price: Option<&str>) -> BinanceFuturesAlgoOrder {
2704 BinanceFuturesAlgoOrder {
2705 algo_id: 123456789,
2706 client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2707 algo_type: BinanceAlgoType::Conditional,
2708 order_type: BinanceFuturesOrderType::TakeProfit,
2709 symbol: Ustr::from("BTCUSDT"),
2710 side: BinanceSide::Sell,
2711 position_side: Some(BinancePositionSide::Both),
2712 time_in_force: Some(BinanceTimeInForce::Gtc),
2713 quantity: Some("0.001".to_string()),
2714 algo_status: Some(BinanceAlgoStatus::New),
2715 trigger_price: Some("45000.00".to_string()),
2716 price: price.map(str::to_string),
2717 working_type: Some(BinanceWorkingType::MarkPrice),
2718 close_position: Some(false),
2719 price_protect: None,
2720 reduce_only: Some(false),
2721 activate_price: None,
2722 callback_rate: None,
2723 good_till_date: Some(0),
2724 create_time: Some(1_625_474_304_765),
2725 update_time: Some(1_625_474_304_765),
2726 trigger_time: None,
2727 actual_order_id: None,
2728 executed_qty: None,
2729 avg_price: None,
2730 }
2731 }
2732
2733 #[rstest]
2734 fn test_user_trade_to_fill_report_rejects_invalid_commission() {
2735 let trade = BinanceUserTrade {
2736 symbol: Ustr::from("BTCUSDT"),
2737 id: 100,
2738 order_id: 200,
2739 price: "50000.00".to_string(),
2740 qty: "0.001".to_string(),
2741 quote_qty: None,
2742 realized_pnl: "0".to_string(),
2743 side: BinanceSide::Buy,
2744 position_side: None,
2745 time: 1_625_474_304_000,
2746 buyer: true,
2747 maker: false,
2748 commission: Some("not-a-number".to_string()),
2749 commission_asset: Some(Ustr::from("USDT")),
2750 margin_asset: None,
2751 };
2752
2753 let result = trade.to_fill_report(
2754 AccountId::from("BINANCE-FUTURES-001"),
2755 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2756 2,
2757 3,
2758 Currency::USDT(),
2759 UnixNanos::from(1_000_000_000u64),
2760 );
2761
2762 let error = result.unwrap_err().to_string();
2763 assert!(error.contains("commission"));
2764 }
2765
2766 #[rstest]
2767 fn test_algo_order_to_report_decodes_broker_id() {
2768 let json = r#"{
2769 "algoId": 123456789,
2770 "clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
2771 "algoType": "CONDITIONAL",
2772 "type": "STOP_MARKET",
2773 "symbol": "BTCUSDT",
2774 "side": "BUY",
2775 "positionSide": "BOTH",
2776 "timeInForce": "GTC",
2777 "quantity": "0.001",
2778 "algoStatus": "NEW",
2779 "triggerPrice": "45000.00",
2780 "workingType": "MARK_PRICE",
2781 "reduceOnly": false,
2782 "createTime": 1625474304765,
2783 "updateTime": 1625474304765
2784 }"#;
2785
2786 let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
2787 let account_id = AccountId::from("BINANCE-FUTURES-001");
2788 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2789 let ts_init = UnixNanos::from(1_000_000_000u64);
2790
2791 let report = order
2792 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2793 .unwrap();
2794
2795 assert_eq!(
2796 report.client_order_id,
2797 Some(ClientOrderId::from("my-algo-order-1")),
2798 );
2799 }
2800
2801 #[rstest]
2802 fn test_algo_order_to_report_rejects_invalid_client_order_id() {
2803 let mut order = algo_order_with_price(None);
2804 order.client_algo_id = "x-aHRE4BCj-R".to_string();
2805
2806 let result = order.to_order_status_report(
2807 AccountId::from("BINANCE-FUTURES-001"),
2808 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2809 2,
2810 3,
2811 UnixNanos::from(1_000_000_000u64),
2812 );
2813
2814 assert_eq!(
2815 result.unwrap_err().to_string(),
2816 "missing raw broker client order ID payload"
2817 );
2818 }
2819
2820 #[rstest]
2821 #[case(None, "123456789")]
2822 #[case(Some(""), "123456789")]
2823 #[case(Some("987654321"), "987654321")]
2824 fn test_algo_order_to_report_selects_valid_venue_order_id(
2825 #[case] actual_order_id: Option<&str>,
2826 #[case] expected_venue_order_id: &str,
2827 ) {
2828 let order = BinanceFuturesAlgoOrder {
2829 algo_id: 123456789,
2830 client_algo_id: "x-aHRE4BCj-Rmy-algo-order-1".to_string(),
2831 algo_type: BinanceAlgoType::Conditional,
2832 order_type: BinanceFuturesOrderType::StopMarket,
2833 symbol: Ustr::from("BTCUSDT"),
2834 side: BinanceSide::Buy,
2835 position_side: Some(BinancePositionSide::Both),
2836 time_in_force: Some(BinanceTimeInForce::Gtc),
2837 quantity: Some("0.001".to_string()),
2838 algo_status: Some(BinanceAlgoStatus::New),
2839 trigger_price: Some("45000.00".to_string()),
2840 price: None,
2841 working_type: Some(BinanceWorkingType::MarkPrice),
2842 close_position: Some(false),
2843 price_protect: None,
2844 reduce_only: Some(false),
2845 activate_price: None,
2846 callback_rate: None,
2847 good_till_date: Some(0),
2848 create_time: Some(1_625_474_304_765),
2849 update_time: Some(1_625_474_304_765),
2850 trigger_time: None,
2851 actual_order_id: actual_order_id.map(str::to_string),
2852 executed_qty: None,
2853 avg_price: None,
2854 };
2855 let account_id = AccountId::from("BINANCE-FUTURES-001");
2856 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2857 let ts_init = UnixNanos::from(1_000_000_000u64);
2858
2859 let report = order
2860 .to_order_status_report(account_id, instrument_id, 2, 3, ts_init)
2861 .unwrap();
2862
2863 assert_eq!(
2864 report.venue_order_id,
2865 VenueOrderId::new(expected_venue_order_id)
2866 );
2867 }
2868
2869 #[rstest]
2870 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
2871 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
2872 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
2873 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
2874 fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
2875 #[case] status: BinanceOrderStatus,
2876 #[case] treat_expired_as_canceled: bool,
2877 #[case] expected: OrderStatus,
2878 ) {
2879 let result = status.to_nautilus_order_status(treat_expired_as_canceled);
2880 assert_eq!(result, expected);
2881 }
2882}