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)]
554#[serde(rename_all = "camelCase")]
555pub struct OKXAccount {
556 pub adj_eq: String,
558 pub borrow_froz: String,
560 pub details: Vec<OKXBalanceDetail>,
562 pub imr: String,
564 pub iso_eq: String,
566 pub mgn_ratio: String,
568 pub mmr: String,
570 pub notional_usd_for_borrow: String,
572 pub notional_usd_for_futures: String,
574 pub notional_usd_for_option: String,
576 pub notional_usd_for_swap: String,
578 pub notional_usd: String,
580 pub ord_froz: String,
582 pub total_eq: String,
584 #[serde(deserialize_with = "deserialize_string_to_u64")]
586 pub u_time: u64,
587 pub upl: String,
589}
590
591#[derive(Clone, Debug, Serialize, Deserialize)]
593#[serde(rename_all = "camelCase")]
594#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
595#[cfg_attr(
596 feature = "python",
597 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
598)]
599pub struct OKXBalanceDetail {
600 pub avail_bal: String,
602 pub avail_eq: String,
604 pub borrow_froz: String,
606 pub cash_bal: String,
608 pub ccy: Ustr,
610 pub cross_liab: String,
612 pub dis_eq: String,
614 pub eq: String,
616 pub eq_usd: String,
618 pub smt_sync_eq: String,
620 pub spot_copy_trading_eq: String,
622 pub fixed_bal: String,
624 pub frozen_bal: String,
626 pub imr: String,
628 pub interest: String,
630 pub iso_eq: String,
632 pub iso_liab: String,
634 pub iso_upl: String,
636 pub liab: String,
638 pub max_loan: String,
640 pub mgn_ratio: String,
642 pub mmr: String,
644 pub notional_lever: String,
646 pub ord_frozen: String,
648 pub reward_bal: String,
650 #[serde(alias = "spotInUse")]
652 pub spot_in_use_amt: String,
653 #[serde(alias = "clSpotInUse")]
655 pub cl_spot_in_use_amt: String,
656 #[serde(alias = "maxSpotInUse")]
658 pub max_spot_in_use_amt: String,
659 pub spot_iso_bal: String,
661 pub stgy_eq: String,
663 pub twap: String,
665 #[serde(deserialize_with = "deserialize_string_to_u64")]
667 pub u_time: u64,
668 pub upl: String,
670 pub upl_liab: String,
672 pub spot_bal: String,
674 pub open_avg_px: String,
676 pub acc_avg_px: String,
678 pub spot_upl: String,
680 pub spot_upl_ratio: String,
682 pub total_pnl: String,
684 pub total_pnl_ratio: String,
686}
687
688#[derive(Clone, Debug, Serialize, Deserialize)]
690#[serde(rename_all = "camelCase")]
691pub struct OKXPosition {
692 pub inst_id: Ustr,
694 pub inst_type: OKXInstrumentType,
696 pub mgn_mode: OKXMarginMode,
698 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
700 pub pos_id: Option<Ustr>,
701 pub pos_side: OKXPositionSide,
703 pub pos: String,
705 pub base_bal: String,
707 pub ccy: String,
709 pub fee: String,
711 pub lever: String,
713 pub last: String,
715 pub mark_px: String,
717 pub liq_px: String,
719 pub mmr: String,
721 pub interest: String,
723 pub trade_id: Ustr,
725 pub notional_usd: String,
727 pub avg_px: String,
729 pub upl: String,
731 pub upl_ratio: String,
733 #[serde(deserialize_with = "deserialize_string_to_u64")]
735 pub u_time: u64,
736 pub margin: String,
738 pub mgn_ratio: String,
740 pub adl: String,
742 pub c_time: String,
744 pub realized_pnl: String,
746 pub upl_last_px: String,
748 pub upl_ratio_last_px: String,
750 pub avail_pos: String,
752 pub be_px: String,
754 pub funding_fee: String,
756 pub idx_px: String,
758 pub liq_penalty: String,
760 pub opt_val: String,
762 pub pending_close_ord_liab_val: String,
764 pub pnl: String,
766 pub pos_ccy: String,
768 pub quote_bal: String,
770 pub quote_borrowed: String,
772 pub quote_interest: String,
774 #[serde(alias = "spotInUse")]
776 pub spot_in_use_amt: String,
777 pub spot_in_use_ccy: String,
779 pub usd_px: String,
781 #[serde(default)]
783 pub delta_bs: String,
784 #[serde(default)]
786 pub gamma_bs: String,
787 #[serde(default)]
789 pub theta_bs: String,
790 #[serde(default)]
792 pub vega_bs: String,
793 #[serde(default)]
795 pub close_order_algo: Vec<OKXCloseOrderAlgo>,
796}
797
798#[derive(Clone, Debug, Serialize, Deserialize)]
800#[serde(rename_all = "camelCase")]
801pub struct OKXCloseOrderAlgo {
802 pub algo_id: String,
804}
805
806#[derive(Clone, Debug, Serialize, Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct OKXPlaceOrderResponse {
811 #[serde(default)]
813 pub ord_id: Option<Ustr>,
814 #[serde(default)]
816 pub cl_ord_id: Option<Ustr>,
817 #[serde(default)]
819 pub tag: Option<String>,
820 #[serde(default)]
822 pub inst_id: Option<Ustr>,
823 #[serde(default)]
825 pub side: Option<OKXSide>,
826 #[serde(default)]
828 pub ord_type: Option<OKXOrderType>,
829 #[serde(default)]
831 pub sz: Option<String>,
832 pub state: Option<OKXOrderStatus>,
834 #[serde(default)]
836 pub px: Option<String>,
837 #[serde(default)]
839 pub avg_px: Option<String>,
840 #[serde(default)]
842 pub acc_fill_sz: Option<String>,
843 #[serde(default)]
845 pub fill_sz: Option<String>,
846 #[serde(default)]
848 pub fill_px: Option<String>,
849 #[serde(default)]
851 pub trade_id: Option<Ustr>,
852 #[serde(default)]
854 pub fill_time: Option<String>,
855 #[serde(default)]
857 pub fee: Option<String>,
858 #[serde(default)]
860 pub fee_ccy: Option<String>,
861 #[serde(default)]
863 pub req_id: Option<Ustr>,
864 #[serde(default)]
866 pub pos_side: Option<OKXPositionSide>,
867 #[serde(default)]
869 pub reduce_only: Option<String>,
870 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
872 pub tgt_ccy: Option<OKXTargetCurrency>,
873 #[serde(default)]
875 pub c_time: Option<String>,
876 #[serde(default)]
878 pub u_time: Option<String>,
879 #[serde(skip_serializing_if = "Option::is_none")]
881 pub s_code: Option<String>,
882 #[serde(skip_serializing_if = "Option::is_none")]
884 pub s_msg: Option<String>,
885 #[serde(skip_serializing_if = "Option::is_none")]
887 pub sub_code: Option<String>,
888}
889
890#[derive(Clone, Debug, Default, Serialize, Deserialize)]
892#[serde(rename_all = "camelCase")]
893pub struct OKXAttachAlgoOrdRequest {
894 #[serde(skip_serializing_if = "Option::is_none")]
896 pub attach_algo_cl_ord_id: Option<String>,
897 #[serde(skip_serializing_if = "Option::is_none")]
899 pub sl_trigger_px: Option<String>,
900 #[serde(skip_serializing_if = "Option::is_none")]
902 pub sl_ord_px: Option<String>,
903 #[serde(skip_serializing_if = "Option::is_none")]
905 pub sl_trigger_px_type: Option<OKXTriggerType>,
906 #[serde(skip_serializing_if = "Option::is_none")]
908 pub tp_trigger_px: Option<String>,
909 #[serde(skip_serializing_if = "Option::is_none")]
911 pub tp_ord_px: Option<String>,
912 #[serde(skip_serializing_if = "Option::is_none")]
914 pub tp_trigger_px_type: Option<OKXTriggerType>,
915 #[serde(skip_serializing_if = "Option::is_none")]
917 pub callback_ratio: Option<String>,
918 #[serde(skip_serializing_if = "Option::is_none")]
920 pub callback_spread: Option<String>,
921 #[serde(skip_serializing_if = "Option::is_none")]
923 pub active_px: Option<String>,
924 #[serde(skip_serializing_if = "Option::is_none")]
926 pub new_callback_ratio: Option<String>,
927 #[serde(skip_serializing_if = "Option::is_none")]
929 pub new_callback_spread: Option<String>,
930 #[serde(skip_serializing_if = "Option::is_none")]
932 pub new_active_px: Option<String>,
933}
934
935#[derive(Clone, Debug, Serialize, Deserialize)]
937#[serde(rename_all = "camelCase")]
938pub struct OKXPlaceOrderRequest {
939 pub inst_id: String,
941 pub td_mode: OKXTradeMode,
943 #[serde(skip_serializing_if = "Option::is_none")]
945 pub ccy: Option<String>,
946 #[serde(skip_serializing_if = "Option::is_none")]
948 pub cl_ord_id: Option<String>,
949 #[serde(skip_serializing_if = "Option::is_none")]
951 pub tag: Option<String>,
952 pub side: OKXSide,
954 #[serde(skip_serializing_if = "Option::is_none")]
956 pub pos_side: Option<OKXPositionSide>,
957 pub ord_type: OKXOrderType,
959 pub sz: String,
961 #[serde(skip_serializing_if = "Option::is_none")]
963 pub px: Option<String>,
964 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
966 pub px_usd: Option<String>,
967 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
970 pub px_vol: Option<String>,
971 #[serde(skip_serializing_if = "Option::is_none")]
973 pub reduce_only: Option<bool>,
974 #[serde(skip_serializing_if = "Option::is_none")]
976 pub tgt_ccy: Option<OKXTargetCurrency>,
977 #[serde(skip_serializing_if = "Option::is_none")]
979 pub trade_quote_ccy: Option<Ustr>,
980 #[serde(skip_serializing_if = "Option::is_none")]
982 pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
983 #[serde(skip_serializing_if = "Option::is_none")]
985 pub outcome: Option<String>,
986 #[serde(skip_serializing_if = "Option::is_none")]
991 pub slippage_pct: Option<String>,
992 #[serde(skip_serializing_if = "Option::is_none")]
994 pub rpi_taker_access: Option<bool>,
995 #[serde(skip_serializing_if = "Option::is_none")]
997 pub rpi_px_round: Option<bool>,
998}
999
1000#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1002#[serde(rename_all = "camelCase")]
1003pub struct OKXAmendOrderRequest {
1004 pub inst_id: String,
1006 #[serde(skip_serializing_if = "Option::is_none")]
1008 pub ord_id: Option<String>,
1009 #[serde(skip_serializing_if = "Option::is_none")]
1011 pub cl_ord_id: Option<String>,
1012 #[serde(skip_serializing_if = "Option::is_none")]
1014 pub req_id: Option<String>,
1015 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub new_sz: Option<String>,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub new_px: Option<String>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub rpi_taker_access: Option<bool>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub rpi_px_round: Option<bool>,
1027}
1028
1029#[derive(Clone, Debug, Serialize, Deserialize)]
1031#[serde(rename_all = "camelCase")]
1032pub struct OKXCancelOrderRequest {
1033 pub inst_id: String,
1035 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1037 pub inst_id_code: Option<u64>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub ord_id: Option<String>,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub cl_ord_id: Option<String>,
1044}
1045
1046#[derive(Clone, Debug, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct OKXCancelOrderResponse {
1050 pub ord_id: String,
1052 #[serde(default)]
1054 pub cl_ord_id: Option<String>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1057 pub s_code: Option<String>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub s_msg: Option<String>,
1061 #[serde(default)]
1063 pub ts: Option<String>,
1064}
1065
1066pub use crate::common::models::OKXAttachedAlgoOrd;
1067
1068#[derive(Clone, Debug, Serialize, Deserialize)]
1070#[serde(rename_all = "camelCase")]
1071pub struct OKXOrderHistory {
1072 pub ord_id: Ustr,
1074 pub cl_ord_id: Ustr,
1076 #[serde(default)]
1078 pub algo_id: Option<Ustr>,
1079 #[serde(default)]
1081 pub algo_cl_ord_id: Option<Ustr>,
1082 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1084 pub attach_algo_cl_ord_id: Option<String>,
1085 #[serde(default)]
1087 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1088 #[serde(default)]
1090 pub cl_act_id: Option<Ustr>,
1091 pub tag: String,
1093 pub inst_type: OKXInstrumentType,
1095 pub uly: Option<Ustr>,
1097 pub inst_id: Ustr,
1099 pub ord_type: OKXOrderType,
1101 pub sz: String,
1103 pub px: String,
1105 #[serde(default)]
1107 pub px_usd: String,
1108 #[serde(default)]
1110 pub px_vol: String,
1111 pub side: OKXSide,
1113 pub pos_side: OKXPositionSide,
1115 pub td_mode: OKXTradeMode,
1117 pub reduce_only: String,
1119 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1121 pub tgt_ccy: Option<OKXTargetCurrency>,
1122 pub state: OKXOrderStatus,
1124 pub avg_px: String,
1126 pub fee: String,
1128 pub fee_ccy: String,
1130 pub fill_sz: String,
1132 pub fill_px: String,
1134 pub trade_id: Ustr,
1136 #[serde(deserialize_with = "deserialize_string_to_u64")]
1138 pub fill_time: u64,
1139 pub acc_fill_sz: String,
1141 #[serde(default)]
1143 pub fill_fee: Option<String>,
1144 #[serde(default)]
1146 pub req_id: Option<Ustr>,
1147 #[serde(default)]
1149 pub cancel_fill_sz: Option<String>,
1150 #[serde(default)]
1152 pub cancel_total_sz: Option<String>,
1153 #[serde(default)]
1155 pub cancel_source: String,
1156 #[serde(default)]
1158 pub cancel_source_reason: String,
1159 #[serde(default)]
1161 pub fee_discount: Option<String>,
1162 pub category: OKXOrderCategory,
1164 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1166 pub outcome: Option<String>,
1167 #[serde(deserialize_with = "deserialize_string_to_u64")]
1169 pub u_time: u64,
1170 #[serde(deserialize_with = "deserialize_string_to_u64")]
1172 pub c_time: u64,
1173}
1174
1175#[derive(Clone, Debug, Serialize, Deserialize)]
1177#[serde(rename_all = "camelCase")]
1178pub struct OKXOrderAlgo {
1179 pub algo_id: String,
1181 #[serde(default)]
1183 pub algo_cl_ord_id: String,
1184 #[serde(default)]
1186 pub cl_ord_id: String,
1187 #[serde(default)]
1189 pub ord_id: String,
1190 #[serde(default)]
1192 pub ord_id_list: Vec<String>,
1193 #[serde(default)]
1195 pub sub_algo_id_list: Vec<String>,
1196 pub inst_id: Ustr,
1198 pub inst_type: OKXInstrumentType,
1200 pub ord_type: OKXAlgoOrderType,
1202 pub state: OKXAlgoOrderStatus,
1204 pub side: OKXSide,
1206 pub pos_side: OKXPositionSide,
1208 #[serde(default)]
1210 pub sz: String,
1211 #[serde(default)]
1213 pub trigger_px: String,
1214 #[serde(default)]
1216 pub trigger_px_type: Option<OKXTriggerType>,
1217 #[serde(default)]
1219 pub sl_trigger_px: String,
1220 #[serde(default)]
1222 pub sl_ord_px: String,
1223 #[serde(default)]
1225 pub sl_trigger_px_type: Option<OKXTriggerType>,
1226 #[serde(default)]
1228 pub tp_trigger_px: String,
1229 #[serde(default)]
1231 pub tp_ord_px: String,
1232 #[serde(default)]
1234 pub tp_trigger_px_type: Option<OKXTriggerType>,
1235 #[serde(default)]
1237 pub ord_px: String,
1238 pub td_mode: OKXTradeMode,
1240 #[serde(default)]
1242 pub lever: String,
1243 #[serde(default)]
1245 pub reduce_only: String,
1246 #[serde(default)]
1248 pub close_fraction: String,
1249 #[serde(default)]
1251 pub actual_px: String,
1252 #[serde(default)]
1254 pub actual_sz: String,
1255 #[serde(default)]
1257 pub notional_usd: String,
1258 #[serde(deserialize_with = "deserialize_string_to_u64")]
1260 pub c_time: u64,
1261 #[serde(deserialize_with = "deserialize_string_to_u64")]
1263 pub u_time: u64,
1264 #[serde(default)]
1266 pub trigger_time: String,
1267 #[serde(default)]
1269 pub tag: String,
1270 #[serde(default)]
1272 pub callback_ratio: String,
1273 #[serde(default)]
1275 pub callback_spread: String,
1276 #[serde(default)]
1278 pub active_px: String,
1279}
1280
1281#[derive(Clone, Debug, Serialize, Deserialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct OKXTransactionDetail {
1285 pub inst_type: OKXInstrumentType,
1287 pub inst_id: Ustr,
1289 pub trade_id: Ustr,
1291 pub ord_id: Ustr,
1293 pub cl_ord_id: Ustr,
1295 pub bill_id: Ustr,
1297 pub fill_px: String,
1299 pub fill_sz: String,
1301 pub side: OKXSide,
1303 pub exec_type: OKXExecType,
1305 pub fee_ccy: String,
1307 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1309 pub fee: Option<String>,
1310 #[serde(deserialize_with = "deserialize_string_to_u64")]
1312 pub ts: u64,
1313}
1314
1315#[derive(Clone, Debug, Serialize, Deserialize)]
1317#[serde(rename_all = "camelCase")]
1318pub struct OKXPositionHistory {
1319 pub inst_type: OKXInstrumentType,
1321 pub inst_id: Ustr,
1323 pub mgn_mode: OKXMarginMode,
1325 #[serde(rename = "type")]
1328 pub r#type: Ustr,
1329 pub c_time: String,
1331 #[serde(deserialize_with = "deserialize_string_to_u64")]
1333 pub u_time: u64,
1334 pub open_avg_px: String,
1336 #[serde(skip_serializing_if = "Option::is_none")]
1338 pub close_avg_px: Option<String>,
1339 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1341 pub pos_id: Option<Ustr>,
1342 #[serde(skip_serializing_if = "Option::is_none")]
1344 pub open_max_pos: Option<String>,
1345 #[serde(skip_serializing_if = "Option::is_none")]
1347 pub close_total_pos: Option<String>,
1348 #[serde(skip_serializing_if = "Option::is_none")]
1350 pub realized_pnl: Option<String>,
1351 #[serde(skip_serializing_if = "Option::is_none")]
1353 pub fee: Option<String>,
1354 #[serde(skip_serializing_if = "Option::is_none")]
1356 pub funding_fee: Option<String>,
1357 #[serde(skip_serializing_if = "Option::is_none")]
1359 pub liq_penalty: Option<String>,
1360 #[serde(skip_serializing_if = "Option::is_none")]
1362 pub pnl: Option<String>,
1363 #[serde(skip_serializing_if = "Option::is_none")]
1365 pub pnl_ratio: Option<String>,
1366 pub pos_side: OKXPositionSide,
1368 pub lever: String,
1370 #[serde(skip_serializing_if = "Option::is_none")]
1372 pub direction: Option<String>,
1373 #[serde(skip_serializing_if = "Option::is_none")]
1375 pub trigger_px: Option<String>,
1376 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub uly: Option<String>,
1379 #[serde(skip_serializing_if = "Option::is_none")]
1381 pub ccy: Option<String>,
1382}
1383
1384#[derive(Clone, Debug, Serialize, Deserialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct OKXPlaceAlgoOrderRequest {
1388 #[serde(rename = "instId")]
1390 pub inst_id: String,
1391 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1393 pub inst_id_code: Option<u64>,
1394 #[serde(rename = "tdMode")]
1396 pub td_mode: OKXTradeMode,
1397 pub side: OKXSide,
1399 #[serde(rename = "ordType")]
1401 pub ord_type: OKXAlgoOrderType,
1402 #[serde(skip_serializing_if = "Option::is_none")]
1404 pub sz: Option<String>,
1405 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1407 pub algo_cl_ord_id: Option<String>,
1408 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1410 pub trigger_px: Option<String>,
1411 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1413 pub order_px: Option<String>,
1414 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1416 pub trigger_px_type: Option<OKXTriggerType>,
1417 #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1419 pub sl_trigger_px: Option<String>,
1420 #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1422 pub sl_ord_px: Option<String>,
1423 #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1425 pub sl_trigger_px_type: Option<OKXTriggerType>,
1426 #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1428 pub tp_trigger_px: Option<String>,
1429 #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1431 pub tp_ord_px: Option<String>,
1432 #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1434 pub tp_trigger_px_type: Option<OKXTriggerType>,
1435 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1437 pub tgt_ccy: Option<OKXTargetCurrency>,
1438 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1440 pub pos_side: Option<OKXPositionSide>,
1441 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1443 pub close_position: Option<bool>,
1444 #[serde(skip_serializing_if = "Option::is_none")]
1446 pub tag: Option<String>,
1447 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1449 pub reduce_only: Option<bool>,
1450 #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1452 pub close_fraction: Option<String>,
1453 #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1456 pub callback_ratio: Option<String>,
1457 #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1460 pub callback_spread: Option<String>,
1461 #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1464 pub active_px: Option<String>,
1465}
1466
1467#[derive(Clone, Debug, Serialize, Deserialize)]
1469#[serde(rename_all = "camelCase")]
1470pub struct OKXPlaceAlgoOrderResponse {
1471 pub algo_id: String,
1473 #[serde(skip_serializing_if = "Option::is_none")]
1475 pub algo_cl_ord_id: Option<String>,
1476 #[serde(skip_serializing_if = "Option::is_none")]
1478 pub s_code: Option<String>,
1479 #[serde(skip_serializing_if = "Option::is_none")]
1481 pub s_msg: Option<String>,
1482 #[serde(skip_serializing_if = "Option::is_none")]
1484 pub req_id: Option<String>,
1485}
1486
1487#[derive(Clone, Debug, Serialize, Deserialize)]
1489#[serde(rename_all = "camelCase")]
1490pub struct OKXCancelAlgoOrderRequest {
1491 pub inst_id: String,
1493 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1495 pub inst_id_code: Option<u64>,
1496 #[serde(skip_serializing_if = "Option::is_none")]
1498 pub algo_id: Option<String>,
1499 #[serde(skip_serializing_if = "Option::is_none")]
1501 pub algo_cl_ord_id: Option<String>,
1502}
1503
1504#[derive(Clone, Debug, Serialize, Deserialize)]
1506#[serde(rename_all = "camelCase")]
1507pub struct OKXCancelAlgoOrderResponse {
1508 pub algo_id: String,
1510 #[serde(skip_serializing_if = "Option::is_none")]
1512 pub s_code: Option<String>,
1513 #[serde(skip_serializing_if = "Option::is_none")]
1515 pub s_msg: Option<String>,
1516}
1517
1518#[derive(Clone, Debug, Serialize, Deserialize)]
1520#[serde(rename_all = "camelCase")]
1521pub struct OKXAmendAlgoOrderRequest {
1522 pub inst_id: String,
1524 pub algo_id: String,
1526 #[serde(skip_serializing_if = "Option::is_none")]
1528 pub algo_cl_ord_id: Option<String>,
1529 #[serde(skip_serializing_if = "Option::is_none")]
1531 pub new_sz: Option<String>,
1532 #[serde(skip_serializing_if = "Option::is_none")]
1534 pub new_trigger_px: Option<String>,
1535 #[serde(skip_serializing_if = "Option::is_none")]
1537 pub new_trigger_px_type: Option<String>,
1538 #[serde(skip_serializing_if = "Option::is_none")]
1540 pub new_tp_trigger_px: Option<String>,
1541 #[serde(skip_serializing_if = "Option::is_none")]
1543 pub new_tp_ord_px: Option<String>,
1544 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub new_tp_trigger_px_type: Option<String>,
1547 #[serde(skip_serializing_if = "Option::is_none")]
1551 pub new_sl_trigger_px: Option<String>,
1552 #[serde(skip_serializing_if = "Option::is_none")]
1554 pub new_sl_ord_px: Option<String>,
1555 #[serde(skip_serializing_if = "Option::is_none")]
1557 pub new_sl_trigger_px_type: Option<String>,
1558 #[serde(skip_serializing_if = "Option::is_none")]
1560 #[serde(rename = "newOrdPx")]
1561 pub new_order_px: Option<String>,
1562 #[serde(skip_serializing_if = "Option::is_none")]
1564 pub new_callback_ratio: Option<String>,
1565 #[serde(skip_serializing_if = "Option::is_none")]
1567 pub new_callback_spread: Option<String>,
1568 #[serde(skip_serializing_if = "Option::is_none")]
1570 pub new_active_px: Option<String>,
1571}
1572
1573#[derive(Clone, Debug, Serialize, Deserialize)]
1575#[serde(rename_all = "camelCase")]
1576pub struct OKXAmendAlgoOrderResponse {
1577 pub algo_id: String,
1579 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub algo_cl_ord_id: Option<String>,
1582 #[serde(skip_serializing_if = "Option::is_none")]
1584 pub s_code: Option<String>,
1585 #[serde(skip_serializing_if = "Option::is_none")]
1587 pub s_msg: Option<String>,
1588 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub req_id: Option<String>,
1591}
1592
1593#[derive(Clone, Debug, Serialize, Deserialize)]
1595#[serde(rename_all = "camelCase")]
1596pub struct OKXServerTime {
1597 #[serde(deserialize_with = "deserialize_string_to_u64")]
1599 pub ts: u64,
1600}
1601
1602#[derive(Clone, Debug, Serialize, Deserialize)]
1607#[serde(rename_all = "camelCase")]
1608pub struct OKXFeeGroup {
1609 pub group_id: String,
1611 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1613 pub maker: Option<Decimal>,
1614 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1616 pub taker: Option<Decimal>,
1617 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1619 pub rpi_maker: Option<Decimal>,
1620 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
1622 pub elp_maker: Option<Decimal>,
1623}
1624
1625#[derive(Clone, Debug, Serialize, Deserialize)]
1627#[serde(rename_all = "camelCase")]
1628pub struct OKXFeeRate {
1629 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1631 pub level: OKXVipLevel,
1632 #[serde(default)]
1634 pub taker: String,
1635 #[serde(default)]
1637 pub maker: String,
1638 #[serde(default)]
1640 pub taker_u: String,
1641 #[serde(default)]
1643 pub maker_u: String,
1644 #[serde(default)]
1646 pub fee_group: Vec<OKXFeeGroup>,
1647 #[serde(
1649 default,
1650 alias = "elpMaker",
1651 deserialize_with = "deserialize_optional_decimal"
1652 )]
1653 pub rpi_maker: Option<Decimal>,
1654 #[serde(default)]
1656 pub delivery: String,
1657 #[serde(default)]
1659 pub exercise: String,
1660 #[serde(default)]
1662 pub settle: String,
1663 pub inst_type: OKXInstrumentType,
1665 #[serde(default)]
1667 pub category: String,
1668 #[serde(deserialize_with = "deserialize_string_to_u64")]
1670 pub ts: u64,
1671}
1672
1673fn deserialize_configuration_enum<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1674where
1675 D: Deserializer<'de>,
1676 T: Deserialize<'de>,
1677{
1678 let value = String::deserialize(deserializer)?;
1679 T::deserialize(value.into_deserializer())
1680}
1681
1682mod account_permissions {
1683 use serde::{Deserialize, Deserializer, Serializer, de::IntoDeserializer};
1684
1685 use crate::common::enums::OKXApiKeyPermission;
1686
1687 pub(super) fn serialize<S>(
1688 permissions: &[OKXApiKeyPermission],
1689 serializer: S,
1690 ) -> Result<S::Ok, S::Error>
1691 where
1692 S: Serializer,
1693 {
1694 let value = permissions
1695 .iter()
1696 .map(AsRef::as_ref)
1697 .collect::<Vec<_>>()
1698 .join(",");
1699 serializer.serialize_str(&value)
1700 }
1701
1702 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<OKXApiKeyPermission>, D::Error>
1703 where
1704 D: Deserializer<'de>,
1705 {
1706 let value = String::deserialize(deserializer)?;
1707 value
1708 .split(',')
1709 .map(|permission| OKXApiKeyPermission::deserialize(permission.into_deserializer()))
1710 .collect()
1711 }
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716 use rstest::rstest;
1717 use serde_json;
1718
1719 use super::*;
1720
1721 #[rstest]
1722 fn test_algo_order_deserializes_current_child_identifier_lists() {
1723 let order: OKXOrderAlgo = serde_json::from_value(serde_json::json!({
1724 "algoId": "123",
1725 "algoClOrdId": "algo-client-1",
1726 "ordId": "456",
1727 "ordIdList": ["456", "457"],
1728 "subAlgoIdList": ["789"],
1729 "instId": "ETH-USDT-SWAP",
1730 "instType": "SWAP",
1731 "ordType": "conditional",
1732 "state": "effective",
1733 "side": "sell",
1734 "posSide": "net",
1735 "tdMode": "cross",
1736 "cTime": "1700000000000",
1737 "uTime": "1700000001000"
1738 }))
1739 .unwrap();
1740
1741 assert_eq!(order.ord_id, "456");
1742 assert_eq!(order.ord_id_list, ["456", "457"]);
1743 assert_eq!(order.sub_algo_id_list, ["789"]);
1744 }
1745
1746 #[rstest]
1747 fn test_algo_order_request_serialization() {
1748 let request = OKXPlaceAlgoOrderRequest {
1749 inst_id: "ETH-USDT-SWAP".to_string(),
1750 inst_id_code: None,
1751 td_mode: OKXTradeMode::Isolated,
1752 side: OKXSide::Buy,
1753 ord_type: OKXAlgoOrderType::Trigger,
1754 sz: Some("0.01".to_string()),
1755 algo_cl_ord_id: Some("test123".to_string()),
1756 trigger_px: Some("3000".to_string()),
1757 order_px: Some("-1".to_string()),
1758 trigger_px_type: Some(OKXTriggerType::Last),
1759 sl_trigger_px: None,
1760 sl_ord_px: None,
1761 sl_trigger_px_type: None,
1762 tp_trigger_px: None,
1763 tp_ord_px: None,
1764 tp_trigger_px_type: None,
1765 tgt_ccy: None,
1766 pos_side: None,
1767 close_position: None,
1768 tag: None,
1769 reduce_only: None,
1770 close_fraction: None,
1771 callback_ratio: None,
1772 callback_spread: None,
1773 active_px: None,
1774 };
1775
1776 let json = serde_json::to_string(&request).unwrap();
1777
1778 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1780 assert!(json.contains("\"tdMode\":\"isolated\""));
1781 assert!(json.contains("\"ordType\":\"trigger\""));
1782 assert!(json.contains("\"algoClOrdId\":\"test123\""));
1783 assert!(json.contains("\"triggerPx\":\"3000\""));
1784 assert!(json.contains("\"orderPx\":\"-1\""));
1785 assert!(json.contains("\"triggerPxType\":\"last\""));
1786
1787 assert!(!json.contains("tgtCcy"));
1789 assert!(!json.contains("posSide"));
1790 assert!(!json.contains("closePosition"));
1791 assert!(!json.contains("closeFraction"));
1792 }
1793
1794 #[rstest]
1795 fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1796 let request = OKXAmendAlgoOrderRequest {
1800 inst_id: "ETH-USDT-SWAP".to_string(),
1801 algo_id: "123".to_string(),
1802 algo_cl_ord_id: None,
1803 new_sz: None,
1804 new_trigger_px: None,
1805 new_trigger_px_type: None,
1806 new_tp_trigger_px: None,
1807 new_tp_ord_px: None,
1808 new_tp_trigger_px_type: None,
1809 new_sl_trigger_px: Some("850".to_string()),
1810 new_sl_ord_px: None,
1811 new_sl_trigger_px_type: None,
1812 new_order_px: None,
1813 new_callback_ratio: None,
1814 new_callback_spread: None,
1815 new_active_px: None,
1816 };
1817
1818 let json = serde_json::to_string(&request).unwrap();
1819
1820 assert!(json.contains("\"algoId\":\"123\""));
1821 assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1822 assert!(!json.contains("newTriggerPx"));
1824 assert!(!json.contains("newSz"));
1825 assert!(!json.contains("algoClOrdId"));
1826 }
1827
1828 #[rstest]
1829 fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1830 let request = OKXAmendAlgoOrderRequest {
1831 inst_id: "DOGE-USDT-SWAP".to_string(),
1832 algo_id: "algo-oco-1".to_string(),
1833 algo_cl_ord_id: None,
1834 new_sz: None,
1835 new_trigger_px: None,
1836 new_trigger_px_type: None,
1837 new_tp_trigger_px: Some("0.10495".to_string()),
1838 new_tp_ord_px: Some("-1".to_string()),
1839 new_tp_trigger_px_type: Some("last".to_string()),
1840 new_sl_trigger_px: Some("0.06297".to_string()),
1841 new_sl_ord_px: Some("-1".to_string()),
1842 new_sl_trigger_px_type: Some("last".to_string()),
1843 new_order_px: None,
1844 new_callback_ratio: None,
1845 new_callback_spread: None,
1846 new_active_px: None,
1847 };
1848
1849 let json = serde_json::to_string(&request).unwrap();
1850
1851 assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1852 assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1853 assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1854 assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1855 assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1856 assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1857 assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1858 assert!(!json.contains("newTriggerPx"));
1859 }
1860
1861 #[rstest]
1862 fn test_algo_order_request_serializes_close_fraction() {
1863 let request = OKXPlaceAlgoOrderRequest {
1864 inst_id: "ETH-USDT-SWAP".to_string(),
1865 inst_id_code: None,
1866 td_mode: OKXTradeMode::Cross,
1867 side: OKXSide::Sell,
1868 ord_type: OKXAlgoOrderType::Conditional,
1869 sz: None,
1870 algo_cl_ord_id: Some("close-frac-123".to_string()),
1871 trigger_px: None,
1872 order_px: None,
1873 trigger_px_type: None,
1874 sl_trigger_px: Some("3000".to_string()),
1875 sl_ord_px: Some("-1".to_string()),
1876 sl_trigger_px_type: Some(OKXTriggerType::Last),
1877 tp_trigger_px: None,
1878 tp_ord_px: None,
1879 tp_trigger_px_type: None,
1880 tgt_ccy: None,
1881 pos_side: Some(OKXPositionSide::Net),
1882 close_position: None,
1883 tag: None,
1884 reduce_only: Some(true),
1885 close_fraction: Some("1".to_string()),
1886 callback_ratio: None,
1887 callback_spread: None,
1888 active_px: None,
1889 };
1890
1891 let json = serde_json::to_string(&request).unwrap();
1892
1893 assert!(json.contains("\"ordType\":\"conditional\""));
1894 assert!(json.contains("\"closeFraction\":\"1\""));
1895 assert!(json.contains("\"slTriggerPx\":\"3000\""));
1896 assert!(json.contains("\"slOrdPx\":\"-1\""));
1897 assert!(json.contains("\"slTriggerPxType\":\"last\""));
1898 assert!(json.contains("\"reduceOnly\":true"));
1899 assert!(!json.contains("\"sz\""));
1900 assert!(!json.contains("triggerPx"));
1901 }
1902
1903 #[rstest]
1904 fn test_algo_order_request_array_serialization() {
1905 let request = OKXPlaceAlgoOrderRequest {
1906 inst_id: "BTC-USDT".to_string(),
1907 inst_id_code: Some(10459),
1908 td_mode: OKXTradeMode::Cross,
1909 side: OKXSide::Sell,
1910 ord_type: OKXAlgoOrderType::Trigger,
1911 sz: Some("0.1".to_string()),
1912 algo_cl_ord_id: None,
1913 trigger_px: Some("50000".to_string()),
1914 order_px: Some("49900".to_string()),
1915 trigger_px_type: Some(OKXTriggerType::Mark),
1916 sl_trigger_px: None,
1917 sl_ord_px: None,
1918 sl_trigger_px_type: None,
1919 tp_trigger_px: None,
1920 tp_ord_px: None,
1921 tp_trigger_px_type: None,
1922 tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1923 pos_side: Some(OKXPositionSide::Net),
1924 close_position: None,
1925 tag: None,
1926 reduce_only: Some(true),
1927 close_fraction: None,
1928 callback_ratio: None,
1929 callback_spread: None,
1930 active_px: None,
1931 };
1932
1933 let json = serde_json::to_string(&[request]).unwrap();
1935
1936 assert!(json.starts_with('['));
1938 assert!(json.ends_with(']'));
1939
1940 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1942 assert!(json.contains("\"tdMode\":\"cross\""));
1943 assert!(json.contains("\"triggerPx\":\"50000\""));
1944 assert!(json.contains("\"orderPx\":\"49900\""));
1945 assert!(json.contains("\"triggerPxType\":\"mark\""));
1946 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1947 assert!(json.contains("\"posSide\":\"net\""));
1948 assert!(json.contains("\"reduceOnly\":true"));
1949 }
1950
1951 #[rstest]
1952 fn test_cancel_algo_order_request_serialization() {
1953 let request = OKXCancelAlgoOrderRequest {
1954 inst_id: "ETH-USDT-SWAP".to_string(),
1955 inst_id_code: None,
1956 algo_id: Some("123456".to_string()),
1957 algo_cl_ord_id: None,
1958 };
1959
1960 let json = serde_json::to_string(&request).unwrap();
1961
1962 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1964 assert!(json.contains("\"algoId\":\"123456\""));
1965 assert!(!json.contains("algoClOrdId"));
1966 }
1967
1968 #[rstest]
1969 fn test_cancel_algo_order_with_client_id_serialization() {
1970 let request = OKXCancelAlgoOrderRequest {
1971 inst_id: "BTC-USDT".to_string(),
1972 inst_id_code: Some(10459),
1973 algo_id: None,
1974 algo_cl_ord_id: Some("client123".to_string()),
1975 };
1976
1977 let json = serde_json::to_string(&[request]).unwrap();
1979
1980 assert!(json.starts_with('['));
1982 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1983 assert!(json.contains("\"algoClOrdId\":\"client123\""));
1984 assert!(!json.contains("\"algoId\""));
1985 }
1986
1987 #[rstest]
1988 fn test_amend_algo_order_trigger_serialization() {
1989 let request = OKXAmendAlgoOrderRequest {
1990 inst_id: "ETH-USDT-SWAP".to_string(),
1991 algo_id: "123456".to_string(),
1992 algo_cl_ord_id: None,
1993 new_sz: None,
1994 new_trigger_px: Some("3500".to_string()),
1995 new_trigger_px_type: None,
1996 new_tp_trigger_px: None,
1997 new_tp_ord_px: None,
1998 new_tp_trigger_px_type: None,
1999 new_sl_trigger_px: None,
2000 new_sl_ord_px: None,
2001 new_sl_trigger_px_type: None,
2002 new_order_px: Some("3490".to_string()),
2003 new_callback_ratio: None,
2004 new_callback_spread: None,
2005 new_active_px: None,
2006 };
2007
2008 let json = serde_json::to_string(&request).unwrap();
2009
2010 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
2011 assert!(json.contains("\"algoId\":\"123456\""));
2012 assert!(json.contains("\"newTriggerPx\":\"3500\""));
2013 assert!(json.contains("\"newOrdPx\":\"3490\""));
2014 assert!(!json.contains("newSz"));
2015 assert!(!json.contains("algoClOrdId"));
2016 assert!(!json.contains("newCallbackRatio"));
2017 }
2018
2019 #[rstest]
2020 fn test_amend_algo_order_trailing_stop_serialization() {
2021 let request = OKXAmendAlgoOrderRequest {
2022 inst_id: "BTC-USDT-SWAP".to_string(),
2023 algo_id: "789012".to_string(),
2024 algo_cl_ord_id: Some("client456".to_string()),
2025 new_sz: Some("0.1".to_string()),
2026 new_trigger_px: None,
2027 new_trigger_px_type: None,
2028 new_tp_trigger_px: None,
2029 new_tp_ord_px: None,
2030 new_tp_trigger_px_type: None,
2031 new_sl_trigger_px: None,
2032 new_sl_ord_px: None,
2033 new_sl_trigger_px_type: None,
2034 new_order_px: None,
2035 new_callback_ratio: Some("0.02".to_string()),
2036 new_callback_spread: None,
2037 new_active_px: Some("50000".to_string()),
2038 };
2039
2040 let json = serde_json::to_string(&request).unwrap();
2041
2042 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2043 assert!(json.contains("\"algoId\":\"789012\""));
2044 assert!(json.contains("\"algoClOrdId\":\"client456\""));
2045 assert!(json.contains("\"newSz\":\"0.1\""));
2046 assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
2047 assert!(json.contains("\"newActivePx\":\"50000\""));
2048 assert!(!json.contains("newTriggerPx"));
2049 assert!(!json.contains("newOrdPx"));
2050 }
2051
2052 #[rstest]
2053 fn test_trailing_stop_request_callback_ratio_serialization() {
2054 let request = OKXPlaceAlgoOrderRequest {
2055 inst_id: "BTC-USDT-SWAP".to_string(),
2056 inst_id_code: None,
2057 td_mode: OKXTradeMode::Cross,
2058 side: OKXSide::Buy,
2059 ord_type: OKXAlgoOrderType::MoveOrderStop,
2060 sz: Some("0.1".to_string()),
2061 algo_cl_ord_id: Some("trail-001".to_string()),
2062 trigger_px: None,
2063 order_px: None,
2064 trigger_px_type: None,
2065 sl_trigger_px: None,
2066 sl_ord_px: None,
2067 sl_trigger_px_type: None,
2068 tp_trigger_px: None,
2069 tp_ord_px: None,
2070 tp_trigger_px_type: None,
2071 tgt_ccy: None,
2072 pos_side: None,
2073 close_position: None,
2074 tag: None,
2075 reduce_only: None,
2076 close_fraction: None,
2077 callback_ratio: Some("0.01".to_string()),
2078 callback_spread: None,
2079 active_px: None,
2080 };
2081
2082 let json = serde_json::to_string(&request).unwrap();
2083
2084 assert!(json.contains("\"ordType\":\"move_order_stop\""));
2085 assert!(json.contains("\"callbackRatio\":\"0.01\""));
2086 assert!(!json.contains("callbackSpread"));
2087 assert!(!json.contains("activePx"));
2088 }
2089
2090 #[rstest]
2091 fn test_trailing_stop_request_callback_spread_serialization() {
2092 let request = OKXPlaceAlgoOrderRequest {
2093 inst_id: "ETH-USDT-SWAP".to_string(),
2094 inst_id_code: None,
2095 td_mode: OKXTradeMode::Isolated,
2096 side: OKXSide::Sell,
2097 ord_type: OKXAlgoOrderType::MoveOrderStop,
2098 sz: Some("1.0".to_string()),
2099 algo_cl_ord_id: None,
2100 trigger_px: None,
2101 order_px: None,
2102 trigger_px_type: None,
2103 sl_trigger_px: None,
2104 sl_ord_px: None,
2105 sl_trigger_px_type: None,
2106 tp_trigger_px: None,
2107 tp_ord_px: None,
2108 tp_trigger_px_type: None,
2109 tgt_ccy: None,
2110 pos_side: None,
2111 close_position: None,
2112 tag: None,
2113 reduce_only: Some(true),
2114 close_fraction: None,
2115 callback_ratio: None,
2116 callback_spread: Some("50.5".to_string()),
2117 active_px: None,
2118 };
2119
2120 let json = serde_json::to_string(&request).unwrap();
2121
2122 assert!(json.contains("\"callbackSpread\":\"50.5\""));
2123 assert!(!json.contains("callbackRatio"));
2124 assert!(!json.contains("activePx"));
2125 }
2126
2127 #[rstest]
2128 fn test_trailing_stop_request_with_activation_price_serialization() {
2129 let request = OKXPlaceAlgoOrderRequest {
2130 inst_id: "BTC-USDT-SWAP".to_string(),
2131 inst_id_code: None,
2132 td_mode: OKXTradeMode::Cross,
2133 side: OKXSide::Buy,
2134 ord_type: OKXAlgoOrderType::MoveOrderStop,
2135 sz: Some("0.5".to_string()),
2136 algo_cl_ord_id: None,
2137 trigger_px: None,
2138 order_px: None,
2139 trigger_px_type: None,
2140 sl_trigger_px: None,
2141 sl_ord_px: None,
2142 sl_trigger_px_type: None,
2143 tp_trigger_px: None,
2144 tp_ord_px: None,
2145 tp_trigger_px_type: None,
2146 tgt_ccy: None,
2147 pos_side: None,
2148 close_position: None,
2149 tag: None,
2150 reduce_only: None,
2151 close_fraction: None,
2152 callback_ratio: Some("0.005".to_string()),
2153 callback_spread: None,
2154 active_px: Some("65000".to_string()),
2155 };
2156
2157 let json = serde_json::to_string(&request).unwrap();
2158
2159 assert!(json.contains("\"callbackRatio\":\"0.005\""));
2160 assert!(json.contains("\"activePx\":\"65000\""));
2161 assert!(!json.contains("callbackSpread"));
2162 }
2163
2164 #[rstest]
2165 fn test_amend_algo_order_callback_spread_serialization() {
2166 let request = OKXAmendAlgoOrderRequest {
2167 inst_id: "ETH-USDT-SWAP".to_string(),
2168 algo_id: "456789".to_string(),
2169 algo_cl_ord_id: None,
2170 new_sz: None,
2171 new_trigger_px: None,
2172 new_trigger_px_type: None,
2173 new_tp_trigger_px: None,
2174 new_tp_ord_px: None,
2175 new_tp_trigger_px_type: None,
2176 new_sl_trigger_px: None,
2177 new_sl_ord_px: None,
2178 new_sl_trigger_px_type: None,
2179 new_order_px: None,
2180 new_callback_ratio: None,
2181 new_callback_spread: Some("25.0".to_string()),
2182 new_active_px: Some("4000".to_string()),
2183 };
2184
2185 let json = serde_json::to_string(&request).unwrap();
2186
2187 assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
2188 assert!(json.contains("\"newActivePx\":\"4000\""));
2189 assert!(!json.contains("newCallbackRatio"));
2190 assert!(!json.contains("newTriggerPx"));
2191 assert!(!json.contains("newSz"));
2192 }
2193
2194 #[rstest]
2195 fn test_amend_algo_order_size_only_serialization() {
2196 let request = OKXAmendAlgoOrderRequest {
2197 inst_id: "BTC-USDT-SWAP".to_string(),
2198 algo_id: "111222".to_string(),
2199 algo_cl_ord_id: None,
2200 new_sz: Some("0.5".to_string()),
2201 new_trigger_px: None,
2202 new_trigger_px_type: None,
2203 new_tp_trigger_px: None,
2204 new_tp_ord_px: None,
2205 new_tp_trigger_px_type: None,
2206 new_sl_trigger_px: None,
2207 new_sl_ord_px: None,
2208 new_sl_trigger_px_type: None,
2209 new_order_px: None,
2210 new_callback_ratio: None,
2211 new_callback_spread: None,
2212 new_active_px: None,
2213 };
2214
2215 let json = serde_json::to_string(&request).unwrap();
2216
2217 assert!(json.contains("\"newSz\":\"0.5\""));
2218 assert!(!json.contains("newTriggerPx"));
2219 assert!(!json.contains("newOrdPx"));
2220 assert!(!json.contains("newCallbackRatio"));
2221 assert!(!json.contains("newCallbackSpread"));
2222 assert!(!json.contains("newActivePx"));
2223 }
2224
2225 #[rstest]
2226 fn test_amend_algo_order_all_fields_serialization() {
2227 let request = OKXAmendAlgoOrderRequest {
2228 inst_id: "BTC-USDT-SWAP".to_string(),
2229 algo_id: "333444".to_string(),
2230 algo_cl_ord_id: Some("client789".to_string()),
2231 new_sz: Some("1.0".to_string()),
2232 new_trigger_px: Some("60000".to_string()),
2233 new_trigger_px_type: None,
2234 new_tp_trigger_px: None,
2235 new_tp_ord_px: None,
2236 new_tp_trigger_px_type: None,
2237 new_sl_trigger_px: None,
2238 new_sl_ord_px: None,
2239 new_sl_trigger_px_type: None,
2240 new_order_px: Some("59900".to_string()),
2241 new_callback_ratio: Some("0.015".to_string()),
2242 new_callback_spread: Some("100".to_string()),
2243 new_active_px: Some("62000".to_string()),
2244 };
2245
2246 let json = serde_json::to_string(&request).unwrap();
2247
2248 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2249 assert!(json.contains("\"algoId\":\"333444\""));
2250 assert!(json.contains("\"algoClOrdId\":\"client789\""));
2251 assert!(json.contains("\"newSz\":\"1.0\""));
2252 assert!(json.contains("\"newTriggerPx\":\"60000\""));
2253 assert!(json.contains("\"newOrdPx\":\"59900\""));
2254 assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2255 assert!(json.contains("\"newCallbackSpread\":\"100\""));
2256 assert!(json.contains("\"newActivePx\":\"62000\""));
2257 }
2258
2259 #[rstest]
2260 fn test_place_order_request_serializes_px_usd() {
2261 let request = OKXPlaceOrderRequest {
2262 inst_id: "BTC-USD-250328-50000-C".to_string(),
2263 td_mode: OKXTradeMode::Cross,
2264 ccy: None,
2265 cl_ord_id: Some("test-opt-1".to_string()),
2266 tag: None,
2267 side: OKXSide::Buy,
2268 pos_side: Some(OKXPositionSide::Net),
2269 ord_type: OKXOrderType::Limit,
2270 sz: "1".to_string(),
2271 px: None,
2272 px_usd: Some("100.5".to_string()),
2273 px_vol: None,
2274 reduce_only: None,
2275 tgt_ccy: None,
2276 trade_quote_ccy: None,
2277 attach_algo_ords: None,
2278 outcome: None,
2279 slippage_pct: None,
2280 rpi_taker_access: None,
2281 rpi_px_round: None,
2282 };
2283
2284 let json = serde_json::to_string(&request).unwrap();
2285 assert!(json.contains("\"pxUsd\":\"100.5\""));
2286 assert!(!json.contains("\"pxVol\""));
2287 assert!(!json.contains("\"px\":"));
2288 assert!(!json.contains("slippagePct"));
2289 }
2290
2291 #[rstest]
2292 fn test_place_order_request_serializes_px_vol() {
2293 let request = OKXPlaceOrderRequest {
2294 inst_id: "BTC-USD-250328-50000-C".to_string(),
2295 td_mode: OKXTradeMode::Cross,
2296 ccy: None,
2297 cl_ord_id: Some("test-opt-2".to_string()),
2298 tag: None,
2299 side: OKXSide::Buy,
2300 pos_side: Some(OKXPositionSide::Net),
2301 ord_type: OKXOrderType::Limit,
2302 sz: "1".to_string(),
2303 px: None,
2304 px_usd: None,
2305 px_vol: Some("0.55".to_string()),
2306 reduce_only: None,
2307 tgt_ccy: None,
2308 trade_quote_ccy: None,
2309 attach_algo_ords: None,
2310 outcome: None,
2311 slippage_pct: None,
2312 rpi_taker_access: None,
2313 rpi_px_round: None,
2314 };
2315
2316 let json = serde_json::to_string(&request).unwrap();
2317 assert!(json.contains("\"pxVol\":\"0.55\""));
2318 assert!(!json.contains("\"pxUsd\""));
2319 assert!(!json.contains("\"px\":"));
2320 }
2321
2322 #[rstest]
2323 fn test_place_order_request_serializes_slippage_pct() {
2324 let request = OKXPlaceOrderRequest {
2325 inst_id: "BTC-USDT-SWAP".to_string(),
2326 td_mode: OKXTradeMode::Cross,
2327 ccy: None,
2328 cl_ord_id: Some("mkt-slip-1".to_string()),
2329 tag: None,
2330 side: OKXSide::Buy,
2331 pos_side: Some(OKXPositionSide::Net),
2332 ord_type: OKXOrderType::Market,
2333 sz: "1".to_string(),
2334 px: None,
2335 px_usd: None,
2336 px_vol: None,
2337 reduce_only: None,
2338 tgt_ccy: None,
2339 trade_quote_ccy: None,
2340 attach_algo_ords: None,
2341 outcome: None,
2342 slippage_pct: Some("0.005".to_string()),
2343 rpi_taker_access: None,
2344 rpi_px_round: None,
2345 };
2346
2347 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2348 assert_eq!(json["slippagePct"], "0.005");
2349 }
2350
2351 #[rstest]
2352 fn test_rpi_place_order_request_serialization() {
2353 let request = OKXPlaceOrderRequest {
2354 inst_id: "OMI-USD".to_string(),
2355 td_mode: OKXTradeMode::Cash,
2356 ccy: None,
2357 cl_ord_id: Some("ORPI001".to_string()),
2358 tag: Some("rpi-test".to_string()),
2359 side: OKXSide::Sell,
2360 pos_side: None,
2361 ord_type: OKXOrderType::Rpi,
2362 sz: "250000".to_string(),
2363 px: Some("0.0001600".to_string()),
2364 px_usd: None,
2365 px_vol: None,
2366 reduce_only: Some(false),
2367 tgt_ccy: None,
2368 trade_quote_ccy: None,
2369 attach_algo_ords: None,
2370 outcome: None,
2371 slippage_pct: None,
2372 rpi_taker_access: Some(true),
2373 rpi_px_round: Some(false),
2374 };
2375
2376 let value = serde_json::to_value(request).unwrap();
2377
2378 assert_eq!(
2379 value,
2380 serde_json::json!({
2381 "instId": "OMI-USD",
2382 "tdMode": "cash",
2383 "clOrdId": "ORPI001",
2384 "tag": "rpi-test",
2385 "side": "sell",
2386 "ordType": "rpi",
2387 "sz": "250000",
2388 "px": "0.0001600",
2389 "reduceOnly": false,
2390 "rpiTakerAccess": true,
2391 "rpiPxRound": false
2392 })
2393 );
2394 }
2395
2396 #[rstest]
2397 fn test_rpi_amend_order_request_serialization() {
2398 let request = OKXAmendOrderRequest {
2399 inst_id: "OMI-USD".to_string(),
2400 ord_id: Some("2500000000000000001".to_string()),
2401 cl_ord_id: None,
2402 req_id: Some("RPI-AMEND-1".to_string()),
2403 new_sz: Some("275000".to_string()),
2404 new_px: Some("0.0001599".to_string()),
2405 rpi_taker_access: Some(false),
2406 rpi_px_round: Some(true),
2407 };
2408
2409 let value = serde_json::to_value(request).unwrap();
2410
2411 assert_eq!(
2412 value,
2413 serde_json::json!({
2414 "instId": "OMI-USD",
2415 "ordId": "2500000000000000001",
2416 "reqId": "RPI-AMEND-1",
2417 "newSz": "275000",
2418 "newPx": "0.0001599",
2419 "rpiTakerAccess": false,
2420 "rpiPxRound": true
2421 })
2422 );
2423 }
2424
2425 #[rstest]
2426 fn test_rpi_place_order_response_parsing() {
2427 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2428 "ordId": "2500000000000000001",
2429 "clOrdId": "ORPI001",
2430 "tag": "rpi-test",
2431 "instId": "OMI-USD",
2432 "side": "sell",
2433 "ordType": "rpi",
2434 "sz": "250000",
2435 "state": "live",
2436 "px": "0.0001600",
2437 "avgPx": "",
2438 "sCode": "0",
2439 "sMsg": ""
2440 }))
2441 .unwrap();
2442
2443 assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
2444 assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
2445 assert_eq!(response.tag.as_deref(), Some("rpi-test"));
2446 assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
2447 assert_eq!(response.side, Some(OKXSide::Sell));
2448 assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
2449 assert_eq!(response.sz.as_deref(), Some("250000"));
2450 assert_eq!(response.state, Some(OKXOrderStatus::Live));
2451 assert_eq!(response.px.as_deref(), Some("0.0001600"));
2452 assert_eq!(response.avg_px.as_deref(), Some(""));
2453 assert_eq!(response.s_code.as_deref(), Some("0"));
2454 assert_eq!(response.s_msg.as_deref(), Some(""));
2455 }
2456
2457 #[rstest]
2458 fn test_event_contract_models_deserialize() {
2459 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2460 "seriesId": "BTC-ABOVE-DAILY",
2461 "freq": "daily",
2462 "title": "BTC above daily",
2463 "category": "1",
2464 "settlement": {
2465 "method": "cash",
2466 "closeEarly": false,
2467 "srcName": "OKX BTC/USD Index",
2468 "underlying": "BTC-USD"
2469 }
2470 }))
2471 .unwrap();
2472 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2473 "seriesId": "BTC-ABOVE-DAILY",
2474 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2475 "fixTime": "",
2476 "expTime": "1769697132335",
2477 "state": "live"
2478 }))
2479 .unwrap();
2480 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2481 "seriesId": "BTC-ABOVE-DAILY",
2482 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2483 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2484 "listTime": "1769697132335",
2485 "fixTime": "",
2486 "expTime": "1769697132335",
2487 "state": "live",
2488 "disputed": false,
2489 "outcome": "0",
2490 "floorStrike": "120000",
2491 "capStrike": "INF",
2492 "settleValue": "",
2493 "hitDir": ""
2494 }))
2495 .unwrap();
2496
2497 assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2498 assert_eq!(series.settlement.underlying, "BTC-USD");
2499 assert_eq!(event.fix_time, None);
2500 assert_eq!(event.exp_time, Some(1_769_697_132_335));
2501 assert_eq!(
2502 market.inst_id,
2503 Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2504 );
2505 assert_eq!(market.list_time, Some(1_769_697_132_335));
2506 assert_eq!(market.exp_time, Some(1_769_697_132_335));
2507 assert_eq!(market.outcome, "0");
2508 assert_eq!(market.cap_strike, "INF");
2509 assert_eq!(market.hit_dir, "");
2510
2511 let serialized = serde_json::to_value(&market).unwrap();
2512 assert_eq!(serialized["capStrike"], "INF");
2513 assert_eq!(serialized["hitDir"], "");
2514 }
2515
2516 #[rstest]
2517 fn test_event_contract_models_accept_missing_optional_fields() {
2518 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2519 "seriesId": "BTC-ABOVE-DAILY"
2520 }))
2521 .unwrap();
2522 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2523 "seriesId": "BTC-ABOVE-DAILY",
2524 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2525 "fixTime": "",
2526 "expTime": "",
2527 "state": "live"
2528 }))
2529 .unwrap();
2530 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2531 "seriesId": "BTC-ABOVE-DAILY",
2532 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2533 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2534 "listTime": "",
2535 "fixTime": "",
2536 "expTime": "",
2537 "state": "live",
2538 "disputed": false,
2539 "outcome": "0",
2540 "floorStrike": "120000",
2541 "settleValue": ""
2542 }))
2543 .unwrap();
2544
2545 assert_eq!(series.freq, "");
2546 assert_eq!(series.settlement.underlying, "");
2547 assert_eq!(event.exp_time, None);
2548 assert_eq!(market.list_time, None);
2549 assert_eq!(market.exp_time, None);
2550 assert_eq!(market.cap_strike, "");
2551 assert_eq!(market.hit_dir, "");
2552 }
2553
2554 #[rstest]
2555 fn test_place_order_request_serializes_event_contract_fields() {
2556 let request = OKXPlaceOrderRequest {
2557 inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2558 td_mode: OKXTradeMode::Cash,
2559 ccy: None,
2560 cl_ord_id: Some("event-1".to_string()),
2561 tag: None,
2562 side: OKXSide::Buy,
2563 pos_side: None,
2564 ord_type: OKXOrderType::Limit,
2565 sz: "10".to_string(),
2566 px: Some("0.42".to_string()),
2567 px_usd: None,
2568 px_vol: None,
2569 reduce_only: None,
2570 tgt_ccy: None,
2571 trade_quote_ccy: None,
2572 attach_algo_ords: None,
2573 outcome: Some("yes".to_string()),
2574 slippage_pct: None,
2575 rpi_taker_access: None,
2576 rpi_px_round: None,
2577 };
2578
2579 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2580
2581 assert!(json.get("speedBump").is_none());
2582 assert_eq!(json["outcome"], "yes");
2583 assert!(json.get("tradeQuoteCcy").is_none());
2584 }
2585
2586 #[rstest]
2587 fn test_place_order_request_serializes_trade_quote_ccy_usd() {
2588 let request = OKXPlaceOrderRequest {
2589 inst_id: "BTC-USDC".to_string(),
2590 td_mode: OKXTradeMode::Cash,
2591 ccy: None,
2592 cl_ord_id: Some("usd-quote-1".to_string()),
2593 tag: None,
2594 side: OKXSide::Buy,
2595 pos_side: None,
2596 ord_type: OKXOrderType::Limit,
2597 sz: "0.01".to_string(),
2598 px: Some("100000".to_string()),
2599 px_usd: None,
2600 px_vol: None,
2601 reduce_only: None,
2602 tgt_ccy: None,
2603 trade_quote_ccy: Some(Ustr::from("USD")),
2604 attach_algo_ords: None,
2605 outcome: None,
2606 slippage_pct: None,
2607 rpi_taker_access: None,
2608 rpi_px_round: None,
2609 };
2610
2611 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2612 assert_eq!(json["instId"], "BTC-USDC");
2613 assert_eq!(json["tradeQuoteCcy"], "USD");
2614 }
2615
2616 #[rstest]
2617 fn test_place_order_request_omits_trade_quote_ccy_when_unset() {
2618 let request = OKXPlaceOrderRequest {
2619 inst_id: "BTC-USDC".to_string(),
2620 td_mode: OKXTradeMode::Cash,
2621 ccy: None,
2622 cl_ord_id: Some("usdc-default-1".to_string()),
2623 tag: None,
2624 side: OKXSide::Buy,
2625 pos_side: None,
2626 ord_type: OKXOrderType::Limit,
2627 sz: "0.01".to_string(),
2628 px: Some("100000".to_string()),
2629 px_usd: None,
2630 px_vol: None,
2631 reduce_only: None,
2632 tgt_ccy: None,
2633 trade_quote_ccy: None,
2634 attach_algo_ords: None,
2635 outcome: None,
2636 slippage_pct: None,
2637 rpi_taker_access: None,
2638 rpi_px_round: None,
2639 };
2640
2641 let json = serde_json::to_string(&request).unwrap();
2642 assert!(!json.contains("tradeQuoteCcy"));
2643 }
2644
2645 #[rstest]
2646 fn test_attach_algo_ord_request_serializes_trailing_fields() {
2647 let request = OKXAttachAlgoOrdRequest {
2648 attach_algo_cl_ord_id: Some("trail-1".to_string()),
2649 sl_trigger_px: None,
2650 sl_ord_px: None,
2651 sl_trigger_px_type: None,
2652 tp_trigger_px: None,
2653 tp_ord_px: None,
2654 tp_trigger_px_type: None,
2655 callback_ratio: Some("0.01".to_string()),
2656 callback_spread: None,
2657 active_px: Some("64000".to_string()),
2658 new_callback_ratio: Some("0.02".to_string()),
2659 new_callback_spread: Some("25".to_string()),
2660 new_active_px: Some("65000".to_string()),
2661 };
2662
2663 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2664
2665 assert_eq!(json["callbackRatio"], "0.01");
2666 assert_eq!(json["activePx"], "64000");
2667 assert_eq!(json["newCallbackRatio"], "0.02");
2668 assert_eq!(json["newCallbackSpread"], "25");
2669 assert_eq!(json["newActivePx"], "65000");
2670 assert!(json.get("callbackSpread").is_none());
2671 }
2672
2673 #[rstest]
2674 fn test_place_order_response_deserializes_sub_code() {
2675 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2676 "ordId": "",
2677 "clOrdId": "event-1",
2678 "sCode": "51000",
2679 "sMsg": "Parameter error",
2680 "subCode": "51004"
2681 }))
2682 .unwrap();
2683
2684 assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2685 assert_eq!(response.s_code, Some("51000".to_string()));
2686 assert_eq!(response.sub_code, Some("51004".to_string()));
2687 }
2688
2689 #[rstest]
2690 fn test_fee_rate_deserializes_settle() {
2691 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2692 "level": "VIP1",
2693 "taker": "-0.0005",
2694 "maker": "-0.0002",
2695 "takerU": "-0.0005",
2696 "makerU": "-0.0002",
2697 "settle": "-0.001",
2698 "instType": "EVENTS",
2699 "category": "1",
2700 "ts": "1769697132335"
2701 }))
2702 .unwrap();
2703
2704 assert_eq!(fee_rate.settle, "-0.001");
2705 assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2706 }
2707
2708 #[rstest]
2709 #[case::missing(None, None)]
2710 #[case::empty(Some(""), None)]
2711 #[case::zero(Some("0"), Some(Decimal::ZERO))]
2712 #[case::commission(Some("-0.001"), Some(Decimal::from_str_exact("-0.001").unwrap()))]
2713 #[case::rebate(Some("0.0002"), Some(Decimal::from_str_exact("0.0002").unwrap()))]
2714 fn test_fee_group_rates_preserve_unavailable_zero_and_signs(
2715 #[case] value: Option<&str>,
2716 #[case] expected: Option<Decimal>,
2717 ) {
2718 let mut payload = serde_json::json!({"groupId": "1"});
2719
2720 if let Some(value) = value {
2721 for field in ["maker", "taker", "rpiMaker", "elpMaker"] {
2722 payload[field] = serde_json::json!(value);
2723 }
2724 }
2725
2726 let group: OKXFeeGroup = serde_json::from_value(payload).unwrap();
2727
2728 assert_eq!(group.group_id, "1");
2729 assert_eq!(group.maker, expected);
2730 assert_eq!(group.taker, expected);
2731 assert_eq!(group.rpi_maker, expected);
2732 assert_eq!(group.elp_maker, expected);
2733 }
2734
2735 #[rstest]
2736 #[case::rpi_only(Some("-0.00015"), None)]
2737 #[case::elp_only(None, Some("-0.00016"))]
2738 #[case::both_equal(Some("-0.00015"), Some("-0.00015"))]
2739 #[case::both_distinct(Some("-0.00015"), Some("-0.00016"))]
2740 fn test_fee_group_preserves_rpi_and_elp_independently(
2741 #[case] rpi: Option<&str>,
2742 #[case] elp: Option<&str>,
2743 ) {
2744 let mut payload = serde_json::json!({"groupId": "1"});
2745 if let Some(value) = rpi {
2746 payload["rpiMaker"] = serde_json::json!(value);
2747 }
2748
2749 if let Some(value) = elp {
2750 payload["elpMaker"] = serde_json::json!(value);
2751 }
2752
2753 let group: OKXFeeGroup = serde_json::from_value(payload).unwrap();
2754
2755 assert_eq!(
2756 group.rpi_maker,
2757 rpi.map(|value| Decimal::from_str_exact(value).unwrap())
2758 );
2759 assert_eq!(
2760 group.elp_maker,
2761 elp.map(|value| Decimal::from_str_exact(value).unwrap())
2762 );
2763 }
2764
2765 #[rstest]
2766 fn test_fee_rate_preserves_legacy_scalars_alongside_groups() {
2767 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2768 "level": "Lv1",
2769 "maker": "-0.0008",
2770 "taker": "-0.001",
2771 "makerU": "",
2772 "takerU": "0",
2773 "feeGroup": [{"groupId": "1", "maker": "0.0002", "taker": "-0.0005"}],
2774 "instType": "SPOT",
2775 "ts": "1763979985847"
2776 }))
2777 .unwrap();
2778
2779 assert_eq!(fee_rate.maker, "-0.0008");
2780 assert_eq!(fee_rate.taker, "-0.001");
2781 assert_eq!(fee_rate.maker_u, "");
2782 assert_eq!(fee_rate.taker_u, "0");
2783 assert_eq!(fee_rate.fee_group.len(), 1);
2784 assert_eq!(
2785 fee_rate.fee_group[0].maker,
2786 Some(Decimal::from_str_exact("0.0002").unwrap())
2787 );
2788 assert_eq!(
2789 fee_rate.fee_group[0].taker,
2790 Some(Decimal::from_str_exact("-0.0005").unwrap())
2791 );
2792 }
2793
2794 #[rstest]
2795 fn test_fee_rate_missing_scalars_do_not_imply_zero() {
2796 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2797 "level": "Lv1",
2798 "instType": "SPOT",
2799 "ts": "1763979985847"
2800 }))
2801 .unwrap();
2802
2803 assert!(fee_rate.maker.is_empty());
2804 assert!(fee_rate.taker.is_empty());
2805 assert!(fee_rate.maker_u.is_empty());
2806 assert!(fee_rate.taker_u.is_empty());
2807 assert!(fee_rate.fee_group.is_empty());
2808 }
2809
2810 #[rstest]
2811 fn test_fee_group_rejects_invalid_rates(
2812 #[values("maker", "taker", "rpiMaker", "elpMaker")] field: &str,
2813 ) {
2814 let mut payload = serde_json::json!({"groupId": "1"});
2815 payload[field] = serde_json::json!("invalid");
2816
2817 assert!(serde_json::from_value::<OKXFeeGroup>(payload).is_err());
2818 }
2819
2820 #[rstest]
2821 fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
2822 let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
2823 "level": "VIP1",
2824 "taker": "-0.0005",
2825 "maker": "-0.0002",
2826 "takerU": "-0.0005",
2827 "makerU": "-0.0002",
2828 "rpiMaker": "-0.00015",
2829 "instType": "SPOT",
2830 "category": "1",
2831 "ts": "1785406500000"
2832 }))
2833 .unwrap();
2834 let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
2835 "level": "VIP1",
2836 "taker": "-0.0005",
2837 "maker": "-0.0002",
2838 "takerU": "-0.0005",
2839 "makerU": "-0.0002",
2840 "elpMaker": "-0.00016",
2841 "instType": "SPOT",
2842 "category": "1",
2843 "ts": "1785406500001"
2844 }))
2845 .unwrap();
2846 let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
2847 "level": "VIP1",
2848 "taker": "-0.0005",
2849 "maker": "-0.0002",
2850 "takerU": "-0.0005",
2851 "makerU": "-0.0002",
2852 "rpiMaker": "",
2853 "instType": "OPTION",
2854 "category": "1",
2855 "ts": "1785406500002"
2856 }))
2857 .unwrap();
2858
2859 assert_eq!(
2860 rpi.rpi_maker,
2861 Some(Decimal::from_str_exact("-0.00015").unwrap())
2862 );
2863 assert_eq!(
2864 legacy.rpi_maker,
2865 Some(Decimal::from_str_exact("-0.00016").unwrap())
2866 );
2867 assert_eq!(not_applicable.rpi_maker, None);
2868 }
2869}