use rust_decimal::Decimal;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
pub use crate::shared::symbol::{InvalidSymbol, Symbol};
#[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 rate_limits: Vec<RateLimit>,
pub exchange_filters: Vec<Filter>,
pub symbols: Vec<SymbolInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sors: Option<Vec<Sor>>,
}
#[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 Sor {
pub base_asset: String,
pub symbols: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SymbolInfo {
pub symbol: String,
pub status: SymbolStatus,
pub base_asset: String,
pub base_asset_precision: u32,
pub quote_asset: String,
pub quote_precision: u32,
pub quote_asset_precision: u32,
pub base_commission_precision: u32,
pub quote_commission_precision: u32,
pub order_types: Vec<String>,
pub iceberg_allowed: bool,
pub oco_allowed: bool,
pub oto_allowed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opo_allowed: Option<bool>,
pub quote_order_qty_market_allowed: bool,
pub allow_trailing_stop: bool,
pub cancel_replace_allowed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amend_allowed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peg_instructions_allowed: Option<bool>,
pub is_spot_trading_allowed: bool,
pub is_margin_trading_allowed: bool,
pub filters: Vec<Filter>,
pub permissions: Vec<String>,
pub permission_sets: Vec<Vec<String>>,
pub default_self_trade_prevention_mode: String,
pub allowed_self_trade_prevention_modes: Vec<String>,
}
polyoxide_venue::open_enum! {
pub enum SymbolStatus {
Trading => "TRADING",
Break => "BREAK",
Halt => "HALT",
PreTrading => "PRE_TRADING",
PostTrading => "POST_TRADING",
EndOfDay => "END_OF_DAY",
AuctionMatch => "AUCTION_MATCH",
}
}
macro_rules! filters {
($(
$(#[$vmeta:meta])*
$variant:ident = $tag:literal {
$( $field:ident : $ty:ty $(=> $with:literal)? ),* $(,)?
}
)+) => {
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Filter {
$(
$(#[$vmeta])*
#[non_exhaustive]
$variant { $( $field: $ty ),* },
)+
Other {
filter_type: String,
raw: Value,
},
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "filterType")]
enum FilterWire {
$(
#[serde(rename = $tag, rename_all = "camelCase")]
$variant { $( $(#[serde(with = $with)])? $field: $ty ),* },
)+
}
impl FilterWire {
const KNOWN: &'static [&'static str] = &[$($tag),+];
}
impl From<FilterWire> for Filter {
fn from(wire: FilterWire) -> Self {
match wire {
$( FilterWire::$variant { $($field),* } => Self::$variant { $($field),* }, )+
}
}
}
impl<'de> Deserialize<'de> for Filter {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = Value::deserialize(deserializer)?;
let filter_type = raw
.get("filterType")
.and_then(Value::as_str)
.ok_or_else(|| <D::Error as de::Error>::custom("a filter without a string filterType"))?
.to_owned();
if !FilterWire::KNOWN.contains(&filter_type.as_str()) {
return Ok(Self::Other { filter_type, raw });
}
let wire: FilterWire =
serde_json::from_value(raw).map_err(<D::Error as de::Error>::custom)?;
Ok(wire.into())
}
}
impl Serialize for Filter {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let wire = match self {
$( Self::$variant { $($field),* } => FilterWire::$variant { $($field: *$field),* }, )+
Self::Other { raw, .. } => return raw.serialize(serializer),
};
wire.serialize(serializer)
}
}
};
}
filters! {
PriceFilter = "PRICE_FILTER" {
min_price: Decimal => "rust_decimal::serde::str",
max_price: Decimal => "rust_decimal::serde::str",
tick_size: Decimal => "rust_decimal::serde::str",
}
PercentPriceBySide = "PERCENT_PRICE_BY_SIDE" {
bid_multiplier_up: Decimal => "rust_decimal::serde::str",
bid_multiplier_down: Decimal => "rust_decimal::serde::str",
ask_multiplier_up: Decimal => "rust_decimal::serde::str",
ask_multiplier_down: Decimal => "rust_decimal::serde::str",
avg_price_mins: u32,
}
LotSize = "LOT_SIZE" {
min_qty: Decimal => "rust_decimal::serde::str",
max_qty: Decimal => "rust_decimal::serde::str",
step_size: Decimal => "rust_decimal::serde::str",
}
Notional = "NOTIONAL" {
min_notional: Decimal => "rust_decimal::serde::str",
apply_min_to_market: bool,
max_notional: Decimal => "rust_decimal::serde::str",
apply_max_to_market: bool,
avg_price_mins: u32,
}
IcebergParts = "ICEBERG_PARTS" {
limit: u32,
}
MarketLotSize = "MARKET_LOT_SIZE" {
min_qty: Decimal => "rust_decimal::serde::str",
max_qty: Decimal => "rust_decimal::serde::str",
step_size: Decimal => "rust_decimal::serde::str",
}
MaxNumOrders = "MAX_NUM_ORDERS" {
max_num_orders: u32,
}
MaxNumAlgoOrders = "MAX_NUM_ALGO_ORDERS" {
max_num_algo_orders: u32,
}
MaxPosition = "MAX_POSITION" {
max_position: Decimal => "rust_decimal::serde::str",
}
TrailingDelta = "TRAILING_DELTA" {
min_trailing_above_delta: u64,
max_trailing_above_delta: u64,
min_trailing_below_delta: u64,
max_trailing_below_delta: u64,
}
MaxNumOrderAmends = "MAX_NUM_ORDER_AMENDS" {
max_num_order_amends: u32,
}
MaxNumOrderLists = "MAX_NUM_ORDER_LISTS" {
max_num_order_lists: u32,
}
}
pub use rows::{
AggTrade, AvgPrice, BookTicker, Depth, Interval, Kline, Level, PriceTicker, Ticker24h, Trade,
};
mod rows {
use std::fmt;
use polyoxide_venue::positional::{drain, element, DecimalStr};
use rust_decimal::Decimal;
use serde::{
de::{SeqAccess, Visitor},
ser::SerializeSeq,
Deserialize, Deserializer, Serialize, Serializer,
};
polyoxide_venue::wire_enum! {
pub enum Interval {
Second1 => "1s",
Minute1 => "1m",
Minute3 => "3m",
Minute5 => "5m",
Minute15 => "15m",
Minute30 => "30m",
Hour1 => "1h",
Hour2 => "2h",
Hour4 => "4h",
Hour6 => "6h",
Hour8 => "8h",
Hour12 => "12h",
Day1 => "1d",
Day3 => "3d",
Week1 => "1w",
Month1 => "1M",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Depth {
pub last_update_id: 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: element::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
quantity: element::<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()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Trade {
pub id: u64,
#[serde(with = "rust_decimal::serde::str")]
pub price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub quote_qty: Decimal,
pub time: u64,
pub is_buyer_maker: bool,
pub is_best_match: bool,
}
#[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 = "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,
#[serde(rename = "M")]
pub is_best_match: bool,
}
#[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,
}
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: element(&mut seq, 0, WHAT)?,
open: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
high: element::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
low: element::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
close: element::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
volume: element::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
close_time: element(&mut seq, 6, WHAT)?,
quote_volume: element::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
trade_count: element(&mut seq, 8, WHAT)?,
taker_buy_base_volume: element::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
taker_buy_quote_volume: element::<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 AvgPrice {
pub mins: u32,
#[serde(with = "rust_decimal::serde::str")]
pub price: Decimal,
pub close_time: 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 prev_close_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 bid_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub bid_qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub ask_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub ask_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)]
#[non_exhaustive]
pub struct PriceTicker {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub price: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BookTicker {
pub symbol: String,
#[serde(with = "rust_decimal::serde::str")]
pub bid_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub bid_qty: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub ask_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub ask_qty: Decimal,
}
}
#[cfg(test)]
mod tests {
use polyoxide_test_support::{agreement, fixtures};
use super::*;
fn text(name: &str) -> String {
fixtures!("spot/rest").text(name)
}
#[test]
fn the_exchange_info_capture_agrees_with_its_types() {
let text = text("exchange_info");
let diff = agreement::compare::<ExchangeInfo>("exchange_info", &text);
assert!(
diff.unmodelled.is_empty(),
"the server sent {:?}, which the types do not model",
diff.unmodelled
);
assert!(
diff.invented.is_empty(),
"the types emit {:?}, which the server did not send",
diff.invented
);
}
#[test]
fn every_symbol_and_filter_serializes_back_to_what_the_server_sent() {
let text = text("exchange_info");
let raw: Value = serde_json::from_str(&text).unwrap();
let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
assert_eq!(
serde_json::to_value(&info.rate_limits).unwrap(),
raw["rateLimits"]
);
assert_eq!(
serde_json::to_value(&info.exchange_filters).unwrap(),
raw["exchangeFilters"]
);
let wire = raw["symbols"].as_array().unwrap();
assert_eq!(info.symbols.len(), wire.len());
for (symbol, sent) in info.symbols.iter().zip(wire) {
assert_eq!(
serde_json::to_value(symbol).unwrap(),
*sent,
"{}",
symbol.symbol
);
}
}
#[test]
fn the_capture_holds_the_cases_the_types_exist_for() {
let info: ExchangeInfo = serde_json::from_str(&text("exchange_info")).unwrap();
assert!(info.symbols.len() >= 2, "BTCUSDT and a non-trading symbol");
assert!(
info.symbols
.iter()
.any(|s| s.symbol == "BTCUSDT" && s.status == SymbolStatus::Trading),
"BTCUSDT, trading"
);
assert!(
info.symbols
.iter()
.any(|s| s.status != SymbolStatus::Trading),
"a symbol that is not trading"
);
let unnamed: Vec<_> = info
.symbols
.iter()
.filter(|s| matches!(s.status, SymbolStatus::Other(_)))
.map(|s| (&s.symbol, &s.status))
.collect();
assert!(
unnamed.is_empty(),
"statuses the enum does not name: {unnamed:?}"
);
let unnamed: Vec<&Filter> = info
.symbols
.iter()
.flat_map(|s| &s.filters)
.chain(&info.exchange_filters)
.filter(|f| matches!(f, Filter::Other { .. }))
.collect();
assert!(
unnamed.is_empty(),
"filter types Filter does not name, add them to `filters!`: {unnamed:?}"
);
}
#[test]
fn the_captured_btcusdt_filters_decode_to_their_values() {
let text = text("exchange_info");
let raw: Value = serde_json::from_str(&text).unwrap();
let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
let btc = info.symbols.iter().find(|s| s.symbol == "BTCUSDT").unwrap();
let wire = raw["symbols"]
.as_array()
.unwrap()
.iter()
.find(|s| s["symbol"] == "BTCUSDT")
.unwrap();
let sent = |filter_type: &str, key: &str| -> String {
wire["filters"]
.as_array()
.unwrap()
.iter()
.find(|f| f["filterType"] == filter_type)
.unwrap_or_else(|| panic!("no {filter_type}"))[key]
.as_str()
.unwrap()
.to_owned()
};
let price = btc
.filters
.iter()
.find_map(|f| match f {
Filter::PriceFilter { tick_size, .. } => Some(*tick_size),
_ => None,
})
.expect("a PRICE_FILTER");
assert_eq!(price.to_string(), sent("PRICE_FILTER", "tickSize"));
let lot = btc
.filters
.iter()
.find_map(|f| match f {
Filter::LotSize { step_size, .. } => Some(*step_size),
_ => None,
})
.expect("a LOT_SIZE");
assert_eq!(lot.to_string(), sent("LOT_SIZE", "stepSize"));
let market = btc
.filters
.iter()
.find_map(|f| match f {
Filter::MarketLotSize { step_size, .. } => Some(*step_size),
_ => None,
})
.expect("a MARKET_LOT_SIZE");
assert_eq!(market.to_string(), sent("MARKET_LOT_SIZE", "stepSize"));
let notional = btc
.filters
.iter()
.find_map(|f| match f {
Filter::Notional { min_notional, .. } => Some(*min_notional),
_ => None,
})
.expect("a NOTIONAL");
assert_eq!(notional.to_string(), sent("NOTIONAL", "minNotional"));
}
#[test]
fn the_time_capture_agrees_with_its_type() {
let text = text("time");
let diff = agreement::compare::<ServerTime>("time", &text);
assert!(diff.unmodelled.is_empty(), "{:?}", diff.unmodelled);
assert!(diff.invented.is_empty(), "{:?}", diff.invented);
let time: ServerTime = serde_json::from_str(&text).unwrap();
assert!(time.server_time > 1_790_000_000_000);
}
#[test]
fn an_unknown_filter_type_is_kept_whole() {
let raw = serde_json::json!({"filterType": "A_FILTER_FROM_THE_FUTURE", "limit": 7});
let filter: Filter = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(
&filter,
Filter::Other { filter_type, raw: kept }
if filter_type == "A_FILTER_FROM_THE_FUTURE" && *kept == raw
));
assert_eq!(serde_json::to_value(&filter).unwrap(), raw);
}
#[test]
fn a_known_filter_that_loses_a_field_fails() {
let raw = serde_json::json!({"filterType": "PRICE_FILTER", "minPrice": "0.01000000"});
assert!(serde_json::from_value::<Filter>(raw).is_err());
}
#[test]
fn a_filter_without_a_type_fails() {
let raw = serde_json::json!({"minPrice": "0.01000000"});
let err = serde_json::from_value::<Filter>(raw).unwrap_err();
assert!(err.to_string().contains("filterType"), "{err}");
}
#[test]
fn a_status_keeps_an_unknown_spelling() {
assert_eq!(SymbolStatus::from_wire("TRADING"), SymbolStatus::Trading);
assert_eq!(
SymbolStatus::from_wire("PRE_TRADING"),
SymbolStatus::PreTrading
);
let unknown = SymbolStatus::from_wire("A_NEW_STATE");
assert_eq!(unknown, SymbolStatus::Other("A_NEW_STATE".into()));
assert_eq!(unknown.as_str(), "A_NEW_STATE");
}
#[test]
fn the_status_spellings_are_binance_s() {
let table = [
(SymbolStatus::Trading, "TRADING"),
(SymbolStatus::Break, "BREAK"),
(SymbolStatus::Halt, "HALT"),
(SymbolStatus::PreTrading, "PRE_TRADING"),
(SymbolStatus::PostTrading, "POST_TRADING"),
(SymbolStatus::EndOfDay, "END_OF_DAY"),
(SymbolStatus::AuctionMatch, "AUCTION_MATCH"),
];
for (status, wire) in table {
assert_eq!(status.as_str(), wire);
}
}
}