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