From adeb203cb1e4829de8386d4d31a2212ccbc19171 Mon Sep 17 00:00:00 2001 From: "Mt. Shit" <284372560+mtshit@users.noreply.github.com> Date: Fri, 15 May 2026 14:36:50 +0000 Subject: [PATCH 1/2] binance: preserve exchangeInfo symbol precision fields --- .../binance/src/convert/market_data.rs | 61 +++++++++++++++++++ crates/mkt-types/src/market.rs | 6 ++ 2 files changed, 67 insertions(+) diff --git a/crates/exchanges/binance/src/convert/market_data.rs b/crates/exchanges/binance/src/convert/market_data.rs index 6bc99fb..764005b 100644 --- a/crates/exchanges/binance/src/convert/market_data.rs +++ b/crates/exchanges/binance/src/convert/market_data.rs @@ -243,6 +243,9 @@ pub(crate) fn market_info_from_exchange_symbol( .quote_asset .ok_or_else(|| crate::error::missing_field(operation, "quoteAsset"))?, ) + .base_asset_precision(symbol_definition.base_asset_precision) + .quote_precision(symbol_definition.quote_precision) + .quote_asset_precision(symbol_definition.quote_asset_precision) .trading_permissions(trading_permissions) .trading_constraints(trading_constraints) .build() @@ -373,6 +376,64 @@ pub(crate) fn trades_from_recent_response( .collect() } +#[cfg(test)] +mod tests { + use super::market_info_from_exchange_symbol; + use binance_sdk::spot::rest_api::{ + ExchangeInfoResponseSymbolsInner, ExchangeInfoSymbolStatusEnum, LotSizeFilter, + PriceFilter, SymbolFilters, + }; + use rust_decimal::Decimal; + use std::str::FromStr; + + #[test] + fn market_info_preserves_symbol_precision_fields() { + let symbol = ExchangeInfoResponseSymbolsInner { + symbol: Some(String::from("BTCUSDT")), + status: Some(ExchangeInfoSymbolStatusEnum::Trading.as_str().to_string()), + base_asset: Some(String::from("BTC")), + base_asset_precision: Some(8), + quote_asset: Some(String::from("USDT")), + quote_precision: Some(8), + quote_asset_precision: Some(8), + order_types: Some(vec![String::from("MARKET")]), + quote_order_qty_market_allowed: Some(true), + is_spot_trading_allowed: Some(true), + filters: Some(vec![ + SymbolFilters::PriceFilter(Box::new(PriceFilter { + filter_type: Some(String::from("PRICE_FILTER")), + price_exponent: None, + min_price: Some(String::from("0.01000000")), + max_price: Some(String::from("1000000.00000000")), + tick_size: Some(String::from("0.01000000")), + })), + SymbolFilters::LotSize(Box::new(LotSizeFilter { + filter_type: Some(String::from("LOT_SIZE")), + qty_exponent: None, + min_qty: Some(String::from("0.00001000")), + max_qty: Some(String::from("9000.00000000")), + step_size: Some(String::from("0.00001000")), + })), + ]), + ..ExchangeInfoResponseSymbolsInner::new() + }; + + let market = market_info_from_exchange_symbol(symbol, "spot.exchange_info") + .expect("market should convert"); + + assert_eq!(market.base_asset_precision, Some(8)); + assert_eq!(market.quote_precision, Some(8)); + assert_eq!(market.quote_asset_precision, Some(8)); + assert_eq!( + market + .trading_constraints + .price_filter + .and_then(|filter| filter.tick_size), + Some(Decimal::from_str("0.01").expect("decimal")) + ); + } +} + pub(crate) fn klines_from_rows( symbol: &Symbol, interval: KlineInterval, diff --git a/crates/mkt-types/src/market.rs b/crates/mkt-types/src/market.rs index 32c9ecc..f8c1a1d 100644 --- a/crates/mkt-types/src/market.rs +++ b/crates/mkt-types/src/market.rs @@ -394,6 +394,12 @@ pub struct MarketInfo { pub base_asset: String, pub quote_asset: String, #[builder(default)] + pub base_asset_precision: Option, + #[builder(default)] + pub quote_precision: Option, + #[builder(default)] + pub quote_asset_precision: Option, + #[builder(default)] pub trading_permissions: TradingPermissions, #[builder(default)] pub trading_constraints: TradingConstraints, From cb31a2ec07ccee22152fcd43dd9a90470055731c Mon Sep 17 00:00:00 2001 From: "Mt. Shit" <284372560+mtshit@users.noreply.github.com> Date: Fri, 15 May 2026 14:40:36 +0000 Subject: [PATCH 2/2] docs: clarify MarketInfo precision fields --- .../binance/src/convert/market_data.rs | 116 +++++++++--------- crates/mkt-types/src/market.rs | 21 ++++ 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/crates/exchanges/binance/src/convert/market_data.rs b/crates/exchanges/binance/src/convert/market_data.rs index 764005b..a0cf02b 100644 --- a/crates/exchanges/binance/src/convert/market_data.rs +++ b/crates/exchanges/binance/src/convert/market_data.rs @@ -376,64 +376,6 @@ pub(crate) fn trades_from_recent_response( .collect() } -#[cfg(test)] -mod tests { - use super::market_info_from_exchange_symbol; - use binance_sdk::spot::rest_api::{ - ExchangeInfoResponseSymbolsInner, ExchangeInfoSymbolStatusEnum, LotSizeFilter, - PriceFilter, SymbolFilters, - }; - use rust_decimal::Decimal; - use std::str::FromStr; - - #[test] - fn market_info_preserves_symbol_precision_fields() { - let symbol = ExchangeInfoResponseSymbolsInner { - symbol: Some(String::from("BTCUSDT")), - status: Some(ExchangeInfoSymbolStatusEnum::Trading.as_str().to_string()), - base_asset: Some(String::from("BTC")), - base_asset_precision: Some(8), - quote_asset: Some(String::from("USDT")), - quote_precision: Some(8), - quote_asset_precision: Some(8), - order_types: Some(vec![String::from("MARKET")]), - quote_order_qty_market_allowed: Some(true), - is_spot_trading_allowed: Some(true), - filters: Some(vec![ - SymbolFilters::PriceFilter(Box::new(PriceFilter { - filter_type: Some(String::from("PRICE_FILTER")), - price_exponent: None, - min_price: Some(String::from("0.01000000")), - max_price: Some(String::from("1000000.00000000")), - tick_size: Some(String::from("0.01000000")), - })), - SymbolFilters::LotSize(Box::new(LotSizeFilter { - filter_type: Some(String::from("LOT_SIZE")), - qty_exponent: None, - min_qty: Some(String::from("0.00001000")), - max_qty: Some(String::from("9000.00000000")), - step_size: Some(String::from("0.00001000")), - })), - ]), - ..ExchangeInfoResponseSymbolsInner::new() - }; - - let market = market_info_from_exchange_symbol(symbol, "spot.exchange_info") - .expect("market should convert"); - - assert_eq!(market.base_asset_precision, Some(8)); - assert_eq!(market.quote_precision, Some(8)); - assert_eq!(market.quote_asset_precision, Some(8)); - assert_eq!( - market - .trading_constraints - .price_filter - .and_then(|filter| filter.tick_size), - Some(Decimal::from_str("0.01").expect("decimal")) - ); - } -} - pub(crate) fn klines_from_rows( symbol: &Symbol, interval: KlineInterval, @@ -519,3 +461,61 @@ pub(crate) fn klines_from_rows( }) .collect() } + +#[cfg(test)] +mod tests { + use super::market_info_from_exchange_symbol; + use binance_sdk::spot::rest_api::{ + ExchangeInfoResponseSymbolsInner, ExchangeInfoSymbolStatusEnum, LotSizeFilter, PriceFilter, + SymbolFilters, + }; + use rust_decimal::Decimal; + use std::str::FromStr; + + #[test] + fn market_info_preserves_symbol_precision_fields() { + let symbol = ExchangeInfoResponseSymbolsInner { + symbol: Some(String::from("BTCUSDT")), + status: Some(ExchangeInfoSymbolStatusEnum::Trading.as_str().to_string()), + base_asset: Some(String::from("BTC")), + base_asset_precision: Some(8), + quote_asset: Some(String::from("USDT")), + quote_precision: Some(8), + quote_asset_precision: Some(8), + order_types: Some(vec![String::from("MARKET")]), + quote_order_qty_market_allowed: Some(true), + is_spot_trading_allowed: Some(true), + filters: Some(vec![ + SymbolFilters::PriceFilter(Box::new(PriceFilter { + filter_type: Some(String::from("PRICE_FILTER")), + price_exponent: None, + min_price: Some(String::from("0.01000000")), + max_price: Some(String::from("1000000.00000000")), + tick_size: Some(String::from("0.01000000")), + })), + SymbolFilters::LotSize(Box::new(LotSizeFilter { + filter_type: Some(String::from("LOT_SIZE")), + qty_exponent: None, + min_qty: Some(String::from("0.00001000")), + max_qty: Some(String::from("9000.00000000")), + step_size: Some(String::from("0.00001000")), + })), + ]), + ..ExchangeInfoResponseSymbolsInner::new() + }; + + let market = market_info_from_exchange_symbol(symbol, "spot.exchange_info") + .expect("market should convert"); + + assert_eq!(market.base_asset_precision, Some(8)); + assert_eq!(market.quote_precision, Some(8)); + assert_eq!(market.quote_asset_precision, Some(8)); + assert_eq!( + market + .trading_constraints + .price_filter + .and_then(|filter| filter.tick_size), + Some(Decimal::from_str("0.01").expect("decimal")) + ); + } +} diff --git a/crates/mkt-types/src/market.rs b/crates/mkt-types/src/market.rs index f8c1a1d..3614138 100644 --- a/crates/mkt-types/src/market.rs +++ b/crates/mkt-types/src/market.rs @@ -388,20 +388,41 @@ impl TradingConstraints { #[derive(Debug, Clone, PartialEq, Builder)] #[builder(pattern = "owned", setter(into))] pub struct MarketInfo { + /// Canonical exchange identifier for the venue that exposed this market. pub exchange_id: ExchangeId, + /// Unified symbol descriptor, including market kind and venue-specific symbol string. pub symbol: Symbol, + /// Current lifecycle status reported by the exchange. pub status: MarketStatus, + /// Venue-reported base asset code for this market. pub base_asset: String, + /// Venue-reported quote asset code for this market. pub quote_asset: String, #[builder(default)] + /// Exchange-reported precision for base-asset quantities at the symbol level, when available. + /// + /// This is raw venue metadata. It may differ from executable step-size filters and should not + /// be treated as a substitute for lot-size validation. pub base_asset_precision: Option, #[builder(default)] + /// Exchange-reported precision for quote-denominated values at the symbol level, when + /// available. + /// + /// On venues such as Binance spot, this is the most relevant symbol-level precision hint for + /// quote-sized order entry like `quoteOrderQty`. pub quote_precision: Option, #[builder(default)] + /// Exchange-reported precision for the quote asset at the symbol level, when available. + /// + /// Some venues expose both `quote_precision` and `quote_asset_precision`. When they differ, + /// adapter code should prefer the field that the venue documents for order-entry precision and + /// treat this field as auxiliary metadata or fallback. pub quote_asset_precision: Option, #[builder(default)] + /// Venue capabilities describing which order types, sides, and quantity modes are supported. pub trading_permissions: TradingPermissions, #[builder(default)] + /// Venue execution constraints such as price filters, lot sizes, and notional limits. pub trading_constraints: TradingConstraints, }