1use nautilus_core::{UUID4, nanos::UnixNanos};
21use nautilus_model::{
22 enums::AccountType,
23 events::AccountState,
24 identifiers::AccountId,
25 types::{AccountBalance, Currency, Money},
26};
27use rust_decimal::Decimal;
28
29use crate::{
30 common::{
31 enums::{
32 BinanceOrderStatus, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
33 },
34 parse::parse_micros_or_init,
35 },
36 spot::sbe::spot::{
37 order_side::OrderSide, order_status::OrderStatus, order_type::OrderType,
38 self_trade_prevention_mode::SelfTradePreventionMode, time_in_force::TimeInForce,
39 },
40};
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct BinancePriceLevel {
45 pub price_mantissa: i64,
47 pub qty_mantissa: i64,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct BinanceDepth {
54 pub last_update_id: i64,
56 pub price_exponent: i8,
58 pub qty_exponent: i8,
60 pub bids: Vec<BinancePriceLevel>,
62 pub asks: Vec<BinancePriceLevel>,
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub struct BinanceTrade {
69 pub id: i64,
71 pub price_mantissa: i64,
73 pub qty_mantissa: i64,
75 pub quote_qty_mantissa: i64,
77 pub time: i64,
79 pub is_buyer_maker: bool,
81 pub is_best_match: bool,
83}
84
85#[derive(Debug, Clone, PartialEq)]
87pub struct BinanceTrades {
88 pub price_exponent: i8,
90 pub qty_exponent: i8,
92 pub trades: Vec<BinanceTrade>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct BinanceAggTrade {
99 pub id: i64,
101 pub price_mantissa: i64,
103 pub qty_mantissa: i64,
105 pub first_trade_id: i64,
107 pub last_trade_id: i64,
109 pub time: i64,
111 pub is_buyer_maker: bool,
113 pub is_best_match: bool,
115}
116
117#[derive(Debug, Clone, PartialEq)]
119pub struct BinanceAggTrades {
120 pub price_exponent: i8,
122 pub qty_exponent: i8,
124 pub trades: Vec<BinanceAggTrade>,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct BinanceOrderFill {
131 pub price_mantissa: i64,
133 pub qty_mantissa: i64,
135 pub commission_mantissa: i64,
137 pub commission_exponent: i8,
139 pub commission_asset: String,
141 pub trade_id: Option<i64>,
143}
144
145#[derive(Debug, Clone, PartialEq)]
147pub struct BinanceNewOrderResponse {
148 pub price_exponent: i8,
150 pub qty_exponent: i8,
152 pub order_id: i64,
154 pub order_list_id: Option<i64>,
156 pub transact_time: i64,
158 pub price_mantissa: i64,
160 pub orig_qty_mantissa: i64,
162 pub executed_qty_mantissa: i64,
164 pub cummulative_quote_qty_mantissa: i64,
166 pub status: OrderStatus,
168 pub time_in_force: TimeInForce,
170 pub order_type: OrderType,
172 pub side: OrderSide,
174 pub stop_price_mantissa: Option<i64>,
176 pub working_time: Option<i64>,
178 pub self_trade_prevention_mode: SelfTradePreventionMode,
180 pub client_order_id: String,
182 pub symbol: String,
184 pub fills: Vec<BinanceOrderFill>,
186 pub expiry_reason: Option<u8>,
188}
189
190#[derive(Debug, Clone, PartialEq)]
192pub struct BinanceCancelOrderResponse {
193 pub price_exponent: i8,
195 pub qty_exponent: i8,
197 pub order_id: i64,
199 pub order_list_id: Option<i64>,
201 pub transact_time: i64,
203 pub price_mantissa: i64,
205 pub orig_qty_mantissa: i64,
207 pub executed_qty_mantissa: i64,
209 pub cummulative_quote_qty_mantissa: i64,
211 pub status: OrderStatus,
213 pub time_in_force: TimeInForce,
215 pub order_type: OrderType,
217 pub side: OrderSide,
219 pub self_trade_prevention_mode: SelfTradePreventionMode,
221 pub client_order_id: String,
223 pub orig_client_order_id: String,
225 pub symbol: String,
227}
228
229#[derive(Debug, Clone, PartialEq)]
231pub struct BinanceOrderResponse {
232 pub price_exponent: i8,
234 pub qty_exponent: i8,
236 pub order_id: i64,
238 pub order_list_id: Option<i64>,
240 pub price_mantissa: i64,
242 pub orig_qty_mantissa: i64,
244 pub executed_qty_mantissa: i64,
246 pub cummulative_quote_qty_mantissa: i64,
248 pub status: OrderStatus,
250 pub time_in_force: TimeInForce,
252 pub order_type: OrderType,
254 pub side: OrderSide,
256 pub stop_price_mantissa: Option<i64>,
258 pub iceberg_qty_mantissa: Option<i64>,
260 pub time: i64,
262 pub update_time: i64,
264 pub is_working: bool,
266 pub working_time: Option<i64>,
268 pub orig_quote_order_qty_mantissa: i64,
270 pub self_trade_prevention_mode: SelfTradePreventionMode,
272 pub client_order_id: String,
274 pub symbol: String,
276 pub expiry_reason: Option<u8>,
278}
279
280#[derive(Debug, Clone, PartialEq)]
282pub struct BinanceBalance {
283 pub asset: String,
285 pub free_mantissa: i64,
287 pub locked_mantissa: i64,
289 pub exponent: i8,
291}
292
293#[derive(Debug, Clone, PartialEq)]
295pub struct BinanceAccountInfo {
296 pub commission_exponent: i8,
298 pub maker_commission_mantissa: i64,
300 pub taker_commission_mantissa: i64,
302 pub buyer_commission_mantissa: i64,
304 pub seller_commission_mantissa: i64,
306 pub can_trade: bool,
308 pub can_withdraw: bool,
310 pub can_deposit: bool,
312 pub require_self_trade_prevention: bool,
314 pub prevent_sor: bool,
316 pub update_time: i64,
318 pub account_type: String,
320 pub balances: Vec<BinanceBalance>,
322}
323
324impl BinanceAccountInfo {
325 #[must_use]
327 pub fn to_account_state(&self, account_id: AccountId, ts_init: UnixNanos) -> AccountState {
328 let mut balances = Vec::with_capacity(self.balances.len());
329
330 for asset in &self.balances {
331 let currency =
332 Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
333
334 let exponent = asset.exponent as i32;
335 let multiplier = Decimal::new(1, (-exponent) as u32);
336
337 let free = Decimal::new(asset.free_mantissa, 0) * multiplier;
338 let locked = Decimal::new(asset.locked_mantissa, 0) * multiplier;
339 let total = free + locked;
340
341 match AccountBalance::from_total_and_locked(total, locked, currency) {
342 Ok(balance) => balances.push(balance),
343 Err(e) => log::warn!("Skipping spot balance for {}: {e}", currency.code.as_str()),
344 }
345 }
346
347 if balances.is_empty() {
349 let zero_currency = Currency::USDT();
350 let zero_money = Money::zero(zero_currency);
351 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
352 balances.push(zero_balance);
353 }
354
355 let ts_event =
356 parse_micros_or_init(self.update_time, "Spot SBE account update time", ts_init);
357
358 AccountState::new(
359 account_id,
360 AccountType::Cash,
361 balances,
362 vec![], true, UUID4::new(),
365 ts_event,
366 ts_init,
367 None, )
369 }
370}
371
372#[derive(Debug, Clone, PartialEq)]
374pub struct BinancePriceFilterSbe {
375 pub price_exponent: i8,
377 pub min_price: i64,
379 pub max_price: i64,
381 pub tick_size: i64,
383}
384
385#[derive(Debug, Clone, PartialEq)]
387pub struct BinanceLotSizeFilterSbe {
388 pub qty_exponent: i8,
390 pub min_qty: i64,
392 pub max_qty: i64,
394 pub step_size: i64,
396}
397
398#[derive(Debug, Clone, Default, PartialEq)]
400pub struct BinanceSymbolFiltersSbe {
401 pub price_filter: Option<BinancePriceFilterSbe>,
403 pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
405}
406
407#[derive(Debug, Clone, PartialEq)]
409pub struct BinanceSymbolSbe {
410 pub symbol: String,
412 pub base_asset: String,
414 pub quote_asset: String,
416 pub base_asset_precision: u8,
418 pub quote_asset_precision: u8,
420 pub status: u8,
422 pub order_types: u16,
424 pub iceberg_allowed: bool,
426 pub oco_allowed: bool,
428 pub oto_allowed: bool,
430 pub quote_order_qty_market_allowed: bool,
432 pub allow_trailing_stop: bool,
434 pub cancel_replace_allowed: bool,
436 pub amend_allowed: bool,
438 pub is_spot_trading_allowed: bool,
440 pub is_margin_trading_allowed: bool,
442 pub filters: BinanceSymbolFiltersSbe,
444 pub permissions: Vec<Vec<String>>,
446}
447
448#[derive(Debug, Clone, PartialEq)]
450pub struct BinanceExchangeInfoSbe {
451 pub symbols: Vec<BinanceSymbolSbe>,
453}
454
455#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
457pub struct BinanceExchangeInfoJson {
458 pub symbols: Vec<BinanceSymbolJson>,
460}
461
462#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct BinanceSymbolJson {
466 pub symbol: String,
468 pub status: String,
470 pub base_asset: String,
472 pub quote_asset: String,
474 pub base_asset_precision: u8,
476 pub quote_asset_precision: u8,
478 pub filters: Vec<BinanceSymbolFilterJson>,
480}
481
482#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct BinanceSymbolFilterJson {
486 pub filter_type: String,
488 pub min_price: Option<String>,
490 pub max_price: Option<String>,
492 pub tick_size: Option<String>,
494 pub min_qty: Option<String>,
496 pub max_qty: Option<String>,
498 pub step_size: Option<String>,
500}
501
502#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct BinanceAccountCommission {
506 pub symbol: String,
508 pub standard_commission: BinanceCommissionRates,
510}
511
512#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
514pub struct BinanceCommissionRates {
515 pub maker: String,
517 pub taker: String,
519}
520
521#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct BinanceAccountRatesJson {
525 pub commission_rates: BinanceCommissionRates,
527}
528
529#[derive(Debug, Clone, PartialEq)]
531pub struct BinanceAccountTrade {
532 pub price_exponent: i8,
534 pub qty_exponent: i8,
536 pub commission_exponent: i8,
538 pub id: i64,
540 pub order_id: i64,
542 pub order_list_id: Option<i64>,
544 pub price_mantissa: i64,
546 pub qty_mantissa: i64,
548 pub quote_qty_mantissa: i64,
550 pub commission_mantissa: i64,
552 pub time: i64,
554 pub is_buyer: bool,
556 pub is_maker: bool,
558 pub is_best_match: bool,
560 pub symbol: String,
562 pub commission_asset: String,
564}
565
566#[derive(Debug, Clone, PartialEq)]
568pub struct BinanceKlines {
569 pub price_exponent: i8,
571 pub qty_exponent: i8,
573 pub klines: Vec<BinanceKline>,
575}
576
577#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct ListenKeyResponse {
581 pub listen_key: String,
583}
584
585#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct Ticker24hr {
589 pub symbol: String,
591 pub price_change: String,
593 pub price_change_percent: String,
595 pub weighted_avg_price: String,
597 pub prev_close_price: String,
599 pub last_price: String,
601 pub last_qty: String,
603 pub bid_price: String,
605 pub bid_qty: String,
607 pub ask_price: String,
609 pub ask_qty: String,
611 pub open_price: String,
613 pub high_price: String,
615 pub low_price: String,
617 pub volume: String,
619 pub quote_volume: String,
621 pub open_time: i64,
623 pub close_time: i64,
625 pub first_id: i64,
627 pub last_id: i64,
629 pub count: i64,
631}
632
633#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
635pub struct TickerPrice {
636 pub symbol: String,
638 pub price: String,
640}
641
642#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct BookTicker {
646 pub symbol: String,
648 pub bid_price: String,
650 pub bid_qty: String,
652 pub ask_price: String,
654 pub ask_qty: String,
656}
657
658#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
660pub struct AvgPrice {
661 pub mins: i64,
663 pub price: String,
665 #[serde(rename = "closeTime")]
667 pub close_time: i64,
668}
669
670#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
672#[serde(rename_all = "camelCase")]
673pub struct TradeFee {
674 pub symbol: String,
676 pub maker_commission: String,
678 pub taker_commission: String,
680}
681
682#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
684#[serde(rename_all = "camelCase")]
685pub struct NewOcoOrderListResponse {
686 pub order_list_id: i64,
688 pub contingency_type: String,
690 pub list_status_type: String,
692 pub list_order_status: String,
694 pub list_client_order_id: String,
696 pub transaction_time: i64,
698 pub symbol: String,
700 pub orders: Vec<OrderListOrder>,
702}
703
704#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct OrderListOrder {
708 pub symbol: String,
710 pub order_id: i64,
712 pub client_order_id: String,
714}
715
716#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
720#[serde(untagged)]
721pub enum BatchOrderResult {
722 Success(Box<BatchOrderSuccess>),
724 Error(BatchOrderError),
726}
727
728#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
730#[serde(rename_all = "camelCase")]
731pub struct BatchOrderSuccess {
732 pub symbol: String,
734 pub order_id: i64,
736 #[serde(default)]
738 pub order_list_id: Option<i64>,
739 pub client_order_id: String,
741 pub transact_time: i64,
743 pub price: String,
745 pub orig_qty: String,
747 pub executed_qty: String,
749 #[serde(rename = "cummulativeQuoteQty")]
751 pub cummulative_quote_qty: String,
752 pub status: BinanceOrderStatus,
754 pub time_in_force: BinanceTimeInForce,
756 #[serde(rename = "type")]
758 pub order_type: String,
759 pub side: BinanceSide,
761 #[serde(default)]
763 pub working_time: Option<i64>,
764 #[serde(default)]
766 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
767}
768
769#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
771pub struct BatchOrderError {
772 pub code: i64,
774 pub msg: String,
776}
777
778#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
780#[serde(untagged)]
781pub enum BatchCancelResult {
782 Success(Box<BatchCancelSuccess>),
784 Error(BatchOrderError),
786}
787
788#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
790#[serde(rename_all = "camelCase")]
791pub struct BatchCancelSuccess {
792 pub symbol: String,
794 pub orig_client_order_id: String,
796 pub order_id: i64,
798 #[serde(default)]
800 pub order_list_id: Option<i64>,
801 pub client_order_id: String,
803 #[serde(default)]
805 pub transact_time: Option<i64>,
806 pub price: String,
808 pub orig_qty: String,
810 pub executed_qty: String,
812 #[serde(rename = "cummulativeQuoteQty")]
814 pub cummulative_quote_qty: String,
815 pub status: BinanceOrderStatus,
817 pub time_in_force: BinanceTimeInForce,
819 #[serde(rename = "type")]
821 pub order_type: String,
822 pub side: BinanceSide,
824 #[serde(default)]
826 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
827}
828
829#[derive(Debug, Clone, PartialEq)]
831pub struct BinanceKline {
832 pub open_time: i64,
834 pub open_price: i64,
836 pub high_price: i64,
838 pub low_price: i64,
840 pub close_price: i64,
842 pub volume: [u8; 16],
844 pub close_time: i64,
846 pub quote_volume: [u8; 16],
848 pub num_trades: i64,
850 pub taker_buy_base_volume: [u8; 16],
852 pub taker_buy_quote_volume: [u8; 16],
854}
855
856#[cfg(test)]
857mod tests {
858 use rstest::rstest;
859
860 use super::*;
861 use crate::common::testing::load_fixture_string;
862
863 #[rstest]
864 fn test_listen_key_response_deserialize() {
865 let json = r#"{"listenKey": "abc123xyz"}"#;
866 let response: ListenKeyResponse = serde_json::from_str(json).unwrap();
867 assert_eq!(response.listen_key, "abc123xyz");
868 }
869
870 #[rstest]
871 fn test_ticker_price_deserialize() {
872 let json = load_fixture_string("spot/http_json/ticker_price_response.json");
873 let response: TickerPrice = serde_json::from_str(&json).unwrap();
874 assert_eq!(response.symbol, "LTCBTC");
875 assert_eq!(response.price, "4.00000200");
876 }
877
878 #[rstest]
879 fn test_book_ticker_deserialize() {
880 let json = load_fixture_string("spot/http_json/book_ticker_response.json");
881 let response: BookTicker = serde_json::from_str(&json).unwrap();
882 assert_eq!(response.symbol, "LTCBTC");
883 assert_eq!(response.bid_price, "4.00000000");
884 assert_eq!(response.ask_price, "4.00000200");
885 }
886
887 #[rstest]
888 fn test_avg_price_deserialize() {
889 let json = load_fixture_string("spot/http_json/avg_price_response.json");
890 let response: AvgPrice = serde_json::from_str(&json).unwrap();
891 assert_eq!(response.mins, 5);
892 assert_eq!(response.price, "9.35751834");
893 assert_eq!(response.close_time, 1694061154503);
894 }
895
896 #[rstest]
897 fn test_trade_fee_deserialize() {
898 let json = r#"{
899 "symbol": "BTCUSDT",
900 "makerCommission": "0.001",
901 "takerCommission": "0.001"
902 }"#;
903 let response: TradeFee = serde_json::from_str(json).unwrap();
904 assert_eq!(response.symbol, "BTCUSDT");
905 assert_eq!(response.maker_commission, "0.001");
906 assert_eq!(response.taker_commission, "0.001");
907 }
908
909 #[rstest]
910 fn test_batch_order_result_success() {
911 let json = load_fixture_string("spot/http_json/new_order_full_response.json");
912 let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
913 match result {
914 BatchOrderResult::Success(order) => {
915 assert_eq!(order.symbol, "BTCUSDT");
916 assert_eq!(order.order_id, 28);
917 assert_eq!(order.status, BinanceOrderStatus::Filled);
918 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
919 assert_eq!(order.order_type, "MARKET");
920 assert_eq!(order.side, BinanceSide::Sell);
921 assert_eq!(
922 order.self_trade_prevention_mode,
923 Some(BinanceSelfTradePreventionMode::None)
924 );
925 }
926 BatchOrderResult::Error(_) => panic!("Expected Success"),
927 }
928 }
929
930 #[rstest]
931 fn test_batch_order_result_error() {
932 let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
933 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
934 match result {
935 BatchOrderResult::Success(_) => panic!("Expected Error"),
936 BatchOrderResult::Error(error) => {
937 assert_eq!(error.code, -1013);
938 assert_eq!(error.msg, "Invalid quantity.");
939 }
940 }
941 }
942
943 #[rstest]
944 fn test_batch_cancel_result_success() {
945 let json = load_fixture_string("spot/http_json/cancel_order_response.json");
946 let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
947 match result {
948 BatchCancelResult::Success(cancel) => {
949 assert_eq!(cancel.symbol, "LTCBTC");
950 assert_eq!(cancel.order_id, 4);
951 assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
952 assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
953 assert_eq!(cancel.order_type, "LIMIT");
954 assert_eq!(cancel.side, BinanceSide::Buy);
955 assert_eq!(
956 cancel.self_trade_prevention_mode,
957 Some(BinanceSelfTradePreventionMode::None)
958 );
959 }
960 BatchCancelResult::Error(_) => panic!("Expected Success"),
961 }
962 }
963
964 #[rstest]
965 fn test_batch_cancel_result_error() {
966 let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
967 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
968 match result {
969 BatchCancelResult::Success(_) => panic!("Expected Error"),
970 BatchCancelResult::Error(error) => {
971 assert_eq!(error.code, -2011);
972 assert_eq!(error.msg, "Unknown order sent.");
973 }
974 }
975 }
976
977 #[rstest]
978 fn test_ticker_24hr_deserialize() {
979 let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
980 let response: Ticker24hr = serde_json::from_str(&json).unwrap();
981 assert_eq!(response.symbol, "BNBBTC");
982 assert_eq!(response.last_price, "4.00000200");
983 assert_eq!(response.count, 76);
984 }
985}