1use anyhow::Context;
19use nautilus_core::{UUID4, UnixNanos, time::get_atomic_clock_realtime};
20use nautilus_model::{
21 enums::{AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
22 events::AccountState,
23 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, Venue, VenueOrderId},
24 reports::{FillReport, OrderStatusReport},
25 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
26};
27use rust_decimal::Decimal;
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use ustr::Ustr;
31
32use crate::common::{
33 enums::{
34 BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
35 BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
36 BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
37 BinanceTradingStatus, BinanceWorkingType,
38 },
39 models::BinanceRateLimit,
40};
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct BinanceServerTime {
46 pub server_time: i64,
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BinanceFuturesTrade {
54 pub id: i64,
56 pub price: String,
58 pub qty: String,
60 pub quote_qty: String,
62 pub time: i64,
64 pub is_buyer_maker: bool,
66}
67
68#[derive(Clone, Debug)]
70pub struct BinanceFuturesKline {
71 pub open_time: i64,
73 pub open: String,
75 pub high: String,
77 pub low: String,
79 pub close: String,
81 pub volume: String,
83 pub close_time: i64,
85 pub quote_volume: String,
87 pub num_trades: i64,
89 pub taker_buy_base_volume: String,
91 pub taker_buy_quote_volume: String,
93}
94
95impl<'de> Deserialize<'de> for BinanceFuturesKline {
96 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97 where
98 D: serde::Deserializer<'de>,
99 {
100 let arr: Vec<Value> = Vec::deserialize(deserializer)?;
101 if arr.len() < 11 {
102 return Err(serde::de::Error::custom("Invalid kline array length"));
103 }
104
105 Ok(Self {
106 open_time: arr[0].as_i64().unwrap_or(0),
107 open: arr[1].as_str().unwrap_or("0").to_string(),
108 high: arr[2].as_str().unwrap_or("0").to_string(),
109 low: arr[3].as_str().unwrap_or("0").to_string(),
110 close: arr[4].as_str().unwrap_or("0").to_string(),
111 volume: arr[5].as_str().unwrap_or("0").to_string(),
112 close_time: arr[6].as_i64().unwrap_or(0),
113 quote_volume: arr[7].as_str().unwrap_or("0").to_string(),
114 num_trades: arr[8].as_i64().unwrap_or(0),
115 taker_buy_base_volume: arr[9].as_str().unwrap_or("0").to_string(),
116 taker_buy_quote_volume: arr[10].as_str().unwrap_or("0").to_string(),
117 })
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct BinanceFuturesUsdExchangeInfo {
125 pub timezone: String,
127 pub server_time: i64,
129 pub rate_limits: Vec<BinanceRateLimit>,
131 #[serde(default)]
133 pub exchange_filters: Vec<Value>,
134 #[serde(default)]
136 pub assets: Vec<BinanceFuturesAsset>,
137 pub symbols: Vec<BinanceFuturesUsdSymbol>,
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct BinanceFuturesAsset {
145 pub asset: Ustr,
147 pub margin_available: bool,
149 #[serde(default)]
151 pub auto_asset_exchange: Option<String>,
152}
153
154#[derive(Clone, Debug, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct BinanceFuturesUsdSymbol {
158 pub symbol: Ustr,
160 pub pair: Ustr,
162 pub contract_type: String,
164 pub delivery_date: i64,
166 pub onboard_date: i64,
168 pub status: BinanceTradingStatus,
170 pub maint_margin_percent: String,
172 pub required_margin_percent: String,
174 pub base_asset: Ustr,
176 pub quote_asset: Ustr,
178 pub margin_asset: Ustr,
180 pub price_precision: i32,
182 pub quantity_precision: i32,
184 pub base_asset_precision: i32,
186 pub quote_precision: i32,
188 #[serde(default)]
190 pub underlying_type: Option<String>,
191 #[serde(default)]
193 pub underlying_sub_type: Vec<String>,
194 #[serde(default)]
196 pub settle_plan: Option<i64>,
197 #[serde(default)]
199 pub trigger_protect: Option<String>,
200 #[serde(default)]
202 pub liquidation_fee: Option<String>,
203 #[serde(default)]
205 pub market_take_bound: Option<String>,
206 pub order_types: Vec<String>,
208 pub time_in_force: Vec<String>,
210 pub filters: Vec<Value>,
212}
213
214#[derive(Clone, Debug, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct BinanceFuturesCoinExchangeInfo {
218 pub timezone: String,
220 pub server_time: i64,
222 pub rate_limits: Vec<BinanceRateLimit>,
224 #[serde(default)]
226 pub exchange_filters: Vec<Value>,
227 pub symbols: Vec<BinanceFuturesCoinSymbol>,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct BinanceFuturesCoinSymbol {
235 pub symbol: Ustr,
237 pub pair: Ustr,
239 pub contract_type: String,
241 pub delivery_date: i64,
243 pub onboard_date: i64,
245 #[serde(default)]
247 pub contract_status: Option<BinanceContractStatus>,
248 pub contract_size: i64,
250 pub maint_margin_percent: String,
252 pub required_margin_percent: String,
254 pub base_asset: Ustr,
256 pub quote_asset: Ustr,
258 pub margin_asset: Ustr,
260 pub price_precision: i32,
262 pub quantity_precision: i32,
264 pub base_asset_precision: i32,
266 pub quote_precision: i32,
268 #[serde(default, rename = "equalQtyPrecision")]
270 pub equal_qty_precision: Option<i32>,
271 #[serde(default)]
273 pub trigger_protect: Option<String>,
274 #[serde(default)]
276 pub liquidation_fee: Option<String>,
277 #[serde(default)]
279 pub market_take_bound: Option<String>,
280 pub order_types: Vec<String>,
282 pub time_in_force: Vec<String>,
284 pub filters: Vec<Value>,
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct BinanceFuturesTicker24hr {
292 pub symbol: Ustr,
294 pub price_change: String,
296 pub price_change_percent: String,
298 pub weighted_avg_price: String,
300 pub last_price: String,
302 #[serde(default)]
304 pub last_qty: Option<String>,
305 pub open_price: String,
307 pub high_price: String,
309 pub low_price: String,
311 pub volume: String,
313 pub quote_volume: String,
315 pub open_time: i64,
317 pub close_time: i64,
319 #[serde(default)]
321 pub first_id: Option<i64>,
322 #[serde(default)]
324 pub last_id: Option<i64>,
325 #[serde(default)]
327 pub count: Option<i64>,
328}
329
330#[derive(Clone, Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct BinanceFuturesMarkPrice {
334 pub symbol: Ustr,
336 pub mark_price: String,
338 #[serde(default)]
340 pub index_price: Option<String>,
341 #[serde(default)]
343 pub estimated_settle_price: Option<String>,
344 #[serde(default)]
346 pub last_funding_rate: Option<String>,
347 #[serde(default)]
349 pub next_funding_time: Option<i64>,
350 #[serde(default)]
352 pub interest_rate: Option<String>,
353 pub time: i64,
355}
356
357#[derive(Clone, Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct BinanceOrderBook {
361 pub last_update_id: i64,
363 pub bids: Vec<(String, String)>,
365 pub asks: Vec<(String, String)>,
367 #[serde(default, rename = "E")]
369 pub event_time: Option<i64>,
370 #[serde(default, rename = "T")]
372 pub transaction_time: Option<i64>,
373}
374
375#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct BinanceBookTicker {
379 pub symbol: Ustr,
381 pub bid_price: String,
383 pub bid_qty: String,
385 pub ask_price: String,
387 pub ask_qty: String,
389 #[serde(default)]
391 pub time: Option<i64>,
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct BinancePriceTicker {
398 pub symbol: Ustr,
400 pub price: String,
402 #[serde(default)]
404 pub time: Option<i64>,
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct BinanceFundingRate {
411 pub symbol: Ustr,
413 pub funding_rate: String,
415 pub funding_time: i64,
417 #[serde(default)]
419 pub mark_price: Option<String>,
420 #[serde(default)]
422 pub index_price: Option<String>,
423}
424
425#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(rename_all = "camelCase")]
428pub struct BinanceOpenInterest {
429 pub symbol: Ustr,
431 pub open_interest: String,
433 pub time: i64,
435}
436
437#[derive(Clone, Debug, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct BinanceFuturesBalance {
441 #[serde(default)]
443 pub account_alias: Option<String>,
444 pub asset: Ustr,
446 #[serde(alias = "balance")]
448 pub wallet_balance: String,
449 #[serde(default)]
451 pub unrealized_profit: Option<String>,
452 #[serde(default)]
454 pub margin_balance: Option<String>,
455 #[serde(default)]
457 pub maint_margin: Option<String>,
458 #[serde(default)]
460 pub initial_margin: Option<String>,
461 #[serde(default)]
463 pub position_initial_margin: Option<String>,
464 #[serde(default)]
466 pub open_order_initial_margin: Option<String>,
467 #[serde(default)]
469 pub cross_wallet_balance: Option<String>,
470 #[serde(default)]
472 pub cross_un_pnl: Option<String>,
473 pub available_balance: String,
475 #[serde(default)]
477 pub max_withdraw_amount: Option<String>,
478 #[serde(default)]
480 pub margin_available: Option<bool>,
481 pub update_time: i64,
483 #[serde(default)]
485 pub withdraw_available: Option<String>,
486}
487
488#[derive(Clone, Debug, Serialize, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub struct BinanceAccountPosition {
492 pub symbol: Ustr,
494 #[serde(default)]
496 pub initial_margin: Option<String>,
497 #[serde(default)]
499 pub maint_margin: Option<String>,
500 #[serde(default)]
502 pub unrealized_profit: Option<String>,
503 #[serde(default)]
505 pub position_initial_margin: Option<String>,
506 #[serde(default)]
508 pub open_order_initial_margin: Option<String>,
509 #[serde(default)]
511 pub leverage: Option<String>,
512 #[serde(default)]
514 pub isolated: Option<bool>,
515 #[serde(default)]
517 pub entry_price: Option<String>,
518 #[serde(default)]
520 pub max_notional: Option<String>,
521 #[serde(default)]
523 pub bid_notional: Option<String>,
524 #[serde(default)]
526 pub ask_notional: Option<String>,
527 #[serde(default)]
529 pub position_side: Option<BinancePositionSide>,
530 #[serde(default)]
532 pub position_amt: Option<String>,
533 #[serde(default)]
535 pub update_time: Option<i64>,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct BinancePositionRisk {
542 pub symbol: Ustr,
544 pub position_amt: String,
546 pub entry_price: String,
548 pub mark_price: String,
550 #[serde(default)]
552 pub un_realized_profit: Option<String>,
553 #[serde(default)]
555 pub liquidation_price: Option<String>,
556 pub leverage: String,
558 #[serde(default)]
560 pub max_notional_value: Option<String>,
561 #[serde(default)]
563 pub margin_type: Option<BinanceMarginType>,
564 #[serde(default)]
566 pub isolated_margin: Option<String>,
567 #[serde(default)]
569 pub is_auto_add_margin: Option<String>,
570 #[serde(default)]
572 pub position_side: Option<BinancePositionSide>,
573 #[serde(default)]
575 pub notional: Option<String>,
576 #[serde(default)]
578 pub isolated_wallet: Option<String>,
579 #[serde(default)]
581 pub adl_quantile: Option<u8>,
582 #[serde(default)]
584 pub update_time: Option<i64>,
585 #[serde(default)]
587 pub break_even_price: Option<String>,
588 #[serde(default)]
590 pub bust_price: Option<String>,
591}
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
595#[serde(rename_all = "camelCase")]
596pub struct BinanceIncomeRecord {
597 #[serde(default)]
599 pub symbol: Option<Ustr>,
600 pub income_type: BinanceIncomeType,
602 pub income: String,
604 pub asset: Ustr,
606 pub time: i64,
608 #[serde(default)]
610 pub info: Option<String>,
611 #[serde(default)]
613 pub tran_id: Option<i64>,
614 #[serde(default)]
616 pub trade_id: Option<i64>,
617}
618
619#[derive(Clone, Debug, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct BinanceUserTrade {
623 pub symbol: Ustr,
625 pub id: i64,
627 pub order_id: i64,
629 pub price: String,
631 pub qty: String,
633 #[serde(default)]
635 pub quote_qty: Option<String>,
636 pub realized_pnl: String,
638 pub side: BinanceSide,
640 #[serde(default)]
642 pub position_side: Option<BinancePositionSide>,
643 pub time: i64,
645 pub buyer: bool,
647 pub maker: bool,
649 #[serde(default)]
651 pub commission: Option<String>,
652 #[serde(default)]
654 pub commission_asset: Option<Ustr>,
655 #[serde(default)]
657 pub margin_asset: Option<Ustr>,
658}
659
660#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct BinanceFuturesAccountInfo {
664 #[serde(default)]
666 pub total_initial_margin: Option<String>,
667 #[serde(default)]
669 pub total_maint_margin: Option<String>,
670 #[serde(default)]
672 pub total_wallet_balance: Option<String>,
673 #[serde(default)]
675 pub total_unrealized_profit: Option<String>,
676 #[serde(default)]
678 pub total_margin_balance: Option<String>,
679 #[serde(default)]
681 pub total_position_initial_margin: Option<String>,
682 #[serde(default)]
684 pub total_open_order_initial_margin: Option<String>,
685 #[serde(default)]
687 pub total_cross_wallet_balance: Option<String>,
688 #[serde(default)]
690 pub total_cross_un_pnl: Option<String>,
691 #[serde(default)]
693 pub available_balance: Option<String>,
694 #[serde(default)]
696 pub max_withdraw_amount: Option<String>,
697 #[serde(default)]
699 pub can_deposit: Option<bool>,
700 #[serde(default)]
702 pub can_trade: Option<bool>,
703 #[serde(default)]
705 pub can_withdraw: Option<bool>,
706 #[serde(default)]
708 pub multi_assets_margin: Option<bool>,
709 #[serde(default)]
711 pub update_time: Option<i64>,
712 #[serde(default)]
714 pub assets: Vec<BinanceFuturesBalance>,
715 #[serde(default)]
717 pub positions: Vec<BinanceAccountPosition>,
718}
719
720impl BinanceFuturesAccountInfo {
721 pub fn to_account_state(
727 &self,
728 account_id: AccountId,
729 ts_init: UnixNanos,
730 ) -> anyhow::Result<AccountState> {
731 let mut balances = Vec::with_capacity(self.assets.len());
732
733 for asset in &self.assets {
734 let currency = Currency::get_or_create_crypto_with_context(
735 asset.asset.as_str(),
736 Some("futures balance"),
737 );
738
739 let total: Decimal = asset.wallet_balance.parse().context("invalid balance")?;
740 let available: Decimal = asset
741 .available_balance
742 .parse()
743 .context("invalid available_balance")?;
744 let locked = total - available;
745
746 let total_money = Money::from_decimal(total, currency)
747 .unwrap_or_else(|_| Money::new(total.to_string().parse().unwrap_or(0.0), currency));
748 let locked_money = Money::from_decimal(locked, currency).unwrap_or_else(|_| {
749 Money::new(locked.to_string().parse().unwrap_or(0.0), currency)
750 });
751 let free_money = Money::from_decimal(available, currency).unwrap_or_else(|_| {
752 Money::new(available.to_string().parse().unwrap_or(0.0), currency)
753 });
754
755 let balance = AccountBalance::new(total_money, locked_money, free_money);
756 balances.push(balance);
757 }
758
759 if balances.is_empty() {
761 let zero_currency = Currency::USDT();
762 let zero_money = Money::new(0.0, zero_currency);
763 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
764 balances.push(zero_balance);
765 }
766
767 let mut margins = Vec::new();
769
770 let initial_margin_dec = self
771 .total_initial_margin
772 .as_ref()
773 .and_then(|s| Decimal::from_str_exact(s).ok());
774 let maint_margin_dec = self
775 .total_maint_margin
776 .as_ref()
777 .and_then(|s| Decimal::from_str_exact(s).ok());
778
779 if let (Some(initial_margin_dec), Some(maint_margin_dec)) =
780 (initial_margin_dec, maint_margin_dec)
781 {
782 let has_margin = !initial_margin_dec.is_zero() || !maint_margin_dec.is_zero();
783 if has_margin {
784 let margin_currency = Currency::USDT();
785 let margin_instrument_id =
786 InstrumentId::new(Symbol::new("ACCOUNT"), Venue::new("BINANCE"));
787
788 let initial_margin = Money::from_decimal(initial_margin_dec, margin_currency)
789 .unwrap_or_else(|_| Money::zero(margin_currency));
790 let maintenance_margin = Money::from_decimal(maint_margin_dec, margin_currency)
791 .unwrap_or_else(|_| Money::zero(margin_currency));
792
793 let margin_balance =
794 MarginBalance::new(initial_margin, maintenance_margin, margin_instrument_id);
795 margins.push(margin_balance);
796 }
797 }
798
799 let ts_event = self
800 .update_time
801 .map_or(ts_init, |t| UnixNanos::from((t * 1_000_000) as u64));
802
803 Ok(AccountState::new(
804 account_id,
805 AccountType::Margin,
806 balances,
807 margins,
808 true, UUID4::new(),
810 ts_event,
811 ts_init,
812 None,
813 ))
814 }
815}
816
817#[derive(Clone, Debug, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct BinanceHedgeModeResponse {
821 pub dual_side_position: bool,
823}
824
825#[derive(Clone, Debug, Serialize, Deserialize)]
827#[serde(rename_all = "camelCase")]
828pub struct BinanceLeverageResponse {
829 pub symbol: Ustr,
831 pub leverage: u32,
833 #[serde(default)]
835 pub max_notional_value: Option<String>,
836}
837
838#[derive(Clone, Debug, Serialize, Deserialize)]
840#[serde(rename_all = "camelCase")]
841pub struct BinanceCancelAllOrdersResponse {
842 pub code: i32,
844 pub msg: String,
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct BinanceFuturesOrder {
852 pub symbol: Ustr,
854 pub order_id: i64,
856 pub client_order_id: String,
858 pub orig_qty: String,
860 pub executed_qty: String,
862 pub cum_quote: String,
864 pub price: String,
866 #[serde(default)]
868 pub avg_price: Option<String>,
869 #[serde(default)]
871 pub stop_price: Option<String>,
872 pub status: BinanceOrderStatus,
874 pub time_in_force: BinanceTimeInForce,
876 #[serde(rename = "type")]
878 pub order_type: BinanceFuturesOrderType,
879 #[serde(default)]
881 pub orig_type: Option<BinanceFuturesOrderType>,
882 pub side: BinanceSide,
884 #[serde(default)]
886 pub position_side: Option<BinancePositionSide>,
887 #[serde(default)]
889 pub reduce_only: Option<bool>,
890 #[serde(default)]
892 pub close_position: Option<bool>,
893 #[serde(default)]
895 pub activate_price: Option<String>,
896 #[serde(default)]
898 pub price_rate: Option<String>,
899 #[serde(default)]
901 pub working_type: Option<BinanceWorkingType>,
902 #[serde(default)]
904 pub price_protect: Option<bool>,
905 #[serde(default)]
907 pub is_isolated: Option<bool>,
908 #[serde(default)]
910 pub good_till_date: Option<i64>,
911 #[serde(default)]
913 pub price_match: Option<BinancePriceMatch>,
914 #[serde(default)]
916 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
917 #[serde(default)]
919 pub update_time: Option<i64>,
920 #[serde(default)]
922 pub working_type_id: Option<i64>,
923}
924
925impl BinanceFuturesOrder {
926 pub fn to_order_status_report(
932 &self,
933 account_id: AccountId,
934 instrument_id: InstrumentId,
935 size_precision: u8,
936 ) -> anyhow::Result<OrderStatusReport> {
937 let ts_now = get_atomic_clock_realtime().get_time_ns();
938 let ts_event = self
939 .update_time
940 .map_or(ts_now, |t| UnixNanos::from((t * 1_000_000) as u64));
941
942 let client_order_id = ClientOrderId::new(&self.client_order_id);
943 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
944
945 let order_side = match self.side {
946 BinanceSide::Buy => OrderSide::Buy,
947 BinanceSide::Sell => OrderSide::Sell,
948 };
949
950 let order_type = self.order_type.to_nautilus_order_type();
951 let time_in_force = self.time_in_force.to_nautilus_time_in_force();
952 let order_status = self.status.to_nautilus_order_status();
953
954 let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
955 let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
956
957 Ok(OrderStatusReport::new(
958 account_id,
959 instrument_id,
960 Some(client_order_id),
961 venue_order_id,
962 order_side,
963 order_type,
964 time_in_force,
965 order_status,
966 Quantity::new(quantity.to_string().parse()?, size_precision),
967 Quantity::new(filled_qty.to_string().parse()?, size_precision),
968 ts_event,
969 ts_event,
970 ts_now,
971 Some(UUID4::new()),
972 ))
973 }
974}
975
976impl BinanceFuturesOrderType {
977 #[must_use]
979 pub fn is_post_only(&self) -> bool {
980 false }
982
983 #[must_use]
985 pub fn to_nautilus_order_type(&self) -> OrderType {
986 match self {
987 Self::Market => OrderType::Market,
988 Self::Limit => OrderType::Limit,
989 Self::Stop => OrderType::StopLimit,
990 Self::StopMarket => OrderType::StopMarket,
991 Self::TakeProfit => OrderType::LimitIfTouched,
992 Self::TakeProfitMarket => OrderType::MarketIfTouched,
993 Self::TrailingStopMarket => OrderType::TrailingStopMarket,
994 Self::Liquidation | Self::Adl => OrderType::Market, Self::Unknown => OrderType::Market,
996 }
997 }
998}
999
1000impl BinanceTimeInForce {
1001 #[must_use]
1003 pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
1004 match self {
1005 Self::Gtc => TimeInForce::Gtc,
1006 Self::Ioc => TimeInForce::Ioc,
1007 Self::Fok => TimeInForce::Fok,
1008 Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
1010 Self::Unknown => TimeInForce::Gtc, }
1012 }
1013}
1014
1015impl BinanceOrderStatus {
1016 #[must_use]
1018 pub fn to_nautilus_order_status(&self) -> OrderStatus {
1019 match self {
1020 Self::New => OrderStatus::Accepted,
1021 Self::PartiallyFilled => OrderStatus::PartiallyFilled,
1022 Self::Filled => OrderStatus::Filled,
1023 Self::Canceled => OrderStatus::Canceled,
1024 Self::PendingCancel => OrderStatus::PendingCancel,
1025 Self::Rejected => OrderStatus::Rejected,
1026 Self::Expired => OrderStatus::Expired,
1027 Self::ExpiredInMatch => OrderStatus::Expired,
1028 Self::Unknown => OrderStatus::Initialized,
1029 }
1030 }
1031}
1032
1033impl BinanceUserTrade {
1034 pub fn to_fill_report(
1040 &self,
1041 account_id: AccountId,
1042 instrument_id: InstrumentId,
1043 price_precision: u8,
1044 size_precision: u8,
1045 ) -> anyhow::Result<FillReport> {
1046 let ts_now = get_atomic_clock_realtime().get_time_ns();
1047 let ts_event = UnixNanos::from((self.time * 1_000_000) as u64);
1048
1049 let venue_order_id = VenueOrderId::new(self.order_id.to_string());
1050 let trade_id = TradeId::new(self.id.to_string());
1051
1052 let order_side = match self.side {
1053 BinanceSide::Buy => OrderSide::Buy,
1054 BinanceSide::Sell => OrderSide::Sell,
1055 };
1056
1057 let liquidity_side = if self.maker {
1058 LiquiditySide::Maker
1059 } else {
1060 LiquiditySide::Taker
1061 };
1062
1063 let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
1064 let last_px: Decimal = self.price.parse().context("invalid price")?;
1065
1066 let commission = {
1067 let comm_val: f64 = self
1068 .commission
1069 .as_ref()
1070 .and_then(|c| c.parse().ok())
1071 .unwrap_or(0.0);
1072 let comm_asset = self
1073 .commission_asset
1074 .as_ref()
1075 .map_or_else(Currency::USDT, Currency::from);
1076 Money::new(comm_val, comm_asset)
1077 };
1078
1079 Ok(FillReport::new(
1080 account_id,
1081 instrument_id,
1082 venue_order_id,
1083 trade_id,
1084 order_side,
1085 Quantity::new(last_qty.to_string().parse()?, size_precision),
1086 Price::new(last_px.to_string().parse()?, price_precision),
1087 commission,
1088 liquidity_side,
1089 None, None, ts_event,
1092 ts_now,
1093 Some(UUID4::new()),
1094 ))
1095 }
1096}
1097
1098#[derive(Clone, Debug, Deserialize)]
1102#[serde(untagged)]
1103pub enum BatchOrderResult {
1104 Success(Box<BinanceFuturesOrder>),
1106 Error(BatchOrderError),
1108}
1109
1110#[derive(Clone, Debug, Deserialize)]
1112pub struct BatchOrderError {
1113 pub code: i64,
1115 pub msg: String,
1117}
1118
1119#[derive(Debug, Clone, Deserialize)]
1121#[serde(rename_all = "camelCase")]
1122pub struct ListenKeyResponse {
1123 pub listen_key: String,
1125}
1126
1127#[derive(Clone, Debug, Serialize, Deserialize)]
1137#[serde(rename_all = "camelCase")]
1138pub struct BinanceFuturesAlgoOrder {
1139 pub algo_id: i64,
1141 pub client_algo_id: String,
1143 pub algo_type: BinanceAlgoType,
1145 #[serde(rename = "type")]
1147 pub order_type: BinanceFuturesOrderType,
1148 pub symbol: Ustr,
1150 pub side: BinanceSide,
1152 #[serde(default)]
1154 pub position_side: Option<BinancePositionSide>,
1155 #[serde(default)]
1157 pub time_in_force: Option<BinanceTimeInForce>,
1158 #[serde(default)]
1160 pub quantity: Option<String>,
1161 #[serde(default)]
1163 pub algo_status: Option<BinanceAlgoStatus>,
1164 #[serde(default)]
1166 pub trigger_price: Option<String>,
1167 #[serde(default)]
1169 pub price: Option<String>,
1170 #[serde(default)]
1172 pub working_type: Option<BinanceWorkingType>,
1173 #[serde(default)]
1175 pub close_position: Option<bool>,
1176 #[serde(default)]
1178 pub price_protect: Option<bool>,
1179 #[serde(default)]
1181 pub reduce_only: Option<bool>,
1182 #[serde(default)]
1184 pub activate_price: Option<String>,
1185 #[serde(default)]
1187 pub callback_rate: Option<String>,
1188 #[serde(default)]
1190 pub create_time: Option<i64>,
1191 #[serde(default)]
1193 pub update_time: Option<i64>,
1194 #[serde(default)]
1196 pub trigger_time: Option<i64>,
1197 #[serde(default)]
1199 pub actual_order_id: Option<String>,
1200 #[serde(default)]
1202 pub executed_qty: Option<String>,
1203 #[serde(default)]
1205 pub avg_price: Option<String>,
1206}
1207
1208impl BinanceFuturesAlgoOrder {
1209 pub fn to_order_status_report(
1215 &self,
1216 account_id: AccountId,
1217 instrument_id: InstrumentId,
1218 size_precision: u8,
1219 ) -> anyhow::Result<OrderStatusReport> {
1220 let ts_now = get_atomic_clock_realtime().get_time_ns();
1221 let ts_event = self
1222 .update_time
1223 .or(self.create_time)
1224 .map_or(ts_now, |t| UnixNanos::from((t * 1_000_000) as u64));
1225
1226 let client_order_id = ClientOrderId::new(&self.client_algo_id);
1227 let venue_order_id = self.actual_order_id.as_ref().map_or_else(
1228 || VenueOrderId::new(self.algo_id.to_string()),
1229 |id| VenueOrderId::new(id.clone()),
1230 );
1231
1232 let order_side = match self.side {
1233 BinanceSide::Buy => OrderSide::Buy,
1234 BinanceSide::Sell => OrderSide::Sell,
1235 };
1236
1237 let order_type = self.parse_order_type();
1238 let time_in_force = self
1239 .time_in_force
1240 .as_ref()
1241 .map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
1242 let order_status = self.parse_order_status();
1243
1244 let quantity: Decimal = self
1245 .quantity
1246 .as_ref()
1247 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1248 .context("invalid quantity")?;
1249 let filled_qty: Decimal = self
1250 .executed_qty
1251 .as_ref()
1252 .map_or(Ok(Decimal::ZERO), |q| q.parse())
1253 .context("invalid executed_qty")?;
1254
1255 Ok(OrderStatusReport::new(
1256 account_id,
1257 instrument_id,
1258 Some(client_order_id),
1259 venue_order_id,
1260 order_side,
1261 order_type,
1262 time_in_force,
1263 order_status,
1264 Quantity::new(quantity.to_string().parse()?, size_precision),
1265 Quantity::new(filled_qty.to_string().parse()?, size_precision),
1266 ts_event,
1267 ts_event,
1268 ts_now,
1269 Some(UUID4::new()),
1270 ))
1271 }
1272
1273 fn parse_order_type(&self) -> OrderType {
1274 self.order_type.into()
1275 }
1276
1277 fn parse_order_status(&self) -> OrderStatus {
1278 match self.algo_status {
1279 Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
1280 Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
1281 Some(BinanceAlgoStatus::Triggered) => OrderStatus::Accepted,
1282 Some(BinanceAlgoStatus::Finished) => {
1283 if let Some(qty) = &self.executed_qty
1285 && let Ok(dec) = qty.parse::<Decimal>()
1286 && !dec.is_zero()
1287 {
1288 return OrderStatus::Filled;
1289 }
1290 OrderStatus::Canceled
1291 }
1292 Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
1293 Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
1294 Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
1295 Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
1296 }
1297 }
1298}
1299
1300#[derive(Clone, Debug, Deserialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct BinanceFuturesAlgoOrderCancelResponse {
1304 pub algo_id: i64,
1306 pub client_algo_id: String,
1308 pub code: i32,
1310 pub msg: String,
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use rstest::rstest;
1317
1318 use super::*;
1319
1320 const ACCOUNT_INFO_V2_JSON: &str = r#"{
1322 "feeTier": 0,
1323 "canTrade": true,
1324 "canDeposit": true,
1325 "canWithdraw": true,
1326 "updateTime": 0,
1327 "multiAssetsMargin": false,
1328 "tradeGroupId": -1,
1329 "totalInitialMargin": "0.00000000",
1330 "totalMaintMargin": "0.00000000",
1331 "totalWalletBalance": "23.72469206",
1332 "totalUnrealizedProfit": "0.00000000",
1333 "totalMarginBalance": "23.72469206",
1334 "totalPositionInitialMargin": "0.00000000",
1335 "totalOpenOrderInitialMargin": "0.00000000",
1336 "totalCrossWalletBalance": "23.72469206",
1337 "totalCrossUnPnl": "0.00000000",
1338 "availableBalance": "23.72469206",
1339 "maxWithdrawAmount": "23.72469206",
1340 "assets": [
1341 {
1342 "asset": "USDT",
1343 "walletBalance": "23.72469206",
1344 "unrealizedProfit": "0.00000000",
1345 "marginBalance": "23.72469206",
1346 "maintMargin": "0.00000000",
1347 "initialMargin": "0.00000000",
1348 "positionInitialMargin": "0.00000000",
1349 "openOrderInitialMargin": "0.00000000",
1350 "crossWalletBalance": "23.72469206",
1351 "crossUnPnl": "0.00000000",
1352 "availableBalance": "23.72469206",
1353 "maxWithdrawAmount": "23.72469206",
1354 "marginAvailable": true,
1355 "updateTime": 1625474304765
1356 }
1357 ],
1358 "positions": [
1359 {
1360 "symbol": "BTCUSDT",
1361 "initialMargin": "0",
1362 "maintMargin": "0",
1363 "unrealizedProfit": "0.00000000",
1364 "positionInitialMargin": "0",
1365 "openOrderInitialMargin": "0",
1366 "leverage": "100",
1367 "isolated": false,
1368 "entryPrice": "0.00000",
1369 "maxNotional": "250000",
1370 "bidNotional": "0",
1371 "askNotional": "0",
1372 "positionSide": "BOTH",
1373 "positionAmt": "0",
1374 "updateTime": 0
1375 }
1376 ]
1377 }"#;
1378
1379 const POSITION_RISK_JSON: &str = r#"[
1381 {
1382 "symbol": "BTCUSDT",
1383 "positionAmt": "0.001",
1384 "entryPrice": "50000.0",
1385 "markPrice": "51000.0",
1386 "unRealizedProfit": "1.00000000",
1387 "liquidationPrice": "45000.0",
1388 "leverage": "20",
1389 "maxNotionalValue": "250000",
1390 "marginType": "cross",
1391 "isolatedMargin": "0.00000000",
1392 "isAutoAddMargin": "false",
1393 "positionSide": "BOTH",
1394 "notional": "51.0",
1395 "isolatedWallet": "0",
1396 "updateTime": 1625474304765,
1397 "breakEvenPrice": "50100.0"
1398 }
1399 ]"#;
1400
1401 const BALANCE_JSON: &str = r#"[
1403 {
1404 "accountAlias": "SgsR",
1405 "asset": "USDT",
1406 "balance": "122.12345678",
1407 "crossWalletBalance": "122.12345678",
1408 "crossUnPnl": "0.00000000",
1409 "availableBalance": "122.12345678",
1410 "maxWithdrawAmount": "122.12345678",
1411 "marginAvailable": true,
1412 "updateTime": 1617939110373
1413 }
1414 ]"#;
1415
1416 const ORDER_JSON: &str = r#"{
1418 "orderId": 12345678,
1419 "symbol": "BTCUSDT",
1420 "status": "NEW",
1421 "clientOrderId": "testOrder123",
1422 "price": "50000.00",
1423 "avgPrice": "0.00",
1424 "origQty": "0.001",
1425 "executedQty": "0.000",
1426 "cumQuote": "0.00",
1427 "timeInForce": "GTC",
1428 "type": "LIMIT",
1429 "reduceOnly": false,
1430 "closePosition": false,
1431 "side": "BUY",
1432 "positionSide": "BOTH",
1433 "stopPrice": "0.00",
1434 "workingType": "CONTRACT_PRICE",
1435 "priceProtect": false,
1436 "origType": "LIMIT",
1437 "priceMatch": "NONE",
1438 "selfTradePreventionMode": "NONE",
1439 "goodTillDate": 0,
1440 "time": 1625474304765,
1441 "updateTime": 1625474304765
1442 }"#;
1443
1444 #[rstest]
1445 fn test_parse_account_info_v2() {
1446 let account: BinanceFuturesAccountInfo =
1447 serde_json::from_str(ACCOUNT_INFO_V2_JSON).expect("Failed to parse account info");
1448
1449 assert_eq!(
1450 account.total_wallet_balance,
1451 Some("23.72469206".to_string())
1452 );
1453 assert_eq!(account.assets.len(), 1);
1454 assert_eq!(account.assets[0].asset.as_str(), "USDT");
1455 assert_eq!(account.assets[0].wallet_balance, "23.72469206");
1456 assert_eq!(account.positions.len(), 1);
1457 assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
1458 assert_eq!(account.positions[0].leverage, Some("100".to_string()));
1459 }
1460
1461 #[rstest]
1462 fn test_parse_position_risk() {
1463 let positions: Vec<BinancePositionRisk> =
1464 serde_json::from_str(POSITION_RISK_JSON).expect("Failed to parse position risk");
1465
1466 assert_eq!(positions.len(), 1);
1467 assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
1468 assert_eq!(positions[0].position_amt, "0.001");
1469 assert_eq!(positions[0].mark_price, "51000.0");
1470 assert_eq!(positions[0].leverage, "20");
1471 }
1472
1473 #[rstest]
1474 fn test_parse_balance_with_v1_field() {
1475 let balances: Vec<BinanceFuturesBalance> =
1477 serde_json::from_str(BALANCE_JSON).expect("Failed to parse balance");
1478
1479 assert_eq!(balances.len(), 1);
1480 assert_eq!(balances[0].asset.as_str(), "USDT");
1481 assert_eq!(balances[0].wallet_balance, "122.12345678");
1483 assert_eq!(balances[0].available_balance, "122.12345678");
1484 }
1485
1486 #[rstest]
1487 fn test_parse_balance_with_v2_field() {
1488 let json = r#"{
1490 "asset": "USDT",
1491 "walletBalance": "100.00000000",
1492 "availableBalance": "100.00000000",
1493 "updateTime": 1617939110373
1494 }"#;
1495
1496 let balance: BinanceFuturesBalance =
1497 serde_json::from_str(json).expect("Failed to parse balance");
1498
1499 assert_eq!(balance.asset.as_str(), "USDT");
1500 assert_eq!(balance.wallet_balance, "100.00000000");
1501 }
1502
1503 #[rstest]
1504 fn test_parse_order() {
1505 let order: BinanceFuturesOrder =
1506 serde_json::from_str(ORDER_JSON).expect("Failed to parse order");
1507
1508 assert_eq!(order.order_id, 12345678);
1509 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1510 assert_eq!(order.status, BinanceOrderStatus::New);
1511 assert_eq!(order.side, BinanceSide::Buy);
1512 assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
1513 }
1514
1515 #[rstest]
1516 fn test_parse_hedge_mode_response() {
1517 let json = r#"{"dualSidePosition": true}"#;
1518 let response: BinanceHedgeModeResponse =
1519 serde_json::from_str(json).expect("Failed to parse hedge mode");
1520 assert!(response.dual_side_position);
1521 }
1522
1523 #[rstest]
1524 fn test_parse_leverage_response() {
1525 let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
1526 let response: BinanceLeverageResponse =
1527 serde_json::from_str(json).expect("Failed to parse leverage");
1528 assert_eq!(response.symbol.as_str(), "BTCUSDT");
1529 assert_eq!(response.leverage, 20);
1530 }
1531
1532 #[rstest]
1533 fn test_parse_listen_key_response() {
1534 let json =
1535 r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
1536 let response: ListenKeyResponse =
1537 serde_json::from_str(json).expect("Failed to parse listen key");
1538 assert!(!response.listen_key.is_empty());
1539 }
1540
1541 #[rstest]
1542 fn test_parse_account_position() {
1543 let json = r#"{
1544 "symbol": "ETHUSDT",
1545 "initialMargin": "100.00",
1546 "maintMargin": "50.00",
1547 "unrealizedProfit": "10.00",
1548 "positionInitialMargin": "100.00",
1549 "openOrderInitialMargin": "0",
1550 "leverage": "10",
1551 "isolated": true,
1552 "entryPrice": "2000.00",
1553 "maxNotional": "100000",
1554 "bidNotional": "0",
1555 "askNotional": "0",
1556 "positionSide": "LONG",
1557 "positionAmt": "0.5",
1558 "updateTime": 1625474304765
1559 }"#;
1560
1561 let position: BinanceAccountPosition =
1562 serde_json::from_str(json).expect("Failed to parse account position");
1563
1564 assert_eq!(position.symbol.as_str(), "ETHUSDT");
1565 assert_eq!(position.leverage, Some("10".to_string()));
1566 assert_eq!(position.isolated, Some(true));
1567 assert_eq!(position.position_side, Some(BinancePositionSide::Long));
1568 }
1569
1570 #[rstest]
1571 fn test_parse_algo_order() {
1572 let json = r#"{
1573 "algoId": 123456789,
1574 "clientAlgoId": "test-algo-order-1",
1575 "algoType": "CONDITIONAL",
1576 "type": "STOP_MARKET",
1577 "symbol": "BTCUSDT",
1578 "side": "BUY",
1579 "positionSide": "BOTH",
1580 "timeInForce": "GTC",
1581 "quantity": "0.001",
1582 "algoStatus": "NEW",
1583 "triggerPrice": "45000.00",
1584 "workingType": "MARK_PRICE",
1585 "reduceOnly": false,
1586 "createTime": 1625474304765,
1587 "updateTime": 1625474304765
1588 }"#;
1589
1590 let order: BinanceFuturesAlgoOrder =
1591 serde_json::from_str(json).expect("Failed to parse algo order");
1592
1593 assert_eq!(order.algo_id, 123456789);
1594 assert_eq!(order.client_algo_id, "test-algo-order-1");
1595 assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
1596 assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
1597 assert_eq!(order.symbol.as_str(), "BTCUSDT");
1598 assert_eq!(order.side, BinanceSide::Buy);
1599 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
1600 assert_eq!(order.trigger_price, Some("45000.00".to_string()));
1601 }
1602
1603 #[rstest]
1604 fn test_parse_algo_order_triggered() {
1605 let json = r#"{
1606 "algoId": 123456789,
1607 "clientAlgoId": "test-algo-order-2",
1608 "algoType": "CONDITIONAL",
1609 "type": "TAKE_PROFIT",
1610 "symbol": "ETHUSDT",
1611 "side": "SELL",
1612 "algoStatus": "TRIGGERED",
1613 "triggerPrice": "2500.00",
1614 "price": "2500.00",
1615 "actualOrderId": "987654321",
1616 "executedQty": "0.5",
1617 "avgPrice": "2499.50"
1618 }"#;
1619
1620 let order: BinanceFuturesAlgoOrder =
1621 serde_json::from_str(json).expect("Failed to parse triggered algo order");
1622
1623 assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Triggered));
1624 assert_eq!(order.order_type, BinanceFuturesOrderType::TakeProfit);
1625 assert_eq!(order.actual_order_id, Some("987654321".to_string()));
1626 assert_eq!(order.executed_qty, Some("0.5".to_string()));
1627 }
1628
1629 #[rstest]
1630 fn test_parse_algo_order_cancel_response() {
1631 let json = r#"{
1632 "algoId": 123456789,
1633 "clientAlgoId": "test-algo-order-1",
1634 "code": 200,
1635 "msg": "success"
1636 }"#;
1637
1638 let response: BinanceFuturesAlgoOrderCancelResponse =
1639 serde_json::from_str(json).expect("Failed to parse algo cancel response");
1640
1641 assert_eq!(response.algo_id, 123456789);
1642 assert_eq!(response.client_algo_id, "test-algo-order-1");
1643 assert_eq!(response.code, 200);
1644 assert_eq!(response.msg, "success");
1645 }
1646}