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::common::sbe::spot::{
30 order_side::OrderSide, order_status::OrderStatus, order_type::OrderType,
31 self_trade_prevention_mode::SelfTradePreventionMode, time_in_force::TimeInForce,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct BinancePriceLevel {
37 pub price_mantissa: i64,
39 pub qty_mantissa: i64,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct BinanceDepth {
46 pub last_update_id: i64,
48 pub price_exponent: i8,
50 pub qty_exponent: i8,
52 pub bids: Vec<BinancePriceLevel>,
54 pub asks: Vec<BinancePriceLevel>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub struct BinanceTrade {
61 pub id: i64,
63 pub price_mantissa: i64,
65 pub qty_mantissa: i64,
67 pub quote_qty_mantissa: i64,
69 pub time: i64,
71 pub is_buyer_maker: bool,
73 pub is_best_match: bool,
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct BinanceTrades {
80 pub price_exponent: i8,
82 pub qty_exponent: i8,
84 pub trades: Vec<BinanceTrade>,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct BinanceOrderFill {
91 pub price_mantissa: i64,
93 pub qty_mantissa: i64,
95 pub commission_mantissa: i64,
97 pub commission_exponent: i8,
99 pub commission_asset: String,
101 pub trade_id: Option<i64>,
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub struct BinanceNewOrderResponse {
108 pub price_exponent: i8,
110 pub qty_exponent: i8,
112 pub order_id: i64,
114 pub order_list_id: Option<i64>,
116 pub transact_time: i64,
118 pub price_mantissa: i64,
120 pub orig_qty_mantissa: i64,
122 pub executed_qty_mantissa: i64,
124 pub cummulative_quote_qty_mantissa: i64,
126 pub status: OrderStatus,
128 pub time_in_force: TimeInForce,
130 pub order_type: OrderType,
132 pub side: OrderSide,
134 pub stop_price_mantissa: Option<i64>,
136 pub working_time: Option<i64>,
138 pub self_trade_prevention_mode: SelfTradePreventionMode,
140 pub client_order_id: String,
142 pub symbol: String,
144 pub fills: Vec<BinanceOrderFill>,
146}
147
148#[derive(Debug, Clone, PartialEq)]
150pub struct BinanceCancelOrderResponse {
151 pub price_exponent: i8,
153 pub qty_exponent: i8,
155 pub order_id: i64,
157 pub order_list_id: Option<i64>,
159 pub transact_time: i64,
161 pub price_mantissa: i64,
163 pub orig_qty_mantissa: i64,
165 pub executed_qty_mantissa: i64,
167 pub cummulative_quote_qty_mantissa: i64,
169 pub status: OrderStatus,
171 pub time_in_force: TimeInForce,
173 pub order_type: OrderType,
175 pub side: OrderSide,
177 pub self_trade_prevention_mode: SelfTradePreventionMode,
179 pub client_order_id: String,
181 pub orig_client_order_id: String,
183 pub symbol: String,
185}
186
187#[derive(Debug, Clone, PartialEq)]
189pub struct BinanceOrderResponse {
190 pub price_exponent: i8,
192 pub qty_exponent: i8,
194 pub order_id: i64,
196 pub order_list_id: Option<i64>,
198 pub price_mantissa: i64,
200 pub orig_qty_mantissa: i64,
202 pub executed_qty_mantissa: i64,
204 pub cummulative_quote_qty_mantissa: i64,
206 pub status: OrderStatus,
208 pub time_in_force: TimeInForce,
210 pub order_type: OrderType,
212 pub side: OrderSide,
214 pub stop_price_mantissa: Option<i64>,
216 pub iceberg_qty_mantissa: Option<i64>,
218 pub time: i64,
220 pub update_time: i64,
222 pub is_working: bool,
224 pub working_time: Option<i64>,
226 pub orig_quote_order_qty_mantissa: i64,
228 pub self_trade_prevention_mode: SelfTradePreventionMode,
230 pub client_order_id: String,
232 pub symbol: String,
234}
235
236#[derive(Debug, Clone, PartialEq)]
238pub struct BinanceBalance {
239 pub asset: String,
241 pub free_mantissa: i64,
243 pub locked_mantissa: i64,
245 pub exponent: i8,
247}
248
249#[derive(Debug, Clone, PartialEq)]
251pub struct BinanceAccountInfo {
252 pub commission_exponent: i8,
254 pub maker_commission_mantissa: i64,
256 pub taker_commission_mantissa: i64,
258 pub buyer_commission_mantissa: i64,
260 pub seller_commission_mantissa: i64,
262 pub can_trade: bool,
264 pub can_withdraw: bool,
266 pub can_deposit: bool,
268 pub require_self_trade_prevention: bool,
270 pub prevent_sor: bool,
272 pub update_time: i64,
274 pub account_type: String,
276 pub balances: Vec<BinanceBalance>,
278}
279
280impl BinanceAccountInfo {
281 #[must_use]
283 pub fn to_account_state(&self, account_id: AccountId, ts_init: UnixNanos) -> AccountState {
284 let mut balances = Vec::with_capacity(self.balances.len());
285
286 for asset in &self.balances {
287 let currency =
288 Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
289
290 let exponent = asset.exponent as i32;
291 let multiplier = Decimal::new(1, (-exponent) as u32);
292
293 let free = Decimal::new(asset.free_mantissa, 0) * multiplier;
294 let locked = Decimal::new(asset.locked_mantissa, 0) * multiplier;
295 let total = free + locked;
296
297 let total_money = Money::from_decimal(total, currency)
298 .unwrap_or_else(|_| Money::new(total.to_string().parse().unwrap_or(0.0), currency));
299 let locked_money = Money::from_decimal(locked, currency).unwrap_or_else(|_| {
300 Money::new(locked.to_string().parse().unwrap_or(0.0), currency)
301 });
302 let free_money = Money::from_decimal(free, currency)
303 .unwrap_or_else(|_| Money::new(free.to_string().parse().unwrap_or(0.0), currency));
304
305 let balance = AccountBalance::new(total_money, locked_money, free_money);
306 balances.push(balance);
307 }
308
309 if balances.is_empty() {
311 let zero_currency = Currency::USDT();
312 let zero_money = Money::new(0.0, zero_currency);
313 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
314 balances.push(zero_balance);
315 }
316
317 let ts_event = UnixNanos::from((self.update_time * 1_000) as u64);
318
319 AccountState::new(
320 account_id,
321 AccountType::Cash,
322 balances,
323 vec![], true, UUID4::new(),
326 ts_event,
327 ts_init,
328 None, )
330 }
331}
332
333#[derive(Debug, Clone, PartialEq)]
335pub struct BinancePriceFilterSbe {
336 pub price_exponent: i8,
338 pub min_price: i64,
340 pub max_price: i64,
342 pub tick_size: i64,
344}
345
346#[derive(Debug, Clone, PartialEq)]
348pub struct BinanceLotSizeFilterSbe {
349 pub qty_exponent: i8,
351 pub min_qty: i64,
353 pub max_qty: i64,
355 pub step_size: i64,
357}
358
359#[derive(Debug, Clone, Default, PartialEq)]
361pub struct BinanceSymbolFiltersSbe {
362 pub price_filter: Option<BinancePriceFilterSbe>,
364 pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
366}
367
368#[derive(Debug, Clone, PartialEq)]
370pub struct BinanceSymbolSbe {
371 pub symbol: String,
373 pub base_asset: String,
375 pub quote_asset: String,
377 pub base_asset_precision: u8,
379 pub quote_asset_precision: u8,
381 pub status: u8,
383 pub order_types: u16,
385 pub iceberg_allowed: bool,
387 pub oco_allowed: bool,
389 pub oto_allowed: bool,
391 pub quote_order_qty_market_allowed: bool,
393 pub allow_trailing_stop: bool,
395 pub cancel_replace_allowed: bool,
397 pub amend_allowed: bool,
399 pub is_spot_trading_allowed: bool,
401 pub is_margin_trading_allowed: bool,
403 pub filters: BinanceSymbolFiltersSbe,
405 pub permissions: Vec<Vec<String>>,
407}
408
409#[derive(Debug, Clone, PartialEq)]
411pub struct BinanceExchangeInfoSbe {
412 pub symbols: Vec<BinanceSymbolSbe>,
414}
415
416#[derive(Debug, Clone, PartialEq)]
418pub struct BinanceAccountTrade {
419 pub price_exponent: i8,
421 pub qty_exponent: i8,
423 pub commission_exponent: i8,
425 pub id: i64,
427 pub order_id: i64,
429 pub order_list_id: Option<i64>,
431 pub price_mantissa: i64,
433 pub qty_mantissa: i64,
435 pub quote_qty_mantissa: i64,
437 pub commission_mantissa: i64,
439 pub time: i64,
441 pub is_buyer: bool,
443 pub is_maker: bool,
445 pub is_best_match: bool,
447 pub symbol: String,
449 pub commission_asset: String,
451}
452
453#[derive(Debug, Clone, PartialEq)]
455pub struct BinanceKlines {
456 pub price_exponent: i8,
458 pub qty_exponent: i8,
460 pub klines: Vec<BinanceKline>,
462}
463
464#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct ListenKeyResponse {
468 pub listen_key: String,
470}
471
472#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct Ticker24hr {
476 pub symbol: String,
478 pub price_change: String,
480 pub price_change_percent: String,
482 pub weighted_avg_price: String,
484 pub prev_close_price: String,
486 pub last_price: String,
488 pub last_qty: String,
490 pub bid_price: String,
492 pub bid_qty: String,
494 pub ask_price: String,
496 pub ask_qty: String,
498 pub open_price: String,
500 pub high_price: String,
502 pub low_price: String,
504 pub volume: String,
506 pub quote_volume: String,
508 pub open_time: i64,
510 pub close_time: i64,
512 pub first_id: i64,
514 pub last_id: i64,
516 pub count: i64,
518}
519
520#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
522pub struct TickerPrice {
523 pub symbol: String,
525 pub price: String,
527}
528
529#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct BookTicker {
533 pub symbol: String,
535 pub bid_price: String,
537 pub bid_qty: String,
539 pub ask_price: String,
541 pub ask_qty: String,
543}
544
545#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
547pub struct AvgPrice {
548 pub mins: i64,
550 pub price: String,
552 #[serde(rename = "closeTime")]
554 pub close_time: i64,
555}
556
557#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct TradeFee {
561 pub symbol: String,
563 pub maker_commission: String,
565 pub taker_commission: String,
567}
568
569#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
573#[serde(untagged)]
574pub enum BatchOrderResult {
575 Success(Box<BatchOrderSuccess>),
577 Error(BatchOrderError),
579}
580
581#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct BatchOrderSuccess {
585 pub symbol: String,
587 pub order_id: i64,
589 #[serde(default)]
591 pub order_list_id: Option<i64>,
592 pub client_order_id: String,
594 pub transact_time: i64,
596 pub price: String,
598 pub orig_qty: String,
600 pub executed_qty: String,
602 #[serde(rename = "cummulativeQuoteQty")]
604 pub cummulative_quote_qty: String,
605 pub status: String,
607 pub time_in_force: String,
609 #[serde(rename = "type")]
611 pub order_type: String,
612 pub side: String,
614 #[serde(default)]
616 pub working_time: Option<i64>,
617 #[serde(default)]
619 pub self_trade_prevention_mode: Option<String>,
620}
621
622#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
624pub struct BatchOrderError {
625 pub code: i64,
627 pub msg: String,
629}
630
631#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
633#[serde(untagged)]
634pub enum BatchCancelResult {
635 Success(Box<BatchCancelSuccess>),
637 Error(BatchOrderError),
639}
640
641#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
643#[serde(rename_all = "camelCase")]
644pub struct BatchCancelSuccess {
645 pub symbol: String,
647 pub orig_client_order_id: String,
649 pub order_id: i64,
651 #[serde(default)]
653 pub order_list_id: Option<i64>,
654 pub client_order_id: String,
656 #[serde(default)]
658 pub transact_time: Option<i64>,
659 pub price: String,
661 pub orig_qty: String,
663 pub executed_qty: String,
665 #[serde(rename = "cummulativeQuoteQty")]
667 pub cummulative_quote_qty: String,
668 pub status: String,
670 pub time_in_force: String,
672 #[serde(rename = "type")]
674 pub order_type: String,
675 pub side: String,
677 #[serde(default)]
679 pub self_trade_prevention_mode: Option<String>,
680}
681
682#[derive(Debug, Clone, PartialEq)]
684pub struct BinanceKline {
685 pub open_time: i64,
687 pub open_price: i64,
689 pub high_price: i64,
691 pub low_price: i64,
693 pub close_price: i64,
695 pub volume: [u8; 16],
697 pub close_time: i64,
699 pub quote_volume: [u8; 16],
701 pub num_trades: i64,
703 pub taker_buy_base_volume: [u8; 16],
705 pub taker_buy_quote_volume: [u8; 16],
707}
708
709#[cfg(test)]
710mod tests {
711 use rstest::rstest;
712
713 use super::*;
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 = r#"{"symbol": "BTCUSDT", "price": "50000.00"}"#;
725 let response: TickerPrice = serde_json::from_str(json).unwrap();
726 assert_eq!(response.symbol, "BTCUSDT");
727 assert_eq!(response.price, "50000.00");
728 }
729
730 #[rstest]
731 fn test_book_ticker_deserialize() {
732 let json = r#"{
733 "symbol": "BTCUSDT",
734 "bidPrice": "49999.00",
735 "bidQty": "1.5",
736 "askPrice": "50001.00",
737 "askQty": "2.0"
738 }"#;
739 let response: BookTicker = serde_json::from_str(json).unwrap();
740 assert_eq!(response.symbol, "BTCUSDT");
741 assert_eq!(response.bid_price, "49999.00");
742 assert_eq!(response.ask_price, "50001.00");
743 }
744
745 #[rstest]
746 fn test_avg_price_deserialize() {
747 let json = r#"{"mins": 5, "price": "50000.00", "closeTime": 1734300000000}"#;
748 let response: AvgPrice = serde_json::from_str(json).unwrap();
749 assert_eq!(response.mins, 5);
750 assert_eq!(response.price, "50000.00");
751 assert_eq!(response.close_time, 1734300000000);
752 }
753
754 #[rstest]
755 fn test_trade_fee_deserialize() {
756 let json = r#"{
757 "symbol": "BTCUSDT",
758 "makerCommission": "0.001",
759 "takerCommission": "0.001"
760 }"#;
761 let response: TradeFee = serde_json::from_str(json).unwrap();
762 assert_eq!(response.symbol, "BTCUSDT");
763 assert_eq!(response.maker_commission, "0.001");
764 assert_eq!(response.taker_commission, "0.001");
765 }
766
767 #[rstest]
768 fn test_batch_order_result_success() {
769 let json = r#"{
770 "symbol": "BTCUSDT",
771 "orderId": 12345,
772 "orderListId": -1,
773 "clientOrderId": "my-order-1",
774 "transactTime": 1734300000000,
775 "price": "50000.00",
776 "origQty": "0.1",
777 "executedQty": "0.0",
778 "cummulativeQuoteQty": "0.0",
779 "status": "NEW",
780 "timeInForce": "GTC",
781 "type": "LIMIT",
782 "side": "BUY"
783 }"#;
784 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
785 match result {
786 BatchOrderResult::Success(order) => {
787 assert_eq!(order.symbol, "BTCUSDT");
788 assert_eq!(order.order_id, 12345);
789 }
790 BatchOrderResult::Error(_) => panic!("Expected Success"),
791 }
792 }
793
794 #[rstest]
795 fn test_batch_order_result_error() {
796 let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
797 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
798 match result {
799 BatchOrderResult::Success(_) => panic!("Expected Error"),
800 BatchOrderResult::Error(error) => {
801 assert_eq!(error.code, -1013);
802 assert_eq!(error.msg, "Invalid quantity.");
803 }
804 }
805 }
806
807 #[rstest]
808 fn test_batch_cancel_result_success() {
809 let json = r#"{
810 "symbol": "BTCUSDT",
811 "orderId": 12345,
812 "orderListId": -1,
813 "origClientOrderId": "my-order-1",
814 "clientOrderId": "cancel-1",
815 "transactTime": 1734300000000,
816 "price": "50000.00",
817 "origQty": "0.1",
818 "executedQty": "0.0",
819 "cummulativeQuoteQty": "0.0",
820 "status": "CANCELED",
821 "timeInForce": "GTC",
822 "type": "LIMIT",
823 "side": "BUY"
824 }"#;
825 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
826 match result {
827 BatchCancelResult::Success(cancel) => {
828 assert_eq!(cancel.symbol, "BTCUSDT");
829 assert_eq!(cancel.order_id, 12345);
830 }
831 BatchCancelResult::Error(_) => panic!("Expected Success"),
832 }
833 }
834
835 #[rstest]
836 fn test_batch_cancel_result_error() {
837 let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
838 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
839 match result {
840 BatchCancelResult::Success(_) => panic!("Expected Error"),
841 BatchCancelResult::Error(error) => {
842 assert_eq!(error.code, -2011);
843 assert_eq!(error.msg, "Unknown order sent.");
844 }
845 }
846 }
847
848 #[rstest]
849 fn test_ticker_24hr_deserialize() {
850 let json = r#"{
851 "symbol": "BTCUSDT",
852 "priceChange": "100.00",
853 "priceChangePercent": "0.2",
854 "weightedAvgPrice": "50050.00",
855 "prevClosePrice": "49950.00",
856 "lastPrice": "50050.00",
857 "lastQty": "0.01",
858 "bidPrice": "50049.00",
859 "bidQty": "1.0",
860 "askPrice": "50051.00",
861 "askQty": "1.0",
862 "openPrice": "49950.00",
863 "highPrice": "50200.00",
864 "lowPrice": "49800.00",
865 "volume": "1000.0",
866 "quoteVolume": "50000000.0",
867 "openTime": 1734200000000,
868 "closeTime": 1734300000000,
869 "firstId": 1000,
870 "lastId": 2000,
871 "count": 1000
872 }"#;
873 let response: Ticker24hr = serde_json::from_str(json).unwrap();
874 assert_eq!(response.symbol, "BTCUSDT");
875 assert_eq!(response.last_price, "50050.00");
876 assert_eq!(response.count, 1000);
877 }
878}