nautilus_okx/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{deserialize_empty_string_as_none, deserialize_empty_ustr_as_none};
22
23/// Represents a trade tick from the GET /api/v5/market/trades endpoint.
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct OKXTrade {
27    /// Instrument ID.
28    pub inst_id: Ustr,
29    /// Trade price.
30    pub px: String,
31    /// Trade size.
32    pub sz: String,
33    /// Trade side: buy or sell.
34    pub side: OKXSide,
35    /// Trade ID assigned by OKX.
36    pub trade_id: Ustr,
37    /// Trade timestamp in milliseconds.
38    #[serde(deserialize_with = "deserialize_string_to_u64")]
39    pub ts: u64,
40}
41
42/// Represents a candlestick from the GET /api/v5/market/history-candles endpoint.
43/// The tuple contains [timestamp(ms), open, high, low, close, volume, turnover, base_volume, count].
44#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct OKXCandlestick(
46    /// Timestamp in milliseconds.
47    pub String,
48    /// Open price.
49    pub String,
50    /// High price.
51    pub String,
52    /// Low price.
53    pub String,
54    /// Close price.
55    pub String,
56    /// Volume.
57    pub String,
58    /// Turnover in quote currency.
59    pub String,
60    /// Base volume.
61    pub String,
62    /// Record count.
63    pub String,
64);
65
66use crate::common::{
67    enums::{
68        OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode, OKXOrderStatus,
69        OKXOrderType, OKXPositionSide, OKXSide, OKXTradeMode, OKXTriggerType, OKXVipLevel,
70    },
71    parse::deserialize_string_to_u64,
72};
73
74/// Represents a mark price from the GET /api/v5/public/mark-price endpoint.
75#[derive(Clone, Debug, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct OKXMarkPrice {
78    /// Underlying.
79    pub uly: Option<Ustr>,
80    /// Instrument ID.
81    pub inst_id: Ustr,
82    /// The mark price.
83    pub mark_px: String,
84    /// The timestamp for the mark price.
85    #[serde(deserialize_with = "deserialize_string_to_u64")]
86    pub ts: u64,
87}
88
89/// Represents an index price from the GET /api/v5/public/index-tickers endpoint.
90#[derive(Clone, Debug, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct OKXIndexTicker {
93    /// Instrument ID.
94    pub inst_id: Ustr,
95    /// The index price.
96    pub idx_px: String,
97    /// The timestamp for the index price.
98    #[serde(deserialize_with = "deserialize_string_to_u64")]
99    pub ts: u64,
100}
101
102/// Represents a position tier from the GET /api/v5/public/position-tiers endpoint.
103#[derive(Clone, Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct OKXPositionTier {
106    /// Underlying.
107    pub uly: Ustr,
108    /// Instrument family.
109    pub inst_family: String,
110    /// Instrument ID.
111    pub inst_id: Ustr,
112    /// Tier level.
113    pub tier: String,
114    /// Minimum size/amount for the tier.
115    pub min_sz: String,
116    /// Maximum size/amount for the tier.
117    pub max_sz: String,
118    /// Maintenance margin requirement rate.
119    pub mmr: String,
120    /// Initial margin requirement rate.
121    pub imr: String,
122    /// Maximum available leverage.
123    pub max_lever: String,
124    /// Option Margin Coefficient (only applicable to options).
125    pub opt_mgn_factor: String,
126    /// Quote currency borrowing amount.
127    pub quote_max_loan: String,
128    /// Base currency borrowing amount.
129    pub base_max_loan: String,
130}
131
132/// Represents an account balance snapshot from `GET /api/v5/account/balance`.
133#[derive(Clone, Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct OKXAccount {
136    /// Adjusted/Effective equity in USD.
137    pub adj_eq: String,
138    /// Borrow frozen amount.
139    pub borrow_froz: String,
140    /// Account details by currency.
141    pub details: Vec<OKXBalanceDetail>,
142    /// Initial margin requirement.
143    pub imr: String,
144    /// Isolated margin equity.
145    pub iso_eq: String,
146    /// Margin ratio.
147    pub mgn_ratio: String,
148    /// Maintenance margin requirement.
149    pub mmr: String,
150    /// Notional value in USD for borrow.
151    pub notional_usd_for_borrow: String,
152    /// Notional value in USD for futures.
153    pub notional_usd_for_futures: String,
154    /// Notional value in USD for option.
155    pub notional_usd_for_option: String,
156    /// Notional value in USD for swap.
157    pub notional_usd_for_swap: String,
158    /// Notional value in USD.
159    pub notional_usd: String,
160    /// Order frozen.
161    pub ord_froz: String,
162    /// Total equity in USD.
163    pub total_eq: String,
164    /// Last update time, Unix timestamp in milliseconds.
165    #[serde(deserialize_with = "deserialize_string_to_u64")]
166    pub u_time: u64,
167    /// Unrealized profit and loss.
168    pub upl: String,
169}
170
171/// Represents a balance detail for a single currency in an OKX account.
172#[derive(Clone, Debug, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct OKXBalanceDetail {
175    /// Available balance.
176    pub avail_bal: String,
177    /// Available equity.
178    pub avail_eq: String,
179    /// Borrow frozen amount.
180    pub borrow_froz: String,
181    /// Cash balance.
182    pub cash_bal: String,
183    /// Currency.
184    pub ccy: Ustr,
185    /// Cross liability.
186    pub cross_liab: String,
187    /// Discount equity in USD.
188    pub dis_eq: String,
189    /// Equity.
190    pub eq: String,
191    /// Equity in USD.
192    pub eq_usd: String,
193    /// Same-token equity.
194    pub smt_sync_eq: String,
195    /// Copy trading equity.
196    pub spot_copy_trading_eq: String,
197    /// Fixed balance.
198    pub fixed_bal: String,
199    /// Frozen balance.
200    pub frozen_bal: String,
201    /// Initial margin requirement.
202    pub imr: String,
203    /// Interest.
204    pub interest: String,
205    /// Isolated margin equity.
206    pub iso_eq: String,
207    /// Isolated margin liability.
208    pub iso_liab: String,
209    /// Isolated unrealized profit and loss.
210    pub iso_upl: String,
211    /// Liability.
212    pub liab: String,
213    /// Maximum loan amount.
214    pub max_loan: String,
215    /// Margin ratio.
216    pub mgn_ratio: String,
217    /// Maintenance margin requirement.
218    pub mmr: String,
219    /// Notional leverage.
220    pub notional_lever: String,
221    /// Order frozen.
222    pub ord_frozen: String,
223    /// Reward balance.
224    pub reward_bal: String,
225    /// Spot in use amount.
226    #[serde(alias = "spotInUse")]
227    pub spot_in_use_amt: String,
228    /// Cross liability spot in use amount.
229    #[serde(alias = "clSpotInUse")]
230    pub cl_spot_in_use_amt: String,
231    /// Maximum spot in use amount.
232    #[serde(alias = "maxSpotInUse")]
233    pub max_spot_in_use_amt: String,
234    /// Spot isolated balance.
235    pub spot_iso_bal: String,
236    /// Strategy equity.
237    pub stgy_eq: String,
238    /// Time-weighted average price.
239    pub twap: String,
240    /// Last update time, Unix timestamp in milliseconds.
241    #[serde(deserialize_with = "deserialize_string_to_u64")]
242    pub u_time: u64,
243    /// Unrealized profit and loss.
244    pub upl: String,
245    /// Unrealized profit and loss liability.
246    pub upl_liab: String,
247    /// Spot balance.
248    pub spot_bal: String,
249    /// Open average price.
250    pub open_avg_px: String,
251    /// Accumulated average price.
252    pub acc_avg_px: String,
253    /// Spot unrealized profit and loss.
254    pub spot_upl: String,
255    /// Spot unrealized profit and loss ratio.
256    pub spot_upl_ratio: String,
257    /// Total profit and loss.
258    pub total_pnl: String,
259    /// Total profit and loss ratio.
260    pub total_pnl_ratio: String,
261}
262
263/// Represents a single open position from `GET /api/v5/account/positions`.
264#[derive(Clone, Debug, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct OKXPosition {
267    /// Instrument ID.
268    pub inst_id: Ustr,
269    /// Instrument type.
270    pub inst_type: OKXInstrumentType,
271    /// Margin mode: isolated/cross.
272    pub mgn_mode: OKXMarginMode,
273    /// Position ID.
274    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
275    pub pos_id: Option<Ustr>,
276    /// Position side: long/short.
277    pub pos_side: OKXPositionSide,
278    /// Position size.
279    pub pos: String,
280    /// Base currency balance.
281    pub base_bal: String,
282    /// Position currency.
283    pub ccy: String,
284    /// Trading fee.
285    pub fee: String,
286    /// Position leverage.
287    pub lever: String,
288    /// Last traded price.
289    pub last: String,
290    /// Mark price.
291    pub mark_px: String,
292    /// Liquidation price.
293    pub liq_px: String,
294    /// Maintenance margin requirement.
295    pub mmr: String,
296    /// Interest.
297    pub interest: String,
298    /// Trade ID.
299    pub trade_id: Ustr,
300    /// Notional value of position in USD.
301    pub notional_usd: String,
302    /// Average entry price.
303    pub avg_px: String,
304    /// Unrealized profit and loss.
305    pub upl: String,
306    /// Unrealized profit and loss ratio.
307    pub upl_ratio: String,
308    /// Last update time, Unix timestamp in milliseconds.
309    #[serde(deserialize_with = "deserialize_string_to_u64")]
310    pub u_time: u64,
311    /// Position margin.
312    pub margin: String,
313    /// Margin ratio.
314    pub mgn_ratio: String,
315    /// Auto-deleveraging (ADL) ranking.
316    pub adl: String,
317    /// Creation time, Unix timestamp in milliseconds.
318    pub c_time: String,
319    /// Realized profit and loss.
320    pub realized_pnl: String,
321    /// Unrealized profit and loss at last price.
322    pub upl_last_px: String,
323    /// Unrealized profit and loss ratio at last price.
324    pub upl_ratio_last_px: String,
325    /// Available position that can be closed.
326    pub avail_pos: String,
327    /// Breakeven price.
328    pub be_px: String,
329    /// Funding fee.
330    pub funding_fee: String,
331    /// Index price.
332    pub idx_px: String,
333    /// Liquidation penalty.
334    pub liq_penalty: String,
335    /// Option value.
336    pub opt_val: String,
337    /// Pending close order liability value.
338    pub pending_close_ord_liab_val: String,
339    /// Total profit and loss.
340    pub pnl: String,
341    /// Position currency.
342    pub pos_ccy: String,
343    /// Quote currency balance.
344    pub quote_bal: String,
345    /// Borrowed amount in quote currency.
346    pub quote_borrowed: String,
347    /// Interest on quote currency.
348    pub quote_interest: String,
349    /// Amount in use for spot trading.
350    #[serde(alias = "spotInUse")]
351    pub spot_in_use_amt: String,
352    /// Currency in use for spot trading.
353    pub spot_in_use_ccy: String,
354    /// USD price.
355    pub usd_px: String,
356}
357
358/// Represents the response from `POST /api/v5/trade/order` (place order).
359/// This model is designed to be flexible and handle the minimal fields that the API returns.
360#[derive(Clone, Debug, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct OKXPlaceOrderResponse {
363    /// Order ID.
364    #[serde(default)]
365    pub ord_id: Option<Ustr>,
366    /// Client order ID.
367    #[serde(default)]
368    pub cl_ord_id: Option<Ustr>,
369    /// Order tag.
370    #[serde(default)]
371    pub tag: Option<String>,
372    /// Instrument ID (optional - might not be in response).
373    #[serde(default)]
374    pub inst_id: Option<Ustr>,
375    /// Order side (optional).
376    #[serde(default)]
377    pub side: Option<OKXSide>,
378    /// Order type (optional).
379    #[serde(default)]
380    pub ord_type: Option<OKXOrderType>,
381    /// Order size (optional).
382    #[serde(default)]
383    pub sz: Option<String>,
384    /// Order state (optional).
385    pub state: Option<OKXOrderStatus>,
386    /// Price (optional).
387    #[serde(default)]
388    pub px: Option<String>,
389    /// Average price (optional).
390    #[serde(default)]
391    pub avg_px: Option<String>,
392    /// Accumulated filled size.
393    #[serde(default)]
394    pub acc_fill_sz: Option<String>,
395    /// Fill size (optional).
396    #[serde(default)]
397    pub fill_sz: Option<String>,
398    /// Fill price (optional).
399    #[serde(default)]
400    pub fill_px: Option<String>,
401    /// Trade ID (optional).
402    #[serde(default)]
403    pub trade_id: Option<Ustr>,
404    /// Fill time (optional).
405    #[serde(default)]
406    pub fill_time: Option<String>,
407    /// Fee (optional).
408    #[serde(default)]
409    pub fee: Option<String>,
410    /// Fee currency (optional).
411    #[serde(default)]
412    pub fee_ccy: Option<String>,
413    /// Request ID (optional).
414    #[serde(default)]
415    pub req_id: Option<Ustr>,
416    /// Position side (optional).
417    #[serde(default)]
418    pub pos_side: Option<OKXPositionSide>,
419    /// Reduce-only flag (optional).
420    #[serde(default)]
421    pub reduce_only: Option<String>,
422    /// Target currency (optional).
423    #[serde(default)]
424    pub tgt_ccy: Option<String>,
425    /// Creation time.
426    #[serde(default)]
427    pub c_time: Option<String>,
428    /// Last update time (optional).
429    #[serde(default)]
430    pub u_time: Option<String>,
431}
432
433/// Represents a single historical order record from `GET /api/v5/trade/orders-history`.
434#[derive(Clone, Debug, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct OKXOrderHistory {
437    /// Order ID.
438    pub ord_id: Ustr,
439    /// Client order ID.
440    pub cl_ord_id: Ustr,
441    /// Algo order ID (for conditional orders).
442    #[serde(default)]
443    pub algo_id: Option<Ustr>,
444    /// Client-supplied algo order ID (for conditional orders).
445    #[serde(default)]
446    pub algo_cl_ord_id: Option<Ustr>,
447    /// Client account ID (may be omitted by OKX).
448    #[serde(default)]
449    pub cl_act_id: Option<Ustr>,
450    /// Order tag.
451    pub tag: String,
452    /// Instrument type.
453    pub inst_type: OKXInstrumentType,
454    /// Underlying (optional).
455    pub uly: Option<Ustr>,
456    /// Instrument ID.
457    pub inst_id: Ustr,
458    /// Order type.
459    pub ord_type: OKXOrderType,
460    /// Order size.
461    pub sz: String,
462    /// Price (optional).
463    pub px: String,
464    /// Side.
465    pub side: OKXSide,
466    /// Position side.
467    pub pos_side: OKXPositionSide,
468    /// Trade mode.
469    pub td_mode: OKXTradeMode,
470    /// Reduce-only flag.
471    pub reduce_only: String,
472    /// Target currency (optional).
473    pub tgt_ccy: String,
474    /// Order state.
475    pub state: OKXOrderStatus,
476    /// Average price (optional).
477    pub avg_px: String,
478    /// Execution fee.
479    pub fee: String,
480    /// Fee currency.
481    pub fee_ccy: String,
482    /// Filled size (optional).
483    pub fill_sz: String,
484    /// Fill price (optional).
485    pub fill_px: String,
486    /// Trade ID (optional).
487    pub trade_id: Ustr,
488    /// Fill time, Unix timestamp in milliseconds.
489    #[serde(deserialize_with = "deserialize_string_to_u64")]
490    pub fill_time: u64,
491    /// Accumulated filled size.
492    pub acc_fill_sz: String,
493    /// Fill fee (optional, may be omitted).
494    #[serde(default)]
495    pub fill_fee: Option<String>,
496    /// Request ID (optional).
497    #[serde(default)]
498    pub req_id: Option<Ustr>,
499    /// Cancelled filled size (optional).
500    #[serde(default)]
501    pub cancel_fill_sz: Option<String>,
502    /// Cancelled total size (optional).
503    #[serde(default)]
504    pub cancel_total_sz: Option<String>,
505    /// Fee discount (optional).
506    #[serde(default)]
507    pub fee_discount: Option<String>,
508    /// Category (optional).
509    pub category: String,
510    /// Last update time, Unix timestamp in milliseconds.
511    #[serde(deserialize_with = "deserialize_string_to_u64")]
512    pub u_time: u64,
513    /// Creation time.
514    #[serde(deserialize_with = "deserialize_string_to_u64")]
515    pub c_time: u64,
516}
517
518/// Represents an algo order response from `/trade/order-algo-*` endpoints.
519#[derive(Clone, Debug, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct OKXOrderAlgo {
522    /// Algo order ID assigned by OKX.
523    pub algo_id: String,
524    /// Client-specified algo order ID.
525    #[serde(default)]
526    pub algo_cl_ord_id: String,
527    /// Client order ID (empty until triggered).
528    #[serde(default)]
529    pub cl_ord_id: String,
530    /// Venue order ID (empty until triggered).
531    #[serde(default)]
532    pub ord_id: String,
533    /// Instrument ID, e.g. `ETH-USDT-SWAP`.
534    pub inst_id: Ustr,
535    /// Instrument type.
536    pub inst_type: OKXInstrumentType,
537    /// Algo order type.
538    pub ord_type: OKXOrderType,
539    /// Current order state.
540    pub state: OKXOrderStatus,
541    /// Order side.
542    pub side: OKXSide,
543    /// Position side.
544    pub pos_side: OKXPositionSide,
545    /// Submitted size.
546    pub sz: String,
547    /// Trigger price (empty for certain algo styles).
548    #[serde(default)]
549    pub trigger_px: String,
550    /// Trigger price type (last/mark/index).
551    #[serde(default)]
552    pub trigger_px_type: Option<OKXTriggerType>,
553    /// Order price (-1 indicates market execution once triggered).
554    #[serde(default)]
555    pub ord_px: String,
556    /// Trade mode (cash/cross/isolated).
557    pub td_mode: OKXTradeMode,
558    /// Algo leverage configuration.
559    #[serde(default)]
560    pub lever: String,
561    /// Reduce-only flag.
562    #[serde(default)]
563    pub reduce_only: String,
564    /// Executed price (if triggered).
565    #[serde(default)]
566    pub actual_px: String,
567    /// Executed size (if triggered).
568    #[serde(default)]
569    pub actual_sz: String,
570    /// Notional value in USD.
571    #[serde(default)]
572    pub notional_usd: String,
573    /// Creation time (milliseconds).
574    #[serde(deserialize_with = "deserialize_string_to_u64")]
575    pub c_time: u64,
576    /// Last update time (milliseconds).
577    #[serde(deserialize_with = "deserialize_string_to_u64")]
578    pub u_time: u64,
579    /// Trigger timestamp (if triggered).
580    #[serde(default)]
581    pub trigger_time: String,
582    /// Optional tag supplied during submission.
583    #[serde(default)]
584    pub tag: String,
585}
586
587/// Represents a transaction detail (fill) from `GET /api/v5/trade/fills`.
588#[derive(Clone, Debug, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct OKXTransactionDetail {
591    /// Product type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
592    pub inst_type: OKXInstrumentType,
593    /// Instrument ID, e.g. "BTC-USDT".
594    pub inst_id: Ustr,
595    /// Trade ID.
596    pub trade_id: Ustr,
597    /// Order ID.
598    pub ord_id: Ustr,
599    /// Client order ID.
600    pub cl_ord_id: Ustr,
601    /// Bill ID.
602    pub bill_id: Ustr,
603    /// Last filled price.
604    pub fill_px: String,
605    /// Last filled quantity.
606    pub fill_sz: String,
607    /// Trade side: buy or sell.
608    pub side: OKXSide,
609    /// Execution type.
610    pub exec_type: OKXExecType,
611    /// Fee currency.
612    pub fee_ccy: String,
613    /// Fee amount.
614    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
615    pub fee: Option<String>,
616    /// Timestamp, Unix timestamp format in milliseconds.
617    #[serde(deserialize_with = "deserialize_string_to_u64")]
618    pub ts: u64,
619}
620
621/// Represents a single historical position record from `GET /api/v5/account/positions-history`.
622#[derive(Clone, Debug, Serialize, Deserialize)]
623#[serde(rename_all = "camelCase")]
624pub struct OKXPositionHistory {
625    /// Instrument type (e.g. "SWAP", "FUTURES", etc.).
626    pub inst_type: OKXInstrumentType,
627    /// Instrument ID (e.g. "BTC-USD-SWAP").
628    pub inst_id: Ustr,
629    /// Margin mode: e.g. "cross", "isolated".
630    pub mgn_mode: OKXMarginMode,
631    /// The type of the last close, e.g. "1" (close partially), "2" (close all), etc.
632    /// See OKX docs for the meaning of each numeric code.
633    #[serde(rename = "type")]
634    pub r#type: Ustr,
635    /// Creation time of the position (Unix timestamp in milliseconds).
636    pub c_time: String,
637    /// Last update time, Unix timestamp in milliseconds.
638    #[serde(deserialize_with = "deserialize_string_to_u64")]
639    pub u_time: u64,
640    /// Average price of opening position.
641    pub open_avg_px: String,
642    /// Average price of closing position (if applicable).
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub close_avg_px: Option<String>,
645    /// The position ID.
646    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
647    pub pos_id: Option<Ustr>,
648    /// Max quantity of the position at open time.
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub open_max_pos: Option<String>,
651    /// Cumulative closed volume of the position.
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub close_total_pos: Option<String>,
654    /// Realized profit and loss (only for FUTURES/SWAP/OPTION).
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub realized_pnl: Option<String>,
657    /// Accumulated fee for the position.
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub fee: Option<String>,
660    /// Accumulated funding fee (for perpetual swaps).
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub funding_fee: Option<String>,
663    /// Accumulated liquidation penalty. Negative if there was a penalty.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub liq_penalty: Option<String>,
666    /// Profit and loss (realized or unrealized depending on status).
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub pnl: Option<String>,
669    /// PnL ratio.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub pnl_ratio: Option<String>,
672    /// Position side: "long" / "short" / "net".
673    pub pos_side: OKXPositionSide,
674    /// Leverage used (the JSON field is "lev", but we rename it in Rust).
675    pub lever: String,
676    /// Direction: "long" or "short" (only for MARGIN/FUTURES/SWAP/OPTION).
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub direction: Option<String>,
679    /// Trigger mark price. Populated if `type` indicates liquidation or ADL.
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub trigger_px: Option<String>,
682    /// The underlying (e.g. "BTC-USD" for futures or swap).
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub uly: Option<String>,
685    /// Currency (e.g. "BTC"). May or may not appear in all responses.
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub ccy: Option<String>,
688}
689
690/// Represents the request body for `POST /api/v5/trade/order-algo` (place algo order).
691#[derive(Clone, Debug, Serialize, Deserialize)]
692#[serde(rename_all = "camelCase")]
693pub struct OKXPlaceAlgoOrderRequest {
694    /// Instrument ID.
695    #[serde(rename = "instId")]
696    pub inst_id: String,
697    /// Trade mode (isolated, cross, cash).
698    #[serde(rename = "tdMode")]
699    pub td_mode: OKXTradeMode,
700    /// Order side (buy, sell).
701    pub side: OKXSide,
702    /// Algo order type (trigger).
703    #[serde(rename = "ordType")]
704    pub ord_type: OKXAlgoOrderType,
705    /// Order size.
706    pub sz: String,
707    /// Client-supplied algo order ID.
708    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
709    pub algo_cl_ord_id: Option<String>,
710    /// Trigger price.
711    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
712    pub trigger_px: Option<String>,
713    /// Order price (for limit orders).
714    #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
715    pub order_px: Option<String>,
716    /// Trigger type (last, mark, index).
717    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
718    pub trigger_px_type: Option<OKXTriggerType>,
719    /// Target currency (base_ccy or quote_ccy).
720    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
721    pub tgt_ccy: Option<String>,
722    /// Position side (net, long, short).
723    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
724    pub pos_side: Option<OKXPositionSide>,
725    /// Whether to close position.
726    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
727    pub close_position: Option<bool>,
728    /// Order tag.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub tag: Option<String>,
731    /// Whether it's a reduce-only order.
732    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
733    pub reduce_only: Option<bool>,
734}
735
736/// Represents the response from `POST /api/v5/trade/order-algo` (place algo order).
737#[derive(Clone, Debug, Serialize, Deserialize)]
738#[serde(rename_all = "camelCase")]
739pub struct OKXPlaceAlgoOrderResponse {
740    /// Algo order ID.
741    pub algo_id: String,
742    /// Client-supplied algo order ID.
743    #[serde(skip_serializing_if = "Option::is_none")]
744    pub algo_cl_ord_id: Option<String>,
745    /// The result of the request.
746    #[serde(skip_serializing_if = "Option::is_none")]
747    pub s_code: Option<String>,
748    /// Error message if the request failed.
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub s_msg: Option<String>,
751    /// Request ID.
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub req_id: Option<String>,
754}
755
756/// Represents the request body for `POST /api/v5/trade/cancel-algos` (cancel algo order).
757#[derive(Clone, Debug, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct OKXCancelAlgoOrderRequest {
760    /// Instrument ID.
761    pub inst_id: String,
762    /// Algo order ID.
763    #[serde(skip_serializing_if = "Option::is_none")]
764    pub algo_id: Option<String>,
765    /// Client-supplied algo order ID.
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub algo_cl_ord_id: Option<String>,
768}
769
770/// Represents the response from `POST /api/v5/trade/cancel-algos` (cancel algo order).
771#[derive(Clone, Debug, Serialize, Deserialize)]
772#[serde(rename_all = "camelCase")]
773pub struct OKXCancelAlgoOrderResponse {
774    /// Algo order ID.
775    pub algo_id: String,
776    /// The result of the request.
777    #[serde(skip_serializing_if = "Option::is_none")]
778    pub s_code: Option<String>,
779    /// Error message if the request failed.
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub s_msg: Option<String>,
782}
783
784/// Represents the response from `GET /api/v5/public/time` (get system time).
785#[derive(Clone, Debug, Serialize, Deserialize)]
786#[serde(rename_all = "camelCase")]
787pub struct OKXServerTime {
788    /// Server timestamp in milliseconds.
789    #[serde(deserialize_with = "deserialize_string_to_u64")]
790    pub ts: u64,
791}
792
793/// Represents a fee rate entry from `GET /api/v5/account/trade-fee`.
794#[derive(Clone, Debug, Serialize, Deserialize)]
795#[serde(rename_all = "camelCase")]
796pub struct OKXFeeRate {
797    /// Fee level (VIP tier) - indicates the user's VIP tier (0-9).
798    #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
799    pub level: OKXVipLevel,
800    /// Taker fee rate for crypto-margined contracts.
801    pub taker: String,
802    /// Maker fee rate for crypto-margined contracts.
803    pub maker: String,
804    /// Taker fee rate for USDT-margined contracts.
805    pub taker_u: String,
806    /// Maker fee rate for USDT-margined contracts.
807    pub maker_u: String,
808    /// Delivery fee rate.
809    #[serde(default)]
810    pub delivery: String,
811    /// Option exercise fee rate.
812    #[serde(default)]
813    pub exercise: String,
814    /// Instrument type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
815    pub inst_type: OKXInstrumentType,
816    /// Fee schedule category (being deprecated).
817    #[serde(default)]
818    pub category: String,
819    /// Data return timestamp (Unix timestamp in milliseconds).
820    #[serde(deserialize_with = "deserialize_string_to_u64")]
821    pub ts: u64,
822}
823
824////////////////////////////////////////////////////////////////////////////////
825// Tests
826////////////////////////////////////////////////////////////////////////////////
827
828#[cfg(test)]
829mod tests {
830    use rstest::rstest;
831    use serde_json;
832
833    use super::*;
834
835    #[rstest]
836    fn test_algo_order_request_serialization() {
837        let request = OKXPlaceAlgoOrderRequest {
838            inst_id: "ETH-USDT-SWAP".to_string(),
839            td_mode: OKXTradeMode::Isolated,
840            side: OKXSide::Buy,
841            ord_type: OKXAlgoOrderType::Trigger,
842            sz: "0.01".to_string(),
843            algo_cl_ord_id: Some("test123".to_string()),
844            trigger_px: Some("3000".to_string()),
845            order_px: Some("-1".to_string()),
846            trigger_px_type: Some(OKXTriggerType::Last),
847            tgt_ccy: None,
848            pos_side: None,
849            close_position: None,
850            tag: None,
851            reduce_only: None,
852        };
853
854        let json = serde_json::to_string(&request).unwrap();
855
856        // Verify that fields are serialized with correct camelCase names
857        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
858        assert!(json.contains("\"tdMode\":\"isolated\""));
859        assert!(json.contains("\"ordType\":\"trigger\""));
860        assert!(json.contains("\"algoClOrdId\":\"test123\""));
861        assert!(json.contains("\"triggerPx\":\"3000\""));
862        assert!(json.contains("\"orderPx\":\"-1\""));
863        assert!(json.contains("\"triggerPxType\":\"last\""));
864
865        // Verify that None fields are not included
866        assert!(!json.contains("tgtCcy"));
867        assert!(!json.contains("posSide"));
868        assert!(!json.contains("closePosition"));
869    }
870
871    #[rstest]
872    fn test_algo_order_request_array_serialization() {
873        let request = OKXPlaceAlgoOrderRequest {
874            inst_id: "BTC-USDT".to_string(),
875            td_mode: OKXTradeMode::Cross,
876            side: OKXSide::Sell,
877            ord_type: OKXAlgoOrderType::Trigger,
878            sz: "0.1".to_string(),
879            algo_cl_ord_id: None,
880            trigger_px: Some("50000".to_string()),
881            order_px: Some("49900".to_string()),
882            trigger_px_type: Some(OKXTriggerType::Mark),
883            tgt_ccy: Some("base_ccy".to_string()),
884            pos_side: Some(OKXPositionSide::Net),
885            close_position: None,
886            tag: None,
887            reduce_only: Some(true),
888        };
889
890        // OKX expects an array of requests
891        let json = serde_json::to_string(&[request]).unwrap();
892
893        // Verify array format
894        assert!(json.starts_with('['));
895        assert!(json.ends_with(']'));
896
897        // Verify correct field names
898        assert!(json.contains("\"instId\":\"BTC-USDT\""));
899        assert!(json.contains("\"tdMode\":\"cross\""));
900        assert!(json.contains("\"triggerPx\":\"50000\""));
901        assert!(json.contains("\"orderPx\":\"49900\""));
902        assert!(json.contains("\"triggerPxType\":\"mark\""));
903        assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
904        assert!(json.contains("\"posSide\":\"net\""));
905        assert!(json.contains("\"reduceOnly\":true"));
906    }
907
908    #[rstest]
909    fn test_cancel_algo_order_request_serialization() {
910        let request = OKXCancelAlgoOrderRequest {
911            inst_id: "ETH-USDT-SWAP".to_string(),
912            algo_id: Some("123456".to_string()),
913            algo_cl_ord_id: None,
914        };
915
916        let json = serde_json::to_string(&request).unwrap();
917
918        // Verify correct field names
919        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
920        assert!(json.contains("\"algoId\":\"123456\""));
921        assert!(!json.contains("algoClOrdId"));
922    }
923
924    #[rstest]
925    fn test_cancel_algo_order_with_client_id_serialization() {
926        let request = OKXCancelAlgoOrderRequest {
927            inst_id: "BTC-USDT".to_string(),
928            algo_id: None,
929            algo_cl_ord_id: Some("client123".to_string()),
930        };
931
932        // OKX expects an array of requests
933        let json = serde_json::to_string(&[request]).unwrap();
934
935        // Verify array format and field names
936        assert!(json.starts_with('['));
937        assert!(json.contains("\"instId\":\"BTC-USDT\""));
938        assert!(json.contains("\"algoClOrdId\":\"client123\""));
939        assert!(!json.contains("\"algoId\""));
940    }
941}