Skip to main content

asterdex_sdk/futures/endpoints/
advanced_trading.rs

1// US-014: Remaining trading and account endpoints
2
3use crate::futures::models::account::{AdlQuantileResponse, CommissionRateResponse, MultiAssetsModeResponse, StatusMsgResponse};
4use crate::futures::models::trading::{
5    AllOrdersParams, CountdownParams, CountdownResponse, EmptyResponse, GetOrderParams,
6    IncomeParams, IncomeRecord, OrderResponse, PositionMarginParams, PositionMarginResponse,
7    UserTrade, UserTradesParams,
8};
9use crate::rest::client::RestClient;
10use crate::rest::error::AsterDexError;
11use crate::rest::response::ApiResponse;
12
13impl RestClient {
14    /// Query a specific order by ID or client order ID.
15    pub async fn get_order(
16        &self,
17        params: GetOrderParams,
18    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
19        let order_id_str = params.order_id.map(|id| id.to_string());
20        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(3);
21        pairs.push(("symbol", &params.symbol));
22        if let Some(ref s) = order_id_str {
23            pairs.push(("orderId", s));
24        }
25        if let Some(ref cid) = params.orig_client_order_id {
26            pairs.push(("origClientOrderId", cid));
27        }
28        self.signed_get("/fapi/v3/order", &pairs).await
29    }
30
31    /// Query a specific open order by ID or client order ID.
32    pub async fn get_open_order(
33        &self,
34        params: GetOrderParams,
35    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
36        let order_id_str = params.order_id.map(|id| id.to_string());
37        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(3);
38        pairs.push(("symbol", &params.symbol));
39        if let Some(ref s) = order_id_str {
40            pairs.push(("orderId", s));
41        }
42        if let Some(ref cid) = params.orig_client_order_id {
43            pairs.push(("origClientOrderId", cid));
44        }
45        self.signed_get("/fapi/v3/openOrder", &pairs).await
46    }
47
48    /// Get all orders (open + historical) for a symbol.
49    pub async fn get_all_orders(
50        &self,
51        params: AllOrdersParams,
52    ) -> Result<ApiResponse<Vec<OrderResponse>>, AsterDexError> {
53        let order_id_str = params.order_id.map(|id| id.to_string());
54        let start_time_str = params.start_time.map(|t| t.to_string());
55        let end_time_str = params.end_time.map(|t| t.to_string());
56        let limit_str = params.limit.map(|l| l.to_string());
57
58        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
59        pairs.push(("symbol", &params.symbol));
60        if let Some(ref s) = order_id_str {
61            pairs.push(("orderId", s));
62        }
63        if let Some(ref s) = start_time_str {
64            pairs.push(("startTime", s));
65        }
66        if let Some(ref s) = end_time_str {
67            pairs.push(("endTime", s));
68        }
69        if let Some(ref s) = limit_str {
70            pairs.push(("limit", s));
71        }
72        self.signed_get("/fapi/v3/allOrders", &pairs).await
73    }
74
75    /// Get user trade history for a symbol.
76    pub async fn get_user_trades(
77        &self,
78        params: UserTradesParams,
79    ) -> Result<ApiResponse<Vec<UserTrade>>, AsterDexError> {
80        let order_id_str = params.order_id.map(|id| id.to_string());
81        let start_time_str = params.start_time.map(|t| t.to_string());
82        let end_time_str = params.end_time.map(|t| t.to_string());
83        let from_id_str = params.from_id.map(|id| id.to_string());
84        let limit_str = params.limit.map(|l| l.to_string());
85
86        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(6);
87        pairs.push(("symbol", &params.symbol));
88        if let Some(ref s) = order_id_str {
89            pairs.push(("orderId", s));
90        }
91        if let Some(ref s) = start_time_str {
92            pairs.push(("startTime", s));
93        }
94        if let Some(ref s) = end_time_str {
95            pairs.push(("endTime", s));
96        }
97        if let Some(ref s) = from_id_str {
98            pairs.push(("fromId", s));
99        }
100        if let Some(ref s) = limit_str {
101            pairs.push(("limit", s));
102        }
103        self.signed_get("/fapi/v3/trades", &pairs).await
104    }
105
106    /// Get income history (funding fees, realized PnL, etc.).
107    pub async fn get_income(
108        &self,
109        params: IncomeParams,
110    ) -> Result<ApiResponse<Vec<IncomeRecord>>, AsterDexError> {
111        let start_time_str = params.start_time.map(|t| t.to_string());
112        let end_time_str = params.end_time.map(|t| t.to_string());
113        let limit_str = params.limit.map(|l| l.to_string());
114
115        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
116        if let Some(ref s) = params.symbol {
117            pairs.push(("symbol", s));
118        }
119        if let Some(ref it) = params.income_type {
120            pairs.push(("incomeType", it));
121        }
122        if let Some(ref s) = start_time_str {
123            pairs.push(("startTime", s));
124        }
125        if let Some(ref s) = end_time_str {
126            pairs.push(("endTime", s));
127        }
128        if let Some(ref s) = limit_str {
129            pairs.push(("limit", s));
130        }
131        self.signed_get("/fapi/v3/income", &pairs).await
132    }
133
134    /// Add or reduce isolated position margin (type 1 = add, type 2 = reduce).
135    pub async fn set_position_margin(
136        &self,
137        params: PositionMarginParams,
138    ) -> Result<ApiResponse<PositionMarginResponse>, AsterDexError> {
139        let margin_type_str = params.margin_type.to_string();
140        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(4);
141        pairs.push(("symbol", &params.symbol));
142        pairs.push(("amount", &params.amount));
143        pairs.push(("type", &margin_type_str));
144        if let Some(ref ps) = params.position_side {
145            pairs.push(("positionSide", ps));
146        }
147        self.signed_post("/fapi/v3/positionMargin", &pairs).await
148    }
149
150    /// Get position margin change history.
151    pub async fn get_position_margin_history(
152        &self,
153        symbol: &str,
154        margin_type: Option<u8>,
155        start_time: Option<u64>,
156        end_time: Option<u64>,
157        limit: Option<u32>,
158    ) -> Result<ApiResponse<Vec<serde_json::Value>>, AsterDexError> {
159        let margin_type_str = margin_type.map(|t| t.to_string());
160        let start_time_str = start_time.map(|t| t.to_string());
161        let end_time_str = end_time.map(|t| t.to_string());
162        let limit_str = limit.map(|l| l.to_string());
163
164        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
165        pairs.push(("symbol", symbol));
166        if let Some(ref s) = margin_type_str {
167            pairs.push(("type", s));
168        }
169        if let Some(ref s) = start_time_str {
170            pairs.push(("startTime", s));
171        }
172        if let Some(ref s) = end_time_str {
173            pairs.push(("endTime", s));
174        }
175        if let Some(ref s) = limit_str {
176            pairs.push(("limit", s));
177        }
178        self.signed_get("/fapi/v3/positionMargin/history", &pairs).await
179    }
180
181    /// Get auto-deleverage quantile for positions.
182    pub async fn get_adl_quantile(
183        &self,
184        symbol: Option<&str>,
185    ) -> Result<ApiResponse<Vec<AdlQuantileResponse>>, AsterDexError> {
186        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(1);
187        if let Some(s) = symbol {
188            pairs.push(("symbol", s));
189        }
190        self.signed_get("/fapi/v3/adlQuantile", &pairs).await
191    }
192
193    /// Get forced liquidation order history.
194    pub async fn get_force_orders(
195        &self,
196        symbol: Option<&str>,
197        auto_close_type: Option<&str>,
198        start_time: Option<u64>,
199        end_time: Option<u64>,
200        limit: Option<u32>,
201    ) -> Result<ApiResponse<Vec<serde_json::Value>>, AsterDexError> {
202        let start_time_str = start_time.map(|t| t.to_string());
203        let end_time_str = end_time.map(|t| t.to_string());
204        let limit_str = limit.map(|l| l.to_string());
205
206        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
207        if let Some(s) = symbol {
208            pairs.push(("symbol", s));
209        }
210        if let Some(t) = auto_close_type {
211            pairs.push(("autoCloseType", t));
212        }
213        if let Some(ref s) = start_time_str {
214            pairs.push(("startTime", s));
215        }
216        if let Some(ref s) = end_time_str {
217            pairs.push(("endTime", s));
218        }
219        if let Some(ref s) = limit_str {
220            pairs.push(("limit", s));
221        }
222        self.signed_get("/fapi/v3/forceOrders", &pairs).await
223    }
224
225    /// Get user commission rate for a symbol.
226    pub async fn get_commission_rate(
227        &self,
228        symbol: &str,
229    ) -> Result<ApiResponse<CommissionRateResponse>, AsterDexError> {
230        self.signed_get("/fapi/v3/commissionRate", &[("symbol", symbol)]).await
231    }
232
233    /// Get multi-assets margin mode status.
234    pub async fn get_multi_assets_margin(
235        &self,
236    ) -> Result<ApiResponse<MultiAssetsModeResponse>, AsterDexError> {
237        self.signed_get("/fapi/v3/multiAssetsMargin", &[]).await
238    }
239
240    /// Change multi-assets margin mode.
241    pub async fn set_multi_assets_margin(
242        &self,
243        enabled: bool,
244    ) -> Result<ApiResponse<StatusMsgResponse>, AsterDexError> {
245        let val = if enabled { "true" } else { "false" };
246        self.signed_post("/fapi/v3/multiAssetsMargin", &[("multiAssetsMargin", val)]).await
247    }
248
249    /// Set countdown to auto-cancel all orders. `countdown_time = 0` disables.
250    pub async fn countdown_cancel_all(
251        &self,
252        params: CountdownParams,
253    ) -> Result<ApiResponse<CountdownResponse>, AsterDexError> {
254        let ct_str = params.countdown_time.to_string();
255        self.signed_post(
256            "/fapi/v3/countdownCancelAll",
257            &[("symbol", params.symbol.as_str()), ("countdownTime", &ct_str)],
258        )
259        .await
260    }
261
262    /// No-operation signed endpoint — useful for testing auth.
263    pub async fn noop(&self) -> Result<ApiResponse<EmptyResponse>, AsterDexError> {
264        self.signed_post("/fapi/v3/noop", &[]).await
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::futures::models::trading::{AllOrdersParams, GetOrderParams};
272    use mockito::Server;
273
274    // US-014: get_order without credentials returns ConfigError
275    #[tokio::test]
276    async fn get_order_without_credentials_returns_config_error() {
277        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
278        let params = GetOrderParams {
279            symbol: "BTCUSDT".to_string(),
280            order_id: Some(123456),
281            orig_client_order_id: None,
282        };
283        let result = client.get_order(params).await;
284        assert!(
285            matches!(result, Err(AsterDexError::ConfigError { .. })),
286            "expected ConfigError, got: {:?}",
287            result
288        );
289    }
290
291    // US-014: get_all_orders without credentials returns ConfigError
292    #[tokio::test]
293    async fn get_all_orders_without_credentials_returns_config_error() {
294        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
295        let params = AllOrdersParams {
296            symbol: "BTCUSDT".to_string(),
297            limit: Some(100),
298            ..Default::default()
299        };
300        let result = client.get_all_orders(params).await;
301        assert!(
302            matches!(result, Err(AsterDexError::ConfigError { .. })),
303            "expected ConfigError, got: {:?}",
304            result
305        );
306    }
307
308    // US-014: noop without credentials returns ConfigError
309    #[tokio::test]
310    async fn noop_without_credentials_returns_config_error() {
311        let client = RestClient::new_public("https://fapi.asterdex.com").unwrap();
312        let result = client.noop().await;
313        assert!(
314            matches!(result, Err(AsterDexError::ConfigError { .. })),
315            "expected ConfigError, got: {:?}",
316            result
317        );
318    }
319
320    // US-014: get_all_orders mock — returns empty vec
321    #[tokio::test]
322    async fn get_all_orders_mock() {
323        let mut server = Server::new_async().await;
324        let mock = server
325            .mock("GET", mockito::Matcher::Regex(r"^/fapi/v3/allOrders".to_string()))
326            .with_status(200)
327            .with_header("content-type", "application/json")
328            .with_body("[]")
329            .create_async()
330            .await;
331
332        // Use a client with credentials to bypass ConfigError — credentials do not
333        // need to be real because the mock server returns a canned response.
334        use crate::auth::Credentials;
335        let creds = Credentials::new(
336            "0x0000000000000000000000000000000000000001",
337            "0x0000000000000000000000000000000000000001",
338            "0x0000000000000000000000000000000000000000000000000000000000000001",
339            714,
340        )
341        .unwrap();
342        let client = RestClient::new(&server.url(), creds).unwrap();
343
344        let params = AllOrdersParams {
345            symbol: "BTCUSDT".to_string(),
346            ..Default::default()
347        };
348        let resp = client.get_all_orders(params).await.unwrap();
349        assert!(resp.data.is_empty(), "expected empty vec, got {:?}", resp.data);
350
351        mock.assert_async().await;
352    }
353}