1use nautilus_core::serialization::deserialize_optional_decimal;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Deserializer, Serialize, de::IntoDeserializer};
21use ustr::Ustr;
22
23use crate::common::{
24 models::OKXRpiBookLevel,
25 parse::{
26 deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
27 deserialize_optional_string_to_u64, deserialize_target_currency_as_none,
28 },
29};
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct OKXTrade {
35 pub inst_id: Ustr,
37 pub px: String,
39 pub sz: String,
41 pub side: OKXSide,
43 pub trade_id: Ustr,
45 #[serde(deserialize_with = "deserialize_string_to_u64")]
47 pub ts: u64,
48 #[serde(default)]
50 pub source: Option<String>,
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct OKXCandlestick(
57 pub String,
59 pub String,
61 pub String,
63 pub String,
65 pub String,
67 pub String,
69 pub String,
71 pub String,
73 pub String,
75);
76
77use crate::common::{
78 enums::{
79 OKXAccountLevel, OKXAlgoOrderStatus, OKXAlgoOrderType, OKXApiKeyPermission, OKXExecType,
80 OKXFeeType, OKXInstrumentType, OKXMarginMode, OKXOrderCategory, OKXOrderStatus,
81 OKXOrderType, OKXPositionMode, OKXPositionSide, OKXSide, OKXSpreadState, OKXSpreadType,
82 OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
83 },
84 parse::deserialize_string_to_u64,
85};
86
87#[derive(Clone, Debug, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct OKXMarkPrice {
91 pub uly: Option<Ustr>,
93 pub inst_id: Ustr,
95 pub mark_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 OKXPriceLimit {
106 pub inst_type: OKXInstrumentType,
108 pub inst_id: Ustr,
110 pub buy_lmt: String,
112 pub sell_lmt: String,
114 #[serde(deserialize_with = "deserialize_string_to_u64")]
116 pub ts: u64,
117 #[serde(default)]
119 pub enabled: bool,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct OKXOptionSummary {
126 pub inst_type: OKXInstrumentType,
128 pub inst_id: Ustr,
130 pub uly: Ustr,
132 pub bid_vol: String,
134 pub ask_vol: String,
136 pub mark_vol: String,
138 pub fwd_px: String,
140 #[serde(deserialize_with = "deserialize_string_to_u64")]
142 pub ts: u64,
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct OKXSpread {
149 pub sprd_id: Ustr,
151 pub sprd_type: OKXSpreadType,
153 pub state: OKXSpreadState,
155 pub base_ccy: Ustr,
157 pub sz_ccy: Ustr,
159 pub quote_ccy: Ustr,
161 pub tick_sz: String,
163 pub min_sz: String,
165 pub lot_sz: String,
167 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
169 pub list_time: Option<u64>,
170 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
172 pub exp_time: Option<u64>,
173 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
175 pub u_time: Option<u64>,
176 pub legs: Vec<OKXSpreadLeg>,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct OKXSpreadLeg {
184 pub inst_id: Ustr,
186 pub side: OKXSide,
188}
189
190#[derive(Clone, Debug, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct OKXPlaceSpreadOrderRequest {
194 pub sprd_id: String,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub cl_ord_id: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub tag: Option<String>,
202 pub side: OKXSide,
204 pub ord_type: OKXOrderType,
206 pub sz: String,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub px: Option<String>,
211}
212
213#[derive(Clone, Debug, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct OKXCancelSpreadOrderRequest {
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub ord_id: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub cl_ord_id: Option<String>,
223}
224
225#[derive(Clone, Debug, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct OKXCancelAllSpreadOrdersRequest {
229 pub sprd_id: String,
231}
232
233#[derive(Clone, Debug, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct OKXSpreadOrder {
237 pub sprd_id: Ustr,
239 pub ord_id: Ustr,
241 #[serde(default)]
243 pub cl_ord_id: Ustr,
244 #[serde(default)]
246 pub tag: String,
247 pub side: OKXSide,
249 pub ord_type: OKXOrderType,
251 pub sz: String,
253 #[serde(default)]
255 pub px: String,
256 #[serde(default)]
258 pub avg_px: String,
259 pub state: OKXOrderStatus,
261 #[serde(default)]
263 pub acc_fill_sz: String,
264 #[serde(default)]
266 pub pending_fill_sz: String,
267 #[serde(default)]
269 pub pending_settle_sz: String,
270 #[serde(default)]
272 pub canceled_sz: String,
273 #[serde(default)]
275 pub fill_sz: String,
276 #[serde(default)]
278 pub fill_px: String,
279 #[serde(default)]
281 pub trade_id: Ustr,
282 #[serde(default)]
284 pub cancel_source: String,
285 #[serde(default)]
287 pub req_id: String,
288 #[serde(default)]
290 pub amend_result: String,
291 #[serde(default)]
293 pub code: String,
294 #[serde(default)]
296 pub msg: String,
297 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
299 pub c_time: Option<u64>,
300 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
302 pub u_time: Option<u64>,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct OKXSpreadTrade {
309 pub sprd_id: Ustr,
311 pub trade_id: Ustr,
313 pub ord_id: Ustr,
315 #[serde(default)]
317 pub cl_ord_id: Ustr,
318 pub fill_px: String,
320 pub fill_sz: String,
322 pub side: OKXSide,
324 #[serde(default)]
326 pub exec_type: OKXExecType,
327 #[serde(default)]
329 pub fee_ccy: String,
330 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
332 pub fee: Option<String>,
333 #[serde(deserialize_with = "deserialize_string_to_u64")]
335 pub ts: u64,
336}
337
338#[derive(Clone, Debug, Default, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct OKXEventContractSettlement {
342 #[serde(default)]
344 pub method: String,
345 #[serde(default)]
347 pub close_early: bool,
348 #[serde(default)]
350 pub src_name: String,
351 #[serde(default)]
353 pub underlying: String,
354}
355
356#[derive(Clone, Debug, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct OKXEventContractSeries {
360 pub series_id: String,
362 #[serde(default)]
364 pub freq: String,
365 #[serde(default)]
367 pub title: String,
368 #[serde(default)]
370 pub category: String,
371 #[serde(default)]
373 pub settlement: OKXEventContractSettlement,
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct OKXEventContractEvent {
380 pub series_id: String,
382 pub event_id: String,
384 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
386 pub fix_time: Option<u64>,
387 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
389 pub exp_time: Option<u64>,
390 pub state: String,
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct OKXEventContractMarket {
398 pub series_id: String,
400 pub event_id: String,
402 pub inst_id: Ustr,
404 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
406 pub list_time: Option<u64>,
407 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
409 pub fix_time: Option<u64>,
410 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
412 pub exp_time: Option<u64>,
413 pub state: String,
415 pub disputed: bool,
417 pub outcome: String,
419 pub floor_strike: String,
421 #[serde(default)]
423 pub cap_strike: String,
424 pub settle_value: String,
426 #[serde(default)]
428 pub hit_dir: String,
429}
430
431#[derive(Clone, Debug, Serialize, Deserialize)]
433#[serde(rename_all = "camelCase")]
434pub struct OKXIndexTicker {
435 pub inst_id: Ustr,
437 pub idx_px: String,
439 #[serde(deserialize_with = "deserialize_string_to_u64")]
441 pub ts: u64,
442}
443
444pub type OKXOrderBookLevel = (String, String, String, String);
447
448#[derive(Clone, Debug, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct OKXOrderBookSnapshot {
452 pub asks: Vec<OKXOrderBookLevel>,
454 pub bids: Vec<OKXOrderBookLevel>,
456 #[serde(deserialize_with = "deserialize_string_to_u64")]
458 pub ts: u64,
459}
460
461#[derive(Clone, Debug, Serialize, Deserialize)]
463#[serde(rename_all = "camelCase")]
464pub struct OKXRpiOrderBookSnapshot {
465 pub asks: Vec<OKXRpiBookLevel>,
467 pub bids: Vec<OKXRpiBookLevel>,
469 pub seq_id: u64,
471 #[serde(deserialize_with = "deserialize_string_to_u64")]
473 pub ts: u64,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct OKXFundingRateHistory {
480 pub inst_type: OKXInstrumentType,
482 pub inst_id: Ustr,
484 pub funding_rate: String,
486 pub realized_rate: String,
488 #[serde(deserialize_with = "deserialize_string_to_u64")]
490 pub funding_time: u64,
491 #[serde(default)]
493 pub method: Option<String>,
494}
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct OKXPositionTier {
500 pub uly: Ustr,
502 pub inst_family: String,
504 pub inst_id: Ustr,
506 pub tier: String,
508 pub min_sz: String,
510 pub max_sz: String,
512 pub mmr: String,
514 pub imr: String,
516 pub max_lever: String,
518 pub opt_mgn_factor: String,
520 pub quote_max_loan: String,
522 pub base_max_loan: String,
524}
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct OKXAccountConfiguration {
533 #[serde(rename = "acctLv", deserialize_with = "deserialize_configuration_enum")]
535 pub account_level: OKXAccountLevel,
536 #[serde(
538 rename = "posMode",
539 deserialize_with = "deserialize_configuration_enum"
540 )]
541 pub position_mode: OKXPositionMode,
542 pub auto_loan: bool,
544 #[serde(deserialize_with = "deserialize_configuration_enum")]
546 pub fee_type: OKXFeeType,
547 #[serde(rename = "perm", with = "account_permissions")]
549 pub permissions: Vec<OKXApiKeyPermission>,
550}
551
552#[derive(Clone, Debug, Serialize, Deserialize)]
558#[serde(rename_all = "camelCase")]
559pub struct OKXAccountBill {
560 pub bill_id: String,
562 pub ccy: String,
564 pub ts: String,
566 pub bal: String,
568 pub bal_chg: String,
570 #[serde(rename = "type")]
572 pub bill_type: String,
573 pub sub_type: String,
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub inst_type: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub inst_id: Option<String>,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub mgn_mode: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub pos_bal: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub pos_bal_chg: Option<String>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub sz: Option<String>,
593 #[serde(skip_serializing_if = "Option::is_none")]
595 pub px: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none")]
598 pub pnl: Option<String>,
599 #[serde(skip_serializing_if = "Option::is_none")]
601 pub fee: Option<String>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub earn_amt: Option<String>,
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub earn_apr: Option<String>,
608 #[serde(skip_serializing_if = "Option::is_none")]
610 pub ord_id: Option<String>,
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub cl_ord_id: Option<String>,
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub trade_id: Option<String>,
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub exec_type: Option<String>,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub from: Option<String>,
623 #[serde(skip_serializing_if = "Option::is_none")]
625 pub to: Option<String>,
626 #[serde(skip_serializing_if = "Option::is_none")]
628 pub notes: Option<String>,
629 #[serde(skip_serializing_if = "Option::is_none")]
631 pub interest: Option<String>,
632 #[serde(skip_serializing_if = "Option::is_none")]
634 pub tag: Option<String>,
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub fill_time: Option<String>,
638 #[serde(skip_serializing_if = "Option::is_none")]
640 pub fill_idx_px: Option<String>,
641 #[serde(skip_serializing_if = "Option::is_none")]
643 pub fill_mark_px: Option<String>,
644 #[serde(skip_serializing_if = "Option::is_none")]
646 pub fill_px_vol: Option<String>,
647 #[serde(skip_serializing_if = "Option::is_none")]
649 pub fill_px_usd: Option<String>,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub fill_mark_vol: Option<String>,
653 #[serde(skip_serializing_if = "Option::is_none")]
655 pub fill_fwd_px: Option<String>,
656}
657
658#[derive(Clone, Debug, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase")]
661pub struct OKXAccount {
662 pub adj_eq: String,
664 pub borrow_froz: String,
666 pub details: Vec<OKXBalanceDetail>,
668 pub imr: String,
670 pub iso_eq: String,
672 pub mgn_ratio: String,
674 pub mmr: String,
676 pub notional_usd_for_borrow: String,
678 pub notional_usd_for_futures: String,
680 pub notional_usd_for_option: String,
682 pub notional_usd_for_swap: String,
684 pub notional_usd: String,
686 pub ord_froz: String,
688 pub total_eq: String,
690 #[serde(deserialize_with = "deserialize_string_to_u64")]
692 pub u_time: u64,
693 pub upl: String,
695}
696
697#[derive(Clone, Debug, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
701#[cfg_attr(
702 feature = "python",
703 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
704)]
705pub struct OKXBalanceDetail {
706 pub avail_bal: String,
708 pub avail_eq: String,
710 pub borrow_froz: String,
712 pub cash_bal: String,
714 pub ccy: Ustr,
716 pub cross_liab: String,
718 pub dis_eq: String,
720 pub eq: String,
722 pub eq_usd: String,
724 pub smt_sync_eq: String,
726 pub spot_copy_trading_eq: String,
728 pub fixed_bal: String,
730 pub frozen_bal: String,
732 pub imr: String,
734 pub interest: String,
736 pub iso_eq: String,
738 pub iso_liab: String,
740 pub iso_upl: String,
742 pub liab: String,
744 pub max_loan: String,
746 pub mgn_ratio: String,
748 pub mmr: String,
750 pub notional_lever: String,
752 pub ord_frozen: String,
754 pub reward_bal: String,
756 #[serde(alias = "spotInUse")]
758 pub spot_in_use_amt: String,
759 #[serde(alias = "clSpotInUse")]
761 pub cl_spot_in_use_amt: String,
762 #[serde(alias = "maxSpotInUse")]
764 pub max_spot_in_use_amt: String,
765 pub spot_iso_bal: String,
767 pub stgy_eq: String,
769 pub twap: String,
771 #[serde(deserialize_with = "deserialize_string_to_u64")]
773 pub u_time: u64,
774 pub upl: String,
776 pub upl_liab: String,
778 pub spot_bal: String,
780 pub open_avg_px: String,
782 pub acc_avg_px: String,
784 pub spot_upl: String,
786 pub spot_upl_ratio: String,
788 pub total_pnl: String,
790 pub total_pnl_ratio: String,
792}
793
794#[derive(Clone, Debug, Serialize, Deserialize)]
796#[serde(rename_all = "camelCase")]
797pub struct OKXPosition {
798 pub inst_id: Ustr,
800 pub inst_type: OKXInstrumentType,
802 pub mgn_mode: OKXMarginMode,
804 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
806 pub pos_id: Option<Ustr>,
807 pub pos_side: OKXPositionSide,
809 pub pos: String,
811 pub base_bal: String,
813 pub ccy: String,
815 pub fee: String,
817 pub lever: String,
819 pub last: String,
821 pub mark_px: String,
823 pub liq_px: String,
825 pub mmr: String,
827 pub interest: String,
829 pub trade_id: Ustr,
831 pub notional_usd: String,
833 pub avg_px: String,
835 pub upl: String,
837 pub upl_ratio: String,
839 #[serde(deserialize_with = "deserialize_string_to_u64")]
841 pub u_time: u64,
842 pub margin: String,
844 pub mgn_ratio: String,
846 pub adl: String,
848 pub c_time: String,
850 pub realized_pnl: String,
852 pub upl_last_px: String,
854 pub upl_ratio_last_px: String,
856 pub avail_pos: String,
858 pub be_px: String,
860 pub funding_fee: String,
862 pub idx_px: String,
864 pub liq_penalty: String,
866 pub opt_val: String,
868 pub pending_close_ord_liab_val: String,
870 pub pnl: String,
872 pub pos_ccy: String,
874 pub quote_bal: String,
876 pub quote_borrowed: String,
878 pub quote_interest: String,
880 #[serde(alias = "spotInUse")]
882 pub spot_in_use_amt: String,
883 pub spot_in_use_ccy: String,
885 pub usd_px: String,
887 #[serde(default)]
889 pub delta_bs: String,
890 #[serde(default)]
892 pub gamma_bs: String,
893 #[serde(default)]
895 pub theta_bs: String,
896 #[serde(default)]
898 pub vega_bs: String,
899 #[serde(default)]
901 pub close_order_algo: Vec<OKXCloseOrderAlgo>,
902}
903
904#[derive(Clone, Debug, Serialize, Deserialize)]
906#[serde(rename_all = "camelCase")]
907pub struct OKXCloseOrderAlgo {
908 pub algo_id: String,
910}
911
912#[derive(Clone, Debug, Serialize, Deserialize)]
915#[serde(rename_all = "camelCase")]
916pub struct OKXPlaceOrderResponse {
917 #[serde(default)]
919 pub ord_id: Option<Ustr>,
920 #[serde(default)]
922 pub cl_ord_id: Option<Ustr>,
923 #[serde(default)]
925 pub tag: Option<String>,
926 #[serde(default)]
928 pub inst_id: Option<Ustr>,
929 #[serde(default)]
931 pub side: Option<OKXSide>,
932 #[serde(default)]
934 pub ord_type: Option<OKXOrderType>,
935 #[serde(default)]
937 pub sz: Option<String>,
938 pub state: Option<OKXOrderStatus>,
940 #[serde(default)]
942 pub px: Option<String>,
943 #[serde(default)]
945 pub avg_px: Option<String>,
946 #[serde(default)]
948 pub acc_fill_sz: Option<String>,
949 #[serde(default)]
951 pub fill_sz: Option<String>,
952 #[serde(default)]
954 pub fill_px: Option<String>,
955 #[serde(default)]
957 pub trade_id: Option<Ustr>,
958 #[serde(default)]
960 pub fill_time: Option<String>,
961 #[serde(default)]
963 pub fee: Option<String>,
964 #[serde(default)]
966 pub fee_ccy: Option<String>,
967 #[serde(default)]
969 pub req_id: Option<Ustr>,
970 #[serde(default)]
972 pub pos_side: Option<OKXPositionSide>,
973 #[serde(default)]
975 pub reduce_only: Option<String>,
976 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
978 pub tgt_ccy: Option<OKXTargetCurrency>,
979 #[serde(default)]
981 pub c_time: Option<String>,
982 #[serde(default)]
984 pub u_time: Option<String>,
985 #[serde(skip_serializing_if = "Option::is_none")]
987 pub s_code: Option<String>,
988 #[serde(skip_serializing_if = "Option::is_none")]
990 pub s_msg: Option<String>,
991 #[serde(skip_serializing_if = "Option::is_none")]
993 pub sub_code: Option<String>,
994}
995
996#[derive(Clone, Debug, Default, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct OKXAttachAlgoOrdRequest {
1000 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub attach_algo_cl_ord_id: Option<String>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub sl_trigger_px: Option<String>,
1006 #[serde(skip_serializing_if = "Option::is_none")]
1008 pub sl_ord_px: Option<String>,
1009 #[serde(skip_serializing_if = "Option::is_none")]
1011 pub sl_trigger_px_type: Option<OKXTriggerType>,
1012 #[serde(skip_serializing_if = "Option::is_none")]
1014 pub tp_trigger_px: Option<String>,
1015 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub tp_ord_px: Option<String>,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub tp_trigger_px_type: Option<OKXTriggerType>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub callback_ratio: Option<String>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub callback_spread: Option<String>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub active_px: Option<String>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub new_callback_ratio: Option<String>,
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub new_callback_spread: Option<String>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub new_active_px: Option<String>,
1039}
1040
1041#[derive(Clone, Debug, Serialize, Deserialize)]
1043#[serde(rename_all = "camelCase")]
1044pub struct OKXPlaceOrderRequest {
1045 pub inst_id: String,
1047 pub td_mode: OKXTradeMode,
1049 #[serde(skip_serializing_if = "Option::is_none")]
1051 pub ccy: Option<String>,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub cl_ord_id: Option<String>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1057 pub tag: Option<String>,
1058 pub side: OKXSide,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1062 pub pos_side: Option<OKXPositionSide>,
1063 pub ord_type: OKXOrderType,
1065 pub sz: String,
1067 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub px: Option<String>,
1070 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
1072 pub px_usd: Option<String>,
1073 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
1076 pub px_vol: Option<String>,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1079 pub reduce_only: Option<bool>,
1080 #[serde(skip_serializing_if = "Option::is_none")]
1082 pub tgt_ccy: Option<OKXTargetCurrency>,
1083 #[serde(skip_serializing_if = "Option::is_none")]
1085 pub trade_quote_ccy: Option<Ustr>,
1086 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1091 pub outcome: Option<String>,
1092 #[serde(skip_serializing_if = "Option::is_none")]
1097 pub slippage_pct: Option<String>,
1098 #[serde(skip_serializing_if = "Option::is_none")]
1100 pub rpi_taker_access: Option<bool>,
1101 #[serde(skip_serializing_if = "Option::is_none")]
1103 pub rpi_px_round: Option<bool>,
1104}
1105
1106#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1108#[serde(rename_all = "camelCase")]
1109pub struct OKXAmendOrderRequest {
1110 pub inst_id: String,
1112 #[serde(skip_serializing_if = "Option::is_none")]
1114 pub ord_id: Option<String>,
1115 #[serde(skip_serializing_if = "Option::is_none")]
1117 pub cl_ord_id: Option<String>,
1118 #[serde(skip_serializing_if = "Option::is_none")]
1120 pub req_id: Option<String>,
1121 #[serde(skip_serializing_if = "Option::is_none")]
1123 pub new_sz: Option<String>,
1124 #[serde(skip_serializing_if = "Option::is_none")]
1126 pub new_px: Option<String>,
1127 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub rpi_taker_access: Option<bool>,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1132 pub rpi_px_round: Option<bool>,
1133}
1134
1135#[derive(Clone, Debug, Serialize, Deserialize)]
1137#[serde(rename_all = "camelCase")]
1138pub struct OKXCancelOrderRequest {
1139 pub inst_id: String,
1141 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1143 pub inst_id_code: Option<u64>,
1144 #[serde(skip_serializing_if = "Option::is_none")]
1146 pub ord_id: Option<String>,
1147 #[serde(skip_serializing_if = "Option::is_none")]
1149 pub cl_ord_id: Option<String>,
1150}
1151
1152#[derive(Clone, Debug, Serialize, Deserialize)]
1154#[serde(rename_all = "camelCase")]
1155pub struct OKXCancelOrderResponse {
1156 pub ord_id: String,
1158 #[serde(default)]
1160 pub cl_ord_id: Option<String>,
1161 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub s_code: Option<String>,
1164 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub s_msg: Option<String>,
1167 #[serde(default)]
1169 pub ts: Option<String>,
1170}
1171
1172pub use crate::common::models::OKXAttachedAlgoOrd;
1173
1174#[derive(Clone, Debug, Serialize, Deserialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct OKXOrderHistory {
1178 pub ord_id: Ustr,
1180 pub cl_ord_id: Ustr,
1182 #[serde(default)]
1184 pub algo_id: Option<Ustr>,
1185 #[serde(default)]
1187 pub algo_cl_ord_id: Option<Ustr>,
1188 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1190 pub attach_algo_cl_ord_id: Option<String>,
1191 #[serde(default)]
1193 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1194 #[serde(default)]
1196 pub cl_act_id: Option<Ustr>,
1197 pub tag: String,
1199 pub inst_type: OKXInstrumentType,
1201 pub uly: Option<Ustr>,
1203 pub inst_id: Ustr,
1205 pub ord_type: OKXOrderType,
1207 pub sz: String,
1209 pub px: String,
1211 #[serde(default)]
1213 pub px_usd: String,
1214 #[serde(default)]
1216 pub px_vol: String,
1217 pub side: OKXSide,
1219 pub pos_side: OKXPositionSide,
1221 pub td_mode: OKXTradeMode,
1223 pub reduce_only: String,
1225 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1227 pub tgt_ccy: Option<OKXTargetCurrency>,
1228 pub state: OKXOrderStatus,
1230 pub avg_px: String,
1232 pub fee: String,
1234 pub fee_ccy: String,
1236 pub fill_sz: String,
1238 pub fill_px: String,
1240 pub trade_id: Ustr,
1242 #[serde(deserialize_with = "deserialize_string_to_u64")]
1244 pub fill_time: u64,
1245 pub acc_fill_sz: String,
1247 #[serde(default)]
1249 pub fill_fee: Option<String>,
1250 #[serde(default)]
1252 pub req_id: Option<Ustr>,
1253 #[serde(default)]
1255 pub cancel_fill_sz: Option<String>,
1256 #[serde(default)]
1258 pub cancel_total_sz: Option<String>,
1259 #[serde(default)]
1261 pub cancel_source: String,
1262 #[serde(default)]
1264 pub cancel_source_reason: String,
1265 #[serde(default)]
1267 pub fee_discount: Option<String>,
1268 pub category: OKXOrderCategory,
1270 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1272 pub outcome: Option<String>,
1273 #[serde(deserialize_with = "deserialize_string_to_u64")]
1275 pub u_time: u64,
1276 #[serde(deserialize_with = "deserialize_string_to_u64")]
1278 pub c_time: u64,
1279}
1280
1281#[derive(Clone, Debug, Serialize, Deserialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct OKXOrderAlgo {
1285 pub algo_id: String,
1287 #[serde(default)]
1289 pub algo_cl_ord_id: String,
1290 #[serde(default)]
1292 pub cl_ord_id: String,
1293 #[serde(default)]
1295 pub ord_id: String,
1296 #[serde(default)]
1298 pub ord_id_list: Vec<String>,
1299 #[serde(default)]
1301 pub sub_algo_id_list: Vec<String>,
1302 pub inst_id: Ustr,
1304 pub inst_type: OKXInstrumentType,
1306 pub ord_type: OKXAlgoOrderType,
1308 pub state: OKXAlgoOrderStatus,
1310 pub side: OKXSide,
1312 pub pos_side: OKXPositionSide,
1314 #[serde(default)]
1316 pub sz: String,
1317 #[serde(default)]
1319 pub trigger_px: String,
1320 #[serde(default)]
1322 pub trigger_px_type: Option<OKXTriggerType>,
1323 #[serde(default)]
1325 pub sl_trigger_px: String,
1326 #[serde(default)]
1328 pub sl_ord_px: String,
1329 #[serde(default)]
1331 pub sl_trigger_px_type: Option<OKXTriggerType>,
1332 #[serde(default)]
1334 pub tp_trigger_px: String,
1335 #[serde(default)]
1337 pub tp_ord_px: String,
1338 #[serde(default)]
1340 pub tp_trigger_px_type: Option<OKXTriggerType>,
1341 #[serde(default)]
1343 pub ord_px: String,
1344 pub td_mode: OKXTradeMode,
1346 #[serde(default)]
1348 pub lever: String,
1349 #[serde(default)]
1351 pub reduce_only: String,
1352 #[serde(default)]
1354 pub close_fraction: String,
1355 #[serde(default)]
1357 pub actual_px: String,
1358 #[serde(default)]
1360 pub actual_sz: String,
1361 #[serde(default)]
1363 pub notional_usd: String,
1364 #[serde(deserialize_with = "deserialize_string_to_u64")]
1366 pub c_time: u64,
1367 #[serde(deserialize_with = "deserialize_string_to_u64")]
1369 pub u_time: u64,
1370 #[serde(default)]
1372 pub trigger_time: String,
1373 #[serde(default)]
1375 pub tag: String,
1376 #[serde(default)]
1378 pub callback_ratio: String,
1379 #[serde(default)]
1381 pub callback_spread: String,
1382 #[serde(default)]
1384 pub active_px: String,
1385}
1386
1387#[derive(Clone, Debug, Serialize, Deserialize)]
1389#[serde(rename_all = "camelCase")]
1390pub struct OKXTransactionDetail {
1391 pub inst_type: OKXInstrumentType,
1393 pub inst_id: Ustr,
1395 pub trade_id: Ustr,
1397 pub ord_id: Ustr,
1399 pub cl_ord_id: Ustr,
1401 pub bill_id: Ustr,
1403 pub fill_px: String,
1405 pub fill_sz: String,
1407 pub side: OKXSide,
1409 #[serde(default)]
1411 pub pos_side: Option<OKXPositionSide>,
1412 pub exec_type: OKXExecType,
1414 pub fee_ccy: String,
1416 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1418 pub fee: Option<String>,
1419 #[serde(deserialize_with = "deserialize_string_to_u64")]
1421 pub ts: u64,
1422}
1423
1424#[derive(Clone, Debug, Serialize, Deserialize)]
1426#[serde(rename_all = "camelCase")]
1427pub struct OKXPositionHistory {
1428 pub inst_type: OKXInstrumentType,
1430 pub inst_id: Ustr,
1432 pub mgn_mode: OKXMarginMode,
1434 #[serde(rename = "type")]
1437 pub r#type: Ustr,
1438 pub c_time: String,
1440 #[serde(deserialize_with = "deserialize_string_to_u64")]
1442 pub u_time: u64,
1443 pub open_avg_px: String,
1445 #[serde(skip_serializing_if = "Option::is_none")]
1447 pub close_avg_px: Option<String>,
1448 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1450 pub pos_id: Option<Ustr>,
1451 #[serde(skip_serializing_if = "Option::is_none")]
1453 pub open_max_pos: Option<String>,
1454 #[serde(skip_serializing_if = "Option::is_none")]
1456 pub close_total_pos: Option<String>,
1457 #[serde(skip_serializing_if = "Option::is_none")]
1459 pub realized_pnl: Option<String>,
1460 #[serde(skip_serializing_if = "Option::is_none")]
1462 pub fee: Option<String>,
1463 #[serde(skip_serializing_if = "Option::is_none")]
1465 pub funding_fee: Option<String>,
1466 #[serde(skip_serializing_if = "Option::is_none")]
1468 pub liq_penalty: Option<String>,
1469 #[serde(skip_serializing_if = "Option::is_none")]
1471 pub pnl: Option<String>,
1472 #[serde(skip_serializing_if = "Option::is_none")]
1474 pub pnl_ratio: Option<String>,
1475 pub pos_side: OKXPositionSide,
1477 pub lever: String,
1479 #[serde(skip_serializing_if = "Option::is_none")]
1481 pub direction: Option<String>,
1482 #[serde(skip_serializing_if = "Option::is_none")]
1484 pub trigger_px: Option<String>,
1485 #[serde(skip_serializing_if = "Option::is_none")]
1487 pub uly: Option<String>,
1488 #[serde(skip_serializing_if = "Option::is_none")]
1490 pub ccy: Option<String>,
1491}
1492
1493#[derive(Clone, Debug, Serialize, Deserialize)]
1495#[serde(rename_all = "camelCase")]
1496pub struct OKXPlaceAlgoOrderRequest {
1497 #[serde(rename = "instId")]
1499 pub inst_id: String,
1500 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1502 pub inst_id_code: Option<u64>,
1503 #[serde(rename = "tdMode")]
1505 pub td_mode: OKXTradeMode,
1506 pub side: OKXSide,
1508 #[serde(rename = "ordType")]
1510 pub ord_type: OKXAlgoOrderType,
1511 #[serde(skip_serializing_if = "Option::is_none")]
1513 pub sz: Option<String>,
1514 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1516 pub algo_cl_ord_id: Option<String>,
1517 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1519 pub trigger_px: Option<String>,
1520 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1522 pub order_px: Option<String>,
1523 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1525 pub trigger_px_type: Option<OKXTriggerType>,
1526 #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1528 pub sl_trigger_px: Option<String>,
1529 #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1531 pub sl_ord_px: Option<String>,
1532 #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1534 pub sl_trigger_px_type: Option<OKXTriggerType>,
1535 #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1537 pub tp_trigger_px: Option<String>,
1538 #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1540 pub tp_ord_px: Option<String>,
1541 #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1543 pub tp_trigger_px_type: Option<OKXTriggerType>,
1544 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1546 pub tgt_ccy: Option<OKXTargetCurrency>,
1547 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1549 pub pos_side: Option<OKXPositionSide>,
1550 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1552 pub close_position: Option<bool>,
1553 #[serde(skip_serializing_if = "Option::is_none")]
1555 pub tag: Option<String>,
1556 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1558 pub reduce_only: Option<bool>,
1559 #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1561 pub close_fraction: Option<String>,
1562 #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1565 pub callback_ratio: Option<String>,
1566 #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1569 pub callback_spread: Option<String>,
1570 #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1573 pub active_px: Option<String>,
1574 #[serde(skip_serializing_if = "Option::is_none")]
1576 pub ccy: Option<String>,
1577}
1578
1579#[derive(Clone, Debug, Serialize, Deserialize)]
1581#[serde(rename_all = "camelCase")]
1582pub struct OKXPlaceAlgoOrderResponse {
1583 pub algo_id: String,
1585 #[serde(skip_serializing_if = "Option::is_none")]
1587 pub algo_cl_ord_id: Option<String>,
1588 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub s_code: Option<String>,
1591 #[serde(skip_serializing_if = "Option::is_none")]
1593 pub s_msg: Option<String>,
1594 #[serde(skip_serializing_if = "Option::is_none")]
1596 pub req_id: Option<String>,
1597}
1598
1599#[derive(Clone, Debug, Serialize, Deserialize)]
1601#[serde(rename_all = "camelCase")]
1602pub struct OKXCancelAlgoOrderRequest {
1603 pub inst_id: String,
1605 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1607 pub inst_id_code: Option<u64>,
1608 #[serde(skip_serializing_if = "Option::is_none")]
1610 pub algo_id: Option<String>,
1611 #[serde(skip_serializing_if = "Option::is_none")]
1613 pub algo_cl_ord_id: Option<String>,
1614}
1615
1616#[derive(Clone, Debug, Serialize, Deserialize)]
1618#[serde(rename_all = "camelCase")]
1619pub struct OKXCancelAlgoOrderResponse {
1620 pub algo_id: String,
1622 #[serde(skip_serializing_if = "Option::is_none")]
1624 pub s_code: Option<String>,
1625 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub s_msg: Option<String>,
1628}
1629
1630#[derive(Clone, Debug, Serialize, Deserialize)]
1632#[serde(rename_all = "camelCase")]
1633pub struct OKXAmendAlgoOrderRequest {
1634 pub inst_id: String,
1636 pub algo_id: String,
1638 #[serde(skip_serializing_if = "Option::is_none")]
1640 pub algo_cl_ord_id: Option<String>,
1641 #[serde(skip_serializing_if = "Option::is_none")]
1643 pub new_sz: Option<String>,
1644 #[serde(skip_serializing_if = "Option::is_none")]
1646 pub new_trigger_px: Option<String>,
1647 #[serde(skip_serializing_if = "Option::is_none")]
1649 pub new_trigger_px_type: Option<String>,
1650 #[serde(skip_serializing_if = "Option::is_none")]
1652 pub new_tp_trigger_px: Option<String>,
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub new_tp_ord_px: Option<String>,
1656 #[serde(skip_serializing_if = "Option::is_none")]
1658 pub new_tp_trigger_px_type: Option<String>,
1659 #[serde(skip_serializing_if = "Option::is_none")]
1663 pub new_sl_trigger_px: Option<String>,
1664 #[serde(skip_serializing_if = "Option::is_none")]
1666 pub new_sl_ord_px: Option<String>,
1667 #[serde(skip_serializing_if = "Option::is_none")]
1669 pub new_sl_trigger_px_type: Option<String>,
1670 #[serde(skip_serializing_if = "Option::is_none")]
1672 #[serde(rename = "newOrdPx")]
1673 pub new_order_px: Option<String>,
1674 #[serde(skip_serializing_if = "Option::is_none")]
1676 pub new_callback_ratio: Option<String>,
1677 #[serde(skip_serializing_if = "Option::is_none")]
1679 pub new_callback_spread: Option<String>,
1680 #[serde(skip_serializing_if = "Option::is_none")]
1682 pub new_active_px: Option<String>,
1683}
1684
1685#[derive(Clone, Debug, Serialize, Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688pub struct OKXAmendAlgoOrderResponse {
1689 pub algo_id: String,
1691 #[serde(skip_serializing_if = "Option::is_none")]
1693 pub algo_cl_ord_id: Option<String>,
1694 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub s_code: Option<String>,
1697 #[serde(skip_serializing_if = "Option::is_none")]
1699 pub s_msg: Option<String>,
1700 #[serde(skip_serializing_if = "Option::is_none")]
1702 pub req_id: Option<String>,
1703}
1704
1705#[derive(Clone, Debug, Serialize, Deserialize)]
1707#[serde(rename_all = "camelCase")]
1708pub struct OKXServerTime {
1709 #[serde(deserialize_with = "deserialize_string_to_u64")]
1711 pub ts: u64,
1712}
1713
1714#[derive(Clone, Debug, Serialize, Deserialize)]
1719#[serde(rename_all = "camelCase")]
1720pub struct OKXFeeGroup {
1721 pub group_id: String,
1723 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1725 pub maker: Option<Decimal>,
1726 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1728 pub taker: Option<Decimal>,
1729 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1731 pub rpi_maker: Option<Decimal>,
1732 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1734 pub elp_maker: Option<Decimal>,
1735}
1736
1737#[derive(Clone, Debug, Serialize, Deserialize)]
1739#[serde(rename_all = "camelCase")]
1740pub struct OKXFeeRate {
1741 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1743 pub level: OKXVipLevel,
1744 #[serde(default)]
1746 pub taker: String,
1747 #[serde(default)]
1749 pub maker: String,
1750 #[serde(default)]
1752 pub taker_u: String,
1753 #[serde(default)]
1755 pub maker_u: String,
1756 #[serde(default)]
1758 pub fee_group: Vec<OKXFeeGroup>,
1759 #[serde(
1761 default,
1762 alias = "elpMaker",
1763 deserialize_with = "deserialize_optional_decimal"
1764 )]
1765 pub rpi_maker: Option<Decimal>,
1766 #[serde(default)]
1768 pub delivery: String,
1769 #[serde(default)]
1771 pub exercise: String,
1772 #[serde(default)]
1774 pub settle: String,
1775 pub inst_type: OKXInstrumentType,
1777 #[serde(default)]
1779 pub category: String,
1780 #[serde(deserialize_with = "deserialize_string_to_u64")]
1782 pub ts: u64,
1783}
1784
1785fn deserialize_configuration_enum<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1786where
1787 D: Deserializer<'de>,
1788 T: Deserialize<'de>,
1789{
1790 let value = String::deserialize(deserializer)?;
1791 T::deserialize(value.into_deserializer())
1792}
1793
1794mod account_permissions {
1795 use serde::{Deserialize, Deserializer, Serializer, de::IntoDeserializer};
1796
1797 use crate::common::enums::OKXApiKeyPermission;
1798
1799 pub(super) fn serialize<S>(
1800 permissions: &[OKXApiKeyPermission],
1801 serializer: S,
1802 ) -> Result<S::Ok, S::Error>
1803 where
1804 S: Serializer,
1805 {
1806 let value = permissions
1807 .iter()
1808 .map(AsRef::as_ref)
1809 .collect::<Vec<_>>()
1810 .join(",");
1811 serializer.serialize_str(&value)
1812 }
1813
1814 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<OKXApiKeyPermission>, D::Error>
1815 where
1816 D: Deserializer<'de>,
1817 {
1818 let value = String::deserialize(deserializer)?;
1819 value
1820 .split(',')
1821 .map(|permission| OKXApiKeyPermission::deserialize(permission.into_deserializer()))
1822 .collect()
1823 }
1824}
1825
1826#[cfg(test)]
1827mod tests {
1828 use rstest::rstest;
1829 use serde_json;
1830
1831 use super::*;
1832
1833 #[rstest]
1834 fn test_algo_order_deserializes_current_child_identifier_lists() {
1835 let order: OKXOrderAlgo = serde_json::from_value(serde_json::json!({
1836 "algoId": "123",
1837 "algoClOrdId": "algo-client-1",
1838 "ordId": "456",
1839 "ordIdList": ["456", "457"],
1840 "subAlgoIdList": ["789"],
1841 "instId": "ETH-USDT-SWAP",
1842 "instType": "SWAP",
1843 "ordType": "conditional",
1844 "state": "effective",
1845 "side": "sell",
1846 "posSide": "net",
1847 "tdMode": "cross",
1848 "cTime": "1700000000000",
1849 "uTime": "1700000001000"
1850 }))
1851 .unwrap();
1852
1853 assert_eq!(order.ord_id, "456");
1854 assert_eq!(order.ord_id_list, ["456", "457"]);
1855 assert_eq!(order.sub_algo_id_list, ["789"]);
1856 }
1857
1858 #[rstest]
1859 fn test_algo_order_request_serialization() {
1860 let request = OKXPlaceAlgoOrderRequest {
1861 inst_id: "ETH-USDT-SWAP".to_string(),
1862 inst_id_code: None,
1863 td_mode: OKXTradeMode::Isolated,
1864 side: OKXSide::Buy,
1865 ord_type: OKXAlgoOrderType::Trigger,
1866 sz: Some("0.01".to_string()),
1867 algo_cl_ord_id: Some("test123".to_string()),
1868 trigger_px: Some("3000".to_string()),
1869 order_px: Some("-1".to_string()),
1870 trigger_px_type: Some(OKXTriggerType::Last),
1871 sl_trigger_px: None,
1872 sl_ord_px: None,
1873 sl_trigger_px_type: None,
1874 tp_trigger_px: None,
1875 tp_ord_px: None,
1876 tp_trigger_px_type: None,
1877 tgt_ccy: None,
1878 pos_side: None,
1879 close_position: None,
1880 tag: None,
1881 reduce_only: None,
1882 close_fraction: None,
1883 callback_ratio: None,
1884 callback_spread: None,
1885 active_px: None,
1886 ccy: None,
1887 };
1888
1889 let json = serde_json::to_string(&request).unwrap();
1890
1891 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1893 assert!(json.contains("\"tdMode\":\"isolated\""));
1894 assert!(json.contains("\"ordType\":\"trigger\""));
1895 assert!(json.contains("\"algoClOrdId\":\"test123\""));
1896 assert!(json.contains("\"triggerPx\":\"3000\""));
1897 assert!(json.contains("\"orderPx\":\"-1\""));
1898 assert!(json.contains("\"triggerPxType\":\"last\""));
1899
1900 assert!(!json.contains("tgtCcy"));
1902 assert!(!json.contains("posSide"));
1903 assert!(!json.contains("closePosition"));
1904 assert!(!json.contains("closeFraction"));
1905 }
1906
1907 #[rstest]
1908 fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1909 let request = OKXAmendAlgoOrderRequest {
1913 inst_id: "ETH-USDT-SWAP".to_string(),
1914 algo_id: "123".to_string(),
1915 algo_cl_ord_id: None,
1916 new_sz: None,
1917 new_trigger_px: None,
1918 new_trigger_px_type: None,
1919 new_tp_trigger_px: None,
1920 new_tp_ord_px: None,
1921 new_tp_trigger_px_type: None,
1922 new_sl_trigger_px: Some("850".to_string()),
1923 new_sl_ord_px: None,
1924 new_sl_trigger_px_type: None,
1925 new_order_px: None,
1926 new_callback_ratio: None,
1927 new_callback_spread: None,
1928 new_active_px: None,
1929 };
1930
1931 let json = serde_json::to_string(&request).unwrap();
1932
1933 assert!(json.contains("\"algoId\":\"123\""));
1934 assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1935 assert!(!json.contains("newTriggerPx"));
1937 assert!(!json.contains("newSz"));
1938 assert!(!json.contains("algoClOrdId"));
1939 }
1940
1941 #[rstest]
1942 fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1943 let request = OKXAmendAlgoOrderRequest {
1944 inst_id: "DOGE-USDT-SWAP".to_string(),
1945 algo_id: "algo-oco-1".to_string(),
1946 algo_cl_ord_id: None,
1947 new_sz: None,
1948 new_trigger_px: None,
1949 new_trigger_px_type: None,
1950 new_tp_trigger_px: Some("0.10495".to_string()),
1951 new_tp_ord_px: Some("-1".to_string()),
1952 new_tp_trigger_px_type: Some("last".to_string()),
1953 new_sl_trigger_px: Some("0.06297".to_string()),
1954 new_sl_ord_px: Some("-1".to_string()),
1955 new_sl_trigger_px_type: Some("last".to_string()),
1956 new_order_px: None,
1957 new_callback_ratio: None,
1958 new_callback_spread: None,
1959 new_active_px: None,
1960 };
1961
1962 let json = serde_json::to_string(&request).unwrap();
1963
1964 assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1965 assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1966 assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1967 assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1968 assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1969 assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1970 assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1971 assert!(!json.contains("newTriggerPx"));
1972 }
1973
1974 #[rstest]
1975 fn test_algo_order_request_serializes_close_fraction() {
1976 let request = OKXPlaceAlgoOrderRequest {
1977 inst_id: "ETH-USDT-SWAP".to_string(),
1978 inst_id_code: None,
1979 td_mode: OKXTradeMode::Cross,
1980 side: OKXSide::Sell,
1981 ord_type: OKXAlgoOrderType::Conditional,
1982 sz: None,
1983 algo_cl_ord_id: Some("close-frac-123".to_string()),
1984 trigger_px: None,
1985 order_px: None,
1986 trigger_px_type: None,
1987 sl_trigger_px: Some("3000".to_string()),
1988 sl_ord_px: Some("-1".to_string()),
1989 sl_trigger_px_type: Some(OKXTriggerType::Last),
1990 tp_trigger_px: None,
1991 tp_ord_px: None,
1992 tp_trigger_px_type: None,
1993 tgt_ccy: None,
1994 pos_side: Some(OKXPositionSide::Net),
1995 close_position: None,
1996 tag: None,
1997 reduce_only: Some(true),
1998 close_fraction: Some("1".to_string()),
1999 callback_ratio: None,
2000 callback_spread: None,
2001 active_px: None,
2002 ccy: None,
2003 };
2004
2005 let json = serde_json::to_string(&request).unwrap();
2006
2007 assert!(json.contains("\"ordType\":\"conditional\""));
2008 assert!(json.contains("\"closeFraction\":\"1\""));
2009 assert!(json.contains("\"slTriggerPx\":\"3000\""));
2010 assert!(json.contains("\"slOrdPx\":\"-1\""));
2011 assert!(json.contains("\"slTriggerPxType\":\"last\""));
2012 assert!(json.contains("\"reduceOnly\":true"));
2013 assert!(!json.contains("\"sz\""));
2014 assert!(!json.contains("triggerPx"));
2015 }
2016
2017 #[rstest]
2018 fn test_algo_order_request_array_serialization() {
2019 let request = OKXPlaceAlgoOrderRequest {
2020 inst_id: "BTC-USDT".to_string(),
2021 inst_id_code: Some(10459),
2022 td_mode: OKXTradeMode::Cross,
2023 side: OKXSide::Sell,
2024 ord_type: OKXAlgoOrderType::Trigger,
2025 sz: Some("0.1".to_string()),
2026 algo_cl_ord_id: None,
2027 trigger_px: Some("50000".to_string()),
2028 order_px: Some("49900".to_string()),
2029 trigger_px_type: Some(OKXTriggerType::Mark),
2030 sl_trigger_px: None,
2031 sl_ord_px: None,
2032 sl_trigger_px_type: None,
2033 tp_trigger_px: None,
2034 tp_ord_px: None,
2035 tp_trigger_px_type: None,
2036 tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
2037 pos_side: Some(OKXPositionSide::Net),
2038 close_position: None,
2039 tag: None,
2040 reduce_only: Some(true),
2041 close_fraction: None,
2042 callback_ratio: None,
2043 callback_spread: None,
2044 active_px: None,
2045 ccy: None,
2046 };
2047
2048 let json = serde_json::to_string(&[request]).unwrap();
2050
2051 assert!(json.starts_with('['));
2053 assert!(json.ends_with(']'));
2054
2055 assert!(json.contains("\"instId\":\"BTC-USDT\""));
2057 assert!(json.contains("\"tdMode\":\"cross\""));
2058 assert!(json.contains("\"triggerPx\":\"50000\""));
2059 assert!(json.contains("\"orderPx\":\"49900\""));
2060 assert!(json.contains("\"triggerPxType\":\"mark\""));
2061 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
2062 assert!(json.contains("\"posSide\":\"net\""));
2063 assert!(json.contains("\"reduceOnly\":true"));
2064 }
2065
2066 #[rstest]
2067 fn test_cancel_algo_order_request_serialization() {
2068 let request = OKXCancelAlgoOrderRequest {
2069 inst_id: "ETH-USDT-SWAP".to_string(),
2070 inst_id_code: None,
2071 algo_id: Some("123456".to_string()),
2072 algo_cl_ord_id: None,
2073 };
2074
2075 let json = serde_json::to_string(&request).unwrap();
2076
2077 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
2079 assert!(json.contains("\"algoId\":\"123456\""));
2080 assert!(!json.contains("algoClOrdId"));
2081 }
2082
2083 #[rstest]
2084 fn test_cancel_algo_order_with_client_id_serialization() {
2085 let request = OKXCancelAlgoOrderRequest {
2086 inst_id: "BTC-USDT".to_string(),
2087 inst_id_code: Some(10459),
2088 algo_id: None,
2089 algo_cl_ord_id: Some("client123".to_string()),
2090 };
2091
2092 let json = serde_json::to_string(&[request]).unwrap();
2094
2095 assert!(json.starts_with('['));
2097 assert!(json.contains("\"instId\":\"BTC-USDT\""));
2098 assert!(json.contains("\"algoClOrdId\":\"client123\""));
2099 assert!(!json.contains("\"algoId\""));
2100 }
2101
2102 #[rstest]
2103 fn test_amend_algo_order_trigger_serialization() {
2104 let request = OKXAmendAlgoOrderRequest {
2105 inst_id: "ETH-USDT-SWAP".to_string(),
2106 algo_id: "123456".to_string(),
2107 algo_cl_ord_id: None,
2108 new_sz: None,
2109 new_trigger_px: Some("3500".to_string()),
2110 new_trigger_px_type: None,
2111 new_tp_trigger_px: None,
2112 new_tp_ord_px: None,
2113 new_tp_trigger_px_type: None,
2114 new_sl_trigger_px: None,
2115 new_sl_ord_px: None,
2116 new_sl_trigger_px_type: None,
2117 new_order_px: Some("3490".to_string()),
2118 new_callback_ratio: None,
2119 new_callback_spread: None,
2120 new_active_px: None,
2121 };
2122
2123 let json = serde_json::to_string(&request).unwrap();
2124
2125 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
2126 assert!(json.contains("\"algoId\":\"123456\""));
2127 assert!(json.contains("\"newTriggerPx\":\"3500\""));
2128 assert!(json.contains("\"newOrdPx\":\"3490\""));
2129 assert!(!json.contains("newSz"));
2130 assert!(!json.contains("algoClOrdId"));
2131 assert!(!json.contains("newCallbackRatio"));
2132 }
2133
2134 #[rstest]
2135 fn test_amend_algo_order_trailing_stop_serialization() {
2136 let request = OKXAmendAlgoOrderRequest {
2137 inst_id: "BTC-USDT-SWAP".to_string(),
2138 algo_id: "789012".to_string(),
2139 algo_cl_ord_id: Some("client456".to_string()),
2140 new_sz: Some("0.1".to_string()),
2141 new_trigger_px: None,
2142 new_trigger_px_type: None,
2143 new_tp_trigger_px: None,
2144 new_tp_ord_px: None,
2145 new_tp_trigger_px_type: None,
2146 new_sl_trigger_px: None,
2147 new_sl_ord_px: None,
2148 new_sl_trigger_px_type: None,
2149 new_order_px: None,
2150 new_callback_ratio: Some("0.02".to_string()),
2151 new_callback_spread: None,
2152 new_active_px: Some("50000".to_string()),
2153 };
2154
2155 let json = serde_json::to_string(&request).unwrap();
2156
2157 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2158 assert!(json.contains("\"algoId\":\"789012\""));
2159 assert!(json.contains("\"algoClOrdId\":\"client456\""));
2160 assert!(json.contains("\"newSz\":\"0.1\""));
2161 assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
2162 assert!(json.contains("\"newActivePx\":\"50000\""));
2163 assert!(!json.contains("newTriggerPx"));
2164 assert!(!json.contains("newOrdPx"));
2165 }
2166
2167 #[rstest]
2168 fn test_trailing_stop_request_callback_ratio_serialization() {
2169 let request = OKXPlaceAlgoOrderRequest {
2170 inst_id: "BTC-USDT-SWAP".to_string(),
2171 inst_id_code: None,
2172 td_mode: OKXTradeMode::Cross,
2173 side: OKXSide::Buy,
2174 ord_type: OKXAlgoOrderType::MoveOrderStop,
2175 sz: Some("0.1".to_string()),
2176 algo_cl_ord_id: Some("trail-001".to_string()),
2177 trigger_px: None,
2178 order_px: None,
2179 trigger_px_type: None,
2180 sl_trigger_px: None,
2181 sl_ord_px: None,
2182 sl_trigger_px_type: None,
2183 tp_trigger_px: None,
2184 tp_ord_px: None,
2185 tp_trigger_px_type: None,
2186 tgt_ccy: None,
2187 pos_side: None,
2188 close_position: None,
2189 tag: None,
2190 reduce_only: None,
2191 close_fraction: None,
2192 callback_ratio: Some("0.01".to_string()),
2193 callback_spread: None,
2194 active_px: None,
2195 ccy: None,
2196 };
2197
2198 let json = serde_json::to_string(&request).unwrap();
2199
2200 assert!(json.contains("\"ordType\":\"move_order_stop\""));
2201 assert!(json.contains("\"callbackRatio\":\"0.01\""));
2202 assert!(!json.contains("callbackSpread"));
2203 assert!(!json.contains("activePx"));
2204 }
2205
2206 #[rstest]
2207 fn test_trailing_stop_request_callback_spread_serialization() {
2208 let request = OKXPlaceAlgoOrderRequest {
2209 inst_id: "ETH-USDT-SWAP".to_string(),
2210 inst_id_code: None,
2211 td_mode: OKXTradeMode::Isolated,
2212 side: OKXSide::Sell,
2213 ord_type: OKXAlgoOrderType::MoveOrderStop,
2214 sz: Some("1.0".to_string()),
2215 algo_cl_ord_id: None,
2216 trigger_px: None,
2217 order_px: None,
2218 trigger_px_type: None,
2219 sl_trigger_px: None,
2220 sl_ord_px: None,
2221 sl_trigger_px_type: None,
2222 tp_trigger_px: None,
2223 tp_ord_px: None,
2224 tp_trigger_px_type: None,
2225 tgt_ccy: None,
2226 pos_side: None,
2227 close_position: None,
2228 tag: None,
2229 reduce_only: Some(true),
2230 close_fraction: None,
2231 callback_ratio: None,
2232 callback_spread: Some("50.5".to_string()),
2233 active_px: None,
2234 ccy: None,
2235 };
2236
2237 let json = serde_json::to_string(&request).unwrap();
2238
2239 assert!(json.contains("\"callbackSpread\":\"50.5\""));
2240 assert!(!json.contains("callbackRatio"));
2241 assert!(!json.contains("activePx"));
2242 }
2243
2244 #[rstest]
2245 fn test_trailing_stop_request_with_activation_price_serialization() {
2246 let request = OKXPlaceAlgoOrderRequest {
2247 inst_id: "BTC-USDT-SWAP".to_string(),
2248 inst_id_code: None,
2249 td_mode: OKXTradeMode::Cross,
2250 side: OKXSide::Buy,
2251 ord_type: OKXAlgoOrderType::MoveOrderStop,
2252 sz: Some("0.5".to_string()),
2253 algo_cl_ord_id: None,
2254 trigger_px: None,
2255 order_px: None,
2256 trigger_px_type: None,
2257 sl_trigger_px: None,
2258 sl_ord_px: None,
2259 sl_trigger_px_type: None,
2260 tp_trigger_px: None,
2261 tp_ord_px: None,
2262 tp_trigger_px_type: None,
2263 tgt_ccy: None,
2264 pos_side: None,
2265 close_position: None,
2266 tag: None,
2267 reduce_only: None,
2268 close_fraction: None,
2269 callback_ratio: Some("0.005".to_string()),
2270 callback_spread: None,
2271 active_px: Some("65000".to_string()),
2272 ccy: None,
2273 };
2274
2275 let json = serde_json::to_string(&request).unwrap();
2276
2277 assert!(json.contains("\"callbackRatio\":\"0.005\""));
2278 assert!(json.contains("\"activePx\":\"65000\""));
2279 assert!(!json.contains("callbackSpread"));
2280 }
2281
2282 #[rstest]
2283 fn test_amend_algo_order_callback_spread_serialization() {
2284 let request = OKXAmendAlgoOrderRequest {
2285 inst_id: "ETH-USDT-SWAP".to_string(),
2286 algo_id: "456789".to_string(),
2287 algo_cl_ord_id: None,
2288 new_sz: None,
2289 new_trigger_px: None,
2290 new_trigger_px_type: None,
2291 new_tp_trigger_px: None,
2292 new_tp_ord_px: None,
2293 new_tp_trigger_px_type: None,
2294 new_sl_trigger_px: None,
2295 new_sl_ord_px: None,
2296 new_sl_trigger_px_type: None,
2297 new_order_px: None,
2298 new_callback_ratio: None,
2299 new_callback_spread: Some("25.0".to_string()),
2300 new_active_px: Some("4000".to_string()),
2301 };
2302
2303 let json = serde_json::to_string(&request).unwrap();
2304
2305 assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
2306 assert!(json.contains("\"newActivePx\":\"4000\""));
2307 assert!(!json.contains("newCallbackRatio"));
2308 assert!(!json.contains("newTriggerPx"));
2309 assert!(!json.contains("newSz"));
2310 }
2311
2312 #[rstest]
2313 fn test_amend_algo_order_size_only_serialization() {
2314 let request = OKXAmendAlgoOrderRequest {
2315 inst_id: "BTC-USDT-SWAP".to_string(),
2316 algo_id: "111222".to_string(),
2317 algo_cl_ord_id: None,
2318 new_sz: Some("0.5".to_string()),
2319 new_trigger_px: None,
2320 new_trigger_px_type: None,
2321 new_tp_trigger_px: None,
2322 new_tp_ord_px: None,
2323 new_tp_trigger_px_type: None,
2324 new_sl_trigger_px: None,
2325 new_sl_ord_px: None,
2326 new_sl_trigger_px_type: None,
2327 new_order_px: None,
2328 new_callback_ratio: None,
2329 new_callback_spread: None,
2330 new_active_px: None,
2331 };
2332
2333 let json = serde_json::to_string(&request).unwrap();
2334
2335 assert!(json.contains("\"newSz\":\"0.5\""));
2336 assert!(!json.contains("newTriggerPx"));
2337 assert!(!json.contains("newOrdPx"));
2338 assert!(!json.contains("newCallbackRatio"));
2339 assert!(!json.contains("newCallbackSpread"));
2340 assert!(!json.contains("newActivePx"));
2341 }
2342
2343 #[rstest]
2344 fn test_amend_algo_order_all_fields_serialization() {
2345 let request = OKXAmendAlgoOrderRequest {
2346 inst_id: "BTC-USDT-SWAP".to_string(),
2347 algo_id: "333444".to_string(),
2348 algo_cl_ord_id: Some("client789".to_string()),
2349 new_sz: Some("1.0".to_string()),
2350 new_trigger_px: Some("60000".to_string()),
2351 new_trigger_px_type: None,
2352 new_tp_trigger_px: None,
2353 new_tp_ord_px: None,
2354 new_tp_trigger_px_type: None,
2355 new_sl_trigger_px: None,
2356 new_sl_ord_px: None,
2357 new_sl_trigger_px_type: None,
2358 new_order_px: Some("59900".to_string()),
2359 new_callback_ratio: Some("0.015".to_string()),
2360 new_callback_spread: Some("100".to_string()),
2361 new_active_px: Some("62000".to_string()),
2362 };
2363
2364 let json = serde_json::to_string(&request).unwrap();
2365
2366 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2367 assert!(json.contains("\"algoId\":\"333444\""));
2368 assert!(json.contains("\"algoClOrdId\":\"client789\""));
2369 assert!(json.contains("\"newSz\":\"1.0\""));
2370 assert!(json.contains("\"newTriggerPx\":\"60000\""));
2371 assert!(json.contains("\"newOrdPx\":\"59900\""));
2372 assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2373 assert!(json.contains("\"newCallbackSpread\":\"100\""));
2374 assert!(json.contains("\"newActivePx\":\"62000\""));
2375 }
2376
2377 #[rstest]
2378 fn test_place_order_request_serializes_px_usd() {
2379 let request = OKXPlaceOrderRequest {
2380 inst_id: "BTC-USD-250328-50000-C".to_string(),
2381 td_mode: OKXTradeMode::Cross,
2382 ccy: None,
2383 cl_ord_id: Some("test-opt-1".to_string()),
2384 tag: None,
2385 side: OKXSide::Buy,
2386 pos_side: Some(OKXPositionSide::Net),
2387 ord_type: OKXOrderType::Limit,
2388 sz: "1".to_string(),
2389 px: None,
2390 px_usd: Some("100.5".to_string()),
2391 px_vol: None,
2392 reduce_only: None,
2393 tgt_ccy: None,
2394 trade_quote_ccy: None,
2395 attach_algo_ords: None,
2396 outcome: None,
2397 slippage_pct: None,
2398 rpi_taker_access: None,
2399 rpi_px_round: None,
2400 };
2401
2402 let json = serde_json::to_string(&request).unwrap();
2403 assert!(json.contains("\"pxUsd\":\"100.5\""));
2404 assert!(!json.contains("\"pxVol\""));
2405 assert!(!json.contains("\"px\":"));
2406 assert!(!json.contains("slippagePct"));
2407 }
2408
2409 #[rstest]
2410 fn test_place_order_request_serializes_px_vol() {
2411 let request = OKXPlaceOrderRequest {
2412 inst_id: "BTC-USD-250328-50000-C".to_string(),
2413 td_mode: OKXTradeMode::Cross,
2414 ccy: None,
2415 cl_ord_id: Some("test-opt-2".to_string()),
2416 tag: None,
2417 side: OKXSide::Buy,
2418 pos_side: Some(OKXPositionSide::Net),
2419 ord_type: OKXOrderType::Limit,
2420 sz: "1".to_string(),
2421 px: None,
2422 px_usd: None,
2423 px_vol: Some("0.55".to_string()),
2424 reduce_only: None,
2425 tgt_ccy: None,
2426 trade_quote_ccy: None,
2427 attach_algo_ords: None,
2428 outcome: None,
2429 slippage_pct: None,
2430 rpi_taker_access: None,
2431 rpi_px_round: None,
2432 };
2433
2434 let json = serde_json::to_string(&request).unwrap();
2435 assert!(json.contains("\"pxVol\":\"0.55\""));
2436 assert!(!json.contains("\"pxUsd\""));
2437 assert!(!json.contains("\"px\":"));
2438 }
2439
2440 #[rstest]
2441 fn test_place_order_request_serializes_slippage_pct() {
2442 let request = OKXPlaceOrderRequest {
2443 inst_id: "BTC-USDT-SWAP".to_string(),
2444 td_mode: OKXTradeMode::Cross,
2445 ccy: None,
2446 cl_ord_id: Some("mkt-slip-1".to_string()),
2447 tag: None,
2448 side: OKXSide::Buy,
2449 pos_side: Some(OKXPositionSide::Net),
2450 ord_type: OKXOrderType::Market,
2451 sz: "1".to_string(),
2452 px: None,
2453 px_usd: None,
2454 px_vol: None,
2455 reduce_only: None,
2456 tgt_ccy: None,
2457 trade_quote_ccy: None,
2458 attach_algo_ords: None,
2459 outcome: None,
2460 slippage_pct: Some("0.005".to_string()),
2461 rpi_taker_access: None,
2462 rpi_px_round: None,
2463 };
2464
2465 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2466 assert_eq!(json["slippagePct"], "0.005");
2467 }
2468
2469 #[rstest]
2470 fn test_rpi_place_order_request_serialization() {
2471 let request = OKXPlaceOrderRequest {
2472 inst_id: "OMI-USD".to_string(),
2473 td_mode: OKXTradeMode::Cash,
2474 ccy: None,
2475 cl_ord_id: Some("ORPI001".to_string()),
2476 tag: Some("rpi-test".to_string()),
2477 side: OKXSide::Sell,
2478 pos_side: None,
2479 ord_type: OKXOrderType::Rpi,
2480 sz: "250000".to_string(),
2481 px: Some("0.0001600".to_string()),
2482 px_usd: None,
2483 px_vol: None,
2484 reduce_only: Some(false),
2485 tgt_ccy: None,
2486 trade_quote_ccy: None,
2487 attach_algo_ords: None,
2488 outcome: None,
2489 slippage_pct: None,
2490 rpi_taker_access: Some(true),
2491 rpi_px_round: Some(false),
2492 };
2493
2494 let value = serde_json::to_value(request).unwrap();
2495
2496 assert_eq!(
2497 value,
2498 serde_json::json!({
2499 "instId": "OMI-USD",
2500 "tdMode": "cash",
2501 "clOrdId": "ORPI001",
2502 "tag": "rpi-test",
2503 "side": "sell",
2504 "ordType": "rpi",
2505 "sz": "250000",
2506 "px": "0.0001600",
2507 "reduceOnly": false,
2508 "rpiTakerAccess": true,
2509 "rpiPxRound": false
2510 })
2511 );
2512 }
2513
2514 #[rstest]
2515 fn test_rpi_amend_order_request_serialization() {
2516 let request = OKXAmendOrderRequest {
2517 inst_id: "OMI-USD".to_string(),
2518 ord_id: Some("2500000000000000001".to_string()),
2519 cl_ord_id: None,
2520 req_id: Some("RPI-AMEND-1".to_string()),
2521 new_sz: Some("275000".to_string()),
2522 new_px: Some("0.0001599".to_string()),
2523 rpi_taker_access: Some(false),
2524 rpi_px_round: Some(true),
2525 };
2526
2527 let value = serde_json::to_value(request).unwrap();
2528
2529 assert_eq!(
2530 value,
2531 serde_json::json!({
2532 "instId": "OMI-USD",
2533 "ordId": "2500000000000000001",
2534 "reqId": "RPI-AMEND-1",
2535 "newSz": "275000",
2536 "newPx": "0.0001599",
2537 "rpiTakerAccess": false,
2538 "rpiPxRound": true
2539 })
2540 );
2541 }
2542
2543 #[rstest]
2544 fn test_rpi_place_order_response_parsing() {
2545 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2546 "ordId": "2500000000000000001",
2547 "clOrdId": "ORPI001",
2548 "tag": "rpi-test",
2549 "instId": "OMI-USD",
2550 "side": "sell",
2551 "ordType": "rpi",
2552 "sz": "250000",
2553 "state": "live",
2554 "px": "0.0001600",
2555 "avgPx": "",
2556 "sCode": "0",
2557 "sMsg": ""
2558 }))
2559 .unwrap();
2560
2561 assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
2562 assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
2563 assert_eq!(response.tag.as_deref(), Some("rpi-test"));
2564 assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
2565 assert_eq!(response.side, Some(OKXSide::Sell));
2566 assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
2567 assert_eq!(response.sz.as_deref(), Some("250000"));
2568 assert_eq!(response.state, Some(OKXOrderStatus::Live));
2569 assert_eq!(response.px.as_deref(), Some("0.0001600"));
2570 assert_eq!(response.avg_px.as_deref(), Some(""));
2571 assert_eq!(response.s_code.as_deref(), Some("0"));
2572 assert_eq!(response.s_msg.as_deref(), Some(""));
2573 }
2574
2575 #[rstest]
2576 fn test_event_contract_models_deserialize() {
2577 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2578 "seriesId": "BTC-ABOVE-DAILY",
2579 "freq": "daily",
2580 "title": "BTC above daily",
2581 "category": "1",
2582 "settlement": {
2583 "method": "cash",
2584 "closeEarly": false,
2585 "srcName": "OKX BTC/USD Index",
2586 "underlying": "BTC-USD"
2587 }
2588 }))
2589 .unwrap();
2590 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2591 "seriesId": "BTC-ABOVE-DAILY",
2592 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2593 "fixTime": "",
2594 "expTime": "1769697132335",
2595 "state": "live"
2596 }))
2597 .unwrap();
2598 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2599 "seriesId": "BTC-ABOVE-DAILY",
2600 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2601 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2602 "listTime": "1769697132335",
2603 "fixTime": "",
2604 "expTime": "1769697132335",
2605 "state": "live",
2606 "disputed": false,
2607 "outcome": "0",
2608 "floorStrike": "120000",
2609 "capStrike": "INF",
2610 "settleValue": "",
2611 "hitDir": ""
2612 }))
2613 .unwrap();
2614
2615 assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2616 assert_eq!(series.settlement.underlying, "BTC-USD");
2617 assert_eq!(event.fix_time, None);
2618 assert_eq!(event.exp_time, Some(1_769_697_132_335));
2619 assert_eq!(
2620 market.inst_id,
2621 Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2622 );
2623 assert_eq!(market.list_time, Some(1_769_697_132_335));
2624 assert_eq!(market.exp_time, Some(1_769_697_132_335));
2625 assert_eq!(market.outcome, "0");
2626 assert_eq!(market.cap_strike, "INF");
2627 assert_eq!(market.hit_dir, "");
2628
2629 let serialized = serde_json::to_value(&market).unwrap();
2630 assert_eq!(serialized["capStrike"], "INF");
2631 assert_eq!(serialized["hitDir"], "");
2632 }
2633
2634 #[rstest]
2635 fn test_event_contract_models_accept_missing_optional_fields() {
2636 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2637 "seriesId": "BTC-ABOVE-DAILY"
2638 }))
2639 .unwrap();
2640 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2641 "seriesId": "BTC-ABOVE-DAILY",
2642 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2643 "fixTime": "",
2644 "expTime": "",
2645 "state": "live"
2646 }))
2647 .unwrap();
2648 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2649 "seriesId": "BTC-ABOVE-DAILY",
2650 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2651 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2652 "listTime": "",
2653 "fixTime": "",
2654 "expTime": "",
2655 "state": "live",
2656 "disputed": false,
2657 "outcome": "0",
2658 "floorStrike": "120000",
2659 "settleValue": ""
2660 }))
2661 .unwrap();
2662
2663 assert_eq!(series.freq, "");
2664 assert_eq!(series.settlement.underlying, "");
2665 assert_eq!(event.exp_time, None);
2666 assert_eq!(market.list_time, None);
2667 assert_eq!(market.exp_time, None);
2668 assert_eq!(market.cap_strike, "");
2669 assert_eq!(market.hit_dir, "");
2670 }
2671
2672 #[rstest]
2673 fn test_place_order_request_serializes_event_contract_fields() {
2674 let request = OKXPlaceOrderRequest {
2675 inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2676 td_mode: OKXTradeMode::Cash,
2677 ccy: None,
2678 cl_ord_id: Some("event-1".to_string()),
2679 tag: None,
2680 side: OKXSide::Buy,
2681 pos_side: None,
2682 ord_type: OKXOrderType::Limit,
2683 sz: "10".to_string(),
2684 px: Some("0.42".to_string()),
2685 px_usd: None,
2686 px_vol: None,
2687 reduce_only: None,
2688 tgt_ccy: None,
2689 trade_quote_ccy: None,
2690 attach_algo_ords: None,
2691 outcome: Some("yes".to_string()),
2692 slippage_pct: None,
2693 rpi_taker_access: None,
2694 rpi_px_round: None,
2695 };
2696
2697 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2698
2699 assert!(json.get("speedBump").is_none());
2700 assert_eq!(json["outcome"], "yes");
2701 assert!(json.get("tradeQuoteCcy").is_none());
2702 }
2703
2704 #[rstest]
2705 fn test_place_order_request_serializes_trade_quote_ccy_usd() {
2706 let request = OKXPlaceOrderRequest {
2707 inst_id: "BTC-USDC".to_string(),
2708 td_mode: OKXTradeMode::Cash,
2709 ccy: None,
2710 cl_ord_id: Some("usd-quote-1".to_string()),
2711 tag: None,
2712 side: OKXSide::Buy,
2713 pos_side: None,
2714 ord_type: OKXOrderType::Limit,
2715 sz: "0.01".to_string(),
2716 px: Some("100000".to_string()),
2717 px_usd: None,
2718 px_vol: None,
2719 reduce_only: None,
2720 tgt_ccy: None,
2721 trade_quote_ccy: Some(Ustr::from("USD")),
2722 attach_algo_ords: None,
2723 outcome: None,
2724 slippage_pct: None,
2725 rpi_taker_access: None,
2726 rpi_px_round: None,
2727 };
2728
2729 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2730 assert_eq!(json["instId"], "BTC-USDC");
2731 assert_eq!(json["tradeQuoteCcy"], "USD");
2732 }
2733
2734 #[rstest]
2735 fn test_place_order_request_omits_trade_quote_ccy_when_unset() {
2736 let request = OKXPlaceOrderRequest {
2737 inst_id: "BTC-USDC".to_string(),
2738 td_mode: OKXTradeMode::Cash,
2739 ccy: None,
2740 cl_ord_id: Some("usdc-default-1".to_string()),
2741 tag: None,
2742 side: OKXSide::Buy,
2743 pos_side: None,
2744 ord_type: OKXOrderType::Limit,
2745 sz: "0.01".to_string(),
2746 px: Some("100000".to_string()),
2747 px_usd: None,
2748 px_vol: None,
2749 reduce_only: None,
2750 tgt_ccy: None,
2751 trade_quote_ccy: None,
2752 attach_algo_ords: None,
2753 outcome: None,
2754 slippage_pct: None,
2755 rpi_taker_access: None,
2756 rpi_px_round: None,
2757 };
2758
2759 let json = serde_json::to_string(&request).unwrap();
2760 assert!(!json.contains("tradeQuoteCcy"));
2761 }
2762
2763 #[rstest]
2764 fn test_attach_algo_ord_request_serializes_trailing_fields() {
2765 let request = OKXAttachAlgoOrdRequest {
2766 attach_algo_cl_ord_id: Some("trail-1".to_string()),
2767 sl_trigger_px: None,
2768 sl_ord_px: None,
2769 sl_trigger_px_type: None,
2770 tp_trigger_px: None,
2771 tp_ord_px: None,
2772 tp_trigger_px_type: None,
2773 callback_ratio: Some("0.01".to_string()),
2774 callback_spread: None,
2775 active_px: Some("64000".to_string()),
2776 new_callback_ratio: Some("0.02".to_string()),
2777 new_callback_spread: Some("25".to_string()),
2778 new_active_px: Some("65000".to_string()),
2779 };
2780
2781 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2782
2783 assert_eq!(json["callbackRatio"], "0.01");
2784 assert_eq!(json["activePx"], "64000");
2785 assert_eq!(json["newCallbackRatio"], "0.02");
2786 assert_eq!(json["newCallbackSpread"], "25");
2787 assert_eq!(json["newActivePx"], "65000");
2788 assert!(json.get("callbackSpread").is_none());
2789 }
2790
2791 #[rstest]
2792 fn test_place_order_response_deserializes_sub_code() {
2793 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2794 "ordId": "",
2795 "clOrdId": "event-1",
2796 "sCode": "51000",
2797 "sMsg": "Parameter error",
2798 "subCode": "51004"
2799 }))
2800 .unwrap();
2801
2802 assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2803 assert_eq!(response.s_code, Some("51000".to_string()));
2804 assert_eq!(response.sub_code, Some("51004".to_string()));
2805 }
2806
2807 #[rstest]
2808 fn test_fee_rate_deserializes_settle() {
2809 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2810 "level": "VIP1",
2811 "taker": "-0.0005",
2812 "maker": "-0.0002",
2813 "takerU": "-0.0005",
2814 "makerU": "-0.0002",
2815 "settle": "-0.001",
2816 "instType": "EVENTS",
2817 "category": "1",
2818 "ts": "1769697132335"
2819 }))
2820 .unwrap();
2821
2822 assert_eq!(fee_rate.settle, "-0.001");
2823 assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2824 }
2825
2826 #[rstest]
2827 #[case::missing(None, None)]
2828 #[case::empty(Some(""), None)]
2829 #[case::zero(Some("0"), Some(Decimal::ZERO))]
2830 #[case::commission(Some("-0.001"), Some(Decimal::from_str_exact("-0.001").unwrap()))]
2831 #[case::rebate(Some("0.0002"), Some(Decimal::from_str_exact("0.0002").unwrap()))]
2832 fn test_fee_group_rates_preserve_unavailable_zero_and_signs(
2833 #[case] value: Option<&str>,
2834 #[case] expected: Option<Decimal>,
2835 ) {
2836 let mut payload = serde_json::json!({"groupId": "1"});
2837
2838 if let Some(value) = value {
2839 for field in ["maker", "taker", "rpiMaker", "elpMaker"] {
2840 payload[field] = serde_json::json!(value);
2841 }
2842 }
2843
2844 let group: OKXFeeGroup = serde_json::from_value(payload).unwrap();
2845
2846 assert_eq!(group.group_id, "1");
2847 assert_eq!(group.maker, expected);
2848 assert_eq!(group.taker, expected);
2849 assert_eq!(group.rpi_maker, expected);
2850 assert_eq!(group.elp_maker, expected);
2851 }
2852
2853 #[rstest]
2854 #[case::rpi_only(Some("-0.00015"), None)]
2855 #[case::elp_only(None, Some("-0.00016"))]
2856 #[case::both_equal(Some("-0.00015"), Some("-0.00015"))]
2857 #[case::both_distinct(Some("-0.00015"), Some("-0.00016"))]
2858 fn test_fee_group_preserves_rpi_and_elp_independently(
2859 #[case] rpi: Option<&str>,
2860 #[case] elp: Option<&str>,
2861 ) {
2862 let mut payload = serde_json::json!({"groupId": "1"});
2863 if let Some(value) = rpi {
2864 payload["rpiMaker"] = serde_json::json!(value);
2865 }
2866
2867 if let Some(value) = elp {
2868 payload["elpMaker"] = serde_json::json!(value);
2869 }
2870
2871 let group: OKXFeeGroup = serde_json::from_value(payload).unwrap();
2872
2873 assert_eq!(
2874 group.rpi_maker,
2875 rpi.map(|value| Decimal::from_str_exact(value).unwrap())
2876 );
2877 assert_eq!(
2878 group.elp_maker,
2879 elp.map(|value| Decimal::from_str_exact(value).unwrap())
2880 );
2881 }
2882
2883 #[rstest]
2884 fn test_fee_rate_preserves_legacy_scalars_alongside_groups() {
2885 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2886 "level": "Lv1",
2887 "maker": "-0.0008",
2888 "taker": "-0.001",
2889 "makerU": "",
2890 "takerU": "0",
2891 "feeGroup": [{"groupId": "1", "maker": "0.0002", "taker": "-0.0005"}],
2892 "instType": "SPOT",
2893 "ts": "1763979985847"
2894 }))
2895 .unwrap();
2896
2897 assert_eq!(fee_rate.maker, "-0.0008");
2898 assert_eq!(fee_rate.taker, "-0.001");
2899 assert_eq!(fee_rate.maker_u, "");
2900 assert_eq!(fee_rate.taker_u, "0");
2901 assert_eq!(fee_rate.fee_group.len(), 1);
2902 assert_eq!(
2903 fee_rate.fee_group[0].maker,
2904 Some(Decimal::from_str_exact("0.0002").unwrap())
2905 );
2906 assert_eq!(
2907 fee_rate.fee_group[0].taker,
2908 Some(Decimal::from_str_exact("-0.0005").unwrap())
2909 );
2910 }
2911
2912 #[rstest]
2913 fn test_fee_rate_missing_scalars_do_not_imply_zero() {
2914 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2915 "level": "Lv1",
2916 "instType": "SPOT",
2917 "ts": "1763979985847"
2918 }))
2919 .unwrap();
2920
2921 assert!(fee_rate.maker.is_empty());
2922 assert!(fee_rate.taker.is_empty());
2923 assert!(fee_rate.maker_u.is_empty());
2924 assert!(fee_rate.taker_u.is_empty());
2925 assert!(fee_rate.fee_group.is_empty());
2926 }
2927
2928 #[rstest]
2929 fn test_fee_group_rejects_invalid_rates(
2930 #[values("maker", "taker", "rpiMaker", "elpMaker")] field: &str,
2931 ) {
2932 let mut payload = serde_json::json!({"groupId": "1"});
2933 payload[field] = serde_json::json!("invalid");
2934
2935 assert!(serde_json::from_value::<OKXFeeGroup>(payload).is_err());
2936 }
2937
2938 #[rstest]
2939 fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
2940 let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
2941 "level": "VIP1",
2942 "taker": "-0.0005",
2943 "maker": "-0.0002",
2944 "takerU": "-0.0005",
2945 "makerU": "-0.0002",
2946 "rpiMaker": "-0.00015",
2947 "instType": "SPOT",
2948 "category": "1",
2949 "ts": "1785406500000"
2950 }))
2951 .unwrap();
2952 let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
2953 "level": "VIP1",
2954 "taker": "-0.0005",
2955 "maker": "-0.0002",
2956 "takerU": "-0.0005",
2957 "makerU": "-0.0002",
2958 "elpMaker": "-0.00016",
2959 "instType": "SPOT",
2960 "category": "1",
2961 "ts": "1785406500001"
2962 }))
2963 .unwrap();
2964 let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
2965 "level": "VIP1",
2966 "taker": "-0.0005",
2967 "maker": "-0.0002",
2968 "takerU": "-0.0005",
2969 "makerU": "-0.0002",
2970 "rpiMaker": "",
2971 "instType": "OPTION",
2972 "category": "1",
2973 "ts": "1785406500002"
2974 }))
2975 .unwrap();
2976
2977 assert_eq!(
2978 rpi.rpi_maker,
2979 Some(Decimal::from_str_exact("-0.00015").unwrap())
2980 );
2981 assert_eq!(
2982 legacy.rpi_maker,
2983 Some(Decimal::from_str_exact("-0.00016").unwrap())
2984 );
2985 assert_eq!(not_applicable.rpi_maker, None);
2986 }
2987}