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