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}
794
795#[derive(Clone, Debug, Serialize, Deserialize)]
798#[serde(rename_all = "camelCase")]
799pub struct OKXPlaceOrderResponse {
800 #[serde(default)]
802 pub ord_id: Option<Ustr>,
803 #[serde(default)]
805 pub cl_ord_id: Option<Ustr>,
806 #[serde(default)]
808 pub tag: Option<String>,
809 #[serde(default)]
811 pub inst_id: Option<Ustr>,
812 #[serde(default)]
814 pub side: Option<OKXSide>,
815 #[serde(default)]
817 pub ord_type: Option<OKXOrderType>,
818 #[serde(default)]
820 pub sz: Option<String>,
821 pub state: Option<OKXOrderStatus>,
823 #[serde(default)]
825 pub px: Option<String>,
826 #[serde(default)]
828 pub avg_px: Option<String>,
829 #[serde(default)]
831 pub acc_fill_sz: Option<String>,
832 #[serde(default)]
834 pub fill_sz: Option<String>,
835 #[serde(default)]
837 pub fill_px: Option<String>,
838 #[serde(default)]
840 pub trade_id: Option<Ustr>,
841 #[serde(default)]
843 pub fill_time: Option<String>,
844 #[serde(default)]
846 pub fee: Option<String>,
847 #[serde(default)]
849 pub fee_ccy: Option<String>,
850 #[serde(default)]
852 pub req_id: Option<Ustr>,
853 #[serde(default)]
855 pub pos_side: Option<OKXPositionSide>,
856 #[serde(default)]
858 pub reduce_only: Option<String>,
859 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
861 pub tgt_ccy: Option<OKXTargetCurrency>,
862 #[serde(default)]
864 pub c_time: Option<String>,
865 #[serde(default)]
867 pub u_time: Option<String>,
868 #[serde(skip_serializing_if = "Option::is_none")]
870 pub s_code: Option<String>,
871 #[serde(skip_serializing_if = "Option::is_none")]
873 pub s_msg: Option<String>,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub sub_code: Option<String>,
877}
878
879#[derive(Clone, Debug, Default, Serialize, Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct OKXAttachAlgoOrdRequest {
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub attach_algo_cl_ord_id: Option<String>,
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub sl_trigger_px: Option<String>,
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub sl_ord_px: Option<String>,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub sl_trigger_px_type: Option<OKXTriggerType>,
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub tp_trigger_px: Option<String>,
898 #[serde(skip_serializing_if = "Option::is_none")]
900 pub tp_ord_px: Option<String>,
901 #[serde(skip_serializing_if = "Option::is_none")]
903 pub tp_trigger_px_type: Option<OKXTriggerType>,
904 #[serde(skip_serializing_if = "Option::is_none")]
906 pub callback_ratio: Option<String>,
907 #[serde(skip_serializing_if = "Option::is_none")]
909 pub callback_spread: Option<String>,
910 #[serde(skip_serializing_if = "Option::is_none")]
912 pub active_px: Option<String>,
913 #[serde(skip_serializing_if = "Option::is_none")]
915 pub new_callback_ratio: Option<String>,
916 #[serde(skip_serializing_if = "Option::is_none")]
918 pub new_callback_spread: Option<String>,
919 #[serde(skip_serializing_if = "Option::is_none")]
921 pub new_active_px: Option<String>,
922}
923
924#[derive(Clone, Debug, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct OKXPlaceOrderRequest {
928 pub inst_id: String,
930 pub td_mode: OKXTradeMode,
932 #[serde(skip_serializing_if = "Option::is_none")]
934 pub ccy: Option<String>,
935 #[serde(skip_serializing_if = "Option::is_none")]
937 pub cl_ord_id: Option<String>,
938 #[serde(skip_serializing_if = "Option::is_none")]
940 pub tag: Option<String>,
941 pub side: OKXSide,
943 #[serde(skip_serializing_if = "Option::is_none")]
945 pub pos_side: Option<OKXPositionSide>,
946 pub ord_type: OKXOrderType,
948 pub sz: String,
950 #[serde(skip_serializing_if = "Option::is_none")]
952 pub px: Option<String>,
953 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
955 pub px_usd: Option<String>,
956 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
959 pub px_vol: Option<String>,
960 #[serde(skip_serializing_if = "Option::is_none")]
962 pub reduce_only: Option<bool>,
963 #[serde(skip_serializing_if = "Option::is_none")]
965 pub tgt_ccy: Option<OKXTargetCurrency>,
966 #[serde(skip_serializing_if = "Option::is_none")]
968 pub trade_quote_ccy: Option<Ustr>,
969 #[serde(skip_serializing_if = "Option::is_none")]
971 pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
972 #[serde(skip_serializing_if = "Option::is_none")]
974 pub outcome: Option<String>,
975 #[serde(skip_serializing_if = "Option::is_none")]
980 pub slippage_pct: Option<String>,
981 #[serde(skip_serializing_if = "Option::is_none")]
983 pub rpi_taker_access: Option<bool>,
984 #[serde(skip_serializing_if = "Option::is_none")]
986 pub rpi_px_round: Option<bool>,
987}
988
989#[derive(Clone, Debug, Default, Serialize, Deserialize)]
991#[serde(rename_all = "camelCase")]
992pub struct OKXAmendOrderRequest {
993 pub inst_id: String,
995 #[serde(skip_serializing_if = "Option::is_none")]
997 pub ord_id: Option<String>,
998 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub cl_ord_id: Option<String>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1003 pub req_id: Option<String>,
1004 #[serde(skip_serializing_if = "Option::is_none")]
1006 pub new_sz: Option<String>,
1007 #[serde(skip_serializing_if = "Option::is_none")]
1009 pub new_px: Option<String>,
1010 #[serde(skip_serializing_if = "Option::is_none")]
1012 pub rpi_taker_access: Option<bool>,
1013 #[serde(skip_serializing_if = "Option::is_none")]
1015 pub rpi_px_round: Option<bool>,
1016}
1017
1018#[derive(Clone, Debug, Serialize, Deserialize)]
1020#[serde(rename_all = "camelCase")]
1021pub struct OKXCancelOrderRequest {
1022 pub inst_id: String,
1024 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1026 pub inst_id_code: Option<u64>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub ord_id: Option<String>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub cl_ord_id: Option<String>,
1033}
1034
1035#[derive(Clone, Debug, Serialize, Deserialize)]
1037#[serde(rename_all = "camelCase")]
1038pub struct OKXCancelOrderResponse {
1039 pub ord_id: String,
1041 #[serde(default)]
1043 pub cl_ord_id: Option<String>,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1046 pub s_code: Option<String>,
1047 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub s_msg: Option<String>,
1050 #[serde(default)]
1052 pub ts: Option<String>,
1053}
1054
1055pub use crate::common::models::OKXAttachedAlgoOrd;
1056
1057#[derive(Clone, Debug, Serialize, Deserialize)]
1059#[serde(rename_all = "camelCase")]
1060pub struct OKXOrderHistory {
1061 pub ord_id: Ustr,
1063 pub cl_ord_id: Ustr,
1065 #[serde(default)]
1067 pub algo_id: Option<Ustr>,
1068 #[serde(default)]
1070 pub algo_cl_ord_id: Option<Ustr>,
1071 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1073 pub attach_algo_cl_ord_id: Option<String>,
1074 #[serde(default)]
1076 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1077 #[serde(default)]
1079 pub cl_act_id: Option<Ustr>,
1080 pub tag: String,
1082 pub inst_type: OKXInstrumentType,
1084 pub uly: Option<Ustr>,
1086 pub inst_id: Ustr,
1088 pub ord_type: OKXOrderType,
1090 pub sz: String,
1092 pub px: String,
1094 #[serde(default)]
1096 pub px_usd: String,
1097 #[serde(default)]
1099 pub px_vol: String,
1100 pub side: OKXSide,
1102 pub pos_side: OKXPositionSide,
1104 pub td_mode: OKXTradeMode,
1106 pub reduce_only: String,
1108 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1110 pub tgt_ccy: Option<OKXTargetCurrency>,
1111 pub state: OKXOrderStatus,
1113 pub avg_px: String,
1115 pub fee: String,
1117 pub fee_ccy: String,
1119 pub fill_sz: String,
1121 pub fill_px: String,
1123 pub trade_id: Ustr,
1125 #[serde(deserialize_with = "deserialize_string_to_u64")]
1127 pub fill_time: u64,
1128 pub acc_fill_sz: String,
1130 #[serde(default)]
1132 pub fill_fee: Option<String>,
1133 #[serde(default)]
1135 pub req_id: Option<Ustr>,
1136 #[serde(default)]
1138 pub cancel_fill_sz: Option<String>,
1139 #[serde(default)]
1141 pub cancel_total_sz: Option<String>,
1142 #[serde(default)]
1144 pub cancel_source: String,
1145 #[serde(default)]
1147 pub cancel_source_reason: String,
1148 #[serde(default)]
1150 pub fee_discount: Option<String>,
1151 pub category: OKXOrderCategory,
1153 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1155 pub outcome: Option<String>,
1156 #[serde(deserialize_with = "deserialize_string_to_u64")]
1158 pub u_time: u64,
1159 #[serde(deserialize_with = "deserialize_string_to_u64")]
1161 pub c_time: u64,
1162}
1163
1164#[derive(Clone, Debug, Serialize, Deserialize)]
1166#[serde(rename_all = "camelCase")]
1167pub struct OKXOrderAlgo {
1168 pub algo_id: String,
1170 #[serde(default)]
1172 pub algo_cl_ord_id: String,
1173 #[serde(default)]
1175 pub cl_ord_id: String,
1176 #[serde(default)]
1178 pub ord_id: String,
1179 #[serde(default)]
1181 pub ord_id_list: Vec<String>,
1182 #[serde(default)]
1184 pub sub_algo_id_list: Vec<String>,
1185 pub inst_id: Ustr,
1187 pub inst_type: OKXInstrumentType,
1189 pub ord_type: OKXAlgoOrderType,
1191 pub state: OKXAlgoOrderStatus,
1193 pub side: OKXSide,
1195 pub pos_side: OKXPositionSide,
1197 #[serde(default)]
1199 pub sz: String,
1200 #[serde(default)]
1202 pub trigger_px: String,
1203 #[serde(default)]
1205 pub trigger_px_type: Option<OKXTriggerType>,
1206 #[serde(default)]
1208 pub sl_trigger_px: String,
1209 #[serde(default)]
1211 pub sl_ord_px: String,
1212 #[serde(default)]
1214 pub sl_trigger_px_type: Option<OKXTriggerType>,
1215 #[serde(default)]
1217 pub tp_trigger_px: String,
1218 #[serde(default)]
1220 pub tp_ord_px: String,
1221 #[serde(default)]
1223 pub tp_trigger_px_type: Option<OKXTriggerType>,
1224 #[serde(default)]
1226 pub ord_px: String,
1227 pub td_mode: OKXTradeMode,
1229 #[serde(default)]
1231 pub lever: String,
1232 #[serde(default)]
1234 pub reduce_only: String,
1235 #[serde(default)]
1237 pub close_fraction: String,
1238 #[serde(default)]
1240 pub actual_px: String,
1241 #[serde(default)]
1243 pub actual_sz: String,
1244 #[serde(default)]
1246 pub notional_usd: String,
1247 #[serde(deserialize_with = "deserialize_string_to_u64")]
1249 pub c_time: u64,
1250 #[serde(deserialize_with = "deserialize_string_to_u64")]
1252 pub u_time: u64,
1253 #[serde(default)]
1255 pub trigger_time: String,
1256 #[serde(default)]
1258 pub tag: String,
1259 #[serde(default)]
1261 pub callback_ratio: String,
1262 #[serde(default)]
1264 pub callback_spread: String,
1265 #[serde(default)]
1267 pub active_px: String,
1268}
1269
1270#[derive(Clone, Debug, Serialize, Deserialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct OKXTransactionDetail {
1274 pub inst_type: OKXInstrumentType,
1276 pub inst_id: Ustr,
1278 pub trade_id: Ustr,
1280 pub ord_id: Ustr,
1282 pub cl_ord_id: Ustr,
1284 pub bill_id: Ustr,
1286 pub fill_px: String,
1288 pub fill_sz: String,
1290 pub side: OKXSide,
1292 pub exec_type: OKXExecType,
1294 pub fee_ccy: String,
1296 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1298 pub fee: Option<String>,
1299 #[serde(deserialize_with = "deserialize_string_to_u64")]
1301 pub ts: u64,
1302}
1303
1304#[derive(Clone, Debug, Serialize, Deserialize)]
1306#[serde(rename_all = "camelCase")]
1307pub struct OKXPositionHistory {
1308 pub inst_type: OKXInstrumentType,
1310 pub inst_id: Ustr,
1312 pub mgn_mode: OKXMarginMode,
1314 #[serde(rename = "type")]
1317 pub r#type: Ustr,
1318 pub c_time: String,
1320 #[serde(deserialize_with = "deserialize_string_to_u64")]
1322 pub u_time: u64,
1323 pub open_avg_px: String,
1325 #[serde(skip_serializing_if = "Option::is_none")]
1327 pub close_avg_px: Option<String>,
1328 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1330 pub pos_id: Option<Ustr>,
1331 #[serde(skip_serializing_if = "Option::is_none")]
1333 pub open_max_pos: Option<String>,
1334 #[serde(skip_serializing_if = "Option::is_none")]
1336 pub close_total_pos: Option<String>,
1337 #[serde(skip_serializing_if = "Option::is_none")]
1339 pub realized_pnl: Option<String>,
1340 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub fee: Option<String>,
1343 #[serde(skip_serializing_if = "Option::is_none")]
1345 pub funding_fee: Option<String>,
1346 #[serde(skip_serializing_if = "Option::is_none")]
1348 pub liq_penalty: Option<String>,
1349 #[serde(skip_serializing_if = "Option::is_none")]
1351 pub pnl: Option<String>,
1352 #[serde(skip_serializing_if = "Option::is_none")]
1354 pub pnl_ratio: Option<String>,
1355 pub pos_side: OKXPositionSide,
1357 pub lever: String,
1359 #[serde(skip_serializing_if = "Option::is_none")]
1361 pub direction: Option<String>,
1362 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub trigger_px: Option<String>,
1365 #[serde(skip_serializing_if = "Option::is_none")]
1367 pub uly: Option<String>,
1368 #[serde(skip_serializing_if = "Option::is_none")]
1370 pub ccy: Option<String>,
1371}
1372
1373#[derive(Clone, Debug, Serialize, Deserialize)]
1375#[serde(rename_all = "camelCase")]
1376pub struct OKXPlaceAlgoOrderRequest {
1377 #[serde(rename = "instId")]
1379 pub inst_id: String,
1380 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1382 pub inst_id_code: Option<u64>,
1383 #[serde(rename = "tdMode")]
1385 pub td_mode: OKXTradeMode,
1386 pub side: OKXSide,
1388 #[serde(rename = "ordType")]
1390 pub ord_type: OKXAlgoOrderType,
1391 #[serde(skip_serializing_if = "Option::is_none")]
1393 pub sz: Option<String>,
1394 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1396 pub algo_cl_ord_id: Option<String>,
1397 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1399 pub trigger_px: Option<String>,
1400 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1402 pub order_px: Option<String>,
1403 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1405 pub trigger_px_type: Option<OKXTriggerType>,
1406 #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1408 pub sl_trigger_px: Option<String>,
1409 #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1411 pub sl_ord_px: Option<String>,
1412 #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1414 pub sl_trigger_px_type: Option<OKXTriggerType>,
1415 #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1417 pub tp_trigger_px: Option<String>,
1418 #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1420 pub tp_ord_px: Option<String>,
1421 #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1423 pub tp_trigger_px_type: Option<OKXTriggerType>,
1424 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1426 pub tgt_ccy: Option<OKXTargetCurrency>,
1427 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1429 pub pos_side: Option<OKXPositionSide>,
1430 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1432 pub close_position: Option<bool>,
1433 #[serde(skip_serializing_if = "Option::is_none")]
1435 pub tag: Option<String>,
1436 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1438 pub reduce_only: Option<bool>,
1439 #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1441 pub close_fraction: Option<String>,
1442 #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1445 pub callback_ratio: Option<String>,
1446 #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1449 pub callback_spread: Option<String>,
1450 #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1453 pub active_px: Option<String>,
1454}
1455
1456#[derive(Clone, Debug, Serialize, Deserialize)]
1458#[serde(rename_all = "camelCase")]
1459pub struct OKXPlaceAlgoOrderResponse {
1460 pub algo_id: String,
1462 #[serde(skip_serializing_if = "Option::is_none")]
1464 pub algo_cl_ord_id: Option<String>,
1465 #[serde(skip_serializing_if = "Option::is_none")]
1467 pub s_code: Option<String>,
1468 #[serde(skip_serializing_if = "Option::is_none")]
1470 pub s_msg: Option<String>,
1471 #[serde(skip_serializing_if = "Option::is_none")]
1473 pub req_id: Option<String>,
1474}
1475
1476#[derive(Clone, Debug, Serialize, Deserialize)]
1478#[serde(rename_all = "camelCase")]
1479pub struct OKXCancelAlgoOrderRequest {
1480 pub inst_id: String,
1482 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1484 pub inst_id_code: Option<u64>,
1485 #[serde(skip_serializing_if = "Option::is_none")]
1487 pub algo_id: Option<String>,
1488 #[serde(skip_serializing_if = "Option::is_none")]
1490 pub algo_cl_ord_id: Option<String>,
1491}
1492
1493#[derive(Clone, Debug, Serialize, Deserialize)]
1495#[serde(rename_all = "camelCase")]
1496pub struct OKXCancelAlgoOrderResponse {
1497 pub algo_id: String,
1499 #[serde(skip_serializing_if = "Option::is_none")]
1501 pub s_code: Option<String>,
1502 #[serde(skip_serializing_if = "Option::is_none")]
1504 pub s_msg: Option<String>,
1505}
1506
1507#[derive(Clone, Debug, Serialize, Deserialize)]
1509#[serde(rename_all = "camelCase")]
1510pub struct OKXAmendAlgoOrderRequest {
1511 pub inst_id: String,
1513 pub algo_id: String,
1515 #[serde(skip_serializing_if = "Option::is_none")]
1517 pub algo_cl_ord_id: Option<String>,
1518 #[serde(skip_serializing_if = "Option::is_none")]
1520 pub new_sz: Option<String>,
1521 #[serde(skip_serializing_if = "Option::is_none")]
1523 pub new_trigger_px: Option<String>,
1524 #[serde(skip_serializing_if = "Option::is_none")]
1526 pub new_tp_trigger_px: Option<String>,
1527 #[serde(skip_serializing_if = "Option::is_none")]
1529 pub new_tp_ord_px: Option<String>,
1530 #[serde(skip_serializing_if = "Option::is_none")]
1532 pub new_tp_trigger_px_type: Option<String>,
1533 #[serde(skip_serializing_if = "Option::is_none")]
1537 pub new_sl_trigger_px: Option<String>,
1538 #[serde(skip_serializing_if = "Option::is_none")]
1540 pub new_sl_ord_px: Option<String>,
1541 #[serde(skip_serializing_if = "Option::is_none")]
1543 pub new_sl_trigger_px_type: Option<String>,
1544 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub new_order_px: Option<String>,
1547 #[serde(skip_serializing_if = "Option::is_none")]
1549 pub new_callback_ratio: Option<String>,
1550 #[serde(skip_serializing_if = "Option::is_none")]
1552 pub new_callback_spread: Option<String>,
1553 #[serde(skip_serializing_if = "Option::is_none")]
1555 pub new_active_px: Option<String>,
1556}
1557
1558#[derive(Clone, Debug, Serialize, Deserialize)]
1560#[serde(rename_all = "camelCase")]
1561pub struct OKXAmendAlgoOrderResponse {
1562 pub algo_id: String,
1564 #[serde(skip_serializing_if = "Option::is_none")]
1566 pub algo_cl_ord_id: Option<String>,
1567 #[serde(skip_serializing_if = "Option::is_none")]
1569 pub s_code: Option<String>,
1570 #[serde(skip_serializing_if = "Option::is_none")]
1572 pub s_msg: Option<String>,
1573 #[serde(skip_serializing_if = "Option::is_none")]
1575 pub req_id: Option<String>,
1576}
1577
1578#[derive(Clone, Debug, Serialize, Deserialize)]
1580#[serde(rename_all = "camelCase")]
1581pub struct OKXServerTime {
1582 #[serde(deserialize_with = "deserialize_string_to_u64")]
1584 pub ts: u64,
1585}
1586
1587#[derive(Clone, Debug, Serialize, Deserialize)]
1589#[serde(rename_all = "camelCase")]
1590pub struct OKXFeeRate {
1591 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1593 pub level: OKXVipLevel,
1594 pub taker: String,
1596 pub maker: String,
1598 pub taker_u: String,
1600 pub maker_u: String,
1602 #[serde(
1604 default,
1605 alias = "elpMaker",
1606 deserialize_with = "deserialize_optional_decimal"
1607 )]
1608 pub rpi_maker: Option<Decimal>,
1609 #[serde(default)]
1611 pub delivery: String,
1612 #[serde(default)]
1614 pub exercise: String,
1615 #[serde(default)]
1617 pub settle: String,
1618 pub inst_type: OKXInstrumentType,
1620 #[serde(default)]
1622 pub category: String,
1623 #[serde(deserialize_with = "deserialize_string_to_u64")]
1625 pub ts: u64,
1626}
1627
1628fn deserialize_configuration_enum<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1629where
1630 D: Deserializer<'de>,
1631 T: Deserialize<'de>,
1632{
1633 let value = String::deserialize(deserializer)?;
1634 T::deserialize(value.into_deserializer())
1635}
1636
1637mod account_permissions {
1638 use serde::{Deserialize, Deserializer, Serializer, de::IntoDeserializer};
1639
1640 use crate::common::enums::OKXApiKeyPermission;
1641
1642 pub(super) fn serialize<S>(
1643 permissions: &[OKXApiKeyPermission],
1644 serializer: S,
1645 ) -> Result<S::Ok, S::Error>
1646 where
1647 S: Serializer,
1648 {
1649 let value = permissions
1650 .iter()
1651 .map(AsRef::as_ref)
1652 .collect::<Vec<_>>()
1653 .join(",");
1654 serializer.serialize_str(&value)
1655 }
1656
1657 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<OKXApiKeyPermission>, D::Error>
1658 where
1659 D: Deserializer<'de>,
1660 {
1661 let value = String::deserialize(deserializer)?;
1662 value
1663 .split(',')
1664 .map(|permission| OKXApiKeyPermission::deserialize(permission.into_deserializer()))
1665 .collect()
1666 }
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671 use rstest::rstest;
1672 use serde_json;
1673
1674 use super::*;
1675
1676 #[rstest]
1677 fn test_algo_order_deserializes_current_child_identifier_lists() {
1678 let order: OKXOrderAlgo = serde_json::from_value(serde_json::json!({
1679 "algoId": "123",
1680 "algoClOrdId": "algo-client-1",
1681 "ordId": "456",
1682 "ordIdList": ["456", "457"],
1683 "subAlgoIdList": ["789"],
1684 "instId": "ETH-USDT-SWAP",
1685 "instType": "SWAP",
1686 "ordType": "conditional",
1687 "state": "effective",
1688 "side": "sell",
1689 "posSide": "net",
1690 "tdMode": "cross",
1691 "cTime": "1700000000000",
1692 "uTime": "1700000001000"
1693 }))
1694 .unwrap();
1695
1696 assert_eq!(order.ord_id, "456");
1697 assert_eq!(order.ord_id_list, ["456", "457"]);
1698 assert_eq!(order.sub_algo_id_list, ["789"]);
1699 }
1700
1701 #[rstest]
1702 fn test_algo_order_request_serialization() {
1703 let request = OKXPlaceAlgoOrderRequest {
1704 inst_id: "ETH-USDT-SWAP".to_string(),
1705 inst_id_code: None,
1706 td_mode: OKXTradeMode::Isolated,
1707 side: OKXSide::Buy,
1708 ord_type: OKXAlgoOrderType::Trigger,
1709 sz: Some("0.01".to_string()),
1710 algo_cl_ord_id: Some("test123".to_string()),
1711 trigger_px: Some("3000".to_string()),
1712 order_px: Some("-1".to_string()),
1713 trigger_px_type: Some(OKXTriggerType::Last),
1714 sl_trigger_px: None,
1715 sl_ord_px: None,
1716 sl_trigger_px_type: None,
1717 tp_trigger_px: None,
1718 tp_ord_px: None,
1719 tp_trigger_px_type: None,
1720 tgt_ccy: None,
1721 pos_side: None,
1722 close_position: None,
1723 tag: None,
1724 reduce_only: None,
1725 close_fraction: None,
1726 callback_ratio: None,
1727 callback_spread: None,
1728 active_px: None,
1729 };
1730
1731 let json = serde_json::to_string(&request).unwrap();
1732
1733 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1735 assert!(json.contains("\"tdMode\":\"isolated\""));
1736 assert!(json.contains("\"ordType\":\"trigger\""));
1737 assert!(json.contains("\"algoClOrdId\":\"test123\""));
1738 assert!(json.contains("\"triggerPx\":\"3000\""));
1739 assert!(json.contains("\"orderPx\":\"-1\""));
1740 assert!(json.contains("\"triggerPxType\":\"last\""));
1741
1742 assert!(!json.contains("tgtCcy"));
1744 assert!(!json.contains("posSide"));
1745 assert!(!json.contains("closePosition"));
1746 assert!(!json.contains("closeFraction"));
1747 }
1748
1749 #[rstest]
1750 fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1751 let request = OKXAmendAlgoOrderRequest {
1755 inst_id: "ETH-USDT-SWAP".to_string(),
1756 algo_id: "123".to_string(),
1757 algo_cl_ord_id: None,
1758 new_sz: None,
1759 new_trigger_px: None,
1760 new_tp_trigger_px: None,
1761 new_tp_ord_px: None,
1762 new_tp_trigger_px_type: None,
1763 new_sl_trigger_px: Some("850".to_string()),
1764 new_sl_ord_px: None,
1765 new_sl_trigger_px_type: None,
1766 new_order_px: None,
1767 new_callback_ratio: None,
1768 new_callback_spread: None,
1769 new_active_px: None,
1770 };
1771
1772 let json = serde_json::to_string(&request).unwrap();
1773
1774 assert!(json.contains("\"algoId\":\"123\""));
1775 assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1776 assert!(!json.contains("newTriggerPx"));
1778 assert!(!json.contains("newSz"));
1779 assert!(!json.contains("algoClOrdId"));
1780 }
1781
1782 #[rstest]
1783 fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1784 let request = OKXAmendAlgoOrderRequest {
1785 inst_id: "DOGE-USDT-SWAP".to_string(),
1786 algo_id: "algo-oco-1".to_string(),
1787 algo_cl_ord_id: None,
1788 new_sz: None,
1789 new_trigger_px: None,
1790 new_tp_trigger_px: Some("0.10495".to_string()),
1791 new_tp_ord_px: Some("-1".to_string()),
1792 new_tp_trigger_px_type: Some("last".to_string()),
1793 new_sl_trigger_px: Some("0.06297".to_string()),
1794 new_sl_ord_px: Some("-1".to_string()),
1795 new_sl_trigger_px_type: Some("last".to_string()),
1796 new_order_px: None,
1797 new_callback_ratio: None,
1798 new_callback_spread: None,
1799 new_active_px: None,
1800 };
1801
1802 let json = serde_json::to_string(&request).unwrap();
1803
1804 assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1805 assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1806 assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1807 assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1808 assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1809 assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1810 assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1811 assert!(!json.contains("newTriggerPx"));
1812 }
1813
1814 #[rstest]
1815 fn test_algo_order_request_serializes_close_fraction() {
1816 let request = OKXPlaceAlgoOrderRequest {
1817 inst_id: "ETH-USDT-SWAP".to_string(),
1818 inst_id_code: None,
1819 td_mode: OKXTradeMode::Cross,
1820 side: OKXSide::Sell,
1821 ord_type: OKXAlgoOrderType::Conditional,
1822 sz: None,
1823 algo_cl_ord_id: Some("close-frac-123".to_string()),
1824 trigger_px: None,
1825 order_px: None,
1826 trigger_px_type: None,
1827 sl_trigger_px: Some("3000".to_string()),
1828 sl_ord_px: Some("-1".to_string()),
1829 sl_trigger_px_type: Some(OKXTriggerType::Last),
1830 tp_trigger_px: None,
1831 tp_ord_px: None,
1832 tp_trigger_px_type: None,
1833 tgt_ccy: None,
1834 pos_side: Some(OKXPositionSide::Net),
1835 close_position: None,
1836 tag: None,
1837 reduce_only: Some(true),
1838 close_fraction: Some("1".to_string()),
1839 callback_ratio: None,
1840 callback_spread: None,
1841 active_px: None,
1842 };
1843
1844 let json = serde_json::to_string(&request).unwrap();
1845
1846 assert!(json.contains("\"ordType\":\"conditional\""));
1847 assert!(json.contains("\"closeFraction\":\"1\""));
1848 assert!(json.contains("\"slTriggerPx\":\"3000\""));
1849 assert!(json.contains("\"slOrdPx\":\"-1\""));
1850 assert!(json.contains("\"slTriggerPxType\":\"last\""));
1851 assert!(json.contains("\"reduceOnly\":true"));
1852 assert!(!json.contains("\"sz\""));
1853 assert!(!json.contains("triggerPx"));
1854 }
1855
1856 #[rstest]
1857 fn test_algo_order_request_array_serialization() {
1858 let request = OKXPlaceAlgoOrderRequest {
1859 inst_id: "BTC-USDT".to_string(),
1860 inst_id_code: Some(10459),
1861 td_mode: OKXTradeMode::Cross,
1862 side: OKXSide::Sell,
1863 ord_type: OKXAlgoOrderType::Trigger,
1864 sz: Some("0.1".to_string()),
1865 algo_cl_ord_id: None,
1866 trigger_px: Some("50000".to_string()),
1867 order_px: Some("49900".to_string()),
1868 trigger_px_type: Some(OKXTriggerType::Mark),
1869 sl_trigger_px: None,
1870 sl_ord_px: None,
1871 sl_trigger_px_type: None,
1872 tp_trigger_px: None,
1873 tp_ord_px: None,
1874 tp_trigger_px_type: None,
1875 tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1876 pos_side: Some(OKXPositionSide::Net),
1877 close_position: None,
1878 tag: None,
1879 reduce_only: Some(true),
1880 close_fraction: None,
1881 callback_ratio: None,
1882 callback_spread: None,
1883 active_px: None,
1884 };
1885
1886 let json = serde_json::to_string(&[request]).unwrap();
1888
1889 assert!(json.starts_with('['));
1891 assert!(json.ends_with(']'));
1892
1893 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1895 assert!(json.contains("\"tdMode\":\"cross\""));
1896 assert!(json.contains("\"triggerPx\":\"50000\""));
1897 assert!(json.contains("\"orderPx\":\"49900\""));
1898 assert!(json.contains("\"triggerPxType\":\"mark\""));
1899 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1900 assert!(json.contains("\"posSide\":\"net\""));
1901 assert!(json.contains("\"reduceOnly\":true"));
1902 }
1903
1904 #[rstest]
1905 fn test_cancel_algo_order_request_serialization() {
1906 let request = OKXCancelAlgoOrderRequest {
1907 inst_id: "ETH-USDT-SWAP".to_string(),
1908 inst_id_code: None,
1909 algo_id: Some("123456".to_string()),
1910 algo_cl_ord_id: None,
1911 };
1912
1913 let json = serde_json::to_string(&request).unwrap();
1914
1915 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1917 assert!(json.contains("\"algoId\":\"123456\""));
1918 assert!(!json.contains("algoClOrdId"));
1919 }
1920
1921 #[rstest]
1922 fn test_cancel_algo_order_with_client_id_serialization() {
1923 let request = OKXCancelAlgoOrderRequest {
1924 inst_id: "BTC-USDT".to_string(),
1925 inst_id_code: Some(10459),
1926 algo_id: None,
1927 algo_cl_ord_id: Some("client123".to_string()),
1928 };
1929
1930 let json = serde_json::to_string(&[request]).unwrap();
1932
1933 assert!(json.starts_with('['));
1935 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1936 assert!(json.contains("\"algoClOrdId\":\"client123\""));
1937 assert!(!json.contains("\"algoId\""));
1938 }
1939
1940 #[rstest]
1941 fn test_amend_algo_order_trigger_serialization() {
1942 let request = OKXAmendAlgoOrderRequest {
1943 inst_id: "ETH-USDT-SWAP".to_string(),
1944 algo_id: "123456".to_string(),
1945 algo_cl_ord_id: None,
1946 new_sz: None,
1947 new_trigger_px: Some("3500".to_string()),
1948 new_tp_trigger_px: None,
1949 new_tp_ord_px: None,
1950 new_tp_trigger_px_type: None,
1951 new_sl_trigger_px: None,
1952 new_sl_ord_px: None,
1953 new_sl_trigger_px_type: None,
1954 new_order_px: Some("3490".to_string()),
1955 new_callback_ratio: None,
1956 new_callback_spread: None,
1957 new_active_px: None,
1958 };
1959
1960 let json = serde_json::to_string(&request).unwrap();
1961
1962 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1963 assert!(json.contains("\"algoId\":\"123456\""));
1964 assert!(json.contains("\"newTriggerPx\":\"3500\""));
1965 assert!(json.contains("\"newOrderPx\":\"3490\""));
1966 assert!(!json.contains("newSz"));
1967 assert!(!json.contains("algoClOrdId"));
1968 assert!(!json.contains("newCallbackRatio"));
1969 }
1970
1971 #[rstest]
1972 fn test_amend_algo_order_trailing_stop_serialization() {
1973 let request = OKXAmendAlgoOrderRequest {
1974 inst_id: "BTC-USDT-SWAP".to_string(),
1975 algo_id: "789012".to_string(),
1976 algo_cl_ord_id: Some("client456".to_string()),
1977 new_sz: Some("0.1".to_string()),
1978 new_trigger_px: None,
1979 new_tp_trigger_px: None,
1980 new_tp_ord_px: None,
1981 new_tp_trigger_px_type: None,
1982 new_sl_trigger_px: None,
1983 new_sl_ord_px: None,
1984 new_sl_trigger_px_type: None,
1985 new_order_px: None,
1986 new_callback_ratio: Some("0.02".to_string()),
1987 new_callback_spread: None,
1988 new_active_px: Some("50000".to_string()),
1989 };
1990
1991 let json = serde_json::to_string(&request).unwrap();
1992
1993 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1994 assert!(json.contains("\"algoId\":\"789012\""));
1995 assert!(json.contains("\"algoClOrdId\":\"client456\""));
1996 assert!(json.contains("\"newSz\":\"0.1\""));
1997 assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1998 assert!(json.contains("\"newActivePx\":\"50000\""));
1999 assert!(!json.contains("newTriggerPx"));
2000 assert!(!json.contains("newOrderPx"));
2001 }
2002
2003 #[rstest]
2004 fn test_trailing_stop_request_callback_ratio_serialization() {
2005 let request = OKXPlaceAlgoOrderRequest {
2006 inst_id: "BTC-USDT-SWAP".to_string(),
2007 inst_id_code: None,
2008 td_mode: OKXTradeMode::Cross,
2009 side: OKXSide::Buy,
2010 ord_type: OKXAlgoOrderType::MoveOrderStop,
2011 sz: Some("0.1".to_string()),
2012 algo_cl_ord_id: Some("trail-001".to_string()),
2013 trigger_px: None,
2014 order_px: None,
2015 trigger_px_type: None,
2016 sl_trigger_px: None,
2017 sl_ord_px: None,
2018 sl_trigger_px_type: None,
2019 tp_trigger_px: None,
2020 tp_ord_px: None,
2021 tp_trigger_px_type: None,
2022 tgt_ccy: None,
2023 pos_side: None,
2024 close_position: None,
2025 tag: None,
2026 reduce_only: None,
2027 close_fraction: None,
2028 callback_ratio: Some("0.01".to_string()),
2029 callback_spread: None,
2030 active_px: None,
2031 };
2032
2033 let json = serde_json::to_string(&request).unwrap();
2034
2035 assert!(json.contains("\"ordType\":\"move_order_stop\""));
2036 assert!(json.contains("\"callbackRatio\":\"0.01\""));
2037 assert!(!json.contains("callbackSpread"));
2038 assert!(!json.contains("activePx"));
2039 }
2040
2041 #[rstest]
2042 fn test_trailing_stop_request_callback_spread_serialization() {
2043 let request = OKXPlaceAlgoOrderRequest {
2044 inst_id: "ETH-USDT-SWAP".to_string(),
2045 inst_id_code: None,
2046 td_mode: OKXTradeMode::Isolated,
2047 side: OKXSide::Sell,
2048 ord_type: OKXAlgoOrderType::MoveOrderStop,
2049 sz: Some("1.0".to_string()),
2050 algo_cl_ord_id: None,
2051 trigger_px: None,
2052 order_px: None,
2053 trigger_px_type: None,
2054 sl_trigger_px: None,
2055 sl_ord_px: None,
2056 sl_trigger_px_type: None,
2057 tp_trigger_px: None,
2058 tp_ord_px: None,
2059 tp_trigger_px_type: None,
2060 tgt_ccy: None,
2061 pos_side: None,
2062 close_position: None,
2063 tag: None,
2064 reduce_only: Some(true),
2065 close_fraction: None,
2066 callback_ratio: None,
2067 callback_spread: Some("50.5".to_string()),
2068 active_px: None,
2069 };
2070
2071 let json = serde_json::to_string(&request).unwrap();
2072
2073 assert!(json.contains("\"callbackSpread\":\"50.5\""));
2074 assert!(!json.contains("callbackRatio"));
2075 assert!(!json.contains("activePx"));
2076 }
2077
2078 #[rstest]
2079 fn test_trailing_stop_request_with_activation_price_serialization() {
2080 let request = OKXPlaceAlgoOrderRequest {
2081 inst_id: "BTC-USDT-SWAP".to_string(),
2082 inst_id_code: None,
2083 td_mode: OKXTradeMode::Cross,
2084 side: OKXSide::Buy,
2085 ord_type: OKXAlgoOrderType::MoveOrderStop,
2086 sz: Some("0.5".to_string()),
2087 algo_cl_ord_id: None,
2088 trigger_px: None,
2089 order_px: None,
2090 trigger_px_type: None,
2091 sl_trigger_px: None,
2092 sl_ord_px: None,
2093 sl_trigger_px_type: None,
2094 tp_trigger_px: None,
2095 tp_ord_px: None,
2096 tp_trigger_px_type: None,
2097 tgt_ccy: None,
2098 pos_side: None,
2099 close_position: None,
2100 tag: None,
2101 reduce_only: None,
2102 close_fraction: None,
2103 callback_ratio: Some("0.005".to_string()),
2104 callback_spread: None,
2105 active_px: Some("65000".to_string()),
2106 };
2107
2108 let json = serde_json::to_string(&request).unwrap();
2109
2110 assert!(json.contains("\"callbackRatio\":\"0.005\""));
2111 assert!(json.contains("\"activePx\":\"65000\""));
2112 assert!(!json.contains("callbackSpread"));
2113 }
2114
2115 #[rstest]
2116 fn test_amend_algo_order_callback_spread_serialization() {
2117 let request = OKXAmendAlgoOrderRequest {
2118 inst_id: "ETH-USDT-SWAP".to_string(),
2119 algo_id: "456789".to_string(),
2120 algo_cl_ord_id: None,
2121 new_sz: None,
2122 new_trigger_px: None,
2123 new_tp_trigger_px: None,
2124 new_tp_ord_px: None,
2125 new_tp_trigger_px_type: None,
2126 new_sl_trigger_px: None,
2127 new_sl_ord_px: None,
2128 new_sl_trigger_px_type: None,
2129 new_order_px: None,
2130 new_callback_ratio: None,
2131 new_callback_spread: Some("25.0".to_string()),
2132 new_active_px: Some("4000".to_string()),
2133 };
2134
2135 let json = serde_json::to_string(&request).unwrap();
2136
2137 assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
2138 assert!(json.contains("\"newActivePx\":\"4000\""));
2139 assert!(!json.contains("newCallbackRatio"));
2140 assert!(!json.contains("newTriggerPx"));
2141 assert!(!json.contains("newSz"));
2142 }
2143
2144 #[rstest]
2145 fn test_amend_algo_order_size_only_serialization() {
2146 let request = OKXAmendAlgoOrderRequest {
2147 inst_id: "BTC-USDT-SWAP".to_string(),
2148 algo_id: "111222".to_string(),
2149 algo_cl_ord_id: None,
2150 new_sz: Some("0.5".to_string()),
2151 new_trigger_px: None,
2152 new_tp_trigger_px: None,
2153 new_tp_ord_px: None,
2154 new_tp_trigger_px_type: None,
2155 new_sl_trigger_px: None,
2156 new_sl_ord_px: None,
2157 new_sl_trigger_px_type: None,
2158 new_order_px: None,
2159 new_callback_ratio: None,
2160 new_callback_spread: None,
2161 new_active_px: None,
2162 };
2163
2164 let json = serde_json::to_string(&request).unwrap();
2165
2166 assert!(json.contains("\"newSz\":\"0.5\""));
2167 assert!(!json.contains("newTriggerPx"));
2168 assert!(!json.contains("newOrderPx"));
2169 assert!(!json.contains("newCallbackRatio"));
2170 assert!(!json.contains("newCallbackSpread"));
2171 assert!(!json.contains("newActivePx"));
2172 }
2173
2174 #[rstest]
2175 fn test_amend_algo_order_all_fields_serialization() {
2176 let request = OKXAmendAlgoOrderRequest {
2177 inst_id: "BTC-USDT-SWAP".to_string(),
2178 algo_id: "333444".to_string(),
2179 algo_cl_ord_id: Some("client789".to_string()),
2180 new_sz: Some("1.0".to_string()),
2181 new_trigger_px: Some("60000".to_string()),
2182 new_tp_trigger_px: None,
2183 new_tp_ord_px: None,
2184 new_tp_trigger_px_type: None,
2185 new_sl_trigger_px: None,
2186 new_sl_ord_px: None,
2187 new_sl_trigger_px_type: None,
2188 new_order_px: Some("59900".to_string()),
2189 new_callback_ratio: Some("0.015".to_string()),
2190 new_callback_spread: Some("100".to_string()),
2191 new_active_px: Some("62000".to_string()),
2192 };
2193
2194 let json = serde_json::to_string(&request).unwrap();
2195
2196 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2197 assert!(json.contains("\"algoId\":\"333444\""));
2198 assert!(json.contains("\"algoClOrdId\":\"client789\""));
2199 assert!(json.contains("\"newSz\":\"1.0\""));
2200 assert!(json.contains("\"newTriggerPx\":\"60000\""));
2201 assert!(json.contains("\"newOrderPx\":\"59900\""));
2202 assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2203 assert!(json.contains("\"newCallbackSpread\":\"100\""));
2204 assert!(json.contains("\"newActivePx\":\"62000\""));
2205 }
2206
2207 #[rstest]
2208 fn test_place_order_request_serializes_px_usd() {
2209 let request = OKXPlaceOrderRequest {
2210 inst_id: "BTC-USD-250328-50000-C".to_string(),
2211 td_mode: OKXTradeMode::Cross,
2212 ccy: None,
2213 cl_ord_id: Some("test-opt-1".to_string()),
2214 tag: None,
2215 side: OKXSide::Buy,
2216 pos_side: Some(OKXPositionSide::Net),
2217 ord_type: OKXOrderType::Limit,
2218 sz: "1".to_string(),
2219 px: None,
2220 px_usd: Some("100.5".to_string()),
2221 px_vol: None,
2222 reduce_only: None,
2223 tgt_ccy: None,
2224 trade_quote_ccy: None,
2225 attach_algo_ords: None,
2226 outcome: None,
2227 slippage_pct: None,
2228 rpi_taker_access: None,
2229 rpi_px_round: None,
2230 };
2231
2232 let json = serde_json::to_string(&request).unwrap();
2233 assert!(json.contains("\"pxUsd\":\"100.5\""));
2234 assert!(!json.contains("\"pxVol\""));
2235 assert!(!json.contains("\"px\":"));
2236 assert!(!json.contains("slippagePct"));
2237 }
2238
2239 #[rstest]
2240 fn test_place_order_request_serializes_px_vol() {
2241 let request = OKXPlaceOrderRequest {
2242 inst_id: "BTC-USD-250328-50000-C".to_string(),
2243 td_mode: OKXTradeMode::Cross,
2244 ccy: None,
2245 cl_ord_id: Some("test-opt-2".to_string()),
2246 tag: None,
2247 side: OKXSide::Buy,
2248 pos_side: Some(OKXPositionSide::Net),
2249 ord_type: OKXOrderType::Limit,
2250 sz: "1".to_string(),
2251 px: None,
2252 px_usd: None,
2253 px_vol: Some("0.55".to_string()),
2254 reduce_only: None,
2255 tgt_ccy: None,
2256 trade_quote_ccy: None,
2257 attach_algo_ords: None,
2258 outcome: None,
2259 slippage_pct: None,
2260 rpi_taker_access: None,
2261 rpi_px_round: None,
2262 };
2263
2264 let json = serde_json::to_string(&request).unwrap();
2265 assert!(json.contains("\"pxVol\":\"0.55\""));
2266 assert!(!json.contains("\"pxUsd\""));
2267 assert!(!json.contains("\"px\":"));
2268 }
2269
2270 #[rstest]
2271 fn test_place_order_request_serializes_slippage_pct() {
2272 let request = OKXPlaceOrderRequest {
2273 inst_id: "BTC-USDT-SWAP".to_string(),
2274 td_mode: OKXTradeMode::Cross,
2275 ccy: None,
2276 cl_ord_id: Some("mkt-slip-1".to_string()),
2277 tag: None,
2278 side: OKXSide::Buy,
2279 pos_side: Some(OKXPositionSide::Net),
2280 ord_type: OKXOrderType::Market,
2281 sz: "1".to_string(),
2282 px: None,
2283 px_usd: None,
2284 px_vol: None,
2285 reduce_only: None,
2286 tgt_ccy: None,
2287 trade_quote_ccy: None,
2288 attach_algo_ords: None,
2289 outcome: None,
2290 slippage_pct: Some("0.005".to_string()),
2291 rpi_taker_access: None,
2292 rpi_px_round: None,
2293 };
2294
2295 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2296 assert_eq!(json["slippagePct"], "0.005");
2297 }
2298
2299 #[rstest]
2300 fn test_rpi_place_order_request_serialization() {
2301 let request = OKXPlaceOrderRequest {
2302 inst_id: "OMI-USD".to_string(),
2303 td_mode: OKXTradeMode::Cash,
2304 ccy: None,
2305 cl_ord_id: Some("ORPI001".to_string()),
2306 tag: Some("rpi-test".to_string()),
2307 side: OKXSide::Sell,
2308 pos_side: None,
2309 ord_type: OKXOrderType::Rpi,
2310 sz: "250000".to_string(),
2311 px: Some("0.0001600".to_string()),
2312 px_usd: None,
2313 px_vol: None,
2314 reduce_only: Some(false),
2315 tgt_ccy: None,
2316 trade_quote_ccy: None,
2317 attach_algo_ords: None,
2318 outcome: None,
2319 slippage_pct: None,
2320 rpi_taker_access: Some(true),
2321 rpi_px_round: Some(false),
2322 };
2323
2324 let value = serde_json::to_value(request).unwrap();
2325
2326 assert_eq!(
2327 value,
2328 serde_json::json!({
2329 "instId": "OMI-USD",
2330 "tdMode": "cash",
2331 "clOrdId": "ORPI001",
2332 "tag": "rpi-test",
2333 "side": "sell",
2334 "ordType": "rpi",
2335 "sz": "250000",
2336 "px": "0.0001600",
2337 "reduceOnly": false,
2338 "rpiTakerAccess": true,
2339 "rpiPxRound": false
2340 })
2341 );
2342 }
2343
2344 #[rstest]
2345 fn test_rpi_amend_order_request_serialization() {
2346 let request = OKXAmendOrderRequest {
2347 inst_id: "OMI-USD".to_string(),
2348 ord_id: Some("2500000000000000001".to_string()),
2349 cl_ord_id: None,
2350 req_id: Some("RPI-AMEND-1".to_string()),
2351 new_sz: Some("275000".to_string()),
2352 new_px: Some("0.0001599".to_string()),
2353 rpi_taker_access: Some(false),
2354 rpi_px_round: Some(true),
2355 };
2356
2357 let value = serde_json::to_value(request).unwrap();
2358
2359 assert_eq!(
2360 value,
2361 serde_json::json!({
2362 "instId": "OMI-USD",
2363 "ordId": "2500000000000000001",
2364 "reqId": "RPI-AMEND-1",
2365 "newSz": "275000",
2366 "newPx": "0.0001599",
2367 "rpiTakerAccess": false,
2368 "rpiPxRound": true
2369 })
2370 );
2371 }
2372
2373 #[rstest]
2374 fn test_rpi_place_order_response_parsing() {
2375 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2376 "ordId": "2500000000000000001",
2377 "clOrdId": "ORPI001",
2378 "tag": "rpi-test",
2379 "instId": "OMI-USD",
2380 "side": "sell",
2381 "ordType": "rpi",
2382 "sz": "250000",
2383 "state": "live",
2384 "px": "0.0001600",
2385 "avgPx": "",
2386 "sCode": "0",
2387 "sMsg": ""
2388 }))
2389 .unwrap();
2390
2391 assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
2392 assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
2393 assert_eq!(response.tag.as_deref(), Some("rpi-test"));
2394 assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
2395 assert_eq!(response.side, Some(OKXSide::Sell));
2396 assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
2397 assert_eq!(response.sz.as_deref(), Some("250000"));
2398 assert_eq!(response.state, Some(OKXOrderStatus::Live));
2399 assert_eq!(response.px.as_deref(), Some("0.0001600"));
2400 assert_eq!(response.avg_px.as_deref(), Some(""));
2401 assert_eq!(response.s_code.as_deref(), Some("0"));
2402 assert_eq!(response.s_msg.as_deref(), Some(""));
2403 }
2404
2405 #[rstest]
2406 fn test_event_contract_models_deserialize() {
2407 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2408 "seriesId": "BTC-ABOVE-DAILY",
2409 "freq": "daily",
2410 "title": "BTC above daily",
2411 "category": "1",
2412 "settlement": {
2413 "method": "cash",
2414 "closeEarly": false,
2415 "srcName": "OKX BTC/USD Index",
2416 "underlying": "BTC-USD"
2417 }
2418 }))
2419 .unwrap();
2420 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2421 "seriesId": "BTC-ABOVE-DAILY",
2422 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2423 "fixTime": "",
2424 "expTime": "1769697132335",
2425 "state": "live"
2426 }))
2427 .unwrap();
2428 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2429 "seriesId": "BTC-ABOVE-DAILY",
2430 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2431 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2432 "listTime": "1769697132335",
2433 "fixTime": "",
2434 "expTime": "1769697132335",
2435 "state": "live",
2436 "disputed": false,
2437 "outcome": "0",
2438 "floorStrike": "120000",
2439 "capStrike": "INF",
2440 "settleValue": "",
2441 "hitDir": ""
2442 }))
2443 .unwrap();
2444
2445 assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2446 assert_eq!(series.settlement.underlying, "BTC-USD");
2447 assert_eq!(event.fix_time, None);
2448 assert_eq!(event.exp_time, Some(1_769_697_132_335));
2449 assert_eq!(
2450 market.inst_id,
2451 Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2452 );
2453 assert_eq!(market.list_time, Some(1_769_697_132_335));
2454 assert_eq!(market.exp_time, Some(1_769_697_132_335));
2455 assert_eq!(market.outcome, "0");
2456 assert_eq!(market.cap_strike, "INF");
2457 assert_eq!(market.hit_dir, "");
2458
2459 let serialized = serde_json::to_value(&market).unwrap();
2460 assert_eq!(serialized["capStrike"], "INF");
2461 assert_eq!(serialized["hitDir"], "");
2462 }
2463
2464 #[rstest]
2465 fn test_event_contract_models_accept_missing_optional_fields() {
2466 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2467 "seriesId": "BTC-ABOVE-DAILY"
2468 }))
2469 .unwrap();
2470 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2471 "seriesId": "BTC-ABOVE-DAILY",
2472 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2473 "fixTime": "",
2474 "expTime": "",
2475 "state": "live"
2476 }))
2477 .unwrap();
2478 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2479 "seriesId": "BTC-ABOVE-DAILY",
2480 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2481 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2482 "listTime": "",
2483 "fixTime": "",
2484 "expTime": "",
2485 "state": "live",
2486 "disputed": false,
2487 "outcome": "0",
2488 "floorStrike": "120000",
2489 "settleValue": ""
2490 }))
2491 .unwrap();
2492
2493 assert_eq!(series.freq, "");
2494 assert_eq!(series.settlement.underlying, "");
2495 assert_eq!(event.exp_time, None);
2496 assert_eq!(market.list_time, None);
2497 assert_eq!(market.exp_time, None);
2498 assert_eq!(market.cap_strike, "");
2499 assert_eq!(market.hit_dir, "");
2500 }
2501
2502 #[rstest]
2503 fn test_place_order_request_serializes_event_contract_fields() {
2504 let request = OKXPlaceOrderRequest {
2505 inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2506 td_mode: OKXTradeMode::Cash,
2507 ccy: None,
2508 cl_ord_id: Some("event-1".to_string()),
2509 tag: None,
2510 side: OKXSide::Buy,
2511 pos_side: None,
2512 ord_type: OKXOrderType::Limit,
2513 sz: "10".to_string(),
2514 px: Some("0.42".to_string()),
2515 px_usd: None,
2516 px_vol: None,
2517 reduce_only: None,
2518 tgt_ccy: None,
2519 trade_quote_ccy: None,
2520 attach_algo_ords: None,
2521 outcome: Some("yes".to_string()),
2522 slippage_pct: None,
2523 rpi_taker_access: None,
2524 rpi_px_round: None,
2525 };
2526
2527 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2528
2529 assert!(json.get("speedBump").is_none());
2530 assert_eq!(json["outcome"], "yes");
2531 assert!(json.get("tradeQuoteCcy").is_none());
2532 }
2533
2534 #[rstest]
2535 fn test_place_order_request_serializes_trade_quote_ccy_usd() {
2536 let request = OKXPlaceOrderRequest {
2537 inst_id: "BTC-USDC".to_string(),
2538 td_mode: OKXTradeMode::Cash,
2539 ccy: None,
2540 cl_ord_id: Some("usd-quote-1".to_string()),
2541 tag: None,
2542 side: OKXSide::Buy,
2543 pos_side: None,
2544 ord_type: OKXOrderType::Limit,
2545 sz: "0.01".to_string(),
2546 px: Some("100000".to_string()),
2547 px_usd: None,
2548 px_vol: None,
2549 reduce_only: None,
2550 tgt_ccy: None,
2551 trade_quote_ccy: Some(Ustr::from("USD")),
2552 attach_algo_ords: None,
2553 outcome: None,
2554 slippage_pct: None,
2555 rpi_taker_access: None,
2556 rpi_px_round: None,
2557 };
2558
2559 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2560 assert_eq!(json["instId"], "BTC-USDC");
2561 assert_eq!(json["tradeQuoteCcy"], "USD");
2562 }
2563
2564 #[rstest]
2565 fn test_place_order_request_omits_trade_quote_ccy_when_unset() {
2566 let request = OKXPlaceOrderRequest {
2567 inst_id: "BTC-USDC".to_string(),
2568 td_mode: OKXTradeMode::Cash,
2569 ccy: None,
2570 cl_ord_id: Some("usdc-default-1".to_string()),
2571 tag: None,
2572 side: OKXSide::Buy,
2573 pos_side: None,
2574 ord_type: OKXOrderType::Limit,
2575 sz: "0.01".to_string(),
2576 px: Some("100000".to_string()),
2577 px_usd: None,
2578 px_vol: None,
2579 reduce_only: None,
2580 tgt_ccy: None,
2581 trade_quote_ccy: None,
2582 attach_algo_ords: None,
2583 outcome: None,
2584 slippage_pct: None,
2585 rpi_taker_access: None,
2586 rpi_px_round: None,
2587 };
2588
2589 let json = serde_json::to_string(&request).unwrap();
2590 assert!(!json.contains("tradeQuoteCcy"));
2591 }
2592
2593 #[rstest]
2594 fn test_attach_algo_ord_request_serializes_trailing_fields() {
2595 let request = OKXAttachAlgoOrdRequest {
2596 attach_algo_cl_ord_id: Some("trail-1".to_string()),
2597 sl_trigger_px: None,
2598 sl_ord_px: None,
2599 sl_trigger_px_type: None,
2600 tp_trigger_px: None,
2601 tp_ord_px: None,
2602 tp_trigger_px_type: None,
2603 callback_ratio: Some("0.01".to_string()),
2604 callback_spread: None,
2605 active_px: Some("64000".to_string()),
2606 new_callback_ratio: Some("0.02".to_string()),
2607 new_callback_spread: Some("25".to_string()),
2608 new_active_px: Some("65000".to_string()),
2609 };
2610
2611 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2612
2613 assert_eq!(json["callbackRatio"], "0.01");
2614 assert_eq!(json["activePx"], "64000");
2615 assert_eq!(json["newCallbackRatio"], "0.02");
2616 assert_eq!(json["newCallbackSpread"], "25");
2617 assert_eq!(json["newActivePx"], "65000");
2618 assert!(json.get("callbackSpread").is_none());
2619 }
2620
2621 #[rstest]
2622 fn test_place_order_response_deserializes_sub_code() {
2623 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2624 "ordId": "",
2625 "clOrdId": "event-1",
2626 "sCode": "51000",
2627 "sMsg": "Parameter error",
2628 "subCode": "51004"
2629 }))
2630 .unwrap();
2631
2632 assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2633 assert_eq!(response.s_code, Some("51000".to_string()));
2634 assert_eq!(response.sub_code, Some("51004".to_string()));
2635 }
2636
2637 #[rstest]
2638 fn test_fee_rate_deserializes_settle() {
2639 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2640 "level": "VIP1",
2641 "taker": "-0.0005",
2642 "maker": "-0.0002",
2643 "takerU": "-0.0005",
2644 "makerU": "-0.0002",
2645 "settle": "-0.001",
2646 "instType": "EVENTS",
2647 "category": "1",
2648 "ts": "1769697132335"
2649 }))
2650 .unwrap();
2651
2652 assert_eq!(fee_rate.settle, "-0.001");
2653 assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2654 }
2655
2656 #[rstest]
2657 fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
2658 let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
2659 "level": "VIP1",
2660 "taker": "-0.0005",
2661 "maker": "-0.0002",
2662 "takerU": "-0.0005",
2663 "makerU": "-0.0002",
2664 "rpiMaker": "-0.00015",
2665 "instType": "SPOT",
2666 "category": "1",
2667 "ts": "1785406500000"
2668 }))
2669 .unwrap();
2670 let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
2671 "level": "VIP1",
2672 "taker": "-0.0005",
2673 "maker": "-0.0002",
2674 "takerU": "-0.0005",
2675 "makerU": "-0.0002",
2676 "elpMaker": "-0.00016",
2677 "instType": "SPOT",
2678 "category": "1",
2679 "ts": "1785406500001"
2680 }))
2681 .unwrap();
2682 let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
2683 "level": "VIP1",
2684 "taker": "-0.0005",
2685 "maker": "-0.0002",
2686 "takerU": "-0.0005",
2687 "makerU": "-0.0002",
2688 "rpiMaker": "",
2689 "instType": "OPTION",
2690 "category": "1",
2691 "ts": "1785406500002"
2692 }))
2693 .unwrap();
2694
2695 assert_eq!(
2696 rpi.rpi_maker,
2697 Some(Decimal::from_str_exact("-0.00015").unwrap())
2698 );
2699 assert_eq!(
2700 legacy.rpi_maker,
2701 Some(Decimal::from_str_exact("-0.00016").unwrap())
2702 );
2703 assert_eq!(not_applicable.rpi_maker, None);
2704 }
2705}