1use nautilus_core::serialization::deserialize_optional_decimal;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
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 OKXAlgoOrderStatus, OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode,
80 OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXSpreadState,
81 OKXSpreadType, OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
82 },
83 parse::deserialize_string_to_u64,
84};
85
86#[derive(Clone, Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct OKXMarkPrice {
90 pub uly: Option<Ustr>,
92 pub inst_id: Ustr,
94 pub mark_px: String,
96 #[serde(deserialize_with = "deserialize_string_to_u64")]
98 pub ts: u64,
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct OKXPriceLimit {
105 pub inst_type: OKXInstrumentType,
107 pub inst_id: Ustr,
109 pub buy_lmt: String,
111 pub sell_lmt: String,
113 #[serde(deserialize_with = "deserialize_string_to_u64")]
115 pub ts: u64,
116 #[serde(default)]
118 pub enabled: bool,
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct OKXOptionSummary {
125 pub inst_type: OKXInstrumentType,
127 pub inst_id: Ustr,
129 pub uly: Ustr,
131 pub bid_vol: String,
133 pub ask_vol: String,
135 pub mark_vol: String,
137 pub fwd_px: String,
139 #[serde(deserialize_with = "deserialize_string_to_u64")]
141 pub ts: u64,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct OKXSpread {
148 pub sprd_id: Ustr,
150 pub sprd_type: OKXSpreadType,
152 pub state: OKXSpreadState,
154 pub base_ccy: Ustr,
156 pub sz_ccy: Ustr,
158 pub quote_ccy: Ustr,
160 pub tick_sz: String,
162 pub min_sz: String,
164 pub lot_sz: String,
166 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
168 pub list_time: Option<u64>,
169 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
171 pub exp_time: Option<u64>,
172 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
174 pub u_time: Option<u64>,
175 pub legs: Vec<OKXSpreadLeg>,
177}
178
179#[derive(Clone, Debug, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct OKXSpreadLeg {
183 pub inst_id: Ustr,
185 pub side: OKXSide,
187}
188
189#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct OKXPlaceSpreadOrderRequest {
193 pub sprd_id: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub cl_ord_id: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub tag: Option<String>,
201 pub side: OKXSide,
203 pub ord_type: OKXOrderType,
205 pub sz: String,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub px: Option<String>,
210}
211
212#[derive(Clone, Debug, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct OKXCancelSpreadOrderRequest {
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub ord_id: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub cl_ord_id: Option<String>,
222}
223
224#[derive(Clone, Debug, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct OKXCancelAllSpreadOrdersRequest {
228 pub sprd_id: String,
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct OKXSpreadOrder {
236 pub sprd_id: Ustr,
238 pub ord_id: Ustr,
240 #[serde(default)]
242 pub cl_ord_id: Ustr,
243 #[serde(default)]
245 pub tag: String,
246 pub side: OKXSide,
248 pub ord_type: OKXOrderType,
250 pub sz: String,
252 #[serde(default)]
254 pub px: String,
255 #[serde(default)]
257 pub avg_px: String,
258 pub state: OKXOrderStatus,
260 #[serde(default)]
262 pub acc_fill_sz: String,
263 #[serde(default)]
265 pub pending_fill_sz: String,
266 #[serde(default)]
268 pub pending_settle_sz: String,
269 #[serde(default)]
271 pub canceled_sz: String,
272 #[serde(default)]
274 pub fill_sz: String,
275 #[serde(default)]
277 pub fill_px: String,
278 #[serde(default)]
280 pub trade_id: Ustr,
281 #[serde(default)]
283 pub cancel_source: String,
284 #[serde(default)]
286 pub req_id: String,
287 #[serde(default)]
289 pub amend_result: String,
290 #[serde(default)]
292 pub code: String,
293 #[serde(default)]
295 pub msg: String,
296 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
298 pub c_time: Option<u64>,
299 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
301 pub u_time: Option<u64>,
302}
303
304#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct OKXSpreadTrade {
308 pub sprd_id: Ustr,
310 pub trade_id: Ustr,
312 pub ord_id: Ustr,
314 #[serde(default)]
316 pub cl_ord_id: Ustr,
317 pub fill_px: String,
319 pub fill_sz: String,
321 pub side: OKXSide,
323 #[serde(default)]
325 pub exec_type: OKXExecType,
326 #[serde(default)]
328 pub fee_ccy: String,
329 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
331 pub fee: Option<String>,
332 #[serde(deserialize_with = "deserialize_string_to_u64")]
334 pub ts: u64,
335}
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct OKXEventContractSettlement {
341 #[serde(default)]
343 pub method: String,
344 #[serde(default)]
346 pub close_early: bool,
347 #[serde(default)]
349 pub src_name: String,
350 #[serde(default)]
352 pub underlying: String,
353}
354
355#[derive(Clone, Debug, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct OKXEventContractSeries {
359 pub series_id: String,
361 #[serde(default)]
363 pub freq: String,
364 #[serde(default)]
366 pub title: String,
367 #[serde(default)]
369 pub category: String,
370 #[serde(default)]
372 pub settlement: OKXEventContractSettlement,
373}
374
375#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct OKXEventContractEvent {
379 pub series_id: String,
381 pub event_id: String,
383 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
385 pub fix_time: Option<u64>,
386 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
388 pub exp_time: Option<u64>,
389 pub state: String,
391}
392
393#[derive(Clone, Debug, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct OKXEventContractMarket {
397 pub series_id: String,
399 pub event_id: String,
401 pub inst_id: Ustr,
403 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
405 pub list_time: Option<u64>,
406 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
408 pub fix_time: Option<u64>,
409 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
411 pub exp_time: Option<u64>,
412 pub state: String,
414 pub disputed: bool,
416 pub outcome: String,
418 pub floor_strike: String,
420 #[serde(default)]
422 pub cap_strike: String,
423 pub settle_value: String,
425 #[serde(default)]
427 pub hit_dir: String,
428}
429
430#[derive(Clone, Debug, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct OKXIndexTicker {
434 pub inst_id: Ustr,
436 pub idx_px: String,
438 #[serde(deserialize_with = "deserialize_string_to_u64")]
440 pub ts: u64,
441}
442
443pub type OKXOrderBookLevel = (String, String, String, String);
446
447#[derive(Clone, Debug, Serialize, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct OKXOrderBookSnapshot {
451 pub asks: Vec<OKXOrderBookLevel>,
453 pub bids: Vec<OKXOrderBookLevel>,
455 #[serde(deserialize_with = "deserialize_string_to_u64")]
457 pub ts: u64,
458}
459
460#[derive(Clone, Debug, Serialize, Deserialize)]
462#[serde(rename_all = "camelCase")]
463pub struct OKXRpiOrderBookSnapshot {
464 pub asks: Vec<OKXRpiBookLevel>,
466 pub bids: Vec<OKXRpiBookLevel>,
468 pub seq_id: u64,
470 #[serde(deserialize_with = "deserialize_string_to_u64")]
472 pub ts: u64,
473}
474
475#[derive(Clone, Debug, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct OKXFundingRateHistory {
479 pub inst_type: OKXInstrumentType,
481 pub inst_id: Ustr,
483 pub funding_rate: String,
485 pub realized_rate: String,
487 #[serde(deserialize_with = "deserialize_string_to_u64")]
489 pub funding_time: u64,
490 #[serde(default)]
492 pub method: Option<String>,
493}
494
495#[derive(Clone, Debug, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct OKXPositionTier {
499 pub uly: Ustr,
501 pub inst_family: String,
503 pub inst_id: Ustr,
505 pub tier: String,
507 pub min_sz: String,
509 pub max_sz: String,
511 pub mmr: String,
513 pub imr: String,
515 pub max_lever: String,
517 pub opt_mgn_factor: String,
519 pub quote_max_loan: String,
521 pub base_max_loan: String,
523}
524
525#[derive(Clone, Debug, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct OKXAccount {
529 pub adj_eq: String,
531 pub borrow_froz: String,
533 pub details: Vec<OKXBalanceDetail>,
535 pub imr: String,
537 pub iso_eq: String,
539 pub mgn_ratio: String,
541 pub mmr: String,
543 pub notional_usd_for_borrow: String,
545 pub notional_usd_for_futures: String,
547 pub notional_usd_for_option: String,
549 pub notional_usd_for_swap: String,
551 pub notional_usd: String,
553 pub ord_froz: String,
555 pub total_eq: String,
557 #[serde(deserialize_with = "deserialize_string_to_u64")]
559 pub u_time: u64,
560 pub upl: String,
562}
563
564#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
568#[cfg_attr(
569 feature = "python",
570 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
571)]
572pub struct OKXBalanceDetail {
573 pub avail_bal: String,
575 pub avail_eq: String,
577 pub borrow_froz: String,
579 pub cash_bal: String,
581 pub ccy: Ustr,
583 pub cross_liab: String,
585 pub dis_eq: String,
587 pub eq: String,
589 pub eq_usd: String,
591 pub smt_sync_eq: String,
593 pub spot_copy_trading_eq: String,
595 pub fixed_bal: String,
597 pub frozen_bal: String,
599 pub imr: String,
601 pub interest: String,
603 pub iso_eq: String,
605 pub iso_liab: String,
607 pub iso_upl: String,
609 pub liab: String,
611 pub max_loan: String,
613 pub mgn_ratio: String,
615 pub mmr: String,
617 pub notional_lever: String,
619 pub ord_frozen: String,
621 pub reward_bal: String,
623 #[serde(alias = "spotInUse")]
625 pub spot_in_use_amt: String,
626 #[serde(alias = "clSpotInUse")]
628 pub cl_spot_in_use_amt: String,
629 #[serde(alias = "maxSpotInUse")]
631 pub max_spot_in_use_amt: String,
632 pub spot_iso_bal: String,
634 pub stgy_eq: String,
636 pub twap: String,
638 #[serde(deserialize_with = "deserialize_string_to_u64")]
640 pub u_time: u64,
641 pub upl: String,
643 pub upl_liab: String,
645 pub spot_bal: String,
647 pub open_avg_px: String,
649 pub acc_avg_px: String,
651 pub spot_upl: String,
653 pub spot_upl_ratio: String,
655 pub total_pnl: String,
657 pub total_pnl_ratio: String,
659}
660
661#[derive(Clone, Debug, Serialize, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct OKXPosition {
665 pub inst_id: Ustr,
667 pub inst_type: OKXInstrumentType,
669 pub mgn_mode: OKXMarginMode,
671 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
673 pub pos_id: Option<Ustr>,
674 pub pos_side: OKXPositionSide,
676 pub pos: String,
678 pub base_bal: String,
680 pub ccy: String,
682 pub fee: String,
684 pub lever: String,
686 pub last: String,
688 pub mark_px: String,
690 pub liq_px: String,
692 pub mmr: String,
694 pub interest: String,
696 pub trade_id: Ustr,
698 pub notional_usd: String,
700 pub avg_px: String,
702 pub upl: String,
704 pub upl_ratio: String,
706 #[serde(deserialize_with = "deserialize_string_to_u64")]
708 pub u_time: u64,
709 pub margin: String,
711 pub mgn_ratio: String,
713 pub adl: String,
715 pub c_time: String,
717 pub realized_pnl: String,
719 pub upl_last_px: String,
721 pub upl_ratio_last_px: String,
723 pub avail_pos: String,
725 pub be_px: String,
727 pub funding_fee: String,
729 pub idx_px: String,
731 pub liq_penalty: String,
733 pub opt_val: String,
735 pub pending_close_ord_liab_val: String,
737 pub pnl: String,
739 pub pos_ccy: String,
741 pub quote_bal: String,
743 pub quote_borrowed: String,
745 pub quote_interest: String,
747 #[serde(alias = "spotInUse")]
749 pub spot_in_use_amt: String,
750 pub spot_in_use_ccy: String,
752 pub usd_px: String,
754 #[serde(default)]
756 pub delta_bs: String,
757 #[serde(default)]
759 pub gamma_bs: String,
760 #[serde(default)]
762 pub theta_bs: String,
763 #[serde(default)]
765 pub vega_bs: String,
766}
767
768#[derive(Clone, Debug, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772pub struct OKXPlaceOrderResponse {
773 #[serde(default)]
775 pub ord_id: Option<Ustr>,
776 #[serde(default)]
778 pub cl_ord_id: Option<Ustr>,
779 #[serde(default)]
781 pub tag: Option<String>,
782 #[serde(default)]
784 pub inst_id: Option<Ustr>,
785 #[serde(default)]
787 pub side: Option<OKXSide>,
788 #[serde(default)]
790 pub ord_type: Option<OKXOrderType>,
791 #[serde(default)]
793 pub sz: Option<String>,
794 pub state: Option<OKXOrderStatus>,
796 #[serde(default)]
798 pub px: Option<String>,
799 #[serde(default)]
801 pub avg_px: Option<String>,
802 #[serde(default)]
804 pub acc_fill_sz: Option<String>,
805 #[serde(default)]
807 pub fill_sz: Option<String>,
808 #[serde(default)]
810 pub fill_px: Option<String>,
811 #[serde(default)]
813 pub trade_id: Option<Ustr>,
814 #[serde(default)]
816 pub fill_time: Option<String>,
817 #[serde(default)]
819 pub fee: Option<String>,
820 #[serde(default)]
822 pub fee_ccy: Option<String>,
823 #[serde(default)]
825 pub req_id: Option<Ustr>,
826 #[serde(default)]
828 pub pos_side: Option<OKXPositionSide>,
829 #[serde(default)]
831 pub reduce_only: Option<String>,
832 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
834 pub tgt_ccy: Option<OKXTargetCurrency>,
835 #[serde(default)]
837 pub c_time: Option<String>,
838 #[serde(default)]
840 pub u_time: Option<String>,
841 #[serde(skip_serializing_if = "Option::is_none")]
843 pub s_code: Option<String>,
844 #[serde(skip_serializing_if = "Option::is_none")]
846 pub s_msg: Option<String>,
847 #[serde(skip_serializing_if = "Option::is_none")]
849 pub sub_code: Option<String>,
850}
851
852#[derive(Clone, Debug, Default, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct OKXAttachAlgoOrdRequest {
856 #[serde(skip_serializing_if = "Option::is_none")]
858 pub attach_algo_cl_ord_id: Option<String>,
859 #[serde(skip_serializing_if = "Option::is_none")]
861 pub sl_trigger_px: Option<String>,
862 #[serde(skip_serializing_if = "Option::is_none")]
864 pub sl_ord_px: Option<String>,
865 #[serde(skip_serializing_if = "Option::is_none")]
867 pub sl_trigger_px_type: Option<OKXTriggerType>,
868 #[serde(skip_serializing_if = "Option::is_none")]
870 pub tp_trigger_px: Option<String>,
871 #[serde(skip_serializing_if = "Option::is_none")]
873 pub tp_ord_px: Option<String>,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub tp_trigger_px_type: Option<OKXTriggerType>,
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub callback_ratio: Option<String>,
880 #[serde(skip_serializing_if = "Option::is_none")]
882 pub callback_spread: Option<String>,
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub active_px: Option<String>,
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub new_callback_ratio: Option<String>,
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub new_callback_spread: Option<String>,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub new_active_px: Option<String>,
895}
896
897#[derive(Clone, Debug, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct OKXPlaceOrderRequest {
901 pub inst_id: String,
903 pub td_mode: OKXTradeMode,
905 #[serde(skip_serializing_if = "Option::is_none")]
907 pub ccy: Option<String>,
908 #[serde(skip_serializing_if = "Option::is_none")]
910 pub cl_ord_id: Option<String>,
911 #[serde(skip_serializing_if = "Option::is_none")]
913 pub tag: Option<String>,
914 pub side: OKXSide,
916 #[serde(skip_serializing_if = "Option::is_none")]
918 pub pos_side: Option<OKXPositionSide>,
919 pub ord_type: OKXOrderType,
921 pub sz: String,
923 #[serde(skip_serializing_if = "Option::is_none")]
925 pub px: Option<String>,
926 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
928 pub px_usd: Option<String>,
929 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
932 pub px_vol: Option<String>,
933 #[serde(skip_serializing_if = "Option::is_none")]
935 pub reduce_only: Option<bool>,
936 #[serde(skip_serializing_if = "Option::is_none")]
938 pub tgt_ccy: Option<OKXTargetCurrency>,
939 #[serde(skip_serializing_if = "Option::is_none")]
941 pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
942 #[serde(skip_serializing_if = "Option::is_none")]
944 pub speed_bump: Option<String>,
945 #[serde(skip_serializing_if = "Option::is_none")]
947 pub outcome: Option<String>,
948 #[serde(skip_serializing_if = "Option::is_none")]
953 pub slippage_pct: Option<String>,
954 #[serde(skip_serializing_if = "Option::is_none")]
956 pub rpi_taker_access: Option<bool>,
957 #[serde(skip_serializing_if = "Option::is_none")]
959 pub rpi_px_round: Option<bool>,
960}
961
962#[derive(Clone, Debug, Default, Serialize, Deserialize)]
964#[serde(rename_all = "camelCase")]
965pub struct OKXAmendOrderRequest {
966 pub inst_id: String,
968 #[serde(skip_serializing_if = "Option::is_none")]
970 pub ord_id: Option<String>,
971 #[serde(skip_serializing_if = "Option::is_none")]
973 pub cl_ord_id: Option<String>,
974 #[serde(skip_serializing_if = "Option::is_none")]
976 pub req_id: Option<String>,
977 #[serde(skip_serializing_if = "Option::is_none")]
979 pub new_sz: Option<String>,
980 #[serde(skip_serializing_if = "Option::is_none")]
982 pub new_px: Option<String>,
983 #[serde(skip_serializing_if = "Option::is_none")]
985 pub rpi_taker_access: Option<bool>,
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub rpi_px_round: Option<bool>,
989}
990
991#[derive(Clone, Debug, Serialize, Deserialize)]
993#[serde(rename_all = "camelCase")]
994pub struct OKXCancelOrderRequest {
995 pub inst_id: String,
997 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
999 pub inst_id_code: Option<u64>,
1000 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub ord_id: Option<String>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub cl_ord_id: Option<String>,
1006}
1007
1008#[derive(Clone, Debug, Serialize, Deserialize)]
1010#[serde(rename_all = "camelCase")]
1011pub struct OKXCancelOrderResponse {
1012 pub ord_id: String,
1014 #[serde(default)]
1016 pub cl_ord_id: Option<String>,
1017 #[serde(skip_serializing_if = "Option::is_none")]
1019 pub s_code: Option<String>,
1020 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub s_msg: Option<String>,
1023 #[serde(default)]
1025 pub ts: Option<String>,
1026}
1027
1028pub use crate::common::models::OKXAttachedAlgoOrd;
1029
1030#[derive(Clone, Debug, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct OKXOrderHistory {
1034 pub ord_id: Ustr,
1036 pub cl_ord_id: Ustr,
1038 #[serde(default)]
1040 pub algo_id: Option<Ustr>,
1041 #[serde(default)]
1043 pub algo_cl_ord_id: Option<Ustr>,
1044 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1046 pub attach_algo_cl_ord_id: Option<String>,
1047 #[serde(default)]
1049 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1050 #[serde(default)]
1052 pub cl_act_id: Option<Ustr>,
1053 pub tag: String,
1055 pub inst_type: OKXInstrumentType,
1057 pub uly: Option<Ustr>,
1059 pub inst_id: Ustr,
1061 pub ord_type: OKXOrderType,
1063 pub sz: String,
1065 pub px: String,
1067 #[serde(default)]
1069 pub px_usd: String,
1070 #[serde(default)]
1072 pub px_vol: String,
1073 pub side: OKXSide,
1075 pub pos_side: OKXPositionSide,
1077 pub td_mode: OKXTradeMode,
1079 pub reduce_only: String,
1081 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1083 pub tgt_ccy: Option<OKXTargetCurrency>,
1084 pub state: OKXOrderStatus,
1086 pub avg_px: String,
1088 pub fee: String,
1090 pub fee_ccy: String,
1092 pub fill_sz: String,
1094 pub fill_px: String,
1096 pub trade_id: Ustr,
1098 #[serde(deserialize_with = "deserialize_string_to_u64")]
1100 pub fill_time: u64,
1101 pub acc_fill_sz: String,
1103 #[serde(default)]
1105 pub fill_fee: Option<String>,
1106 #[serde(default)]
1108 pub req_id: Option<Ustr>,
1109 #[serde(default)]
1111 pub cancel_fill_sz: Option<String>,
1112 #[serde(default)]
1114 pub cancel_total_sz: Option<String>,
1115 #[serde(default)]
1117 pub cancel_source: String,
1118 #[serde(default)]
1120 pub cancel_source_reason: String,
1121 #[serde(default)]
1123 pub fee_discount: Option<String>,
1124 pub category: OKXOrderCategory,
1126 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1128 pub outcome: Option<String>,
1129 #[serde(deserialize_with = "deserialize_string_to_u64")]
1131 pub u_time: u64,
1132 #[serde(deserialize_with = "deserialize_string_to_u64")]
1134 pub c_time: u64,
1135}
1136
1137#[derive(Clone, Debug, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct OKXOrderAlgo {
1141 pub algo_id: String,
1143 #[serde(default)]
1145 pub algo_cl_ord_id: String,
1146 #[serde(default)]
1148 pub cl_ord_id: String,
1149 #[serde(default)]
1151 pub ord_id: String,
1152 #[serde(default)]
1154 pub ord_id_list: Vec<String>,
1155 #[serde(default)]
1157 pub sub_algo_id_list: Vec<String>,
1158 pub inst_id: Ustr,
1160 pub inst_type: OKXInstrumentType,
1162 pub ord_type: OKXAlgoOrderType,
1164 pub state: OKXAlgoOrderStatus,
1166 pub side: OKXSide,
1168 pub pos_side: OKXPositionSide,
1170 #[serde(default)]
1172 pub sz: String,
1173 #[serde(default)]
1175 pub trigger_px: String,
1176 #[serde(default)]
1178 pub trigger_px_type: Option<OKXTriggerType>,
1179 #[serde(default)]
1181 pub sl_trigger_px: String,
1182 #[serde(default)]
1184 pub sl_ord_px: String,
1185 #[serde(default)]
1187 pub sl_trigger_px_type: Option<OKXTriggerType>,
1188 #[serde(default)]
1190 pub tp_trigger_px: String,
1191 #[serde(default)]
1193 pub tp_ord_px: String,
1194 #[serde(default)]
1196 pub tp_trigger_px_type: Option<OKXTriggerType>,
1197 #[serde(default)]
1199 pub ord_px: String,
1200 pub td_mode: OKXTradeMode,
1202 #[serde(default)]
1204 pub lever: String,
1205 #[serde(default)]
1207 pub reduce_only: String,
1208 #[serde(default)]
1210 pub close_fraction: String,
1211 #[serde(default)]
1213 pub actual_px: String,
1214 #[serde(default)]
1216 pub actual_sz: String,
1217 #[serde(default)]
1219 pub notional_usd: String,
1220 #[serde(deserialize_with = "deserialize_string_to_u64")]
1222 pub c_time: u64,
1223 #[serde(deserialize_with = "deserialize_string_to_u64")]
1225 pub u_time: u64,
1226 #[serde(default)]
1228 pub trigger_time: String,
1229 #[serde(default)]
1231 pub tag: String,
1232 #[serde(default)]
1234 pub callback_ratio: String,
1235 #[serde(default)]
1237 pub callback_spread: String,
1238 #[serde(default)]
1240 pub active_px: String,
1241}
1242
1243#[derive(Clone, Debug, Serialize, Deserialize)]
1245#[serde(rename_all = "camelCase")]
1246pub struct OKXTransactionDetail {
1247 pub inst_type: OKXInstrumentType,
1249 pub inst_id: Ustr,
1251 pub trade_id: Ustr,
1253 pub ord_id: Ustr,
1255 pub cl_ord_id: Ustr,
1257 pub bill_id: Ustr,
1259 pub fill_px: String,
1261 pub fill_sz: String,
1263 pub side: OKXSide,
1265 pub exec_type: OKXExecType,
1267 pub fee_ccy: String,
1269 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1271 pub fee: Option<String>,
1272 #[serde(deserialize_with = "deserialize_string_to_u64")]
1274 pub ts: u64,
1275}
1276
1277#[derive(Clone, Debug, Serialize, Deserialize)]
1279#[serde(rename_all = "camelCase")]
1280pub struct OKXPositionHistory {
1281 pub inst_type: OKXInstrumentType,
1283 pub inst_id: Ustr,
1285 pub mgn_mode: OKXMarginMode,
1287 #[serde(rename = "type")]
1290 pub r#type: Ustr,
1291 pub c_time: String,
1293 #[serde(deserialize_with = "deserialize_string_to_u64")]
1295 pub u_time: u64,
1296 pub open_avg_px: String,
1298 #[serde(skip_serializing_if = "Option::is_none")]
1300 pub close_avg_px: Option<String>,
1301 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1303 pub pos_id: Option<Ustr>,
1304 #[serde(skip_serializing_if = "Option::is_none")]
1306 pub open_max_pos: Option<String>,
1307 #[serde(skip_serializing_if = "Option::is_none")]
1309 pub close_total_pos: Option<String>,
1310 #[serde(skip_serializing_if = "Option::is_none")]
1312 pub realized_pnl: Option<String>,
1313 #[serde(skip_serializing_if = "Option::is_none")]
1315 pub fee: Option<String>,
1316 #[serde(skip_serializing_if = "Option::is_none")]
1318 pub funding_fee: Option<String>,
1319 #[serde(skip_serializing_if = "Option::is_none")]
1321 pub liq_penalty: Option<String>,
1322 #[serde(skip_serializing_if = "Option::is_none")]
1324 pub pnl: Option<String>,
1325 #[serde(skip_serializing_if = "Option::is_none")]
1327 pub pnl_ratio: Option<String>,
1328 pub pos_side: OKXPositionSide,
1330 pub lever: String,
1332 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub direction: Option<String>,
1335 #[serde(skip_serializing_if = "Option::is_none")]
1337 pub trigger_px: Option<String>,
1338 #[serde(skip_serializing_if = "Option::is_none")]
1340 pub uly: Option<String>,
1341 #[serde(skip_serializing_if = "Option::is_none")]
1343 pub ccy: Option<String>,
1344}
1345
1346#[derive(Clone, Debug, Serialize, Deserialize)]
1348#[serde(rename_all = "camelCase")]
1349pub struct OKXPlaceAlgoOrderRequest {
1350 #[serde(rename = "instId")]
1352 pub inst_id: String,
1353 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1355 pub inst_id_code: Option<u64>,
1356 #[serde(rename = "tdMode")]
1358 pub td_mode: OKXTradeMode,
1359 pub side: OKXSide,
1361 #[serde(rename = "ordType")]
1363 pub ord_type: OKXAlgoOrderType,
1364 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub sz: Option<String>,
1367 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1369 pub algo_cl_ord_id: Option<String>,
1370 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1372 pub trigger_px: Option<String>,
1373 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1375 pub order_px: Option<String>,
1376 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1378 pub trigger_px_type: Option<OKXTriggerType>,
1379 #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1381 pub sl_trigger_px: Option<String>,
1382 #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1384 pub sl_ord_px: Option<String>,
1385 #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1387 pub sl_trigger_px_type: Option<OKXTriggerType>,
1388 #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1390 pub tp_trigger_px: Option<String>,
1391 #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1393 pub tp_ord_px: Option<String>,
1394 #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1396 pub tp_trigger_px_type: Option<OKXTriggerType>,
1397 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1399 pub tgt_ccy: Option<OKXTargetCurrency>,
1400 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1402 pub pos_side: Option<OKXPositionSide>,
1403 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1405 pub close_position: Option<bool>,
1406 #[serde(skip_serializing_if = "Option::is_none")]
1408 pub tag: Option<String>,
1409 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1411 pub reduce_only: Option<bool>,
1412 #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1414 pub close_fraction: Option<String>,
1415 #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1418 pub callback_ratio: Option<String>,
1419 #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1422 pub callback_spread: Option<String>,
1423 #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1426 pub active_px: Option<String>,
1427}
1428
1429#[derive(Clone, Debug, Serialize, Deserialize)]
1431#[serde(rename_all = "camelCase")]
1432pub struct OKXPlaceAlgoOrderResponse {
1433 pub algo_id: String,
1435 #[serde(skip_serializing_if = "Option::is_none")]
1437 pub algo_cl_ord_id: Option<String>,
1438 #[serde(skip_serializing_if = "Option::is_none")]
1440 pub s_code: Option<String>,
1441 #[serde(skip_serializing_if = "Option::is_none")]
1443 pub s_msg: Option<String>,
1444 #[serde(skip_serializing_if = "Option::is_none")]
1446 pub req_id: Option<String>,
1447}
1448
1449#[derive(Clone, Debug, Serialize, Deserialize)]
1451#[serde(rename_all = "camelCase")]
1452pub struct OKXCancelAlgoOrderRequest {
1453 pub inst_id: String,
1455 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1457 pub inst_id_code: Option<u64>,
1458 #[serde(skip_serializing_if = "Option::is_none")]
1460 pub algo_id: Option<String>,
1461 #[serde(skip_serializing_if = "Option::is_none")]
1463 pub algo_cl_ord_id: Option<String>,
1464}
1465
1466#[derive(Clone, Debug, Serialize, Deserialize)]
1468#[serde(rename_all = "camelCase")]
1469pub struct OKXCancelAlgoOrderResponse {
1470 pub algo_id: String,
1472 #[serde(skip_serializing_if = "Option::is_none")]
1474 pub s_code: Option<String>,
1475 #[serde(skip_serializing_if = "Option::is_none")]
1477 pub s_msg: Option<String>,
1478}
1479
1480#[derive(Clone, Debug, Serialize, Deserialize)]
1482#[serde(rename_all = "camelCase")]
1483pub struct OKXAmendAlgoOrderRequest {
1484 pub inst_id: String,
1486 pub algo_id: String,
1488 #[serde(skip_serializing_if = "Option::is_none")]
1490 pub algo_cl_ord_id: Option<String>,
1491 #[serde(skip_serializing_if = "Option::is_none")]
1493 pub new_sz: Option<String>,
1494 #[serde(skip_serializing_if = "Option::is_none")]
1496 pub new_trigger_px: Option<String>,
1497 #[serde(skip_serializing_if = "Option::is_none")]
1499 pub new_tp_trigger_px: Option<String>,
1500 #[serde(skip_serializing_if = "Option::is_none")]
1502 pub new_tp_ord_px: Option<String>,
1503 #[serde(skip_serializing_if = "Option::is_none")]
1505 pub new_tp_trigger_px_type: Option<String>,
1506 #[serde(skip_serializing_if = "Option::is_none")]
1510 pub new_sl_trigger_px: Option<String>,
1511 #[serde(skip_serializing_if = "Option::is_none")]
1513 pub new_sl_ord_px: Option<String>,
1514 #[serde(skip_serializing_if = "Option::is_none")]
1516 pub new_sl_trigger_px_type: Option<String>,
1517 #[serde(skip_serializing_if = "Option::is_none")]
1519 pub new_order_px: Option<String>,
1520 #[serde(skip_serializing_if = "Option::is_none")]
1522 pub new_callback_ratio: Option<String>,
1523 #[serde(skip_serializing_if = "Option::is_none")]
1525 pub new_callback_spread: Option<String>,
1526 #[serde(skip_serializing_if = "Option::is_none")]
1528 pub new_active_px: Option<String>,
1529}
1530
1531#[derive(Clone, Debug, Serialize, Deserialize)]
1533#[serde(rename_all = "camelCase")]
1534pub struct OKXAmendAlgoOrderResponse {
1535 pub algo_id: String,
1537 #[serde(skip_serializing_if = "Option::is_none")]
1539 pub algo_cl_ord_id: Option<String>,
1540 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub s_code: Option<String>,
1543 #[serde(skip_serializing_if = "Option::is_none")]
1545 pub s_msg: Option<String>,
1546 #[serde(skip_serializing_if = "Option::is_none")]
1548 pub req_id: Option<String>,
1549}
1550
1551#[derive(Clone, Debug, Serialize, Deserialize)]
1553#[serde(rename_all = "camelCase")]
1554pub struct OKXServerTime {
1555 #[serde(deserialize_with = "deserialize_string_to_u64")]
1557 pub ts: u64,
1558}
1559
1560#[derive(Clone, Debug, Serialize, Deserialize)]
1562#[serde(rename_all = "camelCase")]
1563pub struct OKXFeeRate {
1564 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1566 pub level: OKXVipLevel,
1567 pub taker: String,
1569 pub maker: String,
1571 pub taker_u: String,
1573 pub maker_u: String,
1575 #[serde(
1577 default,
1578 alias = "elpMaker",
1579 deserialize_with = "deserialize_optional_decimal"
1580 )]
1581 pub rpi_maker: Option<Decimal>,
1582 #[serde(default)]
1584 pub delivery: String,
1585 #[serde(default)]
1587 pub exercise: String,
1588 #[serde(default)]
1590 pub settle: String,
1591 pub inst_type: OKXInstrumentType,
1593 #[serde(default)]
1595 pub category: String,
1596 #[serde(deserialize_with = "deserialize_string_to_u64")]
1598 pub ts: u64,
1599}
1600
1601#[cfg(test)]
1602mod tests {
1603 use rstest::rstest;
1604 use serde_json;
1605
1606 use super::*;
1607
1608 #[rstest]
1609 fn test_algo_order_deserializes_current_child_identifier_lists() {
1610 let order: OKXOrderAlgo = serde_json::from_value(serde_json::json!({
1611 "algoId": "123",
1612 "algoClOrdId": "algo-client-1",
1613 "ordId": "456",
1614 "ordIdList": ["456", "457"],
1615 "subAlgoIdList": ["789"],
1616 "instId": "ETH-USDT-SWAP",
1617 "instType": "SWAP",
1618 "ordType": "conditional",
1619 "state": "effective",
1620 "side": "sell",
1621 "posSide": "net",
1622 "tdMode": "cross",
1623 "cTime": "1700000000000",
1624 "uTime": "1700000001000"
1625 }))
1626 .unwrap();
1627
1628 assert_eq!(order.ord_id, "456");
1629 assert_eq!(order.ord_id_list, ["456", "457"]);
1630 assert_eq!(order.sub_algo_id_list, ["789"]);
1631 }
1632
1633 #[rstest]
1634 fn test_algo_order_request_serialization() {
1635 let request = OKXPlaceAlgoOrderRequest {
1636 inst_id: "ETH-USDT-SWAP".to_string(),
1637 inst_id_code: None,
1638 td_mode: OKXTradeMode::Isolated,
1639 side: OKXSide::Buy,
1640 ord_type: OKXAlgoOrderType::Trigger,
1641 sz: Some("0.01".to_string()),
1642 algo_cl_ord_id: Some("test123".to_string()),
1643 trigger_px: Some("3000".to_string()),
1644 order_px: Some("-1".to_string()),
1645 trigger_px_type: Some(OKXTriggerType::Last),
1646 sl_trigger_px: None,
1647 sl_ord_px: None,
1648 sl_trigger_px_type: None,
1649 tp_trigger_px: None,
1650 tp_ord_px: None,
1651 tp_trigger_px_type: None,
1652 tgt_ccy: None,
1653 pos_side: None,
1654 close_position: None,
1655 tag: None,
1656 reduce_only: None,
1657 close_fraction: None,
1658 callback_ratio: None,
1659 callback_spread: None,
1660 active_px: None,
1661 };
1662
1663 let json = serde_json::to_string(&request).unwrap();
1664
1665 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1667 assert!(json.contains("\"tdMode\":\"isolated\""));
1668 assert!(json.contains("\"ordType\":\"trigger\""));
1669 assert!(json.contains("\"algoClOrdId\":\"test123\""));
1670 assert!(json.contains("\"triggerPx\":\"3000\""));
1671 assert!(json.contains("\"orderPx\":\"-1\""));
1672 assert!(json.contains("\"triggerPxType\":\"last\""));
1673
1674 assert!(!json.contains("tgtCcy"));
1676 assert!(!json.contains("posSide"));
1677 assert!(!json.contains("closePosition"));
1678 assert!(!json.contains("closeFraction"));
1679 }
1680
1681 #[rstest]
1682 fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1683 let request = OKXAmendAlgoOrderRequest {
1687 inst_id: "ETH-USDT-SWAP".to_string(),
1688 algo_id: "123".to_string(),
1689 algo_cl_ord_id: None,
1690 new_sz: None,
1691 new_trigger_px: None,
1692 new_tp_trigger_px: None,
1693 new_tp_ord_px: None,
1694 new_tp_trigger_px_type: None,
1695 new_sl_trigger_px: Some("850".to_string()),
1696 new_sl_ord_px: None,
1697 new_sl_trigger_px_type: None,
1698 new_order_px: None,
1699 new_callback_ratio: None,
1700 new_callback_spread: None,
1701 new_active_px: None,
1702 };
1703
1704 let json = serde_json::to_string(&request).unwrap();
1705
1706 assert!(json.contains("\"algoId\":\"123\""));
1707 assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1708 assert!(!json.contains("newTriggerPx"));
1710 assert!(!json.contains("newSz"));
1711 assert!(!json.contains("algoClOrdId"));
1712 }
1713
1714 #[rstest]
1715 fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1716 let request = OKXAmendAlgoOrderRequest {
1717 inst_id: "DOGE-USDT-SWAP".to_string(),
1718 algo_id: "algo-oco-1".to_string(),
1719 algo_cl_ord_id: None,
1720 new_sz: None,
1721 new_trigger_px: None,
1722 new_tp_trigger_px: Some("0.10495".to_string()),
1723 new_tp_ord_px: Some("-1".to_string()),
1724 new_tp_trigger_px_type: Some("last".to_string()),
1725 new_sl_trigger_px: Some("0.06297".to_string()),
1726 new_sl_ord_px: Some("-1".to_string()),
1727 new_sl_trigger_px_type: Some("last".to_string()),
1728 new_order_px: None,
1729 new_callback_ratio: None,
1730 new_callback_spread: None,
1731 new_active_px: None,
1732 };
1733
1734 let json = serde_json::to_string(&request).unwrap();
1735
1736 assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1737 assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1738 assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1739 assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1740 assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1741 assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1742 assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1743 assert!(!json.contains("newTriggerPx"));
1744 }
1745
1746 #[rstest]
1747 fn test_algo_order_request_serializes_close_fraction() {
1748 let request = OKXPlaceAlgoOrderRequest {
1749 inst_id: "ETH-USDT-SWAP".to_string(),
1750 inst_id_code: None,
1751 td_mode: OKXTradeMode::Cross,
1752 side: OKXSide::Sell,
1753 ord_type: OKXAlgoOrderType::Conditional,
1754 sz: None,
1755 algo_cl_ord_id: Some("close-frac-123".to_string()),
1756 trigger_px: None,
1757 order_px: None,
1758 trigger_px_type: None,
1759 sl_trigger_px: Some("3000".to_string()),
1760 sl_ord_px: Some("-1".to_string()),
1761 sl_trigger_px_type: Some(OKXTriggerType::Last),
1762 tp_trigger_px: None,
1763 tp_ord_px: None,
1764 tp_trigger_px_type: None,
1765 tgt_ccy: None,
1766 pos_side: Some(OKXPositionSide::Net),
1767 close_position: None,
1768 tag: None,
1769 reduce_only: Some(true),
1770 close_fraction: Some("1".to_string()),
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("\"ordType\":\"conditional\""));
1779 assert!(json.contains("\"closeFraction\":\"1\""));
1780 assert!(json.contains("\"slTriggerPx\":\"3000\""));
1781 assert!(json.contains("\"slOrdPx\":\"-1\""));
1782 assert!(json.contains("\"slTriggerPxType\":\"last\""));
1783 assert!(json.contains("\"reduceOnly\":true"));
1784 assert!(!json.contains("\"sz\""));
1785 assert!(!json.contains("triggerPx"));
1786 }
1787
1788 #[rstest]
1789 fn test_algo_order_request_array_serialization() {
1790 let request = OKXPlaceAlgoOrderRequest {
1791 inst_id: "BTC-USDT".to_string(),
1792 inst_id_code: Some(10459),
1793 td_mode: OKXTradeMode::Cross,
1794 side: OKXSide::Sell,
1795 ord_type: OKXAlgoOrderType::Trigger,
1796 sz: Some("0.1".to_string()),
1797 algo_cl_ord_id: None,
1798 trigger_px: Some("50000".to_string()),
1799 order_px: Some("49900".to_string()),
1800 trigger_px_type: Some(OKXTriggerType::Mark),
1801 sl_trigger_px: None,
1802 sl_ord_px: None,
1803 sl_trigger_px_type: None,
1804 tp_trigger_px: None,
1805 tp_ord_px: None,
1806 tp_trigger_px_type: None,
1807 tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1808 pos_side: Some(OKXPositionSide::Net),
1809 close_position: None,
1810 tag: None,
1811 reduce_only: Some(true),
1812 close_fraction: None,
1813 callback_ratio: None,
1814 callback_spread: None,
1815 active_px: None,
1816 };
1817
1818 let json = serde_json::to_string(&[request]).unwrap();
1820
1821 assert!(json.starts_with('['));
1823 assert!(json.ends_with(']'));
1824
1825 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1827 assert!(json.contains("\"tdMode\":\"cross\""));
1828 assert!(json.contains("\"triggerPx\":\"50000\""));
1829 assert!(json.contains("\"orderPx\":\"49900\""));
1830 assert!(json.contains("\"triggerPxType\":\"mark\""));
1831 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1832 assert!(json.contains("\"posSide\":\"net\""));
1833 assert!(json.contains("\"reduceOnly\":true"));
1834 }
1835
1836 #[rstest]
1837 fn test_cancel_algo_order_request_serialization() {
1838 let request = OKXCancelAlgoOrderRequest {
1839 inst_id: "ETH-USDT-SWAP".to_string(),
1840 inst_id_code: None,
1841 algo_id: Some("123456".to_string()),
1842 algo_cl_ord_id: None,
1843 };
1844
1845 let json = serde_json::to_string(&request).unwrap();
1846
1847 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1849 assert!(json.contains("\"algoId\":\"123456\""));
1850 assert!(!json.contains("algoClOrdId"));
1851 }
1852
1853 #[rstest]
1854 fn test_cancel_algo_order_with_client_id_serialization() {
1855 let request = OKXCancelAlgoOrderRequest {
1856 inst_id: "BTC-USDT".to_string(),
1857 inst_id_code: Some(10459),
1858 algo_id: None,
1859 algo_cl_ord_id: Some("client123".to_string()),
1860 };
1861
1862 let json = serde_json::to_string(&[request]).unwrap();
1864
1865 assert!(json.starts_with('['));
1867 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1868 assert!(json.contains("\"algoClOrdId\":\"client123\""));
1869 assert!(!json.contains("\"algoId\""));
1870 }
1871
1872 #[rstest]
1873 fn test_amend_algo_order_trigger_serialization() {
1874 let request = OKXAmendAlgoOrderRequest {
1875 inst_id: "ETH-USDT-SWAP".to_string(),
1876 algo_id: "123456".to_string(),
1877 algo_cl_ord_id: None,
1878 new_sz: None,
1879 new_trigger_px: Some("3500".to_string()),
1880 new_tp_trigger_px: None,
1881 new_tp_ord_px: None,
1882 new_tp_trigger_px_type: None,
1883 new_sl_trigger_px: None,
1884 new_sl_ord_px: None,
1885 new_sl_trigger_px_type: None,
1886 new_order_px: Some("3490".to_string()),
1887 new_callback_ratio: None,
1888 new_callback_spread: None,
1889 new_active_px: None,
1890 };
1891
1892 let json = serde_json::to_string(&request).unwrap();
1893
1894 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1895 assert!(json.contains("\"algoId\":\"123456\""));
1896 assert!(json.contains("\"newTriggerPx\":\"3500\""));
1897 assert!(json.contains("\"newOrderPx\":\"3490\""));
1898 assert!(!json.contains("newSz"));
1899 assert!(!json.contains("algoClOrdId"));
1900 assert!(!json.contains("newCallbackRatio"));
1901 }
1902
1903 #[rstest]
1904 fn test_amend_algo_order_trailing_stop_serialization() {
1905 let request = OKXAmendAlgoOrderRequest {
1906 inst_id: "BTC-USDT-SWAP".to_string(),
1907 algo_id: "789012".to_string(),
1908 algo_cl_ord_id: Some("client456".to_string()),
1909 new_sz: Some("0.1".to_string()),
1910 new_trigger_px: None,
1911 new_tp_trigger_px: None,
1912 new_tp_ord_px: None,
1913 new_tp_trigger_px_type: None,
1914 new_sl_trigger_px: None,
1915 new_sl_ord_px: None,
1916 new_sl_trigger_px_type: None,
1917 new_order_px: None,
1918 new_callback_ratio: Some("0.02".to_string()),
1919 new_callback_spread: None,
1920 new_active_px: Some("50000".to_string()),
1921 };
1922
1923 let json = serde_json::to_string(&request).unwrap();
1924
1925 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1926 assert!(json.contains("\"algoId\":\"789012\""));
1927 assert!(json.contains("\"algoClOrdId\":\"client456\""));
1928 assert!(json.contains("\"newSz\":\"0.1\""));
1929 assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1930 assert!(json.contains("\"newActivePx\":\"50000\""));
1931 assert!(!json.contains("newTriggerPx"));
1932 assert!(!json.contains("newOrderPx"));
1933 }
1934
1935 #[rstest]
1936 fn test_trailing_stop_request_callback_ratio_serialization() {
1937 let request = OKXPlaceAlgoOrderRequest {
1938 inst_id: "BTC-USDT-SWAP".to_string(),
1939 inst_id_code: None,
1940 td_mode: OKXTradeMode::Cross,
1941 side: OKXSide::Buy,
1942 ord_type: OKXAlgoOrderType::MoveOrderStop,
1943 sz: Some("0.1".to_string()),
1944 algo_cl_ord_id: Some("trail-001".to_string()),
1945 trigger_px: None,
1946 order_px: None,
1947 trigger_px_type: None,
1948 sl_trigger_px: None,
1949 sl_ord_px: None,
1950 sl_trigger_px_type: None,
1951 tp_trigger_px: None,
1952 tp_ord_px: None,
1953 tp_trigger_px_type: None,
1954 tgt_ccy: None,
1955 pos_side: None,
1956 close_position: None,
1957 tag: None,
1958 reduce_only: None,
1959 close_fraction: None,
1960 callback_ratio: Some("0.01".to_string()),
1961 callback_spread: None,
1962 active_px: None,
1963 };
1964
1965 let json = serde_json::to_string(&request).unwrap();
1966
1967 assert!(json.contains("\"ordType\":\"move_order_stop\""));
1968 assert!(json.contains("\"callbackRatio\":\"0.01\""));
1969 assert!(!json.contains("callbackSpread"));
1970 assert!(!json.contains("activePx"));
1971 }
1972
1973 #[rstest]
1974 fn test_trailing_stop_request_callback_spread_serialization() {
1975 let request = OKXPlaceAlgoOrderRequest {
1976 inst_id: "ETH-USDT-SWAP".to_string(),
1977 inst_id_code: None,
1978 td_mode: OKXTradeMode::Isolated,
1979 side: OKXSide::Sell,
1980 ord_type: OKXAlgoOrderType::MoveOrderStop,
1981 sz: Some("1.0".to_string()),
1982 algo_cl_ord_id: None,
1983 trigger_px: None,
1984 order_px: None,
1985 trigger_px_type: None,
1986 sl_trigger_px: None,
1987 sl_ord_px: None,
1988 sl_trigger_px_type: None,
1989 tp_trigger_px: None,
1990 tp_ord_px: None,
1991 tp_trigger_px_type: None,
1992 tgt_ccy: None,
1993 pos_side: None,
1994 close_position: None,
1995 tag: None,
1996 reduce_only: Some(true),
1997 close_fraction: None,
1998 callback_ratio: None,
1999 callback_spread: Some("50.5".to_string()),
2000 active_px: None,
2001 };
2002
2003 let json = serde_json::to_string(&request).unwrap();
2004
2005 assert!(json.contains("\"callbackSpread\":\"50.5\""));
2006 assert!(!json.contains("callbackRatio"));
2007 assert!(!json.contains("activePx"));
2008 }
2009
2010 #[rstest]
2011 fn test_trailing_stop_request_with_activation_price_serialization() {
2012 let request = OKXPlaceAlgoOrderRequest {
2013 inst_id: "BTC-USDT-SWAP".to_string(),
2014 inst_id_code: None,
2015 td_mode: OKXTradeMode::Cross,
2016 side: OKXSide::Buy,
2017 ord_type: OKXAlgoOrderType::MoveOrderStop,
2018 sz: Some("0.5".to_string()),
2019 algo_cl_ord_id: None,
2020 trigger_px: None,
2021 order_px: None,
2022 trigger_px_type: None,
2023 sl_trigger_px: None,
2024 sl_ord_px: None,
2025 sl_trigger_px_type: None,
2026 tp_trigger_px: None,
2027 tp_ord_px: None,
2028 tp_trigger_px_type: None,
2029 tgt_ccy: None,
2030 pos_side: None,
2031 close_position: None,
2032 tag: None,
2033 reduce_only: None,
2034 close_fraction: None,
2035 callback_ratio: Some("0.005".to_string()),
2036 callback_spread: None,
2037 active_px: Some("65000".to_string()),
2038 };
2039
2040 let json = serde_json::to_string(&request).unwrap();
2041
2042 assert!(json.contains("\"callbackRatio\":\"0.005\""));
2043 assert!(json.contains("\"activePx\":\"65000\""));
2044 assert!(!json.contains("callbackSpread"));
2045 }
2046
2047 #[rstest]
2048 fn test_amend_algo_order_callback_spread_serialization() {
2049 let request = OKXAmendAlgoOrderRequest {
2050 inst_id: "ETH-USDT-SWAP".to_string(),
2051 algo_id: "456789".to_string(),
2052 algo_cl_ord_id: None,
2053 new_sz: None,
2054 new_trigger_px: None,
2055 new_tp_trigger_px: None,
2056 new_tp_ord_px: None,
2057 new_tp_trigger_px_type: None,
2058 new_sl_trigger_px: None,
2059 new_sl_ord_px: None,
2060 new_sl_trigger_px_type: None,
2061 new_order_px: None,
2062 new_callback_ratio: None,
2063 new_callback_spread: Some("25.0".to_string()),
2064 new_active_px: Some("4000".to_string()),
2065 };
2066
2067 let json = serde_json::to_string(&request).unwrap();
2068
2069 assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
2070 assert!(json.contains("\"newActivePx\":\"4000\""));
2071 assert!(!json.contains("newCallbackRatio"));
2072 assert!(!json.contains("newTriggerPx"));
2073 assert!(!json.contains("newSz"));
2074 }
2075
2076 #[rstest]
2077 fn test_amend_algo_order_size_only_serialization() {
2078 let request = OKXAmendAlgoOrderRequest {
2079 inst_id: "BTC-USDT-SWAP".to_string(),
2080 algo_id: "111222".to_string(),
2081 algo_cl_ord_id: None,
2082 new_sz: Some("0.5".to_string()),
2083 new_trigger_px: None,
2084 new_tp_trigger_px: None,
2085 new_tp_ord_px: None,
2086 new_tp_trigger_px_type: None,
2087 new_sl_trigger_px: None,
2088 new_sl_ord_px: None,
2089 new_sl_trigger_px_type: None,
2090 new_order_px: None,
2091 new_callback_ratio: None,
2092 new_callback_spread: None,
2093 new_active_px: None,
2094 };
2095
2096 let json = serde_json::to_string(&request).unwrap();
2097
2098 assert!(json.contains("\"newSz\":\"0.5\""));
2099 assert!(!json.contains("newTriggerPx"));
2100 assert!(!json.contains("newOrderPx"));
2101 assert!(!json.contains("newCallbackRatio"));
2102 assert!(!json.contains("newCallbackSpread"));
2103 assert!(!json.contains("newActivePx"));
2104 }
2105
2106 #[rstest]
2107 fn test_amend_algo_order_all_fields_serialization() {
2108 let request = OKXAmendAlgoOrderRequest {
2109 inst_id: "BTC-USDT-SWAP".to_string(),
2110 algo_id: "333444".to_string(),
2111 algo_cl_ord_id: Some("client789".to_string()),
2112 new_sz: Some("1.0".to_string()),
2113 new_trigger_px: Some("60000".to_string()),
2114 new_tp_trigger_px: None,
2115 new_tp_ord_px: None,
2116 new_tp_trigger_px_type: None,
2117 new_sl_trigger_px: None,
2118 new_sl_ord_px: None,
2119 new_sl_trigger_px_type: None,
2120 new_order_px: Some("59900".to_string()),
2121 new_callback_ratio: Some("0.015".to_string()),
2122 new_callback_spread: Some("100".to_string()),
2123 new_active_px: Some("62000".to_string()),
2124 };
2125
2126 let json = serde_json::to_string(&request).unwrap();
2127
2128 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2129 assert!(json.contains("\"algoId\":\"333444\""));
2130 assert!(json.contains("\"algoClOrdId\":\"client789\""));
2131 assert!(json.contains("\"newSz\":\"1.0\""));
2132 assert!(json.contains("\"newTriggerPx\":\"60000\""));
2133 assert!(json.contains("\"newOrderPx\":\"59900\""));
2134 assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2135 assert!(json.contains("\"newCallbackSpread\":\"100\""));
2136 assert!(json.contains("\"newActivePx\":\"62000\""));
2137 }
2138
2139 #[rstest]
2140 fn test_place_order_request_serializes_px_usd() {
2141 let request = OKXPlaceOrderRequest {
2142 inst_id: "BTC-USD-250328-50000-C".to_string(),
2143 td_mode: OKXTradeMode::Cross,
2144 ccy: None,
2145 cl_ord_id: Some("test-opt-1".to_string()),
2146 tag: None,
2147 side: OKXSide::Buy,
2148 pos_side: Some(OKXPositionSide::Net),
2149 ord_type: OKXOrderType::Limit,
2150 sz: "1".to_string(),
2151 px: None,
2152 px_usd: Some("100.5".to_string()),
2153 px_vol: None,
2154 reduce_only: None,
2155 tgt_ccy: None,
2156 attach_algo_ords: None,
2157 speed_bump: None,
2158 outcome: None,
2159 slippage_pct: None,
2160 rpi_taker_access: None,
2161 rpi_px_round: None,
2162 };
2163
2164 let json = serde_json::to_string(&request).unwrap();
2165 assert!(json.contains("\"pxUsd\":\"100.5\""));
2166 assert!(!json.contains("\"pxVol\""));
2167 assert!(!json.contains("\"px\":"));
2168 assert!(!json.contains("slippagePct"));
2169 }
2170
2171 #[rstest]
2172 fn test_place_order_request_serializes_px_vol() {
2173 let request = OKXPlaceOrderRequest {
2174 inst_id: "BTC-USD-250328-50000-C".to_string(),
2175 td_mode: OKXTradeMode::Cross,
2176 ccy: None,
2177 cl_ord_id: Some("test-opt-2".to_string()),
2178 tag: None,
2179 side: OKXSide::Buy,
2180 pos_side: Some(OKXPositionSide::Net),
2181 ord_type: OKXOrderType::Limit,
2182 sz: "1".to_string(),
2183 px: None,
2184 px_usd: None,
2185 px_vol: Some("0.55".to_string()),
2186 reduce_only: None,
2187 tgt_ccy: None,
2188 attach_algo_ords: None,
2189 speed_bump: None,
2190 outcome: None,
2191 slippage_pct: None,
2192 rpi_taker_access: None,
2193 rpi_px_round: None,
2194 };
2195
2196 let json = serde_json::to_string(&request).unwrap();
2197 assert!(json.contains("\"pxVol\":\"0.55\""));
2198 assert!(!json.contains("\"pxUsd\""));
2199 assert!(!json.contains("\"px\":"));
2200 }
2201
2202 #[rstest]
2203 fn test_place_order_request_serializes_slippage_pct() {
2204 let request = OKXPlaceOrderRequest {
2205 inst_id: "BTC-USDT-SWAP".to_string(),
2206 td_mode: OKXTradeMode::Cross,
2207 ccy: None,
2208 cl_ord_id: Some("mkt-slip-1".to_string()),
2209 tag: None,
2210 side: OKXSide::Buy,
2211 pos_side: Some(OKXPositionSide::Net),
2212 ord_type: OKXOrderType::Market,
2213 sz: "1".to_string(),
2214 px: None,
2215 px_usd: None,
2216 px_vol: None,
2217 reduce_only: None,
2218 tgt_ccy: None,
2219 attach_algo_ords: None,
2220 speed_bump: None,
2221 outcome: None,
2222 slippage_pct: Some("0.005".to_string()),
2223 rpi_taker_access: None,
2224 rpi_px_round: None,
2225 };
2226
2227 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2228 assert_eq!(json["slippagePct"], "0.005");
2229 }
2230
2231 #[rstest]
2232 fn test_rpi_place_order_request_serialization() {
2233 let request = OKXPlaceOrderRequest {
2234 inst_id: "OMI-USD".to_string(),
2235 td_mode: OKXTradeMode::Cash,
2236 ccy: None,
2237 cl_ord_id: Some("ORPI001".to_string()),
2238 tag: Some("rpi-test".to_string()),
2239 side: OKXSide::Sell,
2240 pos_side: None,
2241 ord_type: OKXOrderType::Rpi,
2242 sz: "250000".to_string(),
2243 px: Some("0.0001600".to_string()),
2244 px_usd: None,
2245 px_vol: None,
2246 reduce_only: Some(false),
2247 tgt_ccy: None,
2248 attach_algo_ords: None,
2249 speed_bump: None,
2250 outcome: None,
2251 slippage_pct: None,
2252 rpi_taker_access: Some(true),
2253 rpi_px_round: Some(false),
2254 };
2255
2256 let value = serde_json::to_value(request).unwrap();
2257
2258 assert_eq!(
2259 value,
2260 serde_json::json!({
2261 "instId": "OMI-USD",
2262 "tdMode": "cash",
2263 "clOrdId": "ORPI001",
2264 "tag": "rpi-test",
2265 "side": "sell",
2266 "ordType": "rpi",
2267 "sz": "250000",
2268 "px": "0.0001600",
2269 "reduceOnly": false,
2270 "rpiTakerAccess": true,
2271 "rpiPxRound": false
2272 })
2273 );
2274 }
2275
2276 #[rstest]
2277 fn test_rpi_amend_order_request_serialization() {
2278 let request = OKXAmendOrderRequest {
2279 inst_id: "OMI-USD".to_string(),
2280 ord_id: Some("2500000000000000001".to_string()),
2281 cl_ord_id: None,
2282 req_id: Some("RPI-AMEND-1".to_string()),
2283 new_sz: Some("275000".to_string()),
2284 new_px: Some("0.0001599".to_string()),
2285 rpi_taker_access: Some(false),
2286 rpi_px_round: Some(true),
2287 };
2288
2289 let value = serde_json::to_value(request).unwrap();
2290
2291 assert_eq!(
2292 value,
2293 serde_json::json!({
2294 "instId": "OMI-USD",
2295 "ordId": "2500000000000000001",
2296 "reqId": "RPI-AMEND-1",
2297 "newSz": "275000",
2298 "newPx": "0.0001599",
2299 "rpiTakerAccess": false,
2300 "rpiPxRound": true
2301 })
2302 );
2303 }
2304
2305 #[rstest]
2306 fn test_rpi_place_order_response_parsing() {
2307 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2308 "ordId": "2500000000000000001",
2309 "clOrdId": "ORPI001",
2310 "tag": "rpi-test",
2311 "instId": "OMI-USD",
2312 "side": "sell",
2313 "ordType": "rpi",
2314 "sz": "250000",
2315 "state": "live",
2316 "px": "0.0001600",
2317 "avgPx": "",
2318 "sCode": "0",
2319 "sMsg": ""
2320 }))
2321 .unwrap();
2322
2323 assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
2324 assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
2325 assert_eq!(response.tag.as_deref(), Some("rpi-test"));
2326 assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
2327 assert_eq!(response.side, Some(OKXSide::Sell));
2328 assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
2329 assert_eq!(response.sz.as_deref(), Some("250000"));
2330 assert_eq!(response.state, Some(OKXOrderStatus::Live));
2331 assert_eq!(response.px.as_deref(), Some("0.0001600"));
2332 assert_eq!(response.avg_px.as_deref(), Some(""));
2333 assert_eq!(response.s_code.as_deref(), Some("0"));
2334 assert_eq!(response.s_msg.as_deref(), Some(""));
2335 }
2336
2337 #[rstest]
2338 fn test_event_contract_models_deserialize() {
2339 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2340 "seriesId": "BTC-ABOVE-DAILY",
2341 "freq": "daily",
2342 "title": "BTC above daily",
2343 "category": "1",
2344 "settlement": {
2345 "method": "cash",
2346 "closeEarly": false,
2347 "srcName": "OKX BTC/USD Index",
2348 "underlying": "BTC-USD"
2349 }
2350 }))
2351 .unwrap();
2352 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2353 "seriesId": "BTC-ABOVE-DAILY",
2354 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2355 "fixTime": "",
2356 "expTime": "1769697132335",
2357 "state": "live"
2358 }))
2359 .unwrap();
2360 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2361 "seriesId": "BTC-ABOVE-DAILY",
2362 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2363 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2364 "listTime": "1769697132335",
2365 "fixTime": "",
2366 "expTime": "1769697132335",
2367 "state": "live",
2368 "disputed": false,
2369 "outcome": "0",
2370 "floorStrike": "120000",
2371 "capStrike": "INF",
2372 "settleValue": "",
2373 "hitDir": ""
2374 }))
2375 .unwrap();
2376
2377 assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2378 assert_eq!(series.settlement.underlying, "BTC-USD");
2379 assert_eq!(event.fix_time, None);
2380 assert_eq!(event.exp_time, Some(1_769_697_132_335));
2381 assert_eq!(
2382 market.inst_id,
2383 Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2384 );
2385 assert_eq!(market.list_time, Some(1_769_697_132_335));
2386 assert_eq!(market.exp_time, Some(1_769_697_132_335));
2387 assert_eq!(market.outcome, "0");
2388 assert_eq!(market.cap_strike, "INF");
2389 assert_eq!(market.hit_dir, "");
2390
2391 let serialized = serde_json::to_value(&market).unwrap();
2392 assert_eq!(serialized["capStrike"], "INF");
2393 assert_eq!(serialized["hitDir"], "");
2394 }
2395
2396 #[rstest]
2397 fn test_event_contract_models_accept_missing_optional_fields() {
2398 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2399 "seriesId": "BTC-ABOVE-DAILY"
2400 }))
2401 .unwrap();
2402 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2403 "seriesId": "BTC-ABOVE-DAILY",
2404 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2405 "fixTime": "",
2406 "expTime": "",
2407 "state": "live"
2408 }))
2409 .unwrap();
2410 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2411 "seriesId": "BTC-ABOVE-DAILY",
2412 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2413 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2414 "listTime": "",
2415 "fixTime": "",
2416 "expTime": "",
2417 "state": "live",
2418 "disputed": false,
2419 "outcome": "0",
2420 "floorStrike": "120000",
2421 "settleValue": ""
2422 }))
2423 .unwrap();
2424
2425 assert_eq!(series.freq, "");
2426 assert_eq!(series.settlement.underlying, "");
2427 assert_eq!(event.exp_time, None);
2428 assert_eq!(market.list_time, None);
2429 assert_eq!(market.exp_time, None);
2430 assert_eq!(market.cap_strike, "");
2431 assert_eq!(market.hit_dir, "");
2432 }
2433
2434 #[rstest]
2435 fn test_place_order_request_serializes_event_contract_fields() {
2436 let request = OKXPlaceOrderRequest {
2437 inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2438 td_mode: OKXTradeMode::Cash,
2439 ccy: None,
2440 cl_ord_id: Some("event-1".to_string()),
2441 tag: None,
2442 side: OKXSide::Buy,
2443 pos_side: None,
2444 ord_type: OKXOrderType::Limit,
2445 sz: "10".to_string(),
2446 px: Some("0.42".to_string()),
2447 px_usd: None,
2448 px_vol: None,
2449 reduce_only: None,
2450 tgt_ccy: None,
2451 attach_algo_ords: None,
2452 speed_bump: Some("1".to_string()),
2453 outcome: Some("yes".to_string()),
2454 slippage_pct: None,
2455 rpi_taker_access: None,
2456 rpi_px_round: None,
2457 };
2458
2459 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2460
2461 assert_eq!(json["speedBump"], "1");
2462 assert_eq!(json["outcome"], "yes");
2463 }
2464
2465 #[rstest]
2466 fn test_attach_algo_ord_request_serializes_trailing_fields() {
2467 let request = OKXAttachAlgoOrdRequest {
2468 attach_algo_cl_ord_id: Some("trail-1".to_string()),
2469 sl_trigger_px: None,
2470 sl_ord_px: None,
2471 sl_trigger_px_type: None,
2472 tp_trigger_px: None,
2473 tp_ord_px: None,
2474 tp_trigger_px_type: None,
2475 callback_ratio: Some("0.01".to_string()),
2476 callback_spread: None,
2477 active_px: Some("64000".to_string()),
2478 new_callback_ratio: Some("0.02".to_string()),
2479 new_callback_spread: Some("25".to_string()),
2480 new_active_px: Some("65000".to_string()),
2481 };
2482
2483 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2484
2485 assert_eq!(json["callbackRatio"], "0.01");
2486 assert_eq!(json["activePx"], "64000");
2487 assert_eq!(json["newCallbackRatio"], "0.02");
2488 assert_eq!(json["newCallbackSpread"], "25");
2489 assert_eq!(json["newActivePx"], "65000");
2490 assert!(json.get("callbackSpread").is_none());
2491 }
2492
2493 #[rstest]
2494 fn test_place_order_response_deserializes_sub_code() {
2495 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2496 "ordId": "",
2497 "clOrdId": "event-1",
2498 "sCode": "51000",
2499 "sMsg": "Parameter error",
2500 "subCode": "51004"
2501 }))
2502 .unwrap();
2503
2504 assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2505 assert_eq!(response.s_code, Some("51000".to_string()));
2506 assert_eq!(response.sub_code, Some("51004".to_string()));
2507 }
2508
2509 #[rstest]
2510 fn test_fee_rate_deserializes_settle() {
2511 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2512 "level": "VIP1",
2513 "taker": "-0.0005",
2514 "maker": "-0.0002",
2515 "takerU": "-0.0005",
2516 "makerU": "-0.0002",
2517 "settle": "-0.001",
2518 "instType": "EVENTS",
2519 "category": "1",
2520 "ts": "1769697132335"
2521 }))
2522 .unwrap();
2523
2524 assert_eq!(fee_rate.settle, "-0.001");
2525 assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2526 }
2527
2528 #[rstest]
2529 fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
2530 let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
2531 "level": "VIP1",
2532 "taker": "-0.0005",
2533 "maker": "-0.0002",
2534 "takerU": "-0.0005",
2535 "makerU": "-0.0002",
2536 "rpiMaker": "-0.00015",
2537 "instType": "SPOT",
2538 "category": "1",
2539 "ts": "1785406500000"
2540 }))
2541 .unwrap();
2542 let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
2543 "level": "VIP1",
2544 "taker": "-0.0005",
2545 "maker": "-0.0002",
2546 "takerU": "-0.0005",
2547 "makerU": "-0.0002",
2548 "elpMaker": "-0.00016",
2549 "instType": "SPOT",
2550 "category": "1",
2551 "ts": "1785406500001"
2552 }))
2553 .unwrap();
2554 let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
2555 "level": "VIP1",
2556 "taker": "-0.0005",
2557 "maker": "-0.0002",
2558 "takerU": "-0.0005",
2559 "makerU": "-0.0002",
2560 "rpiMaker": "",
2561 "instType": "OPTION",
2562 "category": "1",
2563 "ts": "1785406500002"
2564 }))
2565 .unwrap();
2566
2567 assert_eq!(
2568 rpi.rpi_maker,
2569 Some(Decimal::from_str_exact("-0.00015").unwrap())
2570 );
2571 assert_eq!(
2572 legacy.rpi_maker,
2573 Some(Decimal::from_str_exact("-0.00016").unwrap())
2574 );
2575 assert_eq!(not_applicable.rpi_maker, None);
2576 }
2577}