use std::{fmt, str::FromStr};
use rust_decimal::Decimal;
use serde::{
de::{self, DeserializeOwned, IgnoredAny, SeqAccess, Visitor},
ser::SerializeSeq,
Deserialize, Deserializer, Serialize, Serializer,
};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Symbol(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{0:?} is not a Binance symbol: 1 to 32 letters, digits or underscores")]
pub struct InvalidSymbol(pub String);
impl Symbol {
pub const MAX_LEN: usize = 32;
pub fn new(symbol: impl Into<String>) -> Result<Self, InvalidSymbol> {
let symbol = symbol.into();
let chars = symbol.chars().count();
let valid = (1..=Self::MAX_LEN).contains(&chars)
&& symbol.chars().all(|c| c.is_alphanumeric() || c == '_');
if valid {
Ok(Self(symbol.to_ascii_uppercase()))
} else {
Err(InvalidSymbol(symbol))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Symbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for Symbol {
type Err = InvalidSymbol;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl AsRef<str> for Symbol {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{value:?} is not a valid {type_name}")]
pub struct UnknownVariant {
pub type_name: &'static str,
pub value: String,
}
macro_rules! wire_enum {
($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $wire:literal),+ $(,)? }) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum $name {
$( $(#[$vmeta])* #[serde(rename = $wire)] $variant, )+
}
impl $name {
pub const ALL: &'static [$name] = &[$( $name::$variant, )+];
pub fn as_str(self) -> &'static str {
match self { $( $name::$variant => $wire, )+ }
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for $name {
type Err = UnknownVariant;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$( $wire => Ok($name::$variant), )+
_ => Err(UnknownVariant { type_name: stringify!($name), value: s.to_owned() }),
}
}
}
};
}
macro_rules! open_enum {
($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $wire:literal),+ $(,)? }) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum $name {
$( $(#[$vmeta])* $variant, )+
Other(String),
}
impl $name {
pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
pub fn as_str(&self) -> &str {
match self {
$( Self::$variant => $wire, )+
Self::Other(raw) => raw,
}
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for $name {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
$( $wire => Self::$variant, )+
other => Self::Other(other.to_owned()),
})
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
let Ok(value) = raw.parse();
Ok(value)
}
}
};
}
wire_enum! {
Interval {
M1 => "1m", M3 => "3m", M5 => "5m", M15 => "15m", M30 => "30m",
H1 => "1h", H2 => "2h", H4 => "4h", H6 => "6h", H8 => "8h", H12 => "12h",
D1 => "1d", D3 => "3d", W1 => "1w",
Mo1 => "1M",
}
}
wire_enum! {
DepthLimit {
Five => "5", Ten => "10", Twenty => "20", Fifty => "50",
Hundred => "100", FiveHundred => "500", Thousand => "1000",
}
}
open_enum! {
ContractType {
Perpetual => "PERPETUAL",
TradifiPerpetual => "TRADIFI_PERPETUAL",
CurrentMonth => "CURRENT_MONTH",
NextMonth => "NEXT_MONTH",
CurrentQuarter => "CURRENT_QUARTER",
NextQuarter => "NEXT_QUARTER",
PerpetualDelivering => "PERPETUAL_DELIVERING",
}
}
open_enum! {
SymbolStatus {
PendingTrading => "PENDING_TRADING",
Trading => "TRADING",
PreDelivering => "PRE_DELIVERING",
Delivering => "DELIVERING",
Delivered => "DELIVERED",
PreSettle => "PRE_SETTLE",
Settling => "SETTLING",
Close => "CLOSE",
TradingHalt => "TRADING_HALT",
TradingCancelOnly => "TRADING_CANCEL_ONLY",
}
}
open_enum! {
UnderlyingType {
Coin => "COIN",
Index => "INDEX",
Premarket => "PREMARKET",
Commodity => "COMMODITY",
Equity => "EQUITY",
CnEquity => "CN_EQUITY",
HkEquity => "HK_EQUITY",
KrEquity => "KR_EQUITY",
Fx => "FX",
}
}
mod decimal_or_empty {
use super::*;
pub fn serialize<S: Serializer>(
value: &Option<Decimal>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => serializer.collect_str(value),
None => serializer.serialize_str(""),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Decimal>, D::Error> {
let raw = String::deserialize(deserializer)?;
if raw.is_empty() {
return Ok(None);
}
raw.parse().map(Some).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ServerTime {
pub server_time: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ExchangeInfo {
pub timezone: String,
pub server_time: u64,
pub futures_type: String,
pub rate_limits: Vec<RateLimit>,
pub exchange_filters: Vec<Filter>,
pub assets: Vec<AssetInfo>,
pub symbols: Vec<SymbolInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RateLimit {
pub rate_limit_type: String,
pub interval: String,
pub interval_num: u32,
pub limit: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AssetInfo {
pub asset: String,
pub margin_available: bool,
#[serde(with = "rust_decimal::serde::str")]
pub auto_asset_exchange: Decimal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SymbolInfo {
pub symbol: String,
pub pair: String,
pub contract_type: ContractType,
pub delivery_date: u64,
pub onboard_date: u64,
pub status: SymbolStatus,
#[serde(with = "rust_decimal::serde::str")]
pub maint_margin_percent: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub required_margin_percent: Decimal,
pub base_asset: String,
pub quote_asset: String,
pub margin_asset: String,
pub price_precision: u32,
pub quantity_precision: u32,
pub base_asset_precision: u32,
pub quote_precision: u32,
pub underlying_type: UnderlyingType,
pub underlying_sub_type: Vec<String>,
#[serde(with = "rust_decimal::serde::str")]
pub trigger_protect: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub liquidation_fee: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub market_take_bound: Decimal,
pub max_move_order_limit: u32,
pub filters: Vec<Filter>,
pub order_types: Vec<String>,
pub time_in_force: Vec<String>,
pub permission_sets: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Filter {
#[non_exhaustive]
PriceFilter {
min_price: Decimal,
max_price: Decimal,
tick_size: Decimal,
},
#[non_exhaustive]
LotSize {
min_qty: Decimal,
max_qty: Decimal,
step_size: Decimal,
},
#[non_exhaustive]
MarketLotSize {
min_qty: Decimal,
max_qty: Decimal,
step_size: Decimal,
},
#[non_exhaustive]
MaxNumOrders {
limit: u32,
},
#[non_exhaustive]
MinNotional {
notional: Decimal,
},
#[non_exhaustive]
PercentPrice {
multiplier_up: Decimal,
multiplier_down: Decimal,
multiplier_decimal: Decimal,
},
#[non_exhaustive]
PositionRiskControl {
position_control_side: String,
},
Other {
filter_type: String,
raw: Value,
},
}
mod filter_wire {
use super::*;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Price {
#[serde(with = "rust_decimal::serde::str")]
pub min_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub max_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub tick_size: Decimal,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Lot {
#[serde(with = "rust_decimal::serde::str")]
pub min_qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub max_qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub step_size: Decimal,
}
#[derive(Serialize, Deserialize)]
pub struct MaxNumOrders {
pub limit: u32,
}
#[derive(Serialize, Deserialize)]
pub struct MinNotional {
#[serde(with = "rust_decimal::serde::str")]
pub notional: Decimal,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PercentPrice {
#[serde(with = "rust_decimal::serde::str")]
pub multiplier_up: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub multiplier_down: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub multiplier_decimal: Decimal,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionRiskControl {
pub position_control_side: String,
}
}
impl<'de> Deserialize<'de> for Filter {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use filter_wire as wire;
fn parse<T: DeserializeOwned, E: de::Error>(raw: Value) -> Result<T, E> {
serde_json::from_value(raw).map_err(E::custom)
}
let raw = Value::deserialize(deserializer)?;
let filter_type = raw
.get("filterType")
.and_then(Value::as_str)
.ok_or_else(|| de::Error::custom("a filter without a string filterType"))?
.to_owned();
Ok(match filter_type.as_str() {
"PRICE_FILTER" => {
let f: wire::Price = parse(raw)?;
Self::PriceFilter {
min_price: f.min_price,
max_price: f.max_price,
tick_size: f.tick_size,
}
}
"LOT_SIZE" => {
let f: wire::Lot = parse(raw)?;
Self::LotSize {
min_qty: f.min_qty,
max_qty: f.max_qty,
step_size: f.step_size,
}
}
"MARKET_LOT_SIZE" => {
let f: wire::Lot = parse(raw)?;
Self::MarketLotSize {
min_qty: f.min_qty,
max_qty: f.max_qty,
step_size: f.step_size,
}
}
"MAX_NUM_ORDERS" => {
let f: wire::MaxNumOrders = parse(raw)?;
Self::MaxNumOrders { limit: f.limit }
}
"MIN_NOTIONAL" => {
let f: wire::MinNotional = parse(raw)?;
Self::MinNotional {
notional: f.notional,
}
}
"PERCENT_PRICE" => {
let f: wire::PercentPrice = parse(raw)?;
Self::PercentPrice {
multiplier_up: f.multiplier_up,
multiplier_down: f.multiplier_down,
multiplier_decimal: f.multiplier_decimal,
}
}
"POSITION_RISK_CONTROL" => {
let f: wire::PositionRiskControl = parse(raw)?;
Self::PositionRiskControl {
position_control_side: f.position_control_side,
}
}
_ => Self::Other { filter_type, raw },
})
}
}
impl Serialize for Filter {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use filter_wire as wire;
fn tagged<T: Serialize, E: serde::ser::Error>(
filter_type: &str,
body: &T,
) -> Result<Value, E> {
let mut value = serde_json::to_value(body).map_err(E::custom)?;
if let Value::Object(map) = &mut value {
map.insert(
"filterType".to_owned(),
Value::String(filter_type.to_owned()),
);
}
Ok(value)
}
let value = match self {
Self::PriceFilter {
min_price,
max_price,
tick_size,
} => tagged(
"PRICE_FILTER",
&wire::Price {
min_price: *min_price,
max_price: *max_price,
tick_size: *tick_size,
},
)?,
Self::LotSize {
min_qty,
max_qty,
step_size,
} => tagged(
"LOT_SIZE",
&wire::Lot {
min_qty: *min_qty,
max_qty: *max_qty,
step_size: *step_size,
},
)?,
Self::MarketLotSize {
min_qty,
max_qty,
step_size,
} => tagged(
"MARKET_LOT_SIZE",
&wire::Lot {
min_qty: *min_qty,
max_qty: *max_qty,
step_size: *step_size,
},
)?,
Self::MaxNumOrders { limit } => {
tagged("MAX_NUM_ORDERS", &wire::MaxNumOrders { limit: *limit })?
}
Self::MinNotional { notional } => tagged(
"MIN_NOTIONAL",
&wire::MinNotional {
notional: *notional,
},
)?,
Self::PercentPrice {
multiplier_up,
multiplier_down,
multiplier_decimal,
} => tagged(
"PERCENT_PRICE",
&wire::PercentPrice {
multiplier_up: *multiplier_up,
multiplier_down: *multiplier_down,
multiplier_decimal: *multiplier_decimal,
},
)?,
Self::PositionRiskControl {
position_control_side,
} => tagged(
"POSITION_RISK_CONTROL",
&wire::PositionRiskControl {
position_control_side: position_control_side.clone(),
},
)?,
Self::Other { raw, .. } => raw.clone(),
};
value.serialize(serializer)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FundingInfo {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub adjusted_funding_rate_cap: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub adjusted_funding_rate_floor: Decimal,
pub funding_interval_hours: u32,
pub disclaimer: bool,
pub update_time: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Ticker24h {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub price_change: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub price_change_percent: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub weighted_avg_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub last_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub last_qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub open_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub high_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub low_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub volume: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub quote_volume: Decimal,
pub open_time: u64,
pub close_time: u64,
pub first_id: i64,
pub last_id: i64,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PremiumIndex {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub mark_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub index_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub estimated_settle_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub last_funding_rate: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub interest_rate: Decimal,
pub next_funding_time: u64,
pub time: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Kline {
pub open_time: u64,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: Decimal,
pub close_time: u64,
pub quote_volume: Decimal,
pub trade_count: u64,
pub taker_buy_base_volume: Decimal,
pub taker_buy_quote_volume: Decimal,
}
struct DecimalStr(Decimal);
impl<'de> Deserialize<'de> for DecimalStr {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
rust_decimal::serde::str::deserialize(deserializer).map(Self)
}
}
fn next<'de, T: Deserialize<'de>, A: SeqAccess<'de>>(
seq: &mut A,
index: usize,
what: &str,
) -> Result<T, A::Error> {
seq.next_element()?
.ok_or_else(|| de::Error::invalid_length(index, &what))
}
fn drain<'de, A: SeqAccess<'de>>(seq: &mut A) -> Result<(), A::Error> {
while seq.next_element::<IgnoredAny>()?.is_some() {}
Ok(())
}
impl<'de> Deserialize<'de> for Kline {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct KlineVisitor;
impl<'de> Visitor<'de> for KlineVisitor {
type Value = Kline;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a kline array of at least 11 elements")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
const WHAT: &str = "a kline array of at least 11 elements";
let kline = Kline {
open_time: next(&mut seq, 0, WHAT)?,
open: next::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
high: next::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
low: next::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
close: next::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
volume: next::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
close_time: next(&mut seq, 6, WHAT)?,
quote_volume: next::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
trade_count: next(&mut seq, 8, WHAT)?,
taker_buy_base_volume: next::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
taker_buy_quote_volume: next::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
};
drain(&mut seq)?;
Ok(kline)
}
}
deserializer.deserialize_seq(KlineVisitor)
}
}
impl Serialize for Kline {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(11))?;
seq.serialize_element(&self.open_time)?;
for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
seq.serialize_element(&value.to_string())?;
}
seq.serialize_element(&self.close_time)?;
seq.serialize_element(&self.quote_volume.to_string())?;
seq.serialize_element(&self.trade_count)?;
seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
seq.end()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FundingRate {
pub symbol: String,
pub funding_time: u64,
#[serde(with = "rust_decimal::serde::str")]
pub funding_rate: Decimal,
#[serde(with = "decimal_or_empty")]
pub mark_price: Option<Decimal>,
pub rate_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OpenInterest {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub open_interest: Decimal,
pub time: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AggTrade {
#[serde(rename = "a")]
pub id: u64,
#[serde(rename = "p", with = "rust_decimal::serde::str")]
pub price: Decimal,
#[serde(rename = "q", with = "rust_decimal::serde::str")]
pub quantity: Decimal,
#[serde(rename = "nq", with = "rust_decimal::serde::str")]
pub normal_quantity: Decimal,
#[serde(rename = "f")]
pub first_trade_id: u64,
#[serde(rename = "l")]
pub last_trade_id: u64,
#[serde(rename = "T")]
pub time: u64,
#[serde(rename = "m")]
pub is_buyer_maker: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Depth {
#[serde(rename = "lastUpdateId")]
pub last_update_id: u64,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "T")]
pub transaction_time: u64,
pub bids: Vec<Level>,
pub asks: Vec<Level>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Level {
pub price: Decimal,
pub quantity: Decimal,
}
impl Level {
pub fn new(price: Decimal, quantity: Decimal) -> Self {
Self { price, quantity }
}
}
impl<'de> Deserialize<'de> for Level {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct LevelVisitor;
impl<'de> Visitor<'de> for LevelVisitor {
type Value = Level;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a [price, quantity] array")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
const WHAT: &str = "a [price, quantity] array";
let level = Level {
price: next::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
quantity: next::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
};
drain(&mut seq)?;
Ok(level)
}
}
deserializer.deserialize_seq(LevelVisitor)
}
}
impl Serialize for Level {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&self.price.to_string())?;
seq.serialize_element(&self.quantity.to_string())?;
seq.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbols_take_letters_digits_and_underscores_in_any_script() {
for good in ["BTCUSDT", "BTCUSDT_261225", "币安人生USDT", "1000PEPEUSDT"] {
assert_eq!(Symbol::new(good).unwrap().as_str(), good);
}
for bad in [
"",
"BTC USDT",
"BTC@USDT",
"BTC/USDT",
"BTC-USDT",
&"A".repeat(33),
] {
assert_eq!(
Symbol::new(bad),
Err(InvalidSymbol(bad.to_owned())),
"{bad:?}"
);
}
assert!(Symbol::new("A".repeat(32)).is_ok());
}
#[test]
fn ascii_letters_are_uppercased_and_nothing_else_changes() {
assert_eq!(
Symbol::new("btcusdt").unwrap(),
Symbol::new("BTCUSDT").unwrap()
);
assert_eq!(
Symbol::new("btcusdt_261225").unwrap().as_str(),
"BTCUSDT_261225"
);
assert_eq!(
Symbol::new("币安人生usdt").unwrap().as_str(),
"币安人生USDT"
);
}
#[test]
fn symbols_are_measured_in_characters_and_only_ascii_changes_case() {
assert_eq!(Symbol::new("straße").unwrap().as_str(), "STRAßE");
assert!(Symbol::new("币".repeat(32)).is_ok());
assert!(Symbol::new("币".repeat(33)).is_err());
}
#[test]
fn wire_spellings_are_binance_s() {
let intervals: Vec<&str> = Interval::ALL.iter().map(|i| i.as_str()).collect();
assert_eq!(
intervals,
[
"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d",
"1w", "1M"
]
);
let limits: Vec<&str> = DepthLimit::ALL.iter().map(|l| l.as_str()).collect();
assert_eq!(limits, ["5", "10", "20", "50", "100", "500", "1000"]);
}
#[test]
fn every_interval_round_trips_its_wire_spelling() {
assert_eq!(Interval::ALL.len(), 15);
for interval in Interval::ALL {
assert_eq!(interval.as_str().parse::<Interval>(), Ok(*interval));
}
assert!("1s".parse::<Interval>().is_err(), "this host refuses 1s");
}
#[test]
fn an_unknown_contract_type_is_kept_verbatim() {
let parsed: ContractType = serde_json::from_str(r#""NEXT_DECADE""#).unwrap();
assert_eq!(parsed, ContractType::Other("NEXT_DECADE".to_owned()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), r#""NEXT_DECADE""#);
let known: ContractType = serde_json::from_str(r#""TRADIFI_PERPETUAL""#).unwrap();
assert_eq!(known, ContractType::TradifiPerpetual);
}
#[test]
fn an_unseen_filter_type_is_kept_and_the_known_ones_parse() {
let filters: Vec<Filter> = serde_json::from_str(
r#"[{"filterType":"PRICE_FILTER","minPrice":"261.10","maxPrice":"809484","tickSize":"0.10"},
{"filterType":"MAX_NUM_ICEBERG_ORDERS","maxNumIcebergOrders":5}]"#,
)
.unwrap();
assert!(
matches!(&filters[0], Filter::PriceFilter { tick_size, .. } if *tick_size == Decimal::new(10, 2))
);
let Filter::Other { filter_type, raw } = &filters[1] else {
panic!("an unseen filter type must not fail the response");
};
assert_eq!(filter_type, "MAX_NUM_ICEBERG_ORDERS");
assert_eq!(raw["maxNumIcebergOrders"], 5);
let again: Vec<Value> =
serde_json::from_value(serde_json::to_value(&filters).unwrap()).unwrap();
assert_eq!(again[0]["filterType"], "PRICE_FILTER");
assert_eq!(again[1]["maxNumIcebergOrders"], 5);
}
#[test]
fn a_known_filter_with_a_missing_field_fails_loudly() {
let err = serde_json::from_str::<Filter>(r#"{"filterType":"LOT_SIZE","minQty":"0.001"}"#)
.unwrap_err();
assert!(err.to_string().contains("maxQty"), "{err}");
}
#[test]
fn a_kline_drops_the_ignored_element_and_tolerates_more() {
let wire = r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390","0","extra"]"#;
let kline: Kline = serde_json::from_str(wire).unwrap();
assert_eq!(kline.close, "84106.90".parse::<Decimal>().unwrap());
assert_eq!(kline.trade_count, 2912);
assert_eq!(
serde_json::to_string(&kline).unwrap(),
r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390"]"#
);
assert!(serde_json::from_str::<Kline>("[1791354000000]").is_err());
}
#[test]
fn a_level_keeps_every_digit() {
let level: Level = serde_json::from_str(r#"["84141.123456789012345","14.663"]"#).unwrap();
assert_eq!(level.price.to_string(), "84141.123456789012345");
assert_eq!(
serde_json::to_string(&level).unwrap(),
r#"["84141.123456789012345","14.663"]"#
);
}
#[test]
fn an_old_funding_rate_has_no_mark_price() {
let old: FundingRate = serde_json::from_str(
r#"{"symbol":"BTCUSDT","fundingTime":1568102400000,"fundingRate":"0.00010000","markPrice":"","rateType":"Regular"}"#,
)
.unwrap();
assert_eq!(old.mark_price, None);
assert_eq!(serde_json::to_value(&old).unwrap()["markPrice"], "");
let new: FundingRate = serde_json::from_str(
r#"{"symbol":"BTCUSDT","fundingTime":1791273600001,"fundingRate":"-0.00001592","markPrice":"85514.00007496","rateType":"Regular"}"#,
)
.unwrap();
assert_eq!(new.mark_price, Some("85514.00007496".parse().unwrap()));
}
}