1use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::parse::{
22 deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
23 deserialize_optional_string_to_u64, deserialize_target_currency_as_none,
24};
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct OKXTrade {
30 pub inst_id: Ustr,
32 pub px: String,
34 pub sz: String,
36 pub side: OKXSide,
38 pub trade_id: Ustr,
40 #[serde(deserialize_with = "deserialize_string_to_u64")]
42 pub ts: u64,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct OKXCandlestick(
49 pub String,
51 pub String,
53 pub String,
55 pub String,
57 pub String,
59 pub String,
61 pub String,
63 pub String,
65 pub String,
67);
68
69use crate::common::{
70 enums::{
71 OKXAlgoOrderStatus, OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode,
72 OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXSpreadState,
73 OKXSpreadType, OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
74 },
75 parse::deserialize_string_to_u64,
76};
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct OKXMarkPrice {
82 pub uly: Option<Ustr>,
84 pub inst_id: Ustr,
86 pub mark_px: String,
88 #[serde(deserialize_with = "deserialize_string_to_u64")]
90 pub ts: u64,
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct OKXOptionSummary {
97 pub inst_type: OKXInstrumentType,
99 pub inst_id: Ustr,
101 pub uly: Ustr,
103 pub bid_vol: String,
105 pub ask_vol: String,
107 pub mark_vol: String,
109 pub fwd_px: String,
111 #[serde(deserialize_with = "deserialize_string_to_u64")]
113 pub ts: u64,
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct OKXSpread {
120 pub sprd_id: Ustr,
122 pub sprd_type: OKXSpreadType,
124 pub state: OKXSpreadState,
126 pub base_ccy: Ustr,
128 pub sz_ccy: Ustr,
130 pub quote_ccy: Ustr,
132 pub tick_sz: String,
134 pub min_sz: String,
136 pub lot_sz: String,
138 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
140 pub list_time: Option<u64>,
141 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
143 pub exp_time: Option<u64>,
144 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
146 pub u_time: Option<u64>,
147 pub legs: Vec<OKXSpreadLeg>,
149}
150
151#[derive(Clone, Debug, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct OKXSpreadLeg {
155 pub inst_id: Ustr,
157 pub side: OKXSide,
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct OKXPlaceSpreadOrderRequest {
165 pub sprd_id: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub cl_ord_id: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub tag: Option<String>,
173 pub side: OKXSide,
175 pub ord_type: OKXOrderType,
177 pub sz: String,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub px: Option<String>,
182}
183
184#[derive(Clone, Debug, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct OKXCancelSpreadOrderRequest {
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub ord_id: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub cl_ord_id: Option<String>,
194}
195
196#[derive(Clone, Debug, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct OKXCancelAllSpreadOrdersRequest {
200 pub sprd_id: String,
202}
203
204#[derive(Clone, Debug, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct OKXSpreadOrder {
208 pub sprd_id: Ustr,
210 pub ord_id: Ustr,
212 #[serde(default)]
214 pub cl_ord_id: Ustr,
215 #[serde(default)]
217 pub tag: String,
218 pub side: OKXSide,
220 pub ord_type: OKXOrderType,
222 pub sz: String,
224 #[serde(default)]
226 pub px: String,
227 #[serde(default)]
229 pub avg_px: String,
230 pub state: OKXOrderStatus,
232 #[serde(default)]
234 pub acc_fill_sz: String,
235 #[serde(default)]
237 pub pending_fill_sz: String,
238 #[serde(default)]
240 pub pending_settle_sz: String,
241 #[serde(default)]
243 pub canceled_sz: String,
244 #[serde(default)]
246 pub fill_sz: String,
247 #[serde(default)]
249 pub fill_px: String,
250 #[serde(default)]
252 pub trade_id: Ustr,
253 #[serde(default)]
255 pub cancel_source: String,
256 #[serde(default)]
258 pub req_id: String,
259 #[serde(default)]
261 pub amend_result: String,
262 #[serde(default)]
264 pub code: String,
265 #[serde(default)]
267 pub msg: String,
268 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
270 pub c_time: Option<u64>,
271 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
273 pub u_time: Option<u64>,
274}
275
276#[derive(Clone, Debug, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct OKXSpreadTrade {
280 pub sprd_id: Ustr,
282 pub trade_id: Ustr,
284 pub ord_id: Ustr,
286 #[serde(default)]
288 pub cl_ord_id: Ustr,
289 pub fill_px: String,
291 pub fill_sz: String,
293 pub side: OKXSide,
295 #[serde(default)]
297 pub exec_type: OKXExecType,
298 #[serde(default)]
300 pub fee_ccy: String,
301 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
303 pub fee: Option<String>,
304 #[serde(deserialize_with = "deserialize_string_to_u64")]
306 pub ts: u64,
307}
308
309#[derive(Clone, Debug, Default, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct OKXEventContractSettlement {
313 #[serde(default)]
315 pub method: String,
316 #[serde(default)]
318 pub close_early: bool,
319 #[serde(default)]
321 pub src_name: String,
322 #[serde(default)]
324 pub underlying: String,
325}
326
327#[derive(Clone, Debug, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct OKXEventContractSeries {
331 pub series_id: String,
333 #[serde(default)]
335 pub freq: String,
336 #[serde(default)]
338 pub title: String,
339 #[serde(default)]
341 pub category: String,
342 #[serde(default)]
344 pub settlement: OKXEventContractSettlement,
345}
346
347#[derive(Clone, Debug, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct OKXEventContractEvent {
351 pub series_id: String,
353 pub event_id: String,
355 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
357 pub fix_time: Option<u64>,
358 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
360 pub exp_time: Option<u64>,
361 pub state: String,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct OKXEventContractMarket {
369 pub series_id: String,
371 pub event_id: String,
373 pub inst_id: Ustr,
375 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
377 pub list_time: Option<u64>,
378 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
380 pub fix_time: Option<u64>,
381 #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
383 pub exp_time: Option<u64>,
384 pub state: String,
386 pub disputed: bool,
388 pub outcome: String,
390 pub floor_strike: String,
392 pub settle_value: String,
394}
395
396#[derive(Clone, Debug, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct OKXIndexTicker {
400 pub inst_id: Ustr,
402 pub idx_px: String,
404 #[serde(deserialize_with = "deserialize_string_to_u64")]
406 pub ts: u64,
407}
408
409pub type OKXOrderBookLevel = (String, String, String, String);
412
413#[derive(Clone, Debug, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct OKXOrderBookSnapshot {
417 pub asks: Vec<OKXOrderBookLevel>,
419 pub bids: Vec<OKXOrderBookLevel>,
421 #[serde(deserialize_with = "deserialize_string_to_u64")]
423 pub ts: u64,
424}
425
426#[derive(Clone, Debug, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct OKXFundingRateHistory {
430 pub inst_type: OKXInstrumentType,
432 pub inst_id: Ustr,
434 pub funding_rate: String,
436 pub realized_rate: String,
438 #[serde(deserialize_with = "deserialize_string_to_u64")]
440 pub funding_time: u64,
441 #[serde(default)]
443 pub method: Option<String>,
444}
445
446#[derive(Clone, Debug, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct OKXPositionTier {
450 pub uly: Ustr,
452 pub inst_family: String,
454 pub inst_id: Ustr,
456 pub tier: String,
458 pub min_sz: String,
460 pub max_sz: String,
462 pub mmr: String,
464 pub imr: String,
466 pub max_lever: String,
468 pub opt_mgn_factor: String,
470 pub quote_max_loan: String,
472 pub base_max_loan: String,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct OKXAccount {
480 pub adj_eq: String,
482 pub borrow_froz: String,
484 pub details: Vec<OKXBalanceDetail>,
486 pub imr: String,
488 pub iso_eq: String,
490 pub mgn_ratio: String,
492 pub mmr: String,
494 pub notional_usd_for_borrow: String,
496 pub notional_usd_for_futures: String,
498 pub notional_usd_for_option: String,
500 pub notional_usd_for_swap: String,
502 pub notional_usd: String,
504 pub ord_froz: String,
506 pub total_eq: String,
508 #[serde(deserialize_with = "deserialize_string_to_u64")]
510 pub u_time: u64,
511 pub upl: String,
513}
514
515#[derive(Clone, Debug, Serialize, Deserialize)]
517#[serde(rename_all = "camelCase")]
518#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
519#[cfg_attr(
520 feature = "python",
521 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
522)]
523pub struct OKXBalanceDetail {
524 pub avail_bal: String,
526 pub avail_eq: String,
528 pub borrow_froz: String,
530 pub cash_bal: String,
532 pub ccy: Ustr,
534 pub cross_liab: String,
536 pub dis_eq: String,
538 pub eq: String,
540 pub eq_usd: String,
542 pub smt_sync_eq: String,
544 pub spot_copy_trading_eq: String,
546 pub fixed_bal: String,
548 pub frozen_bal: String,
550 pub imr: String,
552 pub interest: String,
554 pub iso_eq: String,
556 pub iso_liab: String,
558 pub iso_upl: String,
560 pub liab: String,
562 pub max_loan: String,
564 pub mgn_ratio: String,
566 pub mmr: String,
568 pub notional_lever: String,
570 pub ord_frozen: String,
572 pub reward_bal: String,
574 #[serde(alias = "spotInUse")]
576 pub spot_in_use_amt: String,
577 #[serde(alias = "clSpotInUse")]
579 pub cl_spot_in_use_amt: String,
580 #[serde(alias = "maxSpotInUse")]
582 pub max_spot_in_use_amt: String,
583 pub spot_iso_bal: String,
585 pub stgy_eq: String,
587 pub twap: String,
589 #[serde(deserialize_with = "deserialize_string_to_u64")]
591 pub u_time: u64,
592 pub upl: String,
594 pub upl_liab: String,
596 pub spot_bal: String,
598 pub open_avg_px: String,
600 pub acc_avg_px: String,
602 pub spot_upl: String,
604 pub spot_upl_ratio: String,
606 pub total_pnl: String,
608 pub total_pnl_ratio: String,
610}
611
612#[derive(Clone, Debug, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub struct OKXPosition {
616 pub inst_id: Ustr,
618 pub inst_type: OKXInstrumentType,
620 pub mgn_mode: OKXMarginMode,
622 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
624 pub pos_id: Option<Ustr>,
625 pub pos_side: OKXPositionSide,
627 pub pos: String,
629 pub base_bal: String,
631 pub ccy: String,
633 pub fee: String,
635 pub lever: String,
637 pub last: String,
639 pub mark_px: String,
641 pub liq_px: String,
643 pub mmr: String,
645 pub interest: String,
647 pub trade_id: Ustr,
649 pub notional_usd: String,
651 pub avg_px: String,
653 pub upl: String,
655 pub upl_ratio: String,
657 #[serde(deserialize_with = "deserialize_string_to_u64")]
659 pub u_time: u64,
660 pub margin: String,
662 pub mgn_ratio: String,
664 pub adl: String,
666 pub c_time: String,
668 pub realized_pnl: String,
670 pub upl_last_px: String,
672 pub upl_ratio_last_px: String,
674 pub avail_pos: String,
676 pub be_px: String,
678 pub funding_fee: String,
680 pub idx_px: String,
682 pub liq_penalty: String,
684 pub opt_val: String,
686 pub pending_close_ord_liab_val: String,
688 pub pnl: String,
690 pub pos_ccy: String,
692 pub quote_bal: String,
694 pub quote_borrowed: String,
696 pub quote_interest: String,
698 #[serde(alias = "spotInUse")]
700 pub spot_in_use_amt: String,
701 pub spot_in_use_ccy: String,
703 pub usd_px: String,
705 #[serde(default)]
707 pub delta_bs: String,
708 #[serde(default)]
710 pub gamma_bs: String,
711 #[serde(default)]
713 pub theta_bs: String,
714 #[serde(default)]
716 pub vega_bs: String,
717}
718
719#[derive(Clone, Debug, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase")]
723pub struct OKXPlaceOrderResponse {
724 #[serde(default)]
726 pub ord_id: Option<Ustr>,
727 #[serde(default)]
729 pub cl_ord_id: Option<Ustr>,
730 #[serde(default)]
732 pub tag: Option<String>,
733 #[serde(default)]
735 pub inst_id: Option<Ustr>,
736 #[serde(default)]
738 pub side: Option<OKXSide>,
739 #[serde(default)]
741 pub ord_type: Option<OKXOrderType>,
742 #[serde(default)]
744 pub sz: Option<String>,
745 pub state: Option<OKXOrderStatus>,
747 #[serde(default)]
749 pub px: Option<String>,
750 #[serde(default)]
752 pub avg_px: Option<String>,
753 #[serde(default)]
755 pub acc_fill_sz: Option<String>,
756 #[serde(default)]
758 pub fill_sz: Option<String>,
759 #[serde(default)]
761 pub fill_px: Option<String>,
762 #[serde(default)]
764 pub trade_id: Option<Ustr>,
765 #[serde(default)]
767 pub fill_time: Option<String>,
768 #[serde(default)]
770 pub fee: Option<String>,
771 #[serde(default)]
773 pub fee_ccy: Option<String>,
774 #[serde(default)]
776 pub req_id: Option<Ustr>,
777 #[serde(default)]
779 pub pos_side: Option<OKXPositionSide>,
780 #[serde(default)]
782 pub reduce_only: Option<String>,
783 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
785 pub tgt_ccy: Option<OKXTargetCurrency>,
786 #[serde(default)]
788 pub c_time: Option<String>,
789 #[serde(default)]
791 pub u_time: Option<String>,
792 #[serde(skip_serializing_if = "Option::is_none")]
794 pub s_code: Option<String>,
795 #[serde(skip_serializing_if = "Option::is_none")]
797 pub s_msg: Option<String>,
798 #[serde(skip_serializing_if = "Option::is_none")]
800 pub sub_code: Option<String>,
801}
802
803#[derive(Clone, Debug, Default, Serialize, Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct OKXAttachAlgoOrdRequest {
807 #[serde(skip_serializing_if = "Option::is_none")]
809 pub attach_algo_cl_ord_id: Option<String>,
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub sl_trigger_px: Option<String>,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub sl_ord_px: Option<String>,
816 #[serde(skip_serializing_if = "Option::is_none")]
818 pub sl_trigger_px_type: Option<OKXTriggerType>,
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub tp_trigger_px: Option<String>,
822 #[serde(skip_serializing_if = "Option::is_none")]
824 pub tp_ord_px: Option<String>,
825 #[serde(skip_serializing_if = "Option::is_none")]
827 pub tp_trigger_px_type: Option<OKXTriggerType>,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub callback_ratio: Option<String>,
831 #[serde(skip_serializing_if = "Option::is_none")]
833 pub callback_spread: Option<String>,
834 #[serde(skip_serializing_if = "Option::is_none")]
836 pub active_px: Option<String>,
837 #[serde(skip_serializing_if = "Option::is_none")]
839 pub new_callback_ratio: Option<String>,
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub new_callback_spread: Option<String>,
843 #[serde(skip_serializing_if = "Option::is_none")]
845 pub new_active_px: Option<String>,
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct OKXPlaceOrderRequest {
852 pub inst_id: String,
854 pub td_mode: OKXTradeMode,
856 #[serde(skip_serializing_if = "Option::is_none")]
858 pub ccy: Option<String>,
859 #[serde(skip_serializing_if = "Option::is_none")]
861 pub cl_ord_id: Option<String>,
862 #[serde(skip_serializing_if = "Option::is_none")]
864 pub tag: Option<String>,
865 pub side: OKXSide,
867 #[serde(skip_serializing_if = "Option::is_none")]
869 pub pos_side: Option<OKXPositionSide>,
870 pub ord_type: OKXOrderType,
872 pub sz: String,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub px: Option<String>,
877 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
879 pub px_usd: Option<String>,
880 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
883 pub px_vol: Option<String>,
884 #[serde(skip_serializing_if = "Option::is_none")]
886 pub reduce_only: Option<bool>,
887 #[serde(skip_serializing_if = "Option::is_none")]
889 pub tgt_ccy: Option<OKXTargetCurrency>,
890 #[serde(skip_serializing_if = "Option::is_none")]
892 pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
893 #[serde(skip_serializing_if = "Option::is_none")]
895 pub speed_bump: Option<String>,
896 #[serde(skip_serializing_if = "Option::is_none")]
898 pub outcome: Option<String>,
899 #[serde(skip_serializing_if = "Option::is_none")]
904 pub slippage_pct: Option<String>,
905}
906
907#[derive(Clone, Debug, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct OKXCancelOrderRequest {
911 pub inst_id: String,
913 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
915 pub inst_id_code: Option<u64>,
916 #[serde(skip_serializing_if = "Option::is_none")]
918 pub ord_id: Option<String>,
919 #[serde(skip_serializing_if = "Option::is_none")]
921 pub cl_ord_id: Option<String>,
922}
923
924#[derive(Clone, Debug, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct OKXCancelOrderResponse {
928 pub ord_id: String,
930 #[serde(default)]
932 pub cl_ord_id: Option<String>,
933 #[serde(skip_serializing_if = "Option::is_none")]
935 pub s_code: Option<String>,
936 #[serde(skip_serializing_if = "Option::is_none")]
938 pub s_msg: Option<String>,
939 #[serde(default)]
941 pub ts: Option<String>,
942}
943
944pub use crate::common::models::OKXAttachedAlgoOrd;
945
946#[derive(Clone, Debug, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949pub struct OKXOrderHistory {
950 pub ord_id: Ustr,
952 pub cl_ord_id: Ustr,
954 #[serde(default)]
956 pub algo_id: Option<Ustr>,
957 #[serde(default)]
959 pub algo_cl_ord_id: Option<Ustr>,
960 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
962 pub attach_algo_cl_ord_id: Option<String>,
963 #[serde(default)]
965 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
966 #[serde(default)]
968 pub cl_act_id: Option<Ustr>,
969 pub tag: String,
971 pub inst_type: OKXInstrumentType,
973 pub uly: Option<Ustr>,
975 pub inst_id: Ustr,
977 pub ord_type: OKXOrderType,
979 pub sz: String,
981 pub px: String,
983 #[serde(default)]
985 pub px_usd: String,
986 #[serde(default)]
988 pub px_vol: String,
989 pub side: OKXSide,
991 pub pos_side: OKXPositionSide,
993 pub td_mode: OKXTradeMode,
995 pub reduce_only: String,
997 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
999 pub tgt_ccy: Option<OKXTargetCurrency>,
1000 pub state: OKXOrderStatus,
1002 pub avg_px: String,
1004 pub fee: String,
1006 pub fee_ccy: String,
1008 pub fill_sz: String,
1010 pub fill_px: String,
1012 pub trade_id: Ustr,
1014 #[serde(deserialize_with = "deserialize_string_to_u64")]
1016 pub fill_time: u64,
1017 pub acc_fill_sz: String,
1019 #[serde(default)]
1021 pub fill_fee: Option<String>,
1022 #[serde(default)]
1024 pub req_id: Option<Ustr>,
1025 #[serde(default)]
1027 pub cancel_fill_sz: Option<String>,
1028 #[serde(default)]
1030 pub cancel_total_sz: Option<String>,
1031 #[serde(default)]
1033 pub fee_discount: Option<String>,
1034 pub category: OKXOrderCategory,
1036 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1038 pub outcome: Option<String>,
1039 #[serde(deserialize_with = "deserialize_string_to_u64")]
1041 pub u_time: u64,
1042 #[serde(deserialize_with = "deserialize_string_to_u64")]
1044 pub c_time: u64,
1045}
1046
1047#[derive(Clone, Debug, Serialize, Deserialize)]
1049#[serde(rename_all = "camelCase")]
1050pub struct OKXOrderAlgo {
1051 pub algo_id: String,
1053 #[serde(default)]
1055 pub algo_cl_ord_id: String,
1056 #[serde(default)]
1058 pub cl_ord_id: String,
1059 #[serde(default)]
1061 pub ord_id: String,
1062 pub inst_id: Ustr,
1064 pub inst_type: OKXInstrumentType,
1066 pub ord_type: OKXAlgoOrderType,
1068 pub state: OKXAlgoOrderStatus,
1070 pub side: OKXSide,
1072 pub pos_side: OKXPositionSide,
1074 #[serde(default)]
1076 pub sz: String,
1077 #[serde(default)]
1079 pub trigger_px: String,
1080 #[serde(default)]
1082 pub trigger_px_type: Option<OKXTriggerType>,
1083 #[serde(default)]
1085 pub sl_trigger_px: String,
1086 #[serde(default)]
1088 pub sl_ord_px: String,
1089 #[serde(default)]
1091 pub sl_trigger_px_type: Option<OKXTriggerType>,
1092 #[serde(default)]
1094 pub tp_trigger_px: String,
1095 #[serde(default)]
1097 pub tp_ord_px: String,
1098 #[serde(default)]
1100 pub tp_trigger_px_type: Option<OKXTriggerType>,
1101 #[serde(default)]
1103 pub ord_px: String,
1104 pub td_mode: OKXTradeMode,
1106 #[serde(default)]
1108 pub lever: String,
1109 #[serde(default)]
1111 pub reduce_only: String,
1112 #[serde(default)]
1114 pub close_fraction: String,
1115 #[serde(default)]
1117 pub actual_px: String,
1118 #[serde(default)]
1120 pub actual_sz: String,
1121 #[serde(default)]
1123 pub notional_usd: String,
1124 #[serde(deserialize_with = "deserialize_string_to_u64")]
1126 pub c_time: u64,
1127 #[serde(deserialize_with = "deserialize_string_to_u64")]
1129 pub u_time: u64,
1130 #[serde(default)]
1132 pub trigger_time: String,
1133 #[serde(default)]
1135 pub tag: String,
1136 #[serde(default)]
1138 pub callback_ratio: String,
1139 #[serde(default)]
1141 pub callback_spread: String,
1142 #[serde(default)]
1144 pub active_px: String,
1145}
1146
1147#[derive(Clone, Debug, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150pub struct OKXTransactionDetail {
1151 pub inst_type: OKXInstrumentType,
1153 pub inst_id: Ustr,
1155 pub trade_id: Ustr,
1157 pub ord_id: Ustr,
1159 pub cl_ord_id: Ustr,
1161 pub bill_id: Ustr,
1163 pub fill_px: String,
1165 pub fill_sz: String,
1167 pub side: OKXSide,
1169 pub exec_type: OKXExecType,
1171 pub fee_ccy: String,
1173 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1175 pub fee: Option<String>,
1176 #[serde(deserialize_with = "deserialize_string_to_u64")]
1178 pub ts: u64,
1179}
1180
1181#[derive(Clone, Debug, Serialize, Deserialize)]
1183#[serde(rename_all = "camelCase")]
1184pub struct OKXPositionHistory {
1185 pub inst_type: OKXInstrumentType,
1187 pub inst_id: Ustr,
1189 pub mgn_mode: OKXMarginMode,
1191 #[serde(rename = "type")]
1194 pub r#type: Ustr,
1195 pub c_time: String,
1197 #[serde(deserialize_with = "deserialize_string_to_u64")]
1199 pub u_time: u64,
1200 pub open_avg_px: String,
1202 #[serde(skip_serializing_if = "Option::is_none")]
1204 pub close_avg_px: Option<String>,
1205 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1207 pub pos_id: Option<Ustr>,
1208 #[serde(skip_serializing_if = "Option::is_none")]
1210 pub open_max_pos: Option<String>,
1211 #[serde(skip_serializing_if = "Option::is_none")]
1213 pub close_total_pos: Option<String>,
1214 #[serde(skip_serializing_if = "Option::is_none")]
1216 pub realized_pnl: Option<String>,
1217 #[serde(skip_serializing_if = "Option::is_none")]
1219 pub fee: Option<String>,
1220 #[serde(skip_serializing_if = "Option::is_none")]
1222 pub funding_fee: Option<String>,
1223 #[serde(skip_serializing_if = "Option::is_none")]
1225 pub liq_penalty: Option<String>,
1226 #[serde(skip_serializing_if = "Option::is_none")]
1228 pub pnl: Option<String>,
1229 #[serde(skip_serializing_if = "Option::is_none")]
1231 pub pnl_ratio: Option<String>,
1232 pub pos_side: OKXPositionSide,
1234 pub lever: String,
1236 #[serde(skip_serializing_if = "Option::is_none")]
1238 pub direction: Option<String>,
1239 #[serde(skip_serializing_if = "Option::is_none")]
1241 pub trigger_px: Option<String>,
1242 #[serde(skip_serializing_if = "Option::is_none")]
1244 pub uly: Option<String>,
1245 #[serde(skip_serializing_if = "Option::is_none")]
1247 pub ccy: Option<String>,
1248}
1249
1250#[derive(Clone, Debug, Serialize, Deserialize)]
1252#[serde(rename_all = "camelCase")]
1253pub struct OKXPlaceAlgoOrderRequest {
1254 #[serde(rename = "instId")]
1256 pub inst_id: String,
1257 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1259 pub inst_id_code: Option<u64>,
1260 #[serde(rename = "tdMode")]
1262 pub td_mode: OKXTradeMode,
1263 pub side: OKXSide,
1265 #[serde(rename = "ordType")]
1267 pub ord_type: OKXAlgoOrderType,
1268 #[serde(skip_serializing_if = "Option::is_none")]
1270 pub sz: Option<String>,
1271 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1273 pub algo_cl_ord_id: Option<String>,
1274 #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1276 pub trigger_px: Option<String>,
1277 #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1279 pub order_px: Option<String>,
1280 #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1282 pub trigger_px_type: Option<OKXTriggerType>,
1283 #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1285 pub sl_trigger_px: Option<String>,
1286 #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1288 pub sl_ord_px: Option<String>,
1289 #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1291 pub sl_trigger_px_type: Option<OKXTriggerType>,
1292 #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1294 pub tp_trigger_px: Option<String>,
1295 #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1297 pub tp_ord_px: Option<String>,
1298 #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1300 pub tp_trigger_px_type: Option<OKXTriggerType>,
1301 #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1303 pub tgt_ccy: Option<OKXTargetCurrency>,
1304 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1306 pub pos_side: Option<OKXPositionSide>,
1307 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1309 pub close_position: Option<bool>,
1310 #[serde(skip_serializing_if = "Option::is_none")]
1312 pub tag: Option<String>,
1313 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1315 pub reduce_only: Option<bool>,
1316 #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1318 pub close_fraction: Option<String>,
1319 #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1322 pub callback_ratio: Option<String>,
1323 #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1326 pub callback_spread: Option<String>,
1327 #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1330 pub active_px: Option<String>,
1331}
1332
1333#[derive(Clone, Debug, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct OKXPlaceAlgoOrderResponse {
1337 pub algo_id: String,
1339 #[serde(skip_serializing_if = "Option::is_none")]
1341 pub algo_cl_ord_id: Option<String>,
1342 #[serde(skip_serializing_if = "Option::is_none")]
1344 pub s_code: Option<String>,
1345 #[serde(skip_serializing_if = "Option::is_none")]
1347 pub s_msg: Option<String>,
1348 #[serde(skip_serializing_if = "Option::is_none")]
1350 pub req_id: Option<String>,
1351}
1352
1353#[derive(Clone, Debug, Serialize, Deserialize)]
1355#[serde(rename_all = "camelCase")]
1356pub struct OKXCancelAlgoOrderRequest {
1357 pub inst_id: String,
1359 #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1361 pub inst_id_code: Option<u64>,
1362 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub algo_id: Option<String>,
1365 #[serde(skip_serializing_if = "Option::is_none")]
1367 pub algo_cl_ord_id: Option<String>,
1368}
1369
1370#[derive(Clone, Debug, Serialize, Deserialize)]
1372#[serde(rename_all = "camelCase")]
1373pub struct OKXCancelAlgoOrderResponse {
1374 pub algo_id: String,
1376 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub s_code: Option<String>,
1379 #[serde(skip_serializing_if = "Option::is_none")]
1381 pub s_msg: Option<String>,
1382}
1383
1384#[derive(Clone, Debug, Serialize, Deserialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct OKXAmendAlgoOrderRequest {
1388 pub inst_id: String,
1390 pub algo_id: String,
1392 #[serde(skip_serializing_if = "Option::is_none")]
1394 pub algo_cl_ord_id: Option<String>,
1395 #[serde(skip_serializing_if = "Option::is_none")]
1397 pub new_sz: Option<String>,
1398 #[serde(skip_serializing_if = "Option::is_none")]
1400 pub new_trigger_px: Option<String>,
1401 #[serde(skip_serializing_if = "Option::is_none")]
1403 pub new_tp_trigger_px: Option<String>,
1404 #[serde(skip_serializing_if = "Option::is_none")]
1406 pub new_tp_ord_px: Option<String>,
1407 #[serde(skip_serializing_if = "Option::is_none")]
1409 pub new_tp_trigger_px_type: Option<String>,
1410 #[serde(skip_serializing_if = "Option::is_none")]
1414 pub new_sl_trigger_px: Option<String>,
1415 #[serde(skip_serializing_if = "Option::is_none")]
1417 pub new_sl_ord_px: Option<String>,
1418 #[serde(skip_serializing_if = "Option::is_none")]
1420 pub new_sl_trigger_px_type: Option<String>,
1421 #[serde(skip_serializing_if = "Option::is_none")]
1423 pub new_order_px: Option<String>,
1424 #[serde(skip_serializing_if = "Option::is_none")]
1426 pub new_callback_ratio: Option<String>,
1427 #[serde(skip_serializing_if = "Option::is_none")]
1429 pub new_callback_spread: Option<String>,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1432 pub new_active_px: Option<String>,
1433}
1434
1435#[derive(Clone, Debug, Serialize, Deserialize)]
1437#[serde(rename_all = "camelCase")]
1438pub struct OKXAmendAlgoOrderResponse {
1439 pub algo_id: String,
1441 #[serde(skip_serializing_if = "Option::is_none")]
1443 pub algo_cl_ord_id: Option<String>,
1444 #[serde(skip_serializing_if = "Option::is_none")]
1446 pub s_code: Option<String>,
1447 #[serde(skip_serializing_if = "Option::is_none")]
1449 pub s_msg: Option<String>,
1450 #[serde(skip_serializing_if = "Option::is_none")]
1452 pub req_id: Option<String>,
1453}
1454
1455#[derive(Clone, Debug, Serialize, Deserialize)]
1457#[serde(rename_all = "camelCase")]
1458pub struct OKXServerTime {
1459 #[serde(deserialize_with = "deserialize_string_to_u64")]
1461 pub ts: u64,
1462}
1463
1464#[derive(Clone, Debug, Serialize, Deserialize)]
1466#[serde(rename_all = "camelCase")]
1467pub struct OKXFeeRate {
1468 #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1470 pub level: OKXVipLevel,
1471 pub taker: String,
1473 pub maker: String,
1475 pub taker_u: String,
1477 pub maker_u: String,
1479 #[serde(default)]
1481 pub delivery: String,
1482 #[serde(default)]
1484 pub exercise: String,
1485 #[serde(default)]
1487 pub settle: String,
1488 pub inst_type: OKXInstrumentType,
1490 #[serde(default)]
1492 pub category: String,
1493 #[serde(deserialize_with = "deserialize_string_to_u64")]
1495 pub ts: u64,
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500 use rstest::rstest;
1501 use serde_json;
1502
1503 use super::*;
1504
1505 #[rstest]
1506 fn test_algo_order_request_serialization() {
1507 let request = OKXPlaceAlgoOrderRequest {
1508 inst_id: "ETH-USDT-SWAP".to_string(),
1509 inst_id_code: None,
1510 td_mode: OKXTradeMode::Isolated,
1511 side: OKXSide::Buy,
1512 ord_type: OKXAlgoOrderType::Trigger,
1513 sz: Some("0.01".to_string()),
1514 algo_cl_ord_id: Some("test123".to_string()),
1515 trigger_px: Some("3000".to_string()),
1516 order_px: Some("-1".to_string()),
1517 trigger_px_type: Some(OKXTriggerType::Last),
1518 sl_trigger_px: None,
1519 sl_ord_px: None,
1520 sl_trigger_px_type: None,
1521 tp_trigger_px: None,
1522 tp_ord_px: None,
1523 tp_trigger_px_type: None,
1524 tgt_ccy: None,
1525 pos_side: None,
1526 close_position: None,
1527 tag: None,
1528 reduce_only: None,
1529 close_fraction: None,
1530 callback_ratio: None,
1531 callback_spread: None,
1532 active_px: None,
1533 };
1534
1535 let json = serde_json::to_string(&request).unwrap();
1536
1537 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1539 assert!(json.contains("\"tdMode\":\"isolated\""));
1540 assert!(json.contains("\"ordType\":\"trigger\""));
1541 assert!(json.contains("\"algoClOrdId\":\"test123\""));
1542 assert!(json.contains("\"triggerPx\":\"3000\""));
1543 assert!(json.contains("\"orderPx\":\"-1\""));
1544 assert!(json.contains("\"triggerPxType\":\"last\""));
1545
1546 assert!(!json.contains("tgtCcy"));
1548 assert!(!json.contains("posSide"));
1549 assert!(!json.contains("closePosition"));
1550 assert!(!json.contains("closeFraction"));
1551 }
1552
1553 #[rstest]
1554 fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1555 let request = OKXAmendAlgoOrderRequest {
1559 inst_id: "ETH-USDT-SWAP".to_string(),
1560 algo_id: "123".to_string(),
1561 algo_cl_ord_id: None,
1562 new_sz: None,
1563 new_trigger_px: None,
1564 new_tp_trigger_px: None,
1565 new_tp_ord_px: None,
1566 new_tp_trigger_px_type: None,
1567 new_sl_trigger_px: Some("850".to_string()),
1568 new_sl_ord_px: None,
1569 new_sl_trigger_px_type: None,
1570 new_order_px: None,
1571 new_callback_ratio: None,
1572 new_callback_spread: None,
1573 new_active_px: None,
1574 };
1575
1576 let json = serde_json::to_string(&request).unwrap();
1577
1578 assert!(json.contains("\"algoId\":\"123\""));
1579 assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1580 assert!(!json.contains("newTriggerPx"));
1582 assert!(!json.contains("newSz"));
1583 assert!(!json.contains("algoClOrdId"));
1584 }
1585
1586 #[rstest]
1587 fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1588 let request = OKXAmendAlgoOrderRequest {
1589 inst_id: "DOGE-USDT-SWAP".to_string(),
1590 algo_id: "algo-oco-1".to_string(),
1591 algo_cl_ord_id: None,
1592 new_sz: None,
1593 new_trigger_px: None,
1594 new_tp_trigger_px: Some("0.10495".to_string()),
1595 new_tp_ord_px: Some("-1".to_string()),
1596 new_tp_trigger_px_type: Some("last".to_string()),
1597 new_sl_trigger_px: Some("0.06297".to_string()),
1598 new_sl_ord_px: Some("-1".to_string()),
1599 new_sl_trigger_px_type: Some("last".to_string()),
1600 new_order_px: None,
1601 new_callback_ratio: None,
1602 new_callback_spread: None,
1603 new_active_px: None,
1604 };
1605
1606 let json = serde_json::to_string(&request).unwrap();
1607
1608 assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1609 assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1610 assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1611 assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1612 assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1613 assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1614 assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1615 assert!(!json.contains("newTriggerPx"));
1616 }
1617
1618 #[rstest]
1619 fn test_algo_order_request_serializes_close_fraction() {
1620 let request = OKXPlaceAlgoOrderRequest {
1621 inst_id: "ETH-USDT-SWAP".to_string(),
1622 inst_id_code: None,
1623 td_mode: OKXTradeMode::Cross,
1624 side: OKXSide::Sell,
1625 ord_type: OKXAlgoOrderType::Conditional,
1626 sz: None,
1627 algo_cl_ord_id: Some("close-frac-123".to_string()),
1628 trigger_px: None,
1629 order_px: None,
1630 trigger_px_type: None,
1631 sl_trigger_px: Some("3000".to_string()),
1632 sl_ord_px: Some("-1".to_string()),
1633 sl_trigger_px_type: Some(OKXTriggerType::Last),
1634 tp_trigger_px: None,
1635 tp_ord_px: None,
1636 tp_trigger_px_type: None,
1637 tgt_ccy: None,
1638 pos_side: Some(OKXPositionSide::Net),
1639 close_position: None,
1640 tag: None,
1641 reduce_only: Some(true),
1642 close_fraction: Some("1".to_string()),
1643 callback_ratio: None,
1644 callback_spread: None,
1645 active_px: None,
1646 };
1647
1648 let json = serde_json::to_string(&request).unwrap();
1649
1650 assert!(json.contains("\"ordType\":\"conditional\""));
1651 assert!(json.contains("\"closeFraction\":\"1\""));
1652 assert!(json.contains("\"slTriggerPx\":\"3000\""));
1653 assert!(json.contains("\"slOrdPx\":\"-1\""));
1654 assert!(json.contains("\"slTriggerPxType\":\"last\""));
1655 assert!(json.contains("\"reduceOnly\":true"));
1656 assert!(!json.contains("\"sz\""));
1657 assert!(!json.contains("triggerPx"));
1658 }
1659
1660 #[rstest]
1661 fn test_algo_order_request_array_serialization() {
1662 let request = OKXPlaceAlgoOrderRequest {
1663 inst_id: "BTC-USDT".to_string(),
1664 inst_id_code: Some(10459),
1665 td_mode: OKXTradeMode::Cross,
1666 side: OKXSide::Sell,
1667 ord_type: OKXAlgoOrderType::Trigger,
1668 sz: Some("0.1".to_string()),
1669 algo_cl_ord_id: None,
1670 trigger_px: Some("50000".to_string()),
1671 order_px: Some("49900".to_string()),
1672 trigger_px_type: Some(OKXTriggerType::Mark),
1673 sl_trigger_px: None,
1674 sl_ord_px: None,
1675 sl_trigger_px_type: None,
1676 tp_trigger_px: None,
1677 tp_ord_px: None,
1678 tp_trigger_px_type: None,
1679 tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1680 pos_side: Some(OKXPositionSide::Net),
1681 close_position: None,
1682 tag: None,
1683 reduce_only: Some(true),
1684 close_fraction: None,
1685 callback_ratio: None,
1686 callback_spread: None,
1687 active_px: None,
1688 };
1689
1690 let json = serde_json::to_string(&[request]).unwrap();
1692
1693 assert!(json.starts_with('['));
1695 assert!(json.ends_with(']'));
1696
1697 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1699 assert!(json.contains("\"tdMode\":\"cross\""));
1700 assert!(json.contains("\"triggerPx\":\"50000\""));
1701 assert!(json.contains("\"orderPx\":\"49900\""));
1702 assert!(json.contains("\"triggerPxType\":\"mark\""));
1703 assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1704 assert!(json.contains("\"posSide\":\"net\""));
1705 assert!(json.contains("\"reduceOnly\":true"));
1706 }
1707
1708 #[rstest]
1709 fn test_cancel_algo_order_request_serialization() {
1710 let request = OKXCancelAlgoOrderRequest {
1711 inst_id: "ETH-USDT-SWAP".to_string(),
1712 inst_id_code: None,
1713 algo_id: Some("123456".to_string()),
1714 algo_cl_ord_id: None,
1715 };
1716
1717 let json = serde_json::to_string(&request).unwrap();
1718
1719 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1721 assert!(json.contains("\"algoId\":\"123456\""));
1722 assert!(!json.contains("algoClOrdId"));
1723 }
1724
1725 #[rstest]
1726 fn test_cancel_algo_order_with_client_id_serialization() {
1727 let request = OKXCancelAlgoOrderRequest {
1728 inst_id: "BTC-USDT".to_string(),
1729 inst_id_code: Some(10459),
1730 algo_id: None,
1731 algo_cl_ord_id: Some("client123".to_string()),
1732 };
1733
1734 let json = serde_json::to_string(&[request]).unwrap();
1736
1737 assert!(json.starts_with('['));
1739 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1740 assert!(json.contains("\"algoClOrdId\":\"client123\""));
1741 assert!(!json.contains("\"algoId\""));
1742 }
1743
1744 #[rstest]
1745 fn test_amend_algo_order_trigger_serialization() {
1746 let request = OKXAmendAlgoOrderRequest {
1747 inst_id: "ETH-USDT-SWAP".to_string(),
1748 algo_id: "123456".to_string(),
1749 algo_cl_ord_id: None,
1750 new_sz: None,
1751 new_trigger_px: Some("3500".to_string()),
1752 new_tp_trigger_px: None,
1753 new_tp_ord_px: None,
1754 new_tp_trigger_px_type: None,
1755 new_sl_trigger_px: None,
1756 new_sl_ord_px: None,
1757 new_sl_trigger_px_type: None,
1758 new_order_px: Some("3490".to_string()),
1759 new_callback_ratio: None,
1760 new_callback_spread: None,
1761 new_active_px: None,
1762 };
1763
1764 let json = serde_json::to_string(&request).unwrap();
1765
1766 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1767 assert!(json.contains("\"algoId\":\"123456\""));
1768 assert!(json.contains("\"newTriggerPx\":\"3500\""));
1769 assert!(json.contains("\"newOrderPx\":\"3490\""));
1770 assert!(!json.contains("newSz"));
1771 assert!(!json.contains("algoClOrdId"));
1772 assert!(!json.contains("newCallbackRatio"));
1773 }
1774
1775 #[rstest]
1776 fn test_amend_algo_order_trailing_stop_serialization() {
1777 let request = OKXAmendAlgoOrderRequest {
1778 inst_id: "BTC-USDT-SWAP".to_string(),
1779 algo_id: "789012".to_string(),
1780 algo_cl_ord_id: Some("client456".to_string()),
1781 new_sz: Some("0.1".to_string()),
1782 new_trigger_px: None,
1783 new_tp_trigger_px: None,
1784 new_tp_ord_px: None,
1785 new_tp_trigger_px_type: None,
1786 new_sl_trigger_px: None,
1787 new_sl_ord_px: None,
1788 new_sl_trigger_px_type: None,
1789 new_order_px: None,
1790 new_callback_ratio: Some("0.02".to_string()),
1791 new_callback_spread: None,
1792 new_active_px: Some("50000".to_string()),
1793 };
1794
1795 let json = serde_json::to_string(&request).unwrap();
1796
1797 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1798 assert!(json.contains("\"algoId\":\"789012\""));
1799 assert!(json.contains("\"algoClOrdId\":\"client456\""));
1800 assert!(json.contains("\"newSz\":\"0.1\""));
1801 assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1802 assert!(json.contains("\"newActivePx\":\"50000\""));
1803 assert!(!json.contains("newTriggerPx"));
1804 assert!(!json.contains("newOrderPx"));
1805 }
1806
1807 #[rstest]
1808 fn test_trailing_stop_request_callback_ratio_serialization() {
1809 let request = OKXPlaceAlgoOrderRequest {
1810 inst_id: "BTC-USDT-SWAP".to_string(),
1811 inst_id_code: None,
1812 td_mode: OKXTradeMode::Cross,
1813 side: OKXSide::Buy,
1814 ord_type: OKXAlgoOrderType::MoveOrderStop,
1815 sz: Some("0.1".to_string()),
1816 algo_cl_ord_id: Some("trail-001".to_string()),
1817 trigger_px: None,
1818 order_px: None,
1819 trigger_px_type: None,
1820 sl_trigger_px: None,
1821 sl_ord_px: None,
1822 sl_trigger_px_type: None,
1823 tp_trigger_px: None,
1824 tp_ord_px: None,
1825 tp_trigger_px_type: None,
1826 tgt_ccy: None,
1827 pos_side: None,
1828 close_position: None,
1829 tag: None,
1830 reduce_only: None,
1831 close_fraction: None,
1832 callback_ratio: Some("0.01".to_string()),
1833 callback_spread: None,
1834 active_px: None,
1835 };
1836
1837 let json = serde_json::to_string(&request).unwrap();
1838
1839 assert!(json.contains("\"ordType\":\"move_order_stop\""));
1840 assert!(json.contains("\"callbackRatio\":\"0.01\""));
1841 assert!(!json.contains("callbackSpread"));
1842 assert!(!json.contains("activePx"));
1843 }
1844
1845 #[rstest]
1846 fn test_trailing_stop_request_callback_spread_serialization() {
1847 let request = OKXPlaceAlgoOrderRequest {
1848 inst_id: "ETH-USDT-SWAP".to_string(),
1849 inst_id_code: None,
1850 td_mode: OKXTradeMode::Isolated,
1851 side: OKXSide::Sell,
1852 ord_type: OKXAlgoOrderType::MoveOrderStop,
1853 sz: Some("1.0".to_string()),
1854 algo_cl_ord_id: None,
1855 trigger_px: None,
1856 order_px: None,
1857 trigger_px_type: None,
1858 sl_trigger_px: None,
1859 sl_ord_px: None,
1860 sl_trigger_px_type: None,
1861 tp_trigger_px: None,
1862 tp_ord_px: None,
1863 tp_trigger_px_type: None,
1864 tgt_ccy: None,
1865 pos_side: None,
1866 close_position: None,
1867 tag: None,
1868 reduce_only: Some(true),
1869 close_fraction: None,
1870 callback_ratio: None,
1871 callback_spread: Some("50.5".to_string()),
1872 active_px: None,
1873 };
1874
1875 let json = serde_json::to_string(&request).unwrap();
1876
1877 assert!(json.contains("\"callbackSpread\":\"50.5\""));
1878 assert!(!json.contains("callbackRatio"));
1879 assert!(!json.contains("activePx"));
1880 }
1881
1882 #[rstest]
1883 fn test_trailing_stop_request_with_activation_price_serialization() {
1884 let request = OKXPlaceAlgoOrderRequest {
1885 inst_id: "BTC-USDT-SWAP".to_string(),
1886 inst_id_code: None,
1887 td_mode: OKXTradeMode::Cross,
1888 side: OKXSide::Buy,
1889 ord_type: OKXAlgoOrderType::MoveOrderStop,
1890 sz: Some("0.5".to_string()),
1891 algo_cl_ord_id: None,
1892 trigger_px: None,
1893 order_px: None,
1894 trigger_px_type: None,
1895 sl_trigger_px: None,
1896 sl_ord_px: None,
1897 sl_trigger_px_type: None,
1898 tp_trigger_px: None,
1899 tp_ord_px: None,
1900 tp_trigger_px_type: None,
1901 tgt_ccy: None,
1902 pos_side: None,
1903 close_position: None,
1904 tag: None,
1905 reduce_only: None,
1906 close_fraction: None,
1907 callback_ratio: Some("0.005".to_string()),
1908 callback_spread: None,
1909 active_px: Some("65000".to_string()),
1910 };
1911
1912 let json = serde_json::to_string(&request).unwrap();
1913
1914 assert!(json.contains("\"callbackRatio\":\"0.005\""));
1915 assert!(json.contains("\"activePx\":\"65000\""));
1916 assert!(!json.contains("callbackSpread"));
1917 }
1918
1919 #[rstest]
1920 fn test_amend_algo_order_callback_spread_serialization() {
1921 let request = OKXAmendAlgoOrderRequest {
1922 inst_id: "ETH-USDT-SWAP".to_string(),
1923 algo_id: "456789".to_string(),
1924 algo_cl_ord_id: None,
1925 new_sz: None,
1926 new_trigger_px: None,
1927 new_tp_trigger_px: None,
1928 new_tp_ord_px: None,
1929 new_tp_trigger_px_type: None,
1930 new_sl_trigger_px: None,
1931 new_sl_ord_px: None,
1932 new_sl_trigger_px_type: None,
1933 new_order_px: None,
1934 new_callback_ratio: None,
1935 new_callback_spread: Some("25.0".to_string()),
1936 new_active_px: Some("4000".to_string()),
1937 };
1938
1939 let json = serde_json::to_string(&request).unwrap();
1940
1941 assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
1942 assert!(json.contains("\"newActivePx\":\"4000\""));
1943 assert!(!json.contains("newCallbackRatio"));
1944 assert!(!json.contains("newTriggerPx"));
1945 assert!(!json.contains("newSz"));
1946 }
1947
1948 #[rstest]
1949 fn test_amend_algo_order_size_only_serialization() {
1950 let request = OKXAmendAlgoOrderRequest {
1951 inst_id: "BTC-USDT-SWAP".to_string(),
1952 algo_id: "111222".to_string(),
1953 algo_cl_ord_id: None,
1954 new_sz: Some("0.5".to_string()),
1955 new_trigger_px: None,
1956 new_tp_trigger_px: None,
1957 new_tp_ord_px: None,
1958 new_tp_trigger_px_type: None,
1959 new_sl_trigger_px: None,
1960 new_sl_ord_px: None,
1961 new_sl_trigger_px_type: None,
1962 new_order_px: None,
1963 new_callback_ratio: None,
1964 new_callback_spread: None,
1965 new_active_px: None,
1966 };
1967
1968 let json = serde_json::to_string(&request).unwrap();
1969
1970 assert!(json.contains("\"newSz\":\"0.5\""));
1971 assert!(!json.contains("newTriggerPx"));
1972 assert!(!json.contains("newOrderPx"));
1973 assert!(!json.contains("newCallbackRatio"));
1974 assert!(!json.contains("newCallbackSpread"));
1975 assert!(!json.contains("newActivePx"));
1976 }
1977
1978 #[rstest]
1979 fn test_amend_algo_order_all_fields_serialization() {
1980 let request = OKXAmendAlgoOrderRequest {
1981 inst_id: "BTC-USDT-SWAP".to_string(),
1982 algo_id: "333444".to_string(),
1983 algo_cl_ord_id: Some("client789".to_string()),
1984 new_sz: Some("1.0".to_string()),
1985 new_trigger_px: Some("60000".to_string()),
1986 new_tp_trigger_px: None,
1987 new_tp_ord_px: None,
1988 new_tp_trigger_px_type: None,
1989 new_sl_trigger_px: None,
1990 new_sl_ord_px: None,
1991 new_sl_trigger_px_type: None,
1992 new_order_px: Some("59900".to_string()),
1993 new_callback_ratio: Some("0.015".to_string()),
1994 new_callback_spread: Some("100".to_string()),
1995 new_active_px: Some("62000".to_string()),
1996 };
1997
1998 let json = serde_json::to_string(&request).unwrap();
1999
2000 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2001 assert!(json.contains("\"algoId\":\"333444\""));
2002 assert!(json.contains("\"algoClOrdId\":\"client789\""));
2003 assert!(json.contains("\"newSz\":\"1.0\""));
2004 assert!(json.contains("\"newTriggerPx\":\"60000\""));
2005 assert!(json.contains("\"newOrderPx\":\"59900\""));
2006 assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2007 assert!(json.contains("\"newCallbackSpread\":\"100\""));
2008 assert!(json.contains("\"newActivePx\":\"62000\""));
2009 }
2010
2011 #[rstest]
2012 fn test_place_order_request_serializes_px_usd() {
2013 let request = OKXPlaceOrderRequest {
2014 inst_id: "BTC-USD-250328-50000-C".to_string(),
2015 td_mode: OKXTradeMode::Cross,
2016 ccy: None,
2017 cl_ord_id: Some("test-opt-1".to_string()),
2018 tag: None,
2019 side: OKXSide::Buy,
2020 pos_side: Some(OKXPositionSide::Net),
2021 ord_type: OKXOrderType::Limit,
2022 sz: "1".to_string(),
2023 px: None,
2024 px_usd: Some("100.5".to_string()),
2025 px_vol: None,
2026 reduce_only: None,
2027 tgt_ccy: None,
2028 attach_algo_ords: None,
2029 speed_bump: None,
2030 outcome: None,
2031 slippage_pct: None,
2032 };
2033
2034 let json = serde_json::to_string(&request).unwrap();
2035 assert!(json.contains("\"pxUsd\":\"100.5\""));
2036 assert!(!json.contains("\"pxVol\""));
2037 assert!(!json.contains("\"px\":"));
2038 assert!(!json.contains("slippagePct"));
2039 }
2040
2041 #[rstest]
2042 fn test_place_order_request_serializes_px_vol() {
2043 let request = OKXPlaceOrderRequest {
2044 inst_id: "BTC-USD-250328-50000-C".to_string(),
2045 td_mode: OKXTradeMode::Cross,
2046 ccy: None,
2047 cl_ord_id: Some("test-opt-2".to_string()),
2048 tag: None,
2049 side: OKXSide::Buy,
2050 pos_side: Some(OKXPositionSide::Net),
2051 ord_type: OKXOrderType::Limit,
2052 sz: "1".to_string(),
2053 px: None,
2054 px_usd: None,
2055 px_vol: Some("0.55".to_string()),
2056 reduce_only: None,
2057 tgt_ccy: None,
2058 attach_algo_ords: None,
2059 speed_bump: None,
2060 outcome: None,
2061 slippage_pct: None,
2062 };
2063
2064 let json = serde_json::to_string(&request).unwrap();
2065 assert!(json.contains("\"pxVol\":\"0.55\""));
2066 assert!(!json.contains("\"pxUsd\""));
2067 assert!(!json.contains("\"px\":"));
2068 }
2069
2070 #[rstest]
2071 fn test_place_order_request_serializes_slippage_pct() {
2072 let request = OKXPlaceOrderRequest {
2073 inst_id: "BTC-USDT-SWAP".to_string(),
2074 td_mode: OKXTradeMode::Cross,
2075 ccy: None,
2076 cl_ord_id: Some("mkt-slip-1".to_string()),
2077 tag: None,
2078 side: OKXSide::Buy,
2079 pos_side: Some(OKXPositionSide::Net),
2080 ord_type: OKXOrderType::Market,
2081 sz: "1".to_string(),
2082 px: None,
2083 px_usd: None,
2084 px_vol: None,
2085 reduce_only: None,
2086 tgt_ccy: None,
2087 attach_algo_ords: None,
2088 speed_bump: None,
2089 outcome: None,
2090 slippage_pct: Some("0.005".to_string()),
2091 };
2092
2093 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2094 assert_eq!(json["slippagePct"], "0.005");
2095 }
2096
2097 #[rstest]
2098 fn test_event_contract_models_deserialize() {
2099 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2100 "seriesId": "BTC-ABOVE-DAILY",
2101 "freq": "daily",
2102 "title": "BTC above daily",
2103 "category": "1",
2104 "settlement": {
2105 "method": "cash",
2106 "closeEarly": false,
2107 "srcName": "OKX BTC/USD Index",
2108 "underlying": "BTC-USD"
2109 }
2110 }))
2111 .unwrap();
2112 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2113 "seriesId": "BTC-ABOVE-DAILY",
2114 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2115 "fixTime": "",
2116 "expTime": "1769697132335",
2117 "state": "live"
2118 }))
2119 .unwrap();
2120 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2121 "seriesId": "BTC-ABOVE-DAILY",
2122 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2123 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2124 "listTime": "1769697132335",
2125 "fixTime": "",
2126 "expTime": "1769697132335",
2127 "state": "live",
2128 "disputed": false,
2129 "outcome": "0",
2130 "floorStrike": "120000",
2131 "settleValue": ""
2132 }))
2133 .unwrap();
2134
2135 assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2136 assert_eq!(series.settlement.underlying, "BTC-USD");
2137 assert_eq!(event.fix_time, None);
2138 assert_eq!(event.exp_time, Some(1_769_697_132_335));
2139 assert_eq!(
2140 market.inst_id,
2141 Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2142 );
2143 assert_eq!(market.list_time, Some(1_769_697_132_335));
2144 assert_eq!(market.exp_time, Some(1_769_697_132_335));
2145 assert_eq!(market.outcome, "0");
2146 }
2147
2148 #[rstest]
2149 fn test_event_contract_models_accept_missing_optional_fields() {
2150 let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2151 "seriesId": "BTC-ABOVE-DAILY"
2152 }))
2153 .unwrap();
2154 let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2155 "seriesId": "BTC-ABOVE-DAILY",
2156 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2157 "fixTime": "",
2158 "expTime": "",
2159 "state": "live"
2160 }))
2161 .unwrap();
2162 let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2163 "seriesId": "BTC-ABOVE-DAILY",
2164 "eventId": "BTC-ABOVE-DAILY-260224-1600",
2165 "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2166 "listTime": "",
2167 "fixTime": "",
2168 "expTime": "",
2169 "state": "live",
2170 "disputed": false,
2171 "outcome": "0",
2172 "floorStrike": "120000",
2173 "settleValue": ""
2174 }))
2175 .unwrap();
2176
2177 assert_eq!(series.freq, "");
2178 assert_eq!(series.settlement.underlying, "");
2179 assert_eq!(event.exp_time, None);
2180 assert_eq!(market.list_time, None);
2181 assert_eq!(market.exp_time, None);
2182 }
2183
2184 #[rstest]
2185 fn test_place_order_request_serializes_event_contract_fields() {
2186 let request = OKXPlaceOrderRequest {
2187 inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2188 td_mode: OKXTradeMode::Cash,
2189 ccy: None,
2190 cl_ord_id: Some("event-1".to_string()),
2191 tag: None,
2192 side: OKXSide::Buy,
2193 pos_side: None,
2194 ord_type: OKXOrderType::Limit,
2195 sz: "10".to_string(),
2196 px: Some("0.42".to_string()),
2197 px_usd: None,
2198 px_vol: None,
2199 reduce_only: None,
2200 tgt_ccy: None,
2201 attach_algo_ords: None,
2202 speed_bump: Some("1".to_string()),
2203 outcome: Some("yes".to_string()),
2204 slippage_pct: None,
2205 };
2206
2207 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2208
2209 assert_eq!(json["speedBump"], "1");
2210 assert_eq!(json["outcome"], "yes");
2211 }
2212
2213 #[rstest]
2214 fn test_attach_algo_ord_request_serializes_trailing_fields() {
2215 let request = OKXAttachAlgoOrdRequest {
2216 attach_algo_cl_ord_id: Some("trail-1".to_string()),
2217 sl_trigger_px: None,
2218 sl_ord_px: None,
2219 sl_trigger_px_type: None,
2220 tp_trigger_px: None,
2221 tp_ord_px: None,
2222 tp_trigger_px_type: None,
2223 callback_ratio: Some("0.01".to_string()),
2224 callback_spread: None,
2225 active_px: Some("64000".to_string()),
2226 new_callback_ratio: Some("0.02".to_string()),
2227 new_callback_spread: Some("25".to_string()),
2228 new_active_px: Some("65000".to_string()),
2229 };
2230
2231 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2232
2233 assert_eq!(json["callbackRatio"], "0.01");
2234 assert_eq!(json["activePx"], "64000");
2235 assert_eq!(json["newCallbackRatio"], "0.02");
2236 assert_eq!(json["newCallbackSpread"], "25");
2237 assert_eq!(json["newActivePx"], "65000");
2238 assert!(json.get("callbackSpread").is_none());
2239 }
2240
2241 #[rstest]
2242 fn test_place_order_response_deserializes_sub_code() {
2243 let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2244 "ordId": "",
2245 "clOrdId": "event-1",
2246 "sCode": "51000",
2247 "sMsg": "Parameter error",
2248 "subCode": "51004"
2249 }))
2250 .unwrap();
2251
2252 assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2253 assert_eq!(response.s_code, Some("51000".to_string()));
2254 assert_eq!(response.sub_code, Some("51004".to_string()));
2255 }
2256
2257 #[rstest]
2258 fn test_fee_rate_deserializes_settle() {
2259 let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2260 "level": "VIP1",
2261 "taker": "-0.0005",
2262 "maker": "-0.0002",
2263 "takerU": "-0.0005",
2264 "makerU": "-0.0002",
2265 "settle": "-0.001",
2266 "instType": "EVENTS",
2267 "category": "1",
2268 "ts": "1769697132335"
2269 }))
2270 .unwrap();
2271
2272 assert_eq!(fee_rate.settle, "-0.001");
2273 assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2274 }
2275}