Skip to main content

polyoxide_binance/spot/
types.rs

1//! Vocabulary and response rows for Binance spot.
2//!
3//! Prices and quantities are [`Decimal`], decoded from the decimal strings
4//! Binance sends, so no value passes through an `f64`. Timestamps are Unix
5//! milliseconds. Response rows carry symbols as `String`, taken as sent, so a
6//! symbol [`Symbol::new`] would refuse never fails a whole response.
7
8use rust_decimal::Decimal;
9use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::Value;
11
12pub use crate::shared::symbol::{InvalidSymbol, Symbol};
13
14/// `GET /api/v3/time`.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17#[non_exhaustive]
18pub struct ServerTime {
19    /// Server clock, Unix milliseconds.
20    pub server_time: u64,
21}
22
23/// `GET /api/v3/exchangeInfo`.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26#[non_exhaustive]
27pub struct ExchangeInfo {
28    /// Always `UTC`.
29    pub timezone: String,
30    /// Server clock, Unix milliseconds.
31    pub server_time: u64,
32    /// The IP's request-weight, raw-request and order limits.
33    pub rate_limits: Vec<RateLimit>,
34    /// Exchange-wide filters.
35    pub exchange_filters: Vec<Filter>,
36    /// Every listed symbol, including ones that are not trading.
37    pub symbols: Vec<SymbolInfo>,
38    /// Smart order routing: the symbols an order for a base asset may be
39    /// routed across. Absent when the host sends none.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub sors: Option<Vec<Sor>>,
42}
43
44/// One row of `exchangeInfo`'s `rateLimits`.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47#[non_exhaustive]
48pub struct RateLimit {
49    /// `REQUEST_WEIGHT`, `RAW_REQUESTS` or `ORDERS`.
50    pub rate_limit_type: String,
51    /// `SECOND`, `MINUTE` or `DAY`.
52    pub interval: String,
53    /// Intervals per window.
54    pub interval_num: u32,
55    /// The limit per window.
56    pub limit: u32,
57}
58
59/// One smart-order-routing entry in `exchangeInfo`.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62#[non_exhaustive]
63pub struct Sor {
64    /// The base asset the routing applies to.
65    pub base_asset: String,
66    /// The symbols an order may be routed across.
67    pub symbols: Vec<String>,
68}
69
70/// One symbol in `exchangeInfo`.
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73#[non_exhaustive]
74pub struct SymbolInfo {
75    /// The symbol, as Binance spells it.
76    pub symbol: String,
77    /// Trading, halted and so on.
78    pub status: SymbolStatus,
79    /// Base asset.
80    pub base_asset: String,
81    /// Decimal places of the base asset.
82    pub base_asset_precision: u32,
83    /// Quote asset.
84    pub quote_asset: String,
85    /// Decimal places of the quote asset, as Binance's older key spells it.
86    pub quote_precision: u32,
87    /// Decimal places of the quote asset.
88    pub quote_asset_precision: u32,
89    /// Decimal places of the base asset's commission.
90    pub base_commission_precision: u32,
91    /// Decimal places of the quote asset's commission.
92    pub quote_commission_precision: u32,
93    /// Order types the symbol accepts.
94    pub order_types: Vec<String>,
95    /// Whether iceberg orders are allowed.
96    pub iceberg_allowed: bool,
97    /// Whether OCO order lists are allowed.
98    pub oco_allowed: bool,
99    /// Whether OTO order lists are allowed.
100    pub oto_allowed: bool,
101    /// Whether OPO order lists are allowed. Every captured row carries the flag;
102    /// it is optional so a row without it still decodes.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub opo_allowed: Option<bool>,
105    /// Whether a market order may name a quote quantity.
106    pub quote_order_qty_market_allowed: bool,
107    /// Whether trailing stops are allowed.
108    pub allow_trailing_stop: bool,
109    /// Whether cancel-replace is allowed.
110    pub cancel_replace_allowed: bool,
111    /// Whether order amendment is allowed. Every captured row carries the flag;
112    /// it is optional so a row without it still decodes.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub amend_allowed: Option<bool>,
115    /// Whether pegged orders are allowed. Every captured row carries the flag;
116    /// it is optional so a row without it still decodes.
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub peg_instructions_allowed: Option<bool>,
119    /// Whether the symbol can be traded as spot.
120    pub is_spot_trading_allowed: bool,
121    /// Whether the symbol can be traded on margin.
122    pub is_margin_trading_allowed: bool,
123    /// Order filters.
124    pub filters: Vec<Filter>,
125    /// Permissions the symbol is open to (older key).
126    pub permissions: Vec<String>,
127    /// Permission sets: an account needs every permission of one set.
128    pub permission_sets: Vec<Vec<String>>,
129    /// The self-trade-prevention mode applied when an order names none.
130    pub default_self_trade_prevention_mode: String,
131    /// The self-trade-prevention modes an order may name.
132    pub allowed_self_trade_prevention_modes: Vec<String>,
133}
134
135polyoxide_venue::open_enum! {
136    /// A symbol's trading status, from `exchangeInfo`. The variants are the
137    /// list in Binance's common definitions.
138    pub enum SymbolStatus {
139        Trading => "TRADING",
140        Break => "BREAK",
141        Halt => "HALT",
142        PreTrading => "PRE_TRADING",
143        PostTrading => "POST_TRADING",
144        EndOfDay => "END_OF_DAY",
145        AuctionMatch => "AUCTION_MATCH",
146    }
147}
148
149/// Declares [`Filter`] and the private wire enum its serde goes through from
150/// one table: each entry is a variant, its wire `filterType`, and its fields
151/// (camelCase on the wire, a decimal string where `=> "..."` names the
152/// `serde(with)`). Every field type is `Copy`, which the serializer relies on.
153macro_rules! filters {
154    ($(
155        $(#[$vmeta:meta])*
156        $variant:ident = $tag:literal {
157            $( $field:ident : $ty:ty $(=> $with:literal)? ),* $(,)?
158        }
159    )+) => {
160        /// An `exchangeInfo` filter, named after its wire `filterType`.
161        ///
162        /// A filter type this version does not know is kept whole in
163        /// [`Filter::Other`], so a new one never fails the response. A known
164        /// type that loses or retypes a field still fails; one that gains a
165        /// field decodes without it, and the wire-agreement tests are what
166        /// notice.
167        #[derive(Debug, Clone, PartialEq)]
168        #[non_exhaustive]
169        pub enum Filter {
170            $(
171                $(#[$vmeta])*
172                #[non_exhaustive]
173                $variant { $( $field: $ty ),* },
174            )+
175            /// A filter type this version does not model.
176            Other {
177                /// The wire `filterType`.
178                filter_type: String,
179                /// The whole filter object, `filterType` included.
180                raw: Value,
181            },
182        }
183
184        /// The wire bodies of the known filters, which `Filter`'s serde goes
185        /// through.
186        #[derive(Serialize, Deserialize)]
187        #[serde(tag = "filterType")]
188        enum FilterWire {
189            $(
190                #[serde(rename = $tag, rename_all = "camelCase")]
191                $variant { $( $(#[serde(with = $with)])? $field: $ty ),* },
192            )+
193        }
194
195        impl FilterWire {
196            /// Every `filterType` this version models.
197            const KNOWN: &'static [&'static str] = &[$($tag),+];
198        }
199
200        impl From<FilterWire> for Filter {
201            fn from(wire: FilterWire) -> Self {
202                match wire {
203                    $( FilterWire::$variant { $($field),* } => Self::$variant { $($field),* }, )+
204                }
205            }
206        }
207
208        impl<'de> Deserialize<'de> for Filter {
209            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210                let raw = Value::deserialize(deserializer)?;
211                let filter_type = raw
212                    .get("filterType")
213                    .and_then(Value::as_str)
214                    .ok_or_else(|| <D::Error as de::Error>::custom("a filter without a string filterType"))?
215                    .to_owned();
216                if !FilterWire::KNOWN.contains(&filter_type.as_str()) {
217                    return Ok(Self::Other { filter_type, raw });
218                }
219                let wire: FilterWire =
220                    serde_json::from_value(raw).map_err(<D::Error as de::Error>::custom)?;
221                Ok(wire.into())
222            }
223        }
224
225        impl Serialize for Filter {
226            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
227                let wire = match self {
228                    $( Self::$variant { $($field),* } => FilterWire::$variant { $($field: *$field),* }, )+
229                    Self::Other { raw, .. } => return raw.serialize(serializer),
230                };
231                wire.serialize(serializer)
232            }
233        }
234    };
235}
236
237filters! {
238    /// `PRICE_FILTER`.
239    PriceFilter = "PRICE_FILTER" {
240        min_price: Decimal => "rust_decimal::serde::str",
241        max_price: Decimal => "rust_decimal::serde::str",
242        tick_size: Decimal => "rust_decimal::serde::str",
243    }
244    /// `PERCENT_PRICE_BY_SIDE`.
245    PercentPriceBySide = "PERCENT_PRICE_BY_SIDE" {
246        bid_multiplier_up: Decimal => "rust_decimal::serde::str",
247        bid_multiplier_down: Decimal => "rust_decimal::serde::str",
248        ask_multiplier_up: Decimal => "rust_decimal::serde::str",
249        ask_multiplier_down: Decimal => "rust_decimal::serde::str",
250        avg_price_mins: u32,
251    }
252    /// `LOT_SIZE`: limit orders.
253    LotSize = "LOT_SIZE" {
254        min_qty: Decimal => "rust_decimal::serde::str",
255        max_qty: Decimal => "rust_decimal::serde::str",
256        step_size: Decimal => "rust_decimal::serde::str",
257    }
258    /// `NOTIONAL`: the minimum and maximum order value.
259    Notional = "NOTIONAL" {
260        min_notional: Decimal => "rust_decimal::serde::str",
261        apply_min_to_market: bool,
262        max_notional: Decimal => "rust_decimal::serde::str",
263        apply_max_to_market: bool,
264        avg_price_mins: u32,
265    }
266    /// `ICEBERG_PARTS`.
267    IcebergParts = "ICEBERG_PARTS" {
268        limit: u32,
269    }
270    /// `MARKET_LOT_SIZE`: market orders.
271    MarketLotSize = "MARKET_LOT_SIZE" {
272        min_qty: Decimal => "rust_decimal::serde::str",
273        max_qty: Decimal => "rust_decimal::serde::str",
274        step_size: Decimal => "rust_decimal::serde::str",
275    }
276    /// `MAX_NUM_ORDERS`.
277    MaxNumOrders = "MAX_NUM_ORDERS" {
278        max_num_orders: u32,
279    }
280    /// `MAX_NUM_ALGO_ORDERS`.
281    MaxNumAlgoOrders = "MAX_NUM_ALGO_ORDERS" {
282        max_num_algo_orders: u32,
283    }
284    /// `MAX_POSITION`.
285    MaxPosition = "MAX_POSITION" {
286        max_position: Decimal => "rust_decimal::serde::str",
287    }
288    /// `TRAILING_DELTA`.
289    TrailingDelta = "TRAILING_DELTA" {
290        min_trailing_above_delta: u64,
291        max_trailing_above_delta: u64,
292        min_trailing_below_delta: u64,
293        max_trailing_below_delta: u64,
294    }
295    /// `MAX_NUM_ORDER_AMENDS`.
296    MaxNumOrderAmends = "MAX_NUM_ORDER_AMENDS" {
297        max_num_order_amends: u32,
298    }
299    /// `MAX_NUM_ORDER_LISTS`.
300    MaxNumOrderLists = "MAX_NUM_ORDER_LISTS" {
301        max_num_order_lists: u32,
302    }
303}
304
305pub use rows::{
306    AggTrade, AvgPrice, BookTicker, Depth, Interval, Kline, Level, PriceTicker, Ticker24h, Trade,
307};
308
309/// The market routes' rows. Prices and quantities are `Decimal`, decoded from
310/// the decimal strings Binance sends, so no value passes through an `f64`;
311/// timestamps are Unix milliseconds. Where the wire uses one-letter keys
312/// (`aggTrades`), fields take spelled-out names through serde renames.
313mod rows {
314    use std::fmt;
315
316    use polyoxide_venue::positional::{drain, element, DecimalStr};
317    use rust_decimal::Decimal;
318    use serde::{
319        de::{SeqAccess, Visitor},
320        ser::SerializeSeq,
321        Deserialize, Deserializer, Serialize, Serializer,
322    };
323
324    polyoxide_venue::wire_enum! {
325        /// Kline width, for `klines`. This host takes `1s`, which the USDⓈ-M
326        /// host refuses.
327        pub enum Interval {
328            /// One second.
329            Second1 => "1s",
330            /// One minute.
331            Minute1 => "1m",
332            /// Three minutes.
333            Minute3 => "3m",
334            /// Five minutes.
335            Minute5 => "5m",
336            /// Fifteen minutes.
337            Minute15 => "15m",
338            /// Thirty minutes.
339            Minute30 => "30m",
340            /// One hour.
341            Hour1 => "1h",
342            /// Two hours.
343            Hour2 => "2h",
344            /// Four hours.
345            Hour4 => "4h",
346            /// Six hours.
347            Hour6 => "6h",
348            /// Eight hours.
349            Hour8 => "8h",
350            /// Twelve hours.
351            Hour12 => "12h",
352            /// One day.
353            Day1 => "1d",
354            /// Three days.
355            Day3 => "3d",
356            /// One week.
357            Week1 => "1w",
358            /// One calendar month.
359            Month1 => "1M",
360        }
361    }
362
363    /// `GET /api/v3/depth`: an order book snapshot. It carries no time.
364    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365    #[serde(rename_all = "camelCase")]
366    #[non_exhaustive]
367    pub struct Depth {
368        /// Book update id of the snapshot.
369        pub last_update_id: u64,
370        /// Bids, best first.
371        pub bids: Vec<Level>,
372        /// Asks, best first.
373        pub asks: Vec<Level>,
374    }
375
376    /// A price level, a `[price, quantity]` pair on the wire.
377    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
378    #[non_exhaustive]
379    pub struct Level {
380        /// Price.
381        pub price: Decimal,
382        /// Quantity at the price.
383        pub quantity: Decimal,
384    }
385
386    impl Level {
387        /// A level from a price and a quantity.
388        pub fn new(price: Decimal, quantity: Decimal) -> Self {
389            Self { price, quantity }
390        }
391    }
392
393    impl<'de> Deserialize<'de> for Level {
394        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
395            struct LevelVisitor;
396
397            impl<'de> Visitor<'de> for LevelVisitor {
398                type Value = Level;
399
400                fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401                    f.write_str("a [price, quantity] array")
402                }
403
404                fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
405                    const WHAT: &str = "a [price, quantity] array";
406                    let level = Level {
407                        price: element::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
408                        quantity: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
409                    };
410                    drain(&mut seq)?;
411                    Ok(level)
412                }
413            }
414
415            deserializer.deserialize_seq(LevelVisitor)
416        }
417    }
418
419    impl Serialize for Level {
420        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
421            let mut seq = serializer.serialize_seq(Some(2))?;
422            seq.serialize_element(&self.price.to_string())?;
423            seq.serialize_element(&self.quantity.to_string())?;
424            seq.end()
425        }
426    }
427
428    /// One row of `GET /api/v3/trades`, oldest first.
429    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430    #[serde(rename_all = "camelCase")]
431    #[non_exhaustive]
432    pub struct Trade {
433        /// Trade id.
434        pub id: u64,
435        /// Price.
436        #[serde(with = "rust_decimal::serde::str")]
437        pub price: Decimal,
438        /// Base-asset quantity.
439        #[serde(with = "rust_decimal::serde::str")]
440        pub qty: Decimal,
441        /// Quote-asset quantity.
442        #[serde(with = "rust_decimal::serde::str")]
443        pub quote_qty: Decimal,
444        /// Trade time.
445        pub time: u64,
446        /// Whether the buyer was the maker, so the taker sold.
447        pub is_buyer_maker: bool,
448        /// Whether the trade was the best price match.
449        pub is_best_match: bool,
450    }
451
452    /// One row of `GET /api/v3/aggTrades`: trades at one price, taken by one
453    /// order, merged.
454    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455    #[non_exhaustive]
456    pub struct AggTrade {
457        /// Aggregate trade id.
458        #[serde(rename = "a")]
459        pub id: u64,
460        /// Price.
461        #[serde(rename = "p", with = "rust_decimal::serde::str")]
462        pub price: Decimal,
463        /// Quantity.
464        #[serde(rename = "q", with = "rust_decimal::serde::str")]
465        pub quantity: Decimal,
466        /// First trade id merged.
467        #[serde(rename = "f")]
468        pub first_trade_id: u64,
469        /// Last trade id merged.
470        #[serde(rename = "l")]
471        pub last_trade_id: u64,
472        /// Trade time.
473        #[serde(rename = "T")]
474        pub time: u64,
475        /// Whether the buyer was the maker, so the taker sold.
476        #[serde(rename = "m")]
477        pub is_buyer_maker: bool,
478        /// Whether the trade was the best price match.
479        #[serde(rename = "M")]
480        pub is_best_match: bool,
481    }
482
483    /// One candle of `GET /api/v3/klines`, a positional array on the wire.
484    ///
485    /// The wire sends a twelfth element Binance documents as "ignore"; it is
486    /// dropped, and any further elements are tolerated.
487    #[derive(Debug, Clone, PartialEq, Eq)]
488    #[non_exhaustive]
489    pub struct Kline {
490        /// Candle start.
491        pub open_time: u64,
492        /// Open price.
493        pub open: Decimal,
494        /// High price.
495        pub high: Decimal,
496        /// Low price.
497        pub low: Decimal,
498        /// Close price.
499        pub close: Decimal,
500        /// Base-asset volume.
501        pub volume: Decimal,
502        /// Candle end, inclusive.
503        pub close_time: u64,
504        /// Quote-asset volume.
505        pub quote_volume: Decimal,
506        /// Trades in the candle.
507        pub trade_count: u64,
508        /// Base-asset volume bought by takers.
509        pub taker_buy_base_volume: Decimal,
510        /// Quote-asset volume bought by takers.
511        pub taker_buy_quote_volume: Decimal,
512    }
513
514    impl<'de> Deserialize<'de> for Kline {
515        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516            struct KlineVisitor;
517
518            impl<'de> Visitor<'de> for KlineVisitor {
519                type Value = Kline;
520
521                fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522                    f.write_str("a kline array of at least 11 elements")
523                }
524
525                fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
526                    const WHAT: &str = "a kline array of at least 11 elements";
527                    let kline = Kline {
528                        open_time: element(&mut seq, 0, WHAT)?,
529                        open: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
530                        high: element::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
531                        low: element::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
532                        close: element::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
533                        volume: element::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
534                        close_time: element(&mut seq, 6, WHAT)?,
535                        quote_volume: element::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
536                        trade_count: element(&mut seq, 8, WHAT)?,
537                        taker_buy_base_volume: element::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
538                        taker_buy_quote_volume: element::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
539                    };
540                    drain(&mut seq)?;
541                    Ok(kline)
542                }
543            }
544
545            deserializer.deserialize_seq(KlineVisitor)
546        }
547    }
548
549    impl Serialize for Kline {
550        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
551            let mut seq = serializer.serialize_seq(Some(11))?;
552            seq.serialize_element(&self.open_time)?;
553            for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
554                seq.serialize_element(&value.to_string())?;
555            }
556            seq.serialize_element(&self.close_time)?;
557            seq.serialize_element(&self.quote_volume.to_string())?;
558            seq.serialize_element(&self.trade_count)?;
559            seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
560            seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
561            seq.end()
562        }
563    }
564
565    /// `GET /api/v3/avgPrice`: the average price over the last `mins` minutes.
566    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567    #[serde(rename_all = "camelCase")]
568    #[non_exhaustive]
569    pub struct AvgPrice {
570        /// The window, in minutes.
571        pub mins: u32,
572        /// The average price.
573        #[serde(with = "rust_decimal::serde::str")]
574        pub price: Decimal,
575        /// When the average was computed.
576        pub close_time: u64,
577    }
578
579    /// `GET /api/v3/ticker/24hr`: rolling 24-hour statistics.
580    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
581    #[serde(rename_all = "camelCase")]
582    #[non_exhaustive]
583    pub struct Ticker24h {
584        /// The pair.
585        pub symbol: String,
586        /// Last price less the open price.
587        #[serde(with = "rust_decimal::serde::str")]
588        pub price_change: Decimal,
589        /// The change as a percentage.
590        #[serde(with = "rust_decimal::serde::str")]
591        pub price_change_percent: Decimal,
592        /// Volume-weighted average price.
593        #[serde(with = "rust_decimal::serde::str")]
594        pub weighted_avg_price: Decimal,
595        /// The previous day's close.
596        #[serde(with = "rust_decimal::serde::str")]
597        pub prev_close_price: Decimal,
598        /// Last traded price.
599        #[serde(with = "rust_decimal::serde::str")]
600        pub last_price: Decimal,
601        /// Last traded quantity.
602        #[serde(with = "rust_decimal::serde::str")]
603        pub last_qty: Decimal,
604        /// Best bid price.
605        #[serde(with = "rust_decimal::serde::str")]
606        pub bid_price: Decimal,
607        /// Quantity at the best bid.
608        #[serde(with = "rust_decimal::serde::str")]
609        pub bid_qty: Decimal,
610        /// Best ask price.
611        #[serde(with = "rust_decimal::serde::str")]
612        pub ask_price: Decimal,
613        /// Quantity at the best ask.
614        #[serde(with = "rust_decimal::serde::str")]
615        pub ask_qty: Decimal,
616        /// Price 24 hours ago.
617        #[serde(with = "rust_decimal::serde::str")]
618        pub open_price: Decimal,
619        /// Highest price.
620        #[serde(with = "rust_decimal::serde::str")]
621        pub high_price: Decimal,
622        /// Lowest price.
623        #[serde(with = "rust_decimal::serde::str")]
624        pub low_price: Decimal,
625        /// Base-asset volume.
626        #[serde(with = "rust_decimal::serde::str")]
627        pub volume: Decimal,
628        /// Quote-asset volume.
629        #[serde(with = "rust_decimal::serde::str")]
630        pub quote_volume: Decimal,
631        /// Window start.
632        pub open_time: u64,
633        /// Window end.
634        pub close_time: u64,
635        /// First trade id in the window, `-1` when there is none.
636        pub first_id: i64,
637        /// Last trade id in the window, `-1` when there is none.
638        pub last_id: i64,
639        /// Trades in the window.
640        pub count: u64,
641    }
642
643    /// `GET /api/v3/ticker/price`: the last price.
644    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
645    #[non_exhaustive]
646    pub struct PriceTicker {
647        /// The pair.
648        pub symbol: String,
649        /// Last traded price.
650        #[serde(with = "rust_decimal::serde::str")]
651        pub price: Decimal,
652    }
653
654    /// `GET /api/v3/ticker/bookTicker`: the best bid and ask. It carries no
655    /// time.
656    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657    #[serde(rename_all = "camelCase")]
658    #[non_exhaustive]
659    pub struct BookTicker {
660        /// The pair.
661        pub symbol: String,
662        /// Best bid price.
663        #[serde(with = "rust_decimal::serde::str")]
664        pub bid_price: Decimal,
665        /// Quantity at the best bid.
666        #[serde(with = "rust_decimal::serde::str")]
667        pub bid_qty: Decimal,
668        /// Best ask price.
669        #[serde(with = "rust_decimal::serde::str")]
670        pub ask_price: Decimal,
671        /// Quantity at the best ask.
672        #[serde(with = "rust_decimal::serde::str")]
673        pub ask_qty: Decimal,
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use polyoxide_test_support::{agreement, fixtures};
680
681    use super::*;
682
683    fn text(name: &str) -> String {
684        fixtures!("spot/rest").text(name)
685    }
686
687    #[test]
688    fn the_exchange_info_capture_agrees_with_its_types() {
689        let text = text("exchange_info");
690        let diff = agreement::compare::<ExchangeInfo>("exchange_info", &text);
691        assert!(
692            diff.unmodelled.is_empty(),
693            "the server sent {:?}, which the types do not model",
694            diff.unmodelled
695        );
696        assert!(
697            diff.invented.is_empty(),
698            "the types emit {:?}, which the server did not send",
699            diff.invented
700        );
701    }
702
703    #[test]
704    fn every_symbol_and_filter_serializes_back_to_what_the_server_sent() {
705        let text = text("exchange_info");
706        let raw: Value = serde_json::from_str(&text).unwrap();
707        let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
708        assert_eq!(
709            serde_json::to_value(&info.rate_limits).unwrap(),
710            raw["rateLimits"]
711        );
712        assert_eq!(
713            serde_json::to_value(&info.exchange_filters).unwrap(),
714            raw["exchangeFilters"]
715        );
716        let wire = raw["symbols"].as_array().unwrap();
717        assert_eq!(info.symbols.len(), wire.len());
718        for (symbol, sent) in info.symbols.iter().zip(wire) {
719            assert_eq!(
720                serde_json::to_value(symbol).unwrap(),
721                *sent,
722                "{}",
723                symbol.symbol
724            );
725        }
726    }
727
728    #[test]
729    fn the_capture_holds_the_cases_the_types_exist_for() {
730        let info: ExchangeInfo = serde_json::from_str(&text("exchange_info")).unwrap();
731        assert!(info.symbols.len() >= 2, "BTCUSDT and a non-trading symbol");
732        assert!(
733            info.symbols
734                .iter()
735                .any(|s| s.symbol == "BTCUSDT" && s.status == SymbolStatus::Trading),
736            "BTCUSDT, trading"
737        );
738        assert!(
739            info.symbols
740                .iter()
741                .any(|s| s.status != SymbolStatus::Trading),
742            "a symbol that is not trading"
743        );
744        // An unknown value would land in `Other` and decode fine; failing here
745        // is how a value Binance adds gets modelled instead of passing silently.
746        let unnamed: Vec<_> = info
747            .symbols
748            .iter()
749            .filter(|s| matches!(s.status, SymbolStatus::Other(_)))
750            .map(|s| (&s.symbol, &s.status))
751            .collect();
752        assert!(
753            unnamed.is_empty(),
754            "statuses the enum does not name: {unnamed:?}"
755        );
756        // The same for filters: a misspelled tag would turn a PRICE_FILTER into
757        // `Other`, and every other test would still pass.
758        let unnamed: Vec<&Filter> = info
759            .symbols
760            .iter()
761            .flat_map(|s| &s.filters)
762            .chain(&info.exchange_filters)
763            .filter(|f| matches!(f, Filter::Other { .. }))
764            .collect();
765        assert!(
766            unnamed.is_empty(),
767            "filter types Filter does not name, add them to `filters!`: {unnamed:?}"
768        );
769    }
770
771    #[test]
772    fn the_captured_btcusdt_filters_decode_to_their_values() {
773        let text = text("exchange_info");
774        let raw: Value = serde_json::from_str(&text).unwrap();
775        let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
776        let btc = info.symbols.iter().find(|s| s.symbol == "BTCUSDT").unwrap();
777        let wire = raw["symbols"]
778            .as_array()
779            .unwrap()
780            .iter()
781            .find(|s| s["symbol"] == "BTCUSDT")
782            .unwrap();
783        let sent = |filter_type: &str, key: &str| -> String {
784            wire["filters"]
785                .as_array()
786                .unwrap()
787                .iter()
788                .find(|f| f["filterType"] == filter_type)
789                .unwrap_or_else(|| panic!("no {filter_type}"))[key]
790                .as_str()
791                .unwrap()
792                .to_owned()
793        };
794        let price = btc
795            .filters
796            .iter()
797            .find_map(|f| match f {
798                Filter::PriceFilter { tick_size, .. } => Some(*tick_size),
799                _ => None,
800            })
801            .expect("a PRICE_FILTER");
802        assert_eq!(price.to_string(), sent("PRICE_FILTER", "tickSize"));
803        let lot = btc
804            .filters
805            .iter()
806            .find_map(|f| match f {
807                Filter::LotSize { step_size, .. } => Some(*step_size),
808                _ => None,
809            })
810            .expect("a LOT_SIZE");
811        assert_eq!(lot.to_string(), sent("LOT_SIZE", "stepSize"));
812        let market = btc
813            .filters
814            .iter()
815            .find_map(|f| match f {
816                Filter::MarketLotSize { step_size, .. } => Some(*step_size),
817                _ => None,
818            })
819            .expect("a MARKET_LOT_SIZE");
820        assert_eq!(market.to_string(), sent("MARKET_LOT_SIZE", "stepSize"));
821        let notional = btc
822            .filters
823            .iter()
824            .find_map(|f| match f {
825                Filter::Notional { min_notional, .. } => Some(*min_notional),
826                _ => None,
827            })
828            .expect("a NOTIONAL");
829        assert_eq!(notional.to_string(), sent("NOTIONAL", "minNotional"));
830    }
831
832    #[test]
833    fn the_time_capture_agrees_with_its_type() {
834        let text = text("time");
835        let diff = agreement::compare::<ServerTime>("time", &text);
836        assert!(diff.unmodelled.is_empty(), "{:?}", diff.unmodelled);
837        assert!(diff.invented.is_empty(), "{:?}", diff.invented);
838        let time: ServerTime = serde_json::from_str(&text).unwrap();
839        assert!(time.server_time > 1_790_000_000_000);
840    }
841
842    #[test]
843    fn an_unknown_filter_type_is_kept_whole() {
844        let raw = serde_json::json!({"filterType": "A_FILTER_FROM_THE_FUTURE", "limit": 7});
845        let filter: Filter = serde_json::from_value(raw.clone()).unwrap();
846        assert!(matches!(
847            &filter,
848            Filter::Other { filter_type, raw: kept }
849                if filter_type == "A_FILTER_FROM_THE_FUTURE" && *kept == raw
850        ));
851        assert_eq!(serde_json::to_value(&filter).unwrap(), raw);
852    }
853
854    #[test]
855    fn a_known_filter_that_loses_a_field_fails() {
856        let raw = serde_json::json!({"filterType": "PRICE_FILTER", "minPrice": "0.01000000"});
857        assert!(serde_json::from_value::<Filter>(raw).is_err());
858    }
859
860    #[test]
861    fn a_filter_without_a_type_fails() {
862        let raw = serde_json::json!({"minPrice": "0.01000000"});
863        let err = serde_json::from_value::<Filter>(raw).unwrap_err();
864        assert!(err.to_string().contains("filterType"), "{err}");
865    }
866
867    #[test]
868    fn a_status_keeps_an_unknown_spelling() {
869        assert_eq!(SymbolStatus::from_wire("TRADING"), SymbolStatus::Trading);
870        assert_eq!(
871            SymbolStatus::from_wire("PRE_TRADING"),
872            SymbolStatus::PreTrading
873        );
874        let unknown = SymbolStatus::from_wire("A_NEW_STATE");
875        assert_eq!(unknown, SymbolStatus::Other("A_NEW_STATE".into()));
876        assert_eq!(unknown.as_str(), "A_NEW_STATE");
877    }
878
879    #[test]
880    fn the_status_spellings_are_binance_s() {
881        let table = [
882            (SymbolStatus::Trading, "TRADING"),
883            (SymbolStatus::Break, "BREAK"),
884            (SymbolStatus::Halt, "HALT"),
885            (SymbolStatus::PreTrading, "PRE_TRADING"),
886            (SymbolStatus::PostTrading, "POST_TRADING"),
887            (SymbolStatus::EndOfDay, "END_OF_DAY"),
888            (SymbolStatus::AuctionMatch, "AUCTION_MATCH"),
889        ];
890        for (status, wire) in table {
891            assert_eq!(status.as_str(), wire);
892        }
893    }
894}