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::common::{
39 consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
40 encoder::decode_broker_id,
41 enums::{
42 BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
43 BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
44 BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
45 BinanceTradingStatus, BinanceWorkingType,
46 },
47 models::BinanceRateLimit,
48};
49
50#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BinanceServerTime {
54 pub server_time: i64,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct BinanceFuturesTrade {
62 pub id: i64,
64 pub price: String,
66 pub qty: String,
68 pub quote_qty: String,
70 pub time: i64,
72 pub is_buyer_maker: bool,
74}
75
76#[derive(Clone, Debug)]
78pub struct BinanceFuturesKline {
79 pub open_time: i64,
81 pub open: String,
83 pub high: String,
85 pub low: String,
87 pub close: String,
89 pub volume: String,
91 pub close_time: i64,
93 pub quote_volume: String,
95 pub num_trades: i64,
97 pub taker_buy_base_volume: String,
99 pub taker_buy_quote_volume: String,
101}
102
103impl<'de> Deserialize<'de> for BinanceFuturesKline {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: serde::Deserializer<'de>,
107 {
108 let arr: Vec<Value> = Vec::deserialize(deserializer)?;
109 if arr.len() < 11 {
110 return Err(serde::de::Error::custom("Invalid kline array length"));
111 }
112
113 Ok(Self {
114 open_time: arr[0].as_i64().unwrap_or(0),
115 open: arr[1].as_str().unwrap_or("0").to_string(),
116 high: arr[2].as_str().unwrap_or("0").to_string(),
117 low: arr[3].as_str().unwrap_or("0").to_string(),
118 close: arr[4].as_str().unwrap_or("0").to_string(),
119 volume: arr[5].as_str().unwrap_or("0").to_string(),
120 close_time: arr[6].as_i64().unwrap_or(0),
121 quote_volume: arr[7].as_str().unwrap_or("0").to_string(),
122 num_trades: arr[8].as_i64().unwrap_or(0),
123 taker_buy_base_volume: arr[9].as_str().unwrap_or("0").to_string(),
124 taker_buy_quote_volume: arr[10].as_str().unwrap_or("0").to_string(),
125 })
126 }
127}
128
129#[derive(Clone, Debug, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct BinanceFuturesUsdExchangeInfo {
133 pub timezone: String,
135 pub server_time: i64,
137 pub rate_limits: Vec<BinanceRateLimit>,
139 #[serde(default)]
141 pub exchange_filters: Vec<Value>,
142 #[serde(default)]
144 pub assets: Vec<BinanceFuturesAsset>,
145 pub symbols: Vec<BinanceFuturesUsdSymbol>,
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct BinanceFuturesAsset {
153 pub asset: Ustr,
155 pub margin_available: bool,
157 #[serde(default)]
159 pub auto_asset_exchange: Option<String>,
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct BinanceFuturesUsdSymbol {
166 pub symbol: Ustr,
168 pub pair: Ustr,
170 pub contract_type: String,
172 pub delivery_date: i64,
174 pub onboard_date: i64,
176 pub status: BinanceTradingStatus,
178 pub maint_margin_percent: String,
180 pub required_margin_percent: String,
182 pub base_asset: Ustr,
184 pub quote_asset: Ustr,
186 pub margin_asset: Ustr,
188 pub price_precision: i32,
190 pub quantity_precision: i32,
192 pub base_asset_precision: i32,
194 pub quote_precision: i32,
196 #[serde(default)]
198 pub underlying_type: Option<String>,
199 #[serde(default)]
201 pub underlying_sub_type: Vec<String>,
202 #[serde(default)]
204 pub settle_plan: Option<i64>,
205 #[serde(default)]
207 pub trigger_protect: Option<String>,
208 #[serde(default)]
210 pub liquidation_fee: Option<String>,
211 #[serde(default)]
213 pub market_take_bound: Option<String>,
214 pub order_types: Vec<String>,
216 pub time_in_force: Vec<String>,
218 pub filters: Vec<Value>,
220}
221
222#[derive(Clone, Debug, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct BinanceFuturesCoinExchangeInfo {
226 pub timezone: String,
228 pub server_time: i64,
230 pub rate_limits: Vec<BinanceRateLimit>,
232 #[serde(default)]
234 pub exchange_filters: Vec<Value>,
235 pub symbols: Vec<BinanceFuturesCoinSymbol>,
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct BinanceFuturesCoinSymbol {
243 pub symbol: Ustr,
245 pub pair: Ustr,
247 pub contract_type: String,
249 pub delivery_date: i64,
251 pub onboard_date: i64,
253 #[serde(default)]
255 pub contract_status: Option<BinanceContractStatus>,
256 pub contract_size: i64,
258 pub maint_margin_percent: String,
260 pub required_margin_percent: String,
262 pub base_asset: Ustr,
264 pub quote_asset: Ustr,
266 pub margin_asset: Ustr,
268 pub price_precision: i32,
270 pub quantity_precision: i32,
272 pub base_asset_precision: i32,
274 pub quote_precision: i32,
276 #[serde(default, rename = "equalQtyPrecision")]
278 pub equal_qty_precision: Option<i32>,
279 #[serde(default)]
281 pub trigger_protect: Option<String>,
282 #[serde(default)]
284 pub liquidation_fee: Option<String>,
285 #[serde(default)]
287 pub market_take_bound: Option<String>,
288 pub order_types: Vec<String>,
290 pub time_in_force: Vec<String>,
292 pub filters: Vec<Value>,
294}
295
296#[derive(Clone, Debug, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct BinanceFuturesTicker24hr {
300 pub symbol: Ustr,
302 pub price_change: String,
304 pub price_change_percent: String,
306 pub weighted_avg_price: String,
308 pub last_price: String,
310 #[serde(default)]
312 pub last_qty: Option<String>,
313 pub open_price: String,
315 pub high_price: String,
317 pub low_price: String,
319 pub volume: String,
321 pub quote_volume: String,
323 pub open_time: i64,
325 pub close_time: i64,
327 #[serde(default)]
329 pub first_id: Option<i64>,
330 #[serde(default)]
332 pub last_id: Option<i64>,
333 #[serde(default)]
335 pub count: Option<i64>,
336}
337
338#[derive(Clone, Debug, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct BinanceFuturesMarkPrice {
342 pub symbol: Ustr,
344 pub mark_price: String,
346 #[serde(default)]
348 pub index_price: Option<String>,
349 #[serde(default)]
351 pub estimated_settle_price: Option<String>,
352 #[serde(default)]
354 pub last_funding_rate: Option<String>,
355 #[serde(default)]
357 pub next_funding_time: Option<i64>,
358 #[serde(default)]
360 pub interest_rate: Option<String>,
361 pub time: i64,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct BinanceOrderBook {
369 pub last_update_id: i64,
371 pub bids: Vec<(String, String)>,
373 pub asks: Vec<(String, String)>,
375 #[serde(default, rename = "E")]
377 pub event_time: Option<i64>,
378 #[serde(default, rename = "T")]
380 pub transaction_time: Option<i64>,
381}
382
383#[derive(Clone, Debug, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct BinanceBookTicker {
387 pub symbol: Ustr,
389 pub bid_price: String,
391 pub bid_qty: String,
393 pub ask_price: String,
395 pub ask_qty: String,
397 #[serde(default)]
399 pub time: Option<i64>,
400}
401
402#[derive(Clone, Debug, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct BinancePriceTicker {
406 pub symbol: Ustr,
408 pub price: String,
410 #[serde(default)]
412 pub time: Option<i64>,
413}
414
415#[derive(Clone, Debug, Serialize, Deserialize)]
417#[serde(rename_all = "camelCase")]
418pub struct BinanceFundingRate {
419 pub symbol: Ustr,
421 pub funding_rate: String,
423 pub funding_time: i64,
425 #[serde(default)]
427 pub mark_price: Option<String>,
428 #[serde(default)]
430 pub index_price: Option<String>,
431}
432
433#[derive(Clone, Debug, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct BinanceOpenInterest {
437 pub symbol: Ustr,
439 pub open_interest: String,
441 pub time: i64,
443}
444
445#[derive(Clone, Debug, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct BinanceFuturesBalance {
449 #[serde(default)]
451 pub account_alias: Option<String>,
452 pub asset: Ustr,
454 #[serde(
456 alias = "balance",
457 deserialize_with = "deserialize_decimal_or_zero",
458 serialize_with = "serialize_decimal_as_str"
459 )]
460 pub wallet_balance: Decimal,
461 #[serde(
463 default,
464 deserialize_with = "deserialize_optional_decimal_from_str",
465 serialize_with = "serialize_optional_decimal_as_str"
466 )]
467 pub unrealized_profit: Option<Decimal>,
468 #[serde(
470 default,
471 deserialize_with = "deserialize_optional_decimal_from_str",
472 serialize_with = "serialize_optional_decimal_as_str"
473 )]
474 pub margin_balance: Option<Decimal>,
475 #[serde(
477 default,
478 deserialize_with = "deserialize_optional_decimal_from_str",
479 serialize_with = "serialize_optional_decimal_as_str"
480 )]
481 pub maint_margin: Option<Decimal>,
482 #[serde(
484 default,
485 deserialize_with = "deserialize_optional_decimal_from_str",
486 serialize_with = "serialize_optional_decimal_as_str"
487 )]
488 pub initial_margin: Option<Decimal>,
489 #[serde(
491 default,
492 deserialize_with = "deserialize_optional_decimal_from_str",
493 serialize_with = "serialize_optional_decimal_as_str"
494 )]
495 pub position_initial_margin: Option<Decimal>,
496 #[serde(
498 default,
499 deserialize_with = "deserialize_optional_decimal_from_str",
500 serialize_with = "serialize_optional_decimal_as_str"
501 )]
502 pub open_order_initial_margin: Option<Decimal>,
503 #[serde(
505 default,
506 deserialize_with = "deserialize_optional_decimal_from_str",
507 serialize_with = "serialize_optional_decimal_as_str"
508 )]
509 pub cross_wallet_balance: Option<Decimal>,
510 #[serde(
512 default,
513 deserialize_with = "deserialize_optional_decimal_from_str",
514 serialize_with = "serialize_optional_decimal_as_str"
515 )]
516 pub cross_un_pnl: Option<Decimal>,
517 #[serde(
519 deserialize_with = "deserialize_decimal_or_zero",
520 serialize_with = "serialize_decimal_as_str"
521 )]
522 pub available_balance: Decimal,
523 #[serde(
525 default,
526 deserialize_with = "deserialize_optional_decimal_from_str",
527 serialize_with = "serialize_optional_decimal_as_str"
528 )]
529 pub max_withdraw_amount: Option<Decimal>,
530 #[serde(default)]
532 pub margin_available: Option<bool>,
533 pub update_time: i64,
535 #[serde(
537 default,
538 deserialize_with = "deserialize_optional_decimal_from_str",
539 serialize_with = "serialize_optional_decimal_as_str"
540 )]
541 pub withdraw_available: Option<Decimal>,
542}
543
544#[derive(Clone, Debug, Serialize, Deserialize)]
546#[serde(rename_all = "camelCase")]
547pub struct BinanceAccountPosition {
548 pub symbol: Ustr,
550 #[serde(default)]
552 pub initial_margin: Option<String>,
553 #[serde(default)]
555 pub maint_margin: Option<String>,
556 #[serde(default)]
558 pub unrealized_profit: Option<String>,
559 #[serde(default)]
561 pub position_initial_margin: Option<String>,
562 #[serde(default)]
564 pub open_order_initial_margin: Option<String>,
565 #[serde(default)]
567 pub leverage: Option<String>,
568 #[serde(default)]
570 pub isolated: Option<bool>,
571 #[serde(default)]
573 pub entry_price: Option<String>,
574 #[serde(default)]
576 pub max_notional: Option<String>,
577 #[serde(default)]
579 pub bid_notional: Option<String>,
580 #[serde(default)]
582 pub ask_notional: Option<String>,
583 #[serde(default)]
585 pub position_side: Option<BinancePositionSide>,
586 #[serde(default)]
588 pub position_amt: Option<String>,
589 #[serde(default)]
591 pub update_time: Option<i64>,
592}
593
594#[derive(Clone, Debug, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct BinancePositionRisk {
598 pub symbol: Ustr,
600 pub position_amt: String,
602 pub entry_price: String,
604 pub mark_price: String,
606 #[serde(default)]
608 pub un_realized_profit: Option<String>,
609 #[serde(default)]
611 pub liquidation_price: Option<String>,
612 pub leverage: String,
614 #[serde(default)]
616 pub max_notional_value: Option<String>,
617 #[serde(default)]
619 pub margin_type: Option<BinanceMarginType>,
620 #[serde(default)]
622 pub isolated_margin: Option<String>,
623 #[serde(default)]
625 pub is_auto_add_margin: Option<String>,
626 #[serde(default)]
628 pub position_side: Option<BinancePositionSide>,
629 #[serde(default)]
631 pub notional: Option<String>,
632 #[serde(default)]
634 pub isolated_wallet: Option<String>,
635 #[serde(default)]
637 pub adl_quantile: Option<u8>,
638 #[serde(default)]
640 pub update_time: Option<i64>,
641 #[serde(default)]
643 pub break_even_price: Option<String>,
644 #[serde(default)]
646 pub bust_price: Option<String>,
647}
648
649#[derive(Clone, Debug, Serialize, Deserialize)]
651#[serde(rename_all = "camelCase")]
652pub struct BinanceIncomeRecord {
653 #[serde(default)]
655 pub symbol: Option<Ustr>,
656 pub income_type: BinanceIncomeType,
658 pub income: String,
660 pub asset: Ustr,
662 pub time: i64,
664 #[serde(default)]
666 pub info: Option<String>,
667 #[serde(default)]
669 pub tran_id: Option<i64>,
670 #[serde(default)]
672 pub trade_id: Option<i64>,
673}
674
675#[derive(Clone, Debug, Serialize, Deserialize)]
677#[serde(rename_all = "camelCase")]
678pub struct BinanceUserTrade {
679 pub symbol: Ustr,
681 pub id: i64,
683 pub order_id: i64,
685 pub price: String,
687 pub qty: String,
689 #[serde(default)]
691 pub quote_qty: Option<String>,
692 pub realized_pnl: String,
694 pub side: BinanceSide,
696 #[serde(default)]
698 pub position_side: Option<BinancePositionSide>,
699 pub time: i64,
701 pub buyer: bool,
703 pub maker: bool,
705 #[serde(default)]
707 pub commission: Option<String>,
708 #[serde(default)]
710 pub commission_asset: Option<Ustr>,
711 #[serde(default)]
713 pub margin_asset: Option<Ustr>,
714}
715
716#[derive(Clone, Debug, Serialize, Deserialize)]
718#[serde(rename_all = "camelCase")]
719pub struct BinanceFuturesAccountInfo {
720 #[serde(
722 default,
723 deserialize_with = "deserialize_optional_decimal_from_str",
724 serialize_with = "serialize_optional_decimal_as_str"
725 )]
726 pub total_initial_margin: Option<Decimal>,
727 #[serde(
729 default,
730 deserialize_with = "deserialize_optional_decimal_from_str",
731 serialize_with = "serialize_optional_decimal_as_str"
732 )]
733 pub total_maint_margin: Option<Decimal>,
734 #[serde(
736 default,
737 deserialize_with = "deserialize_optional_decimal_from_str",
738 serialize_with = "serialize_optional_decimal_as_str"
739 )]
740 pub total_wallet_balance: Option<Decimal>,
741 #[serde(
743 default,
744 deserialize_with = "deserialize_optional_decimal_from_str",
745 serialize_with = "serialize_optional_decimal_as_str"
746 )]
747 pub total_unrealized_profit: Option<Decimal>,
748 #[serde(
750 default,
751 deserialize_with = "deserialize_optional_decimal_from_str",
752 serialize_with = "serialize_optional_decimal_as_str"
753 )]
754 pub total_margin_balance: Option<Decimal>,
755 #[serde(
757 default,
758 deserialize_with = "deserialize_optional_decimal_from_str",
759 serialize_with = "serialize_optional_decimal_as_str"
760 )]
761 pub total_position_initial_margin: Option<Decimal>,
762 #[serde(
764 default,
765 deserialize_with = "deserialize_optional_decimal_from_str",
766 serialize_with = "serialize_optional_decimal_as_str"
767 )]
768 pub total_open_order_initial_margin: Option<Decimal>,
769 #[serde(
771 default,
772 deserialize_with = "deserialize_optional_decimal_from_str",
773 serialize_with = "serialize_optional_decimal_as_str"
774 )]
775 pub total_cross_wallet_balance: Option<Decimal>,
776 #[serde(
778 default,
779 deserialize_with = "deserialize_optional_decimal_from_str",
780 serialize_with = "serialize_optional_decimal_as_str"
781 )]
782 pub total_cross_un_pnl: Option<Decimal>,
783 #[serde(
785 default,
786 deserialize_with = "deserialize_optional_decimal_from_str",
787 serialize_with = "serialize_optional_decimal_as_str"
788 )]
789 pub available_balance: Option<Decimal>,
790 #[serde(
792 default,
793 deserialize_with = "deserialize_optional_decimal_from_str",
794 serialize_with = "serialize_optional_decimal_as_str"
795 )]
796 pub max_withdraw_amount: Option<Decimal>,
797 #[serde(default)]
799 pub can_deposit: Option<bool>,
800 #[serde(default)]
802 pub can_trade: Option<bool>,
803 #[serde(default)]
805 pub can_withdraw: Option<bool>,
806 #[serde(default)]
808 pub multi_assets_margin: Option<bool>,
809 #[serde(default)]
811 pub update_time: Option<i64>,
812 #[serde(default)]
814 pub assets: Vec<BinanceFuturesBalance>,
815 #[serde(default)]
817 pub positions: Vec<BinanceAccountPosition>,
818}
819
820impl BinanceFuturesAccountInfo {
821 pub fn to_account_state(
827 &self,
828 account_id: AccountId,
829 ts_init: UnixNanos,
830 ) -> anyhow::Result<AccountState> {
831 let mut balances = Vec::with_capacity(self.assets.len());
832
833 for asset in &self.assets {
834 let currency = Currency::get_or_create_crypto_with_context(
835 asset.asset.as_str(),
836 Some("futures balance"),
837 );
838
839 let balance = AccountBalance::from_total_and_free(
840 asset.wallet_balance,
841 asset.available_balance,
842 currency,
843 )
844 .context("failed to build account balance")?;
845 balances.push(balance);
846 }
847
848 if balances.is_empty() {
850 let zero_currency = Currency::USDT();
851 let zero_money = Money::new(0.0, zero_currency);
852 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
853 balances.push(zero_balance);
854 }
855
856 let mut margins = Vec::new();
861
862 for asset in &self.assets {
863 let initial_dec = asset.initial_margin.unwrap_or_default();
864 let maint_dec = asset.maint_margin.unwrap_or_default();
865
866 if initial_dec.is_zero() && maint_dec.is_zero() {
867 continue;
868 }
869
870 let currency = Currency::get_or_create_crypto_with_context(
871 asset.asset.as_str(),
872 Some("futures margin"),
873 );
874 let initial = Money::from_decimal(initial_dec, currency)
875 .unwrap_or_else(|_| Money::zero(currency));
876 let maintenance =
877 Money::from_decimal(maint_dec, currency).unwrap_or_else(|_| Money::zero(currency));
878 margins.push(MarginBalance::new(initial, maintenance, None));
879 }
880
881 let ts_event = self
882 .update_time
883 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
884
885 Ok(AccountState::new(
886 account_id,
887 AccountType::Margin,
888 balances,
889 margins,
890 true, UUID4::new(),
892 ts_event,
893 ts_init,
894 None,
895 ))
896 }
897}
898
899#[derive(Clone, Debug, Serialize, Deserialize)]
901#[serde(rename_all = "camelCase")]
902pub struct BinanceHedgeModeResponse {
903 pub dual_side_position: bool,
905}
906
907#[derive(Clone, Debug, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct BinanceLeverageResponse {
911 pub symbol: Ustr,
913 pub leverage: u32,
915 #[serde(default)]
917 pub max_notional_value: Option<String>,
918}
919
920#[derive(Clone, Debug, Serialize, Deserialize)]
922#[serde(rename_all = "camelCase")]
923pub struct BinanceCancelAllOrdersResponse {
924 pub code: i32,
926 pub msg: String,
928}
929
930#[derive(Clone, Debug, Serialize, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub struct BinanceFuturesOrder {
934 pub symbol: Ustr,
936 pub order_id: i64,
938 pub client_order_id: String,
940 pub orig_qty: String,
942 pub executed_qty: String,
944 pub cum_quote: String,
946 pub price: String,
948 #[serde(default)]
950 pub avg_price: Option<String>,
951 #[serde(default)]
953 pub stop_price: Option<String>,
954 pub status: BinanceOrderStatus,
956 pub time_in_force: BinanceTimeInForce,
958 #[serde(rename = "type")]
960 pub order_type: BinanceFuturesOrderType,
961 #[serde(default)]
963 pub orig_type: Option<BinanceFuturesOrderType>,
964 pub side: BinanceSide,
966 #[serde(default)]
968 pub position_side: Option<BinancePositionSide>,
969 #[serde(default)]
971 pub reduce_only: Option<bool>,
972 #[serde(default)]
974 pub close_position: Option<bool>,
975 #[serde(default)]
977 pub activate_price: Option<String>,
978 #[serde(default)]
980 pub price_rate: Option<String>,
981 #[serde(default)]
983 pub working_type: Option<BinanceWorkingType>,
984 #[serde(default)]
986 pub price_protect: Option<bool>,
987 #[serde(default)]
989 pub is_isolated: Option<bool>,
990 #[serde(default)]
992 pub good_till_date: Option<i64>,
993 #[serde(default)]
995 pub price_match: Option<BinancePriceMatch>,
996 #[serde(default)]
998 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
999 #[serde(default)]
1001 pub update_time: Option<i64>,
1002 #[serde(default)]
1004 pub working_type_id: Option<i64>,
1005}
1006
1007impl BinanceFuturesOrder {
1008 pub fn to_order_status_report(
1014 &self,
1015 account_id: AccountId,
1016 instrument_id: InstrumentId,
1017 size_precision: u8,
1018 treat_expired_as_canceled: bool,
1019 ts_init: UnixNanos,
1020 ) -> anyhow::Result<OrderStatusReport> {
1021 let ts_event = self
1022 .update_time
1023 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
1024
1025 let client_order_id = ClientOrderId::new(decode_broker_id(
1026 &self.client_order_id,
1027 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1028 ));
1029 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1030
1031 let order_side = match self.side {
1032 BinanceSide::Buy => OrderSide::Buy,
1033 BinanceSide::Sell => OrderSide::Sell,
1034 };
1035
1036 let order_type = self.order_type.to_nautilus_order_type();
1037 let time_in_force = self.time_in_force.to_nautilus_time_in_force();
1038 let order_status = self
1039 .status
1040 .to_nautilus_order_status(treat_expired_as_canceled);
1041
1042 let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
1043 let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
1044
1045 Ok(OrderStatusReport::new(
1046 account_id,
1047 instrument_id,
1048 Some(client_order_id),
1049 venue_order_id,
1050 order_side,
1051 order_type,
1052 time_in_force,
1053 order_status,
1054 Quantity::new(quantity.to_string().parse()?, size_precision),
1055 Quantity::new(filled_qty.to_string().parse()?, size_precision),
1056 ts_event,
1057 ts_event,
1058 ts_init,
1059 Some(UUID4::new()),
1060 ))
1061 }
1062}
1063
1064impl BinanceFuturesOrderType {
1065 #[must_use]
1067 pub fn is_post_only(&self) -> bool {
1068 false }
1070
1071 #[must_use]
1073 pub fn to_nautilus_order_type(&self) -> OrderType {
1074 match self {
1075 Self::Market => OrderType::Market,
1076 Self::Limit => OrderType::Limit,
1077 Self::Stop => OrderType::StopLimit,
1078 Self::StopMarket => OrderType::StopMarket,
1079 Self::TakeProfit => OrderType::LimitIfTouched,
1080 Self::TakeProfitMarket => OrderType::MarketIfTouched,
1081 Self::TrailingStopMarket => OrderType::TrailingStopMarket,
1082 Self::Liquidation | Self::Adl => OrderType::Market, Self::Unknown => OrderType::Market,
1084 }
1085 }
1086}
1087
1088impl BinanceTimeInForce {
1089 #[must_use]
1091 pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1092 match self {
1093 Self::Gtc => TimeInForce::Gtc,
1094 Self::Ioc => TimeInForce::Ioc,
1095 Self::Fok => TimeInForce::Fok,
1096 Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
1098 Self::Rpi => TimeInForce::Ioc, Self::Unknown => TimeInForce::Gtc, }
1101 }
1102}
1103
1104impl BinanceOrderStatus {
1105 #[must_use]
1107 pub fn to_nautilus_order_status(&self, treat_expired_as_canceled: bool) -> OrderStatus {
1108 match self {
1109 Self::New | Self::PendingNew => OrderStatus::Accepted,
1110 Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1111 Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
1112 Self::Canceled => OrderStatus::Canceled,
1113 Self::PendingCancel => OrderStatus::PendingCancel,
1114 Self::Rejected => OrderStatus::Rejected,
1115 Self::Expired | Self::ExpiredInMatch => {
1116 if treat_expired_as_canceled {
1117 OrderStatus::Canceled
1118 } else {
1119 OrderStatus::Expired
1120 }
1121 }
1122 Self::Unknown => OrderStatus::Initialized,
1123 }
1124 }
1125}
1126
1127impl BinanceUserTrade {
1128 pub fn to_fill_report(
1134 &self,
1135 account_id: AccountId,
1136 instrument_id: InstrumentId,
1137 price_precision: u8,
1138 size_precision: u8,
1139 ts_init: UnixNanos,
1140 ) -> anyhow::Result<FillReport> {
1141 let ts_event = UnixNanos::from_millis(self.time as u64);
1142
1143 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1144 let trade_id = TradeId::new(self.id.to_string());
1145
1146 let order_side = match self.side {
1147 BinanceSide::Buy => OrderSide::Buy,
1148 BinanceSide::Sell => OrderSide::Sell,
1149 };
1150
1151 let liquidity_side = if self.maker {
1152 LiquiditySide::Maker
1153 } else {
1154 LiquiditySide::Taker
1155 };
1156
1157 let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1158 let last_px: Decimal = self.price.parse().context("invalid price")?;
1159
1160 let commission = {
1161 let comm_val: f64 = self
1162 .commission
1163 .as_ref()
1164 .and_then(|c| c.parse().ok())
1165 .unwrap_or(0.0);
1166 let comm_asset = self
1167 .commission_asset
1168 .as_ref()
1169 .map_or_else(Currency::USDT, Currency::from);
1170 Money::new(comm_val, comm_asset)
1171 };
1172
1173 Ok(FillReport::new(
1174 account_id,
1175 instrument_id,
1176 venue_order_id,
1177 trade_id,
1178 order_side,
1179 Quantity::new(last_qty.to_string().parse()?, size_precision),
1180 Price::new(last_px.to_string().parse()?, price_precision),
1181 commission,
1182 liquidity_side,
1183 None, None, ts_event,
1186 ts_init,
1187 Some(UUID4::new()),
1188 ))
1189 }
1190}
1191
1192#[derive(Clone, Debug, Deserialize)]
1196#[serde(untagged)]
1197pub enum BatchOrderResult {
1198 Success(Box<BinanceFuturesOrder>),
1200 Error(BatchOrderError),
1202}
1203
1204#[derive(Clone, Debug, Deserialize)]
1206pub struct BatchOrderError {
1207 pub code: i64,
1209 pub msg: String,
1211}
1212
1213#[derive(Debug, Clone, Deserialize)]
1215#[serde(rename_all = "camelCase")]
1216pub struct ListenKeyResponse {
1217 pub listen_key: String,
1219}
1220
1221#[derive(Clone, Debug, Serialize, Deserialize)]
1231#[serde(rename_all = "camelCase")]
1232pub struct BinanceFuturesAlgoOrder {
1233 pub algo_id: i64,
1235 pub client_algo_id: String,
1237 pub algo_type: BinanceAlgoType,
1239 #[serde(rename = "orderType", alias = "type")]
1241 pub order_type: BinanceFuturesOrderType,
1242 pub symbol: Ustr,
1244 pub side: BinanceSide,
1246 #[serde(default)]
1248 pub position_side: Option<BinancePositionSide>,
1249 #[serde(default)]
1251 pub time_in_force: Option<BinanceTimeInForce>,
1252 #[serde(default)]
1254 pub quantity: Option<String>,
1255 #[serde(default)]
1257 pub algo_status: Option<BinanceAlgoStatus>,
1258 #[serde(default)]
1260 pub trigger_price: Option<String>,
1261 #[serde(default)]
1263 pub price: Option<String>,
1264 #[serde(default)]
1266 pub working_type: Option<BinanceWorkingType>,
1267 #[serde(default)]
1269 pub close_position: Option<bool>,
1270 #[serde(default)]
1272 pub price_protect: Option<bool>,
1273 #[serde(default)]
1275 pub reduce_only: Option<bool>,
1276 #[serde(default)]
1278 pub activate_price: Option<String>,
1279 #[serde(default)]
1281 pub callback_rate: Option<String>,
1282 #[serde(default)]
1284 pub create_time: Option<i64>,
1285 #[serde(default)]
1287 pub update_time: Option<i64>,
1288 #[serde(default)]
1290 pub trigger_time: Option<i64>,
1291 #[serde(default)]
1293 pub actual_order_id: Option<String>,
1294 #[serde(default)]
1296 pub executed_qty: Option<String>,
1297 #[serde(default)]
1299 pub avg_price: Option<String>,
1300}
1301
1302impl BinanceFuturesAlgoOrder {
1303 pub fn to_order_status_report(
1309 &self,
1310 account_id: AccountId,
1311 instrument_id: InstrumentId,
1312 size_precision: u8,
1313 ts_init: UnixNanos,
1314 ) -> anyhow::Result<OrderStatusReport> {
1315 let ts_event = self
1316 .update_time
1317 .or(self.create_time)
1318 .map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
1319
1320 let client_order_id = ClientOrderId::new(decode_broker_id(
1321 &self.client_algo_id,
1322 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1323 ));
1324 let venue_order_id = self.actual_order_id.as_ref().map_or_else(
1325 || VenueOrderId::new(self.algo_id.to_string()),
1326 |id| VenueOrderId::new(id.clone()),
1327 );
1328
1329 let order_side = match self.side {
1330 BinanceSide::Buy => OrderSide::Buy,
1331 BinanceSide::Sell => OrderSide::Sell,
1332 };
1333
1334 let order_type = self.parse_order_type();
1335 let time_in_force = self
1336 .time_in_force
1337 .as_ref()
1338 .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1339 let order_status = self.parse_order_status();
1340
1341 let quantity: Decimal = self
1342 .quantity
1343 .as_ref()
1344 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1345 .context("invalid quantity")?;
1346 let filled_qty: Decimal = self
1347 .executed_qty
1348 .as_ref()
1349 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1350 .context("invalid executed_qty")?;
1351
1352 Ok(OrderStatusReport::new(
1353 account_id,
1354 instrument_id,
1355 Some(client_order_id),
1356 venue_order_id,
1357 order_side,
1358 order_type,
1359 time_in_force,
1360 order_status,
1361 Quantity::new(quantity.to_string().parse()?, size_precision),
1362 Quantity::new(filled_qty.to_string().parse()?, size_precision),
1363 ts_event,
1364 ts_event,
1365 ts_init,
1366 Some(UUID4::new()),
1367 ))
1368 }
1369
1370 fn parse_order_type(&self) -> OrderType {
1371 self.order_type.into()
1372 }
1373
1374 fn parse_order_status(&self) -> OrderStatus {
1375 match self.algo_status {
1376 Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1377 Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1378 Some(BinanceAlgoStatus::Triggered) => OrderStatus::Accepted,
1379 Some(BinanceAlgoStatus::Finished) => {
1380 if let Some(qty) = &self.executed_qty
1382 && let Ok(dec) = qty.parse::<Decimal>()
1383 && !dec.is_zero()
1384 {
1385 return OrderStatus::Filled;
1386 }
1387 OrderStatus::Canceled
1388 }
1389 Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1390 Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1391 Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1392 Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1393 }
1394 }
1395}
1396
1397#[derive(Clone, Debug, Deserialize)]
1399#[serde(rename_all = "camelCase")]
1400pub struct BinanceFuturesAlgoOrderCancelResponse {
1401 pub algo_id: i64,
1403 pub client_algo_id: String,
1405 pub code: String,
1407 pub msg: String,
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use rstest::rstest;
1414
1415 use super::*;
1416 use crate::common::testing::load_fixture_string;
1417
1418 #[rstest]
1419 fn test_parse_account_info_v2() {
1420 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1421 let account: BinanceFuturesAccountInfo =
1422 serde_json::from_str(&json).expect("Failed to parse account info");
1423
1424 assert_eq!(
1425 account.total_wallet_balance,
1426 Some(Decimal::from_str_exact("23.72469206").unwrap())
1427 );
1428 assert_eq!(account.assets.len(), 1);
1429 assert_eq!(account.assets[0].asset.as_str(), "USDT");
1430 assert_eq!(
1431 account.assets[0].wallet_balance,
1432 Decimal::from_str_exact("23.72469206").unwrap()
1433 );
1434 assert_eq!(account.positions.len(), 1);
1435 assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1436 assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1437 }
1438
1439 #[rstest]
1440 fn test_account_info_to_account_state_zero_margins() {
1441 let json = load_fixture_string("futures/http_json/account_info_v2.json");
1442 let account: BinanceFuturesAccountInfo =
1443 serde_json::from_str(&json).expect("Failed to parse account info");
1444
1445 let account_id = AccountId::from("BINANCE-001");
1446 let ts_init = UnixNanos::from(1_000_000_000u64);
1447 let state = account.to_account_state(account_id, ts_init).unwrap();
1448
1449 assert_eq!(state.account_id, account_id);
1450 assert_eq!(state.account_type, AccountType::Margin);
1451 assert!(!state.balances.is_empty());
1452 assert_eq!(state.margins.len(), 0);
1453 }
1454
1455 #[rstest]
1456 fn test_account_info_to_account_state_with_margins() {
1457 let json = r#"{
1458 "totalInitialMargin": "500.25000000",
1459 "totalMaintMargin": "250.75000000",
1460 "totalWalletBalance": "10000.00000000",
1461 "assets": [{
1462 "asset": "USDT",
1463 "walletBalance": "10000.00000000",
1464 "availableBalance": "9500.00000000",
1465 "initialMargin": "500.25000000",
1466 "maintMargin": "250.75000000",
1467 "updateTime": 1617939110373
1468 }],
1469 "positions": []
1470 }"#;
1471 let account: BinanceFuturesAccountInfo =
1472 serde_json::from_str(json).expect("Failed to parse account info");
1473
1474 let account_id = AccountId::from("BINANCE-001");
1475 let ts_init = UnixNanos::from(1_000_000_000u64);
1476 let state = account.to_account_state(account_id, ts_init).unwrap();
1477
1478 assert_eq!(state.margins.len(), 1);
1479 let margin = &state.margins[0];
1480 assert!(margin.instrument_id.is_none());
1481 assert_eq!(margin.currency.code.as_str(), "USDT");
1482 assert_eq!(margin.initial.as_f64(), 500.25);
1483 assert_eq!(margin.maintenance.as_f64(), 250.75);
1484 }
1485
1486 #[rstest]
1487 fn test_account_info_to_account_state_coin_margined_per_base_coin() {
1488 let json = r#"{
1489 "totalWalletBalance": "0.00000000",
1490 "assets": [
1491 {
1492 "asset": "BTC",
1493 "walletBalance": "1.50000000",
1494 "availableBalance": "1.40000000",
1495 "initialMargin": "0.05000000",
1496 "maintMargin": "0.02500000",
1497 "updateTime": 1617939110373
1498 },
1499 {
1500 "asset": "ETH",
1501 "walletBalance": "10.00000000",
1502 "availableBalance": "9.00000000",
1503 "initialMargin": "0.80000000",
1504 "maintMargin": "0.40000000",
1505 "updateTime": 1617939110373
1506 }
1507 ],
1508 "positions": []
1509 }"#;
1510 let account: BinanceFuturesAccountInfo =
1511 serde_json::from_str(json).expect("Failed to parse account info");
1512
1513 let account_id = AccountId::from("BINANCE-001");
1514 let ts_init = UnixNanos::from(1_000_000_000u64);
1515 let state = account.to_account_state(account_id, ts_init).unwrap();
1516
1517 assert_eq!(state.margins.len(), 2);
1518 assert!(state.margins.iter().all(|m| m.instrument_id.is_none()));
1519 let btc = state
1520 .margins
1521 .iter()
1522 .find(|m| m.currency.code.as_str() == "BTC")
1523 .expect("BTC margin missing");
1524 assert_eq!(btc.initial.as_f64(), 0.05);
1525 assert_eq!(btc.maintenance.as_f64(), 0.025);
1526 let eth = state
1527 .margins
1528 .iter()
1529 .find(|m| m.currency.code.as_str() == "ETH")
1530 .expect("ETH margin missing");
1531 assert_eq!(eth.initial.as_f64(), 0.8);
1532 assert_eq!(eth.maintenance.as_f64(), 0.4);
1533 }
1534
1535 #[rstest]
1540 fn test_account_info_to_account_state_precision_drift() {
1541 let json = r#"{
1542 "assets": [{
1543 "asset": "USDT",
1544 "walletBalance": "10.000000034999",
1545 "availableBalance": "9.999999994999",
1546 "updateTime": 1617939110373
1547 }],
1548 "positions": []
1549 }"#;
1550 let account: BinanceFuturesAccountInfo =
1551 serde_json::from_str(json).expect("Failed to parse account info");
1552
1553 let account_id = AccountId::from("BINANCE-001");
1554 let ts_init = UnixNanos::from(1_000_000_000u64);
1555 let state = account.to_account_state(account_id, ts_init).unwrap();
1556
1557 assert_eq!(state.balances.len(), 1);
1558 let balance = &state.balances[0];
1559 assert_eq!(balance.total.raw, balance.locked.raw + balance.free.raw);
1560 }
1561
1562 #[rstest]
1563 fn test_account_info_to_account_state_empty_balance() {
1564 let json = r#"{
1566 "assets": [{
1567 "asset": "USDT",
1568 "walletBalance": "",
1569 "availableBalance": "",
1570 "updateTime": 0
1571 }],
1572 "positions": []
1573 }"#;
1574 let account: BinanceFuturesAccountInfo =
1575 serde_json::from_str(json).expect("Failed to parse account info");
1576
1577 let account_id = AccountId::from("BINANCE-001");
1578 let ts_init = UnixNanos::from(1_000_000_000u64);
1579 let state = account.to_account_state(account_id, ts_init).unwrap();
1580
1581 assert_eq!(state.balances.len(), 1);
1582 let balance = &state.balances[0];
1583 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1584 assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
1585 assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
1586 }
1587
1588 #[rstest]
1589 fn test_account_info_to_account_state_empty_assets() {
1590 let json = r#"{
1592 "assets": [],
1593 "positions": []
1594 }"#;
1595 let account: BinanceFuturesAccountInfo =
1596 serde_json::from_str(json).expect("Failed to parse account info");
1597
1598 let account_id = AccountId::from("BINANCE-001");
1599 let ts_init = UnixNanos::from(1_000_000_000u64);
1600 let state = account.to_account_state(account_id, ts_init).unwrap();
1601
1602 assert_eq!(state.balances.len(), 1);
1603 let balance = &state.balances[0];
1604 assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
1605 }
1606
1607 #[rstest]
1608 fn test_parse_position_risk() {
1609 let json = load_fixture_string("futures/http_json/position_risk.json");
1610 let positions: Vec<BinancePositionRisk> =
1611 serde_json::from_str(&json).expect("Failed to parse position risk");
1612
1613 assert_eq!(positions.len(), 1);
1614 assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
1615 assert_eq!(positions[0].position_amt, "0.001");
1616 assert_eq!(positions[0].mark_price, "51000.0");
1617 assert_eq!(positions[0].leverage, "20");
1618 }
1619
1620 #[rstest]
1621 fn test_parse_balance_with_v1_field() {
1622 let json = load_fixture_string("futures/http_json/balance.json");
1624 let balances: Vec<BinanceFuturesBalance> =
1625 serde_json::from_str(&json).expect("Failed to parse balance");
1626
1627 assert_eq!(balances.len(), 1);
1628 assert_eq!(balances[0].asset.as_str(), "USDT");
1629 assert_eq!(
1631 balances[0].wallet_balance,
1632 Decimal::from_str_exact("122.12345678").unwrap()
1633 );
1634 assert_eq!(
1635 balances[0].available_balance,
1636 Decimal::from_str_exact("122.12345678").unwrap()
1637 );
1638 }
1639
1640 #[rstest]
1641 fn test_parse_balance_with_v2_field() {
1642 let json = r#"{
1644 "asset": "USDT",
1645 "walletBalance": "100.00000000",
1646 "availableBalance": "100.00000000",
1647 "updateTime": 1617939110373
1648 }"#;
1649
1650 let balance: BinanceFuturesBalance =
1651 serde_json::from_str(json).expect("Failed to parse balance");
1652
1653 assert_eq!(balance.asset.as_str(), "USDT");
1654 assert_eq!(
1655 balance.wallet_balance,
1656 Decimal::from_str_exact("100.00000000").unwrap()
1657 );
1658 }
1659
1660 #[rstest]
1661 fn test_parse_order() {
1662 let json = load_fixture_string("futures/http_json/order_response.json");
1663 let order: BinanceFuturesOrder =
1664 serde_json::from_str(&json).expect("Failed to parse order");
1665
1666 assert_eq!(order.order_id, 12345678);
1667 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1668 assert_eq!(order.status, BinanceOrderStatus::New);
1669 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
1670 assert_eq!(order.side, BinanceSide::Buy);
1671 assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
1672 assert_eq!(order.price_match, Some(BinancePriceMatch::None));
1673 assert_eq!(
1674 order.self_trade_prevention_mode,
1675 Some(BinanceSelfTradePreventionMode::None)
1676 );
1677 }
1678
1679 #[rstest]
1680 fn test_parse_hedge_mode_response() {
1681 let json = r#"{"dualSidePosition": true}"#;
1682 let response: BinanceHedgeModeResponse =
1683 serde_json::from_str(json).expect("Failed to parse hedge mode");
1684 assert!(response.dual_side_position);
1685 }
1686
1687 #[rstest]
1688 fn test_parse_leverage_response() {
1689 let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
1690 let response: BinanceLeverageResponse =
1691 serde_json::from_str(json).expect("Failed to parse leverage");
1692 assert_eq!(response.symbol.as_str(), "BTCUSDT");
1693 assert_eq!(response.leverage, 20);
1694 }
1695
1696 #[rstest]
1697 fn test_parse_listen_key_response() {
1698 let json =
1699 r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
1700 let response: ListenKeyResponse =
1701 serde_json::from_str(json).expect("Failed to parse listen key");
1702 assert!(!response.listen_key.is_empty());
1703 }
1704
1705 #[rstest]
1706 fn test_parse_account_position() {
1707 let json = r#"{
1708 "symbol": "ETHUSDT",
1709 "initialMargin": "100.00",
1710 "maintMargin": "50.00",
1711 "unrealizedProfit": "10.00",
1712 "positionInitialMargin": "100.00",
1713 "openOrderInitialMargin": "0",
1714 "leverage": "10",
1715 "isolated": true,
1716 "entryPrice": "2000.00",
1717 "maxNotional": "100000",
1718 "bidNotional": "0",
1719 "askNotional": "0",
1720 "positionSide": "LONG",
1721 "positionAmt": "0.5",
1722 "updateTime": 1625474304765
1723 }"#;
1724
1725 let position: BinanceAccountPosition =
1726 serde_json::from_str(json).expect("Failed to parse account position");
1727
1728 assert_eq!(position.symbol.as_str(), "ETHUSDT");
1729 assert_eq!(position.leverage, Some("10".to_string()));
1730 assert_eq!(position.isolated, Some(true));
1731 assert_eq!(position.position_side, Some(BinancePositionSide::Long));
1732 }
1733
1734 #[rstest]
1735 fn test_parse_algo_order() {
1736 let json = r#"{
1737 "algoId": 123456789,
1738 "clientAlgoId": "test-algo-order-1",
1739 "algoType": "CONDITIONAL",
1740 "type": "STOP_MARKET",
1741 "symbol": "BTCUSDT",
1742 "side": "BUY",
1743 "positionSide": "BOTH",
1744 "timeInForce": "GTC",
1745 "quantity": "0.001",
1746 "algoStatus": "NEW",
1747 "triggerPrice": "45000.00",
1748 "workingType": "MARK_PRICE",
1749 "reduceOnly": false,
1750 "createTime": 1625474304765,
1751 "updateTime": 1625474304765
1752 }"#;
1753
1754 let order: BinanceFuturesAlgoOrder =
1755 serde_json::from_str(json).expect("Failed to parse algo order");
1756
1757 assert_eq!(order.algo_id, 123456789);
1758 assert_eq!(order.client_algo_id, "test-algo-order-1");
1759 assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
1760 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
1761 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1762 assert_eq!(order.side, BinanceSide::Buy);
1763 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
1764 assert_eq!(order.trigger_price, Some("45000.00".to_string()));
1765 }
1766
1767 #[rstest]
1768 fn test_parse_algo_order_triggered() {
1769 let json = r#"{
1770 "algoId": 123456789,
1771 "clientAlgoId": "test-algo-order-2",
1772 "algoType": "CONDITIONAL",
1773 "type": "TAKE_PROFIT",
1774 "symbol": "ETHUSDT",
1775 "side": "SELL",
1776 "algoStatus": "TRIGGERED",
1777 "triggerPrice": "2500.00",
1778 "price": "2500.00",
1779 "actualOrderId": "987654321",
1780 "executedQty": "0.5",
1781 "avgPrice": "2499.50"
1782 }"#;
1783
1784 let order: BinanceFuturesAlgoOrder =
1785 serde_json::from_str(json).expect("Failed to parse triggered algo order");
1786
1787 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Triggered));
1788 assert_eq!(order.order_type, BinanceFuturesOrderType::TakeProfit);
1789 assert_eq!(order.actual_order_id, Some("987654321".to_string()));
1790 assert_eq!(order.executed_qty, Some("0.5".to_string()));
1791 }
1792
1793 #[rstest]
1794 fn test_parse_algo_order_cancel_response() {
1795 let json = r#"{
1796 "algoId": 123456789,
1797 "clientAlgoId": "test-algo-order-1",
1798 "code": "200",
1799 "msg": "success"
1800 }"#;
1801
1802 let response: BinanceFuturesAlgoOrderCancelResponse =
1803 serde_json::from_str(json).expect("Failed to parse algo cancel response");
1804
1805 assert_eq!(response.algo_id, 123456789);
1806 assert_eq!(response.client_algo_id, "test-algo-order-1");
1807 assert_eq!(response.code, "200");
1808 assert_eq!(response.msg, "success");
1809 }
1810
1811 #[rstest]
1812 fn test_order_to_report_decodes_broker_id() {
1813 let json = r#"{
1814 "orderId": 12345678,
1815 "symbol": "BTCUSDT",
1816 "status": "NEW",
1817 "clientOrderId": "x-aHRE4BCj-T0000000000000",
1818 "price": "50000.00",
1819 "avgPrice": "0.00",
1820 "origQty": "0.001",
1821 "executedQty": "0.000",
1822 "cumQuote": "0.00",
1823 "timeInForce": "GTC",
1824 "type": "LIMIT",
1825 "reduceOnly": false,
1826 "closePosition": false,
1827 "side": "BUY",
1828 "positionSide": "BOTH",
1829 "stopPrice": "0.00",
1830 "workingType": "CONTRACT_PRICE",
1831 "priceProtect": false,
1832 "origType": "LIMIT",
1833 "priceMatch": "NONE",
1834 "selfTradePreventionMode": "NONE",
1835 "goodTillDate": 0,
1836 "time": 1625474304765,
1837 "updateTime": 1625474304765
1838 }"#;
1839
1840 let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
1841 let account_id = AccountId::from("BINANCE-FUTURES-001");
1842 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1843 let ts_init = UnixNanos::from(1_000_000_000u64);
1844
1845 let report = order
1846 .to_order_status_report(account_id, instrument_id, 3, false, ts_init)
1847 .unwrap();
1848
1849 assert_eq!(
1850 report.client_order_id,
1851 Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
1852 );
1853 }
1854
1855 #[rstest]
1856 fn test_algo_order_to_report_decodes_broker_id() {
1857 let json = r#"{
1858 "algoId": 123456789,
1859 "clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
1860 "algoType": "CONDITIONAL",
1861 "type": "STOP_MARKET",
1862 "symbol": "BTCUSDT",
1863 "side": "BUY",
1864 "positionSide": "BOTH",
1865 "timeInForce": "GTC",
1866 "quantity": "0.001",
1867 "algoStatus": "NEW",
1868 "triggerPrice": "45000.00",
1869 "workingType": "MARK_PRICE",
1870 "reduceOnly": false,
1871 "createTime": 1625474304765,
1872 "updateTime": 1625474304765
1873 }"#;
1874
1875 let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
1876 let account_id = AccountId::from("BINANCE-FUTURES-001");
1877 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1878 let ts_init = UnixNanos::from(1_000_000_000u64);
1879
1880 let report = order
1881 .to_order_status_report(account_id, instrument_id, 3, ts_init)
1882 .unwrap();
1883
1884 assert_eq!(
1885 report.client_order_id,
1886 Some(ClientOrderId::from("my-algo-order-1")),
1887 );
1888 }
1889
1890 #[rstest]
1891 #[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
1892 #[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
1893 #[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
1894 #[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
1895 fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
1896 #[case] status: BinanceOrderStatus,
1897 #[case] treat_expired_as_canceled: bool,
1898 #[case] expected: OrderStatus,
1899 ) {
1900 let result = status.to_nautilus_order_status(treat_expired_as_canceled);
1901 assert_eq!(result, expected);
1902 }
1903}