Skip to main content

asterdex_sdk/futures/endpoints/
market_data.rs

1// US-001: Market data endpoints stub — implemented in US-006, US-007
2// US-006: get_server_time endpoint
3// US-007: public market data endpoints (ping, exchange info, depth, trades, klines, tickers, etc.)
4
5use crate::futures::models::market::{
6    AggTrade, BookTickerShape, DepthResponse, ExchangeInfoResponse, FundingInfoResponse,
7    FundingRateRecord, IndexReferencesResponse, Kline, KlineInterval, MarkPriceShape,
8    PingResponse, ServerTimeResponse, Ticker24hrShape, TickerPriceShape, TradeRecord,
9};
10use crate::rest::client::RestClient;
11use crate::rest::error::AsterDexError;
12use crate::rest::response::ApiResponse;
13
14impl RestClient {
15    /// Get server time in milliseconds.
16    ///
17    /// Use to detect clock drift before placing orders.
18    /// This is a public endpoint (no authentication required).
19    pub async fn get_server_time(&self) -> Result<ApiResponse<ServerTimeResponse>, AsterDexError> {
20        self.get("/fapi/v3/time", &[]).await
21    }
22
23    // -------------------------------------------------------------------------
24    // US-007: Public market data endpoints
25    // -------------------------------------------------------------------------
26
27    /// Ping the REST API to check connectivity.
28    ///
29    /// Public endpoint — no authentication required.
30    pub async fn ping(&self) -> Result<ApiResponse<PingResponse>, AsterDexError> {
31        self.get("/fapi/v3/ping", &[]).await
32    }
33
34    /// Get exchange information including all trading symbols.
35    ///
36    /// Public endpoint — no authentication required.
37    pub async fn get_exchange_info(&self) -> Result<ApiResponse<ExchangeInfoResponse>, AsterDexError> {
38        self.get("/fapi/v3/exchangeInfo", &[]).await
39    }
40
41    /// Get order book depth for a symbol.
42    ///
43    /// Public endpoint — no authentication required.
44    pub async fn get_depth(
45        &self,
46        symbol: &str,
47        limit: Option<u32>,
48    ) -> Result<ApiResponse<DepthResponse>, AsterDexError> {
49        let limit_str;
50        let mut params = vec![("symbol", symbol)];
51        if let Some(l) = limit {
52            limit_str = l.to_string();
53            params.push(("limit", &limit_str));
54        }
55        self.get("/fapi/v3/depth", &params).await
56    }
57
58    /// Get recent trades for a symbol.
59    ///
60    /// Public endpoint — no authentication required.
61    pub async fn get_trades(
62        &self,
63        symbol: &str,
64        limit: Option<u32>,
65    ) -> Result<ApiResponse<Vec<TradeRecord>>, AsterDexError> {
66        let limit_str;
67        let mut params = vec![("symbol", symbol)];
68        if let Some(l) = limit {
69            limit_str = l.to_string();
70            params.push(("limit", &limit_str));
71        }
72        self.get("/fapi/v3/trades", &params).await
73    }
74
75    /// Get historical (older) trades for a symbol.
76    ///
77    /// Public endpoint — no authentication required.
78    pub async fn get_historical_trades(
79        &self,
80        symbol: &str,
81        limit: Option<u32>,
82        from_id: Option<i64>,
83    ) -> Result<ApiResponse<Vec<TradeRecord>>, AsterDexError> {
84        let limit_str;
85        let from_id_str;
86        let mut params = vec![("symbol", symbol)];
87        if let Some(l) = limit {
88            limit_str = l.to_string();
89            params.push(("limit", &limit_str));
90        }
91        if let Some(id) = from_id {
92            from_id_str = id.to_string();
93            params.push(("fromId", &from_id_str));
94        }
95        self.get("/fapi/v3/historicalTrades", &params).await
96    }
97
98    /// Get compressed/aggregate trades for a symbol.
99    ///
100    /// Public endpoint — no authentication required.
101    pub async fn get_agg_trades(
102        &self,
103        symbol: &str,
104        from_id: Option<i64>,
105        start_time: Option<u64>,
106        end_time: Option<u64>,
107        limit: Option<u32>,
108    ) -> Result<ApiResponse<Vec<AggTrade>>, AsterDexError> {
109        let from_id_str;
110        let start_time_str;
111        let end_time_str;
112        let limit_str;
113        let mut params = vec![("symbol", symbol)];
114        if let Some(id) = from_id {
115            from_id_str = id.to_string();
116            params.push(("fromId", &from_id_str));
117        }
118        if let Some(st) = start_time {
119            start_time_str = st.to_string();
120            params.push(("startTime", &start_time_str));
121        }
122        if let Some(et) = end_time {
123            end_time_str = et.to_string();
124            params.push(("endTime", &end_time_str));
125        }
126        if let Some(l) = limit {
127            limit_str = l.to_string();
128            params.push(("limit", &limit_str));
129        }
130        self.get("/fapi/v3/aggTrades", &params).await
131    }
132
133    /// Get kline/candlestick data for a symbol.
134    ///
135    /// Public endpoint — no authentication required.
136    pub async fn get_klines(
137        &self,
138        symbol: &str,
139        interval: KlineInterval,
140        start_time: Option<u64>,
141        end_time: Option<u64>,
142        limit: Option<u32>,
143    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
144        let start_time_str;
145        let end_time_str;
146        let limit_str;
147        let interval_str = interval.to_str();
148        let mut params = vec![("symbol", symbol), ("interval", interval_str)];
149        if let Some(st) = start_time {
150            start_time_str = st.to_string();
151            params.push(("startTime", &start_time_str));
152        }
153        if let Some(et) = end_time {
154            end_time_str = et.to_string();
155            params.push(("endTime", &end_time_str));
156        }
157        if let Some(l) = limit {
158            limit_str = l.to_string();
159            params.push(("limit", &limit_str));
160        }
161        self.get("/fapi/v3/klines", &params).await
162    }
163
164    /// Get kline/candlestick data for an index price.
165    ///
166    /// Public endpoint — no authentication required.
167    pub async fn get_index_price_klines(
168        &self,
169        pair: &str,
170        interval: KlineInterval,
171        start_time: Option<u64>,
172        end_time: Option<u64>,
173        limit: Option<u32>,
174    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
175        let start_time_str;
176        let end_time_str;
177        let limit_str;
178        let interval_str = interval.to_str();
179        let mut params = vec![("pair", pair), ("interval", interval_str)];
180        if let Some(st) = start_time {
181            start_time_str = st.to_string();
182            params.push(("startTime", &start_time_str));
183        }
184        if let Some(et) = end_time {
185            end_time_str = et.to_string();
186            params.push(("endTime", &end_time_str));
187        }
188        if let Some(l) = limit {
189            limit_str = l.to_string();
190            params.push(("limit", &limit_str));
191        }
192        self.get("/fapi/v3/indexPriceKlines", &params).await
193    }
194
195    /// Get kline/candlestick data for a mark price.
196    ///
197    /// Public endpoint — no authentication required.
198    pub async fn get_mark_price_klines(
199        &self,
200        symbol: &str,
201        interval: KlineInterval,
202        start_time: Option<u64>,
203        end_time: Option<u64>,
204        limit: Option<u32>,
205    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
206        let start_time_str;
207        let end_time_str;
208        let limit_str;
209        let interval_str = interval.to_str();
210        let mut params = vec![("symbol", symbol), ("interval", interval_str)];
211        if let Some(st) = start_time {
212            start_time_str = st.to_string();
213            params.push(("startTime", &start_time_str));
214        }
215        if let Some(et) = end_time {
216            end_time_str = et.to_string();
217            params.push(("endTime", &end_time_str));
218        }
219        if let Some(l) = limit {
220            limit_str = l.to_string();
221            params.push(("limit", &limit_str));
222        }
223        self.get("/fapi/v3/markPriceKlines", &params).await
224    }
225
226    /// Get mark price and funding rate for a symbol (or all symbols if None).
227    ///
228    /// Public endpoint — no authentication required.
229    pub async fn get_mark_price(
230        &self,
231        symbol: Option<&str>,
232    ) -> Result<ApiResponse<MarkPriceShape>, AsterDexError> {
233        let mut params: Vec<(&str, &str)> = vec![];
234        if let Some(s) = symbol {
235            params.push(("symbol", s));
236        }
237        self.get("/fapi/v3/premiumIndex", &params).await
238    }
239
240    /// Get funding rate history for a symbol.
241    ///
242    /// Public endpoint — no authentication required.
243    pub async fn get_funding_rate(
244        &self,
245        symbol: Option<&str>,
246        start_time: Option<u64>,
247        end_time: Option<u64>,
248        limit: Option<u32>,
249    ) -> Result<ApiResponse<Vec<FundingRateRecord>>, AsterDexError> {
250        let start_time_str;
251        let end_time_str;
252        let limit_str;
253        let mut params: Vec<(&str, &str)> = vec![];
254        if let Some(s) = symbol {
255            params.push(("symbol", s));
256        }
257        if let Some(st) = start_time {
258            start_time_str = st.to_string();
259            params.push(("startTime", &start_time_str));
260        }
261        if let Some(et) = end_time {
262            end_time_str = et.to_string();
263            params.push(("endTime", &end_time_str));
264        }
265        if let Some(l) = limit {
266            limit_str = l.to_string();
267            params.push(("limit", &limit_str));
268        }
269        self.get("/fapi/v3/fundingRate", &params).await
270    }
271
272    /// Get funding rate information for all symbols.
273    ///
274    /// Public endpoint — no authentication required.
275    pub async fn get_funding_info(&self) -> Result<ApiResponse<Vec<FundingInfoResponse>>, AsterDexError> {
276        self.get("/fapi/v3/fundingInfo", &[]).await
277    }
278
279    /// Get 24-hour ticker price change statistics.
280    ///
281    /// Public endpoint — no authentication required.
282    pub async fn get_ticker_24hr(
283        &self,
284        symbol: Option<&str>,
285    ) -> Result<ApiResponse<Ticker24hrShape>, AsterDexError> {
286        let mut params: Vec<(&str, &str)> = vec![];
287        if let Some(s) = symbol {
288            params.push(("symbol", s));
289        }
290        self.get("/fapi/v3/ticker/24hr", &params).await
291    }
292
293    /// Get latest price for a symbol or all symbols.
294    ///
295    /// Public endpoint — no authentication required.
296    pub async fn get_ticker_price(
297        &self,
298        symbol: Option<&str>,
299    ) -> Result<ApiResponse<TickerPriceShape>, AsterDexError> {
300        let mut params: Vec<(&str, &str)> = vec![];
301        if let Some(s) = symbol {
302            params.push(("symbol", s));
303        }
304        self.get("/fapi/v3/ticker/price", &params).await
305    }
306
307    /// Get best price/quantity on the order book for a symbol.
308    ///
309    /// Public endpoint — no authentication required.
310    pub async fn get_book_ticker(
311        &self,
312        symbol: Option<&str>,
313    ) -> Result<ApiResponse<BookTickerShape>, AsterDexError> {
314        let mut params: Vec<(&str, &str)> = vec![];
315        if let Some(s) = symbol {
316            params.push(("symbol", s));
317        }
318        self.get("/fapi/v3/ticker/bookTicker", &params).await
319    }
320
321    /// Get index price references for a symbol.
322    ///
323    /// Public endpoint — no authentication required.
324    pub async fn get_index_references(
325        &self,
326        symbol: Option<&str>,
327    ) -> Result<ApiResponse<Vec<IndexReferencesResponse>>, AsterDexError> {
328        let mut params: Vec<(&str, &str)> = vec![];
329        if let Some(s) = symbol {
330            params.push(("symbol", s));
331        }
332        self.get("/fapi/v3/indexreferences", &params).await
333    }
334}
335
336// -------------------------------------------------------------------------
337// US-007: Unit tests (mockito)
338// -------------------------------------------------------------------------
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::rest::client::RestClient;
344
345    /// Test 1: ping returns Ok
346    #[tokio::test]
347    async fn ping_returns_ok() {
348        let mut server = mockito::Server::new_async().await;
349        let _mock = server
350            .mock("GET", "/fapi/v3/ping")
351            .with_status(200)
352            .with_header("content-type", "application/json")
353            .with_body("{}")
354            .create_async()
355            .await;
356        let client = RestClient::new_public(&server.url()).unwrap();
357        let resp = client.ping().await.unwrap();
358        let _ = resp.data; // PingResponse is empty struct — just assert Ok
359    }
360
361    /// Test 2: get_depth returns bids and asks
362    #[tokio::test]
363    async fn get_depth_returns_bids_asks() {
364        let mut server = mockito::Server::new_async().await;
365        let _mock = server
366            .mock("GET", "/fapi/v3/depth")
367            .match_query(mockito::Matcher::AllOf(vec![
368                mockito::Matcher::UrlEncoded("symbol".to_string(), "BTCUSDT".to_string()),
369                mockito::Matcher::UrlEncoded("limit".to_string(), "20".to_string()),
370            ]))
371            .with_status(200)
372            .with_header("content-type", "application/json")
373            .with_body(r#"{"lastUpdateId":12345,"bids":[["45000.00","1.5"]],"asks":[["45001.00","0.5"]]}"#)
374            .create_async()
375            .await;
376        let client = RestClient::new_public(&server.url()).unwrap();
377        let resp = client.get_depth("BTCUSDT", Some(20)).await.unwrap();
378        assert_eq!(resp.data.last_update_id, 12345);
379        assert!(!resp.data.bids.is_empty());
380        assert!(!resp.data.asks.is_empty());
381    }
382
383    /// Test 3: get_klines returns OHLCV data
384    #[tokio::test]
385    async fn get_klines_returns_ohlcv() {
386        let mut server = mockito::Server::new_async().await;
387        let _mock = server
388            .mock("GET", "/fapi/v3/klines")
389            .match_query(mockito::Matcher::AllOf(vec![
390                mockito::Matcher::UrlEncoded("symbol".to_string(), "BTCUSDT".to_string()),
391                mockito::Matcher::UrlEncoded("interval".to_string(), "1m".to_string()),
392                mockito::Matcher::UrlEncoded("limit".to_string(), "1".to_string()),
393            ]))
394            .with_status(200)
395            .with_header("content-type", "application/json")
396            .with_body(r#"[[1700000000000,"45000","46000","44000","45500","100.5",1700000059999,"4550000",500,"50.5","2275000"]]"#)
397            .create_async()
398            .await;
399        let client = RestClient::new_public(&server.url()).unwrap();
400        let resp = client
401            .get_klines("BTCUSDT", KlineInterval::OneMinute, None, None, Some(1))
402            .await
403            .unwrap();
404        assert_eq!(resp.data[0].open_time, 1_700_000_000_000u64);
405    }
406
407    /// Test 4: get_ticker_24hr returns JSON response
408    #[tokio::test]
409    async fn get_ticker_24hr_returns_json() {
410        let mut server = mockito::Server::new_async().await;
411        let _mock = server
412            .mock("GET", "/fapi/v3/ticker/24hr")
413            .match_query(mockito::Matcher::UrlEncoded(
414                "symbol".to_string(),
415                "BTCUSDT".to_string(),
416            ))
417            .with_status(200)
418            .with_header("content-type", "application/json")
419            .with_body(r#"{"symbol":"BTCUSDT","lastPrice":"45000.00","priceChange":"100.00","priceChangePercent":"0.22","weightedAvgPrice":"44950.00","prevClosePrice":"44900.00","lastQty":"1.0","openPrice":"44900.00","highPrice":"46000.00","lowPrice":"44500.00","volume":"5000.0","quoteVolume":"224750000.0","openTime":1699999200000,"closeTime":1700085600000,"firstId":1,"lastId":5000,"count":5000}"#)
420            .create_async()
421            .await;
422        let client = RestClient::new_public(&server.url()).unwrap();
423        let resp = client.get_ticker_24hr(Some("BTCUSDT")).await;
424        assert!(resp.is_ok());
425    }
426
427    /// Test 5: public endpoint does NOT include auth params in request URL.
428    ///
429    /// Strategy: mock matches EXACT query "symbol=BTCUSDT" (no auth fields).
430    /// If auth params were injected the mock would not match and the call would fail.
431    #[tokio::test]
432    async fn public_endpoint_no_auth_params() {
433        let mut server = mockito::Server::new_async().await;
434        let _mock = server
435            .mock("GET", "/fapi/v3/depth")
436            .match_query(mockito::Matcher::UrlEncoded(
437                "symbol".to_string(),
438                "BTCUSDT".to_string(),
439            ))
440            .with_status(200)
441            .with_header("content-type", "application/json")
442            .with_body(r#"{"lastUpdateId":1,"bids":[],"asks":[]}"#)
443            .create_async()
444            .await;
445        let client = RestClient::new_public(&server.url()).unwrap();
446        // If the client were injecting auth params (user=, signer=, nonce=, signature=)
447        // the mock would NOT match (wrong query string) and the call would return an error.
448        let result = client.get_depth("BTCUSDT", None).await;
449        assert!(
450            result.is_ok(),
451            "Expected Ok but got error — auth params may have been injected: {:?}",
452            result
453        );
454    }
455}