/*
* Binance Derivatives Trading COIN Futures WebSocket API
*
* OpenAPI Specification for the Binance Derivatives Trading COIN Futures WebSocket API
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
#![allow(unused_imports)]
use anyhow::Context;
use async_trait::async_trait;
use derive_builder::Builder;
use rust_decimal::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::BTreeMap, sync::Arc};
use crate::common::{
errors::WebsocketError,
models::{ParamBuildError, WebsocketApiResponse},
utils::remove_empty_value,
websocket::{WebsocketApi, WebsocketMessageSendOptions},
};
use crate::derivatives_trading_coin_futures::websocket_api::models;
#[async_trait]
pub trait TradeApi: Send + Sync {
async fn cancel_order(
&self,
params: CancelOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::CancelOrderResponseResult>>>;
async fn modify_order(
&self,
params: ModifyOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::ModifyOrderResponseResult>>>;
async fn new_order(
&self,
params: NewOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::NewOrderResponseResult>>>;
async fn position_information(
&self,
params: PositionInformationParams,
) -> anyhow::Result<WebsocketApiResponse<Vec<models::PositionInformationResponseResultInner>>>;
async fn query_order(
&self,
params: QueryOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::QueryOrderResponseResult>>>;
}
#[derive(Clone)]
pub struct TradeApiClient {
websocket_api_base: Arc<WebsocketApi>,
}
impl TradeApiClient {
pub fn new(websocket_api_base: Arc<WebsocketApi>) -> Self {
Self { websocket_api_base }
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModifyOrderSideEnum {
#[serde(rename = "BUY")]
Buy,
#[serde(rename = "SELL")]
Sell,
}
impl ModifyOrderSideEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Buy => "BUY",
Self::Sell => "SELL",
}
}
}
impl std::str::FromStr for ModifyOrderSideEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"BUY" => Ok(Self::Buy),
"SELL" => Ok(Self::Sell),
other => Err(format!("invalid ModifyOrderSideEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModifyOrderPriceMatchEnum {
#[serde(rename = "NONE")]
None,
#[serde(rename = "OPPONENT")]
Opponent,
#[serde(rename = "OPPONENT_5")]
Opponent5,
#[serde(rename = "OPPONENT_10")]
Opponent10,
#[serde(rename = "OPPONENT_20")]
Opponent20,
#[serde(rename = "QUEUE")]
Queue,
#[serde(rename = "QUEUE_5")]
Queue5,
#[serde(rename = "QUEUE_10")]
Queue10,
#[serde(rename = "QUEUE_20")]
Queue20,
}
impl ModifyOrderPriceMatchEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "NONE",
Self::Opponent => "OPPONENT",
Self::Opponent5 => "OPPONENT_5",
Self::Opponent10 => "OPPONENT_10",
Self::Opponent20 => "OPPONENT_20",
Self::Queue => "QUEUE",
Self::Queue5 => "QUEUE_5",
Self::Queue10 => "QUEUE_10",
Self::Queue20 => "QUEUE_20",
}
}
}
impl std::str::FromStr for ModifyOrderPriceMatchEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"NONE" => Ok(Self::None),
"OPPONENT" => Ok(Self::Opponent),
"OPPONENT_5" => Ok(Self::Opponent5),
"OPPONENT_10" => Ok(Self::Opponent10),
"OPPONENT_20" => Ok(Self::Opponent20),
"QUEUE" => Ok(Self::Queue),
"QUEUE_5" => Ok(Self::Queue5),
"QUEUE_10" => Ok(Self::Queue10),
"QUEUE_20" => Ok(Self::Queue20),
other => Err(format!("invalid ModifyOrderPriceMatchEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderSideEnum {
#[serde(rename = "BUY")]
Buy,
#[serde(rename = "SELL")]
Sell,
}
impl NewOrderSideEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Buy => "BUY",
Self::Sell => "SELL",
}
}
}
impl std::str::FromStr for NewOrderSideEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"BUY" => Ok(Self::Buy),
"SELL" => Ok(Self::Sell),
other => Err(format!("invalid NewOrderSideEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderTypeEnum {
#[serde(rename = "LIMIT")]
Limit,
#[serde(rename = "MARKET")]
Market,
#[serde(rename = "STOP")]
Stop,
#[serde(rename = "STOP_MARKET")]
StopMarket,
#[serde(rename = "TAKE_PROFIT")]
TakeProfit,
#[serde(rename = "TAKE_PROFIT_MARKET")]
TakeProfitMarket,
#[serde(rename = "TRAILING_STOP_MARKET")]
TrailingStopMarket,
}
impl NewOrderTypeEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Limit => "LIMIT",
Self::Market => "MARKET",
Self::Stop => "STOP",
Self::StopMarket => "STOP_MARKET",
Self::TakeProfit => "TAKE_PROFIT",
Self::TakeProfitMarket => "TAKE_PROFIT_MARKET",
Self::TrailingStopMarket => "TRAILING_STOP_MARKET",
}
}
}
impl std::str::FromStr for NewOrderTypeEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"LIMIT" => Ok(Self::Limit),
"MARKET" => Ok(Self::Market),
"STOP" => Ok(Self::Stop),
"STOP_MARKET" => Ok(Self::StopMarket),
"TAKE_PROFIT" => Ok(Self::TakeProfit),
"TAKE_PROFIT_MARKET" => Ok(Self::TakeProfitMarket),
"TRAILING_STOP_MARKET" => Ok(Self::TrailingStopMarket),
other => Err(format!("invalid NewOrderTypeEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderPositionSideEnum {
#[serde(rename = "BOTH")]
Both,
#[serde(rename = "LONG")]
Long,
#[serde(rename = "SHORT")]
Short,
}
impl NewOrderPositionSideEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Both => "BOTH",
Self::Long => "LONG",
Self::Short => "SHORT",
}
}
}
impl std::str::FromStr for NewOrderPositionSideEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"BOTH" => Ok(Self::Both),
"LONG" => Ok(Self::Long),
"SHORT" => Ok(Self::Short),
other => Err(format!("invalid NewOrderPositionSideEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderTimeInForceEnum {
#[serde(rename = "GTC")]
Gtc,
#[serde(rename = "IOC")]
Ioc,
#[serde(rename = "FOK")]
Fok,
#[serde(rename = "GTX")]
Gtx,
}
impl NewOrderTimeInForceEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Gtc => "GTC",
Self::Ioc => "IOC",
Self::Fok => "FOK",
Self::Gtx => "GTX",
}
}
}
impl std::str::FromStr for NewOrderTimeInForceEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GTC" => Ok(Self::Gtc),
"IOC" => Ok(Self::Ioc),
"FOK" => Ok(Self::Fok),
"GTX" => Ok(Self::Gtx),
other => Err(format!("invalid NewOrderTimeInForceEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderWorkingTypeEnum {
#[serde(rename = "MARK_PRICE")]
MarkPrice,
#[serde(rename = "CONTRACT_PRICE")]
ContractPrice,
}
impl NewOrderWorkingTypeEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::MarkPrice => "MARK_PRICE",
Self::ContractPrice => "CONTRACT_PRICE",
}
}
}
impl std::str::FromStr for NewOrderWorkingTypeEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"MARK_PRICE" => Ok(Self::MarkPrice),
"CONTRACT_PRICE" => Ok(Self::ContractPrice),
other => Err(format!("invalid NewOrderWorkingTypeEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderNewOrderRespTypeEnum {
#[serde(rename = "ACK")]
Ack,
#[serde(rename = "RESULT")]
Result,
}
impl NewOrderNewOrderRespTypeEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Ack => "ACK",
Self::Result => "RESULT",
}
}
}
impl std::str::FromStr for NewOrderNewOrderRespTypeEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ACK" => Ok(Self::Ack),
"RESULT" => Ok(Self::Result),
other => Err(format!("invalid NewOrderNewOrderRespTypeEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderPriceMatchEnum {
#[serde(rename = "NONE")]
None,
#[serde(rename = "OPPONENT")]
Opponent,
#[serde(rename = "OPPONENT_5")]
Opponent5,
#[serde(rename = "OPPONENT_10")]
Opponent10,
#[serde(rename = "OPPONENT_20")]
Opponent20,
#[serde(rename = "QUEUE")]
Queue,
#[serde(rename = "QUEUE_5")]
Queue5,
#[serde(rename = "QUEUE_10")]
Queue10,
#[serde(rename = "QUEUE_20")]
Queue20,
}
impl NewOrderPriceMatchEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "NONE",
Self::Opponent => "OPPONENT",
Self::Opponent5 => "OPPONENT_5",
Self::Opponent10 => "OPPONENT_10",
Self::Opponent20 => "OPPONENT_20",
Self::Queue => "QUEUE",
Self::Queue5 => "QUEUE_5",
Self::Queue10 => "QUEUE_10",
Self::Queue20 => "QUEUE_20",
}
}
}
impl std::str::FromStr for NewOrderPriceMatchEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"NONE" => Ok(Self::None),
"OPPONENT" => Ok(Self::Opponent),
"OPPONENT_5" => Ok(Self::Opponent5),
"OPPONENT_10" => Ok(Self::Opponent10),
"OPPONENT_20" => Ok(Self::Opponent20),
"QUEUE" => Ok(Self::Queue),
"QUEUE_5" => Ok(Self::Queue5),
"QUEUE_10" => Ok(Self::Queue10),
"QUEUE_20" => Ok(Self::Queue20),
other => Err(format!("invalid NewOrderPriceMatchEnum: {}", other).into()),
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NewOrderSelfTradePreventionModeEnum {
#[serde(rename = "NONE")]
None,
#[serde(rename = "EXPIRE_TAKER")]
ExpireTaker,
#[serde(rename = "EXPIRE_BOTH")]
ExpireBoth,
#[serde(rename = "EXPIRE_MAKER")]
ExpireMaker,
}
impl NewOrderSelfTradePreventionModeEnum {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "NONE",
Self::ExpireTaker => "EXPIRE_TAKER",
Self::ExpireBoth => "EXPIRE_BOTH",
Self::ExpireMaker => "EXPIRE_MAKER",
}
}
}
impl std::str::FromStr for NewOrderSelfTradePreventionModeEnum {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"NONE" => Ok(Self::None),
"EXPIRE_TAKER" => Ok(Self::ExpireTaker),
"EXPIRE_BOTH" => Ok(Self::ExpireBoth),
"EXPIRE_MAKER" => Ok(Self::ExpireMaker),
other => Err(format!("invalid NewOrderSelfTradePreventionModeEnum: {}", other).into()),
}
}
}
/// Request parameters for the [`cancel_order`] operation.
///
/// This struct holds all of the inputs you can pass when calling
/// [`cancel_order`](#method.cancel_order).
#[derive(Clone, Debug, Builder)]
#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
pub struct CancelOrderParams {
///
/// The `symbol` parameter.
///
/// This field is **required.
#[builder(setter(into))]
pub symbol: String,
/// Unique WebSocket request ID.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub id: Option<String>,
///
/// The `order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub order_id: Option<i64>,
///
/// The `orig_client_order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub orig_client_order_id: Option<String>,
///
/// The `recv_window` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub recv_window: Option<i64>,
}
impl CancelOrderParams {
/// Create a builder for [`cancel_order`].
///
/// Required parameters:
///
/// * `symbol` — String
///
#[must_use]
pub fn builder(symbol: String) -> CancelOrderParamsBuilder {
CancelOrderParamsBuilder::default().symbol(symbol)
}
}
/// Request parameters for the [`modify_order`] operation.
///
/// This struct holds all of the inputs you can pass when calling
/// [`modify_order`](#method.modify_order).
#[derive(Clone, Debug, Builder)]
#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
pub struct ModifyOrderParams {
///
/// The `symbol` parameter.
///
/// This field is **required.
#[builder(setter(into))]
pub symbol: String,
/// `SELL`, `BUY`
///
/// This field is **required.
#[builder(setter(into))]
pub side: ModifyOrderSideEnum,
/// Order quantity, cannot be sent with `closePosition=true`
///
/// This field is **required.
#[builder(setter(into))]
pub quantity: rust_decimal::Decimal,
///
/// The `price` parameter.
///
/// This field is **required.
#[builder(setter(into))]
pub price: rust_decimal::Decimal,
/// Unique WebSocket request ID.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub id: Option<String>,
///
/// The `order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub order_id: Option<i64>,
///
/// The `orig_client_order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub orig_client_order_id: Option<String>,
/// only available for `LIMIT`/`STOP`/`TAKE_PROFIT` order; can be set to `OPPONENT`/ `OPPONENT_5`/ `OPPONENT_10`/ `OPPONENT_20`: /`QUEUE`/ `QUEUE_5`/ `QUEUE_10`/ `QUEUE_20`; Can't be passed together with `price`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub price_match: Option<ModifyOrderPriceMatchEnum>,
///
/// The `recv_window` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub recv_window: Option<i64>,
}
impl ModifyOrderParams {
/// Create a builder for [`modify_order`].
///
/// Required parameters:
///
/// * `symbol` — String
/// * `side` — `SELL`, `BUY`
/// * `quantity` — Order quantity, cannot be sent with `closePosition=true`
/// * `price` — `rust_decimal::Decimal`
///
#[must_use]
pub fn builder(
symbol: String,
side: ModifyOrderSideEnum,
quantity: rust_decimal::Decimal,
price: rust_decimal::Decimal,
) -> ModifyOrderParamsBuilder {
ModifyOrderParamsBuilder::default()
.symbol(symbol)
.side(side)
.quantity(quantity)
.price(price)
}
}
/// Request parameters for the [`new_order`] operation.
///
/// This struct holds all of the inputs you can pass when calling
/// [`new_order`](#method.new_order).
#[derive(Clone, Debug, Builder)]
#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
pub struct NewOrderParams {
///
/// The `symbol` parameter.
///
/// This field is **required.
#[builder(setter(into))]
pub symbol: String,
/// `SELL`, `BUY`
///
/// This field is **required.
#[builder(setter(into))]
pub side: NewOrderSideEnum,
/// `LIMIT`, `MARKET`, `STOP`, `STOP_MARKET`, `TAKE_PROFIT`, `TAKE_PROFIT_MARKET`, `TRAILING_STOP_MARKET`
///
/// This field is **required.
#[builder(setter(into))]
pub r#type: NewOrderTypeEnum,
/// Unique WebSocket request ID.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub id: Option<String>,
/// Default `BOTH` for One-way Mode; `LONG` or `SHORT` for Hedge Mode. It must be sent in Hedge Mode.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub position_side: Option<NewOrderPositionSideEnum>,
///
/// The `time_in_force` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub time_in_force: Option<NewOrderTimeInForceEnum>,
/// Quantity measured by contract number, Cannot be sent with `closePosition`=`true`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub quantity: Option<rust_decimal::Decimal>,
/// `true` or `false`. default `false`. Cannot be sent in Hedge Mode; cannot be sent with `closePosition`=`true` (Close-All)
///
/// This field is **optional.
#[builder(setter(into), default)]
pub reduce_only: Option<String>,
///
/// The `price` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub price: Option<rust_decimal::Decimal>,
/// A unique id among open orders. Automatically generated if not sent. Can only be string following the rule: `^[\.A-Z\:/a-z0-9_-]{1,36}$`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub new_client_order_id: Option<String>,
/// Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub stop_price: Option<rust_decimal::Decimal>,
/// `true`, `false`;Close-All,used with `STOP_MARKET` or `TAKE_PROFIT_MARKET`.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub close_position: Option<String>,
/// Used with `TRAILING_STOP_MARKET` orders, default as the latest price(supporting different workingType)
///
/// This field is **optional.
#[builder(setter(into), default)]
pub activation_price: Option<rust_decimal::Decimal>,
/// Used with `TRAILING_STOP_MARKET` orders, min 0.1, max 10 where 1 for 1%
///
/// This field is **optional.
#[builder(setter(into), default)]
pub callback_rate: Option<rust_decimal::Decimal>,
/// stopPrice triggered by: "`MARK_PRICE`", "`CONTRACT_PRICE`". Default "`CONTRACT_PRICE`"
///
/// This field is **optional.
#[builder(setter(into), default)]
pub working_type: Option<NewOrderWorkingTypeEnum>,
/// "TRUE" or "FALSE", default "FALSE". Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub price_protect: Option<String>,
/// `ACK`,`RESULT`, default `ACK`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub new_order_resp_type: Option<NewOrderNewOrderRespTypeEnum>,
/// only available for `LIMIT`/`STOP`/`TAKE_PROFIT` order; can be set to `OPPONENT`/ `OPPONENT_5`/ `OPPONENT_10`/ `OPPONENT_20`: /`QUEUE`/ `QUEUE_5`/ `QUEUE_10`/ `QUEUE_20`; Can't be passed together with `price`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub price_match: Option<NewOrderPriceMatchEnum>,
/// `NONE`: No STP / `EXPIRE_TAKER`:expire taker order when STP triggers/ `EXPIRE_MAKER`:expire taker order when STP triggers/ `EXPIRE_BOTH`:expire both orders when STP triggers; default `NONE`
///
/// This field is **optional.
#[builder(setter(into), default)]
pub self_trade_prevention_mode: Option<NewOrderSelfTradePreventionModeEnum>,
///
/// The `recv_window` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub recv_window: Option<i64>,
}
impl NewOrderParams {
/// Create a builder for [`new_order`].
///
/// Required parameters:
///
/// * `symbol` — String
/// * `side` — `SELL`, `BUY`
/// * `r#type` — `LIMIT`, `MARKET`, `STOP`, `STOP_MARKET`, `TAKE_PROFIT`, `TAKE_PROFIT_MARKET`, `TRAILING_STOP_MARKET`
///
#[must_use]
pub fn builder(
symbol: String,
side: NewOrderSideEnum,
r#type: NewOrderTypeEnum,
) -> NewOrderParamsBuilder {
NewOrderParamsBuilder::default()
.symbol(symbol)
.side(side)
.r#type(r#type)
}
}
/// Request parameters for the [`position_information`] operation.
///
/// This struct holds all of the inputs you can pass when calling
/// [`position_information`](#method.position_information).
#[derive(Clone, Debug, Builder, Default)]
#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
pub struct PositionInformationParams {
/// Unique WebSocket request ID.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub id: Option<String>,
///
/// The `margin_asset` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub margin_asset: Option<String>,
///
/// The `pair` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub pair: Option<String>,
///
/// The `recv_window` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub recv_window: Option<i64>,
}
impl PositionInformationParams {
/// Create a builder for [`position_information`].
///
#[must_use]
pub fn builder() -> PositionInformationParamsBuilder {
PositionInformationParamsBuilder::default()
}
}
/// Request parameters for the [`query_order`] operation.
///
/// This struct holds all of the inputs you can pass when calling
/// [`query_order`](#method.query_order).
#[derive(Clone, Debug, Builder)]
#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
pub struct QueryOrderParams {
///
/// The `symbol` parameter.
///
/// This field is **required.
#[builder(setter(into))]
pub symbol: String,
/// Unique WebSocket request ID.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub id: Option<String>,
///
/// The `order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub order_id: Option<i64>,
///
/// The `orig_client_order_id` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub orig_client_order_id: Option<String>,
///
/// The `recv_window` parameter.
///
/// This field is **optional.
#[builder(setter(into), default)]
pub recv_window: Option<i64>,
}
impl QueryOrderParams {
/// Create a builder for [`query_order`].
///
/// Required parameters:
///
/// * `symbol` — String
///
#[must_use]
pub fn builder(symbol: String) -> QueryOrderParamsBuilder {
QueryOrderParamsBuilder::default().symbol(symbol)
}
}
#[async_trait]
impl TradeApi for TradeApiClient {
async fn cancel_order(
&self,
params: CancelOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::CancelOrderResponseResult>>> {
let CancelOrderParams {
symbol,
id,
order_id,
orig_client_order_id,
recv_window,
} = params;
let mut payload: BTreeMap<String, Value> = BTreeMap::new();
payload.insert("symbol".to_string(), serde_json::json!(symbol));
if let Some(value) = id {
payload.insert("id".to_string(), serde_json::json!(value));
}
if let Some(value) = order_id {
payload.insert("orderId".to_string(), serde_json::json!(value));
}
if let Some(value) = orig_client_order_id {
payload.insert("origClientOrderId".to_string(), serde_json::json!(value));
}
if let Some(value) = recv_window {
payload.insert("recvWindow".to_string(), serde_json::json!(value));
}
let payload = remove_empty_value(payload);
self.websocket_api_base
.send_message::<Box<models::CancelOrderResponseResult>>(
"/order.cancel".trim_start_matches('/'),
payload,
WebsocketMessageSendOptions::new().signed(),
)
.await
.map_err(anyhow::Error::from)?
.into_iter()
.next()
.ok_or(WebsocketError::NoResponse)
.map_err(anyhow::Error::from)
}
async fn modify_order(
&self,
params: ModifyOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::ModifyOrderResponseResult>>> {
let ModifyOrderParams {
symbol,
side,
quantity,
price,
id,
order_id,
orig_client_order_id,
price_match,
recv_window,
} = params;
let mut payload: BTreeMap<String, Value> = BTreeMap::new();
payload.insert("symbol".to_string(), serde_json::json!(symbol));
payload.insert("side".to_string(), serde_json::json!(side));
payload.insert("quantity".to_string(), serde_json::json!(quantity));
payload.insert("price".to_string(), serde_json::json!(price));
if let Some(value) = id {
payload.insert("id".to_string(), serde_json::json!(value));
}
if let Some(value) = order_id {
payload.insert("orderId".to_string(), serde_json::json!(value));
}
if let Some(value) = orig_client_order_id {
payload.insert("origClientOrderId".to_string(), serde_json::json!(value));
}
if let Some(value) = price_match {
payload.insert("priceMatch".to_string(), serde_json::json!(value));
}
if let Some(value) = recv_window {
payload.insert("recvWindow".to_string(), serde_json::json!(value));
}
let payload = remove_empty_value(payload);
self.websocket_api_base
.send_message::<Box<models::ModifyOrderResponseResult>>(
"/order.modify".trim_start_matches('/'),
payload,
WebsocketMessageSendOptions::new().signed(),
)
.await
.map_err(anyhow::Error::from)?
.into_iter()
.next()
.ok_or(WebsocketError::NoResponse)
.map_err(anyhow::Error::from)
}
async fn new_order(
&self,
params: NewOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::NewOrderResponseResult>>> {
let NewOrderParams {
symbol,
side,
r#type,
id,
position_side,
time_in_force,
quantity,
reduce_only,
price,
new_client_order_id,
stop_price,
close_position,
activation_price,
callback_rate,
working_type,
price_protect,
new_order_resp_type,
price_match,
self_trade_prevention_mode,
recv_window,
} = params;
let mut payload: BTreeMap<String, Value> = BTreeMap::new();
payload.insert("symbol".to_string(), serde_json::json!(symbol));
payload.insert("side".to_string(), serde_json::json!(side));
payload.insert("type".to_string(), serde_json::json!(r#type));
if let Some(value) = id {
payload.insert("id".to_string(), serde_json::json!(value));
}
if let Some(value) = position_side {
payload.insert("positionSide".to_string(), serde_json::json!(value));
}
if let Some(value) = time_in_force {
payload.insert("timeInForce".to_string(), serde_json::json!(value));
}
if let Some(value) = quantity {
payload.insert("quantity".to_string(), serde_json::json!(value));
}
if let Some(value) = reduce_only {
payload.insert("reduceOnly".to_string(), serde_json::json!(value));
}
if let Some(value) = price {
payload.insert("price".to_string(), serde_json::json!(value));
}
if let Some(value) = new_client_order_id {
payload.insert("newClientOrderId".to_string(), serde_json::json!(value));
}
if let Some(value) = stop_price {
payload.insert("stopPrice".to_string(), serde_json::json!(value));
}
if let Some(value) = close_position {
payload.insert("closePosition".to_string(), serde_json::json!(value));
}
if let Some(value) = activation_price {
payload.insert("activationPrice".to_string(), serde_json::json!(value));
}
if let Some(value) = callback_rate {
payload.insert("callbackRate".to_string(), serde_json::json!(value));
}
if let Some(value) = working_type {
payload.insert("workingType".to_string(), serde_json::json!(value));
}
if let Some(value) = price_protect {
payload.insert("priceProtect".to_string(), serde_json::json!(value));
}
if let Some(value) = new_order_resp_type {
payload.insert("newOrderRespType".to_string(), serde_json::json!(value));
}
if let Some(value) = price_match {
payload.insert("priceMatch".to_string(), serde_json::json!(value));
}
if let Some(value) = self_trade_prevention_mode {
payload.insert(
"selfTradePreventionMode".to_string(),
serde_json::json!(value),
);
}
if let Some(value) = recv_window {
payload.insert("recvWindow".to_string(), serde_json::json!(value));
}
let payload = remove_empty_value(payload);
self.websocket_api_base
.send_message::<Box<models::NewOrderResponseResult>>(
"/order.place".trim_start_matches('/'),
payload,
WebsocketMessageSendOptions::new().signed(),
)
.await
.map_err(anyhow::Error::from)?
.into_iter()
.next()
.ok_or(WebsocketError::NoResponse)
.map_err(anyhow::Error::from)
}
async fn position_information(
&self,
params: PositionInformationParams,
) -> anyhow::Result<WebsocketApiResponse<Vec<models::PositionInformationResponseResultInner>>>
{
let PositionInformationParams {
id,
margin_asset,
pair,
recv_window,
} = params;
let mut payload: BTreeMap<String, Value> = BTreeMap::new();
if let Some(value) = id {
payload.insert("id".to_string(), serde_json::json!(value));
}
if let Some(value) = margin_asset {
payload.insert("marginAsset".to_string(), serde_json::json!(value));
}
if let Some(value) = pair {
payload.insert("pair".to_string(), serde_json::json!(value));
}
if let Some(value) = recv_window {
payload.insert("recvWindow".to_string(), serde_json::json!(value));
}
let payload = remove_empty_value(payload);
self.websocket_api_base
.send_message::<Vec<models::PositionInformationResponseResultInner>>(
"/account.position".trim_start_matches('/'),
payload,
WebsocketMessageSendOptions::new().signed(),
)
.await
.map_err(anyhow::Error::from)?
.into_iter()
.next()
.ok_or(WebsocketError::NoResponse)
.map_err(anyhow::Error::from)
}
async fn query_order(
&self,
params: QueryOrderParams,
) -> anyhow::Result<WebsocketApiResponse<Box<models::QueryOrderResponseResult>>> {
let QueryOrderParams {
symbol,
id,
order_id,
orig_client_order_id,
recv_window,
} = params;
let mut payload: BTreeMap<String, Value> = BTreeMap::new();
payload.insert("symbol".to_string(), serde_json::json!(symbol));
if let Some(value) = id {
payload.insert("id".to_string(), serde_json::json!(value));
}
if let Some(value) = order_id {
payload.insert("orderId".to_string(), serde_json::json!(value));
}
if let Some(value) = orig_client_order_id {
payload.insert("origClientOrderId".to_string(), serde_json::json!(value));
}
if let Some(value) = recv_window {
payload.insert("recvWindow".to_string(), serde_json::json!(value));
}
let payload = remove_empty_value(payload);
self.websocket_api_base
.send_message::<Box<models::QueryOrderResponseResult>>(
"/order.status".trim_start_matches('/'),
payload,
WebsocketMessageSendOptions::new().signed(),
)
.await
.map_err(anyhow::Error::from)?
.into_iter()
.next()
.ok_or(WebsocketError::NoResponse)
.map_err(anyhow::Error::from)
}
}
#[cfg(all(test, feature = "derivatives_trading_coin_futures"))]
mod tests {
use super::*;
use crate::TOKIO_SHARED_RT;
use crate::common::websocket::{WebsocketApi, WebsocketConnection, WebsocketHandler};
use crate::config::ConfigurationWebsocketApi;
use crate::errors::WebsocketError;
use crate::models::WebsocketApiRateLimit;
use serde_json::{Value, json};
use tokio::spawn;
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
use tokio::time::{Duration, timeout};
use tokio_tungstenite::tungstenite::Message;
async fn setup() -> (
Arc<WebsocketApi>,
Arc<WebsocketConnection>,
UnboundedReceiver<Message>,
) {
let conn = WebsocketConnection::new("test-conn");
let (tx, rx) = unbounded_channel::<Message>();
{
let mut conn_state = conn.state.lock().await;
conn_state.ws_write_tx = Some(tx);
}
let config = ConfigurationWebsocketApi::builder()
.api_key("key")
.api_secret("secret")
.build()
.expect("Failed to build configuration");
let ws_api = WebsocketApi::new(config, vec![conn.clone()]);
conn.set_handler(ws_api.clone() as Arc<dyn WebsocketHandler>)
.await;
ws_api.clone().connect().await.unwrap();
(ws_api, conn, rx)
}
#[test]
fn cancel_order_success() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = CancelOrderParams::builder("symbol_example".to_string(),).build().unwrap();
client.cancel_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap();
assert_eq!(v["method"], "/order.cancel".trim_start_matches('/'));
let mut resp_json: Value = serde_json::from_str(r#"{"id":"a8627ea5-8b9f-452f-90ae-4136f2b442e2","status":200,"result":{"orderId":333245211,"symbol":"BTCUSD_PERP","pair":"BTCUSD","status":"CANCELED","clientOrderId":"5SztZiGFAxgAqw4J9EN9fA","price":"51000","avgPrice":"0.00","origQty":"1","executedQty":"0","cumQty":"0","cumBase":"0","timeInForce":"GTC","type":"LIMIT","reduceOnly":false,"closePosition":false,"side":"BUY","positionSide":"BOTH","stopPrice":"0","workingType":"CONTRACT_PRICE","priceProtect":false,"origType":"LIMIT","updateTime":1728416138285},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6}]}"#).unwrap();
resp_json["id"] = id.into();
let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
let expected_data: Box<models::CancelOrderResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
let empty_array = Value::Array(vec![]);
let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
match raw_rate_limits.as_array() {
Some(arr) if arr.is_empty() => None,
Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
None => None,
};
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
let response_rate_limits = response.rate_limits.clone();
let response_data = response.data().expect("deserialize data");
assert_eq!(response_rate_limits, expected_rate_limits);
assert_eq!(response_data, expected_data);
});
}
#[test]
fn cancel_order_error_response() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = tokio::spawn(async move {
let params = CancelOrderParams::builder("symbol_example".to_string(),).build().unwrap();
client.cancel_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap().to_string();
let resp_json = json!({
"id": id,
"status": 400,
"error": {
"code": -2010,
"msg": "Account has insufficient balance for requested action.",
},
"rateLimits": [
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"intervalNum": 10,
"limit": 50,
"count": 13
},
],
});
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let join = timeout(Duration::from_secs(1), handle).await.unwrap();
match join {
Ok(Err(e)) => {
let msg = e.to_string();
assert!(
msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
"Expected error msg to contain server error, got: {msg}"
);
}
Ok(Ok(_)) => panic!("Expected error"),
Err(_) => panic!("Task panicked"),
}
});
}
#[test]
fn cancel_order_request_timeout() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, _conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = CancelOrderParams::builder("symbol_example".to_string())
.build()
.unwrap();
client.cancel_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("send should occur")
.expect("channel closed");
let Message::Text(text) = sent else {
panic!("expected Message Text")
};
let _: Value = serde_json::from_str(&text).unwrap();
let result = handle.await.expect("task completed");
match result {
Err(e) => {
if let Some(inner) = e.downcast_ref::<WebsocketError>() {
assert!(matches!(inner, WebsocketError::Timeout));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
Ok(_) => panic!("Expected timeout error"),
}
});
}
#[test]
fn modify_order_success() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = ModifyOrderParams::builder("symbol_example".to_string(),ModifyOrderSideEnum::Buy,dec!(1.0),dec!(1.0),).build().unwrap();
client.modify_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap();
assert_eq!(v["method"], "/order.modify".trim_start_matches('/'));
let mut resp_json: Value = serde_json::from_str(r#"{"id":"88601d02-bd0d-430d-8733-2708a569ebda","status":200,"result":{"orderId":333245211,"symbol":"BTCUSD_PERP","pair":"BTCUSD","status":"NEW","clientOrderId":"5SztZiGFAxgAqw4J9EN9fA","price":"51000","avgPrice":"0.00","origQty":"1","executedQty":"0","cumQty":"0","cumBase":"0","timeInForce":"GTC","type":"LIMIT","reduceOnly":false,"closePosition":false,"side":"BUY","positionSide":"BOTH","stopPrice":"0","workingType":"CONTRACT_PRICE","priceProtect":false,"origType":"LIMIT","updateTime":1728415765493},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6},{"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":1200,"count":1}]}"#).unwrap();
resp_json["id"] = id.into();
let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
let expected_data: Box<models::ModifyOrderResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
let empty_array = Value::Array(vec![]);
let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
match raw_rate_limits.as_array() {
Some(arr) if arr.is_empty() => None,
Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
None => None,
};
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
let response_rate_limits = response.rate_limits.clone();
let response_data = response.data().expect("deserialize data");
assert_eq!(response_rate_limits, expected_rate_limits);
assert_eq!(response_data, expected_data);
});
}
#[test]
fn modify_order_error_response() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = tokio::spawn(async move {
let params = ModifyOrderParams::builder("symbol_example".to_string(),ModifyOrderSideEnum::Buy,dec!(1.0),dec!(1.0),).build().unwrap();
client.modify_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap().to_string();
let resp_json = json!({
"id": id,
"status": 400,
"error": {
"code": -2010,
"msg": "Account has insufficient balance for requested action.",
},
"rateLimits": [
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"intervalNum": 10,
"limit": 50,
"count": 13
},
],
});
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let join = timeout(Duration::from_secs(1), handle).await.unwrap();
match join {
Ok(Err(e)) => {
let msg = e.to_string();
assert!(
msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
"Expected error msg to contain server error, got: {msg}"
);
}
Ok(Ok(_)) => panic!("Expected error"),
Err(_) => panic!("Task panicked"),
}
});
}
#[test]
fn modify_order_request_timeout() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, _conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = ModifyOrderParams::builder(
"symbol_example".to_string(),
ModifyOrderSideEnum::Buy,
dec!(1.0),
dec!(1.0),
)
.build()
.unwrap();
client.modify_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("send should occur")
.expect("channel closed");
let Message::Text(text) = sent else {
panic!("expected Message Text")
};
let _: Value = serde_json::from_str(&text).unwrap();
let result = handle.await.expect("task completed");
match result {
Err(e) => {
if let Some(inner) = e.downcast_ref::<WebsocketError>() {
assert!(matches!(inner, WebsocketError::Timeout));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
Ok(_) => panic!("Expected timeout error"),
}
});
}
#[test]
fn new_order_success() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = NewOrderParams::builder("symbol_example".to_string(),NewOrderSideEnum::Buy,NewOrderTypeEnum::Limit,).build().unwrap();
client.new_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap();
assert_eq!(v["method"], "/order.place".trim_start_matches('/'));
let mut resp_json: Value = serde_json::from_str(r#"{"id":"60fa4366-f96e-42fe-a82b-f819952c6db4","status":200,"result":{"orderId":333245211,"symbol":"BTCUSD_PERP","pair":"BTCUSD","status":"NEW","clientOrderId":"5SztZiGFAxgAqw4J9EN9fA","price":"50000","avgPrice":"0.00","origQty":"1","executedQty":"0","cumQty":"0","cumBase":"0","timeInForce":"GTC","type":"LIMIT","reduceOnly":false,"closePosition":false,"side":"BUY","positionSide":"BOTH","stopPrice":"0","workingType":"CONTRACT_PRICE","priceProtect":false,"origType":"LIMIT","updateTime":1728413795125},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6},{"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":1200,"count":1}]}"#).unwrap();
resp_json["id"] = id.into();
let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
let expected_data: Box<models::NewOrderResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
let empty_array = Value::Array(vec![]);
let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
match raw_rate_limits.as_array() {
Some(arr) if arr.is_empty() => None,
Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
None => None,
};
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
let response_rate_limits = response.rate_limits.clone();
let response_data = response.data().expect("deserialize data");
assert_eq!(response_rate_limits, expected_rate_limits);
assert_eq!(response_data, expected_data);
});
}
#[test]
fn new_order_error_response() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = tokio::spawn(async move {
let params = NewOrderParams::builder("symbol_example".to_string(),NewOrderSideEnum::Buy,NewOrderTypeEnum::Limit,).build().unwrap();
client.new_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap().to_string();
let resp_json = json!({
"id": id,
"status": 400,
"error": {
"code": -2010,
"msg": "Account has insufficient balance for requested action.",
},
"rateLimits": [
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"intervalNum": 10,
"limit": 50,
"count": 13
},
],
});
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let join = timeout(Duration::from_secs(1), handle).await.unwrap();
match join {
Ok(Err(e)) => {
let msg = e.to_string();
assert!(
msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
"Expected error msg to contain server error, got: {msg}"
);
}
Ok(Ok(_)) => panic!("Expected error"),
Err(_) => panic!("Task panicked"),
}
});
}
#[test]
fn new_order_request_timeout() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, _conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = NewOrderParams::builder(
"symbol_example".to_string(),
NewOrderSideEnum::Buy,
NewOrderTypeEnum::Limit,
)
.build()
.unwrap();
client.new_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("send should occur")
.expect("channel closed");
let Message::Text(text) = sent else {
panic!("expected Message Text")
};
let _: Value = serde_json::from_str(&text).unwrap();
let result = handle.await.expect("task completed");
match result {
Err(e) => {
if let Some(inner) = e.downcast_ref::<WebsocketError>() {
assert!(matches!(inner, WebsocketError::Timeout));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
Ok(_) => panic!("Expected timeout error"),
}
});
}
#[test]
fn position_information_success() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = PositionInformationParams::builder().build().unwrap();
client.position_information(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap();
assert_eq!(v["method"], "/account.position".trim_start_matches('/'));
let mut resp_json: Value = serde_json::from_str(r#"{"id":"233b8741-a96d-48e8-8ce1-160f43548aeb","status":200,"result":[{"symbol":"BTCUSD_PERP","positionAmt":"0","entryPrice":"0.00000000","markPrice":"62297.60417296","unRealizedProfit":"0.00000000","liquidationPrice":"0","leverage":"7","maxQty":"100","marginType":"cross","isolatedMargin":"0.00000000","isAutoAddMargin":"false","positionSide":"BOTH","notionalValue":"0","isolatedWallet":"0","updateTime":1726731195634,"breakEvenPrice":"0.00000000"}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":10}]}"#).unwrap();
resp_json["id"] = id.into();
let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
let expected_data: Vec<models::PositionInformationResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
let empty_array = Value::Array(vec![]);
let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
match raw_rate_limits.as_array() {
Some(arr) if arr.is_empty() => None,
Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
None => None,
};
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
let response_rate_limits = response.rate_limits.clone();
let response_data = response.data().expect("deserialize data");
assert_eq!(response_rate_limits, expected_rate_limits);
assert_eq!(response_data, expected_data);
});
}
#[test]
fn position_information_error_response() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = tokio::spawn(async move {
let params = PositionInformationParams::builder().build().unwrap();
client.position_information(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap().to_string();
let resp_json = json!({
"id": id,
"status": 400,
"error": {
"code": -2010,
"msg": "Account has insufficient balance for requested action.",
},
"rateLimits": [
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"intervalNum": 10,
"limit": 50,
"count": 13
},
],
});
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let join = timeout(Duration::from_secs(1), handle).await.unwrap();
match join {
Ok(Err(e)) => {
let msg = e.to_string();
assert!(
msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
"Expected error msg to contain server error, got: {msg}"
);
}
Ok(Ok(_)) => panic!("Expected error"),
Err(_) => panic!("Task panicked"),
}
});
}
#[test]
fn position_information_request_timeout() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, _conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = PositionInformationParams::builder().build().unwrap();
client.position_information(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("send should occur")
.expect("channel closed");
let Message::Text(text) = sent else {
panic!("expected Message Text")
};
let _: Value = serde_json::from_str(&text).unwrap();
let result = handle.await.expect("task completed");
match result {
Err(e) => {
if let Some(inner) = e.downcast_ref::<WebsocketError>() {
assert!(matches!(inner, WebsocketError::Timeout));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
Ok(_) => panic!("Expected timeout error"),
}
});
}
#[test]
fn query_order_success() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = QueryOrderParams::builder("symbol_example".to_string(),).build().unwrap();
client.query_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap();
assert_eq!(v["method"], "/order.status".trim_start_matches('/'));
let mut resp_json: Value = serde_json::from_str(r#"{"id":"0ce5d070-a5e5-4ff2-b57f-1556741a4204","status":200,"result":{"orderId":328999071,"symbol":"BTCUSD_PERP","pair":"BTCUSD","status":"NEW","clientOrderId":"ArY8Ng1rln0s9x3fclmAHy","price":"58000","avgPrice":"0.00","origQty":"1","executedQty":"0","cumBase":"0","timeInForce":"GTC","type":"LIMIT","reduceOnly":false,"closePosition":false,"side":"BUY","positionSide":"LONG","stopPrice":"0","workingType":"CONTRACT_PRICE","priceProtect":false,"origType":"LIMIT","selfTradePreventionMode":"EXPIRE_TAKER","time":1733740063619,"updateTime":1733740063619,"priceMatch":"NONE"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6}]}"#).unwrap();
resp_json["id"] = id.into();
let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
let expected_data: Box<models::QueryOrderResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
let empty_array = Value::Array(vec![]);
let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
match raw_rate_limits.as_array() {
Some(arr) if arr.is_empty() => None,
Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
None => None,
};
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
let response_rate_limits = response.rate_limits.clone();
let response_data = response.data().expect("deserialize data");
assert_eq!(response_rate_limits, expected_rate_limits);
assert_eq!(response_data, expected_data);
});
}
#[test]
fn query_order_error_response() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = tokio::spawn(async move {
let params = QueryOrderParams::builder("symbol_example".to_string(),).build().unwrap();
client.query_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
let Message::Text(text) = sent else { panic!() };
let v: Value = serde_json::from_str(&text).unwrap();
let id = v["id"].as_str().unwrap().to_string();
let resp_json = json!({
"id": id,
"status": 400,
"error": {
"code": -2010,
"msg": "Account has insufficient balance for requested action.",
},
"rateLimits": [
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"intervalNum": 10,
"limit": 50,
"count": 13
},
],
});
WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
let join = timeout(Duration::from_secs(1), handle).await.unwrap();
match join {
Ok(Err(e)) => {
let msg = e.to_string();
assert!(
msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
"Expected error msg to contain server error, got: {msg}"
);
}
Ok(Ok(_)) => panic!("Expected error"),
Err(_) => panic!("Task panicked"),
}
});
}
#[test]
fn query_order_request_timeout() {
TOKIO_SHARED_RT.block_on(async {
let (ws_api, _conn, mut rx) = setup().await;
let client = TradeApiClient::new(ws_api.clone());
let handle = spawn(async move {
let params = QueryOrderParams::builder("symbol_example".to_string())
.build()
.unwrap();
client.query_order(params).await
});
let sent = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("send should occur")
.expect("channel closed");
let Message::Text(text) = sent else {
panic!("expected Message Text")
};
let _: Value = serde_json::from_str(&text).unwrap();
let result = handle.await.expect("task completed");
match result {
Err(e) => {
if let Some(inner) = e.downcast_ref::<WebsocketError>() {
assert!(matches!(inner, WebsocketError::Timeout));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
Ok(_) => panic!("Expected timeout error"),
}
});
}
}