use serde::{Deserialize, Serialize};
use crate::types::{OrderType, RateLimitInterval, RateLimitType, SymbolPermission, SymbolStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerTime {
pub server_time: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExchangeInfo {
pub timezone: String,
pub server_time: u64,
pub rate_limits: Vec<RateLimit>,
pub symbols: Vec<Symbol>,
#[serde(default)]
pub exchange_filters: Vec<SymbolFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RateLimit {
pub rate_limit_type: RateLimitType,
pub interval: RateLimitInterval,
pub interval_num: i32,
pub limit: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Symbol {
pub symbol: String,
pub status: SymbolStatus,
pub base_asset: String,
pub base_asset_precision: u8,
pub quote_asset: String,
pub quote_precision: u8,
pub quote_asset_precision: u8,
#[serde(default)]
pub base_commission_precision: u8,
#[serde(default)]
pub quote_commission_precision: u8,
pub order_types: Vec<OrderType>,
pub iceberg_allowed: bool,
pub oco_allowed: bool,
#[serde(default)]
pub quote_order_qty_market_allowed: bool,
#[serde(default = "default_true")]
pub is_spot_trading_allowed: bool,
#[serde(default)]
pub is_margin_trading_allowed: bool,
pub filters: Vec<SymbolFilter>,
#[serde(default)]
pub permissions: Vec<SymbolPermission>,
}
fn default_true() -> bool {
true
}
impl Symbol {
pub fn lot_size(&self) -> Option<&SymbolFilter> {
self.filters
.iter()
.find(|f| matches!(f, SymbolFilter::LotSize { .. }))
}
pub fn price_filter(&self) -> Option<&SymbolFilter> {
self.filters
.iter()
.find(|f| matches!(f, SymbolFilter::PriceFilter { .. }))
}
pub fn min_notional(&self) -> Option<&SymbolFilter> {
self.filters
.iter()
.find(|f| matches!(f, SymbolFilter::MinNotional { .. }))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "filterType")]
pub enum SymbolFilter {
#[serde(rename = "PRICE_FILTER")]
#[serde(rename_all = "camelCase")]
PriceFilter {
#[serde(with = "string_or_float")]
min_price: f64,
#[serde(with = "string_or_float")]
max_price: f64,
#[serde(with = "string_or_float")]
tick_size: f64,
},
#[serde(rename = "PERCENT_PRICE")]
#[serde(rename_all = "camelCase")]
PercentPrice {
#[serde(with = "string_or_float")]
multiplier_up: f64,
#[serde(with = "string_or_float")]
multiplier_down: f64,
avg_price_mins: u64,
},
#[serde(rename = "LOT_SIZE")]
#[serde(rename_all = "camelCase")]
LotSize {
#[serde(with = "string_or_float")]
min_qty: f64,
#[serde(with = "string_or_float")]
max_qty: f64,
#[serde(with = "string_or_float")]
step_size: f64,
},
#[serde(rename = "MARKET_LOT_SIZE")]
#[serde(rename_all = "camelCase")]
MarketLotSize {
#[serde(with = "string_or_float")]
min_qty: f64,
#[serde(with = "string_or_float")]
max_qty: f64,
#[serde(with = "string_or_float")]
step_size: f64,
},
#[serde(rename = "MIN_NOTIONAL")]
#[serde(rename_all = "camelCase")]
MinNotional {
#[serde(with = "string_or_float")]
min_notional: f64,
apply_to_market: bool,
avg_price_mins: u64,
},
#[serde(rename = "NOTIONAL")]
#[serde(rename_all = "camelCase")]
Notional {
#[serde(with = "string_or_float")]
min_notional: f64,
apply_min_to_market: bool,
#[serde(with = "string_or_float")]
max_notional: f64,
apply_max_to_market: bool,
avg_price_mins: u64,
},
#[serde(rename = "ICEBERG_PARTS")]
#[serde(rename_all = "camelCase")]
IcebergParts {
limit: u16,
},
#[serde(rename = "MAX_NUM_ORDERS")]
#[serde(rename_all = "camelCase")]
MaxNumOrders {
max_num_orders: u16,
},
#[serde(rename = "MAX_NUM_ALGO_ORDERS")]
#[serde(rename_all = "camelCase")]
MaxNumAlgoOrders {
max_num_algo_orders: u16,
},
#[serde(rename = "MAX_NUM_ICEBERG_ORDERS")]
#[serde(rename_all = "camelCase")]
MaxNumIcebergOrders {
max_num_iceberg_orders: u16,
},
#[serde(rename = "MAX_POSITION")]
#[serde(rename_all = "camelCase")]
MaxPosition {
#[serde(with = "string_or_float")]
max_position: f64,
},
#[serde(rename = "EXCHANGE_MAX_NUM_ORDERS")]
#[serde(rename_all = "camelCase")]
ExchangeMaxNumOrders {
max_num_orders: u16,
},
#[serde(rename = "EXCHANGE_MAX_NUM_ALGO_ORDERS")]
#[serde(rename_all = "camelCase")]
ExchangeMaxNumAlgoOrders {
max_num_algo_orders: u16,
},
#[serde(rename = "TRAILING_DELTA")]
#[serde(rename_all = "camelCase")]
TrailingDelta {
min_trailing_above_delta: u32,
max_trailing_above_delta: u32,
min_trailing_below_delta: u32,
max_trailing_below_delta: u32,
},
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderBook {
pub last_update_id: u64,
pub bids: Vec<OrderBookEntry>,
pub asks: Vec<OrderBookEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookEntry {
#[serde(with = "string_or_float")]
pub price: f64,
#[serde(with = "string_or_float")]
pub quantity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub id: u64,
#[serde(with = "string_or_float")]
pub price: f64,
#[serde(rename = "qty", with = "string_or_float")]
pub quantity: f64,
#[serde(rename = "quoteQty", with = "string_or_float")]
pub quote_quantity: f64,
pub time: u64,
pub is_buyer_maker: bool,
pub is_best_match: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggTrade {
#[serde(rename = "a")]
pub agg_trade_id: u64,
#[serde(rename = "p", with = "string_or_float")]
pub price: f64,
#[serde(rename = "q", with = "string_or_float")]
pub quantity: f64,
#[serde(rename = "f")]
pub first_trade_id: u64,
#[serde(rename = "l")]
pub last_trade_id: u64,
#[serde(rename = "T")]
pub timestamp: u64,
#[serde(rename = "m")]
pub is_buyer_maker: bool,
#[serde(rename = "M")]
pub is_best_match: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Kline {
pub open_time: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub close_time: i64,
pub quote_asset_volume: f64,
pub number_of_trades: i64,
pub taker_buy_base_asset_volume: f64,
pub taker_buy_quote_asset_volume: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ticker24h {
pub symbol: String,
#[serde(with = "string_or_float")]
pub price_change: f64,
#[serde(with = "string_or_float")]
pub price_change_percent: f64,
#[serde(with = "string_or_float")]
pub weighted_avg_price: f64,
#[serde(with = "string_or_float")]
pub prev_close_price: f64,
#[serde(with = "string_or_float")]
pub last_price: f64,
#[serde(with = "string_or_float")]
pub last_qty: f64,
#[serde(with = "string_or_float")]
pub bid_price: f64,
#[serde(with = "string_or_float")]
pub bid_qty: f64,
#[serde(with = "string_or_float")]
pub ask_price: f64,
#[serde(with = "string_or_float")]
pub ask_qty: f64,
#[serde(with = "string_or_float")]
pub open_price: f64,
#[serde(with = "string_or_float")]
pub high_price: f64,
#[serde(with = "string_or_float")]
pub low_price: f64,
#[serde(with = "string_or_float")]
pub volume: f64,
#[serde(with = "string_or_float")]
pub quote_volume: f64,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradingDayTicker {
pub symbol: String,
#[serde(with = "string_or_float")]
pub price_change: f64,
#[serde(with = "string_or_float")]
pub price_change_percent: f64,
#[serde(with = "string_or_float")]
pub weighted_avg_price: f64,
#[serde(with = "string_or_float")]
pub open_price: f64,
#[serde(with = "string_or_float")]
pub high_price: f64,
#[serde(with = "string_or_float")]
pub low_price: f64,
#[serde(with = "string_or_float")]
pub last_price: f64,
#[serde(with = "string_or_float")]
pub volume: f64,
#[serde(with = "string_or_float")]
pub quote_volume: f64,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradingDayTickerMini {
pub symbol: String,
#[serde(with = "string_or_float")]
pub open_price: f64,
#[serde(with = "string_or_float")]
pub high_price: f64,
#[serde(with = "string_or_float")]
pub low_price: f64,
#[serde(with = "string_or_float")]
pub last_price: f64,
#[serde(with = "string_or_float")]
pub volume: f64,
#[serde(with = "string_or_float")]
pub quote_volume: f64,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RollingWindowTicker {
pub symbol: String,
#[serde(with = "string_or_float")]
pub price_change: f64,
#[serde(with = "string_or_float")]
pub price_change_percent: f64,
#[serde(with = "string_or_float")]
pub weighted_avg_price: f64,
#[serde(with = "string_or_float")]
pub open_price: f64,
#[serde(with = "string_or_float")]
pub high_price: f64,
#[serde(with = "string_or_float")]
pub low_price: f64,
#[serde(with = "string_or_float")]
pub last_price: f64,
#[serde(with = "string_or_float")]
pub volume: f64,
#[serde(with = "string_or_float")]
pub quote_volume: f64,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RollingWindowTickerMini {
pub symbol: String,
#[serde(with = "string_or_float")]
pub open_price: f64,
#[serde(with = "string_or_float")]
pub high_price: f64,
#[serde(with = "string_or_float")]
pub low_price: f64,
#[serde(with = "string_or_float")]
pub last_price: f64,
#[serde(with = "string_or_float")]
pub volume: f64,
#[serde(with = "string_or_float")]
pub quote_volume: f64,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TickerPrice {
pub symbol: String,
#[serde(with = "string_or_float")]
pub price: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BookTicker {
pub symbol: String,
#[serde(with = "string_or_float")]
pub bid_price: f64,
#[serde(with = "string_or_float")]
pub bid_qty: f64,
#[serde(with = "string_or_float")]
pub ask_price: f64,
#[serde(with = "string_or_float")]
pub ask_qty: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AveragePrice {
pub mins: u64,
#[serde(with = "string_or_float")]
pub price: f64,
}
pub mod string_or_float {
use serde::{Deserialize, Deserializer, Serializer, de};
use std::fmt;
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
serializer.collect_str(value)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrFloat {
String(String),
Float(f64),
}
match StringOrFloat::deserialize(deserializer)? {
StringOrFloat::String(s) => s.parse().map_err(de::Error::custom),
StringOrFloat::Float(f) => Ok(f),
}
}
}
pub mod string_or_float_opt {
use serde::{Deserialize, Deserializer, Serializer};
use std::fmt;
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
match value {
Some(v) => super::string_or_float::serialize(v, serializer),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrFloat {
String(String),
Float(f64),
Null,
}
match StringOrFloat::deserialize(deserializer)? {
StringOrFloat::String(s) if s.is_empty() => Ok(None),
StringOrFloat::String(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
StringOrFloat::Float(f) => Ok(Some(f)),
StringOrFloat::Null => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_time_deserialize() {
let json = r#"{"serverTime": 1234567890123}"#;
let time: ServerTime = serde_json::from_str(json).unwrap();
assert_eq!(time.server_time, 1234567890123);
}
#[test]
fn test_ticker_price_deserialize() {
let json = r#"{"symbol": "BTCUSDT", "price": "50000.00"}"#;
let ticker: TickerPrice = serde_json::from_str(json).unwrap();
assert_eq!(ticker.symbol, "BTCUSDT");
assert_eq!(ticker.price, 50000.0);
}
#[test]
fn test_book_ticker_deserialize() {
let json = r#"{
"symbol": "BTCUSDT",
"bidPrice": "50000.00",
"bidQty": "1.5",
"askPrice": "50001.00",
"askQty": "2.0"
}"#;
let ticker: BookTicker = serde_json::from_str(json).unwrap();
assert_eq!(ticker.symbol, "BTCUSDT");
assert_eq!(ticker.bid_price, 50000.0);
assert_eq!(ticker.bid_qty, 1.5);
assert_eq!(ticker.ask_price, 50001.0);
assert_eq!(ticker.ask_qty, 2.0);
}
#[test]
fn test_order_book_entry_deserialize() {
let json = r#"["50000.00", "1.5"]"#;
let entry: OrderBookEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.price, 50000.0);
assert_eq!(entry.quantity, 1.5);
}
#[test]
fn test_agg_trade_deserialize() {
let json = r#"{
"a": 12345,
"p": "50000.00",
"q": "1.5",
"f": 100,
"l": 105,
"T": 1234567890123,
"m": true,
"M": true
}"#;
let trade: AggTrade = serde_json::from_str(json).unwrap();
assert_eq!(trade.agg_trade_id, 12345);
assert_eq!(trade.price, 50000.0);
assert_eq!(trade.quantity, 1.5);
assert_eq!(trade.first_trade_id, 100);
assert_eq!(trade.last_trade_id, 105);
assert_eq!(trade.timestamp, 1234567890123);
assert!(trade.is_buyer_maker);
assert!(trade.is_best_match);
}
#[test]
fn test_average_price_deserialize() {
let json = r#"{"mins": 5, "price": "50000.00"}"#;
let avg: AveragePrice = serde_json::from_str(json).unwrap();
assert_eq!(avg.mins, 5);
assert_eq!(avg.price, 50000.0);
}
#[test]
fn test_symbol_filter_price_filter() {
let json = r#"{
"filterType": "PRICE_FILTER",
"minPrice": "0.00001000",
"maxPrice": "1000000.00000000",
"tickSize": "0.00001000"
}"#;
let filter: SymbolFilter = serde_json::from_str(json).unwrap();
match filter {
SymbolFilter::PriceFilter {
min_price,
max_price,
tick_size,
} => {
assert_eq!(min_price, 0.00001);
assert_eq!(max_price, 1000000.0);
assert_eq!(tick_size, 0.00001);
}
_ => panic!("Expected PriceFilter"),
}
}
#[test]
fn test_symbol_filter_lot_size() {
let json = r#"{
"filterType": "LOT_SIZE",
"minQty": "0.00100000",
"maxQty": "100000.00000000",
"stepSize": "0.00100000"
}"#;
let filter: SymbolFilter = serde_json::from_str(json).unwrap();
match filter {
SymbolFilter::LotSize {
min_qty,
max_qty,
step_size,
} => {
assert_eq!(min_qty, 0.001);
assert_eq!(max_qty, 100000.0);
assert_eq!(step_size, 0.001);
}
_ => panic!("Expected LotSize"),
}
}
#[test]
fn test_unknown_filter_type() {
let json = r#"{"filterType": "UNKNOWN_FILTER_TYPE"}"#;
let filter: SymbolFilter = serde_json::from_str(json).unwrap();
assert_eq!(filter, SymbolFilter::Other);
}
}