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