1use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::parse::{deserialize_empty_string_as_none, deserialize_empty_ustr_as_none};
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct OKXTrade {
27 pub inst_id: Ustr,
29 pub px: String,
31 pub sz: String,
33 pub side: OKXSide,
35 pub trade_id: Ustr,
37 #[serde(deserialize_with = "deserialize_string_to_u64")]
39 pub ts: u64,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct OKXCandlestick(
46 pub String,
48 pub String,
50 pub String,
52 pub String,
54 pub String,
56 pub String,
58 pub String,
60 pub String,
62 pub String,
64);
65
66use crate::common::{
67 enums::{
68 OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode, OKXOrderStatus,
69 OKXOrderType, OKXPositionSide, OKXSide, OKXTradeMode, OKXTriggerType, OKXVipLevel,
70 },
71 parse::deserialize_string_to_u64,
72};
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct OKXMarkPrice {
78 pub uly: Option<Ustr>,
80 pub inst_id: Ustr,
82 pub mark_px: String,
84 #[serde(deserialize_with = "deserialize_string_to_u64")]
86 pub ts: u64,
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct OKXIndexTicker {
93 pub inst_id: Ustr,
95 pub idx_px: String,
97 #[serde(deserialize_with = "deserialize_string_to_u64")]
99 pub ts: u64,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct OKXPositionTier {
106 pub uly: Ustr,
108 pub inst_family: String,
110 pub inst_id: Ustr,
112 pub tier: String,
114 pub min_sz: String,
116 pub max_sz: String,
118 pub mmr: String,
120 pub imr: String,
122 pub max_lever: String,
124 pub opt_mgn_factor: String,
126 pub quote_max_loan: String,
128 pub base_max_loan: String,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct OKXAccount {
136 pub adj_eq: String,
138 pub borrow_froz: String,
140 pub details: Vec<OKXBalanceDetail>,
142 pub imr: String,
144 pub iso_eq: String,
146 pub mgn_ratio: String,
148 pub mmr: String,
150 pub notional_usd_for_borrow: String,
152 pub notional_usd_for_futures: String,
154 pub notional_usd_for_option: String,
156 pub notional_usd_for_swap: String,
158 pub notional_usd: String,
160 pub ord_froz: String,
162 pub total_eq: String,
164 #[serde(deserialize_with = "deserialize_string_to_u64")]
166 pub u_time: u64,
167 pub upl: String,
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct OKXBalanceDetail {
175 pub avail_bal: String,
177 pub avail_eq: String,
179 pub borrow_froz: String,
181 pub cash_bal: String,
183 pub ccy: Ustr,
185 pub cross_liab: String,
187 pub dis_eq: String,
189 pub eq: String,
191 pub eq_usd: String,
193 pub smt_sync_eq: String,
195 pub spot_copy_trading_eq: String,
197 pub fixed_bal: String,
199 pub frozen_bal: String,
201 pub imr: String,
203 pub interest: String,
205 pub iso_eq: String,
207 pub iso_liab: String,
209 pub iso_upl: String,
211 pub liab: String,
213 pub max_loan: String,
215 pub mgn_ratio: String,
217 pub mmr: String,
219 pub notional_lever: String,
221 pub ord_frozen: String,
223 pub reward_bal: String,
225 #[serde(alias = "spotInUse")]
227 pub spot_in_use_amt: String,
228 #[serde(alias = "clSpotInUse")]
230 pub cl_spot_in_use_amt: String,
231 #[serde(alias = "maxSpotInUse")]
233 pub max_spot_in_use_amt: String,
234 pub spot_iso_bal: String,
236 pub stgy_eq: String,
238 pub twap: String,
240 #[serde(deserialize_with = "deserialize_string_to_u64")]
242 pub u_time: u64,
243 pub upl: String,
245 pub upl_liab: String,
247 pub spot_bal: String,
249 pub open_avg_px: String,
251 pub acc_avg_px: String,
253 pub spot_upl: String,
255 pub spot_upl_ratio: String,
257 pub total_pnl: String,
259 pub total_pnl_ratio: String,
261}
262
263#[derive(Clone, Debug, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct OKXPosition {
267 pub inst_id: Ustr,
269 pub inst_type: OKXInstrumentType,
271 pub mgn_mode: OKXMarginMode,
273 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
275 pub pos_id: Option<Ustr>,
276 pub pos_side: OKXPositionSide,
278 pub pos: String,
280 pub base_bal: String,
282 pub ccy: String,
284 pub fee: String,
286 pub lever: String,
288 pub last: String,
290 pub mark_px: String,
292 pub liq_px: String,
294 pub mmr: String,
296 pub interest: String,
298 pub trade_id: Ustr,
300 pub notional_usd: String,
302 pub avg_px: String,
304 pub upl: String,
306 pub upl_ratio: String,
308 #[serde(deserialize_with = "deserialize_string_to_u64")]
310 pub u_time: u64,
311 pub margin: String,
313 pub mgn_ratio: String,
315 pub adl: String,
317 pub c_time: String,
319 pub realized_pnl: String,
321 pub upl_last_px: String,
323 pub upl_ratio_last_px: String,
325 pub avail_pos: String,
327 pub be_px: String,
329 pub funding_fee: String,
331 pub idx_px: String,
333 pub liq_penalty: String,
335 pub opt_val: String,
337 pub pending_close_ord_liab_val: String,
339 pub pnl: String,
341 pub pos_ccy: String,
343 pub quote_bal: String,
345 pub quote_borrowed: String,
347 pub quote_interest: String,
349 #[serde(alias = "spotInUse")]
351 pub spot_in_use_amt: String,
352 pub spot_in_use_ccy: String,
354 pub usd_px: String,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct OKXPlaceOrderResponse {
363 #[serde(default)]
365 pub ord_id: Option<Ustr>,
366 #[serde(default)]
368 pub cl_ord_id: Option<Ustr>,
369 #[serde(default)]
371 pub tag: Option<String>,
372 #[serde(default)]
374 pub inst_id: Option<Ustr>,
375 #[serde(default)]
377 pub side: Option<OKXSide>,
378 #[serde(default)]
380 pub ord_type: Option<OKXOrderType>,
381 #[serde(default)]
383 pub sz: Option<String>,
384 pub state: Option<OKXOrderStatus>,
386 #[serde(default)]
388 pub px: Option<String>,
389 #[serde(default)]
391 pub avg_px: Option<String>,
392 #[serde(default)]
394 pub acc_fill_sz: Option<String>,
395 #[serde(default)]
397 pub fill_sz: Option<String>,
398 #[serde(default)]
400 pub fill_px: Option<String>,
401 #[serde(default)]
403 pub trade_id: Option<Ustr>,
404 #[serde(default)]
406 pub fill_time: Option<String>,
407 #[serde(default)]
409 pub fee: Option<String>,
410 #[serde(default)]
412 pub fee_ccy: Option<String>,
413 #[serde(default)]
415 pub req_id: Option<Ustr>,
416 #[serde(default)]
418 pub pos_side: Option<OKXPositionSide>,
419 #[serde(default)]
421 pub reduce_only: Option<String>,
422 #[serde(default)]
424 pub tgt_ccy: Option<String>,
425 #[serde(default)]
427 pub c_time: Option<String>,
428 #[serde(default)]
430 pub u_time: Option<String>,
431}
432
433#[derive(Clone, Debug, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct OKXOrderHistory {
437 pub ord_id: Ustr,
439 pub cl_ord_id: Ustr,
441 #[serde(default)]
443 pub algo_id: Option<Ustr>,
444 #[serde(default)]
446 pub algo_cl_ord_id: Option<Ustr>,
447 #[serde(default)]
449 pub cl_act_id: Option<Ustr>,
450 pub tag: String,
452 pub inst_type: OKXInstrumentType,
454 pub uly: Option<Ustr>,
456 pub inst_id: Ustr,
458 pub ord_type: OKXOrderType,
460 pub sz: String,
462 pub px: String,
464 pub side: OKXSide,
466 pub pos_side: OKXPositionSide,
468 pub td_mode: OKXTradeMode,
470 pub reduce_only: String,
472 pub tgt_ccy: String,
474 pub state: OKXOrderStatus,
476 pub avg_px: String,
478 pub fee: String,
480 pub fee_ccy: String,
482 pub fill_sz: String,
484 pub fill_px: String,
486 pub trade_id: Ustr,
488 #[serde(deserialize_with = "deserialize_string_to_u64")]
490 pub fill_time: u64,
491 pub acc_fill_sz: String,
493 #[serde(default)]
495 pub fill_fee: Option<String>,
496 #[serde(default)]
498 pub req_id: Option<Ustr>,
499 #[serde(default)]
501 pub cancel_fill_sz: Option<String>,
502 #[serde(default)]
504 pub cancel_total_sz: Option<String>,
505 #[serde(default)]
507 pub fee_discount: Option<String>,
508 pub category: String,
510 #[serde(deserialize_with = "deserialize_string_to_u64")]
512 pub u_time: u64,
513 #[serde(deserialize_with = "deserialize_string_to_u64")]
515 pub c_time: u64,
516}
517
518#[derive(Clone, Debug, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct OKXOrderAlgo {
522 pub algo_id: String,
524 #[serde(default)]
526 pub algo_cl_ord_id: String,
527 #[serde(default)]
529 pub cl_ord_id: String,
530 #[serde(default)]
532 pub ord_id: String,
533 pub inst_id: Ustr,
535 pub inst_type: OKXInstrumentType,
537 pub ord_type: OKXOrderType,
539 pub state: OKXOrderStatus,
541 pub side: OKXSide,
543 pub pos_side: OKXPositionSide,
545 pub sz: String,
547 #[serde(default)]
549 pub trigger_px: String,
550 #[serde(default)]
552 pub trigger_px_type: Option<OKXTriggerType>,
553 #[serde(default)]
555 pub ord_px: String,
556 pub td_mode: OKXTradeMode,
558 #[serde(default)]
560 pub lever: String,
561 #[serde(default)]
563 pub reduce_only: String,
564 #[serde(default)]
566 pub actual_px: String,
567 #[serde(default)]
569 pub actual_sz: String,
570 #[serde(default)]
572 pub notional_usd: String,
573 #[serde(deserialize_with = "deserialize_string_to_u64")]
575 pub c_time: u64,
576 #[serde(deserialize_with = "deserialize_string_to_u64")]
578 pub u_time: u64,
579 #[serde(default)]
581 pub trigger_time: String,
582 #[serde(default)]
584 pub tag: String,
585}
586
587#[derive(Clone, Debug, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct OKXTransactionDetail {
591 pub inst_type: OKXInstrumentType,
593 pub inst_id: Ustr,
595 pub trade_id: Ustr,
597 pub ord_id: Ustr,
599 pub cl_ord_id: Ustr,
601 pub bill_id: Ustr,
603 pub fill_px: String,
605 pub fill_sz: String,
607 pub side: OKXSide,
609 pub exec_type: OKXExecType,
611 pub fee_ccy: String,
613 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
615 pub fee: Option<String>,
616 #[serde(deserialize_with = "deserialize_string_to_u64")]
618 pub ts: u64,
619}
620
621#[derive(Clone, Debug, Serialize, Deserialize)]
623#[serde(rename_all = "camelCase")]
624pub struct OKXPositionHistory {
625 pub inst_type: OKXInstrumentType,
627 pub inst_id: Ustr,
629 pub mgn_mode: OKXMarginMode,
631 #[serde(rename = "type")]
634 pub r#type: Ustr,
635 pub c_time: String,
637 #[serde(deserialize_with = "deserialize_string_to_u64")]
639 pub u_time: u64,
640 pub open_avg_px: String,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 pub close_avg_px: Option<String>,
645 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
647 pub pos_id: Option<Ustr>,
648 #[serde(skip_serializing_if = "Option::is_none")]
650 pub open_max_pos: Option<String>,
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub close_total_pos: Option<String>,
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub realized_pnl: Option<String>,
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub fee: Option<String>,
660 #[serde(skip_serializing_if = "Option::is_none")]
662 pub funding_fee: Option<String>,
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub liq_penalty: Option<String>,
666 #[serde(skip_serializing_if = "Option::is_none")]
668 pub pnl: Option<String>,
669 #[serde(skip_serializing_if = "Option::is_none")]
671 pub pnl_ratio: Option<String>,
672 pub pos_side: OKXPositionSide,
674 pub lever: String,
676 #[serde(skip_serializing_if = "Option::is_none")]
678 pub direction: Option<String>,
679 #[serde(skip_serializing_if = "Option::is_none")]
681 pub trigger_px: Option<String>,
682 #[serde(skip_serializing_if = "Option::is_none")]
684 pub uly: Option<String>,
685 #[serde(skip_serializing_if = "Option::is_none")]
687 pub ccy: Option<String>,
688}
689
690#[derive(Clone, Debug, Serialize, Deserialize)]
692#[serde(rename_all = "camelCase")]
693pub struct OKXPlaceAlgoOrderRequest {
694 #[serde(rename = "instId")]
696 pub inst_id: String,
697 #[serde(rename = "tdMode")]
699 pub td_mode: OKXTradeMode,
700 pub side: OKXSide,
702 #[serde(rename = "ordType")]
704 pub ord_type: OKXAlgoOrderType,
705 pub sz: String,
707 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
709 pub algo_cl_ord_id: Option<String>,
710 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
712 pub trigger_px: Option<String>,
713 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
715 pub order_px: Option<String>,
716 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
718 pub trigger_px_type: Option<OKXTriggerType>,
719 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
721 pub tgt_ccy: Option<String>,
722 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
724 pub pos_side: Option<OKXPositionSide>,
725 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
727 pub close_position: Option<bool>,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub tag: Option<String>,
731 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
733 pub reduce_only: Option<bool>,
734}
735
736#[derive(Clone, Debug, Serialize, Deserialize)]
738#[serde(rename_all = "camelCase")]
739pub struct OKXPlaceAlgoOrderResponse {
740 pub algo_id: String,
742 #[serde(skip_serializing_if = "Option::is_none")]
744 pub algo_cl_ord_id: Option<String>,
745 #[serde(skip_serializing_if = "Option::is_none")]
747 pub s_code: Option<String>,
748 #[serde(skip_serializing_if = "Option::is_none")]
750 pub s_msg: Option<String>,
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub req_id: Option<String>,
754}
755
756#[derive(Clone, Debug, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct OKXCancelAlgoOrderRequest {
760 pub inst_id: String,
762 #[serde(skip_serializing_if = "Option::is_none")]
764 pub algo_id: Option<String>,
765 #[serde(skip_serializing_if = "Option::is_none")]
767 pub algo_cl_ord_id: Option<String>,
768}
769
770#[derive(Clone, Debug, Serialize, Deserialize)]
772#[serde(rename_all = "camelCase")]
773pub struct OKXCancelAlgoOrderResponse {
774 pub algo_id: String,
776 #[serde(skip_serializing_if = "Option::is_none")]
778 pub s_code: Option<String>,
779 #[serde(skip_serializing_if = "Option::is_none")]
781 pub s_msg: Option<String>,
782}
783
784#[derive(Clone, Debug, Serialize, Deserialize)]
786#[serde(rename_all = "camelCase")]
787pub struct OKXServerTime {
788 #[serde(deserialize_with = "deserialize_string_to_u64")]
790 pub ts: u64,
791}
792
793#[derive(Clone, Debug, Serialize, Deserialize)]
795#[serde(rename_all = "camelCase")]
796pub struct OKXFeeRate {
797 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
799 pub level: OKXVipLevel,
800 pub taker: String,
802 pub maker: String,
804 pub taker_u: String,
806 pub maker_u: String,
808 #[serde(default)]
810 pub delivery: String,
811 #[serde(default)]
813 pub exercise: String,
814 pub inst_type: OKXInstrumentType,
816 #[serde(default)]
818 pub category: String,
819 #[serde(deserialize_with = "deserialize_string_to_u64")]
821 pub ts: u64,
822}
823
824#[cfg(test)]
829mod tests {
830 use rstest::rstest;
831 use serde_json;
832
833 use super::*;
834
835 #[rstest]
836 fn test_algo_order_request_serialization() {
837 let request = OKXPlaceAlgoOrderRequest {
838 inst_id: "ETH-USDT-SWAP".to_string(),
839 td_mode: OKXTradeMode::Isolated,
840 side: OKXSide::Buy,
841 ord_type: OKXAlgoOrderType::Trigger,
842 sz: "0.01".to_string(),
843 algo_cl_ord_id: Some("test123".to_string()),
844 trigger_px: Some("3000".to_string()),
845 order_px: Some("-1".to_string()),
846 trigger_px_type: Some(OKXTriggerType::Last),
847 tgt_ccy: None,
848 pos_side: None,
849 close_position: None,
850 tag: None,
851 reduce_only: None,
852 };
853
854 let json = serde_json::to_string(&request).unwrap();
855
856 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
858 assert!(json.contains("\"tdMode\":\"isolated\""));
859 assert!(json.contains("\"ordType\":\"trigger\""));
860 assert!(json.contains("\"algoClOrdId\":\"test123\""));
861 assert!(json.contains("\"triggerPx\":\"3000\""));
862 assert!(json.contains("\"orderPx\":\"-1\""));
863 assert!(json.contains("\"triggerPxType\":\"last\""));
864
865 assert!(!json.contains("tgtCcy"));
867 assert!(!json.contains("posSide"));
868 assert!(!json.contains("closePosition"));
869 }
870
871 #[rstest]
872 fn test_algo_order_request_array_serialization() {
873 let request = OKXPlaceAlgoOrderRequest {
874 inst_id: "BTC-USDT".to_string(),
875 td_mode: OKXTradeMode::Cross,
876 side: OKXSide::Sell,
877 ord_type: OKXAlgoOrderType::Trigger,
878 sz: "0.1".to_string(),
879 algo_cl_ord_id: None,
880 trigger_px: Some("50000".to_string()),
881 order_px: Some("49900".to_string()),
882 trigger_px_type: Some(OKXTriggerType::Mark),
883 tgt_ccy: Some("base_ccy".to_string()),
884 pos_side: Some(OKXPositionSide::Net),
885 close_position: None,
886 tag: None,
887 reduce_only: Some(true),
888 };
889
890 let json = serde_json::to_string(&[request]).unwrap();
892
893 assert!(json.starts_with('['));
895 assert!(json.ends_with(']'));
896
897 assert!(json.contains("\"instId\":\"BTC-USDT\""));
899 assert!(json.contains("\"tdMode\":\"cross\""));
900 assert!(json.contains("\"triggerPx\":\"50000\""));
901 assert!(json.contains("\"orderPx\":\"49900\""));
902 assert!(json.contains("\"triggerPxType\":\"mark\""));
903 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
904 assert!(json.contains("\"posSide\":\"net\""));
905 assert!(json.contains("\"reduceOnly\":true"));
906 }
907
908 #[rstest]
909 fn test_cancel_algo_order_request_serialization() {
910 let request = OKXCancelAlgoOrderRequest {
911 inst_id: "ETH-USDT-SWAP".to_string(),
912 algo_id: Some("123456".to_string()),
913 algo_cl_ord_id: None,
914 };
915
916 let json = serde_json::to_string(&request).unwrap();
917
918 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
920 assert!(json.contains("\"algoId\":\"123456\""));
921 assert!(!json.contains("algoClOrdId"));
922 }
923
924 #[rstest]
925 fn test_cancel_algo_order_with_client_id_serialization() {
926 let request = OKXCancelAlgoOrderRequest {
927 inst_id: "BTC-USDT".to_string(),
928 algo_id: None,
929 algo_cl_ord_id: Some("client123".to_string()),
930 };
931
932 let json = serde_json::to_string(&[request]).unwrap();
934
935 assert!(json.starts_with('['));
937 assert!(json.contains("\"instId\":\"BTC-USDT\""));
938 assert!(json.contains("\"algoClOrdId\":\"client123\""));
939 assert!(!json.contains("\"algoId\""));
940 }
941}