Skip to main content

polyoxide_binance/usdm/
types.rs

1//! Vocabulary and response rows for USDⓈ-M futures.
2//!
3//! Prices, quantities and rates are [`Decimal`],
4//! decoded from the decimal strings Binance sends, so no value passes through
5//! an `f64`. Timestamps are Unix milliseconds. Where the wire uses one-letter
6//! keys (`aggTrades`, `depth`), fields take spelled-out names through serde
7//! renames.
8
9use std::{fmt, str::FromStr};
10
11use rust_decimal::Decimal;
12use serde::{
13    de::{self, DeserializeOwned, IgnoredAny, SeqAccess, Visitor},
14    ser::SerializeSeq,
15    Deserialize, Deserializer, Serialize, Serializer,
16};
17use serde_json::Value;
18
19/// A Binance symbol such as `BTCUSDT`, `BTCUSDT_261225` or `币安人生USDT`.
20///
21/// [`Symbol::new`] accepts 1 to 32 characters, each a Unicode letter, a digit
22/// or `_`. Binance lists Chinese-character symbols, and quarterly contracts
23/// carry their delivery date after an underscore; on 2026-10-07 the 924 listed
24/// symbols used no other character and the longest had 17. ASCII letters are
25/// uppercased: no listed symbol has a lowercase one, stream names spell every
26/// symbol in lowercase, and uppercasing is what lets a stream name be parsed
27/// back to the symbol that built it.
28///
29/// Response rows carry symbols as `String`, taken as sent, so a symbol this
30/// type would refuse never fails a whole response.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct Symbol(String);
33
34/// A string [`Symbol::new`] refused.
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36#[error("{0:?} is not a Binance symbol: 1 to 32 letters, digits or underscores")]
37pub struct InvalidSymbol(pub String);
38
39impl Symbol {
40    /// The longest symbol [`Symbol::new`] accepts, in characters.
41    pub const MAX_LEN: usize = 32;
42
43    /// Checks a symbol and uppercases its ASCII letters.
44    pub fn new(symbol: impl Into<String>) -> Result<Self, InvalidSymbol> {
45        let symbol = symbol.into();
46        let chars = symbol.chars().count();
47        let valid = (1..=Self::MAX_LEN).contains(&chars)
48            && symbol.chars().all(|c| c.is_alphanumeric() || c == '_');
49        if valid {
50            Ok(Self(symbol.to_ascii_uppercase()))
51        } else {
52            Err(InvalidSymbol(symbol))
53        }
54    }
55
56    /// The symbol, as REST spells it.
57    pub fn as_str(&self) -> &str {
58        &self.0
59    }
60}
61
62impl fmt::Display for Symbol {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.write_str(&self.0)
65    }
66}
67
68impl FromStr for Symbol {
69    type Err = InvalidSymbol;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        Self::new(s)
73    }
74}
75
76impl AsRef<str> for Symbol {
77    fn as_ref(&self) -> &str {
78        &self.0
79    }
80}
81
82/// A string that is not one of a closed set's wire spellings.
83#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
84#[error("{value:?} is not a valid {type_name}")]
85pub struct UnknownVariant {
86    /// The Rust type being parsed.
87    pub type_name: &'static str,
88    /// The offending input.
89    pub value: String,
90}
91
92/// A closed set the client sends: one wire spelling per variant, and an `ALL`
93/// table so a test can walk every spelling.
94macro_rules! wire_enum {
95    ($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $wire:literal),+ $(,)? }) => {
96        $(#[$meta])*
97        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98        pub enum $name {
99            $( $(#[$vmeta])* #[serde(rename = $wire)] $variant, )+
100        }
101
102        impl $name {
103            /// Every variant, in declaration order.
104            pub const ALL: &'static [$name] = &[$( $name::$variant, )+];
105
106            /// The wire spelling.
107            pub fn as_str(self) -> &'static str {
108                match self { $( $name::$variant => $wire, )+ }
109            }
110        }
111
112        impl fmt::Display for $name {
113            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114                f.write_str(self.as_str())
115            }
116        }
117
118        impl FromStr for $name {
119            type Err = UnknownVariant;
120            fn from_str(s: &str) -> Result<Self, Self::Err> {
121                match s {
122                    $( $wire => Ok($name::$variant), )+
123                    _ => Err(UnknownVariant { type_name: stringify!($name), value: s.to_owned() }),
124                }
125            }
126        }
127    };
128}
129
130/// A set Binance reports and extends over time: a value this version does not
131/// know is kept verbatim in `Other` instead of failing the response.
132macro_rules! open_enum {
133    ($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $wire:literal),+ $(,)? }) => {
134        $(#[$meta])*
135        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
136        #[non_exhaustive]
137        pub enum $name {
138            $( $(#[$vmeta])* $variant, )+
139            /// A value this version of the SDK does not recognise, kept verbatim.
140            Other(String),
141        }
142
143        impl $name {
144            /// Every variant this SDK knows, in declaration order.
145            pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
146
147            /// The wire spelling.
148            pub fn as_str(&self) -> &str {
149                match self {
150                    $( Self::$variant => $wire, )+
151                    Self::Other(raw) => raw,
152                }
153            }
154        }
155
156        impl fmt::Display for $name {
157            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158                f.write_str(self.as_str())
159            }
160        }
161
162        impl FromStr for $name {
163            type Err = std::convert::Infallible;
164            fn from_str(s: &str) -> Result<Self, Self::Err> {
165                Ok(match s {
166                    $( $wire => Self::$variant, )+
167                    other => Self::Other(other.to_owned()),
168                })
169            }
170        }
171
172        impl Serialize for $name {
173            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
174                serializer.serialize_str(self.as_str())
175            }
176        }
177
178        impl<'de> Deserialize<'de> for $name {
179            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
180                let raw = String::deserialize(deserializer)?;
181                let Ok(value) = raw.parse();
182                Ok(value)
183            }
184        }
185    };
186}
187
188wire_enum! {
189    /// Kline width, shared by REST `klines` and the kline stream. The docs'
190    /// list also has `1s`, which this host refuses (`-1120`).
191    Interval {
192        M1 => "1m", M3 => "3m", M5 => "5m", M15 => "15m", M30 => "30m",
193        H1 => "1h", H2 => "2h", H4 => "4h", H6 => "6h", H8 => "8h", H12 => "12h",
194        D1 => "1d", D3 => "3d", W1 => "1w",
195        /// One calendar month.
196        Mo1 => "1M",
197    }
198}
199
200wire_enum! {
201    /// Levels per side `depth` can return. Any other `limit` is refused
202    /// (`-4021`).
203    DepthLimit {
204        Five => "5", Ten => "10", Twenty => "20", Fifty => "50",
205        Hundred => "100", FiveHundred => "500", Thousand => "1000",
206    }
207}
208
209open_enum! {
210    /// A contract's type, from `exchangeInfo`.
211    ContractType {
212        Perpetual => "PERPETUAL",
213        /// A perpetual on a traditional-finance underlying (equities, metals, FX).
214        TradifiPerpetual => "TRADIFI_PERPETUAL",
215        CurrentMonth => "CURRENT_MONTH",
216        NextMonth => "NEXT_MONTH",
217        CurrentQuarter => "CURRENT_QUARTER",
218        NextQuarter => "NEXT_QUARTER",
219        PerpetualDelivering => "PERPETUAL_DELIVERING",
220    }
221}
222
223open_enum! {
224    /// A contract's status, from `exchangeInfo`. The variants are the list in
225    /// Binance's common definitions; `exchangeInfo` used three of them on
226    /// 2026-10-07.
227    SymbolStatus {
228        PendingTrading => "PENDING_TRADING",
229        Trading => "TRADING",
230        PreDelivering => "PRE_DELIVERING",
231        Delivering => "DELIVERING",
232        Delivered => "DELIVERED",
233        PreSettle => "PRE_SETTLE",
234        /// A delisted perpetual. 134 of 924 symbols on 2026-10-07.
235        Settling => "SETTLING",
236        Close => "CLOSE",
237        TradingHalt => "TRADING_HALT",
238        TradingCancelOnly => "TRADING_CANCEL_ONLY",
239    }
240}
241
242open_enum! {
243    /// What a contract's underlying is, from `exchangeInfo`. The values seen on
244    /// 2026-10-07; Binance's docs give no list.
245    UnderlyingType {
246        Coin => "COIN",
247        Index => "INDEX",
248        Premarket => "PREMARKET",
249        Commodity => "COMMODITY",
250        Equity => "EQUITY",
251        CnEquity => "CN_EQUITY",
252        HkEquity => "HK_EQUITY",
253        KrEquity => "KR_EQUITY",
254        Fx => "FX",
255    }
256}
257
258/// `serde(with)` for a decimal string that is empty when the value is unknown.
259///
260/// `fundingRate` sends `"markPrice": ""` for funding events before about 2022,
261/// so a plain `Decimal` would fail a backfill on its first old page.
262mod decimal_or_empty {
263    use super::*;
264
265    pub fn serialize<S: Serializer>(
266        value: &Option<Decimal>,
267        serializer: S,
268    ) -> Result<S::Ok, S::Error> {
269        match value {
270            Some(value) => serializer.collect_str(value),
271            None => serializer.serialize_str(""),
272        }
273    }
274
275    pub fn deserialize<'de, D: Deserializer<'de>>(
276        deserializer: D,
277    ) -> Result<Option<Decimal>, D::Error> {
278        let raw = String::deserialize(deserializer)?;
279        if raw.is_empty() {
280            return Ok(None);
281        }
282        raw.parse().map(Some).map_err(de::Error::custom)
283    }
284}
285
286/// `GET /fapi/v1/time`.
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289#[non_exhaustive]
290pub struct ServerTime {
291    /// Server clock, Unix milliseconds.
292    pub server_time: u64,
293}
294
295/// `GET /fapi/v1/exchangeInfo`.
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298#[non_exhaustive]
299pub struct ExchangeInfo {
300    /// Always `UTC`.
301    pub timezone: String,
302    /// Server clock, Unix milliseconds.
303    pub server_time: u64,
304    /// `U_MARGINED` on this host.
305    pub futures_type: String,
306    /// The IP's request-weight and order limits.
307    pub rate_limits: Vec<RateLimit>,
308    /// Exchange-wide filters; empty on 2026-10-07.
309    pub exchange_filters: Vec<Filter>,
310    /// Margin assets.
311    pub assets: Vec<AssetInfo>,
312    /// Every listed contract, including delisted (`SETTLING`) ones.
313    pub symbols: Vec<SymbolInfo>,
314}
315
316/// One row of `exchangeInfo`'s `rateLimits`.
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319#[non_exhaustive]
320pub struct RateLimit {
321    /// `REQUEST_WEIGHT` or `ORDERS`.
322    pub rate_limit_type: String,
323    /// `MINUTE` or `SECOND`.
324    pub interval: String,
325    /// Intervals per window.
326    pub interval_num: u32,
327    /// The limit per window.
328    pub limit: u32,
329}
330
331/// One margin asset in `exchangeInfo`.
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334#[non_exhaustive]
335pub struct AssetInfo {
336    /// Asset name, such as `USDT`.
337    pub asset: String,
338    /// Whether the asset can be margin.
339    pub margin_available: bool,
340    /// Binance's auto-exchange threshold.
341    #[serde(with = "rust_decimal::serde::str")]
342    pub auto_asset_exchange: Decimal,
343}
344
345/// One contract in `exchangeInfo`.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct SymbolInfo {
350    /// The contract's symbol.
351    pub symbol: String,
352    /// The underlying pair, `BTCUSDT` for `BTCUSDT_261225`.
353    pub pair: String,
354    /// Perpetual, TradFi perpetual or a delivery contract.
355    pub contract_type: ContractType,
356    /// Delivery time; far in the future for a perpetual.
357    pub delivery_date: u64,
358    /// Listing time.
359    pub onboard_date: u64,
360    /// Trading, settling (delisted) and so on.
361    pub status: SymbolStatus,
362    /// Ignore; Binance documents it so.
363    #[serde(with = "rust_decimal::serde::str")]
364    pub maint_margin_percent: Decimal,
365    /// Ignore; Binance documents it so.
366    #[serde(with = "rust_decimal::serde::str")]
367    pub required_margin_percent: Decimal,
368    /// Base asset.
369    pub base_asset: String,
370    /// Quote asset.
371    pub quote_asset: String,
372    /// Margin asset.
373    pub margin_asset: String,
374    /// Decimal places in a price. Use `PRICE_FILTER`'s tick size to round.
375    pub price_precision: u32,
376    /// Decimal places in a quantity. Use `LOT_SIZE`'s step size to round.
377    pub quantity_precision: u32,
378    /// Decimal places of the base asset.
379    pub base_asset_precision: u32,
380    /// Decimal places of the quote asset.
381    pub quote_precision: u32,
382    /// What the underlying is.
383    pub underlying_type: UnderlyingType,
384    /// Free-text tags such as `Layer-2` or `TradFi`.
385    pub underlying_sub_type: Vec<String>,
386    /// Threshold for algo orders with `priceProtect`.
387    #[serde(with = "rust_decimal::serde::str")]
388    pub trigger_protect: Decimal,
389    /// Liquidation fee rate.
390    #[serde(with = "rust_decimal::serde::str")]
391    pub liquidation_fee: Decimal,
392    /// The most a market order may deviate from the mark price.
393    #[serde(with = "rust_decimal::serde::str")]
394    pub market_take_bound: Decimal,
395    /// Most orders a move-order request may touch.
396    pub max_move_order_limit: u32,
397    /// Order filters.
398    pub filters: Vec<Filter>,
399    /// Order types the contract accepts.
400    pub order_types: Vec<String>,
401    /// Times in force the contract accepts.
402    pub time_in_force: Vec<String>,
403    /// Trading products the contract is open to (`GRID`, `COPY`, …).
404    pub permission_sets: Vec<String>,
405}
406
407/// An `exchangeInfo` filter, named after its wire `filterType`.
408///
409/// A filter type this version does not know is kept whole in [`Filter::Other`],
410/// so a new one never fails the response. A known type that loses or retypes a
411/// field still fails; one that gains a field decodes without it, and the
412/// wire-agreement tests are what notice.
413#[derive(Debug, Clone, PartialEq)]
414#[non_exhaustive]
415pub enum Filter {
416    /// `PRICE_FILTER`.
417    #[non_exhaustive]
418    PriceFilter {
419        /// Lowest price.
420        min_price: Decimal,
421        /// Highest price.
422        max_price: Decimal,
423        /// Price increment.
424        tick_size: Decimal,
425    },
426    /// `LOT_SIZE`: limit orders.
427    #[non_exhaustive]
428    LotSize {
429        /// Smallest quantity.
430        min_qty: Decimal,
431        /// Largest quantity.
432        max_qty: Decimal,
433        /// Quantity increment.
434        step_size: Decimal,
435    },
436    /// `MARKET_LOT_SIZE`: market orders.
437    #[non_exhaustive]
438    MarketLotSize {
439        /// Smallest quantity.
440        min_qty: Decimal,
441        /// Largest quantity.
442        max_qty: Decimal,
443        /// Quantity increment.
444        step_size: Decimal,
445    },
446    /// `MAX_NUM_ORDERS`.
447    #[non_exhaustive]
448    MaxNumOrders {
449        /// Most open orders.
450        limit: u32,
451    },
452    /// `MIN_NOTIONAL`.
453    #[non_exhaustive]
454    MinNotional {
455        /// Smallest order value.
456        notional: Decimal,
457    },
458    /// `PERCENT_PRICE`.
459    #[non_exhaustive]
460    PercentPrice {
461        /// Highest price as a multiple of the mark price.
462        multiplier_up: Decimal,
463        /// Lowest price as a multiple of the mark price.
464        multiplier_down: Decimal,
465        /// Decimal places of the multipliers.
466        multiplier_decimal: Decimal,
467    },
468    /// `POSITION_RISK_CONTROL`.
469    #[non_exhaustive]
470    PositionRiskControl {
471        /// Which side's position is controlled, such as `NONE`.
472        position_control_side: String,
473    },
474    /// A filter type this version does not model.
475    Other {
476        /// The wire `filterType`.
477        filter_type: String,
478        /// The whole filter object, `filterType` included.
479        raw: Value,
480    },
481}
482
483/// The wire bodies of the known filters, which `Filter`'s serde goes through.
484mod filter_wire {
485    use super::*;
486
487    #[derive(Serialize, Deserialize)]
488    #[serde(rename_all = "camelCase")]
489    pub struct Price {
490        #[serde(with = "rust_decimal::serde::str")]
491        pub min_price: Decimal,
492        #[serde(with = "rust_decimal::serde::str")]
493        pub max_price: Decimal,
494        #[serde(with = "rust_decimal::serde::str")]
495        pub tick_size: Decimal,
496    }
497
498    #[derive(Serialize, Deserialize)]
499    #[serde(rename_all = "camelCase")]
500    pub struct Lot {
501        #[serde(with = "rust_decimal::serde::str")]
502        pub min_qty: Decimal,
503        #[serde(with = "rust_decimal::serde::str")]
504        pub max_qty: Decimal,
505        #[serde(with = "rust_decimal::serde::str")]
506        pub step_size: Decimal,
507    }
508
509    #[derive(Serialize, Deserialize)]
510    pub struct MaxNumOrders {
511        pub limit: u32,
512    }
513
514    #[derive(Serialize, Deserialize)]
515    pub struct MinNotional {
516        #[serde(with = "rust_decimal::serde::str")]
517        pub notional: Decimal,
518    }
519
520    #[derive(Serialize, Deserialize)]
521    #[serde(rename_all = "camelCase")]
522    pub struct PercentPrice {
523        #[serde(with = "rust_decimal::serde::str")]
524        pub multiplier_up: Decimal,
525        #[serde(with = "rust_decimal::serde::str")]
526        pub multiplier_down: Decimal,
527        #[serde(with = "rust_decimal::serde::str")]
528        pub multiplier_decimal: Decimal,
529    }
530
531    #[derive(Serialize, Deserialize)]
532    #[serde(rename_all = "camelCase")]
533    pub struct PositionRiskControl {
534        pub position_control_side: String,
535    }
536}
537
538impl<'de> Deserialize<'de> for Filter {
539    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
540        use filter_wire as wire;
541
542        fn parse<T: DeserializeOwned, E: de::Error>(raw: Value) -> Result<T, E> {
543            serde_json::from_value(raw).map_err(E::custom)
544        }
545
546        let raw = Value::deserialize(deserializer)?;
547        let filter_type = raw
548            .get("filterType")
549            .and_then(Value::as_str)
550            .ok_or_else(|| de::Error::custom("a filter without a string filterType"))?
551            .to_owned();
552        Ok(match filter_type.as_str() {
553            "PRICE_FILTER" => {
554                let f: wire::Price = parse(raw)?;
555                Self::PriceFilter {
556                    min_price: f.min_price,
557                    max_price: f.max_price,
558                    tick_size: f.tick_size,
559                }
560            }
561            "LOT_SIZE" => {
562                let f: wire::Lot = parse(raw)?;
563                Self::LotSize {
564                    min_qty: f.min_qty,
565                    max_qty: f.max_qty,
566                    step_size: f.step_size,
567                }
568            }
569            "MARKET_LOT_SIZE" => {
570                let f: wire::Lot = parse(raw)?;
571                Self::MarketLotSize {
572                    min_qty: f.min_qty,
573                    max_qty: f.max_qty,
574                    step_size: f.step_size,
575                }
576            }
577            "MAX_NUM_ORDERS" => {
578                let f: wire::MaxNumOrders = parse(raw)?;
579                Self::MaxNumOrders { limit: f.limit }
580            }
581            "MIN_NOTIONAL" => {
582                let f: wire::MinNotional = parse(raw)?;
583                Self::MinNotional {
584                    notional: f.notional,
585                }
586            }
587            "PERCENT_PRICE" => {
588                let f: wire::PercentPrice = parse(raw)?;
589                Self::PercentPrice {
590                    multiplier_up: f.multiplier_up,
591                    multiplier_down: f.multiplier_down,
592                    multiplier_decimal: f.multiplier_decimal,
593                }
594            }
595            "POSITION_RISK_CONTROL" => {
596                let f: wire::PositionRiskControl = parse(raw)?;
597                Self::PositionRiskControl {
598                    position_control_side: f.position_control_side,
599                }
600            }
601            _ => Self::Other { filter_type, raw },
602        })
603    }
604}
605
606impl Serialize for Filter {
607    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
608        use filter_wire as wire;
609
610        fn tagged<T: Serialize, E: serde::ser::Error>(
611            filter_type: &str,
612            body: &T,
613        ) -> Result<Value, E> {
614            let mut value = serde_json::to_value(body).map_err(E::custom)?;
615            if let Value::Object(map) = &mut value {
616                map.insert(
617                    "filterType".to_owned(),
618                    Value::String(filter_type.to_owned()),
619                );
620            }
621            Ok(value)
622        }
623
624        let value = match self {
625            Self::PriceFilter {
626                min_price,
627                max_price,
628                tick_size,
629            } => tagged(
630                "PRICE_FILTER",
631                &wire::Price {
632                    min_price: *min_price,
633                    max_price: *max_price,
634                    tick_size: *tick_size,
635                },
636            )?,
637            Self::LotSize {
638                min_qty,
639                max_qty,
640                step_size,
641            } => tagged(
642                "LOT_SIZE",
643                &wire::Lot {
644                    min_qty: *min_qty,
645                    max_qty: *max_qty,
646                    step_size: *step_size,
647                },
648            )?,
649            Self::MarketLotSize {
650                min_qty,
651                max_qty,
652                step_size,
653            } => tagged(
654                "MARKET_LOT_SIZE",
655                &wire::Lot {
656                    min_qty: *min_qty,
657                    max_qty: *max_qty,
658                    step_size: *step_size,
659                },
660            )?,
661            Self::MaxNumOrders { limit } => {
662                tagged("MAX_NUM_ORDERS", &wire::MaxNumOrders { limit: *limit })?
663            }
664            Self::MinNotional { notional } => tagged(
665                "MIN_NOTIONAL",
666                &wire::MinNotional {
667                    notional: *notional,
668                },
669            )?,
670            Self::PercentPrice {
671                multiplier_up,
672                multiplier_down,
673                multiplier_decimal,
674            } => tagged(
675                "PERCENT_PRICE",
676                &wire::PercentPrice {
677                    multiplier_up: *multiplier_up,
678                    multiplier_down: *multiplier_down,
679                    multiplier_decimal: *multiplier_decimal,
680                },
681            )?,
682            Self::PositionRiskControl {
683                position_control_side,
684            } => tagged(
685                "POSITION_RISK_CONTROL",
686                &wire::PositionRiskControl {
687                    position_control_side: position_control_side.clone(),
688                },
689            )?,
690            Self::Other { raw, .. } => raw.clone(),
691        };
692        value.serialize(serializer)
693    }
694}
695
696/// One row of `GET /fapi/v1/fundingInfo`: the symbols whose funding cap,
697/// floor or interval was adjusted.
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700#[non_exhaustive]
701pub struct FundingInfo {
702    /// The contract. This host's list also carries COIN-M perpetuals
703    /// (`BTCUSD_PERP`), which `exchangeInfo` here does not.
704    pub symbol: String,
705    /// Highest funding rate.
706    #[serde(with = "rust_decimal::serde::str")]
707    pub adjusted_funding_rate_cap: Decimal,
708    /// Lowest funding rate.
709    #[serde(with = "rust_decimal::serde::str")]
710    pub adjusted_funding_rate_floor: Decimal,
711    /// Hours between funding events.
712    pub funding_interval_hours: u32,
713    /// Whether Binance shows a disclaimer for the contract.
714    pub disclaimer: bool,
715    /// When the adjustment was made. `null` on 57 of 805 rows on 2026-10-07,
716    /// `BTCUSDT` among them.
717    pub update_time: Option<u64>,
718}
719
720/// `GET /fapi/v1/ticker/24hr`: rolling 24-hour statistics.
721#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase")]
723#[non_exhaustive]
724pub struct Ticker24h {
725    /// The contract.
726    pub symbol: String,
727    /// Last price less the open price.
728    #[serde(with = "rust_decimal::serde::str")]
729    pub price_change: Decimal,
730    /// The change as a percentage.
731    #[serde(with = "rust_decimal::serde::str")]
732    pub price_change_percent: Decimal,
733    /// Volume-weighted average price.
734    #[serde(with = "rust_decimal::serde::str")]
735    pub weighted_avg_price: Decimal,
736    /// Last traded price.
737    #[serde(with = "rust_decimal::serde::str")]
738    pub last_price: Decimal,
739    /// Last traded quantity.
740    #[serde(with = "rust_decimal::serde::str")]
741    pub last_qty: Decimal,
742    /// Price 24 hours ago.
743    #[serde(with = "rust_decimal::serde::str")]
744    pub open_price: Decimal,
745    /// Highest price.
746    #[serde(with = "rust_decimal::serde::str")]
747    pub high_price: Decimal,
748    /// Lowest price.
749    #[serde(with = "rust_decimal::serde::str")]
750    pub low_price: Decimal,
751    /// Base-asset volume.
752    #[serde(with = "rust_decimal::serde::str")]
753    pub volume: Decimal,
754    /// Quote-asset volume.
755    #[serde(with = "rust_decimal::serde::str")]
756    pub quote_volume: Decimal,
757    /// Window start.
758    pub open_time: u64,
759    /// Window end.
760    pub close_time: u64,
761    /// First trade id in the window.
762    pub first_id: i64,
763    /// Last trade id in the window.
764    pub last_id: i64,
765    /// Trades in the window.
766    pub count: u64,
767}
768
769/// `GET /fapi/v1/premiumIndex`: mark price, index price and funding.
770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772#[non_exhaustive]
773pub struct PremiumIndex {
774    /// The contract.
775    pub symbol: String,
776    /// Mark price.
777    #[serde(with = "rust_decimal::serde::str")]
778    pub mark_price: Decimal,
779    /// Index price.
780    #[serde(with = "rust_decimal::serde::str")]
781    pub index_price: Decimal,
782    /// Estimated settle price, meaningful only in the hour before a settlement.
783    #[serde(with = "rust_decimal::serde::str")]
784    pub estimated_settle_price: Decimal,
785    /// The funding rate of the current period.
786    #[serde(with = "rust_decimal::serde::str")]
787    pub last_funding_rate: Decimal,
788    /// Interest rate.
789    #[serde(with = "rust_decimal::serde::str")]
790    pub interest_rate: Decimal,
791    /// Next funding event, or `0` for a contract with none scheduled.
792    pub next_funding_time: u64,
793    /// When the values were computed.
794    pub time: u64,
795}
796
797/// One candle of `GET /fapi/v1/klines`, a positional array on the wire.
798///
799/// The wire sends a twelfth element Binance documents as "ignore"; it is
800/// dropped, and any further elements are tolerated.
801#[derive(Debug, Clone, PartialEq, Eq)]
802#[non_exhaustive]
803pub struct Kline {
804    /// Candle start.
805    pub open_time: u64,
806    /// Open price.
807    pub open: Decimal,
808    /// High price.
809    pub high: Decimal,
810    /// Low price.
811    pub low: Decimal,
812    /// Close price.
813    pub close: Decimal,
814    /// Base-asset volume.
815    pub volume: Decimal,
816    /// Candle end, inclusive.
817    pub close_time: u64,
818    /// Quote-asset volume.
819    pub quote_volume: Decimal,
820    /// Trades in the candle.
821    pub trade_count: u64,
822    /// Base-asset volume bought by takers.
823    pub taker_buy_base_volume: Decimal,
824    /// Quote-asset volume bought by takers.
825    pub taker_buy_quote_volume: Decimal,
826}
827
828/// A decimal string inside a positional array.
829struct DecimalStr(Decimal);
830
831impl<'de> Deserialize<'de> for DecimalStr {
832    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
833        rust_decimal::serde::str::deserialize(deserializer).map(Self)
834    }
835}
836
837fn next<'de, T: Deserialize<'de>, A: SeqAccess<'de>>(
838    seq: &mut A,
839    index: usize,
840    what: &str,
841) -> Result<T, A::Error> {
842    seq.next_element()?
843        .ok_or_else(|| de::Error::invalid_length(index, &what))
844}
845
846fn drain<'de, A: SeqAccess<'de>>(seq: &mut A) -> Result<(), A::Error> {
847    while seq.next_element::<IgnoredAny>()?.is_some() {}
848    Ok(())
849}
850
851impl<'de> Deserialize<'de> for Kline {
852    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
853        struct KlineVisitor;
854
855        impl<'de> Visitor<'de> for KlineVisitor {
856            type Value = Kline;
857
858            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
859                f.write_str("a kline array of at least 11 elements")
860            }
861
862            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
863                const WHAT: &str = "a kline array of at least 11 elements";
864                let kline = Kline {
865                    open_time: next(&mut seq, 0, WHAT)?,
866                    open: next::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
867                    high: next::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
868                    low: next::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
869                    close: next::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
870                    volume: next::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
871                    close_time: next(&mut seq, 6, WHAT)?,
872                    quote_volume: next::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
873                    trade_count: next(&mut seq, 8, WHAT)?,
874                    taker_buy_base_volume: next::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
875                    taker_buy_quote_volume: next::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
876                };
877                drain(&mut seq)?;
878                Ok(kline)
879            }
880        }
881
882        deserializer.deserialize_seq(KlineVisitor)
883    }
884}
885
886impl Serialize for Kline {
887    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
888        let mut seq = serializer.serialize_seq(Some(11))?;
889        seq.serialize_element(&self.open_time)?;
890        for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
891            seq.serialize_element(&value.to_string())?;
892        }
893        seq.serialize_element(&self.close_time)?;
894        seq.serialize_element(&self.quote_volume.to_string())?;
895        seq.serialize_element(&self.trade_count)?;
896        seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
897        seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
898        seq.end()
899    }
900}
901
902/// One row of `GET /fapi/v1/fundingRate`.
903#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
904#[serde(rename_all = "camelCase")]
905#[non_exhaustive]
906pub struct FundingRate {
907    /// The contract.
908    pub symbol: String,
909    /// When funding was paid.
910    pub funding_time: u64,
911    /// The rate paid.
912    #[serde(with = "rust_decimal::serde::str")]
913    pub funding_rate: Decimal,
914    /// The mark price at funding. `None` for events before about 2022, which
915    /// the wire sends as `""`.
916    #[serde(with = "decimal_or_empty")]
917    pub mark_price: Option<Decimal>,
918    /// `Regular` on every row seen on 2026-10-07.
919    pub rate_type: String,
920}
921
922/// `GET /fapi/v1/openInterest`.
923#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
924#[serde(rename_all = "camelCase")]
925#[non_exhaustive]
926pub struct OpenInterest {
927    /// The contract.
928    pub symbol: String,
929    /// Open interest in contracts.
930    #[serde(with = "rust_decimal::serde::str")]
931    pub open_interest: Decimal,
932    /// When it was computed.
933    pub time: u64,
934}
935
936/// One row of `GET /fapi/v1/aggTrades`: trades at one price, taken by one
937/// order, merged.
938#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
939#[non_exhaustive]
940pub struct AggTrade {
941    /// Aggregate trade id.
942    #[serde(rename = "a")]
943    pub id: u64,
944    /// Price.
945    #[serde(rename = "p", with = "rust_decimal::serde::str")]
946    pub price: Decimal,
947    /// Quantity.
948    #[serde(rename = "q", with = "rust_decimal::serde::str")]
949    pub quantity: Decimal,
950    /// Quantity without the trades involving RPI (Retail Price Improvement)
951    /// orders, as Binance documents `nq`.
952    #[serde(rename = "nq", with = "rust_decimal::serde::str")]
953    pub normal_quantity: Decimal,
954    /// First trade id merged.
955    #[serde(rename = "f")]
956    pub first_trade_id: u64,
957    /// Last trade id merged.
958    #[serde(rename = "l")]
959    pub last_trade_id: u64,
960    /// Trade time.
961    #[serde(rename = "T")]
962    pub time: u64,
963    /// Whether the buyer was the maker, so the taker sold.
964    #[serde(rename = "m")]
965    pub is_buyer_maker: bool,
966}
967
968/// `GET /fapi/v1/depth`: an order book snapshot.
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
970#[non_exhaustive]
971pub struct Depth {
972    /// Book update id of the snapshot.
973    #[serde(rename = "lastUpdateId")]
974    pub last_update_id: u64,
975    /// When the message was produced.
976    #[serde(rename = "E")]
977    pub event_time: u64,
978    /// When the book was last changed.
979    #[serde(rename = "T")]
980    pub transaction_time: u64,
981    /// Bids, best first.
982    pub bids: Vec<Level>,
983    /// Asks, best first.
984    pub asks: Vec<Level>,
985}
986
987/// A price level, a `[price, quantity]` pair on the wire.
988#[derive(Debug, Clone, Copy, PartialEq, Eq)]
989#[non_exhaustive]
990pub struct Level {
991    /// Price.
992    pub price: Decimal,
993    /// Quantity at the price.
994    pub quantity: Decimal,
995}
996
997impl Level {
998    /// A level from a price and a quantity.
999    pub fn new(price: Decimal, quantity: Decimal) -> Self {
1000        Self { price, quantity }
1001    }
1002}
1003
1004impl<'de> Deserialize<'de> for Level {
1005    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1006        struct LevelVisitor;
1007
1008        impl<'de> Visitor<'de> for LevelVisitor {
1009            type Value = Level;
1010
1011            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1012                f.write_str("a [price, quantity] array")
1013            }
1014
1015            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
1016                const WHAT: &str = "a [price, quantity] array";
1017                let level = Level {
1018                    price: next::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
1019                    quantity: next::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
1020                };
1021                drain(&mut seq)?;
1022                Ok(level)
1023            }
1024        }
1025
1026        deserializer.deserialize_seq(LevelVisitor)
1027    }
1028}
1029
1030impl Serialize for Level {
1031    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1032        let mut seq = serializer.serialize_seq(Some(2))?;
1033        seq.serialize_element(&self.price.to_string())?;
1034        seq.serialize_element(&self.quantity.to_string())?;
1035        seq.end()
1036    }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn symbols_take_letters_digits_and_underscores_in_any_script() {
1045        for good in ["BTCUSDT", "BTCUSDT_261225", "币安人生USDT", "1000PEPEUSDT"] {
1046            assert_eq!(Symbol::new(good).unwrap().as_str(), good);
1047        }
1048        for bad in [
1049            "",
1050            "BTC USDT",
1051            "BTC@USDT",
1052            "BTC/USDT",
1053            "BTC-USDT",
1054            &"A".repeat(33),
1055        ] {
1056            assert_eq!(
1057                Symbol::new(bad),
1058                Err(InvalidSymbol(bad.to_owned())),
1059                "{bad:?}"
1060            );
1061        }
1062        assert!(Symbol::new("A".repeat(32)).is_ok());
1063    }
1064
1065    #[test]
1066    fn ascii_letters_are_uppercased_and_nothing_else_changes() {
1067        assert_eq!(
1068            Symbol::new("btcusdt").unwrap(),
1069            Symbol::new("BTCUSDT").unwrap()
1070        );
1071        assert_eq!(
1072            Symbol::new("btcusdt_261225").unwrap().as_str(),
1073            "BTCUSDT_261225"
1074        );
1075        assert_eq!(
1076            Symbol::new("币安人生usdt").unwrap().as_str(),
1077            "币安人生USDT"
1078        );
1079    }
1080
1081    #[test]
1082    fn symbols_are_measured_in_characters_and_only_ascii_changes_case() {
1083        // `to_uppercase` would turn ß into SS, and `len` would count bytes.
1084        assert_eq!(Symbol::new("straße").unwrap().as_str(), "STRAßE");
1085        assert!(Symbol::new("币".repeat(32)).is_ok());
1086        assert!(Symbol::new("币".repeat(33)).is_err());
1087    }
1088
1089    #[test]
1090    fn wire_spellings_are_binance_s() {
1091        // `as_str` and `from_str` share each literal, so a round trip cannot
1092        // see a wrong one; these are the spellings the host accepted.
1093        let intervals: Vec<&str> = Interval::ALL.iter().map(|i| i.as_str()).collect();
1094        assert_eq!(
1095            intervals,
1096            [
1097                "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d",
1098                "1w", "1M"
1099            ]
1100        );
1101        let limits: Vec<&str> = DepthLimit::ALL.iter().map(|l| l.as_str()).collect();
1102        assert_eq!(limits, ["5", "10", "20", "50", "100", "500", "1000"]);
1103    }
1104
1105    #[test]
1106    fn every_interval_round_trips_its_wire_spelling() {
1107        assert_eq!(Interval::ALL.len(), 15);
1108        for interval in Interval::ALL {
1109            assert_eq!(interval.as_str().parse::<Interval>(), Ok(*interval));
1110        }
1111        assert!("1s".parse::<Interval>().is_err(), "this host refuses 1s");
1112    }
1113
1114    #[test]
1115    fn an_unknown_contract_type_is_kept_verbatim() {
1116        let parsed: ContractType = serde_json::from_str(r#""NEXT_DECADE""#).unwrap();
1117        assert_eq!(parsed, ContractType::Other("NEXT_DECADE".to_owned()));
1118        assert_eq!(serde_json::to_string(&parsed).unwrap(), r#""NEXT_DECADE""#);
1119        let known: ContractType = serde_json::from_str(r#""TRADIFI_PERPETUAL""#).unwrap();
1120        assert_eq!(known, ContractType::TradifiPerpetual);
1121    }
1122
1123    #[test]
1124    fn an_unseen_filter_type_is_kept_and_the_known_ones_parse() {
1125        let filters: Vec<Filter> = serde_json::from_str(
1126            r#"[{"filterType":"PRICE_FILTER","minPrice":"261.10","maxPrice":"809484","tickSize":"0.10"},
1127                {"filterType":"MAX_NUM_ICEBERG_ORDERS","maxNumIcebergOrders":5}]"#,
1128        )
1129        .unwrap();
1130        assert!(
1131            matches!(&filters[0], Filter::PriceFilter { tick_size, .. } if *tick_size == Decimal::new(10, 2))
1132        );
1133        let Filter::Other { filter_type, raw } = &filters[1] else {
1134            panic!("an unseen filter type must not fail the response");
1135        };
1136        assert_eq!(filter_type, "MAX_NUM_ICEBERG_ORDERS");
1137        assert_eq!(raw["maxNumIcebergOrders"], 5);
1138        let again: Vec<Value> =
1139            serde_json::from_value(serde_json::to_value(&filters).unwrap()).unwrap();
1140        assert_eq!(again[0]["filterType"], "PRICE_FILTER");
1141        assert_eq!(again[1]["maxNumIcebergOrders"], 5);
1142    }
1143
1144    #[test]
1145    fn a_known_filter_with_a_missing_field_fails_loudly() {
1146        let err = serde_json::from_str::<Filter>(r#"{"filterType":"LOT_SIZE","minQty":"0.001"}"#)
1147            .unwrap_err();
1148        assert!(err.to_string().contains("maxQty"), "{err}");
1149    }
1150
1151    #[test]
1152    fn a_kline_drops_the_ignored_element_and_tolerates_more() {
1153        let wire = r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390","0","extra"]"#;
1154        let kline: Kline = serde_json::from_str(wire).unwrap();
1155        assert_eq!(kline.close, "84106.90".parse::<Decimal>().unwrap());
1156        assert_eq!(kline.trade_count, 2912);
1157        assert_eq!(
1158            serde_json::to_string(&kline).unwrap(),
1159            r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390"]"#
1160        );
1161        assert!(serde_json::from_str::<Kline>("[1791354000000]").is_err());
1162    }
1163
1164    #[test]
1165    fn a_level_keeps_every_digit() {
1166        // More significant digits than an f64 holds.
1167        let level: Level = serde_json::from_str(r#"["84141.123456789012345","14.663"]"#).unwrap();
1168        assert_eq!(level.price.to_string(), "84141.123456789012345");
1169        assert_eq!(
1170            serde_json::to_string(&level).unwrap(),
1171            r#"["84141.123456789012345","14.663"]"#
1172        );
1173    }
1174
1175    #[test]
1176    fn an_old_funding_rate_has_no_mark_price() {
1177        // Captured 2026-10-07: `fundingRate?symbol=BTCUSDT&startTime=1568102400000`.
1178        let old: FundingRate = serde_json::from_str(
1179            r#"{"symbol":"BTCUSDT","fundingTime":1568102400000,"fundingRate":"0.00010000","markPrice":"","rateType":"Regular"}"#,
1180        )
1181        .unwrap();
1182        assert_eq!(old.mark_price, None);
1183        assert_eq!(serde_json::to_value(&old).unwrap()["markPrice"], "");
1184
1185        let new: FundingRate = serde_json::from_str(
1186            r#"{"symbol":"BTCUSDT","fundingTime":1791273600001,"fundingRate":"-0.00001592","markPrice":"85514.00007496","rateType":"Regular"}"#,
1187        )
1188        .unwrap();
1189        assert_eq!(new.mark_price, Some("85514.00007496".parse().unwrap()));
1190    }
1191}