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::enums::{
31 BinanceOrderStatus, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
32 },
33 spot::sbe::spot::{
34 order_side::OrderSide, order_status::OrderStatus, order_type::OrderType,
35 self_trade_prevention_mode::SelfTradePreventionMode, time_in_force::TimeInForce,
36 },
37};
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct BinancePriceLevel {
42 pub price_mantissa: i64,
44 pub qty_mantissa: i64,
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct BinanceDepth {
51 pub last_update_id: i64,
53 pub price_exponent: i8,
55 pub qty_exponent: i8,
57 pub bids: Vec<BinancePriceLevel>,
59 pub asks: Vec<BinancePriceLevel>,
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct BinanceTrade {
66 pub id: i64,
68 pub price_mantissa: i64,
70 pub qty_mantissa: i64,
72 pub quote_qty_mantissa: i64,
74 pub time: i64,
76 pub is_buyer_maker: bool,
78 pub is_best_match: bool,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct BinanceTrades {
85 pub price_exponent: i8,
87 pub qty_exponent: i8,
89 pub trades: Vec<BinanceTrade>,
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct BinanceOrderFill {
96 pub price_mantissa: i64,
98 pub qty_mantissa: i64,
100 pub commission_mantissa: i64,
102 pub commission_exponent: i8,
104 pub commission_asset: String,
106 pub trade_id: Option<i64>,
108}
109
110#[derive(Debug, Clone, PartialEq)]
112pub struct BinanceNewOrderResponse {
113 pub price_exponent: i8,
115 pub qty_exponent: i8,
117 pub order_id: i64,
119 pub order_list_id: Option<i64>,
121 pub transact_time: i64,
123 pub price_mantissa: i64,
125 pub orig_qty_mantissa: i64,
127 pub executed_qty_mantissa: i64,
129 pub cummulative_quote_qty_mantissa: i64,
131 pub status: OrderStatus,
133 pub time_in_force: TimeInForce,
135 pub order_type: OrderType,
137 pub side: OrderSide,
139 pub stop_price_mantissa: Option<i64>,
141 pub working_time: Option<i64>,
143 pub self_trade_prevention_mode: SelfTradePreventionMode,
145 pub client_order_id: String,
147 pub symbol: String,
149 pub fills: Vec<BinanceOrderFill>,
151}
152
153#[derive(Debug, Clone, PartialEq)]
155pub struct BinanceCancelOrderResponse {
156 pub price_exponent: i8,
158 pub qty_exponent: i8,
160 pub order_id: i64,
162 pub order_list_id: Option<i64>,
164 pub transact_time: i64,
166 pub price_mantissa: i64,
168 pub orig_qty_mantissa: i64,
170 pub executed_qty_mantissa: i64,
172 pub cummulative_quote_qty_mantissa: i64,
174 pub status: OrderStatus,
176 pub time_in_force: TimeInForce,
178 pub order_type: OrderType,
180 pub side: OrderSide,
182 pub self_trade_prevention_mode: SelfTradePreventionMode,
184 pub client_order_id: String,
186 pub orig_client_order_id: String,
188 pub symbol: String,
190}
191
192#[derive(Debug, Clone, PartialEq)]
194pub struct BinanceOrderResponse {
195 pub price_exponent: i8,
197 pub qty_exponent: i8,
199 pub order_id: i64,
201 pub order_list_id: Option<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 stop_price_mantissa: Option<i64>,
221 pub iceberg_qty_mantissa: Option<i64>,
223 pub time: i64,
225 pub update_time: i64,
227 pub is_working: bool,
229 pub working_time: Option<i64>,
231 pub orig_quote_order_qty_mantissa: i64,
233 pub self_trade_prevention_mode: SelfTradePreventionMode,
235 pub client_order_id: String,
237 pub symbol: String,
239}
240
241#[derive(Debug, Clone, PartialEq)]
243pub struct BinanceBalance {
244 pub asset: String,
246 pub free_mantissa: i64,
248 pub locked_mantissa: i64,
250 pub exponent: i8,
252}
253
254#[derive(Debug, Clone, PartialEq)]
256pub struct BinanceAccountInfo {
257 pub commission_exponent: i8,
259 pub maker_commission_mantissa: i64,
261 pub taker_commission_mantissa: i64,
263 pub buyer_commission_mantissa: i64,
265 pub seller_commission_mantissa: i64,
267 pub can_trade: bool,
269 pub can_withdraw: bool,
271 pub can_deposit: bool,
273 pub require_self_trade_prevention: bool,
275 pub prevent_sor: bool,
277 pub update_time: i64,
279 pub account_type: String,
281 pub balances: Vec<BinanceBalance>,
283}
284
285impl BinanceAccountInfo {
286 #[must_use]
288 pub fn to_account_state(&self, account_id: AccountId, ts_init: UnixNanos) -> AccountState {
289 let mut balances = Vec::with_capacity(self.balances.len());
290
291 for asset in &self.balances {
292 let currency =
293 Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
294
295 let exponent = asset.exponent as i32;
296 let multiplier = Decimal::new(1, (-exponent) as u32);
297
298 let free = Decimal::new(asset.free_mantissa, 0) * multiplier;
299 let locked = Decimal::new(asset.locked_mantissa, 0) * multiplier;
300 let total = free + locked;
301
302 match AccountBalance::from_total_and_locked(total, locked, currency) {
303 Ok(balance) => balances.push(balance),
304 Err(e) => log::warn!("Skipping spot balance for {}: {e}", currency.code.as_str()),
305 }
306 }
307
308 if balances.is_empty() {
310 let zero_currency = Currency::USDT();
311 let zero_money = Money::new(0.0, zero_currency);
312 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
313 balances.push(zero_balance);
314 }
315
316 let ts_event = UnixNanos::from_micros(self.update_time as u64);
317
318 AccountState::new(
319 account_id,
320 AccountType::Cash,
321 balances,
322 vec![], true, UUID4::new(),
325 ts_event,
326 ts_init,
327 None, )
329 }
330}
331
332#[derive(Debug, Clone, PartialEq)]
334pub struct BinancePriceFilterSbe {
335 pub price_exponent: i8,
337 pub min_price: i64,
339 pub max_price: i64,
341 pub tick_size: i64,
343}
344
345#[derive(Debug, Clone, PartialEq)]
347pub struct BinanceLotSizeFilterSbe {
348 pub qty_exponent: i8,
350 pub min_qty: i64,
352 pub max_qty: i64,
354 pub step_size: i64,
356}
357
358#[derive(Debug, Clone, Default, PartialEq)]
360pub struct BinanceSymbolFiltersSbe {
361 pub price_filter: Option<BinancePriceFilterSbe>,
363 pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
365}
366
367#[derive(Debug, Clone, PartialEq)]
369pub struct BinanceSymbolSbe {
370 pub symbol: String,
372 pub base_asset: String,
374 pub quote_asset: String,
376 pub base_asset_precision: u8,
378 pub quote_asset_precision: u8,
380 pub status: u8,
382 pub order_types: u16,
384 pub iceberg_allowed: bool,
386 pub oco_allowed: bool,
388 pub oto_allowed: bool,
390 pub quote_order_qty_market_allowed: bool,
392 pub allow_trailing_stop: bool,
394 pub cancel_replace_allowed: bool,
396 pub amend_allowed: bool,
398 pub is_spot_trading_allowed: bool,
400 pub is_margin_trading_allowed: bool,
402 pub filters: BinanceSymbolFiltersSbe,
404 pub permissions: Vec<Vec<String>>,
406}
407
408#[derive(Debug, Clone, PartialEq)]
410pub struct BinanceExchangeInfoSbe {
411 pub symbols: Vec<BinanceSymbolSbe>,
413}
414
415#[derive(Debug, Clone, PartialEq)]
417pub struct BinanceAccountTrade {
418 pub price_exponent: i8,
420 pub qty_exponent: i8,
422 pub commission_exponent: i8,
424 pub id: i64,
426 pub order_id: i64,
428 pub order_list_id: Option<i64>,
430 pub price_mantissa: i64,
432 pub qty_mantissa: i64,
434 pub quote_qty_mantissa: i64,
436 pub commission_mantissa: i64,
438 pub time: i64,
440 pub is_buyer: bool,
442 pub is_maker: bool,
444 pub is_best_match: bool,
446 pub symbol: String,
448 pub commission_asset: String,
450}
451
452#[derive(Debug, Clone, PartialEq)]
454pub struct BinanceKlines {
455 pub price_exponent: i8,
457 pub qty_exponent: i8,
459 pub klines: Vec<BinanceKline>,
461}
462
463#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct ListenKeyResponse {
467 pub listen_key: String,
469}
470
471#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct Ticker24hr {
475 pub symbol: String,
477 pub price_change: String,
479 pub price_change_percent: String,
481 pub weighted_avg_price: String,
483 pub prev_close_price: String,
485 pub last_price: String,
487 pub last_qty: String,
489 pub bid_price: String,
491 pub bid_qty: String,
493 pub ask_price: String,
495 pub ask_qty: String,
497 pub open_price: String,
499 pub high_price: String,
501 pub low_price: String,
503 pub volume: String,
505 pub quote_volume: String,
507 pub open_time: i64,
509 pub close_time: i64,
511 pub first_id: i64,
513 pub last_id: i64,
515 pub count: i64,
517}
518
519#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
521pub struct TickerPrice {
522 pub symbol: String,
524 pub price: String,
526}
527
528#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
530#[serde(rename_all = "camelCase")]
531pub struct BookTicker {
532 pub symbol: String,
534 pub bid_price: String,
536 pub bid_qty: String,
538 pub ask_price: String,
540 pub ask_qty: String,
542}
543
544#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
546pub struct AvgPrice {
547 pub mins: i64,
549 pub price: String,
551 #[serde(rename = "closeTime")]
553 pub close_time: i64,
554}
555
556#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
558#[serde(rename_all = "camelCase")]
559pub struct TradeFee {
560 pub symbol: String,
562 pub maker_commission: String,
564 pub taker_commission: String,
566}
567
568#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
572#[serde(untagged)]
573pub enum BatchOrderResult {
574 Success(Box<BatchOrderSuccess>),
576 Error(BatchOrderError),
578}
579
580#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub struct BatchOrderSuccess {
584 pub symbol: String,
586 pub order_id: i64,
588 #[serde(default)]
590 pub order_list_id: Option<i64>,
591 pub client_order_id: String,
593 pub transact_time: i64,
595 pub price: String,
597 pub orig_qty: String,
599 pub executed_qty: String,
601 #[serde(rename = "cummulativeQuoteQty")]
603 pub cummulative_quote_qty: String,
604 pub status: BinanceOrderStatus,
606 pub time_in_force: BinanceTimeInForce,
608 #[serde(rename = "type")]
610 pub order_type: String,
611 pub side: BinanceSide,
613 #[serde(default)]
615 pub working_time: Option<i64>,
616 #[serde(default)]
618 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
619}
620
621#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
623pub struct BatchOrderError {
624 pub code: i64,
626 pub msg: String,
628}
629
630#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
632#[serde(untagged)]
633pub enum BatchCancelResult {
634 Success(Box<BatchCancelSuccess>),
636 Error(BatchOrderError),
638}
639
640#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub struct BatchCancelSuccess {
644 pub symbol: String,
646 pub orig_client_order_id: String,
648 pub order_id: i64,
650 #[serde(default)]
652 pub order_list_id: Option<i64>,
653 pub client_order_id: String,
655 #[serde(default)]
657 pub transact_time: Option<i64>,
658 pub price: String,
660 pub orig_qty: String,
662 pub executed_qty: String,
664 #[serde(rename = "cummulativeQuoteQty")]
666 pub cummulative_quote_qty: String,
667 pub status: BinanceOrderStatus,
669 pub time_in_force: BinanceTimeInForce,
671 #[serde(rename = "type")]
673 pub order_type: String,
674 pub side: BinanceSide,
676 #[serde(default)]
678 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
679}
680
681#[derive(Debug, Clone, PartialEq)]
683pub struct BinanceKline {
684 pub open_time: i64,
686 pub open_price: i64,
688 pub high_price: i64,
690 pub low_price: i64,
692 pub close_price: i64,
694 pub volume: [u8; 16],
696 pub close_time: i64,
698 pub quote_volume: [u8; 16],
700 pub num_trades: i64,
702 pub taker_buy_base_volume: [u8; 16],
704 pub taker_buy_quote_volume: [u8; 16],
706}
707
708#[cfg(test)]
709mod tests {
710 use rstest::rstest;
711
712 use super::*;
713 use crate::common::testing::load_fixture_string;
714
715 #[rstest]
716 fn test_listen_key_response_deserialize() {
717 let json = r#"{"listenKey": "abc123xyz"}"#;
718 let response: ListenKeyResponse = serde_json::from_str(json).unwrap();
719 assert_eq!(response.listen_key, "abc123xyz");
720 }
721
722 #[rstest]
723 fn test_ticker_price_deserialize() {
724 let json = load_fixture_string("spot/http_json/ticker_price_response.json");
725 let response: TickerPrice = serde_json::from_str(&json).unwrap();
726 assert_eq!(response.symbol, "LTCBTC");
727 assert_eq!(response.price, "4.00000200");
728 }
729
730 #[rstest]
731 fn test_book_ticker_deserialize() {
732 let json = load_fixture_string("spot/http_json/book_ticker_response.json");
733 let response: BookTicker = serde_json::from_str(&json).unwrap();
734 assert_eq!(response.symbol, "LTCBTC");
735 assert_eq!(response.bid_price, "4.00000000");
736 assert_eq!(response.ask_price, "4.00000200");
737 }
738
739 #[rstest]
740 fn test_avg_price_deserialize() {
741 let json = load_fixture_string("spot/http_json/avg_price_response.json");
742 let response: AvgPrice = serde_json::from_str(&json).unwrap();
743 assert_eq!(response.mins, 5);
744 assert_eq!(response.price, "9.35751834");
745 assert_eq!(response.close_time, 1694061154503);
746 }
747
748 #[rstest]
749 fn test_trade_fee_deserialize() {
750 let json = r#"{
751 "symbol": "BTCUSDT",
752 "makerCommission": "0.001",
753 "takerCommission": "0.001"
754 }"#;
755 let response: TradeFee = serde_json::from_str(json).unwrap();
756 assert_eq!(response.symbol, "BTCUSDT");
757 assert_eq!(response.maker_commission, "0.001");
758 assert_eq!(response.taker_commission, "0.001");
759 }
760
761 #[rstest]
762 fn test_batch_order_result_success() {
763 let json = load_fixture_string("spot/http_json/new_order_full_response.json");
764 let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
765 match result {
766 BatchOrderResult::Success(order) => {
767 assert_eq!(order.symbol, "BTCUSDT");
768 assert_eq!(order.order_id, 28);
769 assert_eq!(order.status, BinanceOrderStatus::Filled);
770 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
771 assert_eq!(order.order_type, "MARKET");
772 assert_eq!(order.side, BinanceSide::Sell);
773 assert_eq!(
774 order.self_trade_prevention_mode,
775 Some(BinanceSelfTradePreventionMode::None)
776 );
777 }
778 BatchOrderResult::Error(_) => panic!("Expected Success"),
779 }
780 }
781
782 #[rstest]
783 fn test_batch_order_result_error() {
784 let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
785 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
786 match result {
787 BatchOrderResult::Success(_) => panic!("Expected Error"),
788 BatchOrderResult::Error(error) => {
789 assert_eq!(error.code, -1013);
790 assert_eq!(error.msg, "Invalid quantity.");
791 }
792 }
793 }
794
795 #[rstest]
796 fn test_batch_cancel_result_success() {
797 let json = load_fixture_string("spot/http_json/cancel_order_response.json");
798 let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
799 match result {
800 BatchCancelResult::Success(cancel) => {
801 assert_eq!(cancel.symbol, "LTCBTC");
802 assert_eq!(cancel.order_id, 4);
803 assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
804 assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
805 assert_eq!(cancel.order_type, "LIMIT");
806 assert_eq!(cancel.side, BinanceSide::Buy);
807 assert_eq!(
808 cancel.self_trade_prevention_mode,
809 Some(BinanceSelfTradePreventionMode::None)
810 );
811 }
812 BatchCancelResult::Error(_) => panic!("Expected Success"),
813 }
814 }
815
816 #[rstest]
817 fn test_batch_cancel_result_error() {
818 let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
819 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
820 match result {
821 BatchCancelResult::Success(_) => panic!("Expected Error"),
822 BatchCancelResult::Error(error) => {
823 assert_eq!(error.code, -2011);
824 assert_eq!(error.msg, "Unknown order sent.");
825 }
826 }
827 }
828
829 #[rstest]
830 fn test_ticker_24hr_deserialize() {
831 let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
832 let response: Ticker24hr = serde_json::from_str(&json).unwrap();
833 assert_eq!(response.symbol, "BNBBTC");
834 assert_eq!(response.last_price, "4.00000200");
835 assert_eq!(response.count, 76);
836 }
837}