asterdex-sdk 0.1.1

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// Tech Lead mini-task (Wave 4): replaced stubs with full type definitions
// per 05_detail_design.md §2.3

use serde::{Deserialize, Serialize};

// --- Ping ---

/// Empty ping response — indicates the server is reachable.
#[derive(Debug, Deserialize, Default)]
pub struct PingResponse {}

// --- Server Time ---

/// Server time response from `/fapi/v3/time`.
#[derive(Debug, Deserialize)]
pub struct ServerTimeResponse {
    #[serde(rename = "serverTime")]
    pub server_time: u64,
}

// --- Exchange Info ---

/// Exchange information including trading symbols and server metadata.
#[derive(Debug, Deserialize)]
pub struct ExchangeInfoResponse {
    pub timezone: String,
    #[serde(rename = "serverTime")]
    pub server_time: u64,
    pub symbols: Vec<SymbolInfo>,
}

/// Basic information about a tradable symbol.
#[derive(Debug, Deserialize)]
pub struct SymbolInfo {
    pub symbol: String,
    pub status: String,
    #[serde(rename = "baseAsset")]
    pub base_asset: String,
    #[serde(rename = "quoteAsset")]
    pub quote_asset: String,
    // Additional fields ignored by serde
}

// --- Depth ---

/// Order book depth snapshot for a symbol.
#[derive(Debug, Deserialize)]
pub struct DepthResponse {
    #[serde(rename = "lastUpdateId")]
    pub last_update_id: u64,
    pub bids: Vec<[String; 2]>,
    pub asks: Vec<[String; 2]>,
}

// --- Klines ---

/// Kline/candlestick interval enum for request parameters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum KlineInterval {
    #[serde(rename = "1m")]
    OneMinute,
    #[serde(rename = "3m")]
    ThreeMinutes,
    #[serde(rename = "5m")]
    FiveMinutes,
    #[serde(rename = "15m")]
    FifteenMinutes,
    #[serde(rename = "30m")]
    ThirtyMinutes,
    #[serde(rename = "1h")]
    OneHour,
    #[serde(rename = "2h")]
    TwoHours,
    #[serde(rename = "4h")]
    FourHours,
    #[serde(rename = "6h")]
    SixHours,
    #[serde(rename = "8h")]
    EightHours,
    #[serde(rename = "12h")]
    TwelveHours,
    #[serde(rename = "1d")]
    OneDay,
    #[serde(rename = "3d")]
    ThreeDays,
    #[serde(rename = "1w")]
    OneWeek,
    #[serde(rename = "1M")]
    OneMonth,
}

impl KlineInterval {
    /// Returns the API string representation of this interval (e.g. `"1m"`, `"1h"`).
    pub fn to_str(self) -> &'static str {
        match self {
            KlineInterval::OneMinute => "1m",
            KlineInterval::ThreeMinutes => "3m",
            KlineInterval::FiveMinutes => "5m",
            KlineInterval::FifteenMinutes => "15m",
            KlineInterval::ThirtyMinutes => "30m",
            KlineInterval::OneHour => "1h",
            KlineInterval::TwoHours => "2h",
            KlineInterval::FourHours => "4h",
            KlineInterval::SixHours => "6h",
            KlineInterval::EightHours => "8h",
            KlineInterval::TwelveHours => "12h",
            KlineInterval::OneDay => "1d",
            KlineInterval::ThreeDays => "3d",
            KlineInterval::OneWeek => "1w",
            KlineInterval::OneMonth => "1M",
        }
    }
}

/// Kline/candlestick data.
///
/// The API returns klines as JSON arrays:
/// `[[open_time, open, high, low, close, volume, close_time, quote_vol, trades, taker_base, taker_quote], ...]`.
///
/// Deserialized directly from the JSON sequence via a `Visitor` — avoids the
/// `Vec<serde_json::Value>` DOM intermediate, cutting allocations roughly
/// 12× per kline and giving a 5–10× speedup on large `get_klines` responses.
#[derive(Debug)]
pub struct Kline {
    pub open_time: u64,
    pub open: String,
    pub high: String,
    pub low: String,
    pub close: String,
    pub volume: String,
    pub close_time: u64,
    pub quote_volume: String,
    pub trade_count: u64,
    pub taker_buy_base_volume: String,
    pub taker_buy_quote_volume: String,
}

impl<'de> Deserialize<'de> for Kline {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{Error as DeError, SeqAccess, Visitor};
        use std::fmt;

        struct KlineVisitor;

        impl<'de> Visitor<'de> for KlineVisitor {
            type Value = Kline;

            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                f.write_str("a Kline array of 11 elements (open_time, open, high, low, close, volume, close_time, quote_volume, trade_count, taker_buy_base, taker_buy_quote)")
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Kline, A::Error>
            where
                A: SeqAccess<'de>,
            {
                #[inline]
                fn next<'de, T, A>(seq: &mut A, idx: usize) -> Result<T, A::Error>
                where
                    A: SeqAccess<'de>,
                    T: serde::Deserialize<'de>,
                {
                    seq.next_element::<T>()?
                        .ok_or_else(|| A::Error::invalid_length(idx, &"Kline array of 11 elements"))
                }

                let open_time: u64 = next(&mut seq, 0)?;
                let open: String = next(&mut seq, 1)?;
                let high: String = next(&mut seq, 2)?;
                let low: String = next(&mut seq, 3)?;
                let close: String = next(&mut seq, 4)?;
                let volume: String = next(&mut seq, 5)?;
                let close_time: u64 = next(&mut seq, 6)?;
                let quote_volume: String = next(&mut seq, 7)?;
                let trade_count: u64 = next(&mut seq, 8)?;
                let taker_buy_base_volume: String = next(&mut seq, 9)?;
                let taker_buy_quote_volume: String = next(&mut seq, 10)?;

                // Drain any trailing elements the server may add in the future —
                // be forward-compatible without failing.
                while seq.next_element::<serde::de::IgnoredAny>()?.is_some() {}

                Ok(Kline {
                    open_time,
                    open,
                    high,
                    low,
                    close,
                    volume,
                    close_time,
                    quote_volume,
                    trade_count,
                    taker_buy_base_volume,
                    taker_buy_quote_volume,
                })
            }
        }

        deserializer.deserialize_seq(KlineVisitor)
    }
}

// --- Aggregate Trades ---

/// Compressed aggregate trade record.
#[derive(Debug, Deserialize)]
pub struct AggTrade {
    #[serde(rename = "a")]
    pub agg_trade_id: u64,
    #[serde(rename = "p")]
    pub price: String,
    #[serde(rename = "q")]
    pub quantity: String,
    #[serde(rename = "f")]
    pub first_trade_id: u64,
    #[serde(rename = "l")]
    pub last_trade_id: u64,
    #[serde(rename = "T")]
    pub trade_time: u64,
    #[serde(rename = "m")]
    pub is_buyer_maker: bool,
}

// --- Mark Price ---

/// Mark price and funding rate for a symbol.
#[derive(Debug, Deserialize)]
pub struct MarkPriceResponse {
    pub symbol: String,
    #[serde(rename = "markPrice")]
    pub mark_price: String,
    #[serde(rename = "indexPrice")]
    pub index_price: String,
    #[serde(rename = "estimatedSettlePrice")]
    pub estimated_settle_price: String,
    #[serde(rename = "lastFundingRate")]
    pub last_funding_rate: String,
    #[serde(rename = "nextFundingTime")]
    pub next_funding_time: u64,
    #[serde(rename = "interestRate")]
    pub interest_rate: String,
    pub time: u64,
}

/// Shape of the mark price response: single symbol or all symbols.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum MarkPriceShape {
    /// Single symbol mark price.
    Single(MarkPriceResponse),
    /// All symbols mark prices.
    Multi(Vec<MarkPriceResponse>),
}

// --- Tickers ---

/// 24-hour price change statistics for a symbol.
#[derive(Debug, Deserialize)]
pub struct Ticker24hrResponse {
    pub symbol: String,
    #[serde(rename = "priceChange")]
    pub price_change: String,
    #[serde(rename = "priceChangePercent")]
    pub price_change_percent: String,
    #[serde(rename = "weightedAvgPrice")]
    pub weighted_avg_price: String,
    /// Absent for some symbol types on AsterDex testnet.
    #[serde(rename = "prevClosePrice", default)]
    pub prev_close_price: String,
    #[serde(rename = "lastPrice")]
    pub last_price: String,
    #[serde(rename = "lastQty")]
    pub last_qty: String,
    #[serde(rename = "openPrice")]
    pub open_price: String,
    #[serde(rename = "highPrice")]
    pub high_price: String,
    #[serde(rename = "lowPrice")]
    pub low_price: String,
    pub volume: String,
    #[serde(rename = "quoteVolume")]
    pub quote_volume: String,
    #[serde(rename = "openTime")]
    pub open_time: u64,
    #[serde(rename = "closeTime")]
    pub close_time: u64,
    #[serde(rename = "firstId")]
    pub first_id: i64,
    #[serde(rename = "lastId")]
    pub last_id: i64,
    pub count: u64,
}

/// Shape of the 24-hour ticker response: single symbol or all symbols.
///
/// The API returns either a single object or an array depending on whether
/// `symbol` was specified. The `Single` variant is boxed to reduce stack size.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Ticker24hrShape {
    /// Single symbol ticker (boxed to reduce enum stack size — US-016 clippy fix).
    Single(Box<Ticker24hrResponse>),
    /// All symbols tickers.
    Multi(Vec<Ticker24hrResponse>),
}

/// Latest price ticker for a symbol.
#[derive(Debug, Deserialize)]
pub struct TickerPriceResponse {
    pub symbol: String,
    pub price: String,
    pub time: u64,
}

/// Shape of the price ticker response: single symbol or all symbols.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TickerPriceShape {
    /// Single symbol price ticker.
    Single(TickerPriceResponse),
    /// All symbols price tickers.
    Multi(Vec<TickerPriceResponse>),
}

/// Best bid and ask price/quantity on the order book.
#[derive(Debug, Deserialize)]
pub struct BookTickerResponse {
    pub symbol: String,
    #[serde(rename = "bidPrice")]
    pub bid_price: String,
    #[serde(rename = "bidQty")]
    pub bid_qty: String,
    #[serde(rename = "askPrice")]
    pub ask_price: String,
    #[serde(rename = "askQty")]
    pub ask_qty: String,
    pub time: u64,
}

/// Shape of the book ticker response: single symbol or all symbols.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum BookTickerShape {
    /// Single symbol book ticker.
    Single(BookTickerResponse),
    /// All symbols book tickers.
    Multi(Vec<BookTickerResponse>),
}

// --- Funding ---

/// Historical funding rate record for a symbol.
#[derive(Debug, Deserialize)]
pub struct FundingRateRecord {
    pub symbol: String,
    #[serde(rename = "fundingRate")]
    pub funding_rate: String,
    #[serde(rename = "fundingTime")]
    pub funding_time: u64,
    #[serde(rename = "markPrice")]
    pub mark_price: Option<String>,
}

/// Funding rate configuration for a symbol.
#[derive(Debug, Deserialize)]
pub struct FundingInfoResponse {
    pub symbol: String,
    #[serde(rename = "adjustedFundingRateCap")]
    pub adjusted_funding_rate_cap: String,
    #[serde(rename = "adjustedFundingRateFloor")]
    pub adjusted_funding_rate_floor: String,
    #[serde(rename = "fundingIntervalHours")]
    pub funding_interval_hours: u32,
}

// --- Trades ---

/// Individual trade record for a symbol.
#[derive(Debug, Deserialize)]
pub struct TradeRecord {
    pub id: i64,
    pub price: String,
    pub qty: String,
    #[serde(rename = "quoteQty")]
    pub quote_qty: String,
    pub time: u64,
    #[serde(rename = "isBuyerMaker")]
    pub is_buyer_maker: bool,
}