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 pub expiry_reason: Option<u8>,
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub struct BinanceCancelOrderResponse {
158 pub price_exponent: i8,
160 pub qty_exponent: i8,
162 pub order_id: i64,
164 pub order_list_id: Option<i64>,
166 pub transact_time: i64,
168 pub price_mantissa: i64,
170 pub orig_qty_mantissa: i64,
172 pub executed_qty_mantissa: i64,
174 pub cummulative_quote_qty_mantissa: i64,
176 pub status: OrderStatus,
178 pub time_in_force: TimeInForce,
180 pub order_type: OrderType,
182 pub side: OrderSide,
184 pub self_trade_prevention_mode: SelfTradePreventionMode,
186 pub client_order_id: String,
188 pub orig_client_order_id: String,
190 pub symbol: String,
192}
193
194#[derive(Debug, Clone, PartialEq)]
196pub struct BinanceOrderResponse {
197 pub price_exponent: i8,
199 pub qty_exponent: i8,
201 pub order_id: i64,
203 pub order_list_id: Option<i64>,
205 pub price_mantissa: i64,
207 pub orig_qty_mantissa: i64,
209 pub executed_qty_mantissa: i64,
211 pub cummulative_quote_qty_mantissa: i64,
213 pub status: OrderStatus,
215 pub time_in_force: TimeInForce,
217 pub order_type: OrderType,
219 pub side: OrderSide,
221 pub stop_price_mantissa: Option<i64>,
223 pub iceberg_qty_mantissa: Option<i64>,
225 pub time: i64,
227 pub update_time: i64,
229 pub is_working: bool,
231 pub working_time: Option<i64>,
233 pub orig_quote_order_qty_mantissa: i64,
235 pub self_trade_prevention_mode: SelfTradePreventionMode,
237 pub client_order_id: String,
239 pub symbol: String,
241 pub expiry_reason: Option<u8>,
243}
244
245#[derive(Debug, Clone, PartialEq)]
247pub struct BinanceBalance {
248 pub asset: String,
250 pub free_mantissa: i64,
252 pub locked_mantissa: i64,
254 pub exponent: i8,
256}
257
258#[derive(Debug, Clone, PartialEq)]
260pub struct BinanceAccountInfo {
261 pub commission_exponent: i8,
263 pub maker_commission_mantissa: i64,
265 pub taker_commission_mantissa: i64,
267 pub buyer_commission_mantissa: i64,
269 pub seller_commission_mantissa: i64,
271 pub can_trade: bool,
273 pub can_withdraw: bool,
275 pub can_deposit: bool,
277 pub require_self_trade_prevention: bool,
279 pub prevent_sor: bool,
281 pub update_time: i64,
283 pub account_type: String,
285 pub balances: Vec<BinanceBalance>,
287}
288
289impl BinanceAccountInfo {
290 #[must_use]
292 pub fn to_account_state(&self, account_id: AccountId, ts_init: UnixNanos) -> AccountState {
293 let mut balances = Vec::with_capacity(self.balances.len());
294
295 for asset in &self.balances {
296 let currency =
297 Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
298
299 let exponent = asset.exponent as i32;
300 let multiplier = Decimal::new(1, (-exponent) as u32);
301
302 let free = Decimal::new(asset.free_mantissa, 0) * multiplier;
303 let locked = Decimal::new(asset.locked_mantissa, 0) * multiplier;
304 let total = free + locked;
305
306 match AccountBalance::from_total_and_locked(total, locked, currency) {
307 Ok(balance) => balances.push(balance),
308 Err(e) => log::warn!("Skipping spot balance for {}: {e}", currency.code.as_str()),
309 }
310 }
311
312 if balances.is_empty() {
314 let zero_currency = Currency::USDT();
315 let zero_money = Money::zero(zero_currency);
316 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
317 balances.push(zero_balance);
318 }
319
320 let ts_event = UnixNanos::from_micros(self.update_time as u64);
321
322 AccountState::new(
323 account_id,
324 AccountType::Cash,
325 balances,
326 vec![], true, UUID4::new(),
329 ts_event,
330 ts_init,
331 None, )
333 }
334}
335
336#[derive(Debug, Clone, PartialEq)]
338pub struct BinancePriceFilterSbe {
339 pub price_exponent: i8,
341 pub min_price: i64,
343 pub max_price: i64,
345 pub tick_size: i64,
347}
348
349#[derive(Debug, Clone, PartialEq)]
351pub struct BinanceLotSizeFilterSbe {
352 pub qty_exponent: i8,
354 pub min_qty: i64,
356 pub max_qty: i64,
358 pub step_size: i64,
360}
361
362#[derive(Debug, Clone, Default, PartialEq)]
364pub struct BinanceSymbolFiltersSbe {
365 pub price_filter: Option<BinancePriceFilterSbe>,
367 pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
369}
370
371#[derive(Debug, Clone, PartialEq)]
373pub struct BinanceSymbolSbe {
374 pub symbol: String,
376 pub base_asset: String,
378 pub quote_asset: String,
380 pub base_asset_precision: u8,
382 pub quote_asset_precision: u8,
384 pub status: u8,
386 pub order_types: u16,
388 pub iceberg_allowed: bool,
390 pub oco_allowed: bool,
392 pub oto_allowed: bool,
394 pub quote_order_qty_market_allowed: bool,
396 pub allow_trailing_stop: bool,
398 pub cancel_replace_allowed: bool,
400 pub amend_allowed: bool,
402 pub is_spot_trading_allowed: bool,
404 pub is_margin_trading_allowed: bool,
406 pub filters: BinanceSymbolFiltersSbe,
408 pub permissions: Vec<Vec<String>>,
410}
411
412#[derive(Debug, Clone, PartialEq)]
414pub struct BinanceExchangeInfoSbe {
415 pub symbols: Vec<BinanceSymbolSbe>,
417}
418
419#[derive(Debug, Clone, PartialEq)]
421pub struct BinanceAccountTrade {
422 pub price_exponent: i8,
424 pub qty_exponent: i8,
426 pub commission_exponent: i8,
428 pub id: i64,
430 pub order_id: i64,
432 pub order_list_id: Option<i64>,
434 pub price_mantissa: i64,
436 pub qty_mantissa: i64,
438 pub quote_qty_mantissa: i64,
440 pub commission_mantissa: i64,
442 pub time: i64,
444 pub is_buyer: bool,
446 pub is_maker: bool,
448 pub is_best_match: bool,
450 pub symbol: String,
452 pub commission_asset: String,
454}
455
456#[derive(Debug, Clone, PartialEq)]
458pub struct BinanceKlines {
459 pub price_exponent: i8,
461 pub qty_exponent: i8,
463 pub klines: Vec<BinanceKline>,
465}
466
467#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct ListenKeyResponse {
471 pub listen_key: String,
473}
474
475#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct Ticker24hr {
479 pub symbol: String,
481 pub price_change: String,
483 pub price_change_percent: String,
485 pub weighted_avg_price: String,
487 pub prev_close_price: String,
489 pub last_price: String,
491 pub last_qty: String,
493 pub bid_price: String,
495 pub bid_qty: String,
497 pub ask_price: String,
499 pub ask_qty: String,
501 pub open_price: String,
503 pub high_price: String,
505 pub low_price: String,
507 pub volume: String,
509 pub quote_volume: String,
511 pub open_time: i64,
513 pub close_time: i64,
515 pub first_id: i64,
517 pub last_id: i64,
519 pub count: i64,
521}
522
523#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
525pub struct TickerPrice {
526 pub symbol: String,
528 pub price: String,
530}
531
532#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct BookTicker {
536 pub symbol: String,
538 pub bid_price: String,
540 pub bid_qty: String,
542 pub ask_price: String,
544 pub ask_qty: String,
546}
547
548#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
550pub struct AvgPrice {
551 pub mins: i64,
553 pub price: String,
555 #[serde(rename = "closeTime")]
557 pub close_time: i64,
558}
559
560#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
562#[serde(rename_all = "camelCase")]
563pub struct TradeFee {
564 pub symbol: String,
566 pub maker_commission: String,
568 pub taker_commission: String,
570}
571
572#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct NewOcoOrderListResponse {
576 pub order_list_id: i64,
578 pub contingency_type: String,
580 pub list_status_type: String,
582 pub list_order_status: String,
584 pub list_client_order_id: String,
586 pub transaction_time: i64,
588 pub symbol: String,
590 pub orders: Vec<OrderListOrder>,
592}
593
594#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct OrderListOrder {
598 pub symbol: String,
600 pub order_id: i64,
602 pub client_order_id: String,
604}
605
606#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
610#[serde(untagged)]
611pub enum BatchOrderResult {
612 Success(Box<BatchOrderSuccess>),
614 Error(BatchOrderError),
616}
617
618#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
620#[serde(rename_all = "camelCase")]
621pub struct BatchOrderSuccess {
622 pub symbol: String,
624 pub order_id: i64,
626 #[serde(default)]
628 pub order_list_id: Option<i64>,
629 pub client_order_id: String,
631 pub transact_time: i64,
633 pub price: String,
635 pub orig_qty: String,
637 pub executed_qty: String,
639 #[serde(rename = "cummulativeQuoteQty")]
641 pub cummulative_quote_qty: String,
642 pub status: BinanceOrderStatus,
644 pub time_in_force: BinanceTimeInForce,
646 #[serde(rename = "type")]
648 pub order_type: String,
649 pub side: BinanceSide,
651 #[serde(default)]
653 pub working_time: Option<i64>,
654 #[serde(default)]
656 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
657}
658
659#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
661pub struct BatchOrderError {
662 pub code: i64,
664 pub msg: String,
666}
667
668#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
670#[serde(untagged)]
671pub enum BatchCancelResult {
672 Success(Box<BatchCancelSuccess>),
674 Error(BatchOrderError),
676}
677
678#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
680#[serde(rename_all = "camelCase")]
681pub struct BatchCancelSuccess {
682 pub symbol: String,
684 pub orig_client_order_id: String,
686 pub order_id: i64,
688 #[serde(default)]
690 pub order_list_id: Option<i64>,
691 pub client_order_id: String,
693 #[serde(default)]
695 pub transact_time: Option<i64>,
696 pub price: String,
698 pub orig_qty: String,
700 pub executed_qty: String,
702 #[serde(rename = "cummulativeQuoteQty")]
704 pub cummulative_quote_qty: String,
705 pub status: BinanceOrderStatus,
707 pub time_in_force: BinanceTimeInForce,
709 #[serde(rename = "type")]
711 pub order_type: String,
712 pub side: BinanceSide,
714 #[serde(default)]
716 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
717}
718
719#[derive(Debug, Clone, PartialEq)]
721pub struct BinanceKline {
722 pub open_time: i64,
724 pub open_price: i64,
726 pub high_price: i64,
728 pub low_price: i64,
730 pub close_price: i64,
732 pub volume: [u8; 16],
734 pub close_time: i64,
736 pub quote_volume: [u8; 16],
738 pub num_trades: i64,
740 pub taker_buy_base_volume: [u8; 16],
742 pub taker_buy_quote_volume: [u8; 16],
744}
745
746#[cfg(test)]
747mod tests {
748 use rstest::rstest;
749
750 use super::*;
751 use crate::common::testing::load_fixture_string;
752
753 #[rstest]
754 fn test_listen_key_response_deserialize() {
755 let json = r#"{"listenKey": "abc123xyz"}"#;
756 let response: ListenKeyResponse = serde_json::from_str(json).unwrap();
757 assert_eq!(response.listen_key, "abc123xyz");
758 }
759
760 #[rstest]
761 fn test_ticker_price_deserialize() {
762 let json = load_fixture_string("spot/http_json/ticker_price_response.json");
763 let response: TickerPrice = serde_json::from_str(&json).unwrap();
764 assert_eq!(response.symbol, "LTCBTC");
765 assert_eq!(response.price, "4.00000200");
766 }
767
768 #[rstest]
769 fn test_book_ticker_deserialize() {
770 let json = load_fixture_string("spot/http_json/book_ticker_response.json");
771 let response: BookTicker = serde_json::from_str(&json).unwrap();
772 assert_eq!(response.symbol, "LTCBTC");
773 assert_eq!(response.bid_price, "4.00000000");
774 assert_eq!(response.ask_price, "4.00000200");
775 }
776
777 #[rstest]
778 fn test_avg_price_deserialize() {
779 let json = load_fixture_string("spot/http_json/avg_price_response.json");
780 let response: AvgPrice = serde_json::from_str(&json).unwrap();
781 assert_eq!(response.mins, 5);
782 assert_eq!(response.price, "9.35751834");
783 assert_eq!(response.close_time, 1694061154503);
784 }
785
786 #[rstest]
787 fn test_trade_fee_deserialize() {
788 let json = r#"{
789 "symbol": "BTCUSDT",
790 "makerCommission": "0.001",
791 "takerCommission": "0.001"
792 }"#;
793 let response: TradeFee = serde_json::from_str(json).unwrap();
794 assert_eq!(response.symbol, "BTCUSDT");
795 assert_eq!(response.maker_commission, "0.001");
796 assert_eq!(response.taker_commission, "0.001");
797 }
798
799 #[rstest]
800 fn test_batch_order_result_success() {
801 let json = load_fixture_string("spot/http_json/new_order_full_response.json");
802 let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
803 match result {
804 BatchOrderResult::Success(order) => {
805 assert_eq!(order.symbol, "BTCUSDT");
806 assert_eq!(order.order_id, 28);
807 assert_eq!(order.status, BinanceOrderStatus::Filled);
808 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
809 assert_eq!(order.order_type, "MARKET");
810 assert_eq!(order.side, BinanceSide::Sell);
811 assert_eq!(
812 order.self_trade_prevention_mode,
813 Some(BinanceSelfTradePreventionMode::None)
814 );
815 }
816 BatchOrderResult::Error(_) => panic!("Expected Success"),
817 }
818 }
819
820 #[rstest]
821 fn test_batch_order_result_error() {
822 let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
823 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
824 match result {
825 BatchOrderResult::Success(_) => panic!("Expected Error"),
826 BatchOrderResult::Error(error) => {
827 assert_eq!(error.code, -1013);
828 assert_eq!(error.msg, "Invalid quantity.");
829 }
830 }
831 }
832
833 #[rstest]
834 fn test_batch_cancel_result_success() {
835 let json = load_fixture_string("spot/http_json/cancel_order_response.json");
836 let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
837 match result {
838 BatchCancelResult::Success(cancel) => {
839 assert_eq!(cancel.symbol, "LTCBTC");
840 assert_eq!(cancel.order_id, 4);
841 assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
842 assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
843 assert_eq!(cancel.order_type, "LIMIT");
844 assert_eq!(cancel.side, BinanceSide::Buy);
845 assert_eq!(
846 cancel.self_trade_prevention_mode,
847 Some(BinanceSelfTradePreventionMode::None)
848 );
849 }
850 BatchCancelResult::Error(_) => panic!("Expected Success"),
851 }
852 }
853
854 #[rstest]
855 fn test_batch_cancel_result_error() {
856 let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
857 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
858 match result {
859 BatchCancelResult::Success(_) => panic!("Expected Error"),
860 BatchCancelResult::Error(error) => {
861 assert_eq!(error.code, -2011);
862 assert_eq!(error.msg, "Unknown order sent.");
863 }
864 }
865 }
866
867 #[rstest]
868 fn test_ticker_24hr_deserialize() {
869 let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
870 let response: Ticker24hr = serde_json::from_str(&json).unwrap();
871 assert_eq!(response.symbol, "BNBBTC");
872 assert_eq!(response.last_price, "4.00000200");
873 assert_eq!(response.count, 76);
874 }
875}