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