1use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{NumberString, OrderSide, OrderState, OrderType, PositionSide, TradeMode};
8use crate::transport::Transport;
9
10const ORDER: &str = "/api/v5/trade/order";
11const BATCH_ORDERS: &str = "/api/v5/trade/batch-orders";
12const CANCEL_ORDER: &str = "/api/v5/trade/cancel-order";
13const CANCEL_BATCH_ORDERS: &str = "/api/v5/trade/cancel-batch-orders";
14const AMEND_ORDER: &str = "/api/v5/trade/amend-order";
15const AMEND_BATCH_ORDERS: &str = "/api/v5/trade/amend-batch-orders";
16const CLOSE_POSITION: &str = "/api/v5/trade/close-position";
17const ORDERS_PENDING: &str = "/api/v5/trade/orders-pending";
18const ORDERS_HISTORY: &str = "/api/v5/trade/orders-history";
19const ORDERS_HISTORY_ARCHIVE: &str = "/api/v5/trade/orders-history-archive";
20const FILLS: &str = "/api/v5/trade/fills";
21const FILLS_HISTORY: &str = "/api/v5/trade/fills-history";
22
23pub struct Trade<'a, T> {
28 client: &'a OkxClient<T>,
29}
30
31impl<'a, T: Transport> Trade<'a, T> {
32 pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
33 Self { client }
34 }
35
36 pub async fn place_order(
48 &self,
49 request: &PlaceOrderRequest,
50 ) -> Result<Vec<PlaceOrderResult>, Error> {
51 self.client.post(ORDER, request, true).await
52 }
53
54 pub async fn place_multiple_orders(
62 &self,
63 requests: &[PlaceOrderRequest],
64 ) -> Result<Vec<PlaceOrderResult>, Error> {
65 self.client.post(BATCH_ORDERS, &requests, true).await
66 }
67
68 pub async fn cancel_order(
76 &self,
77 inst_id: &str,
78 ord_id: &str,
79 ) -> Result<Vec<CancelOrderResult>, Error> {
80 let body = CancelOrderBody { inst_id, ord_id };
81 self.client.post(CANCEL_ORDER, &body, true).await
82 }
83
84 pub async fn cancel_multiple_orders(
92 &self,
93 requests: &[CancelOrderRequest],
94 ) -> Result<Vec<CancelOrderResult>, Error> {
95 self.client.post(CANCEL_BATCH_ORDERS, &requests, true).await
96 }
97
98 pub async fn amend_order(
106 &self,
107 request: &AmendOrderRequest,
108 ) -> Result<Vec<AmendOrderResult>, Error> {
109 self.client.post(AMEND_ORDER, request, true).await
110 }
111
112 pub async fn amend_multiple_orders(
120 &self,
121 requests: &[AmendOrderRequest],
122 ) -> Result<Vec<AmendOrderResult>, Error> {
123 self.client.post(AMEND_BATCH_ORDERS, &requests, true).await
124 }
125
126 pub async fn close_positions(
134 &self,
135 request: &ClosePositionRequest,
136 ) -> Result<Vec<ClosePositionResult>, Error> {
137 self.client.post(CLOSE_POSITION, request, true).await
138 }
139
140 pub async fn get_order(&self, inst_id: &str, ord_id: &str) -> Result<Vec<Order>, Error> {
148 let query = GetOrderQuery { inst_id, ord_id };
149 self.client.get(ORDER, &query, true).await
150 }
151
152 pub async fn get_order_list(&self, request: &OrderListRequest) -> Result<Vec<Order>, Error> {
160 self.client.get(ORDERS_PENDING, request, true).await
161 }
162
163 pub async fn get_orders_history(
171 &self,
172 request: &OrderHistoryRequest,
173 ) -> Result<Vec<Order>, Error> {
174 self.client.get(ORDERS_HISTORY, request, true).await
175 }
176
177 pub async fn get_orders_history_archive(
185 &self,
186 request: &OrderHistoryRequest,
187 ) -> Result<Vec<Order>, Error> {
188 self.client.get(ORDERS_HISTORY_ARCHIVE, request, true).await
189 }
190
191 pub async fn get_fills(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
199 self.client.get(FILLS, request, true).await
200 }
201
202 pub async fn get_fills_history(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
210 self.client.get(FILLS_HISTORY, request, true).await
211 }
212}
213
214#[derive(Debug, Clone, Serialize)]
220pub struct PlaceOrderRequest {
221 #[serde(rename = "instId")]
222 inst_id: String,
223 #[serde(rename = "tdMode")]
224 td_mode: TradeMode,
225 side: OrderSide,
226 #[serde(rename = "ordType")]
227 ord_type: OrderType,
228 sz: String,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 ccy: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 tag: Option<String>,
233 #[serde(rename = "px", skip_serializing_if = "Option::is_none")]
234 px: Option<String>,
235 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
236 pos_side: Option<PositionSide>,
237 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
238 cl_ord_id: Option<String>,
239 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
240 reduce_only: Option<bool>,
241 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
242 tgt_ccy: Option<String>,
243}
244
245impl PlaceOrderRequest {
246 pub fn new(
248 inst_id: impl Into<String>,
249 td_mode: TradeMode,
250 side: OrderSide,
251 ord_type: OrderType,
252 sz: impl Into<String>,
253 ) -> Self {
254 Self {
255 inst_id: inst_id.into(),
256 td_mode,
257 side,
258 ord_type,
259 sz: sz.into(),
260 ccy: None,
261 tag: None,
262 px: None,
263 pos_side: None,
264 cl_ord_id: None,
265 reduce_only: None,
266 tgt_ccy: None,
267 }
268 }
269
270 pub fn price(mut self, px: impl Into<String>) -> Self {
272 self.px = Some(px.into());
273 self
274 }
275
276 pub fn position_side(mut self, pos_side: PositionSide) -> Self {
278 self.pos_side = Some(pos_side);
279 self
280 }
281
282 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
284 self.cl_ord_id = Some(cl_ord_id.into());
285 self
286 }
287
288 pub fn reduce_only(mut self, reduce_only: bool) -> Self {
290 self.reduce_only = Some(reduce_only);
291 self
292 }
293
294 pub fn target_ccy(mut self, tgt_ccy: impl Into<String>) -> Self {
296 self.tgt_ccy = Some(tgt_ccy.into());
297 self
298 }
299
300 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
302 self.ccy = Some(ccy.into());
303 self
304 }
305
306 pub fn tag(mut self, tag: impl Into<String>) -> Self {
308 self.tag = Some(tag.into());
309 self
310 }
311}
312
313#[derive(Serialize)]
314struct CancelOrderBody<'a> {
315 #[serde(rename = "instId")]
316 inst_id: &'a str,
317 #[serde(rename = "ordId")]
318 ord_id: &'a str,
319}
320
321#[derive(Serialize)]
322struct GetOrderQuery<'a> {
323 #[serde(rename = "instId")]
324 inst_id: &'a str,
325 #[serde(rename = "ordId")]
326 ord_id: &'a str,
327}
328
329#[derive(Debug, Clone, Serialize)]
331pub struct CancelOrderRequest {
332 #[serde(rename = "instId")]
333 inst_id: String,
334 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
335 ord_id: Option<String>,
336 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
337 cl_ord_id: Option<String>,
338}
339
340impl CancelOrderRequest {
341 pub fn by_order_id(inst_id: impl Into<String>, ord_id: impl Into<String>) -> Self {
343 Self {
344 inst_id: inst_id.into(),
345 ord_id: Some(ord_id.into()),
346 cl_ord_id: None,
347 }
348 }
349
350 pub fn by_client_order_id(inst_id: impl Into<String>, cl_ord_id: impl Into<String>) -> Self {
352 Self {
353 inst_id: inst_id.into(),
354 ord_id: None,
355 cl_ord_id: Some(cl_ord_id.into()),
356 }
357 }
358}
359
360#[derive(Debug, Clone, Serialize)]
362pub struct AmendOrderRequest {
363 #[serde(rename = "instId")]
364 inst_id: String,
365 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
366 ord_id: Option<String>,
367 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
368 cl_ord_id: Option<String>,
369 #[serde(rename = "reqId", skip_serializing_if = "Option::is_none")]
370 req_id: Option<String>,
371 #[serde(rename = "cxlOnFail", skip_serializing_if = "Option::is_none")]
372 cxl_on_fail: Option<bool>,
373 #[serde(rename = "newSz", skip_serializing_if = "Option::is_none")]
374 new_sz: Option<String>,
375 #[serde(rename = "newPx", skip_serializing_if = "Option::is_none")]
376 new_px: Option<String>,
377}
378
379impl AmendOrderRequest {
380 pub fn new(inst_id: impl Into<String>) -> Self {
382 Self {
383 inst_id: inst_id.into(),
384 ord_id: None,
385 cl_ord_id: None,
386 req_id: None,
387 cxl_on_fail: None,
388 new_sz: None,
389 new_px: None,
390 }
391 }
392
393 pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
395 self.ord_id = Some(ord_id.into());
396 self
397 }
398
399 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
401 self.cl_ord_id = Some(cl_ord_id.into());
402 self
403 }
404
405 pub fn request_id(mut self, req_id: impl Into<String>) -> Self {
407 self.req_id = Some(req_id.into());
408 self
409 }
410
411 pub fn cancel_on_fail(mut self, cxl_on_fail: bool) -> Self {
413 self.cxl_on_fail = Some(cxl_on_fail);
414 self
415 }
416
417 pub fn new_size(mut self, new_sz: impl Into<String>) -> Self {
419 self.new_sz = Some(new_sz.into());
420 self
421 }
422
423 pub fn new_price(mut self, new_px: impl Into<String>) -> Self {
425 self.new_px = Some(new_px.into());
426 self
427 }
428}
429
430#[derive(Debug, Clone, Serialize)]
432pub struct ClosePositionRequest {
433 #[serde(rename = "instId")]
434 inst_id: String,
435 #[serde(rename = "mgnMode")]
436 mgn_mode: TradeMode,
437 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
438 pos_side: Option<PositionSide>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 ccy: Option<String>,
441 #[serde(rename = "autoCxl", skip_serializing_if = "Option::is_none")]
442 auto_cancel: Option<bool>,
443 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
444 cl_ord_id: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 tag: Option<String>,
447}
448
449impl ClosePositionRequest {
450 pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
452 Self {
453 inst_id: inst_id.into(),
454 mgn_mode,
455 pos_side: None,
456 ccy: None,
457 auto_cancel: None,
458 cl_ord_id: None,
459 tag: None,
460 }
461 }
462
463 pub fn position_side(mut self, pos_side: PositionSide) -> Self {
465 self.pos_side = Some(pos_side);
466 self
467 }
468
469 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
471 self.ccy = Some(ccy.into());
472 self
473 }
474
475 pub fn auto_cancel(mut self, auto_cancel: bool) -> Self {
477 self.auto_cancel = Some(auto_cancel);
478 self
479 }
480
481 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
483 self.cl_ord_id = Some(cl_ord_id.into());
484 self
485 }
486
487 pub fn tag(mut self, tag: impl Into<String>) -> Self {
489 self.tag = Some(tag.into());
490 self
491 }
492}
493
494#[derive(Debug, Clone, Default, Serialize)]
496pub struct OrderListRequest {
497 #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
498 inst_type: Option<crate::model::InstType>,
499 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
500 underlying: Option<String>,
501 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
502 inst_id: Option<String>,
503 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
504 ord_type: Option<OrderType>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 state: Option<OrderState>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 after: Option<String>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 before: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 limit: Option<u32>,
513 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
514 inst_family: Option<String>,
515}
516
517impl OrderListRequest {
518 pub fn new() -> Self {
520 Self::default()
521 }
522
523 pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
525 self.inst_type = Some(inst_type);
526 self
527 }
528
529 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
531 self.underlying = Some(underlying.into());
532 self
533 }
534
535 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
537 self.inst_id = Some(inst_id.into());
538 self
539 }
540
541 pub fn order_type(mut self, ord_type: OrderType) -> Self {
543 self.ord_type = Some(ord_type);
544 self
545 }
546
547 pub fn state(mut self, state: OrderState) -> Self {
549 self.state = Some(state);
550 self
551 }
552
553 pub fn after(mut self, after: impl Into<String>) -> Self {
555 self.after = Some(after.into());
556 self
557 }
558
559 pub fn before(mut self, before: impl Into<String>) -> Self {
561 self.before = Some(before.into());
562 self
563 }
564
565 pub fn limit(mut self, limit: u32) -> Self {
567 self.limit = Some(limit);
568 self
569 }
570
571 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
573 self.inst_family = Some(inst_family.into());
574 self
575 }
576}
577
578#[derive(Debug, Clone, Serialize)]
580pub struct OrderHistoryRequest {
581 #[serde(flatten)]
582 base: OrderListRequest,
583 #[serde(skip_serializing_if = "Option::is_none")]
584 begin: Option<String>,
585 #[serde(skip_serializing_if = "Option::is_none")]
586 end: Option<String>,
587}
588
589impl OrderHistoryRequest {
590 pub fn new(inst_type: crate::model::InstType) -> Self {
592 Self {
593 base: OrderListRequest::new().inst_type(inst_type),
594 begin: None,
595 end: None,
596 }
597 }
598
599 pub fn filters(mut self, base: OrderListRequest) -> Self {
601 self.base = base;
602 self
603 }
604
605 pub fn begin(mut self, begin: impl Into<String>) -> Self {
607 self.begin = Some(begin.into());
608 self
609 }
610
611 pub fn end(mut self, end: impl Into<String>) -> Self {
613 self.end = Some(end.into());
614 self
615 }
616}
617
618#[derive(Debug, Clone, Default, Serialize)]
620pub struct FillsRequest {
621 #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
622 inst_type: Option<crate::model::InstType>,
623 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
624 underlying: Option<String>,
625 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
626 inst_id: Option<String>,
627 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
628 ord_id: Option<String>,
629 #[serde(skip_serializing_if = "Option::is_none")]
630 after: Option<String>,
631 #[serde(skip_serializing_if = "Option::is_none")]
632 before: Option<String>,
633 #[serde(skip_serializing_if = "Option::is_none")]
634 begin: Option<String>,
635 #[serde(skip_serializing_if = "Option::is_none")]
636 end: Option<String>,
637 #[serde(skip_serializing_if = "Option::is_none")]
638 limit: Option<u32>,
639 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
640 inst_family: Option<String>,
641}
642
643impl FillsRequest {
644 pub fn new() -> Self {
646 Self::default()
647 }
648
649 pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
651 self.inst_type = Some(inst_type);
652 self
653 }
654
655 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
657 self.underlying = Some(underlying.into());
658 self
659 }
660
661 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
663 self.inst_id = Some(inst_id.into());
664 self
665 }
666
667 pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
669 self.ord_id = Some(ord_id.into());
670 self
671 }
672
673 pub fn after(mut self, after: impl Into<String>) -> Self {
675 self.after = Some(after.into());
676 self
677 }
678
679 pub fn before(mut self, before: impl Into<String>) -> Self {
681 self.before = Some(before.into());
682 self
683 }
684
685 pub fn begin(mut self, begin: impl Into<String>) -> Self {
687 self.begin = Some(begin.into());
688 self
689 }
690
691 pub fn end(mut self, end: impl Into<String>) -> Self {
693 self.end = Some(end.into());
694 self
695 }
696
697 pub fn limit(mut self, limit: u32) -> Self {
699 self.limit = Some(limit);
700 self
701 }
702
703 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
705 self.inst_family = Some(inst_family.into());
706 self
707 }
708}
709
710#[derive(Debug, Clone, Deserialize)]
712#[serde(rename_all = "camelCase")]
713#[non_exhaustive]
714pub struct PlaceOrderResult {
715 #[serde(default)]
717 pub ord_id: String,
718 #[serde(default)]
720 pub cl_ord_id: String,
721 #[serde(default)]
723 pub tag: String,
724 pub s_code: String,
726 #[serde(default)]
728 pub s_msg: String,
729 #[serde(default)]
731 pub ts: NumberString,
732}
733
734#[derive(Debug, Clone, Deserialize)]
736#[serde(rename_all = "camelCase")]
737#[non_exhaustive]
738pub struct CancelOrderResult {
739 #[serde(default)]
741 pub ord_id: String,
742 #[serde(default)]
744 pub cl_ord_id: String,
745 pub s_code: String,
747 #[serde(default)]
749 pub s_msg: String,
750}
751
752#[derive(Debug, Clone, Deserialize)]
754#[serde(rename_all = "camelCase")]
755#[non_exhaustive]
756pub struct AmendOrderResult {
757 #[serde(default)]
759 pub ord_id: String,
760 #[serde(default)]
762 pub cl_ord_id: String,
763 #[serde(default)]
765 pub req_id: String,
766 pub s_code: String,
768 #[serde(default)]
770 pub s_msg: String,
771}
772
773#[derive(Debug, Clone, Deserialize)]
775#[serde(rename_all = "camelCase")]
776#[non_exhaustive]
777pub struct ClosePositionResult {
778 #[serde(default)]
780 pub inst_id: String,
781 #[serde(default)]
783 pub pos_side: String,
784 #[serde(default)]
786 pub cl_ord_id: String,
787 #[serde(default)]
789 pub tag: String,
790}
791
792#[derive(Debug, Clone, Deserialize)]
794#[serde(rename_all = "camelCase")]
795#[non_exhaustive]
796pub struct Order {
797 pub inst_id: String,
799 pub ord_id: String,
801 #[serde(default)]
803 pub cl_ord_id: String,
804 #[serde(default)]
806 pub px: NumberString,
807 #[serde(default)]
809 pub sz: NumberString,
810 pub ord_type: OrderType,
812 pub side: OrderSide,
814 pub pos_side: PositionSide,
816 pub td_mode: TradeMode,
818 #[serde(default)]
820 pub acc_fill_sz: NumberString,
821 #[serde(default)]
823 pub avg_px: NumberString,
824 pub state: OrderState,
826 #[serde(default)]
828 pub c_time: NumberString,
829}
830
831#[derive(Debug, Clone, Deserialize)]
833#[serde(rename_all = "camelCase")]
834#[non_exhaustive]
835pub struct Fill {
836 #[serde(default)]
838 pub inst_type: String,
839 pub inst_id: String,
841 #[serde(default)]
843 pub trade_id: String,
844 #[serde(default)]
846 pub ord_id: String,
847 #[serde(default)]
849 pub fill_px: NumberString,
850 #[serde(default)]
852 pub fill_sz: NumberString,
853 pub side: OrderSide,
855 pub ord_type: OrderType,
857 #[serde(default)]
859 pub fee_ccy: String,
860 #[serde(default)]
862 pub fee: NumberString,
863 #[serde(default)]
865 pub ts: NumberString,
866}
867
868#[cfg(test)]
869mod tests {
870 use crate::model::{OrderSide, OrderType, TradeMode};
871 use crate::test_util::MockTransport;
872 use crate::{Credentials, OkxClient};
873
874 use super::PlaceOrderRequest;
875
876 fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
877 OkxClient::with_transport(mock)
878 .credentials(Credentials::new("key", "secret", "pass"))
879 .build()
880 }
881
882 #[tokio::test]
883 async fn place_order_posts_signed_json_body() {
884 let body = r#"{"code":"0","msg":"","data":[
885 {"ordId":"312","clOrdId":"b1","tag":"","sCode":"0","sMsg":"","ts":"1597026383085"}]}"#;
886 let mock = MockTransport::new(body);
887 let client = signed_client(mock.clone());
888
889 let request = PlaceOrderRequest::new(
890 "BTC-USDT",
891 TradeMode::Cash,
892 OrderSide::Buy,
893 OrderType::Limit,
894 "0.01",
895 )
896 .price("42000")
897 .client_order_id("b1");
898
899 let result = client.trade().place_order(&request).await.unwrap();
900 assert_eq!(result[0].ord_id, "312");
901 assert_eq!(result[0].s_code, "0");
902
903 let req = mock.captured();
904 assert_eq!(req.method, http::Method::POST);
905 assert!(req.uri.ends_with("/api/v5/trade/order"));
906 assert!(req.is_signed());
907
908 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
909 assert_eq!(sent["instId"], "BTC-USDT");
910 assert_eq!(sent["tdMode"], "cash");
911 assert_eq!(sent["side"], "buy");
912 assert_eq!(sent["ordType"], "limit");
913 assert_eq!(sent["sz"], "0.01");
914 assert_eq!(sent["px"], "42000");
915 assert_eq!(sent["clOrdId"], "b1");
916 assert!(sent.get("reduceOnly").is_none());
918 }
919
920 #[tokio::test]
921 async fn place_multiple_orders_posts_array_body() {
922 let body = r#"{"code":"0","msg":"","data":[
923 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""},
924 {"ordId":"313","clOrdId":"b2","sCode":"0","sMsg":""}]}"#;
925 let mock = MockTransport::new(body);
926 let client = signed_client(mock.clone());
927 let requests = vec![
928 PlaceOrderRequest::new(
929 "BTC-USDT",
930 TradeMode::Cash,
931 OrderSide::Buy,
932 OrderType::Limit,
933 "0.01",
934 )
935 .price("42000")
936 .client_order_id("b1"),
937 PlaceOrderRequest::new(
938 "BTC-USDT",
939 TradeMode::Cash,
940 OrderSide::Sell,
941 OrderType::Limit,
942 "0.02",
943 )
944 .price("43000")
945 .client_order_id("b2"),
946 ];
947
948 let result = client
949 .trade()
950 .place_multiple_orders(&requests)
951 .await
952 .unwrap();
953 assert_eq!(result[1].ord_id, "313");
954
955 let req = mock.captured();
956 assert_eq!(req.method, http::Method::POST);
957 assert!(req.uri.ends_with("/api/v5/trade/batch-orders"));
958 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
959 assert_eq!(sent[0]["clOrdId"], "b1");
960 assert_eq!(sent[1]["side"], "sell");
961 assert!(req.is_signed());
962 }
963
964 #[tokio::test]
965 async fn cancel_order_posts_ids() {
966 let body = r#"{"code":"0","msg":"","data":[
967 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
968 let mock = MockTransport::new(body);
969 let client = signed_client(mock.clone());
970
971 let result = client
972 .trade()
973 .cancel_order("BTC-USDT", "312")
974 .await
975 .unwrap();
976 assert_eq!(result[0].ord_id, "312");
977
978 let req = mock.captured();
979 assert_eq!(req.method, http::Method::POST);
980 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
981 assert_eq!(sent["instId"], "BTC-USDT");
982 assert_eq!(sent["ordId"], "312");
983 }
984
985 #[tokio::test]
986 async fn cancel_multiple_orders_posts_array_body() {
987 let body = r#"{"code":"0","msg":"","data":[
988 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
989 let mock = MockTransport::new(body);
990 let client = signed_client(mock.clone());
991 let requests = vec![super::CancelOrderRequest::by_order_id("BTC-USDT", "312")];
992
993 let result = client
994 .trade()
995 .cancel_multiple_orders(&requests)
996 .await
997 .unwrap();
998 assert_eq!(result[0].s_code, "0");
999
1000 let req = mock.captured();
1001 assert!(req.uri.ends_with("/api/v5/trade/cancel-batch-orders"));
1002 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1003 assert_eq!(sent[0]["instId"], "BTC-USDT");
1004 assert_eq!(sent[0]["ordId"], "312");
1005 assert!(sent[0].get("clOrdId").is_none());
1006 assert!(req.is_signed());
1007 }
1008
1009 #[tokio::test]
1010 async fn amend_order_posts_builder_body() {
1011 let body = r#"{"code":"0","msg":"","data":[
1012 {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1013 let mock = MockTransport::new(body);
1014 let client = signed_client(mock.clone());
1015 let request = super::AmendOrderRequest::new("BTC-USDT")
1016 .order_id("312")
1017 .request_id("r1")
1018 .new_price("42100");
1019
1020 let result = client.trade().amend_order(&request).await.unwrap();
1021 assert_eq!(result[0].req_id, "r1");
1022
1023 let req = mock.captured();
1024 assert!(req.uri.ends_with("/api/v5/trade/amend-order"));
1025 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1026 assert_eq!(sent["instId"], "BTC-USDT");
1027 assert_eq!(sent["ordId"], "312");
1028 assert_eq!(sent["newPx"], "42100");
1029 assert!(sent.get("newSz").is_none());
1030 assert!(req.is_signed());
1031 }
1032
1033 #[tokio::test]
1034 async fn amend_multiple_orders_posts_array_body() {
1035 let body = r#"{"code":"0","msg":"","data":[
1036 {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1037 let mock = MockTransport::new(body);
1038 let client = signed_client(mock.clone());
1039 let requests = vec![
1040 super::AmendOrderRequest::new("BTC-USDT")
1041 .client_order_id("b1")
1042 .new_size("0.03"),
1043 ];
1044
1045 let result = client
1046 .trade()
1047 .amend_multiple_orders(&requests)
1048 .await
1049 .unwrap();
1050 assert_eq!(result[0].s_code, "0");
1051
1052 let req = mock.captured();
1053 assert!(req.uri.ends_with("/api/v5/trade/amend-batch-orders"));
1054 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1055 assert_eq!(sent[0]["clOrdId"], "b1");
1056 assert_eq!(sent[0]["newSz"], "0.03");
1057 assert!(req.is_signed());
1058 }
1059
1060 #[tokio::test]
1061 async fn close_positions_posts_builder_body() {
1062 let body = r#"{"code":"0","msg":"","data":[
1063 {"instId":"BTC-USDT-SWAP","posSide":"long","clOrdId":"close1","tag":"t"}]}"#;
1064 let mock = MockTransport::new(body);
1065 let client = signed_client(mock.clone());
1066 let request = super::ClosePositionRequest::new("BTC-USDT-SWAP", TradeMode::Cross)
1067 .position_side(crate::model::PositionSide::Long)
1068 .auto_cancel(true)
1069 .client_order_id("close1");
1070
1071 let result = client.trade().close_positions(&request).await.unwrap();
1072 assert_eq!(result[0].cl_ord_id, "close1");
1073
1074 let req = mock.captured();
1075 assert!(req.uri.ends_with("/api/v5/trade/close-position"));
1076 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1077 assert_eq!(sent["instId"], "BTC-USDT-SWAP");
1078 assert_eq!(sent["mgnMode"], "cross");
1079 assert_eq!(sent["posSide"], "long");
1080 assert_eq!(sent["autoCxl"], true);
1081 assert!(req.is_signed());
1082 }
1083
1084 #[tokio::test]
1085 async fn get_order_queries_and_parses() {
1086 let body = r#"{"code":"0","msg":"","data":[
1087 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1088 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1089 "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1090 let mock = MockTransport::new(body);
1091 let client = signed_client(mock.clone());
1092
1093 let orders = client.trade().get_order("BTC-USDT", "312").await.unwrap();
1094 assert_eq!(orders[0].ord_id, "312");
1095 assert_eq!(orders[0].state, crate::model::OrderState::Live);
1096 assert_eq!(orders[0].side, OrderSide::Buy);
1097
1098 let req = mock.captured();
1099 assert_eq!(req.method, http::Method::GET);
1100 assert_eq!(req.query(), Some("instId=BTC-USDT&ordId=312"));
1101 assert!(req.is_signed());
1102 }
1103
1104 #[tokio::test]
1105 async fn get_order_list_uses_builder_query() {
1106 let body = r#"{"code":"0","msg":"","data":[
1107 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1108 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1109 "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1110 let mock = MockTransport::new(body);
1111 let client = signed_client(mock.clone());
1112 let request = super::OrderListRequest::new()
1113 .inst_type(crate::model::InstType::Spot)
1114 .inst_id("BTC-USDT")
1115 .limit(1);
1116
1117 let orders = client.trade().get_order_list(&request).await.unwrap();
1118 assert_eq!(orders[0].ord_id, "312");
1119
1120 let req = mock.captured();
1121 assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT&limit=1"));
1122 assert!(req.is_signed());
1123 }
1124
1125 #[tokio::test]
1126 async fn get_orders_history_uses_builder_query() {
1127 let body = r#"{"code":"0","msg":"","data":[
1128 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1129 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1130 "accFillSz":"0","avgPx":"","state":"filled","cTime":"1597026383085"}]}"#;
1131 let mock = MockTransport::new(body);
1132 let client = signed_client(mock.clone());
1133 let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
1134 .begin("100")
1135 .end("200");
1136
1137 let orders = client.trade().get_orders_history(&request).await.unwrap();
1138 assert_eq!(orders[0].state, crate::model::OrderState::Filled);
1139
1140 let req = mock.captured();
1141 assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200"));
1142 assert!(req.is_signed());
1143 }
1144
1145 #[tokio::test]
1146 async fn get_orders_history_archive_uses_builder_query() {
1147 let body = r#"{"code":"0","msg":"","data":[
1148 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1149 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1150 "accFillSz":"0","avgPx":"","state":"canceled","cTime":"1597026383085"}]}"#;
1151 let mock = MockTransport::new(body);
1152 let client = signed_client(mock.clone());
1153 let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot).filters(
1154 super::OrderListRequest::new()
1155 .inst_type(crate::model::InstType::Spot)
1156 .limit(1),
1157 );
1158
1159 let orders = client
1160 .trade()
1161 .get_orders_history_archive(&request)
1162 .await
1163 .unwrap();
1164 assert_eq!(orders[0].state, crate::model::OrderState::Canceled);
1165
1166 let req = mock.captured();
1167 assert_eq!(req.query(), Some("instType=SPOT&limit=1"));
1168 assert!(req.is_signed());
1169 }
1170
1171 #[tokio::test]
1172 async fn get_fills_uses_builder_query() {
1173 let body = r#"{"code":"0","msg":"","data":[
1174 {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1175 "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1176 "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1177 let mock = MockTransport::new(body);
1178 let client = signed_client(mock.clone());
1179 let request = super::FillsRequest::new()
1180 .inst_type(crate::model::InstType::Spot)
1181 .order_id("312");
1182
1183 let fills = client.trade().get_fills(&request).await.unwrap();
1184 assert_eq!(fills[0].trade_id, "t1");
1185
1186 let req = mock.captured();
1187 assert_eq!(req.query(), Some("instType=SPOT&ordId=312"));
1188 assert!(req.is_signed());
1189 }
1190
1191 #[tokio::test]
1192 async fn get_fills_history_uses_builder_query() {
1193 let body = r#"{"code":"0","msg":"","data":[
1194 {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1195 "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1196 "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1197 let mock = MockTransport::new(body);
1198 let client = signed_client(mock.clone());
1199 let request = super::FillsRequest::new()
1200 .inst_type(crate::model::InstType::Spot)
1201 .begin("100")
1202 .end("200")
1203 .limit(1);
1204
1205 let fills = client.trade().get_fills_history(&request).await.unwrap();
1206 assert_eq!(fills[0].fee.as_str(), "-1");
1207
1208 let req = mock.captured();
1209 assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200&limit=1"));
1210 assert!(req.is_signed());
1211 }
1212}