use serde::{Deserialize, Serialize};
use crate::client::OkxClient;
use crate::error::Error;
use crate::model::{NumberString, OrderSide, OrderState, OrderType, PositionSide, TradeMode};
use crate::transport::Transport;
const ORDER: &str = "/api/v5/trade/order";
const BATCH_ORDERS: &str = "/api/v5/trade/batch-orders";
const CANCEL_ORDER: &str = "/api/v5/trade/cancel-order";
const CANCEL_BATCH_ORDERS: &str = "/api/v5/trade/cancel-batch-orders";
const AMEND_ORDER: &str = "/api/v5/trade/amend-order";
const AMEND_BATCH_ORDERS: &str = "/api/v5/trade/amend-batch-orders";
const CLOSE_POSITION: &str = "/api/v5/trade/close-position";
const ORDERS_PENDING: &str = "/api/v5/trade/orders-pending";
const ORDERS_HISTORY: &str = "/api/v5/trade/orders-history";
const ORDERS_HISTORY_ARCHIVE: &str = "/api/v5/trade/orders-history-archive";
const FILLS: &str = "/api/v5/trade/fills";
const FILLS_HISTORY: &str = "/api/v5/trade/fills-history";
pub struct Trade<'a, T> {
client: &'a OkxClient<T>,
}
impl<'a, T: Transport> Trade<'a, T> {
pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
Self { client }
}
pub async fn place_order(
&self,
request: &PlaceOrderRequest,
) -> Result<Vec<PlaceOrderResult>, Error> {
self.client.post(ORDER, request, true).await
}
pub async fn place_multiple_orders(
&self,
requests: &[PlaceOrderRequest],
) -> Result<Vec<PlaceOrderResult>, Error> {
self.client.post(BATCH_ORDERS, &requests, true).await
}
pub async fn cancel_order(
&self,
inst_id: &str,
ord_id: &str,
) -> Result<Vec<CancelOrderResult>, Error> {
let body = CancelOrderBody { inst_id, ord_id };
self.client.post(CANCEL_ORDER, &body, true).await
}
pub async fn cancel_multiple_orders(
&self,
requests: &[CancelOrderRequest],
) -> Result<Vec<CancelOrderResult>, Error> {
self.client.post(CANCEL_BATCH_ORDERS, &requests, true).await
}
pub async fn amend_order(
&self,
request: &AmendOrderRequest,
) -> Result<Vec<AmendOrderResult>, Error> {
self.client.post(AMEND_ORDER, request, true).await
}
pub async fn amend_multiple_orders(
&self,
requests: &[AmendOrderRequest],
) -> Result<Vec<AmendOrderResult>, Error> {
self.client.post(AMEND_BATCH_ORDERS, &requests, true).await
}
pub async fn close_positions(
&self,
request: &ClosePositionRequest,
) -> Result<Vec<ClosePositionResult>, Error> {
self.client.post(CLOSE_POSITION, request, true).await
}
pub async fn get_order(&self, inst_id: &str, ord_id: &str) -> Result<Vec<Order>, Error> {
let query = GetOrderQuery { inst_id, ord_id };
self.client.get(ORDER, &query, true).await
}
pub async fn get_order_list(&self, request: &OrderListRequest) -> Result<Vec<Order>, Error> {
self.client.get(ORDERS_PENDING, request, true).await
}
pub async fn get_orders_history(
&self,
request: &OrderHistoryRequest,
) -> Result<Vec<Order>, Error> {
self.client.get(ORDERS_HISTORY, request, true).await
}
pub async fn get_orders_history_archive(
&self,
request: &OrderHistoryRequest,
) -> Result<Vec<Order>, Error> {
self.client.get(ORDERS_HISTORY_ARCHIVE, request, true).await
}
pub async fn get_fills(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
self.client.get(FILLS, request, true).await
}
pub async fn get_fills_history(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
self.client.get(FILLS_HISTORY, request, true).await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PlaceOrderRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "tdMode")]
td_mode: TradeMode,
side: OrderSide,
#[serde(rename = "ordType")]
ord_type: OrderType,
sz: String,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
#[serde(rename = "px", skip_serializing_if = "Option::is_none")]
px: Option<String>,
#[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
pos_side: Option<PositionSide>,
#[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
cl_ord_id: Option<String>,
#[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
reduce_only: Option<bool>,
#[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
tgt_ccy: Option<String>,
}
impl PlaceOrderRequest {
pub fn new(
inst_id: impl Into<String>,
td_mode: TradeMode,
side: OrderSide,
ord_type: OrderType,
sz: impl Into<String>,
) -> Self {
Self {
inst_id: inst_id.into(),
td_mode,
side,
ord_type,
sz: sz.into(),
ccy: None,
tag: None,
px: None,
pos_side: None,
cl_ord_id: None,
reduce_only: None,
tgt_ccy: None,
}
}
pub fn price(mut self, px: impl Into<String>) -> Self {
self.px = Some(px.into());
self
}
pub fn position_side(mut self, pos_side: PositionSide) -> Self {
self.pos_side = Some(pos_side);
self
}
pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
self.cl_ord_id = Some(cl_ord_id.into());
self
}
pub fn reduce_only(mut self, reduce_only: bool) -> Self {
self.reduce_only = Some(reduce_only);
self
}
pub fn target_ccy(mut self, tgt_ccy: impl Into<String>) -> Self {
self.tgt_ccy = Some(tgt_ccy.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
}
#[derive(Serialize)]
struct CancelOrderBody<'a> {
#[serde(rename = "instId")]
inst_id: &'a str,
#[serde(rename = "ordId")]
ord_id: &'a str,
}
#[derive(Serialize)]
struct GetOrderQuery<'a> {
#[serde(rename = "instId")]
inst_id: &'a str,
#[serde(rename = "ordId")]
ord_id: &'a str,
}
#[derive(Debug, Clone, Serialize)]
pub struct CancelOrderRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
ord_id: Option<String>,
#[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
cl_ord_id: Option<String>,
}
impl CancelOrderRequest {
pub fn by_order_id(inst_id: impl Into<String>, ord_id: impl Into<String>) -> Self {
Self {
inst_id: inst_id.into(),
ord_id: Some(ord_id.into()),
cl_ord_id: None,
}
}
pub fn by_client_order_id(inst_id: impl Into<String>, cl_ord_id: impl Into<String>) -> Self {
Self {
inst_id: inst_id.into(),
ord_id: None,
cl_ord_id: Some(cl_ord_id.into()),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AmendOrderRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
ord_id: Option<String>,
#[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
cl_ord_id: Option<String>,
#[serde(rename = "reqId", skip_serializing_if = "Option::is_none")]
req_id: Option<String>,
#[serde(rename = "cxlOnFail", skip_serializing_if = "Option::is_none")]
cxl_on_fail: Option<bool>,
#[serde(rename = "newSz", skip_serializing_if = "Option::is_none")]
new_sz: Option<String>,
#[serde(rename = "newPx", skip_serializing_if = "Option::is_none")]
new_px: Option<String>,
}
impl AmendOrderRequest {
pub fn new(inst_id: impl Into<String>) -> Self {
Self {
inst_id: inst_id.into(),
ord_id: None,
cl_ord_id: None,
req_id: None,
cxl_on_fail: None,
new_sz: None,
new_px: None,
}
}
pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
self.ord_id = Some(ord_id.into());
self
}
pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
self.cl_ord_id = Some(cl_ord_id.into());
self
}
pub fn request_id(mut self, req_id: impl Into<String>) -> Self {
self.req_id = Some(req_id.into());
self
}
pub fn cancel_on_fail(mut self, cxl_on_fail: bool) -> Self {
self.cxl_on_fail = Some(cxl_on_fail);
self
}
pub fn new_size(mut self, new_sz: impl Into<String>) -> Self {
self.new_sz = Some(new_sz.into());
self
}
pub fn new_price(mut self, new_px: impl Into<String>) -> Self {
self.new_px = Some(new_px.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ClosePositionRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "mgnMode")]
mgn_mode: TradeMode,
#[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
pos_side: Option<PositionSide>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "autoCxl", skip_serializing_if = "Option::is_none")]
auto_cancel: Option<bool>,
#[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
}
impl ClosePositionRequest {
pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
Self {
inst_id: inst_id.into(),
mgn_mode,
pos_side: None,
ccy: None,
auto_cancel: None,
cl_ord_id: None,
tag: None,
}
}
pub fn position_side(mut self, pos_side: PositionSide) -> Self {
self.pos_side = Some(pos_side);
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn auto_cancel(mut self, auto_cancel: bool) -> Self {
self.auto_cancel = Some(auto_cancel);
self
}
pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
self.cl_ord_id = Some(cl_ord_id.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct OrderListRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<crate::model::InstType>,
#[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
underlying: Option<String>,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
ord_type: Option<OrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
state: Option<OrderState>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
#[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
inst_family: Option<String>,
}
impl OrderListRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
self.underlying = Some(underlying.into());
self
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn order_type(mut self, ord_type: OrderType) -> Self {
self.ord_type = Some(ord_type);
self
}
pub fn state(mut self, state: OrderState) -> Self {
self.state = Some(state);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
self.inst_family = Some(inst_family.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct OrderHistoryRequest {
#[serde(flatten)]
base: OrderListRequest,
#[serde(skip_serializing_if = "Option::is_none")]
begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<String>,
}
impl OrderHistoryRequest {
pub fn new(inst_type: crate::model::InstType) -> Self {
Self {
base: OrderListRequest::new().inst_type(inst_type),
begin: None,
end: None,
}
}
pub fn filters(mut self, base: OrderListRequest) -> Self {
self.base = base;
self
}
pub fn begin(mut self, begin: impl Into<String>) -> Self {
self.begin = Some(begin.into());
self
}
pub fn end(mut self, end: impl Into<String>) -> Self {
self.end = Some(end.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct FillsRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<crate::model::InstType>,
#[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
underlying: Option<String>,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
#[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
inst_family: Option<String>,
}
impl FillsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
self.underlying = Some(underlying.into());
self
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
self.ord_id = Some(ord_id.into());
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn begin(mut self, begin: impl Into<String>) -> Self {
self.begin = Some(begin.into());
self
}
pub fn end(mut self, end: impl Into<String>) -> Self {
self.end = Some(end.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
self.inst_family = Some(inst_family.into());
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlaceOrderResult {
#[serde(default)]
pub ord_id: String,
#[serde(default)]
pub cl_ord_id: String,
#[serde(default)]
pub tag: String,
pub s_code: String,
#[serde(default)]
pub s_msg: String,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CancelOrderResult {
#[serde(default)]
pub ord_id: String,
#[serde(default)]
pub cl_ord_id: String,
pub s_code: String,
#[serde(default)]
pub s_msg: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AmendOrderResult {
#[serde(default)]
pub ord_id: String,
#[serde(default)]
pub cl_ord_id: String,
#[serde(default)]
pub req_id: String,
pub s_code: String,
#[serde(default)]
pub s_msg: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ClosePositionResult {
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub pos_side: String,
#[serde(default)]
pub cl_ord_id: String,
#[serde(default)]
pub tag: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Order {
pub inst_id: String,
pub ord_id: String,
#[serde(default)]
pub cl_ord_id: String,
#[serde(default)]
pub px: NumberString,
#[serde(default)]
pub sz: NumberString,
pub ord_type: OrderType,
pub side: OrderSide,
pub pos_side: PositionSide,
pub td_mode: TradeMode,
#[serde(default)]
pub acc_fill_sz: NumberString,
#[serde(default)]
pub avg_px: NumberString,
pub state: OrderState,
#[serde(default)]
pub c_time: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Fill {
#[serde(default)]
pub inst_type: String,
pub inst_id: String,
#[serde(default)]
pub trade_id: String,
#[serde(default)]
pub ord_id: String,
#[serde(default)]
pub fill_px: NumberString,
#[serde(default)]
pub fill_sz: NumberString,
pub side: OrderSide,
pub ord_type: OrderType,
#[serde(default)]
pub fee_ccy: String,
#[serde(default)]
pub fee: NumberString,
#[serde(default)]
pub ts: NumberString,
}
#[cfg(test)]
mod tests {
use crate::model::{OrderSide, OrderType, TradeMode};
use crate::test_util::MockTransport;
use crate::{Credentials, OkxClient};
use super::PlaceOrderRequest;
fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
OkxClient::with_transport(mock)
.credentials(Credentials::new("key", "secret", "pass"))
.build()
}
#[tokio::test]
async fn place_order_posts_signed_json_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","tag":"","sCode":"0","sMsg":"","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = PlaceOrderRequest::new(
"BTC-USDT",
TradeMode::Cash,
OrderSide::Buy,
OrderType::Limit,
"0.01",
)
.price("42000")
.client_order_id("b1");
let result = client.trade().place_order(&request).await.unwrap();
assert_eq!(result[0].ord_id, "312");
assert_eq!(result[0].s_code, "0");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/trade/order"));
assert!(req.is_signed());
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instId"], "BTC-USDT");
assert_eq!(sent["tdMode"], "cash");
assert_eq!(sent["side"], "buy");
assert_eq!(sent["ordType"], "limit");
assert_eq!(sent["sz"], "0.01");
assert_eq!(sent["px"], "42000");
assert_eq!(sent["clOrdId"], "b1");
assert!(sent.get("reduceOnly").is_none());
}
#[tokio::test]
async fn place_multiple_orders_posts_array_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""},
{"ordId":"313","clOrdId":"b2","sCode":"0","sMsg":""}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let requests = vec![
PlaceOrderRequest::new(
"BTC-USDT",
TradeMode::Cash,
OrderSide::Buy,
OrderType::Limit,
"0.01",
)
.price("42000")
.client_order_id("b1"),
PlaceOrderRequest::new(
"BTC-USDT",
TradeMode::Cash,
OrderSide::Sell,
OrderType::Limit,
"0.02",
)
.price("43000")
.client_order_id("b2"),
];
let result = client
.trade()
.place_multiple_orders(&requests)
.await
.unwrap();
assert_eq!(result[1].ord_id, "313");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/trade/batch-orders"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent[0]["clOrdId"], "b1");
assert_eq!(sent[1]["side"], "sell");
assert!(req.is_signed());
}
#[tokio::test]
async fn cancel_order_posts_ids() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client
.trade()
.cancel_order("BTC-USDT", "312")
.await
.unwrap();
assert_eq!(result[0].ord_id, "312");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instId"], "BTC-USDT");
assert_eq!(sent["ordId"], "312");
}
#[tokio::test]
async fn cancel_multiple_orders_posts_array_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let requests = vec![super::CancelOrderRequest::by_order_id("BTC-USDT", "312")];
let result = client
.trade()
.cancel_multiple_orders(&requests)
.await
.unwrap();
assert_eq!(result[0].s_code, "0");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/trade/cancel-batch-orders"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent[0]["instId"], "BTC-USDT");
assert_eq!(sent[0]["ordId"], "312");
assert!(sent[0].get("clOrdId").is_none());
assert!(req.is_signed());
}
#[tokio::test]
async fn amend_order_posts_builder_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::AmendOrderRequest::new("BTC-USDT")
.order_id("312")
.request_id("r1")
.new_price("42100");
let result = client.trade().amend_order(&request).await.unwrap();
assert_eq!(result[0].req_id, "r1");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/trade/amend-order"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instId"], "BTC-USDT");
assert_eq!(sent["ordId"], "312");
assert_eq!(sent["newPx"], "42100");
assert!(sent.get("newSz").is_none());
assert!(req.is_signed());
}
#[tokio::test]
async fn amend_multiple_orders_posts_array_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let requests = vec![
super::AmendOrderRequest::new("BTC-USDT")
.client_order_id("b1")
.new_size("0.03"),
];
let result = client
.trade()
.amend_multiple_orders(&requests)
.await
.unwrap();
assert_eq!(result[0].s_code, "0");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/trade/amend-batch-orders"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent[0]["clOrdId"], "b1");
assert_eq!(sent[0]["newSz"], "0.03");
assert!(req.is_signed());
}
#[tokio::test]
async fn close_positions_posts_builder_body() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT-SWAP","posSide":"long","clOrdId":"close1","tag":"t"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::ClosePositionRequest::new("BTC-USDT-SWAP", TradeMode::Cross)
.position_side(crate::model::PositionSide::Long)
.auto_cancel(true)
.client_order_id("close1");
let result = client.trade().close_positions(&request).await.unwrap();
assert_eq!(result[0].cl_ord_id, "close1");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/trade/close-position"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instId"], "BTC-USDT-SWAP");
assert_eq!(sent["mgnMode"], "cross");
assert_eq!(sent["posSide"], "long");
assert_eq!(sent["autoCxl"], true);
assert!(req.is_signed());
}
#[tokio::test]
async fn get_order_queries_and_parses() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
"ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
"accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let orders = client.trade().get_order("BTC-USDT", "312").await.unwrap();
assert_eq!(orders[0].ord_id, "312");
assert_eq!(orders[0].state, crate::model::OrderState::Live);
assert_eq!(orders[0].side, OrderSide::Buy);
let req = mock.captured();
assert_eq!(req.method, http::Method::GET);
assert_eq!(req.query(), Some("instId=BTC-USDT&ordId=312"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_order_list_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
"ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
"accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::OrderListRequest::new()
.inst_type(crate::model::InstType::Spot)
.inst_id("BTC-USDT")
.limit(1);
let orders = client.trade().get_order_list(&request).await.unwrap();
assert_eq!(orders[0].ord_id, "312");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT&limit=1"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_orders_history_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
"ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
"accFillSz":"0","avgPx":"","state":"filled","cTime":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
.begin("100")
.end("200");
let orders = client.trade().get_orders_history(&request).await.unwrap();
assert_eq!(orders[0].state, crate::model::OrderState::Filled);
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_orders_history_archive_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
"ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
"accFillSz":"0","avgPx":"","state":"canceled","cTime":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot).filters(
super::OrderListRequest::new()
.inst_type(crate::model::InstType::Spot)
.limit(1),
);
let orders = client
.trade()
.get_orders_history_archive(&request)
.await
.unwrap();
assert_eq!(orders[0].state, crate::model::OrderState::Canceled);
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&limit=1"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_fills_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
"fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
"feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::FillsRequest::new()
.inst_type(crate::model::InstType::Spot)
.order_id("312");
let fills = client.trade().get_fills(&request).await.unwrap();
assert_eq!(fills[0].trade_id, "t1");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&ordId=312"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_fills_history_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
"fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
"feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::FillsRequest::new()
.inst_type(crate::model::InstType::Spot)
.begin("100")
.end("200")
.limit(1);
let fills = client.trade().get_fills_history(&request).await.unwrap();
assert_eq!(fills[0].fee.as_str(), "-1");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200&limit=1"));
assert!(req.is_signed());
}
}