asterdex-sdk 0.1.2

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-001: Trading endpoints stub — implemented in US-004, US-005
// US-004: place_order endpoint
// US-005: cancel_order, get_open_orders, get_position_risk endpoints

use crate::futures::models::trading::{
    CancelOrderParams, CancelOrderResponse, ModifyOrderParams, OrderResponse, PlaceOrderParams,
    PositionResponse,
};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

impl RestClient {
    /// Place a new futures order.
    ///
    /// # Errors
    /// - `ApiError { code: -1102 }` — required parameter missing (e.g., price for LIMIT)
    /// - `ApiError { code: -1121 }` — invalid symbol
    /// - `RateLimited` — HTTP 429
    /// - `IpBanned` — HTTP 418
    pub async fn place_order(
        &self,
        params: PlaceOrderParams,
    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
        // Convert bool → &'static str once, outside the Vec, so we don't need
        // String conversions per call.
        let reduce_only_s = params.reduce_only.map(bool_as_str);
        let price_protect_s = params.price_protect.map(bool_as_str);

        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(12);
        pairs.push(("symbol", &params.symbol));
        pairs.push(("side", &params.side));
        pairs.push(("type", &params.order_type));
        pairs.push(("quantity", &params.quantity));
        if let Some(p) = params.price.as_deref() {
            pairs.push(("price", p));
        }
        if let Some(tif) = params.time_in_force.as_deref() {
            pairs.push(("timeInForce", tif));
        }
        if let Some(s) = reduce_only_s {
            pairs.push(("reduceOnly", s));
        }
        if let Some(id) = params.new_client_order_id.as_deref() {
            pairs.push(("newClientOrderId", id));
        }
        if let Some(sp) = params.stop_price.as_deref() {
            pairs.push(("stopPrice", sp));
        }
        if let Some(ap) = params.activation_price.as_deref() {
            pairs.push(("activationPrice", ap));
        }
        if let Some(cr) = params.callback_rate.as_deref() {
            pairs.push(("callbackRate", cr));
        }
        if let Some(wt) = params.working_type.as_deref() {
            pairs.push(("workingType", wt));
        }
        if let Some(s) = price_protect_s {
            pairs.push(("priceProtect", s));
        }
        if let Some(ort) = params.new_order_resp_type.as_deref() {
            pairs.push(("newOrderRespType", ort));
        }
        self.signed_post("/fapi/v3/order", &pairs).await
    }

    /// Cancel an open order.
    ///
    /// One of `params.order_id` or `params.orig_client_order_id` must be set.
    ///
    /// # Errors
    /// - `ApiError { code: -2011 }` — order not found
    /// - `ApiError { code: -1102 }` — neither orderId nor origClientOrderId provided
    pub async fn cancel_order(
        &self,
        params: CancelOrderParams,
    ) -> Result<ApiResponse<CancelOrderResponse>, AsterDexError> {
        // `order_id` is numeric → one allocation; bind outside the Vec so the
        // string lives long enough.
        let order_id_str = params.order_id.map(|id| id.to_string());
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(3);
        pairs.push(("symbol", &params.symbol));
        if let Some(s) = order_id_str.as_deref() {
            pairs.push(("orderId", s));
        }
        if let Some(cid) = params.orig_client_order_id.as_deref() {
            pairs.push(("origClientOrderId", cid));
        }
        self.signed_delete("/fapi/v3/order", &pairs).await
    }

    /// Modify an existing open LIMIT order in-place (price and/or quantity).
    ///
    /// Calls `PUT /fapi/v3/order` directly. The original `order_id` is preserved —
    /// this is a true amendment, not a cancel + re-place. At least one of
    /// `params.quantity` or `params.price` must be set. Either `params.order_id`
    /// or `params.orig_client_order_id` must be provided to identify the order.
    ///
    /// Only LIMIT orders can be modified. If the new qty/price fails exchange
    /// filters the modification is rejected and the original order remains unchanged.
    ///
    /// # Errors
    /// - `ApiError { code: -2011 }` — order not found
    /// - `ApiError { code: -1102 }` — required parameter missing
    pub async fn modify_order(
        &self,
        params: ModifyOrderParams,
    ) -> Result<ApiResponse<OrderResponse>, AsterDexError> {
        let order_id_str = params.order_id.map(|id| id.to_string());
        let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(5);
        pairs.push(("symbol", &params.symbol));
        if let Some(s) = order_id_str.as_deref() {
            pairs.push(("orderId", s));
        }
        if let Some(cid) = params.orig_client_order_id.as_deref() {
            pairs.push(("origClientOrderId", cid));
        }
        if let Some(q) = params.quantity.as_deref() {
            pairs.push(("quantity", q));
        }
        if let Some(p) = params.price.as_deref() {
            pairs.push(("price", p));
        }
        self.signed_put("/fapi/v3/order", &pairs).await
    }

    /// Get all open orders. Pass `None` for all symbols or `Some("BTCUSDT")` for one symbol.
    pub async fn get_open_orders(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Vec<OrderResponse>>, AsterDexError> {
        let mut kv: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            kv.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/openOrders", &kv).await
    }

    /// Get position risk information. Pass `None` for all symbols or `Some("BTCUSDT")` for one symbol.
    pub async fn get_position_risk(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Vec<PositionResponse>>, AsterDexError> {
        let mut kv: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            kv.push(("symbol", s));
        }
        self.signed_get("/fapi/v3/positionRisk", &kv).await
    }
}

/// Zero-alloc bool → form-value string.
#[inline]
fn bool_as_str(b: bool) -> &'static str {
    if b { "true" } else { "false" }
}