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