use nautilus_core::{UUID4, nanos::UnixNanos};
use nautilus_model::{
enums::AccountType,
events::AccountState,
identifiers::AccountId,
types::{AccountBalance, Currency, Money},
};
use rust_decimal::Decimal;
use crate::{
common::enums::{
BinanceOrderStatus, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
},
spot::sbe::spot::{
order_side::OrderSide, order_status::OrderStatus, order_type::OrderType,
self_trade_prevention_mode::SelfTradePreventionMode, time_in_force::TimeInForce,
},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BinancePriceLevel {
pub price_mantissa: i64,
pub qty_mantissa: i64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceDepth {
pub last_update_id: i64,
pub price_exponent: i8,
pub qty_exponent: i8,
pub bids: Vec<BinancePriceLevel>,
pub asks: Vec<BinancePriceLevel>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceTrade {
pub id: i64,
pub price_mantissa: i64,
pub qty_mantissa: i64,
pub quote_qty_mantissa: i64,
pub time: i64,
pub is_buyer_maker: bool,
pub is_best_match: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceTrades {
pub price_exponent: i8,
pub qty_exponent: i8,
pub trades: Vec<BinanceTrade>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceOrderFill {
pub price_mantissa: i64,
pub qty_mantissa: i64,
pub commission_mantissa: i64,
pub commission_exponent: i8,
pub commission_asset: String,
pub trade_id: Option<i64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceNewOrderResponse {
pub price_exponent: i8,
pub qty_exponent: i8,
pub order_id: i64,
pub order_list_id: Option<i64>,
pub transact_time: i64,
pub price_mantissa: i64,
pub orig_qty_mantissa: i64,
pub executed_qty_mantissa: i64,
pub cummulative_quote_qty_mantissa: i64,
pub status: OrderStatus,
pub time_in_force: TimeInForce,
pub order_type: OrderType,
pub side: OrderSide,
pub stop_price_mantissa: Option<i64>,
pub working_time: Option<i64>,
pub self_trade_prevention_mode: SelfTradePreventionMode,
pub client_order_id: String,
pub symbol: String,
pub fills: Vec<BinanceOrderFill>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceCancelOrderResponse {
pub price_exponent: i8,
pub qty_exponent: i8,
pub order_id: i64,
pub order_list_id: Option<i64>,
pub transact_time: i64,
pub price_mantissa: i64,
pub orig_qty_mantissa: i64,
pub executed_qty_mantissa: i64,
pub cummulative_quote_qty_mantissa: i64,
pub status: OrderStatus,
pub time_in_force: TimeInForce,
pub order_type: OrderType,
pub side: OrderSide,
pub self_trade_prevention_mode: SelfTradePreventionMode,
pub client_order_id: String,
pub orig_client_order_id: String,
pub symbol: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceOrderResponse {
pub price_exponent: i8,
pub qty_exponent: i8,
pub order_id: i64,
pub order_list_id: Option<i64>,
pub price_mantissa: i64,
pub orig_qty_mantissa: i64,
pub executed_qty_mantissa: i64,
pub cummulative_quote_qty_mantissa: i64,
pub status: OrderStatus,
pub time_in_force: TimeInForce,
pub order_type: OrderType,
pub side: OrderSide,
pub stop_price_mantissa: Option<i64>,
pub iceberg_qty_mantissa: Option<i64>,
pub time: i64,
pub update_time: i64,
pub is_working: bool,
pub working_time: Option<i64>,
pub orig_quote_order_qty_mantissa: i64,
pub self_trade_prevention_mode: SelfTradePreventionMode,
pub client_order_id: String,
pub symbol: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceBalance {
pub asset: String,
pub free_mantissa: i64,
pub locked_mantissa: i64,
pub exponent: i8,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceAccountInfo {
pub commission_exponent: i8,
pub maker_commission_mantissa: i64,
pub taker_commission_mantissa: i64,
pub buyer_commission_mantissa: i64,
pub seller_commission_mantissa: i64,
pub can_trade: bool,
pub can_withdraw: bool,
pub can_deposit: bool,
pub require_self_trade_prevention: bool,
pub prevent_sor: bool,
pub update_time: i64,
pub account_type: String,
pub balances: Vec<BinanceBalance>,
}
impl BinanceAccountInfo {
#[must_use]
pub fn to_account_state(&self, account_id: AccountId, ts_init: UnixNanos) -> AccountState {
let mut balances = Vec::with_capacity(self.balances.len());
for asset in &self.balances {
let currency =
Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
let exponent = asset.exponent as i32;
let multiplier = Decimal::new(1, (-exponent) as u32);
let free = Decimal::new(asset.free_mantissa, 0) * multiplier;
let locked = Decimal::new(asset.locked_mantissa, 0) * multiplier;
let total = free + locked;
let total_money = Money::from_decimal(total, currency)
.unwrap_or_else(|_| Money::new(total.to_string().parse().unwrap_or(0.0), currency));
let locked_money = Money::from_decimal(locked, currency).unwrap_or_else(|_| {
Money::new(locked.to_string().parse().unwrap_or(0.0), currency)
});
let free_money = Money::from_decimal(free, currency)
.unwrap_or_else(|_| Money::new(free.to_string().parse().unwrap_or(0.0), currency));
let balance = AccountBalance::new(total_money, locked_money, free_money);
balances.push(balance);
}
if balances.is_empty() {
let zero_currency = Currency::USDT();
let zero_money = Money::new(0.0, zero_currency);
let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
balances.push(zero_balance);
}
let ts_event = UnixNanos::from_micros(self.update_time as u64);
AccountState::new(
account_id,
AccountType::Cash,
balances,
vec![], true, UUID4::new(),
ts_event,
ts_init,
None, )
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinancePriceFilterSbe {
pub price_exponent: i8,
pub min_price: i64,
pub max_price: i64,
pub tick_size: i64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceLotSizeFilterSbe {
pub qty_exponent: i8,
pub min_qty: i64,
pub max_qty: i64,
pub step_size: i64,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BinanceSymbolFiltersSbe {
pub price_filter: Option<BinancePriceFilterSbe>,
pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceSymbolSbe {
pub symbol: String,
pub base_asset: String,
pub quote_asset: String,
pub base_asset_precision: u8,
pub quote_asset_precision: u8,
pub status: u8,
pub order_types: u16,
pub iceberg_allowed: bool,
pub oco_allowed: bool,
pub oto_allowed: bool,
pub quote_order_qty_market_allowed: bool,
pub allow_trailing_stop: bool,
pub cancel_replace_allowed: bool,
pub amend_allowed: bool,
pub is_spot_trading_allowed: bool,
pub is_margin_trading_allowed: bool,
pub filters: BinanceSymbolFiltersSbe,
pub permissions: Vec<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceExchangeInfoSbe {
pub symbols: Vec<BinanceSymbolSbe>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceAccountTrade {
pub price_exponent: i8,
pub qty_exponent: i8,
pub commission_exponent: i8,
pub id: i64,
pub order_id: i64,
pub order_list_id: Option<i64>,
pub price_mantissa: i64,
pub qty_mantissa: i64,
pub quote_qty_mantissa: i64,
pub commission_mantissa: i64,
pub time: i64,
pub is_buyer: bool,
pub is_maker: bool,
pub is_best_match: bool,
pub symbol: String,
pub commission_asset: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceKlines {
pub price_exponent: i8,
pub qty_exponent: i8,
pub klines: Vec<BinanceKline>,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListenKeyResponse {
pub listen_key: String,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ticker24hr {
pub symbol: String,
pub price_change: String,
pub price_change_percent: String,
pub weighted_avg_price: String,
pub prev_close_price: String,
pub last_price: String,
pub last_qty: String,
pub bid_price: String,
pub bid_qty: String,
pub ask_price: String,
pub ask_qty: String,
pub open_price: String,
pub high_price: String,
pub low_price: String,
pub volume: String,
pub quote_volume: String,
pub open_time: i64,
pub close_time: i64,
pub first_id: i64,
pub last_id: i64,
pub count: i64,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct TickerPrice {
pub symbol: String,
pub price: String,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BookTicker {
pub symbol: String,
pub bid_price: String,
pub bid_qty: String,
pub ask_price: String,
pub ask_qty: String,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct AvgPrice {
pub mins: i64,
pub price: String,
#[serde(rename = "closeTime")]
pub close_time: i64,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeFee {
pub symbol: String,
pub maker_commission: String,
pub taker_commission: String,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(untagged)]
pub enum BatchOrderResult {
Success(Box<BatchOrderSuccess>),
Error(BatchOrderError),
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchOrderSuccess {
pub symbol: String,
pub order_id: i64,
#[serde(default)]
pub order_list_id: Option<i64>,
pub client_order_id: String,
pub transact_time: i64,
pub price: String,
pub orig_qty: String,
pub executed_qty: String,
#[serde(rename = "cummulativeQuoteQty")]
pub cummulative_quote_qty: String,
pub status: BinanceOrderStatus,
pub time_in_force: BinanceTimeInForce,
#[serde(rename = "type")]
pub order_type: String,
pub side: BinanceSide,
#[serde(default)]
pub working_time: Option<i64>,
#[serde(default)]
pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct BatchOrderError {
pub code: i64,
pub msg: String,
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(untagged)]
pub enum BatchCancelResult {
Success(Box<BatchCancelSuccess>),
Error(BatchOrderError),
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchCancelSuccess {
pub symbol: String,
pub orig_client_order_id: String,
pub order_id: i64,
#[serde(default)]
pub order_list_id: Option<i64>,
pub client_order_id: String,
#[serde(default)]
pub transact_time: Option<i64>,
pub price: String,
pub orig_qty: String,
pub executed_qty: String,
#[serde(rename = "cummulativeQuoteQty")]
pub cummulative_quote_qty: String,
pub status: BinanceOrderStatus,
pub time_in_force: BinanceTimeInForce,
#[serde(rename = "type")]
pub order_type: String,
pub side: BinanceSide,
#[serde(default)]
pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinanceKline {
pub open_time: i64,
pub open_price: i64,
pub high_price: i64,
pub low_price: i64,
pub close_price: i64,
pub volume: [u8; 16],
pub close_time: i64,
pub quote_volume: [u8; 16],
pub num_trades: i64,
pub taker_buy_base_volume: [u8; 16],
pub taker_buy_quote_volume: [u8; 16],
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::common::testing::load_fixture_string;
#[rstest]
fn test_listen_key_response_deserialize() {
let json = r#"{"listenKey": "abc123xyz"}"#;
let response: ListenKeyResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.listen_key, "abc123xyz");
}
#[rstest]
fn test_ticker_price_deserialize() {
let json = load_fixture_string("spot/http_json/ticker_price_response.json");
let response: TickerPrice = serde_json::from_str(&json).unwrap();
assert_eq!(response.symbol, "LTCBTC");
assert_eq!(response.price, "4.00000200");
}
#[rstest]
fn test_book_ticker_deserialize() {
let json = load_fixture_string("spot/http_json/book_ticker_response.json");
let response: BookTicker = serde_json::from_str(&json).unwrap();
assert_eq!(response.symbol, "LTCBTC");
assert_eq!(response.bid_price, "4.00000000");
assert_eq!(response.ask_price, "4.00000200");
}
#[rstest]
fn test_avg_price_deserialize() {
let json = load_fixture_string("spot/http_json/avg_price_response.json");
let response: AvgPrice = serde_json::from_str(&json).unwrap();
assert_eq!(response.mins, 5);
assert_eq!(response.price, "9.35751834");
assert_eq!(response.close_time, 1694061154503);
}
#[rstest]
fn test_trade_fee_deserialize() {
let json = r#"{
"symbol": "BTCUSDT",
"makerCommission": "0.001",
"takerCommission": "0.001"
}"#;
let response: TradeFee = serde_json::from_str(json).unwrap();
assert_eq!(response.symbol, "BTCUSDT");
assert_eq!(response.maker_commission, "0.001");
assert_eq!(response.taker_commission, "0.001");
}
#[rstest]
fn test_batch_order_result_success() {
let json = load_fixture_string("spot/http_json/new_order_full_response.json");
let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
match result {
BatchOrderResult::Success(order) => {
assert_eq!(order.symbol, "BTCUSDT");
assert_eq!(order.order_id, 28);
assert_eq!(order.status, BinanceOrderStatus::Filled);
assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
assert_eq!(order.order_type, "MARKET");
assert_eq!(order.side, BinanceSide::Sell);
assert_eq!(
order.self_trade_prevention_mode,
Some(BinanceSelfTradePreventionMode::None)
);
}
BatchOrderResult::Error(_) => panic!("Expected Success"),
}
}
#[rstest]
fn test_batch_order_result_error() {
let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
let result: BatchOrderResult = serde_json::from_str(json).unwrap();
match result {
BatchOrderResult::Success(_) => panic!("Expected Error"),
BatchOrderResult::Error(error) => {
assert_eq!(error.code, -1013);
assert_eq!(error.msg, "Invalid quantity.");
}
}
}
#[rstest]
fn test_batch_cancel_result_success() {
let json = load_fixture_string("spot/http_json/cancel_order_response.json");
let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
match result {
BatchCancelResult::Success(cancel) => {
assert_eq!(cancel.symbol, "LTCBTC");
assert_eq!(cancel.order_id, 4);
assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
assert_eq!(cancel.order_type, "LIMIT");
assert_eq!(cancel.side, BinanceSide::Buy);
assert_eq!(
cancel.self_trade_prevention_mode,
Some(BinanceSelfTradePreventionMode::None)
);
}
BatchCancelResult::Error(_) => panic!("Expected Success"),
}
}
#[rstest]
fn test_batch_cancel_result_error() {
let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
let result: BatchCancelResult = serde_json::from_str(json).unwrap();
match result {
BatchCancelResult::Success(_) => panic!("Expected Error"),
BatchCancelResult::Error(error) => {
assert_eq!(error.code, -2011);
assert_eq!(error.msg, "Unknown order sent.");
}
}
}
#[rstest]
fn test_ticker_24hr_deserialize() {
let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
let response: Ticker24hr = serde_json::from_str(&json).unwrap();
assert_eq!(response.symbol, "BNBBTC");
assert_eq!(response.last_price, "4.00000200");
assert_eq!(response.count, 76);
}
}