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
189 .get(ORDERS_HISTORY_ARCHIVE, request, true)
190 .await
191 }
192
193 pub async fn get_fills(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
201 self.client.get(FILLS, request, true).await
202 }
203
204 pub async fn get_fills_history(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
212 self.client.get(FILLS_HISTORY, request, true).await
213 }
214}
215
216#[derive(Debug, Clone, Serialize)]
222pub struct PlaceOrderRequest {
223 #[serde(rename = "instId")]
224 inst_id: String,
225 #[serde(rename = "tdMode")]
226 td_mode: TradeMode,
227 side: OrderSide,
228 #[serde(rename = "ordType")]
229 ord_type: OrderType,
230 sz: String,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 ccy: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 tag: Option<String>,
235 #[serde(rename = "px", skip_serializing_if = "Option::is_none")]
236 px: Option<String>,
237 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
238 pos_side: Option<PositionSide>,
239 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
240 cl_ord_id: Option<String>,
241 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
242 reduce_only: Option<bool>,
243 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
244 tgt_ccy: Option<String>,
245}
246
247impl PlaceOrderRequest {
248 pub fn new(
250 inst_id: impl Into<String>,
251 td_mode: TradeMode,
252 side: OrderSide,
253 ord_type: OrderType,
254 sz: impl Into<String>,
255 ) -> Self {
256 Self {
257 inst_id: inst_id.into(),
258 td_mode,
259 side,
260 ord_type,
261 sz: sz.into(),
262 ccy: None,
263 tag: None,
264 px: None,
265 pos_side: None,
266 cl_ord_id: None,
267 reduce_only: None,
268 tgt_ccy: None,
269 }
270 }
271
272 pub fn price(mut self, px: impl Into<String>) -> Self {
274 self.px = Some(px.into());
275 self
276 }
277
278 pub fn position_side(mut self, pos_side: PositionSide) -> Self {
280 self.pos_side = Some(pos_side);
281 self
282 }
283
284 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
286 self.cl_ord_id = Some(cl_ord_id.into());
287 self
288 }
289
290 pub fn reduce_only(mut self, reduce_only: bool) -> Self {
292 self.reduce_only = Some(reduce_only);
293 self
294 }
295
296 pub fn target_ccy(mut self, tgt_ccy: impl Into<String>) -> Self {
298 self.tgt_ccy = Some(tgt_ccy.into());
299 self
300 }
301
302 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
304 self.ccy = Some(ccy.into());
305 self
306 }
307
308 pub fn tag(mut self, tag: impl Into<String>) -> Self {
310 self.tag = Some(tag.into());
311 self
312 }
313}
314
315#[derive(Serialize)]
316struct CancelOrderBody<'a> {
317 #[serde(rename = "instId")]
318 inst_id: &'a str,
319 #[serde(rename = "ordId")]
320 ord_id: &'a str,
321}
322
323#[derive(Serialize)]
324struct GetOrderQuery<'a> {
325 #[serde(rename = "instId")]
326 inst_id: &'a str,
327 #[serde(rename = "ordId")]
328 ord_id: &'a str,
329}
330
331#[derive(Debug, Clone, Serialize)]
333pub struct CancelOrderRequest {
334 #[serde(rename = "instId")]
335 inst_id: String,
336 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
337 ord_id: Option<String>,
338 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
339 cl_ord_id: Option<String>,
340}
341
342impl CancelOrderRequest {
343 pub fn by_order_id(inst_id: impl Into<String>, ord_id: impl Into<String>) -> Self {
345 Self {
346 inst_id: inst_id.into(),
347 ord_id: Some(ord_id.into()),
348 cl_ord_id: None,
349 }
350 }
351
352 pub fn by_client_order_id(inst_id: impl Into<String>, cl_ord_id: impl Into<String>) -> Self {
354 Self {
355 inst_id: inst_id.into(),
356 ord_id: None,
357 cl_ord_id: Some(cl_ord_id.into()),
358 }
359 }
360}
361
362#[derive(Debug, Clone, Serialize)]
364pub struct AmendOrderRequest {
365 #[serde(rename = "instId")]
366 inst_id: String,
367 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
368 ord_id: Option<String>,
369 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
370 cl_ord_id: Option<String>,
371 #[serde(rename = "reqId", skip_serializing_if = "Option::is_none")]
372 req_id: Option<String>,
373 #[serde(rename = "cxlOnFail", skip_serializing_if = "Option::is_none")]
374 cxl_on_fail: Option<bool>,
375 #[serde(rename = "newSz", skip_serializing_if = "Option::is_none")]
376 new_sz: Option<String>,
377 #[serde(rename = "newPx", skip_serializing_if = "Option::is_none")]
378 new_px: Option<String>,
379}
380
381impl AmendOrderRequest {
382 pub fn new(inst_id: impl Into<String>) -> Self {
384 Self {
385 inst_id: inst_id.into(),
386 ord_id: None,
387 cl_ord_id: None,
388 req_id: None,
389 cxl_on_fail: None,
390 new_sz: None,
391 new_px: None,
392 }
393 }
394
395 pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
397 self.ord_id = Some(ord_id.into());
398 self
399 }
400
401 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
403 self.cl_ord_id = Some(cl_ord_id.into());
404 self
405 }
406
407 pub fn request_id(mut self, req_id: impl Into<String>) -> Self {
409 self.req_id = Some(req_id.into());
410 self
411 }
412
413 pub fn cancel_on_fail(mut self, cxl_on_fail: bool) -> Self {
415 self.cxl_on_fail = Some(cxl_on_fail);
416 self
417 }
418
419 pub fn new_size(mut self, new_sz: impl Into<String>) -> Self {
421 self.new_sz = Some(new_sz.into());
422 self
423 }
424
425 pub fn new_price(mut self, new_px: impl Into<String>) -> Self {
427 self.new_px = Some(new_px.into());
428 self
429 }
430}
431
432#[derive(Debug, Clone, Serialize)]
434pub struct ClosePositionRequest {
435 #[serde(rename = "instId")]
436 inst_id: String,
437 #[serde(rename = "mgnMode")]
438 mgn_mode: TradeMode,
439 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
440 pos_side: Option<PositionSide>,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 ccy: Option<String>,
443 #[serde(rename = "autoCxl", skip_serializing_if = "Option::is_none")]
444 auto_cancel: Option<bool>,
445 #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
446 cl_ord_id: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 tag: Option<String>,
449}
450
451impl ClosePositionRequest {
452 pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
454 Self {
455 inst_id: inst_id.into(),
456 mgn_mode,
457 pos_side: None,
458 ccy: None,
459 auto_cancel: None,
460 cl_ord_id: None,
461 tag: None,
462 }
463 }
464
465 pub fn position_side(mut self, pos_side: PositionSide) -> Self {
467 self.pos_side = Some(pos_side);
468 self
469 }
470
471 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
473 self.ccy = Some(ccy.into());
474 self
475 }
476
477 pub fn auto_cancel(mut self, auto_cancel: bool) -> Self {
479 self.auto_cancel = Some(auto_cancel);
480 self
481 }
482
483 pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
485 self.cl_ord_id = Some(cl_ord_id.into());
486 self
487 }
488
489 pub fn tag(mut self, tag: impl Into<String>) -> Self {
491 self.tag = Some(tag.into());
492 self
493 }
494}
495
496#[derive(Debug, Clone, Default, Serialize)]
498pub struct OrderListRequest {
499 #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
500 inst_type: Option<crate::model::InstType>,
501 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
502 underlying: Option<String>,
503 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
504 inst_id: Option<String>,
505 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
506 ord_type: Option<OrderType>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 state: Option<OrderState>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 after: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 before: Option<String>,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 limit: Option<u32>,
515 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
516 inst_family: Option<String>,
517}
518
519impl OrderListRequest {
520 pub fn new() -> Self {
522 Self::default()
523 }
524
525 pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
527 self.inst_type = Some(inst_type);
528 self
529 }
530
531 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
533 self.underlying = Some(underlying.into());
534 self
535 }
536
537 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
539 self.inst_id = Some(inst_id.into());
540 self
541 }
542
543 pub fn order_type(mut self, ord_type: OrderType) -> Self {
545 self.ord_type = Some(ord_type);
546 self
547 }
548
549 pub fn state(mut self, state: OrderState) -> Self {
551 self.state = Some(state);
552 self
553 }
554
555 pub fn after(mut self, after: impl Into<String>) -> Self {
557 self.after = Some(after.into());
558 self
559 }
560
561 pub fn before(mut self, before: impl Into<String>) -> Self {
563 self.before = Some(before.into());
564 self
565 }
566
567 pub fn limit(mut self, limit: u32) -> Self {
569 self.limit = Some(limit);
570 self
571 }
572
573 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
575 self.inst_family = Some(inst_family.into());
576 self
577 }
578}
579
580#[derive(Debug, Clone, Serialize)]
582pub struct OrderHistoryRequest {
583 #[serde(flatten)]
584 base: OrderListRequest,
585 #[serde(skip_serializing_if = "Option::is_none")]
586 begin: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
588 end: Option<String>,
589}
590
591impl OrderHistoryRequest {
592 pub fn new(inst_type: crate::model::InstType) -> Self {
594 Self {
595 base: OrderListRequest::new().inst_type(inst_type),
596 begin: None,
597 end: None,
598 }
599 }
600
601 pub fn filters(mut self, base: OrderListRequest) -> Self {
603 self.base = base;
604 self
605 }
606
607 pub fn begin(mut self, begin: impl Into<String>) -> Self {
609 self.begin = Some(begin.into());
610 self
611 }
612
613 pub fn end(mut self, end: impl Into<String>) -> Self {
615 self.end = Some(end.into());
616 self
617 }
618}
619
620#[derive(Debug, Clone, Default, Serialize)]
622pub struct FillsRequest {
623 #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
624 inst_type: Option<crate::model::InstType>,
625 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
626 underlying: Option<String>,
627 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
628 inst_id: Option<String>,
629 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
630 ord_id: Option<String>,
631 #[serde(skip_serializing_if = "Option::is_none")]
632 after: Option<String>,
633 #[serde(skip_serializing_if = "Option::is_none")]
634 before: Option<String>,
635 #[serde(skip_serializing_if = "Option::is_none")]
636 begin: Option<String>,
637 #[serde(skip_serializing_if = "Option::is_none")]
638 end: Option<String>,
639 #[serde(skip_serializing_if = "Option::is_none")]
640 limit: Option<u32>,
641 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
642 inst_family: Option<String>,
643}
644
645impl FillsRequest {
646 pub fn new() -> Self {
648 Self::default()
649 }
650
651 pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
653 self.inst_type = Some(inst_type);
654 self
655 }
656
657 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
659 self.underlying = Some(underlying.into());
660 self
661 }
662
663 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
665 self.inst_id = Some(inst_id.into());
666 self
667 }
668
669 pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
671 self.ord_id = Some(ord_id.into());
672 self
673 }
674
675 pub fn after(mut self, after: impl Into<String>) -> Self {
677 self.after = Some(after.into());
678 self
679 }
680
681 pub fn before(mut self, before: impl Into<String>) -> Self {
683 self.before = Some(before.into());
684 self
685 }
686
687 pub fn begin(mut self, begin: impl Into<String>) -> Self {
689 self.begin = Some(begin.into());
690 self
691 }
692
693 pub fn end(mut self, end: impl Into<String>) -> Self {
695 self.end = Some(end.into());
696 self
697 }
698
699 pub fn limit(mut self, limit: u32) -> Self {
701 self.limit = Some(limit);
702 self
703 }
704
705 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
707 self.inst_family = Some(inst_family.into());
708 self
709 }
710}
711
712#[derive(Debug, Clone, Deserialize)]
714#[serde(rename_all = "camelCase")]
715#[non_exhaustive]
716pub struct PlaceOrderResult {
717 #[serde(default)]
719 pub ord_id: String,
720 #[serde(default)]
722 pub cl_ord_id: String,
723 #[serde(default)]
725 pub tag: String,
726 pub s_code: String,
728 #[serde(default)]
730 pub s_msg: String,
731 #[serde(default)]
733 pub ts: NumberString,
734}
735
736#[derive(Debug, Clone, Deserialize)]
738#[serde(rename_all = "camelCase")]
739#[non_exhaustive]
740pub struct CancelOrderResult {
741 #[serde(default)]
743 pub ord_id: String,
744 #[serde(default)]
746 pub cl_ord_id: String,
747 pub s_code: String,
749 #[serde(default)]
751 pub s_msg: String,
752}
753
754#[derive(Debug, Clone, Deserialize)]
756#[serde(rename_all = "camelCase")]
757#[non_exhaustive]
758pub struct AmendOrderResult {
759 #[serde(default)]
761 pub ord_id: String,
762 #[serde(default)]
764 pub cl_ord_id: String,
765 #[serde(default)]
767 pub req_id: String,
768 pub s_code: String,
770 #[serde(default)]
772 pub s_msg: String,
773}
774
775#[derive(Debug, Clone, Deserialize)]
777#[serde(rename_all = "camelCase")]
778#[non_exhaustive]
779pub struct ClosePositionResult {
780 #[serde(default)]
782 pub inst_id: String,
783 #[serde(default)]
785 pub pos_side: String,
786 #[serde(default)]
788 pub cl_ord_id: String,
789 #[serde(default)]
791 pub tag: String,
792}
793
794#[derive(Debug, Clone, Deserialize)]
796#[serde(rename_all = "camelCase")]
797#[non_exhaustive]
798pub struct Order {
799 pub inst_id: String,
801 pub ord_id: String,
803 #[serde(default)]
805 pub cl_ord_id: String,
806 #[serde(default)]
808 pub px: NumberString,
809 #[serde(default)]
811 pub sz: NumberString,
812 pub ord_type: OrderType,
814 pub side: OrderSide,
816 pub pos_side: PositionSide,
818 pub td_mode: TradeMode,
820 #[serde(default)]
822 pub acc_fill_sz: NumberString,
823 #[serde(default)]
825 pub avg_px: NumberString,
826 pub state: OrderState,
828 #[serde(default)]
830 pub c_time: NumberString,
831}
832
833#[derive(Debug, Clone, Deserialize)]
835#[serde(rename_all = "camelCase")]
836#[non_exhaustive]
837pub struct Fill {
838 #[serde(default)]
840 pub inst_type: String,
841 pub inst_id: String,
843 #[serde(default)]
845 pub trade_id: String,
846 #[serde(default)]
848 pub ord_id: String,
849 #[serde(default)]
851 pub fill_px: NumberString,
852 #[serde(default)]
854 pub fill_sz: NumberString,
855 pub side: OrderSide,
857 pub ord_type: OrderType,
859 #[serde(default)]
861 pub fee_ccy: String,
862 #[serde(default)]
864 pub fee: NumberString,
865 #[serde(default)]
867 pub ts: NumberString,
868}
869
870#[cfg(test)]
871mod tests {
872 use crate::model::{OrderSide, OrderType, TradeMode};
873 use crate::test_util::MockTransport;
874 use crate::{Credentials, OkxClient};
875
876 use super::PlaceOrderRequest;
877
878 fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
879 OkxClient::with_transport(mock)
880 .credentials(Credentials::new("key", "secret", "pass"))
881 .build()
882 }
883
884 #[tokio::test]
885 async fn place_order_posts_signed_json_body() {
886 let body = r#"{"code":"0","msg":"","data":[
887 {"ordId":"312","clOrdId":"b1","tag":"","sCode":"0","sMsg":"","ts":"1597026383085"}]}"#;
888 let mock = MockTransport::new(body);
889 let client = signed_client(mock.clone());
890
891 let request = PlaceOrderRequest::new(
892 "BTC-USDT",
893 TradeMode::Cash,
894 OrderSide::Buy,
895 OrderType::Limit,
896 "0.01",
897 )
898 .price("42000")
899 .client_order_id("b1");
900
901 let result = client.trade().place_order(&request).await.unwrap();
902 assert_eq!(result[0].ord_id, "312");
903 assert_eq!(result[0].s_code, "0");
904
905 let req = mock.captured();
906 assert_eq!(req.method, http::Method::POST);
907 assert!(req.uri.ends_with("/api/v5/trade/order"));
908 assert!(req.is_signed());
909
910 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
911 assert_eq!(sent["instId"], "BTC-USDT");
912 assert_eq!(sent["tdMode"], "cash");
913 assert_eq!(sent["side"], "buy");
914 assert_eq!(sent["ordType"], "limit");
915 assert_eq!(sent["sz"], "0.01");
916 assert_eq!(sent["px"], "42000");
917 assert_eq!(sent["clOrdId"], "b1");
918 assert!(sent.get("reduceOnly").is_none());
920 }
921
922 #[tokio::test]
923 async fn place_multiple_orders_posts_array_body() {
924 let body = r#"{"code":"0","msg":"","data":[
925 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""},
926 {"ordId":"313","clOrdId":"b2","sCode":"0","sMsg":""}]}"#;
927 let mock = MockTransport::new(body);
928 let client = signed_client(mock.clone());
929 let requests = vec![
930 PlaceOrderRequest::new(
931 "BTC-USDT",
932 TradeMode::Cash,
933 OrderSide::Buy,
934 OrderType::Limit,
935 "0.01",
936 )
937 .price("42000")
938 .client_order_id("b1"),
939 PlaceOrderRequest::new(
940 "BTC-USDT",
941 TradeMode::Cash,
942 OrderSide::Sell,
943 OrderType::Limit,
944 "0.02",
945 )
946 .price("43000")
947 .client_order_id("b2"),
948 ];
949
950 let result = client
951 .trade()
952 .place_multiple_orders(&requests)
953 .await
954 .unwrap();
955 assert_eq!(result[1].ord_id, "313");
956
957 let req = mock.captured();
958 assert_eq!(req.method, http::Method::POST);
959 assert!(req.uri.ends_with("/api/v5/trade/batch-orders"));
960 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
961 assert_eq!(sent[0]["clOrdId"], "b1");
962 assert_eq!(sent[1]["side"], "sell");
963 assert!(req.is_signed());
964 }
965
966 #[tokio::test]
967 async fn cancel_order_posts_ids() {
968 let body = r#"{"code":"0","msg":"","data":[
969 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
970 let mock = MockTransport::new(body);
971 let client = signed_client(mock.clone());
972
973 let result = client.trade().cancel_order("BTC-USDT", "312").await.unwrap();
974 assert_eq!(result[0].ord_id, "312");
975
976 let req = mock.captured();
977 assert_eq!(req.method, http::Method::POST);
978 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
979 assert_eq!(sent["instId"], "BTC-USDT");
980 assert_eq!(sent["ordId"], "312");
981 }
982
983 #[tokio::test]
984 async fn cancel_multiple_orders_posts_array_body() {
985 let body = r#"{"code":"0","msg":"","data":[
986 {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
987 let mock = MockTransport::new(body);
988 let client = signed_client(mock.clone());
989 let requests = vec![super::CancelOrderRequest::by_order_id("BTC-USDT", "312")];
990
991 let result = client
992 .trade()
993 .cancel_multiple_orders(&requests)
994 .await
995 .unwrap();
996 assert_eq!(result[0].s_code, "0");
997
998 let req = mock.captured();
999 assert!(req.uri.ends_with("/api/v5/trade/cancel-batch-orders"));
1000 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1001 assert_eq!(sent[0]["instId"], "BTC-USDT");
1002 assert_eq!(sent[0]["ordId"], "312");
1003 assert!(sent[0].get("clOrdId").is_none());
1004 assert!(req.is_signed());
1005 }
1006
1007 #[tokio::test]
1008 async fn amend_order_posts_builder_body() {
1009 let body = r#"{"code":"0","msg":"","data":[
1010 {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1011 let mock = MockTransport::new(body);
1012 let client = signed_client(mock.clone());
1013 let request = super::AmendOrderRequest::new("BTC-USDT")
1014 .order_id("312")
1015 .request_id("r1")
1016 .new_price("42100");
1017
1018 let result = client.trade().amend_order(&request).await.unwrap();
1019 assert_eq!(result[0].req_id, "r1");
1020
1021 let req = mock.captured();
1022 assert!(req.uri.ends_with("/api/v5/trade/amend-order"));
1023 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1024 assert_eq!(sent["instId"], "BTC-USDT");
1025 assert_eq!(sent["ordId"], "312");
1026 assert_eq!(sent["newPx"], "42100");
1027 assert!(sent.get("newSz").is_none());
1028 assert!(req.is_signed());
1029 }
1030
1031 #[tokio::test]
1032 async fn amend_multiple_orders_posts_array_body() {
1033 let body = r#"{"code":"0","msg":"","data":[
1034 {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1035 let mock = MockTransport::new(body);
1036 let client = signed_client(mock.clone());
1037 let requests = vec![super::AmendOrderRequest::new("BTC-USDT")
1038 .client_order_id("b1")
1039 .new_size("0.03")];
1040
1041 let result = client
1042 .trade()
1043 .amend_multiple_orders(&requests)
1044 .await
1045 .unwrap();
1046 assert_eq!(result[0].s_code, "0");
1047
1048 let req = mock.captured();
1049 assert!(req.uri.ends_with("/api/v5/trade/amend-batch-orders"));
1050 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1051 assert_eq!(sent[0]["clOrdId"], "b1");
1052 assert_eq!(sent[0]["newSz"], "0.03");
1053 assert!(req.is_signed());
1054 }
1055
1056 #[tokio::test]
1057 async fn close_positions_posts_builder_body() {
1058 let body = r#"{"code":"0","msg":"","data":[
1059 {"instId":"BTC-USDT-SWAP","posSide":"long","clOrdId":"close1","tag":"t"}]}"#;
1060 let mock = MockTransport::new(body);
1061 let client = signed_client(mock.clone());
1062 let request = super::ClosePositionRequest::new("BTC-USDT-SWAP", TradeMode::Cross)
1063 .position_side(crate::model::PositionSide::Long)
1064 .auto_cancel(true)
1065 .client_order_id("close1");
1066
1067 let result = client.trade().close_positions(&request).await.unwrap();
1068 assert_eq!(result[0].cl_ord_id, "close1");
1069
1070 let req = mock.captured();
1071 assert!(req.uri.ends_with("/api/v5/trade/close-position"));
1072 let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1073 assert_eq!(sent["instId"], "BTC-USDT-SWAP");
1074 assert_eq!(sent["mgnMode"], "cross");
1075 assert_eq!(sent["posSide"], "long");
1076 assert_eq!(sent["autoCxl"], true);
1077 assert!(req.is_signed());
1078 }
1079
1080 #[tokio::test]
1081 async fn get_order_queries_and_parses() {
1082 let body = r#"{"code":"0","msg":"","data":[
1083 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1084 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1085 "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1086 let mock = MockTransport::new(body);
1087 let client = signed_client(mock.clone());
1088
1089 let orders = client.trade().get_order("BTC-USDT", "312").await.unwrap();
1090 assert_eq!(orders[0].ord_id, "312");
1091 assert_eq!(orders[0].state, crate::model::OrderState::Live);
1092 assert_eq!(orders[0].side, OrderSide::Buy);
1093
1094 let req = mock.captured();
1095 assert_eq!(req.method, http::Method::GET);
1096 assert_eq!(req.query(), Some("instId=BTC-USDT&ordId=312"));
1097 assert!(req.is_signed());
1098 }
1099
1100 #[tokio::test]
1101 async fn get_order_list_uses_builder_query() {
1102 let body = r#"{"code":"0","msg":"","data":[
1103 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1104 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1105 "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1106 let mock = MockTransport::new(body);
1107 let client = signed_client(mock.clone());
1108 let request = super::OrderListRequest::new()
1109 .inst_type(crate::model::InstType::Spot)
1110 .inst_id("BTC-USDT")
1111 .limit(1);
1112
1113 let orders = client.trade().get_order_list(&request).await.unwrap();
1114 assert_eq!(orders[0].ord_id, "312");
1115
1116 let req = mock.captured();
1117 assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT&limit=1"));
1118 assert!(req.is_signed());
1119 }
1120
1121 #[tokio::test]
1122 async fn get_orders_history_uses_builder_query() {
1123 let body = r#"{"code":"0","msg":"","data":[
1124 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1125 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1126 "accFillSz":"0","avgPx":"","state":"filled","cTime":"1597026383085"}]}"#;
1127 let mock = MockTransport::new(body);
1128 let client = signed_client(mock.clone());
1129 let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
1130 .begin("100")
1131 .end("200");
1132
1133 let orders = client.trade().get_orders_history(&request).await.unwrap();
1134 assert_eq!(orders[0].state, crate::model::OrderState::Filled);
1135
1136 let req = mock.captured();
1137 assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200"));
1138 assert!(req.is_signed());
1139 }
1140
1141 #[tokio::test]
1142 async fn get_orders_history_archive_uses_builder_query() {
1143 let body = r#"{"code":"0","msg":"","data":[
1144 {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1145 "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1146 "accFillSz":"0","avgPx":"","state":"canceled","cTime":"1597026383085"}]}"#;
1147 let mock = MockTransport::new(body);
1148 let client = signed_client(mock.clone());
1149 let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
1150 .filters(super::OrderListRequest::new().inst_type(crate::model::InstType::Spot).limit(1));
1151
1152 let orders = client
1153 .trade()
1154 .get_orders_history_archive(&request)
1155 .await
1156 .unwrap();
1157 assert_eq!(orders[0].state, crate::model::OrderState::Canceled);
1158
1159 let req = mock.captured();
1160 assert_eq!(req.query(), Some("instType=SPOT&limit=1"));
1161 assert!(req.is_signed());
1162 }
1163
1164 #[tokio::test]
1165 async fn get_fills_uses_builder_query() {
1166 let body = r#"{"code":"0","msg":"","data":[
1167 {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1168 "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1169 "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1170 let mock = MockTransport::new(body);
1171 let client = signed_client(mock.clone());
1172 let request = super::FillsRequest::new()
1173 .inst_type(crate::model::InstType::Spot)
1174 .order_id("312");
1175
1176 let fills = client.trade().get_fills(&request).await.unwrap();
1177 assert_eq!(fills[0].trade_id, "t1");
1178
1179 let req = mock.captured();
1180 assert_eq!(req.query(), Some("instType=SPOT&ordId=312"));
1181 assert!(req.is_signed());
1182 }
1183
1184 #[tokio::test]
1185 async fn get_fills_history_uses_builder_query() {
1186 let body = r#"{"code":"0","msg":"","data":[
1187 {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1188 "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1189 "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1190 let mock = MockTransport::new(body);
1191 let client = signed_client(mock.clone());
1192 let request = super::FillsRequest::new()
1193 .inst_type(crate::model::InstType::Spot)
1194 .begin("100")
1195 .end("200")
1196 .limit(1);
1197
1198 let fills = client.trade().get_fills_history(&request).await.unwrap();
1199 assert_eq!(fills[0].fee.as_str(), "-1");
1200
1201 let req = mock.captured();
1202 assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200&limit=1"));
1203 assert!(req.is_signed());
1204 }
1205}