Skip to main content

nautilus_okx/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data transfer objects for deserializing OKX HTTP API payloads.
17
18use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::parse::{
22    deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
23    deserialize_target_currency_as_none,
24};
25
26/// Represents a trade tick from the GET /api/v5/market/trades endpoint.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct OKXTrade {
30    /// Instrument ID.
31    pub inst_id: Ustr,
32    /// Trade price.
33    pub px: String,
34    /// Trade size.
35    pub sz: String,
36    /// Trade side: buy or sell.
37    pub side: OKXSide,
38    /// Trade ID assigned by OKX.
39    pub trade_id: Ustr,
40    /// Trade timestamp in milliseconds.
41    #[serde(deserialize_with = "deserialize_string_to_u64")]
42    pub ts: u64,
43}
44
45/// Represents a candlestick from the GET /api/v5/market/history-candles endpoint.
46/// The tuple contains [timestamp(ms), open, high, low, close, volume, turnover, base_volume, count].
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct OKXCandlestick(
49    /// Timestamp in milliseconds.
50    pub String,
51    /// Open price.
52    pub String,
53    /// High price.
54    pub String,
55    /// Low price.
56    pub String,
57    /// Close price.
58    pub String,
59    /// Volume.
60    pub String,
61    /// Turnover in quote currency.
62    pub String,
63    /// Base volume.
64    pub String,
65    /// Record count.
66    pub String,
67);
68
69use crate::common::{
70    enums::{
71        OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode, OKXOrderCategory,
72        OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXTargetCurrency, OKXTradeMode,
73        OKXTriggerType, OKXVipLevel,
74    },
75    parse::deserialize_string_to_u64,
76};
77
78/// Represents a mark price from the GET /api/v5/public/mark-price endpoint.
79#[derive(Clone, Debug, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct OKXMarkPrice {
82    /// Underlying.
83    pub uly: Option<Ustr>,
84    /// Instrument ID.
85    pub inst_id: Ustr,
86    /// The mark price.
87    pub mark_px: String,
88    /// The timestamp for the mark price.
89    #[serde(deserialize_with = "deserialize_string_to_u64")]
90    pub ts: u64,
91}
92
93/// Represents an option summary row from the GET /api/v5/public/opt-summary endpoint.
94#[derive(Clone, Debug, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct OKXOptionSummary {
97    /// Instrument type.
98    pub inst_type: OKXInstrumentType,
99    /// Instrument ID.
100    pub inst_id: Ustr,
101    /// Underlying index.
102    pub uly: Ustr,
103    /// Bid volatility.
104    pub bid_vol: String,
105    /// Ask volatility.
106    pub ask_vol: String,
107    /// Mark volatility.
108    pub mark_vol: String,
109    /// Forward price.
110    pub fwd_px: String,
111    /// Data timestamp in milliseconds.
112    #[serde(deserialize_with = "deserialize_string_to_u64")]
113    pub ts: u64,
114}
115
116/// Represents an index price from the GET /api/v5/public/index-tickers endpoint.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct OKXIndexTicker {
120    /// Instrument ID.
121    pub inst_id: Ustr,
122    /// The index price.
123    pub idx_px: String,
124    /// The timestamp for the index price.
125    #[serde(deserialize_with = "deserialize_string_to_u64")]
126    pub ts: u64,
127}
128
129/// Represents an order book level from the GET /api/v5/market/books endpoint.
130/// Each entry is a 4-element tuple: [price, size, liquidated_orders, num_orders].
131pub type OKXOrderBookLevel = (String, String, String, String);
132
133/// Represents an order book snapshot from the GET /api/v5/market/books endpoint.
134#[derive(Clone, Debug, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct OKXOrderBookSnapshot {
137    /// Ask levels [price, size, liquidated_orders_count, orders_count].
138    pub asks: Vec<OKXOrderBookLevel>,
139    /// Bid levels [price, size, liquidated_orders_count, orders_count].
140    pub bids: Vec<OKXOrderBookLevel>,
141    /// Timestamp in milliseconds.
142    #[serde(deserialize_with = "deserialize_string_to_u64")]
143    pub ts: u64,
144}
145
146/// Represents a funding rate history entry from the GET /api/v5/public/funding-rate-history endpoint.
147#[derive(Clone, Debug, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct OKXFundingRateHistory {
150    /// Instrument type.
151    pub inst_type: OKXInstrumentType,
152    /// Instrument ID.
153    pub inst_id: Ustr,
154    /// Funding rate.
155    pub funding_rate: String,
156    /// Realized rate.
157    pub realized_rate: String,
158    /// Funding time, Unix timestamp in milliseconds.
159    #[serde(deserialize_with = "deserialize_string_to_u64")]
160    pub funding_time: u64,
161    /// Funding rate calculation method.
162    #[serde(default)]
163    pub method: Option<String>,
164}
165
166/// Represents a position tier from the GET /api/v5/public/position-tiers endpoint.
167#[derive(Clone, Debug, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct OKXPositionTier {
170    /// Underlying.
171    pub uly: Ustr,
172    /// Instrument family.
173    pub inst_family: String,
174    /// Instrument ID.
175    pub inst_id: Ustr,
176    /// Tier level.
177    pub tier: String,
178    /// Minimum size/amount for the tier.
179    pub min_sz: String,
180    /// Maximum size/amount for the tier.
181    pub max_sz: String,
182    /// Maintenance margin requirement rate.
183    pub mmr: String,
184    /// Initial margin requirement rate.
185    pub imr: String,
186    /// Maximum available leverage.
187    pub max_lever: String,
188    /// Option Margin Coefficient (only applicable to options).
189    pub opt_mgn_factor: String,
190    /// Quote currency borrowing amount.
191    pub quote_max_loan: String,
192    /// Base currency borrowing amount.
193    pub base_max_loan: String,
194}
195
196/// Represents an account balance snapshot from `GET /api/v5/account/balance`.
197#[derive(Clone, Debug, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct OKXAccount {
200    /// Adjusted/Effective equity in USD.
201    pub adj_eq: String,
202    /// Borrow frozen amount.
203    pub borrow_froz: String,
204    /// Account details by currency.
205    pub details: Vec<OKXBalanceDetail>,
206    /// Initial margin requirement.
207    pub imr: String,
208    /// Isolated margin equity.
209    pub iso_eq: String,
210    /// Margin ratio.
211    pub mgn_ratio: String,
212    /// Maintenance margin requirement.
213    pub mmr: String,
214    /// Notional value in USD for borrow.
215    pub notional_usd_for_borrow: String,
216    /// Notional value in USD for futures.
217    pub notional_usd_for_futures: String,
218    /// Notional value in USD for option.
219    pub notional_usd_for_option: String,
220    /// Notional value in USD for swap.
221    pub notional_usd_for_swap: String,
222    /// Notional value in USD.
223    pub notional_usd: String,
224    /// Order frozen.
225    pub ord_froz: String,
226    /// Total equity in USD.
227    pub total_eq: String,
228    /// Last update time, Unix timestamp in milliseconds.
229    #[serde(deserialize_with = "deserialize_string_to_u64")]
230    pub u_time: u64,
231    /// Unrealized profit and loss.
232    pub upl: String,
233}
234
235/// Represents a balance detail for a single currency in an OKX account.
236#[derive(Clone, Debug, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
239#[cfg_attr(
240    feature = "python",
241    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.okx")
242)]
243pub struct OKXBalanceDetail {
244    /// Available balance.
245    pub avail_bal: String,
246    /// Available equity.
247    pub avail_eq: String,
248    /// Borrow frozen amount.
249    pub borrow_froz: String,
250    /// Cash balance.
251    pub cash_bal: String,
252    /// Currency.
253    pub ccy: Ustr,
254    /// Cross liability.
255    pub cross_liab: String,
256    /// Discount equity in USD.
257    pub dis_eq: String,
258    /// Equity.
259    pub eq: String,
260    /// Equity in USD.
261    pub eq_usd: String,
262    /// Same-token equity.
263    pub smt_sync_eq: String,
264    /// Copy trading equity.
265    pub spot_copy_trading_eq: String,
266    /// Fixed balance.
267    pub fixed_bal: String,
268    /// Frozen balance.
269    pub frozen_bal: String,
270    /// Initial margin requirement.
271    pub imr: String,
272    /// Interest.
273    pub interest: String,
274    /// Isolated margin equity.
275    pub iso_eq: String,
276    /// Isolated margin liability.
277    pub iso_liab: String,
278    /// Isolated unrealized profit and loss.
279    pub iso_upl: String,
280    /// Liability.
281    pub liab: String,
282    /// Maximum loan amount.
283    pub max_loan: String,
284    /// Margin ratio.
285    pub mgn_ratio: String,
286    /// Maintenance margin requirement.
287    pub mmr: String,
288    /// Notional leverage.
289    pub notional_lever: String,
290    /// Order frozen.
291    pub ord_frozen: String,
292    /// Reward balance.
293    pub reward_bal: String,
294    /// Spot in use amount.
295    #[serde(alias = "spotInUse")]
296    pub spot_in_use_amt: String,
297    /// Cross liability spot in use amount.
298    #[serde(alias = "clSpotInUse")]
299    pub cl_spot_in_use_amt: String,
300    /// Maximum spot in use amount.
301    #[serde(alias = "maxSpotInUse")]
302    pub max_spot_in_use_amt: String,
303    /// Spot isolated balance.
304    pub spot_iso_bal: String,
305    /// Strategy equity.
306    pub stgy_eq: String,
307    /// Time-weighted average price.
308    pub twap: String,
309    /// Last update time, Unix timestamp in milliseconds.
310    #[serde(deserialize_with = "deserialize_string_to_u64")]
311    pub u_time: u64,
312    /// Unrealized profit and loss.
313    pub upl: String,
314    /// Unrealized profit and loss liability.
315    pub upl_liab: String,
316    /// Spot balance.
317    pub spot_bal: String,
318    /// Open average price.
319    pub open_avg_px: String,
320    /// Accumulated average price.
321    pub acc_avg_px: String,
322    /// Spot unrealized profit and loss.
323    pub spot_upl: String,
324    /// Spot unrealized profit and loss ratio.
325    pub spot_upl_ratio: String,
326    /// Total profit and loss.
327    pub total_pnl: String,
328    /// Total profit and loss ratio.
329    pub total_pnl_ratio: String,
330}
331
332/// Represents a single open position from `GET /api/v5/account/positions`.
333#[derive(Clone, Debug, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct OKXPosition {
336    /// Instrument ID.
337    pub inst_id: Ustr,
338    /// Instrument type.
339    pub inst_type: OKXInstrumentType,
340    /// Margin mode: isolated/cross.
341    pub mgn_mode: OKXMarginMode,
342    /// Position ID.
343    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
344    pub pos_id: Option<Ustr>,
345    /// Position side: long/short.
346    pub pos_side: OKXPositionSide,
347    /// Position size.
348    pub pos: String,
349    /// Base currency balance.
350    pub base_bal: String,
351    /// Position currency.
352    pub ccy: String,
353    /// Trading fee.
354    pub fee: String,
355    /// Position leverage.
356    pub lever: String,
357    /// Last traded price.
358    pub last: String,
359    /// Mark price.
360    pub mark_px: String,
361    /// Liquidation price.
362    pub liq_px: String,
363    /// Maintenance margin requirement.
364    pub mmr: String,
365    /// Interest.
366    pub interest: String,
367    /// Trade ID.
368    pub trade_id: Ustr,
369    /// Notional value of position in USD.
370    pub notional_usd: String,
371    /// Average entry price.
372    pub avg_px: String,
373    /// Unrealized profit and loss.
374    pub upl: String,
375    /// Unrealized profit and loss ratio.
376    pub upl_ratio: String,
377    /// Last update time, Unix timestamp in milliseconds.
378    #[serde(deserialize_with = "deserialize_string_to_u64")]
379    pub u_time: u64,
380    /// Position margin.
381    pub margin: String,
382    /// Margin ratio.
383    pub mgn_ratio: String,
384    /// Auto-deleveraging (ADL) ranking.
385    pub adl: String,
386    /// Creation time, Unix timestamp in milliseconds.
387    pub c_time: String,
388    /// Realized profit and loss.
389    pub realized_pnl: String,
390    /// Unrealized profit and loss at last price.
391    pub upl_last_px: String,
392    /// Unrealized profit and loss ratio at last price.
393    pub upl_ratio_last_px: String,
394    /// Available position that can be closed.
395    pub avail_pos: String,
396    /// Breakeven price.
397    pub be_px: String,
398    /// Funding fee.
399    pub funding_fee: String,
400    /// Index price.
401    pub idx_px: String,
402    /// Liquidation penalty.
403    pub liq_penalty: String,
404    /// Option value.
405    pub opt_val: String,
406    /// Pending close order liability value.
407    pub pending_close_ord_liab_val: String,
408    /// Total profit and loss.
409    pub pnl: String,
410    /// Position currency.
411    pub pos_ccy: String,
412    /// Quote currency balance.
413    pub quote_bal: String,
414    /// Borrowed amount in quote currency.
415    pub quote_borrowed: String,
416    /// Interest on quote currency.
417    pub quote_interest: String,
418    /// Amount in use for spot trading.
419    #[serde(alias = "spotInUse")]
420    pub spot_in_use_amt: String,
421    /// Currency in use for spot trading.
422    pub spot_in_use_ccy: String,
423    /// USD price.
424    pub usd_px: String,
425    /// Black-Scholes delta in dollars, only applicable to OPTION.
426    #[serde(default)]
427    pub delta_bs: String,
428    /// Black-Scholes gamma in dollars, only applicable to OPTION.
429    #[serde(default)]
430    pub gamma_bs: String,
431    /// Black-Scholes theta in dollars, only applicable to OPTION.
432    #[serde(default)]
433    pub theta_bs: String,
434    /// Black-Scholes vega in dollars, only applicable to OPTION.
435    #[serde(default)]
436    pub vega_bs: String,
437}
438
439/// Represents the response from `POST /api/v5/trade/order` (place order).
440/// This model is designed to be flexible and handle the minimal fields that the API returns.
441#[derive(Clone, Debug, Serialize, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct OKXPlaceOrderResponse {
444    /// Order ID.
445    #[serde(default)]
446    pub ord_id: Option<Ustr>,
447    /// Client order ID.
448    #[serde(default)]
449    pub cl_ord_id: Option<Ustr>,
450    /// Order tag.
451    #[serde(default)]
452    pub tag: Option<String>,
453    /// Instrument ID (optional - might not be in response).
454    #[serde(default)]
455    pub inst_id: Option<Ustr>,
456    /// Order side (optional).
457    #[serde(default)]
458    pub side: Option<OKXSide>,
459    /// Order type (optional).
460    #[serde(default)]
461    pub ord_type: Option<OKXOrderType>,
462    /// Order size (optional).
463    #[serde(default)]
464    pub sz: Option<String>,
465    /// Order state (optional).
466    pub state: Option<OKXOrderStatus>,
467    /// Price (optional).
468    #[serde(default)]
469    pub px: Option<String>,
470    /// Average price (optional).
471    #[serde(default)]
472    pub avg_px: Option<String>,
473    /// Accumulated filled size.
474    #[serde(default)]
475    pub acc_fill_sz: Option<String>,
476    /// Fill size (optional).
477    #[serde(default)]
478    pub fill_sz: Option<String>,
479    /// Fill price (optional).
480    #[serde(default)]
481    pub fill_px: Option<String>,
482    /// Trade ID (optional).
483    #[serde(default)]
484    pub trade_id: Option<Ustr>,
485    /// Fill time (optional).
486    #[serde(default)]
487    pub fill_time: Option<String>,
488    /// Fee (optional).
489    #[serde(default)]
490    pub fee: Option<String>,
491    /// Fee currency (optional).
492    #[serde(default)]
493    pub fee_ccy: Option<String>,
494    /// Request ID (optional).
495    #[serde(default)]
496    pub req_id: Option<Ustr>,
497    /// Position side (optional).
498    #[serde(default)]
499    pub pos_side: Option<OKXPositionSide>,
500    /// Reduce-only flag (optional).
501    #[serde(default)]
502    pub reduce_only: Option<String>,
503    /// Target currency (optional).
504    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
505    pub tgt_ccy: Option<OKXTargetCurrency>,
506    /// Creation time.
507    #[serde(default)]
508    pub c_time: Option<String>,
509    /// Last update time (optional).
510    #[serde(default)]
511    pub u_time: Option<String>,
512    /// The result of the request.
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub s_code: Option<String>,
515    /// Error message if the request failed.
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub s_msg: Option<String>,
518}
519
520/// Represents an attached TP/SL instruction on `POST /api/v5/trade/order`.
521#[derive(Clone, Debug, Default, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct OKXAttachAlgoOrdRequest {
524    /// Client order ID for the attached TP/SL OCO object.
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub attach_algo_cl_ord_id: Option<String>,
527    /// Stop-loss trigger price.
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub sl_trigger_px: Option<String>,
530    /// Stop-loss order price.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub sl_ord_px: Option<String>,
533    /// Stop-loss trigger price type.
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub sl_trigger_px_type: Option<OKXTriggerType>,
536    /// Take-profit trigger price.
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub tp_trigger_px: Option<String>,
539    /// Take-profit order price.
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub tp_ord_px: Option<String>,
542    /// Take-profit trigger price type.
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub tp_trigger_px_type: Option<OKXTriggerType>,
545}
546
547/// Represents the request body for `POST /api/v5/trade/order` (place order).
548#[derive(Clone, Debug, Serialize, Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct OKXPlaceOrderRequest {
551    /// Instrument ID.
552    pub inst_id: String,
553    /// Trade mode (cash, cross, isolated).
554    pub td_mode: OKXTradeMode,
555    /// Currency used for margin trading when required by OKX.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub ccy: Option<String>,
558    /// Client-supplied order ID.
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub cl_ord_id: Option<String>,
561    /// Order tag.
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub tag: Option<String>,
564    /// Order side (buy, sell).
565    pub side: OKXSide,
566    /// Position side for derivatives.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub pos_side: Option<OKXPositionSide>,
569    /// Order type.
570    pub ord_type: OKXOrderType,
571    /// Order size.
572    pub sz: String,
573    /// Limit price when required by the order type.
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub px: Option<String>,
576    /// Price in USD, only applicable to options. Mutually exclusive with `px` and `px_vol`.
577    #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
578    pub px_usd: Option<String>,
579    /// Price in implied volatility (1 = 100%), only applicable to options.
580    /// Mutually exclusive with `px` and `px_usd`.
581    #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
582    pub px_vol: Option<String>,
583    /// Reduce-only flag.
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub reduce_only: Option<bool>,
586    /// Target currency for spot market orders.
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub tgt_ccy: Option<OKXTargetCurrency>,
589    /// Attached TP/SL OCO instructions.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
592}
593
594pub use crate::common::models::OKXAttachedAlgoOrd;
595
596/// Represents a single historical order record from `GET /api/v5/trade/orders-history`.
597#[derive(Clone, Debug, Serialize, Deserialize)]
598#[serde(rename_all = "camelCase")]
599pub struct OKXOrderHistory {
600    /// Order ID.
601    pub ord_id: Ustr,
602    /// Client order ID.
603    pub cl_ord_id: Ustr,
604    /// Algo order ID (for conditional orders).
605    #[serde(default)]
606    pub algo_id: Option<Ustr>,
607    /// Client-supplied algo order ID (for conditional orders).
608    #[serde(default)]
609    pub algo_cl_ord_id: Option<Ustr>,
610    /// Attached child client order ID if OKX surfaces one at the top level.
611    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
612    pub attach_algo_cl_ord_id: Option<String>,
613    /// Attached TP/SL child orders associated with the parent order.
614    #[serde(default)]
615    pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
616    /// Client account ID (may be omitted by OKX).
617    #[serde(default)]
618    pub cl_act_id: Option<Ustr>,
619    /// Order tag.
620    pub tag: String,
621    /// Instrument type.
622    pub inst_type: OKXInstrumentType,
623    /// Underlying (optional).
624    pub uly: Option<Ustr>,
625    /// Instrument ID.
626    pub inst_id: Ustr,
627    /// Order type.
628    pub ord_type: OKXOrderType,
629    /// Order size.
630    pub sz: String,
631    /// Price (optional).
632    pub px: String,
633    /// Price in USD (options only).
634    #[serde(default)]
635    pub px_usd: String,
636    /// Price in implied volatility (options only).
637    #[serde(default)]
638    pub px_vol: String,
639    /// Side.
640    pub side: OKXSide,
641    /// Position side.
642    pub pos_side: OKXPositionSide,
643    /// Trade mode.
644    pub td_mode: OKXTradeMode,
645    /// Reduce-only flag.
646    pub reduce_only: String,
647    /// Target currency (optional).
648    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
649    pub tgt_ccy: Option<OKXTargetCurrency>,
650    /// Order state.
651    pub state: OKXOrderStatus,
652    /// Average price (optional).
653    pub avg_px: String,
654    /// Execution fee.
655    pub fee: String,
656    /// Fee currency.
657    pub fee_ccy: String,
658    /// Filled size (optional).
659    pub fill_sz: String,
660    /// Fill price (optional).
661    pub fill_px: String,
662    /// Trade ID (optional).
663    pub trade_id: Ustr,
664    /// Fill time, Unix timestamp in milliseconds.
665    #[serde(deserialize_with = "deserialize_string_to_u64")]
666    pub fill_time: u64,
667    /// Accumulated filled size.
668    pub acc_fill_sz: String,
669    /// Fill fee (optional, may be omitted).
670    #[serde(default)]
671    pub fill_fee: Option<String>,
672    /// Request ID (optional).
673    #[serde(default)]
674    pub req_id: Option<Ustr>,
675    /// Cancelled filled size (optional).
676    #[serde(default)]
677    pub cancel_fill_sz: Option<String>,
678    /// Cancelled total size (optional).
679    #[serde(default)]
680    pub cancel_total_sz: Option<String>,
681    /// Fee discount (optional).
682    #[serde(default)]
683    pub fee_discount: Option<String>,
684    /// Order category (normal, liquidation, ADL, etc.).
685    pub category: OKXOrderCategory,
686    /// Last update time, Unix timestamp in milliseconds.
687    #[serde(deserialize_with = "deserialize_string_to_u64")]
688    pub u_time: u64,
689    /// Creation time.
690    #[serde(deserialize_with = "deserialize_string_to_u64")]
691    pub c_time: u64,
692}
693
694/// Represents an algo order response from `/trade/order-algo-*` endpoints.
695#[derive(Clone, Debug, Serialize, Deserialize)]
696#[serde(rename_all = "camelCase")]
697pub struct OKXOrderAlgo {
698    /// Algo order ID assigned by OKX.
699    pub algo_id: String,
700    /// Client-specified algo order ID.
701    #[serde(default)]
702    pub algo_cl_ord_id: String,
703    /// Client order ID (empty until triggered).
704    #[serde(default)]
705    pub cl_ord_id: String,
706    /// Venue order ID (empty until triggered).
707    #[serde(default)]
708    pub ord_id: String,
709    /// Instrument ID, e.g. `ETH-USDT-SWAP`.
710    pub inst_id: Ustr,
711    /// Instrument type.
712    pub inst_type: OKXInstrumentType,
713    /// Algo order type.
714    pub ord_type: OKXAlgoOrderType,
715    /// Current order state.
716    pub state: OKXOrderStatus,
717    /// Order side.
718    pub side: OKXSide,
719    /// Position side.
720    pub pos_side: OKXPositionSide,
721    /// Submitted size.
722    #[serde(default)]
723    pub sz: String,
724    /// Trigger price (empty for certain algo styles).
725    #[serde(default)]
726    pub trigger_px: String,
727    /// Trigger price type (last/mark/index).
728    #[serde(default)]
729    pub trigger_px_type: Option<OKXTriggerType>,
730    /// Stop-loss trigger price for conditional close orders.
731    #[serde(default)]
732    pub sl_trigger_px: String,
733    /// Stop-loss order price for conditional close orders.
734    #[serde(default)]
735    pub sl_ord_px: String,
736    /// Stop-loss trigger price type (last/mark/index).
737    #[serde(default)]
738    pub sl_trigger_px_type: Option<OKXTriggerType>,
739    /// Take-profit trigger price for conditional close orders.
740    #[serde(default)]
741    pub tp_trigger_px: String,
742    /// Take-profit order price for conditional close orders.
743    #[serde(default)]
744    pub tp_ord_px: String,
745    /// Take-profit trigger price type (last/mark/index).
746    #[serde(default)]
747    pub tp_trigger_px_type: Option<OKXTriggerType>,
748    /// Order price (-1 indicates market execution once triggered).
749    #[serde(default)]
750    pub ord_px: String,
751    /// Trade mode (cash/cross/isolated).
752    pub td_mode: OKXTradeMode,
753    /// Algo leverage configuration.
754    #[serde(default)]
755    pub lever: String,
756    /// Reduce-only flag.
757    #[serde(default)]
758    pub reduce_only: String,
759    /// Fraction of the position to close for close-order algos.
760    #[serde(default)]
761    pub close_fraction: String,
762    /// Executed price (if triggered).
763    #[serde(default)]
764    pub actual_px: String,
765    /// Executed size (if triggered).
766    #[serde(default)]
767    pub actual_sz: String,
768    /// Notional value in USD.
769    #[serde(default)]
770    pub notional_usd: String,
771    /// Creation time (milliseconds).
772    #[serde(deserialize_with = "deserialize_string_to_u64")]
773    pub c_time: u64,
774    /// Last update time (milliseconds).
775    #[serde(deserialize_with = "deserialize_string_to_u64")]
776    pub u_time: u64,
777    /// Trigger timestamp (if triggered).
778    #[serde(default)]
779    pub trigger_time: String,
780    /// Optional tag supplied during submission.
781    #[serde(default)]
782    pub tag: String,
783    /// Callback price ratio for trailing stop (e.g. "0.01" for 1%).
784    #[serde(default)]
785    pub callback_ratio: String,
786    /// Callback price spread for trailing stop (absolute distance).
787    #[serde(default)]
788    pub callback_spread: String,
789    /// Activation price for trailing stop.
790    #[serde(default)]
791    pub active_px: String,
792}
793
794/// Represents a transaction detail (fill) from `GET /api/v5/trade/fills`.
795#[derive(Clone, Debug, Serialize, Deserialize)]
796#[serde(rename_all = "camelCase")]
797pub struct OKXTransactionDetail {
798    /// Product type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
799    pub inst_type: OKXInstrumentType,
800    /// Instrument ID, e.g. "BTC-USDT".
801    pub inst_id: Ustr,
802    /// Trade ID.
803    pub trade_id: Ustr,
804    /// Order ID.
805    pub ord_id: Ustr,
806    /// Client order ID.
807    pub cl_ord_id: Ustr,
808    /// Bill ID.
809    pub bill_id: Ustr,
810    /// Last filled price.
811    pub fill_px: String,
812    /// Last filled quantity.
813    pub fill_sz: String,
814    /// Trade side: buy or sell.
815    pub side: OKXSide,
816    /// Execution type.
817    pub exec_type: OKXExecType,
818    /// Fee currency.
819    pub fee_ccy: String,
820    /// Fee amount.
821    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
822    pub fee: Option<String>,
823    /// Timestamp, Unix timestamp format in milliseconds.
824    #[serde(deserialize_with = "deserialize_string_to_u64")]
825    pub ts: u64,
826}
827
828/// Represents a single historical position record from `GET /api/v5/account/positions-history`.
829#[derive(Clone, Debug, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct OKXPositionHistory {
832    /// Instrument type (e.g. "SWAP", "FUTURES", etc.).
833    pub inst_type: OKXInstrumentType,
834    /// Instrument ID (e.g. "BTC-USD-SWAP").
835    pub inst_id: Ustr,
836    /// Margin mode: e.g. "cross", "isolated".
837    pub mgn_mode: OKXMarginMode,
838    /// The type of the last close, e.g. "1" (close partially), "2" (close all), etc.
839    /// See OKX docs for the meaning of each numeric code.
840    #[serde(rename = "type")]
841    pub r#type: Ustr,
842    /// Creation time of the position (Unix timestamp in milliseconds).
843    pub c_time: String,
844    /// Last update time, Unix timestamp in milliseconds.
845    #[serde(deserialize_with = "deserialize_string_to_u64")]
846    pub u_time: u64,
847    /// Average price of opening position.
848    pub open_avg_px: String,
849    /// Average price of closing position (if applicable).
850    #[serde(skip_serializing_if = "Option::is_none")]
851    pub close_avg_px: Option<String>,
852    /// The position ID.
853    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
854    pub pos_id: Option<Ustr>,
855    /// Max quantity of the position at open time.
856    #[serde(skip_serializing_if = "Option::is_none")]
857    pub open_max_pos: Option<String>,
858    /// Cumulative closed volume of the position.
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub close_total_pos: Option<String>,
861    /// Realized profit and loss (only for FUTURES/SWAP/OPTION).
862    #[serde(skip_serializing_if = "Option::is_none")]
863    pub realized_pnl: Option<String>,
864    /// Accumulated fee for the position.
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub fee: Option<String>,
867    /// Accumulated funding fee (for perpetual swaps).
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub funding_fee: Option<String>,
870    /// Accumulated liquidation penalty. Negative if there was a penalty.
871    #[serde(skip_serializing_if = "Option::is_none")]
872    pub liq_penalty: Option<String>,
873    /// Profit and loss (realized or unrealized depending on status).
874    #[serde(skip_serializing_if = "Option::is_none")]
875    pub pnl: Option<String>,
876    /// PnL ratio.
877    #[serde(skip_serializing_if = "Option::is_none")]
878    pub pnl_ratio: Option<String>,
879    /// Position side: "long" / "short" / "net".
880    pub pos_side: OKXPositionSide,
881    /// Leverage used (the JSON field is "lev", but we rename it in Rust).
882    pub lever: String,
883    /// Direction: "long" or "short" (only for MARGIN/FUTURES/SWAP/OPTION).
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub direction: Option<String>,
886    /// Trigger mark price. Populated if `type` indicates liquidation or ADL.
887    #[serde(skip_serializing_if = "Option::is_none")]
888    pub trigger_px: Option<String>,
889    /// The underlying (e.g. "BTC-USD" for futures or swap).
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub uly: Option<String>,
892    /// Currency (e.g. "BTC"). May or may not appear in all responses.
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub ccy: Option<String>,
895}
896
897/// Represents the request body for `POST /api/v5/trade/order-algo` (place algo order).
898#[derive(Clone, Debug, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct OKXPlaceAlgoOrderRequest {
901    /// Instrument ID.
902    #[serde(rename = "instId")]
903    pub inst_id: String,
904    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
905    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
906    pub inst_id_code: Option<u64>,
907    /// Trade mode (isolated, cross, cash).
908    #[serde(rename = "tdMode")]
909    pub td_mode: OKXTradeMode,
910    /// Order side (buy, sell).
911    pub side: OKXSide,
912    /// Algo order type (trigger, conditional, move_order_stop, etc.).
913    #[serde(rename = "ordType")]
914    pub ord_type: OKXAlgoOrderType,
915    /// Order size. Omitted for `closeFraction` close orders.
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub sz: Option<String>,
918    /// Client-supplied algo order ID.
919    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
920    pub algo_cl_ord_id: Option<String>,
921    /// Trigger price.
922    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
923    pub trigger_px: Option<String>,
924    /// Order price (for limit orders).
925    #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
926    pub order_px: Option<String>,
927    /// Trigger type (last, mark, index).
928    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
929    pub trigger_px_type: Option<OKXTriggerType>,
930    /// Stop-loss trigger price for conditional close orders.
931    #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
932    pub sl_trigger_px: Option<String>,
933    /// Stop-loss order price for conditional close orders.
934    #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
935    pub sl_ord_px: Option<String>,
936    /// Stop-loss trigger type (last, mark, index).
937    #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
938    pub sl_trigger_px_type: Option<OKXTriggerType>,
939    /// Take-profit trigger price for conditional close orders.
940    #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
941    pub tp_trigger_px: Option<String>,
942    /// Take-profit order price for conditional close orders.
943    #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
944    pub tp_ord_px: Option<String>,
945    /// Take-profit trigger type (last, mark, index).
946    #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
947    pub tp_trigger_px_type: Option<OKXTriggerType>,
948    /// Target currency (base_ccy or quote_ccy).
949    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
950    pub tgt_ccy: Option<OKXTargetCurrency>,
951    /// Position side (net, long, short).
952    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
953    pub pos_side: Option<OKXPositionSide>,
954    /// Whether to close position.
955    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
956    pub close_position: Option<bool>,
957    /// Order tag.
958    #[serde(skip_serializing_if = "Option::is_none")]
959    pub tag: Option<String>,
960    /// Whether it's a reduce-only order.
961    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
962    pub reduce_only: Option<bool>,
963    /// Fraction of the position to close for eligible algo close orders.
964    #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
965    pub close_fraction: Option<String>,
966    /// Callback rate for trailing stop (e.g., "0.01" for 1%). Either this or
967    /// `callback_spread` is required for `move_order_stop` orders.
968    #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
969    pub callback_ratio: Option<String>,
970    /// Callback spread for trailing stop (fixed price distance). Either this or
971    /// `callback_ratio` is required for `move_order_stop` orders.
972    #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
973    pub callback_spread: Option<String>,
974    /// Activation price for trailing stop. If empty, the trailing stop
975    /// activates immediately when placed.
976    #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
977    pub active_px: Option<String>,
978}
979
980/// Represents the response from `POST /api/v5/trade/order-algo` (place algo order).
981#[derive(Clone, Debug, Serialize, Deserialize)]
982#[serde(rename_all = "camelCase")]
983pub struct OKXPlaceAlgoOrderResponse {
984    /// Algo order ID.
985    pub algo_id: String,
986    /// Client-supplied algo order ID.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub algo_cl_ord_id: Option<String>,
989    /// The result of the request.
990    #[serde(skip_serializing_if = "Option::is_none")]
991    pub s_code: Option<String>,
992    /// Error message if the request failed.
993    #[serde(skip_serializing_if = "Option::is_none")]
994    pub s_msg: Option<String>,
995    /// Request ID.
996    #[serde(skip_serializing_if = "Option::is_none")]
997    pub req_id: Option<String>,
998}
999
1000/// Represents the request body for `POST /api/v5/trade/cancel-algos` (cancel algo order).
1001#[derive(Clone, Debug, Serialize, Deserialize)]
1002#[serde(rename_all = "camelCase")]
1003pub struct OKXCancelAlgoOrderRequest {
1004    /// Instrument ID.
1005    pub inst_id: String,
1006    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
1007    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1008    pub inst_id_code: Option<u64>,
1009    /// Algo order ID.
1010    #[serde(skip_serializing_if = "Option::is_none")]
1011    pub algo_id: Option<String>,
1012    /// Client-supplied algo order ID.
1013    #[serde(skip_serializing_if = "Option::is_none")]
1014    pub algo_cl_ord_id: Option<String>,
1015}
1016
1017/// Represents the response from `POST /api/v5/trade/cancel-algos` (cancel algo order).
1018#[derive(Clone, Debug, Serialize, Deserialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct OKXCancelAlgoOrderResponse {
1021    /// Algo order ID.
1022    pub algo_id: String,
1023    /// The result of the request.
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    pub s_code: Option<String>,
1026    /// Error message if the request failed.
1027    #[serde(skip_serializing_if = "Option::is_none")]
1028    pub s_msg: Option<String>,
1029}
1030
1031/// Represents the request body for `POST /api/v5/trade/amend-algos` (amend algo order).
1032#[derive(Clone, Debug, Serialize, Deserialize)]
1033#[serde(rename_all = "camelCase")]
1034pub struct OKXAmendAlgoOrderRequest {
1035    /// Instrument ID.
1036    pub inst_id: String,
1037    /// Algo order ID.
1038    pub algo_id: String,
1039    /// Client-supplied algo order ID.
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub algo_cl_ord_id: Option<String>,
1042    /// New order size.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub new_sz: Option<String>,
1045    /// New trigger price (for trigger/conditional orders).
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub new_trigger_px: Option<String>,
1048    /// New order price (for limit orders after trigger).
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub new_order_px: Option<String>,
1051    /// New callback ratio for trailing stop (e.g., "0.01" for 1%).
1052    #[serde(skip_serializing_if = "Option::is_none")]
1053    pub new_callback_ratio: Option<String>,
1054    /// New callback spread for trailing stop (fixed price distance).
1055    #[serde(skip_serializing_if = "Option::is_none")]
1056    pub new_callback_spread: Option<String>,
1057    /// New activation price for trailing stop.
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub new_active_px: Option<String>,
1060}
1061
1062/// Represents the response from `POST /api/v5/trade/amend-algos` (amend algo order).
1063#[derive(Clone, Debug, Serialize, Deserialize)]
1064#[serde(rename_all = "camelCase")]
1065pub struct OKXAmendAlgoOrderResponse {
1066    /// Algo order ID.
1067    pub algo_id: String,
1068    /// Client-supplied algo order ID.
1069    #[serde(skip_serializing_if = "Option::is_none")]
1070    pub algo_cl_ord_id: Option<String>,
1071    /// The result of the request.
1072    #[serde(skip_serializing_if = "Option::is_none")]
1073    pub s_code: Option<String>,
1074    /// Error message if the request failed.
1075    #[serde(skip_serializing_if = "Option::is_none")]
1076    pub s_msg: Option<String>,
1077    /// Request ID.
1078    #[serde(skip_serializing_if = "Option::is_none")]
1079    pub req_id: Option<String>,
1080}
1081
1082/// Represents the response from `GET /api/v5/public/time` (get system time).
1083#[derive(Clone, Debug, Serialize, Deserialize)]
1084#[serde(rename_all = "camelCase")]
1085pub struct OKXServerTime {
1086    /// Server timestamp in milliseconds.
1087    #[serde(deserialize_with = "deserialize_string_to_u64")]
1088    pub ts: u64,
1089}
1090
1091/// Represents a fee rate entry from `GET /api/v5/account/trade-fee`.
1092#[derive(Clone, Debug, Serialize, Deserialize)]
1093#[serde(rename_all = "camelCase")]
1094pub struct OKXFeeRate {
1095    /// Fee level (VIP tier) - indicates the user's VIP tier (0-9).
1096    #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1097    pub level: OKXVipLevel,
1098    /// Taker fee rate for crypto-margined contracts.
1099    pub taker: String,
1100    /// Maker fee rate for crypto-margined contracts.
1101    pub maker: String,
1102    /// Taker fee rate for USDT-margined contracts.
1103    pub taker_u: String,
1104    /// Maker fee rate for USDT-margined contracts.
1105    pub maker_u: String,
1106    /// Delivery fee rate.
1107    #[serde(default)]
1108    pub delivery: String,
1109    /// Option exercise fee rate.
1110    #[serde(default)]
1111    pub exercise: String,
1112    /// Instrument type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
1113    pub inst_type: OKXInstrumentType,
1114    /// Fee schedule category (being deprecated).
1115    #[serde(default)]
1116    pub category: String,
1117    /// Data return timestamp (Unix timestamp in milliseconds).
1118    #[serde(deserialize_with = "deserialize_string_to_u64")]
1119    pub ts: u64,
1120}
1121
1122#[cfg(test)]
1123mod tests {
1124    use rstest::rstest;
1125    use serde_json;
1126
1127    use super::*;
1128
1129    #[rstest]
1130    fn test_algo_order_request_serialization() {
1131        let request = OKXPlaceAlgoOrderRequest {
1132            inst_id: "ETH-USDT-SWAP".to_string(),
1133            inst_id_code: None,
1134            td_mode: OKXTradeMode::Isolated,
1135            side: OKXSide::Buy,
1136            ord_type: OKXAlgoOrderType::Trigger,
1137            sz: Some("0.01".to_string()),
1138            algo_cl_ord_id: Some("test123".to_string()),
1139            trigger_px: Some("3000".to_string()),
1140            order_px: Some("-1".to_string()),
1141            trigger_px_type: Some(OKXTriggerType::Last),
1142            sl_trigger_px: None,
1143            sl_ord_px: None,
1144            sl_trigger_px_type: None,
1145            tp_trigger_px: None,
1146            tp_ord_px: None,
1147            tp_trigger_px_type: None,
1148            tgt_ccy: None,
1149            pos_side: None,
1150            close_position: None,
1151            tag: None,
1152            reduce_only: None,
1153            close_fraction: None,
1154            callback_ratio: None,
1155            callback_spread: None,
1156            active_px: None,
1157        };
1158
1159        let json = serde_json::to_string(&request).unwrap();
1160
1161        // Verify that fields are serialized with correct camelCase names
1162        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1163        assert!(json.contains("\"tdMode\":\"isolated\""));
1164        assert!(json.contains("\"ordType\":\"trigger\""));
1165        assert!(json.contains("\"algoClOrdId\":\"test123\""));
1166        assert!(json.contains("\"triggerPx\":\"3000\""));
1167        assert!(json.contains("\"orderPx\":\"-1\""));
1168        assert!(json.contains("\"triggerPxType\":\"last\""));
1169
1170        // Verify that None fields are not included
1171        assert!(!json.contains("tgtCcy"));
1172        assert!(!json.contains("posSide"));
1173        assert!(!json.contains("closePosition"));
1174        assert!(!json.contains("closeFraction"));
1175    }
1176
1177    #[rstest]
1178    fn test_algo_order_request_serializes_close_fraction() {
1179        let request = OKXPlaceAlgoOrderRequest {
1180            inst_id: "ETH-USDT-SWAP".to_string(),
1181            inst_id_code: None,
1182            td_mode: OKXTradeMode::Cross,
1183            side: OKXSide::Sell,
1184            ord_type: OKXAlgoOrderType::Conditional,
1185            sz: None,
1186            algo_cl_ord_id: Some("close-frac-123".to_string()),
1187            trigger_px: None,
1188            order_px: None,
1189            trigger_px_type: None,
1190            sl_trigger_px: Some("3000".to_string()),
1191            sl_ord_px: Some("-1".to_string()),
1192            sl_trigger_px_type: Some(OKXTriggerType::Last),
1193            tp_trigger_px: None,
1194            tp_ord_px: None,
1195            tp_trigger_px_type: None,
1196            tgt_ccy: None,
1197            pos_side: Some(OKXPositionSide::Net),
1198            close_position: None,
1199            tag: None,
1200            reduce_only: Some(true),
1201            close_fraction: Some("1".to_string()),
1202            callback_ratio: None,
1203            callback_spread: None,
1204            active_px: None,
1205        };
1206
1207        let json = serde_json::to_string(&request).unwrap();
1208
1209        assert!(json.contains("\"ordType\":\"conditional\""));
1210        assert!(json.contains("\"closeFraction\":\"1\""));
1211        assert!(json.contains("\"slTriggerPx\":\"3000\""));
1212        assert!(json.contains("\"slOrdPx\":\"-1\""));
1213        assert!(json.contains("\"slTriggerPxType\":\"last\""));
1214        assert!(json.contains("\"reduceOnly\":true"));
1215        assert!(!json.contains("\"sz\""));
1216        assert!(!json.contains("triggerPx"));
1217    }
1218
1219    #[rstest]
1220    fn test_algo_order_request_array_serialization() {
1221        let request = OKXPlaceAlgoOrderRequest {
1222            inst_id: "BTC-USDT".to_string(),
1223            inst_id_code: Some(10459),
1224            td_mode: OKXTradeMode::Cross,
1225            side: OKXSide::Sell,
1226            ord_type: OKXAlgoOrderType::Trigger,
1227            sz: Some("0.1".to_string()),
1228            algo_cl_ord_id: None,
1229            trigger_px: Some("50000".to_string()),
1230            order_px: Some("49900".to_string()),
1231            trigger_px_type: Some(OKXTriggerType::Mark),
1232            sl_trigger_px: None,
1233            sl_ord_px: None,
1234            sl_trigger_px_type: None,
1235            tp_trigger_px: None,
1236            tp_ord_px: None,
1237            tp_trigger_px_type: None,
1238            tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1239            pos_side: Some(OKXPositionSide::Net),
1240            close_position: None,
1241            tag: None,
1242            reduce_only: Some(true),
1243            close_fraction: None,
1244            callback_ratio: None,
1245            callback_spread: None,
1246            active_px: None,
1247        };
1248
1249        // OKX expects an array of requests
1250        let json = serde_json::to_string(&[request]).unwrap();
1251
1252        // Verify array format
1253        assert!(json.starts_with('['));
1254        assert!(json.ends_with(']'));
1255
1256        // Verify correct field names
1257        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1258        assert!(json.contains("\"tdMode\":\"cross\""));
1259        assert!(json.contains("\"triggerPx\":\"50000\""));
1260        assert!(json.contains("\"orderPx\":\"49900\""));
1261        assert!(json.contains("\"triggerPxType\":\"mark\""));
1262        assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1263        assert!(json.contains("\"posSide\":\"net\""));
1264        assert!(json.contains("\"reduceOnly\":true"));
1265    }
1266
1267    #[rstest]
1268    fn test_cancel_algo_order_request_serialization() {
1269        let request = OKXCancelAlgoOrderRequest {
1270            inst_id: "ETH-USDT-SWAP".to_string(),
1271            inst_id_code: None,
1272            algo_id: Some("123456".to_string()),
1273            algo_cl_ord_id: None,
1274        };
1275
1276        let json = serde_json::to_string(&request).unwrap();
1277
1278        // Verify correct field names
1279        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1280        assert!(json.contains("\"algoId\":\"123456\""));
1281        assert!(!json.contains("algoClOrdId"));
1282    }
1283
1284    #[rstest]
1285    fn test_cancel_algo_order_with_client_id_serialization() {
1286        let request = OKXCancelAlgoOrderRequest {
1287            inst_id: "BTC-USDT".to_string(),
1288            inst_id_code: Some(10459),
1289            algo_id: None,
1290            algo_cl_ord_id: Some("client123".to_string()),
1291        };
1292
1293        // OKX expects an array of requests
1294        let json = serde_json::to_string(&[request]).unwrap();
1295
1296        // Verify array format and field names
1297        assert!(json.starts_with('['));
1298        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1299        assert!(json.contains("\"algoClOrdId\":\"client123\""));
1300        assert!(!json.contains("\"algoId\""));
1301    }
1302
1303    #[rstest]
1304    fn test_amend_algo_order_trigger_serialization() {
1305        let request = OKXAmendAlgoOrderRequest {
1306            inst_id: "ETH-USDT-SWAP".to_string(),
1307            algo_id: "123456".to_string(),
1308            algo_cl_ord_id: None,
1309            new_sz: None,
1310            new_trigger_px: Some("3500".to_string()),
1311            new_order_px: Some("3490".to_string()),
1312            new_callback_ratio: None,
1313            new_callback_spread: None,
1314            new_active_px: None,
1315        };
1316
1317        let json = serde_json::to_string(&request).unwrap();
1318
1319        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1320        assert!(json.contains("\"algoId\":\"123456\""));
1321        assert!(json.contains("\"newTriggerPx\":\"3500\""));
1322        assert!(json.contains("\"newOrderPx\":\"3490\""));
1323        assert!(!json.contains("newSz"));
1324        assert!(!json.contains("algoClOrdId"));
1325        assert!(!json.contains("newCallbackRatio"));
1326    }
1327
1328    #[rstest]
1329    fn test_amend_algo_order_trailing_stop_serialization() {
1330        let request = OKXAmendAlgoOrderRequest {
1331            inst_id: "BTC-USDT-SWAP".to_string(),
1332            algo_id: "789012".to_string(),
1333            algo_cl_ord_id: Some("client456".to_string()),
1334            new_sz: Some("0.1".to_string()),
1335            new_trigger_px: None,
1336            new_order_px: None,
1337            new_callback_ratio: Some("0.02".to_string()),
1338            new_callback_spread: None,
1339            new_active_px: Some("50000".to_string()),
1340        };
1341
1342        let json = serde_json::to_string(&request).unwrap();
1343
1344        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1345        assert!(json.contains("\"algoId\":\"789012\""));
1346        assert!(json.contains("\"algoClOrdId\":\"client456\""));
1347        assert!(json.contains("\"newSz\":\"0.1\""));
1348        assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1349        assert!(json.contains("\"newActivePx\":\"50000\""));
1350        assert!(!json.contains("newTriggerPx"));
1351        assert!(!json.contains("newOrderPx"));
1352    }
1353
1354    #[rstest]
1355    fn test_trailing_stop_request_callback_ratio_serialization() {
1356        let request = OKXPlaceAlgoOrderRequest {
1357            inst_id: "BTC-USDT-SWAP".to_string(),
1358            inst_id_code: None,
1359            td_mode: OKXTradeMode::Cross,
1360            side: OKXSide::Buy,
1361            ord_type: OKXAlgoOrderType::MoveOrderStop,
1362            sz: Some("0.1".to_string()),
1363            algo_cl_ord_id: Some("trail-001".to_string()),
1364            trigger_px: None,
1365            order_px: None,
1366            trigger_px_type: None,
1367            sl_trigger_px: None,
1368            sl_ord_px: None,
1369            sl_trigger_px_type: None,
1370            tp_trigger_px: None,
1371            tp_ord_px: None,
1372            tp_trigger_px_type: None,
1373            tgt_ccy: None,
1374            pos_side: None,
1375            close_position: None,
1376            tag: None,
1377            reduce_only: None,
1378            close_fraction: None,
1379            callback_ratio: Some("0.01".to_string()),
1380            callback_spread: None,
1381            active_px: None,
1382        };
1383
1384        let json = serde_json::to_string(&request).unwrap();
1385
1386        assert!(json.contains("\"ordType\":\"move_order_stop\""));
1387        assert!(json.contains("\"callbackRatio\":\"0.01\""));
1388        assert!(!json.contains("callbackSpread"));
1389        assert!(!json.contains("activePx"));
1390    }
1391
1392    #[rstest]
1393    fn test_trailing_stop_request_callback_spread_serialization() {
1394        let request = OKXPlaceAlgoOrderRequest {
1395            inst_id: "ETH-USDT-SWAP".to_string(),
1396            inst_id_code: None,
1397            td_mode: OKXTradeMode::Isolated,
1398            side: OKXSide::Sell,
1399            ord_type: OKXAlgoOrderType::MoveOrderStop,
1400            sz: Some("1.0".to_string()),
1401            algo_cl_ord_id: None,
1402            trigger_px: None,
1403            order_px: None,
1404            trigger_px_type: None,
1405            sl_trigger_px: None,
1406            sl_ord_px: None,
1407            sl_trigger_px_type: None,
1408            tp_trigger_px: None,
1409            tp_ord_px: None,
1410            tp_trigger_px_type: None,
1411            tgt_ccy: None,
1412            pos_side: None,
1413            close_position: None,
1414            tag: None,
1415            reduce_only: Some(true),
1416            close_fraction: None,
1417            callback_ratio: None,
1418            callback_spread: Some("50.5".to_string()),
1419            active_px: None,
1420        };
1421
1422        let json = serde_json::to_string(&request).unwrap();
1423
1424        assert!(json.contains("\"callbackSpread\":\"50.5\""));
1425        assert!(!json.contains("callbackRatio"));
1426        assert!(!json.contains("activePx"));
1427    }
1428
1429    #[rstest]
1430    fn test_trailing_stop_request_with_activation_price_serialization() {
1431        let request = OKXPlaceAlgoOrderRequest {
1432            inst_id: "BTC-USDT-SWAP".to_string(),
1433            inst_id_code: None,
1434            td_mode: OKXTradeMode::Cross,
1435            side: OKXSide::Buy,
1436            ord_type: OKXAlgoOrderType::MoveOrderStop,
1437            sz: Some("0.5".to_string()),
1438            algo_cl_ord_id: None,
1439            trigger_px: None,
1440            order_px: None,
1441            trigger_px_type: None,
1442            sl_trigger_px: None,
1443            sl_ord_px: None,
1444            sl_trigger_px_type: None,
1445            tp_trigger_px: None,
1446            tp_ord_px: None,
1447            tp_trigger_px_type: None,
1448            tgt_ccy: None,
1449            pos_side: None,
1450            close_position: None,
1451            tag: None,
1452            reduce_only: None,
1453            close_fraction: None,
1454            callback_ratio: Some("0.005".to_string()),
1455            callback_spread: None,
1456            active_px: Some("65000".to_string()),
1457        };
1458
1459        let json = serde_json::to_string(&request).unwrap();
1460
1461        assert!(json.contains("\"callbackRatio\":\"0.005\""));
1462        assert!(json.contains("\"activePx\":\"65000\""));
1463        assert!(!json.contains("callbackSpread"));
1464    }
1465
1466    #[rstest]
1467    fn test_amend_algo_order_callback_spread_serialization() {
1468        let request = OKXAmendAlgoOrderRequest {
1469            inst_id: "ETH-USDT-SWAP".to_string(),
1470            algo_id: "456789".to_string(),
1471            algo_cl_ord_id: None,
1472            new_sz: None,
1473            new_trigger_px: None,
1474            new_order_px: None,
1475            new_callback_ratio: None,
1476            new_callback_spread: Some("25.0".to_string()),
1477            new_active_px: Some("4000".to_string()),
1478        };
1479
1480        let json = serde_json::to_string(&request).unwrap();
1481
1482        assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
1483        assert!(json.contains("\"newActivePx\":\"4000\""));
1484        assert!(!json.contains("newCallbackRatio"));
1485        assert!(!json.contains("newTriggerPx"));
1486        assert!(!json.contains("newSz"));
1487    }
1488
1489    #[rstest]
1490    fn test_amend_algo_order_size_only_serialization() {
1491        let request = OKXAmendAlgoOrderRequest {
1492            inst_id: "BTC-USDT-SWAP".to_string(),
1493            algo_id: "111222".to_string(),
1494            algo_cl_ord_id: None,
1495            new_sz: Some("0.5".to_string()),
1496            new_trigger_px: None,
1497            new_order_px: None,
1498            new_callback_ratio: None,
1499            new_callback_spread: None,
1500            new_active_px: None,
1501        };
1502
1503        let json = serde_json::to_string(&request).unwrap();
1504
1505        assert!(json.contains("\"newSz\":\"0.5\""));
1506        assert!(!json.contains("newTriggerPx"));
1507        assert!(!json.contains("newOrderPx"));
1508        assert!(!json.contains("newCallbackRatio"));
1509        assert!(!json.contains("newCallbackSpread"));
1510        assert!(!json.contains("newActivePx"));
1511    }
1512
1513    #[rstest]
1514    fn test_amend_algo_order_all_fields_serialization() {
1515        let request = OKXAmendAlgoOrderRequest {
1516            inst_id: "BTC-USDT-SWAP".to_string(),
1517            algo_id: "333444".to_string(),
1518            algo_cl_ord_id: Some("client789".to_string()),
1519            new_sz: Some("1.0".to_string()),
1520            new_trigger_px: Some("60000".to_string()),
1521            new_order_px: Some("59900".to_string()),
1522            new_callback_ratio: Some("0.015".to_string()),
1523            new_callback_spread: Some("100".to_string()),
1524            new_active_px: Some("62000".to_string()),
1525        };
1526
1527        let json = serde_json::to_string(&request).unwrap();
1528
1529        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1530        assert!(json.contains("\"algoId\":\"333444\""));
1531        assert!(json.contains("\"algoClOrdId\":\"client789\""));
1532        assert!(json.contains("\"newSz\":\"1.0\""));
1533        assert!(json.contains("\"newTriggerPx\":\"60000\""));
1534        assert!(json.contains("\"newOrderPx\":\"59900\""));
1535        assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
1536        assert!(json.contains("\"newCallbackSpread\":\"100\""));
1537        assert!(json.contains("\"newActivePx\":\"62000\""));
1538    }
1539
1540    #[rstest]
1541    fn test_place_order_request_serializes_px_usd() {
1542        let request = OKXPlaceOrderRequest {
1543            inst_id: "BTC-USD-250328-50000-C".to_string(),
1544            td_mode: OKXTradeMode::Cross,
1545            ccy: None,
1546            cl_ord_id: Some("test-opt-1".to_string()),
1547            tag: None,
1548            side: OKXSide::Buy,
1549            pos_side: Some(OKXPositionSide::Net),
1550            ord_type: OKXOrderType::Limit,
1551            sz: "1".to_string(),
1552            px: None,
1553            px_usd: Some("100.5".to_string()),
1554            px_vol: None,
1555            reduce_only: None,
1556            tgt_ccy: None,
1557            attach_algo_ords: None,
1558        };
1559
1560        let json = serde_json::to_string(&request).unwrap();
1561        assert!(json.contains("\"pxUsd\":\"100.5\""));
1562        assert!(!json.contains("\"pxVol\""));
1563        assert!(!json.contains("\"px\":"));
1564    }
1565
1566    #[rstest]
1567    fn test_place_order_request_serializes_px_vol() {
1568        let request = OKXPlaceOrderRequest {
1569            inst_id: "BTC-USD-250328-50000-C".to_string(),
1570            td_mode: OKXTradeMode::Cross,
1571            ccy: None,
1572            cl_ord_id: Some("test-opt-2".to_string()),
1573            tag: None,
1574            side: OKXSide::Buy,
1575            pos_side: Some(OKXPositionSide::Net),
1576            ord_type: OKXOrderType::Limit,
1577            sz: "1".to_string(),
1578            px: None,
1579            px_usd: None,
1580            px_vol: Some("0.55".to_string()),
1581            reduce_only: None,
1582            tgt_ccy: None,
1583            attach_algo_ords: None,
1584        };
1585
1586        let json = serde_json::to_string(&request).unwrap();
1587        assert!(json.contains("\"pxVol\":\"0.55\""));
1588        assert!(!json.contains("\"pxUsd\""));
1589        assert!(!json.contains("\"px\":"));
1590    }
1591}