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_optional_string_to_u64, 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        OKXAlgoOrderStatus, OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode,
72        OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXSpreadState,
73        OKXSpreadType, OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
74    },
75    parse::deserialize_string_to_u64,
76};
77
78/// 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 a spread from the GET /api/v5/sprd/spreads endpoint.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct OKXSpread {
120    /// Spread ID.
121    pub sprd_id: Ustr,
122    /// Spread type.
123    pub sprd_type: OKXSpreadType,
124    /// Spread status.
125    pub state: OKXSpreadState,
126    /// Base currency.
127    pub base_ccy: Ustr,
128    /// Size currency.
129    pub sz_ccy: Ustr,
130    /// Quote currency.
131    pub quote_ccy: Ustr,
132    /// Tick size in quote currency.
133    pub tick_sz: String,
134    /// Minimum order size in size currency.
135    pub min_sz: String,
136    /// Order size increment in size currency.
137    pub lot_sz: String,
138    /// Listing time in milliseconds.
139    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
140    pub list_time: Option<u64>,
141    /// Expiry time in milliseconds.
142    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
143    pub exp_time: Option<u64>,
144    /// Last update time in milliseconds.
145    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
146    pub u_time: Option<u64>,
147    /// Spread legs.
148    pub legs: Vec<OKXSpreadLeg>,
149}
150
151/// Represents a leg in an OKX spread.
152#[derive(Clone, Debug, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct OKXSpreadLeg {
155    /// Instrument ID.
156    pub inst_id: Ustr,
157    /// Leg side.
158    pub side: OKXSide,
159}
160
161/// Represents the request body for `POST /api/v5/sprd/order`.
162#[derive(Clone, Debug, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct OKXPlaceSpreadOrderRequest {
165    /// Spread ID.
166    pub sprd_id: String,
167    /// Client-supplied order ID.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub cl_ord_id: Option<String>,
170    /// Order tag.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub tag: Option<String>,
173    /// Order side.
174    pub side: OKXSide,
175    /// Order type.
176    pub ord_type: OKXOrderType,
177    /// Order size.
178    pub sz: String,
179    /// Limit price.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub px: Option<String>,
182}
183
184/// Represents the request body for `POST /api/v5/sprd/cancel-order`.
185#[derive(Clone, Debug, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct OKXCancelSpreadOrderRequest {
188    /// Order ID.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub ord_id: Option<String>,
191    /// Client-supplied order ID.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub cl_ord_id: Option<String>,
194}
195
196/// Represents the request body for `POST /api/v5/sprd/mass-cancel`.
197#[derive(Clone, Debug, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct OKXCancelAllSpreadOrdersRequest {
200    /// Spread ID.
201    pub sprd_id: String,
202}
203
204/// Represents a spread order from `GET /api/v5/sprd/order` and history endpoints.
205#[derive(Clone, Debug, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct OKXSpreadOrder {
208    /// Spread ID.
209    pub sprd_id: Ustr,
210    /// Order ID.
211    pub ord_id: Ustr,
212    /// Client order ID.
213    #[serde(default)]
214    pub cl_ord_id: Ustr,
215    /// Order tag.
216    #[serde(default)]
217    pub tag: String,
218    /// Order side.
219    pub side: OKXSide,
220    /// Order type.
221    pub ord_type: OKXOrderType,
222    /// Order size.
223    pub sz: String,
224    /// Order price.
225    #[serde(default)]
226    pub px: String,
227    /// Average fill price.
228    #[serde(default)]
229    pub avg_px: String,
230    /// Order state.
231    pub state: OKXOrderStatus,
232    /// Accumulated filled size.
233    #[serde(default)]
234    pub acc_fill_sz: String,
235    /// Pending fill size.
236    #[serde(default)]
237    pub pending_fill_sz: String,
238    /// Pending settlement size.
239    #[serde(default)]
240    pub pending_settle_sz: String,
241    /// Canceled size.
242    #[serde(default)]
243    pub canceled_sz: String,
244    /// Last fill size.
245    #[serde(default)]
246    pub fill_sz: String,
247    /// Last fill price.
248    #[serde(default)]
249    pub fill_px: String,
250    /// Trade ID for the last fill, if provided.
251    #[serde(default)]
252    pub trade_id: Ustr,
253    /// Cancel source.
254    #[serde(default)]
255    pub cancel_source: String,
256    /// Request ID for amend responses.
257    #[serde(default)]
258    pub req_id: String,
259    /// Amend result.
260    #[serde(default)]
261    pub amend_result: String,
262    /// Response code.
263    #[serde(default)]
264    pub code: String,
265    /// Response message.
266    #[serde(default)]
267    pub msg: String,
268    /// Creation time in milliseconds.
269    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
270    pub c_time: Option<u64>,
271    /// Last update time in milliseconds.
272    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
273    pub u_time: Option<u64>,
274}
275
276/// Represents a spread trade from `GET /api/v5/sprd/trades`.
277#[derive(Clone, Debug, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct OKXSpreadTrade {
280    /// Spread ID.
281    pub sprd_id: Ustr,
282    /// Trade ID.
283    pub trade_id: Ustr,
284    /// Order ID.
285    pub ord_id: Ustr,
286    /// Client order ID.
287    #[serde(default)]
288    pub cl_ord_id: Ustr,
289    /// Last filled price.
290    pub fill_px: String,
291    /// Last filled quantity.
292    pub fill_sz: String,
293    /// Trade side.
294    pub side: OKXSide,
295    /// Execution type.
296    #[serde(default)]
297    pub exec_type: OKXExecType,
298    /// Fee currency.
299    #[serde(default)]
300    pub fee_ccy: String,
301    /// Fee amount.
302    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
303    pub fee: Option<String>,
304    /// Timestamp in milliseconds.
305    #[serde(deserialize_with = "deserialize_string_to_u64")]
306    pub ts: u64,
307}
308
309/// Settlement configuration for an OKX event contract series.
310#[derive(Clone, Debug, Default, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct OKXEventContractSettlement {
313    /// Settlement method.
314    #[serde(default)]
315    pub method: String,
316    /// Whether the market can settle before expiry.
317    #[serde(default)]
318    pub close_early: bool,
319    /// Settlement source name.
320    #[serde(default)]
321    pub src_name: String,
322    /// Price underlying in OKX symbol format.
323    #[serde(default)]
324    pub underlying: String,
325}
326
327/// Represents an event contract series from the GET /api/v5/public/event-contract/series endpoint.
328#[derive(Clone, Debug, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct OKXEventContractSeries {
331    /// Series ID.
332    pub series_id: String,
333    /// Series frequency.
334    #[serde(default)]
335    pub freq: String,
336    /// Series title.
337    #[serde(default)]
338    pub title: String,
339    /// Series category.
340    #[serde(default)]
341    pub category: String,
342    /// Settlement information.
343    #[serde(default)]
344    pub settlement: OKXEventContractSettlement,
345}
346
347/// Represents an event from the GET /api/v5/public/event-contract/events endpoint.
348#[derive(Clone, Debug, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct OKXEventContractEvent {
351    /// Series ID.
352    pub series_id: String,
353    /// Event ID.
354    pub event_id: String,
355    /// Fixing time in milliseconds, if available.
356    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
357    pub fix_time: Option<u64>,
358    /// Expiry time in milliseconds.
359    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
360    pub exp_time: Option<u64>,
361    /// Event state.
362    pub state: String,
363}
364
365/// Represents an event market from the GET /api/v5/public/event-contract/markets endpoint.
366#[derive(Clone, Debug, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct OKXEventContractMarket {
369    /// Series ID.
370    pub series_id: String,
371    /// Event ID.
372    pub event_id: String,
373    /// Instrument ID.
374    pub inst_id: Ustr,
375    /// Listing time in milliseconds.
376    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
377    pub list_time: Option<u64>,
378    /// Fixing time in milliseconds, if available.
379    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
380    pub fix_time: Option<u64>,
381    /// Expiry time in milliseconds.
382    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
383    pub exp_time: Option<u64>,
384    /// Market state.
385    pub state: String,
386    /// Whether the market has been disputed.
387    pub disputed: bool,
388    /// Market outcome: 0 unavailable, 1 yes, 2 no.
389    pub outcome: String,
390    /// Minimum expiration value for a yes outcome.
391    pub floor_strike: String,
392    /// Settlement value when expired.
393    pub settle_value: String,
394}
395
396/// Represents an index price from the GET /api/v5/public/index-tickers endpoint.
397#[derive(Clone, Debug, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct OKXIndexTicker {
400    /// Instrument ID.
401    pub inst_id: Ustr,
402    /// The index price.
403    pub idx_px: String,
404    /// The timestamp for the index price.
405    #[serde(deserialize_with = "deserialize_string_to_u64")]
406    pub ts: u64,
407}
408
409/// Represents an order book level from the GET /api/v5/market/books endpoint.
410/// Each entry is a 4-element tuple: [price, size, liquidated_orders, num_orders].
411pub type OKXOrderBookLevel = (String, String, String, String);
412
413/// Represents an order book snapshot from the GET /api/v5/market/books endpoint.
414#[derive(Clone, Debug, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct OKXOrderBookSnapshot {
417    /// Ask levels [price, size, liquidated_orders_count, orders_count].
418    pub asks: Vec<OKXOrderBookLevel>,
419    /// Bid levels [price, size, liquidated_orders_count, orders_count].
420    pub bids: Vec<OKXOrderBookLevel>,
421    /// Timestamp in milliseconds.
422    #[serde(deserialize_with = "deserialize_string_to_u64")]
423    pub ts: u64,
424}
425
426/// Represents a funding rate history entry from the GET /api/v5/public/funding-rate-history endpoint.
427#[derive(Clone, Debug, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct OKXFundingRateHistory {
430    /// Instrument type.
431    pub inst_type: OKXInstrumentType,
432    /// Instrument ID.
433    pub inst_id: Ustr,
434    /// Funding rate.
435    pub funding_rate: String,
436    /// Realized rate.
437    pub realized_rate: String,
438    /// Funding time, Unix timestamp in milliseconds.
439    #[serde(deserialize_with = "deserialize_string_to_u64")]
440    pub funding_time: u64,
441    /// Funding rate calculation method.
442    #[serde(default)]
443    pub method: Option<String>,
444}
445
446/// Represents a position tier from the GET /api/v5/public/position-tiers endpoint.
447#[derive(Clone, Debug, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct OKXPositionTier {
450    /// Underlying.
451    pub uly: Ustr,
452    /// Instrument family.
453    pub inst_family: String,
454    /// Instrument ID.
455    pub inst_id: Ustr,
456    /// Tier level.
457    pub tier: String,
458    /// Minimum size/amount for the tier.
459    pub min_sz: String,
460    /// Maximum size/amount for the tier.
461    pub max_sz: String,
462    /// Maintenance margin requirement rate.
463    pub mmr: String,
464    /// Initial margin requirement rate.
465    pub imr: String,
466    /// Maximum available leverage.
467    pub max_lever: String,
468    /// Option Margin Coefficient (only applicable to options).
469    pub opt_mgn_factor: String,
470    /// Quote currency borrowing amount.
471    pub quote_max_loan: String,
472    /// Base currency borrowing amount.
473    pub base_max_loan: String,
474}
475
476/// Represents an account balance snapshot from `GET /api/v5/account/balance`.
477#[derive(Clone, Debug, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct OKXAccount {
480    /// Adjusted/Effective equity in USD.
481    pub adj_eq: String,
482    /// Borrow frozen amount.
483    pub borrow_froz: String,
484    /// Account details by currency.
485    pub details: Vec<OKXBalanceDetail>,
486    /// Initial margin requirement.
487    pub imr: String,
488    /// Isolated margin equity.
489    pub iso_eq: String,
490    /// Margin ratio.
491    pub mgn_ratio: String,
492    /// Maintenance margin requirement.
493    pub mmr: String,
494    /// Notional value in USD for borrow.
495    pub notional_usd_for_borrow: String,
496    /// Notional value in USD for futures.
497    pub notional_usd_for_futures: String,
498    /// Notional value in USD for option.
499    pub notional_usd_for_option: String,
500    /// Notional value in USD for swap.
501    pub notional_usd_for_swap: String,
502    /// Notional value in USD.
503    pub notional_usd: String,
504    /// Order frozen.
505    pub ord_froz: String,
506    /// Total equity in USD.
507    pub total_eq: String,
508    /// Last update time, Unix timestamp in milliseconds.
509    #[serde(deserialize_with = "deserialize_string_to_u64")]
510    pub u_time: u64,
511    /// Unrealized profit and loss.
512    pub upl: String,
513}
514
515/// Represents a balance detail for a single currency in an OKX account.
516#[derive(Clone, Debug, Serialize, Deserialize)]
517#[serde(rename_all = "camelCase")]
518#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
519#[cfg_attr(
520    feature = "python",
521    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
522)]
523pub struct OKXBalanceDetail {
524    /// Available balance.
525    pub avail_bal: String,
526    /// Available equity.
527    pub avail_eq: String,
528    /// Borrow frozen amount.
529    pub borrow_froz: String,
530    /// Cash balance.
531    pub cash_bal: String,
532    /// Currency.
533    pub ccy: Ustr,
534    /// Cross liability.
535    pub cross_liab: String,
536    /// Discount equity in USD.
537    pub dis_eq: String,
538    /// Equity.
539    pub eq: String,
540    /// Equity in USD.
541    pub eq_usd: String,
542    /// Same-token equity.
543    pub smt_sync_eq: String,
544    /// Copy trading equity.
545    pub spot_copy_trading_eq: String,
546    /// Fixed balance.
547    pub fixed_bal: String,
548    /// Frozen balance.
549    pub frozen_bal: String,
550    /// Initial margin requirement.
551    pub imr: String,
552    /// Interest.
553    pub interest: String,
554    /// Isolated margin equity.
555    pub iso_eq: String,
556    /// Isolated margin liability.
557    pub iso_liab: String,
558    /// Isolated unrealized profit and loss.
559    pub iso_upl: String,
560    /// Liability.
561    pub liab: String,
562    /// Maximum loan amount.
563    pub max_loan: String,
564    /// Margin ratio.
565    pub mgn_ratio: String,
566    /// Maintenance margin requirement.
567    pub mmr: String,
568    /// Notional leverage.
569    pub notional_lever: String,
570    /// Order frozen.
571    pub ord_frozen: String,
572    /// Reward balance.
573    pub reward_bal: String,
574    /// Spot in use amount.
575    #[serde(alias = "spotInUse")]
576    pub spot_in_use_amt: String,
577    /// Cross liability spot in use amount.
578    #[serde(alias = "clSpotInUse")]
579    pub cl_spot_in_use_amt: String,
580    /// Maximum spot in use amount.
581    #[serde(alias = "maxSpotInUse")]
582    pub max_spot_in_use_amt: String,
583    /// Spot isolated balance.
584    pub spot_iso_bal: String,
585    /// Strategy equity.
586    pub stgy_eq: String,
587    /// Time-weighted average price.
588    pub twap: String,
589    /// Last update time, Unix timestamp in milliseconds.
590    #[serde(deserialize_with = "deserialize_string_to_u64")]
591    pub u_time: u64,
592    /// Unrealized profit and loss.
593    pub upl: String,
594    /// Unrealized profit and loss liability.
595    pub upl_liab: String,
596    /// Spot balance.
597    pub spot_bal: String,
598    /// Open average price.
599    pub open_avg_px: String,
600    /// Accumulated average price.
601    pub acc_avg_px: String,
602    /// Spot unrealized profit and loss.
603    pub spot_upl: String,
604    /// Spot unrealized profit and loss ratio.
605    pub spot_upl_ratio: String,
606    /// Total profit and loss.
607    pub total_pnl: String,
608    /// Total profit and loss ratio.
609    pub total_pnl_ratio: String,
610}
611
612/// Represents a single open position from `GET /api/v5/account/positions`.
613#[derive(Clone, Debug, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub struct OKXPosition {
616    /// Instrument ID.
617    pub inst_id: Ustr,
618    /// Instrument type.
619    pub inst_type: OKXInstrumentType,
620    /// Margin mode: isolated/cross.
621    pub mgn_mode: OKXMarginMode,
622    /// Position ID.
623    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
624    pub pos_id: Option<Ustr>,
625    /// Position side: long/short.
626    pub pos_side: OKXPositionSide,
627    /// Position size.
628    pub pos: String,
629    /// Base currency balance.
630    pub base_bal: String,
631    /// Position currency.
632    pub ccy: String,
633    /// Trading fee.
634    pub fee: String,
635    /// Position leverage.
636    pub lever: String,
637    /// Last traded price.
638    pub last: String,
639    /// Mark price.
640    pub mark_px: String,
641    /// Liquidation price.
642    pub liq_px: String,
643    /// Maintenance margin requirement.
644    pub mmr: String,
645    /// Interest.
646    pub interest: String,
647    /// Trade ID.
648    pub trade_id: Ustr,
649    /// Notional value of position in USD.
650    pub notional_usd: String,
651    /// Average entry price.
652    pub avg_px: String,
653    /// Unrealized profit and loss.
654    pub upl: String,
655    /// Unrealized profit and loss ratio.
656    pub upl_ratio: String,
657    /// Last update time, Unix timestamp in milliseconds.
658    #[serde(deserialize_with = "deserialize_string_to_u64")]
659    pub u_time: u64,
660    /// Position margin.
661    pub margin: String,
662    /// Margin ratio.
663    pub mgn_ratio: String,
664    /// Auto-deleveraging (ADL) ranking.
665    pub adl: String,
666    /// Creation time, Unix timestamp in milliseconds.
667    pub c_time: String,
668    /// Realized profit and loss.
669    pub realized_pnl: String,
670    /// Unrealized profit and loss at last price.
671    pub upl_last_px: String,
672    /// Unrealized profit and loss ratio at last price.
673    pub upl_ratio_last_px: String,
674    /// Available position that can be closed.
675    pub avail_pos: String,
676    /// Breakeven price.
677    pub be_px: String,
678    /// Funding fee.
679    pub funding_fee: String,
680    /// Index price.
681    pub idx_px: String,
682    /// Liquidation penalty.
683    pub liq_penalty: String,
684    /// Option value.
685    pub opt_val: String,
686    /// Pending close order liability value.
687    pub pending_close_ord_liab_val: String,
688    /// Total profit and loss.
689    pub pnl: String,
690    /// Position currency.
691    pub pos_ccy: String,
692    /// Quote currency balance.
693    pub quote_bal: String,
694    /// Borrowed amount in quote currency.
695    pub quote_borrowed: String,
696    /// Interest on quote currency.
697    pub quote_interest: String,
698    /// Amount in use for spot trading.
699    #[serde(alias = "spotInUse")]
700    pub spot_in_use_amt: String,
701    /// Currency in use for spot trading.
702    pub spot_in_use_ccy: String,
703    /// USD price.
704    pub usd_px: String,
705    /// Black-Scholes delta in dollars, only applicable to OPTION.
706    #[serde(default)]
707    pub delta_bs: String,
708    /// Black-Scholes gamma in dollars, only applicable to OPTION.
709    #[serde(default)]
710    pub gamma_bs: String,
711    /// Black-Scholes theta in dollars, only applicable to OPTION.
712    #[serde(default)]
713    pub theta_bs: String,
714    /// Black-Scholes vega in dollars, only applicable to OPTION.
715    #[serde(default)]
716    pub vega_bs: String,
717}
718
719/// Represents the response from `POST /api/v5/trade/order` (place order).
720/// This model is designed to be flexible and handle the minimal fields that the API returns.
721#[derive(Clone, Debug, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase")]
723pub struct OKXPlaceOrderResponse {
724    /// Order ID.
725    #[serde(default)]
726    pub ord_id: Option<Ustr>,
727    /// Client order ID.
728    #[serde(default)]
729    pub cl_ord_id: Option<Ustr>,
730    /// Order tag.
731    #[serde(default)]
732    pub tag: Option<String>,
733    /// Instrument ID (optional - might not be in response).
734    #[serde(default)]
735    pub inst_id: Option<Ustr>,
736    /// Order side (optional).
737    #[serde(default)]
738    pub side: Option<OKXSide>,
739    /// Order type (optional).
740    #[serde(default)]
741    pub ord_type: Option<OKXOrderType>,
742    /// Order size (optional).
743    #[serde(default)]
744    pub sz: Option<String>,
745    /// Order state (optional).
746    pub state: Option<OKXOrderStatus>,
747    /// Price (optional).
748    #[serde(default)]
749    pub px: Option<String>,
750    /// Average price (optional).
751    #[serde(default)]
752    pub avg_px: Option<String>,
753    /// Accumulated filled size.
754    #[serde(default)]
755    pub acc_fill_sz: Option<String>,
756    /// Fill size (optional).
757    #[serde(default)]
758    pub fill_sz: Option<String>,
759    /// Fill price (optional).
760    #[serde(default)]
761    pub fill_px: Option<String>,
762    /// Trade ID (optional).
763    #[serde(default)]
764    pub trade_id: Option<Ustr>,
765    /// Fill time (optional).
766    #[serde(default)]
767    pub fill_time: Option<String>,
768    /// Fee (optional).
769    #[serde(default)]
770    pub fee: Option<String>,
771    /// Fee currency (optional).
772    #[serde(default)]
773    pub fee_ccy: Option<String>,
774    /// Request ID (optional).
775    #[serde(default)]
776    pub req_id: Option<Ustr>,
777    /// Position side (optional).
778    #[serde(default)]
779    pub pos_side: Option<OKXPositionSide>,
780    /// Reduce-only flag (optional).
781    #[serde(default)]
782    pub reduce_only: Option<String>,
783    /// Target currency (optional).
784    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
785    pub tgt_ccy: Option<OKXTargetCurrency>,
786    /// Creation time.
787    #[serde(default)]
788    pub c_time: Option<String>,
789    /// Last update time (optional).
790    #[serde(default)]
791    pub u_time: Option<String>,
792    /// The result of the request.
793    #[serde(skip_serializing_if = "Option::is_none")]
794    pub s_code: Option<String>,
795    /// Error message if the request failed.
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub s_msg: Option<String>,
798    /// Detailed error code if the request failed.
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub sub_code: Option<String>,
801}
802
803/// Represents an attached TP/SL instruction on `POST /api/v5/trade/order`.
804#[derive(Clone, Debug, Default, Serialize, Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct OKXAttachAlgoOrdRequest {
807    /// Client order ID for the attached TP/SL OCO object.
808    #[serde(skip_serializing_if = "Option::is_none")]
809    pub attach_algo_cl_ord_id: Option<String>,
810    /// Stop-loss trigger price.
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub sl_trigger_px: Option<String>,
813    /// Stop-loss order price.
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub sl_ord_px: Option<String>,
816    /// Stop-loss trigger price type.
817    #[serde(skip_serializing_if = "Option::is_none")]
818    pub sl_trigger_px_type: Option<OKXTriggerType>,
819    /// Take-profit trigger price.
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub tp_trigger_px: Option<String>,
822    /// Take-profit order price.
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub tp_ord_px: Option<String>,
825    /// Take-profit trigger price type.
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub tp_trigger_px_type: Option<OKXTriggerType>,
828    /// Callback ratio for attached trailing stop orders.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub callback_ratio: Option<String>,
831    /// Callback spread for attached trailing stop orders.
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub callback_spread: Option<String>,
834    /// Activation price for attached trailing stop orders.
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub active_px: Option<String>,
837    /// New callback ratio for amended attached trailing stop orders.
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub new_callback_ratio: Option<String>,
840    /// New callback spread for amended attached trailing stop orders.
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub new_callback_spread: Option<String>,
843    /// New activation price for amended attached trailing stop orders.
844    #[serde(skip_serializing_if = "Option::is_none")]
845    pub new_active_px: Option<String>,
846}
847
848/// Represents the request body for `POST /api/v5/trade/order` (place order).
849#[derive(Clone, Debug, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct OKXPlaceOrderRequest {
852    /// Instrument ID.
853    pub inst_id: String,
854    /// Trade mode (cash, cross, isolated).
855    pub td_mode: OKXTradeMode,
856    /// Currency used for margin trading when required by OKX.
857    #[serde(skip_serializing_if = "Option::is_none")]
858    pub ccy: Option<String>,
859    /// Client-supplied order ID.
860    #[serde(skip_serializing_if = "Option::is_none")]
861    pub cl_ord_id: Option<String>,
862    /// Order tag.
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub tag: Option<String>,
865    /// Order side (buy, sell).
866    pub side: OKXSide,
867    /// Position side for derivatives.
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub pos_side: Option<OKXPositionSide>,
870    /// Order type.
871    pub ord_type: OKXOrderType,
872    /// Order size.
873    pub sz: String,
874    /// Limit price when required by the order type.
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub px: Option<String>,
877    /// Price in USD, only applicable to options. Mutually exclusive with `px` and `px_vol`.
878    #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
879    pub px_usd: Option<String>,
880    /// Price in implied volatility (1 = 100%), only applicable to options.
881    /// Mutually exclusive with `px` and `px_usd`.
882    #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
883    pub px_vol: Option<String>,
884    /// Reduce-only flag.
885    #[serde(skip_serializing_if = "Option::is_none")]
886    pub reduce_only: Option<bool>,
887    /// Target currency for spot market orders.
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub tgt_ccy: Option<OKXTargetCurrency>,
890    /// Attached TP/SL OCO instructions.
891    #[serde(skip_serializing_if = "Option::is_none")]
892    pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
893    /// Event contract speed bump flag. Use "1" for non-post-only EVENTS orders.
894    #[serde(skip_serializing_if = "Option::is_none")]
895    pub speed_bump: Option<String>,
896    /// Event contract market outcome: yes or no.
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub outcome: Option<String>,
899    /// Slippage tolerance for market orders, expressed as a decimal fraction
900    /// (e.g., "0.005" for 0.5%). Supported instrument/order-type scope is
901    /// venue-controlled; rejected with `54084`/`54085` if exceeded or out of
902    /// the venue's accepted range. See the OKX v5 docs for the current matrix.
903    #[serde(skip_serializing_if = "Option::is_none")]
904    pub slippage_pct: Option<String>,
905}
906
907/// Represents the request body for `POST /api/v5/trade/cancel-batch-orders`.
908#[derive(Clone, Debug, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct OKXCancelOrderRequest {
911    /// Instrument ID.
912    pub inst_id: String,
913    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
914    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
915    pub inst_id_code: Option<u64>,
916    /// Order ID.
917    #[serde(skip_serializing_if = "Option::is_none")]
918    pub ord_id: Option<String>,
919    /// Client-supplied order ID.
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub cl_ord_id: Option<String>,
922}
923
924/// Represents a single response item from `POST /api/v5/trade/cancel-batch-orders`.
925#[derive(Clone, Debug, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct OKXCancelOrderResponse {
928    /// Order ID.
929    pub ord_id: String,
930    /// Client-supplied order ID.
931    #[serde(default)]
932    pub cl_ord_id: Option<String>,
933    /// The result of the request.
934    #[serde(skip_serializing_if = "Option::is_none")]
935    pub s_code: Option<String>,
936    /// Error message if the request failed.
937    #[serde(skip_serializing_if = "Option::is_none")]
938    pub s_msg: Option<String>,
939    /// Response timestamp.
940    #[serde(default)]
941    pub ts: Option<String>,
942}
943
944pub use crate::common::models::OKXAttachedAlgoOrd;
945
946/// Represents a single historical order record from `GET /api/v5/trade/orders-history`.
947#[derive(Clone, Debug, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949pub struct OKXOrderHistory {
950    /// Order ID.
951    pub ord_id: Ustr,
952    /// Client order ID.
953    pub cl_ord_id: Ustr,
954    /// Algo order ID (for conditional orders).
955    #[serde(default)]
956    pub algo_id: Option<Ustr>,
957    /// Client-supplied algo order ID (for conditional orders).
958    #[serde(default)]
959    pub algo_cl_ord_id: Option<Ustr>,
960    /// Attached child client order ID if OKX surfaces one at the top level.
961    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
962    pub attach_algo_cl_ord_id: Option<String>,
963    /// Attached TP/SL child orders associated with the parent order.
964    #[serde(default)]
965    pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
966    /// Client account ID (may be omitted by OKX).
967    #[serde(default)]
968    pub cl_act_id: Option<Ustr>,
969    /// Order tag.
970    pub tag: String,
971    /// Instrument type.
972    pub inst_type: OKXInstrumentType,
973    /// Underlying (optional).
974    pub uly: Option<Ustr>,
975    /// Instrument ID.
976    pub inst_id: Ustr,
977    /// Order type.
978    pub ord_type: OKXOrderType,
979    /// Order size.
980    pub sz: String,
981    /// Price (optional).
982    pub px: String,
983    /// Price in USD (options only).
984    #[serde(default)]
985    pub px_usd: String,
986    /// Price in implied volatility (options only).
987    #[serde(default)]
988    pub px_vol: String,
989    /// Side.
990    pub side: OKXSide,
991    /// Position side.
992    pub pos_side: OKXPositionSide,
993    /// Trade mode.
994    pub td_mode: OKXTradeMode,
995    /// Reduce-only flag.
996    pub reduce_only: String,
997    /// Target currency (optional).
998    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
999    pub tgt_ccy: Option<OKXTargetCurrency>,
1000    /// Order state.
1001    pub state: OKXOrderStatus,
1002    /// Average price (optional).
1003    pub avg_px: String,
1004    /// Execution fee.
1005    pub fee: String,
1006    /// Fee currency.
1007    pub fee_ccy: String,
1008    /// Filled size (optional).
1009    pub fill_sz: String,
1010    /// Fill price (optional).
1011    pub fill_px: String,
1012    /// Trade ID (optional).
1013    pub trade_id: Ustr,
1014    /// Fill time, Unix timestamp in milliseconds.
1015    #[serde(deserialize_with = "deserialize_string_to_u64")]
1016    pub fill_time: u64,
1017    /// Accumulated filled size.
1018    pub acc_fill_sz: String,
1019    /// Fill fee (optional, may be omitted).
1020    #[serde(default)]
1021    pub fill_fee: Option<String>,
1022    /// Request ID (optional).
1023    #[serde(default)]
1024    pub req_id: Option<Ustr>,
1025    /// Cancelled filled size (optional).
1026    #[serde(default)]
1027    pub cancel_fill_sz: Option<String>,
1028    /// Cancelled total size (optional).
1029    #[serde(default)]
1030    pub cancel_total_sz: Option<String>,
1031    /// Fee discount (optional).
1032    #[serde(default)]
1033    pub fee_discount: Option<String>,
1034    /// Order category (normal, liquidation, ADL, etc.).
1035    pub category: OKXOrderCategory,
1036    /// Event contract market outcome, if applicable.
1037    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1038    pub outcome: Option<String>,
1039    /// Last update time, Unix timestamp in milliseconds.
1040    #[serde(deserialize_with = "deserialize_string_to_u64")]
1041    pub u_time: u64,
1042    /// Creation time.
1043    #[serde(deserialize_with = "deserialize_string_to_u64")]
1044    pub c_time: u64,
1045}
1046
1047/// Represents an algo order response from `/trade/order-algo-*` endpoints.
1048#[derive(Clone, Debug, Serialize, Deserialize)]
1049#[serde(rename_all = "camelCase")]
1050pub struct OKXOrderAlgo {
1051    /// Algo order ID assigned by OKX.
1052    pub algo_id: String,
1053    /// Client-specified algo order ID.
1054    #[serde(default)]
1055    pub algo_cl_ord_id: String,
1056    /// Client order ID (empty until triggered).
1057    #[serde(default)]
1058    pub cl_ord_id: String,
1059    /// Venue order ID (empty until triggered).
1060    #[serde(default)]
1061    pub ord_id: String,
1062    /// Instrument ID, e.g. `ETH-USDT-SWAP`.
1063    pub inst_id: Ustr,
1064    /// Instrument type.
1065    pub inst_type: OKXInstrumentType,
1066    /// Algo order type.
1067    pub ord_type: OKXAlgoOrderType,
1068    /// Current order state.
1069    pub state: OKXAlgoOrderStatus,
1070    /// Order side.
1071    pub side: OKXSide,
1072    /// Position side.
1073    pub pos_side: OKXPositionSide,
1074    /// Submitted size.
1075    #[serde(default)]
1076    pub sz: String,
1077    /// Trigger price (empty for certain algo styles).
1078    #[serde(default)]
1079    pub trigger_px: String,
1080    /// Trigger price type (last/mark/index).
1081    #[serde(default)]
1082    pub trigger_px_type: Option<OKXTriggerType>,
1083    /// Stop-loss trigger price for conditional close orders.
1084    #[serde(default)]
1085    pub sl_trigger_px: String,
1086    /// Stop-loss order price for conditional close orders.
1087    #[serde(default)]
1088    pub sl_ord_px: String,
1089    /// Stop-loss trigger price type (last/mark/index).
1090    #[serde(default)]
1091    pub sl_trigger_px_type: Option<OKXTriggerType>,
1092    /// Take-profit trigger price for conditional close orders.
1093    #[serde(default)]
1094    pub tp_trigger_px: String,
1095    /// Take-profit order price for conditional close orders.
1096    #[serde(default)]
1097    pub tp_ord_px: String,
1098    /// Take-profit trigger price type (last/mark/index).
1099    #[serde(default)]
1100    pub tp_trigger_px_type: Option<OKXTriggerType>,
1101    /// Order price (-1 indicates market execution once triggered).
1102    #[serde(default)]
1103    pub ord_px: String,
1104    /// Trade mode (cash/cross/isolated).
1105    pub td_mode: OKXTradeMode,
1106    /// Algo leverage configuration.
1107    #[serde(default)]
1108    pub lever: String,
1109    /// Reduce-only flag.
1110    #[serde(default)]
1111    pub reduce_only: String,
1112    /// Fraction of the position to close for close-order algos.
1113    #[serde(default)]
1114    pub close_fraction: String,
1115    /// Executed price (if triggered).
1116    #[serde(default)]
1117    pub actual_px: String,
1118    /// Executed size (if triggered).
1119    #[serde(default)]
1120    pub actual_sz: String,
1121    /// Notional value in USD.
1122    #[serde(default)]
1123    pub notional_usd: String,
1124    /// Creation time (milliseconds).
1125    #[serde(deserialize_with = "deserialize_string_to_u64")]
1126    pub c_time: u64,
1127    /// Last update time (milliseconds).
1128    #[serde(deserialize_with = "deserialize_string_to_u64")]
1129    pub u_time: u64,
1130    /// Trigger timestamp (if triggered).
1131    #[serde(default)]
1132    pub trigger_time: String,
1133    /// Optional tag supplied during submission.
1134    #[serde(default)]
1135    pub tag: String,
1136    /// Callback price ratio for trailing stop (e.g. "0.01" for 1%).
1137    #[serde(default)]
1138    pub callback_ratio: String,
1139    /// Callback price spread for trailing stop (absolute distance).
1140    #[serde(default)]
1141    pub callback_spread: String,
1142    /// Activation price for trailing stop.
1143    #[serde(default)]
1144    pub active_px: String,
1145}
1146
1147/// Represents a transaction detail (fill) from `GET /api/v5/trade/fills`.
1148#[derive(Clone, Debug, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150pub struct OKXTransactionDetail {
1151    /// Product type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
1152    pub inst_type: OKXInstrumentType,
1153    /// Instrument ID, e.g. "BTC-USDT".
1154    pub inst_id: Ustr,
1155    /// Trade ID.
1156    pub trade_id: Ustr,
1157    /// Order ID.
1158    pub ord_id: Ustr,
1159    /// Client order ID.
1160    pub cl_ord_id: Ustr,
1161    /// Bill ID.
1162    pub bill_id: Ustr,
1163    /// Last filled price.
1164    pub fill_px: String,
1165    /// Last filled quantity.
1166    pub fill_sz: String,
1167    /// Trade side: buy or sell.
1168    pub side: OKXSide,
1169    /// Execution type.
1170    pub exec_type: OKXExecType,
1171    /// Fee currency.
1172    pub fee_ccy: String,
1173    /// Fee amount.
1174    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1175    pub fee: Option<String>,
1176    /// Timestamp, Unix timestamp format in milliseconds.
1177    #[serde(deserialize_with = "deserialize_string_to_u64")]
1178    pub ts: u64,
1179}
1180
1181/// Represents a single historical position record from `GET /api/v5/account/positions-history`.
1182#[derive(Clone, Debug, Serialize, Deserialize)]
1183#[serde(rename_all = "camelCase")]
1184pub struct OKXPositionHistory {
1185    /// Instrument type (e.g. "SWAP", "FUTURES", etc.).
1186    pub inst_type: OKXInstrumentType,
1187    /// Instrument ID (e.g. "BTC-USD-SWAP").
1188    pub inst_id: Ustr,
1189    /// Margin mode: e.g. "cross", "isolated".
1190    pub mgn_mode: OKXMarginMode,
1191    /// The type of the last close, e.g. "1" (close partially), "2" (close all), etc.
1192    /// See OKX docs for the meaning of each numeric code.
1193    #[serde(rename = "type")]
1194    pub r#type: Ustr,
1195    /// Creation time of the position (Unix timestamp in milliseconds).
1196    pub c_time: String,
1197    /// Last update time, Unix timestamp in milliseconds.
1198    #[serde(deserialize_with = "deserialize_string_to_u64")]
1199    pub u_time: u64,
1200    /// Average price of opening position.
1201    pub open_avg_px: String,
1202    /// Average price of closing position (if applicable).
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    pub close_avg_px: Option<String>,
1205    /// The position ID.
1206    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1207    pub pos_id: Option<Ustr>,
1208    /// Max quantity of the position at open time.
1209    #[serde(skip_serializing_if = "Option::is_none")]
1210    pub open_max_pos: Option<String>,
1211    /// Cumulative closed volume of the position.
1212    #[serde(skip_serializing_if = "Option::is_none")]
1213    pub close_total_pos: Option<String>,
1214    /// Realized profit and loss (only for FUTURES/SWAP/OPTION).
1215    #[serde(skip_serializing_if = "Option::is_none")]
1216    pub realized_pnl: Option<String>,
1217    /// Accumulated fee for the position.
1218    #[serde(skip_serializing_if = "Option::is_none")]
1219    pub fee: Option<String>,
1220    /// Accumulated funding fee (for perpetual swaps).
1221    #[serde(skip_serializing_if = "Option::is_none")]
1222    pub funding_fee: Option<String>,
1223    /// Accumulated liquidation penalty. Negative if there was a penalty.
1224    #[serde(skip_serializing_if = "Option::is_none")]
1225    pub liq_penalty: Option<String>,
1226    /// Profit and loss (realized or unrealized depending on status).
1227    #[serde(skip_serializing_if = "Option::is_none")]
1228    pub pnl: Option<String>,
1229    /// PnL ratio.
1230    #[serde(skip_serializing_if = "Option::is_none")]
1231    pub pnl_ratio: Option<String>,
1232    /// Position side: "long" / "short" / "net".
1233    pub pos_side: OKXPositionSide,
1234    /// Leverage used (the JSON field is "lev", but we rename it in Rust).
1235    pub lever: String,
1236    /// Direction: "long" or "short" (only for MARGIN/FUTURES/SWAP/OPTION).
1237    #[serde(skip_serializing_if = "Option::is_none")]
1238    pub direction: Option<String>,
1239    /// Trigger mark price. Populated if `type` indicates liquidation or ADL.
1240    #[serde(skip_serializing_if = "Option::is_none")]
1241    pub trigger_px: Option<String>,
1242    /// The underlying (e.g. "BTC-USD" for futures or swap).
1243    #[serde(skip_serializing_if = "Option::is_none")]
1244    pub uly: Option<String>,
1245    /// Currency (e.g. "BTC"). May or may not appear in all responses.
1246    #[serde(skip_serializing_if = "Option::is_none")]
1247    pub ccy: Option<String>,
1248}
1249
1250/// Represents the request body for `POST /api/v5/trade/order-algo` (place algo order).
1251#[derive(Clone, Debug, Serialize, Deserialize)]
1252#[serde(rename_all = "camelCase")]
1253pub struct OKXPlaceAlgoOrderRequest {
1254    /// Instrument ID.
1255    #[serde(rename = "instId")]
1256    pub inst_id: String,
1257    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
1258    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1259    pub inst_id_code: Option<u64>,
1260    /// Trade mode (isolated, cross, cash).
1261    #[serde(rename = "tdMode")]
1262    pub td_mode: OKXTradeMode,
1263    /// Order side (buy, sell).
1264    pub side: OKXSide,
1265    /// Algo order type (trigger, conditional, move_order_stop, etc.).
1266    #[serde(rename = "ordType")]
1267    pub ord_type: OKXAlgoOrderType,
1268    /// Order size. Omitted for `closeFraction` close orders.
1269    #[serde(skip_serializing_if = "Option::is_none")]
1270    pub sz: Option<String>,
1271    /// Client-supplied algo order ID.
1272    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1273    pub algo_cl_ord_id: Option<String>,
1274    /// Trigger price.
1275    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1276    pub trigger_px: Option<String>,
1277    /// Order price (for limit orders).
1278    #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1279    pub order_px: Option<String>,
1280    /// Trigger type (last, mark, index).
1281    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1282    pub trigger_px_type: Option<OKXTriggerType>,
1283    /// Stop-loss trigger price for conditional close orders.
1284    #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1285    pub sl_trigger_px: Option<String>,
1286    /// Stop-loss order price for conditional close orders.
1287    #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1288    pub sl_ord_px: Option<String>,
1289    /// Stop-loss trigger type (last, mark, index).
1290    #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1291    pub sl_trigger_px_type: Option<OKXTriggerType>,
1292    /// Take-profit trigger price for conditional close orders.
1293    #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1294    pub tp_trigger_px: Option<String>,
1295    /// Take-profit order price for conditional close orders.
1296    #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1297    pub tp_ord_px: Option<String>,
1298    /// Take-profit trigger type (last, mark, index).
1299    #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1300    pub tp_trigger_px_type: Option<OKXTriggerType>,
1301    /// Target currency (base_ccy or quote_ccy).
1302    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1303    pub tgt_ccy: Option<OKXTargetCurrency>,
1304    /// Position side (net, long, short).
1305    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1306    pub pos_side: Option<OKXPositionSide>,
1307    /// Whether to close position.
1308    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1309    pub close_position: Option<bool>,
1310    /// Order tag.
1311    #[serde(skip_serializing_if = "Option::is_none")]
1312    pub tag: Option<String>,
1313    /// Whether it's a reduce-only order.
1314    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1315    pub reduce_only: Option<bool>,
1316    /// Fraction of the position to close for eligible algo close orders.
1317    #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1318    pub close_fraction: Option<String>,
1319    /// Callback rate for trailing stop (e.g., "0.01" for 1%). Either this or
1320    /// `callback_spread` is required for `move_order_stop` orders.
1321    #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1322    pub callback_ratio: Option<String>,
1323    /// Callback spread for trailing stop (fixed price distance). Either this or
1324    /// `callback_ratio` is required for `move_order_stop` orders.
1325    #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1326    pub callback_spread: Option<String>,
1327    /// Activation price for trailing stop. If empty, the trailing stop
1328    /// activates immediately when placed.
1329    #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1330    pub active_px: Option<String>,
1331}
1332
1333/// Represents the response from `POST /api/v5/trade/order-algo` (place algo order).
1334#[derive(Clone, Debug, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct OKXPlaceAlgoOrderResponse {
1337    /// Algo order ID.
1338    pub algo_id: String,
1339    /// Client-supplied algo order ID.
1340    #[serde(skip_serializing_if = "Option::is_none")]
1341    pub algo_cl_ord_id: Option<String>,
1342    /// The result of the request.
1343    #[serde(skip_serializing_if = "Option::is_none")]
1344    pub s_code: Option<String>,
1345    /// Error message if the request failed.
1346    #[serde(skip_serializing_if = "Option::is_none")]
1347    pub s_msg: Option<String>,
1348    /// Request ID.
1349    #[serde(skip_serializing_if = "Option::is_none")]
1350    pub req_id: Option<String>,
1351}
1352
1353/// Represents the request body for `POST /api/v5/trade/cancel-algos` (cancel algo order).
1354#[derive(Clone, Debug, Serialize, Deserialize)]
1355#[serde(rename_all = "camelCase")]
1356pub struct OKXCancelAlgoOrderRequest {
1357    /// Instrument ID.
1358    pub inst_id: String,
1359    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
1360    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1361    pub inst_id_code: Option<u64>,
1362    /// Algo order ID.
1363    #[serde(skip_serializing_if = "Option::is_none")]
1364    pub algo_id: Option<String>,
1365    /// Client-supplied algo order ID.
1366    #[serde(skip_serializing_if = "Option::is_none")]
1367    pub algo_cl_ord_id: Option<String>,
1368}
1369
1370/// Represents the response from `POST /api/v5/trade/cancel-algos` (cancel algo order).
1371#[derive(Clone, Debug, Serialize, Deserialize)]
1372#[serde(rename_all = "camelCase")]
1373pub struct OKXCancelAlgoOrderResponse {
1374    /// Algo order ID.
1375    pub algo_id: String,
1376    /// The result of the request.
1377    #[serde(skip_serializing_if = "Option::is_none")]
1378    pub s_code: Option<String>,
1379    /// Error message if the request failed.
1380    #[serde(skip_serializing_if = "Option::is_none")]
1381    pub s_msg: Option<String>,
1382}
1383
1384/// Represents the request body for `POST /api/v5/trade/amend-algos` (amend algo order).
1385#[derive(Clone, Debug, Serialize, Deserialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct OKXAmendAlgoOrderRequest {
1388    /// Instrument ID.
1389    pub inst_id: String,
1390    /// Algo order ID.
1391    pub algo_id: String,
1392    /// Client-supplied algo order ID.
1393    #[serde(skip_serializing_if = "Option::is_none")]
1394    pub algo_cl_ord_id: Option<String>,
1395    /// New order size.
1396    #[serde(skip_serializing_if = "Option::is_none")]
1397    pub new_sz: Option<String>,
1398    /// New trigger price (for `trigger` algo orders).
1399    #[serde(skip_serializing_if = "Option::is_none")]
1400    pub new_trigger_px: Option<String>,
1401    /// New take-profit trigger price (for attached/OCO TP legs).
1402    #[serde(skip_serializing_if = "Option::is_none")]
1403    pub new_tp_trigger_px: Option<String>,
1404    /// New take-profit order price (`-1` for market).
1405    #[serde(skip_serializing_if = "Option::is_none")]
1406    pub new_tp_ord_px: Option<String>,
1407    /// New take-profit trigger price type (last, mark, index).
1408    #[serde(skip_serializing_if = "Option::is_none")]
1409    pub new_tp_trigger_px_type: Option<String>,
1410    /// New stop-loss trigger price (for `conditional` SL algo orders, incl.
1411    /// `closeFraction` close-position stops, which OKX amends via `newSlTriggerPx`
1412    /// rather than `newTriggerPx`).
1413    #[serde(skip_serializing_if = "Option::is_none")]
1414    pub new_sl_trigger_px: Option<String>,
1415    /// New stop-loss order price (`-1` for market).
1416    #[serde(skip_serializing_if = "Option::is_none")]
1417    pub new_sl_ord_px: Option<String>,
1418    /// New stop-loss trigger price type (last, mark, index).
1419    #[serde(skip_serializing_if = "Option::is_none")]
1420    pub new_sl_trigger_px_type: Option<String>,
1421    /// New order price (for limit orders after trigger).
1422    #[serde(skip_serializing_if = "Option::is_none")]
1423    pub new_order_px: Option<String>,
1424    /// New callback ratio for trailing stop (e.g., "0.01" for 1%).
1425    #[serde(skip_serializing_if = "Option::is_none")]
1426    pub new_callback_ratio: Option<String>,
1427    /// New callback spread for trailing stop (fixed price distance).
1428    #[serde(skip_serializing_if = "Option::is_none")]
1429    pub new_callback_spread: Option<String>,
1430    /// New activation price for trailing stop.
1431    #[serde(skip_serializing_if = "Option::is_none")]
1432    pub new_active_px: Option<String>,
1433}
1434
1435/// Represents the response from `POST /api/v5/trade/amend-algos` (amend algo order).
1436#[derive(Clone, Debug, Serialize, Deserialize)]
1437#[serde(rename_all = "camelCase")]
1438pub struct OKXAmendAlgoOrderResponse {
1439    /// Algo order ID.
1440    pub algo_id: String,
1441    /// Client-supplied algo order ID.
1442    #[serde(skip_serializing_if = "Option::is_none")]
1443    pub algo_cl_ord_id: Option<String>,
1444    /// The result of the request.
1445    #[serde(skip_serializing_if = "Option::is_none")]
1446    pub s_code: Option<String>,
1447    /// Error message if the request failed.
1448    #[serde(skip_serializing_if = "Option::is_none")]
1449    pub s_msg: Option<String>,
1450    /// Request ID.
1451    #[serde(skip_serializing_if = "Option::is_none")]
1452    pub req_id: Option<String>,
1453}
1454
1455/// Represents the response from `GET /api/v5/public/time` (get system time).
1456#[derive(Clone, Debug, Serialize, Deserialize)]
1457#[serde(rename_all = "camelCase")]
1458pub struct OKXServerTime {
1459    /// Server timestamp in milliseconds.
1460    #[serde(deserialize_with = "deserialize_string_to_u64")]
1461    pub ts: u64,
1462}
1463
1464/// Represents a fee rate entry from `GET /api/v5/account/trade-fee`.
1465#[derive(Clone, Debug, Serialize, Deserialize)]
1466#[serde(rename_all = "camelCase")]
1467pub struct OKXFeeRate {
1468    /// Fee level (VIP tier) - indicates the user's VIP tier (0-9).
1469    #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1470    pub level: OKXVipLevel,
1471    /// Taker fee rate for crypto-margined contracts.
1472    pub taker: String,
1473    /// Maker fee rate for crypto-margined contracts.
1474    pub maker: String,
1475    /// Taker fee rate for USDT-margined contracts.
1476    pub taker_u: String,
1477    /// Maker fee rate for USDT-margined contracts.
1478    pub maker_u: String,
1479    /// Delivery fee rate.
1480    #[serde(default)]
1481    pub delivery: String,
1482    /// Option exercise fee rate.
1483    #[serde(default)]
1484    pub exercise: String,
1485    /// Event contract settlement fee rate.
1486    #[serde(default)]
1487    pub settle: String,
1488    /// Instrument type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
1489    pub inst_type: OKXInstrumentType,
1490    /// Fee schedule category (being deprecated).
1491    #[serde(default)]
1492    pub category: String,
1493    /// Data return timestamp (Unix timestamp in milliseconds).
1494    #[serde(deserialize_with = "deserialize_string_to_u64")]
1495    pub ts: u64,
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500    use rstest::rstest;
1501    use serde_json;
1502
1503    use super::*;
1504
1505    #[rstest]
1506    fn test_algo_order_request_serialization() {
1507        let request = OKXPlaceAlgoOrderRequest {
1508            inst_id: "ETH-USDT-SWAP".to_string(),
1509            inst_id_code: None,
1510            td_mode: OKXTradeMode::Isolated,
1511            side: OKXSide::Buy,
1512            ord_type: OKXAlgoOrderType::Trigger,
1513            sz: Some("0.01".to_string()),
1514            algo_cl_ord_id: Some("test123".to_string()),
1515            trigger_px: Some("3000".to_string()),
1516            order_px: Some("-1".to_string()),
1517            trigger_px_type: Some(OKXTriggerType::Last),
1518            sl_trigger_px: None,
1519            sl_ord_px: None,
1520            sl_trigger_px_type: None,
1521            tp_trigger_px: None,
1522            tp_ord_px: None,
1523            tp_trigger_px_type: None,
1524            tgt_ccy: None,
1525            pos_side: None,
1526            close_position: None,
1527            tag: None,
1528            reduce_only: None,
1529            close_fraction: None,
1530            callback_ratio: None,
1531            callback_spread: None,
1532            active_px: None,
1533        };
1534
1535        let json = serde_json::to_string(&request).unwrap();
1536
1537        // Verify that fields are serialized with correct camelCase names
1538        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1539        assert!(json.contains("\"tdMode\":\"isolated\""));
1540        assert!(json.contains("\"ordType\":\"trigger\""));
1541        assert!(json.contains("\"algoClOrdId\":\"test123\""));
1542        assert!(json.contains("\"triggerPx\":\"3000\""));
1543        assert!(json.contains("\"orderPx\":\"-1\""));
1544        assert!(json.contains("\"triggerPxType\":\"last\""));
1545
1546        // Verify that None fields are not included
1547        assert!(!json.contains("tgtCcy"));
1548        assert!(!json.contains("posSide"));
1549        assert!(!json.contains("closePosition"));
1550        assert!(!json.contains("closeFraction"));
1551    }
1552
1553    #[rstest]
1554    fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1555        // A conditional stop-loss algo order (incl. closeFraction stops) is amended
1556        // via `newSlTriggerPx`, not `newTriggerPx`. Verify the camelCase field name
1557        // and that an unset `new_trigger_px` is omitted.
1558        let request = OKXAmendAlgoOrderRequest {
1559            inst_id: "ETH-USDT-SWAP".to_string(),
1560            algo_id: "123".to_string(),
1561            algo_cl_ord_id: None,
1562            new_sz: None,
1563            new_trigger_px: None,
1564            new_tp_trigger_px: None,
1565            new_tp_ord_px: None,
1566            new_tp_trigger_px_type: None,
1567            new_sl_trigger_px: Some("850".to_string()),
1568            new_sl_ord_px: None,
1569            new_sl_trigger_px_type: None,
1570            new_order_px: None,
1571            new_callback_ratio: None,
1572            new_callback_spread: None,
1573            new_active_px: None,
1574        };
1575
1576        let json = serde_json::to_string(&request).unwrap();
1577
1578        assert!(json.contains("\"algoId\":\"123\""));
1579        assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1580        // Unset optional fields must be omitted (OKX rejects an empty newTriggerPx).
1581        assert!(!json.contains("newTriggerPx"));
1582        assert!(!json.contains("newSz"));
1583        assert!(!json.contains("algoClOrdId"));
1584    }
1585
1586    #[rstest]
1587    fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1588        let request = OKXAmendAlgoOrderRequest {
1589            inst_id: "DOGE-USDT-SWAP".to_string(),
1590            algo_id: "algo-oco-1".to_string(),
1591            algo_cl_ord_id: None,
1592            new_sz: None,
1593            new_trigger_px: None,
1594            new_tp_trigger_px: Some("0.10495".to_string()),
1595            new_tp_ord_px: Some("-1".to_string()),
1596            new_tp_trigger_px_type: Some("last".to_string()),
1597            new_sl_trigger_px: Some("0.06297".to_string()),
1598            new_sl_ord_px: Some("-1".to_string()),
1599            new_sl_trigger_px_type: Some("last".to_string()),
1600            new_order_px: None,
1601            new_callback_ratio: None,
1602            new_callback_spread: None,
1603            new_active_px: None,
1604        };
1605
1606        let json = serde_json::to_string(&request).unwrap();
1607
1608        assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1609        assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1610        assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1611        assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1612        assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1613        assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1614        assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1615        assert!(!json.contains("newTriggerPx"));
1616    }
1617
1618    #[rstest]
1619    fn test_algo_order_request_serializes_close_fraction() {
1620        let request = OKXPlaceAlgoOrderRequest {
1621            inst_id: "ETH-USDT-SWAP".to_string(),
1622            inst_id_code: None,
1623            td_mode: OKXTradeMode::Cross,
1624            side: OKXSide::Sell,
1625            ord_type: OKXAlgoOrderType::Conditional,
1626            sz: None,
1627            algo_cl_ord_id: Some("close-frac-123".to_string()),
1628            trigger_px: None,
1629            order_px: None,
1630            trigger_px_type: None,
1631            sl_trigger_px: Some("3000".to_string()),
1632            sl_ord_px: Some("-1".to_string()),
1633            sl_trigger_px_type: Some(OKXTriggerType::Last),
1634            tp_trigger_px: None,
1635            tp_ord_px: None,
1636            tp_trigger_px_type: None,
1637            tgt_ccy: None,
1638            pos_side: Some(OKXPositionSide::Net),
1639            close_position: None,
1640            tag: None,
1641            reduce_only: Some(true),
1642            close_fraction: Some("1".to_string()),
1643            callback_ratio: None,
1644            callback_spread: None,
1645            active_px: None,
1646        };
1647
1648        let json = serde_json::to_string(&request).unwrap();
1649
1650        assert!(json.contains("\"ordType\":\"conditional\""));
1651        assert!(json.contains("\"closeFraction\":\"1\""));
1652        assert!(json.contains("\"slTriggerPx\":\"3000\""));
1653        assert!(json.contains("\"slOrdPx\":\"-1\""));
1654        assert!(json.contains("\"slTriggerPxType\":\"last\""));
1655        assert!(json.contains("\"reduceOnly\":true"));
1656        assert!(!json.contains("\"sz\""));
1657        assert!(!json.contains("triggerPx"));
1658    }
1659
1660    #[rstest]
1661    fn test_algo_order_request_array_serialization() {
1662        let request = OKXPlaceAlgoOrderRequest {
1663            inst_id: "BTC-USDT".to_string(),
1664            inst_id_code: Some(10459),
1665            td_mode: OKXTradeMode::Cross,
1666            side: OKXSide::Sell,
1667            ord_type: OKXAlgoOrderType::Trigger,
1668            sz: Some("0.1".to_string()),
1669            algo_cl_ord_id: None,
1670            trigger_px: Some("50000".to_string()),
1671            order_px: Some("49900".to_string()),
1672            trigger_px_type: Some(OKXTriggerType::Mark),
1673            sl_trigger_px: None,
1674            sl_ord_px: None,
1675            sl_trigger_px_type: None,
1676            tp_trigger_px: None,
1677            tp_ord_px: None,
1678            tp_trigger_px_type: None,
1679            tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1680            pos_side: Some(OKXPositionSide::Net),
1681            close_position: None,
1682            tag: None,
1683            reduce_only: Some(true),
1684            close_fraction: None,
1685            callback_ratio: None,
1686            callback_spread: None,
1687            active_px: None,
1688        };
1689
1690        // OKX expects an array of requests
1691        let json = serde_json::to_string(&[request]).unwrap();
1692
1693        // Verify array format
1694        assert!(json.starts_with('['));
1695        assert!(json.ends_with(']'));
1696
1697        // Verify correct field names
1698        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1699        assert!(json.contains("\"tdMode\":\"cross\""));
1700        assert!(json.contains("\"triggerPx\":\"50000\""));
1701        assert!(json.contains("\"orderPx\":\"49900\""));
1702        assert!(json.contains("\"triggerPxType\":\"mark\""));
1703        assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1704        assert!(json.contains("\"posSide\":\"net\""));
1705        assert!(json.contains("\"reduceOnly\":true"));
1706    }
1707
1708    #[rstest]
1709    fn test_cancel_algo_order_request_serialization() {
1710        let request = OKXCancelAlgoOrderRequest {
1711            inst_id: "ETH-USDT-SWAP".to_string(),
1712            inst_id_code: None,
1713            algo_id: Some("123456".to_string()),
1714            algo_cl_ord_id: None,
1715        };
1716
1717        let json = serde_json::to_string(&request).unwrap();
1718
1719        // Verify correct field names
1720        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1721        assert!(json.contains("\"algoId\":\"123456\""));
1722        assert!(!json.contains("algoClOrdId"));
1723    }
1724
1725    #[rstest]
1726    fn test_cancel_algo_order_with_client_id_serialization() {
1727        let request = OKXCancelAlgoOrderRequest {
1728            inst_id: "BTC-USDT".to_string(),
1729            inst_id_code: Some(10459),
1730            algo_id: None,
1731            algo_cl_ord_id: Some("client123".to_string()),
1732        };
1733
1734        // OKX expects an array of requests
1735        let json = serde_json::to_string(&[request]).unwrap();
1736
1737        // Verify array format and field names
1738        assert!(json.starts_with('['));
1739        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1740        assert!(json.contains("\"algoClOrdId\":\"client123\""));
1741        assert!(!json.contains("\"algoId\""));
1742    }
1743
1744    #[rstest]
1745    fn test_amend_algo_order_trigger_serialization() {
1746        let request = OKXAmendAlgoOrderRequest {
1747            inst_id: "ETH-USDT-SWAP".to_string(),
1748            algo_id: "123456".to_string(),
1749            algo_cl_ord_id: None,
1750            new_sz: None,
1751            new_trigger_px: Some("3500".to_string()),
1752            new_tp_trigger_px: None,
1753            new_tp_ord_px: None,
1754            new_tp_trigger_px_type: None,
1755            new_sl_trigger_px: None,
1756            new_sl_ord_px: None,
1757            new_sl_trigger_px_type: None,
1758            new_order_px: Some("3490".to_string()),
1759            new_callback_ratio: None,
1760            new_callback_spread: None,
1761            new_active_px: None,
1762        };
1763
1764        let json = serde_json::to_string(&request).unwrap();
1765
1766        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1767        assert!(json.contains("\"algoId\":\"123456\""));
1768        assert!(json.contains("\"newTriggerPx\":\"3500\""));
1769        assert!(json.contains("\"newOrderPx\":\"3490\""));
1770        assert!(!json.contains("newSz"));
1771        assert!(!json.contains("algoClOrdId"));
1772        assert!(!json.contains("newCallbackRatio"));
1773    }
1774
1775    #[rstest]
1776    fn test_amend_algo_order_trailing_stop_serialization() {
1777        let request = OKXAmendAlgoOrderRequest {
1778            inst_id: "BTC-USDT-SWAP".to_string(),
1779            algo_id: "789012".to_string(),
1780            algo_cl_ord_id: Some("client456".to_string()),
1781            new_sz: Some("0.1".to_string()),
1782            new_trigger_px: None,
1783            new_tp_trigger_px: None,
1784            new_tp_ord_px: None,
1785            new_tp_trigger_px_type: None,
1786            new_sl_trigger_px: None,
1787            new_sl_ord_px: None,
1788            new_sl_trigger_px_type: None,
1789            new_order_px: None,
1790            new_callback_ratio: Some("0.02".to_string()),
1791            new_callback_spread: None,
1792            new_active_px: Some("50000".to_string()),
1793        };
1794
1795        let json = serde_json::to_string(&request).unwrap();
1796
1797        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1798        assert!(json.contains("\"algoId\":\"789012\""));
1799        assert!(json.contains("\"algoClOrdId\":\"client456\""));
1800        assert!(json.contains("\"newSz\":\"0.1\""));
1801        assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1802        assert!(json.contains("\"newActivePx\":\"50000\""));
1803        assert!(!json.contains("newTriggerPx"));
1804        assert!(!json.contains("newOrderPx"));
1805    }
1806
1807    #[rstest]
1808    fn test_trailing_stop_request_callback_ratio_serialization() {
1809        let request = OKXPlaceAlgoOrderRequest {
1810            inst_id: "BTC-USDT-SWAP".to_string(),
1811            inst_id_code: None,
1812            td_mode: OKXTradeMode::Cross,
1813            side: OKXSide::Buy,
1814            ord_type: OKXAlgoOrderType::MoveOrderStop,
1815            sz: Some("0.1".to_string()),
1816            algo_cl_ord_id: Some("trail-001".to_string()),
1817            trigger_px: None,
1818            order_px: None,
1819            trigger_px_type: None,
1820            sl_trigger_px: None,
1821            sl_ord_px: None,
1822            sl_trigger_px_type: None,
1823            tp_trigger_px: None,
1824            tp_ord_px: None,
1825            tp_trigger_px_type: None,
1826            tgt_ccy: None,
1827            pos_side: None,
1828            close_position: None,
1829            tag: None,
1830            reduce_only: None,
1831            close_fraction: None,
1832            callback_ratio: Some("0.01".to_string()),
1833            callback_spread: None,
1834            active_px: None,
1835        };
1836
1837        let json = serde_json::to_string(&request).unwrap();
1838
1839        assert!(json.contains("\"ordType\":\"move_order_stop\""));
1840        assert!(json.contains("\"callbackRatio\":\"0.01\""));
1841        assert!(!json.contains("callbackSpread"));
1842        assert!(!json.contains("activePx"));
1843    }
1844
1845    #[rstest]
1846    fn test_trailing_stop_request_callback_spread_serialization() {
1847        let request = OKXPlaceAlgoOrderRequest {
1848            inst_id: "ETH-USDT-SWAP".to_string(),
1849            inst_id_code: None,
1850            td_mode: OKXTradeMode::Isolated,
1851            side: OKXSide::Sell,
1852            ord_type: OKXAlgoOrderType::MoveOrderStop,
1853            sz: Some("1.0".to_string()),
1854            algo_cl_ord_id: None,
1855            trigger_px: None,
1856            order_px: None,
1857            trigger_px_type: None,
1858            sl_trigger_px: None,
1859            sl_ord_px: None,
1860            sl_trigger_px_type: None,
1861            tp_trigger_px: None,
1862            tp_ord_px: None,
1863            tp_trigger_px_type: None,
1864            tgt_ccy: None,
1865            pos_side: None,
1866            close_position: None,
1867            tag: None,
1868            reduce_only: Some(true),
1869            close_fraction: None,
1870            callback_ratio: None,
1871            callback_spread: Some("50.5".to_string()),
1872            active_px: None,
1873        };
1874
1875        let json = serde_json::to_string(&request).unwrap();
1876
1877        assert!(json.contains("\"callbackSpread\":\"50.5\""));
1878        assert!(!json.contains("callbackRatio"));
1879        assert!(!json.contains("activePx"));
1880    }
1881
1882    #[rstest]
1883    fn test_trailing_stop_request_with_activation_price_serialization() {
1884        let request = OKXPlaceAlgoOrderRequest {
1885            inst_id: "BTC-USDT-SWAP".to_string(),
1886            inst_id_code: None,
1887            td_mode: OKXTradeMode::Cross,
1888            side: OKXSide::Buy,
1889            ord_type: OKXAlgoOrderType::MoveOrderStop,
1890            sz: Some("0.5".to_string()),
1891            algo_cl_ord_id: None,
1892            trigger_px: None,
1893            order_px: None,
1894            trigger_px_type: None,
1895            sl_trigger_px: None,
1896            sl_ord_px: None,
1897            sl_trigger_px_type: None,
1898            tp_trigger_px: None,
1899            tp_ord_px: None,
1900            tp_trigger_px_type: None,
1901            tgt_ccy: None,
1902            pos_side: None,
1903            close_position: None,
1904            tag: None,
1905            reduce_only: None,
1906            close_fraction: None,
1907            callback_ratio: Some("0.005".to_string()),
1908            callback_spread: None,
1909            active_px: Some("65000".to_string()),
1910        };
1911
1912        let json = serde_json::to_string(&request).unwrap();
1913
1914        assert!(json.contains("\"callbackRatio\":\"0.005\""));
1915        assert!(json.contains("\"activePx\":\"65000\""));
1916        assert!(!json.contains("callbackSpread"));
1917    }
1918
1919    #[rstest]
1920    fn test_amend_algo_order_callback_spread_serialization() {
1921        let request = OKXAmendAlgoOrderRequest {
1922            inst_id: "ETH-USDT-SWAP".to_string(),
1923            algo_id: "456789".to_string(),
1924            algo_cl_ord_id: None,
1925            new_sz: None,
1926            new_trigger_px: None,
1927            new_tp_trigger_px: None,
1928            new_tp_ord_px: None,
1929            new_tp_trigger_px_type: None,
1930            new_sl_trigger_px: None,
1931            new_sl_ord_px: None,
1932            new_sl_trigger_px_type: None,
1933            new_order_px: None,
1934            new_callback_ratio: None,
1935            new_callback_spread: Some("25.0".to_string()),
1936            new_active_px: Some("4000".to_string()),
1937        };
1938
1939        let json = serde_json::to_string(&request).unwrap();
1940
1941        assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
1942        assert!(json.contains("\"newActivePx\":\"4000\""));
1943        assert!(!json.contains("newCallbackRatio"));
1944        assert!(!json.contains("newTriggerPx"));
1945        assert!(!json.contains("newSz"));
1946    }
1947
1948    #[rstest]
1949    fn test_amend_algo_order_size_only_serialization() {
1950        let request = OKXAmendAlgoOrderRequest {
1951            inst_id: "BTC-USDT-SWAP".to_string(),
1952            algo_id: "111222".to_string(),
1953            algo_cl_ord_id: None,
1954            new_sz: Some("0.5".to_string()),
1955            new_trigger_px: None,
1956            new_tp_trigger_px: None,
1957            new_tp_ord_px: None,
1958            new_tp_trigger_px_type: None,
1959            new_sl_trigger_px: None,
1960            new_sl_ord_px: None,
1961            new_sl_trigger_px_type: None,
1962            new_order_px: None,
1963            new_callback_ratio: None,
1964            new_callback_spread: None,
1965            new_active_px: None,
1966        };
1967
1968        let json = serde_json::to_string(&request).unwrap();
1969
1970        assert!(json.contains("\"newSz\":\"0.5\""));
1971        assert!(!json.contains("newTriggerPx"));
1972        assert!(!json.contains("newOrderPx"));
1973        assert!(!json.contains("newCallbackRatio"));
1974        assert!(!json.contains("newCallbackSpread"));
1975        assert!(!json.contains("newActivePx"));
1976    }
1977
1978    #[rstest]
1979    fn test_amend_algo_order_all_fields_serialization() {
1980        let request = OKXAmendAlgoOrderRequest {
1981            inst_id: "BTC-USDT-SWAP".to_string(),
1982            algo_id: "333444".to_string(),
1983            algo_cl_ord_id: Some("client789".to_string()),
1984            new_sz: Some("1.0".to_string()),
1985            new_trigger_px: Some("60000".to_string()),
1986            new_tp_trigger_px: None,
1987            new_tp_ord_px: None,
1988            new_tp_trigger_px_type: None,
1989            new_sl_trigger_px: None,
1990            new_sl_ord_px: None,
1991            new_sl_trigger_px_type: None,
1992            new_order_px: Some("59900".to_string()),
1993            new_callback_ratio: Some("0.015".to_string()),
1994            new_callback_spread: Some("100".to_string()),
1995            new_active_px: Some("62000".to_string()),
1996        };
1997
1998        let json = serde_json::to_string(&request).unwrap();
1999
2000        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2001        assert!(json.contains("\"algoId\":\"333444\""));
2002        assert!(json.contains("\"algoClOrdId\":\"client789\""));
2003        assert!(json.contains("\"newSz\":\"1.0\""));
2004        assert!(json.contains("\"newTriggerPx\":\"60000\""));
2005        assert!(json.contains("\"newOrderPx\":\"59900\""));
2006        assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2007        assert!(json.contains("\"newCallbackSpread\":\"100\""));
2008        assert!(json.contains("\"newActivePx\":\"62000\""));
2009    }
2010
2011    #[rstest]
2012    fn test_place_order_request_serializes_px_usd() {
2013        let request = OKXPlaceOrderRequest {
2014            inst_id: "BTC-USD-250328-50000-C".to_string(),
2015            td_mode: OKXTradeMode::Cross,
2016            ccy: None,
2017            cl_ord_id: Some("test-opt-1".to_string()),
2018            tag: None,
2019            side: OKXSide::Buy,
2020            pos_side: Some(OKXPositionSide::Net),
2021            ord_type: OKXOrderType::Limit,
2022            sz: "1".to_string(),
2023            px: None,
2024            px_usd: Some("100.5".to_string()),
2025            px_vol: None,
2026            reduce_only: None,
2027            tgt_ccy: None,
2028            attach_algo_ords: None,
2029            speed_bump: None,
2030            outcome: None,
2031            slippage_pct: None,
2032        };
2033
2034        let json = serde_json::to_string(&request).unwrap();
2035        assert!(json.contains("\"pxUsd\":\"100.5\""));
2036        assert!(!json.contains("\"pxVol\""));
2037        assert!(!json.contains("\"px\":"));
2038        assert!(!json.contains("slippagePct"));
2039    }
2040
2041    #[rstest]
2042    fn test_place_order_request_serializes_px_vol() {
2043        let request = OKXPlaceOrderRequest {
2044            inst_id: "BTC-USD-250328-50000-C".to_string(),
2045            td_mode: OKXTradeMode::Cross,
2046            ccy: None,
2047            cl_ord_id: Some("test-opt-2".to_string()),
2048            tag: None,
2049            side: OKXSide::Buy,
2050            pos_side: Some(OKXPositionSide::Net),
2051            ord_type: OKXOrderType::Limit,
2052            sz: "1".to_string(),
2053            px: None,
2054            px_usd: None,
2055            px_vol: Some("0.55".to_string()),
2056            reduce_only: None,
2057            tgt_ccy: None,
2058            attach_algo_ords: None,
2059            speed_bump: None,
2060            outcome: None,
2061            slippage_pct: None,
2062        };
2063
2064        let json = serde_json::to_string(&request).unwrap();
2065        assert!(json.contains("\"pxVol\":\"0.55\""));
2066        assert!(!json.contains("\"pxUsd\""));
2067        assert!(!json.contains("\"px\":"));
2068    }
2069
2070    #[rstest]
2071    fn test_place_order_request_serializes_slippage_pct() {
2072        let request = OKXPlaceOrderRequest {
2073            inst_id: "BTC-USDT-SWAP".to_string(),
2074            td_mode: OKXTradeMode::Cross,
2075            ccy: None,
2076            cl_ord_id: Some("mkt-slip-1".to_string()),
2077            tag: None,
2078            side: OKXSide::Buy,
2079            pos_side: Some(OKXPositionSide::Net),
2080            ord_type: OKXOrderType::Market,
2081            sz: "1".to_string(),
2082            px: None,
2083            px_usd: None,
2084            px_vol: None,
2085            reduce_only: None,
2086            tgt_ccy: None,
2087            attach_algo_ords: None,
2088            speed_bump: None,
2089            outcome: None,
2090            slippage_pct: Some("0.005".to_string()),
2091        };
2092
2093        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2094        assert_eq!(json["slippagePct"], "0.005");
2095    }
2096
2097    #[rstest]
2098    fn test_event_contract_models_deserialize() {
2099        let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2100            "seriesId": "BTC-ABOVE-DAILY",
2101            "freq": "daily",
2102            "title": "BTC above daily",
2103            "category": "1",
2104            "settlement": {
2105                "method": "cash",
2106                "closeEarly": false,
2107                "srcName": "OKX BTC/USD Index",
2108                "underlying": "BTC-USD"
2109            }
2110        }))
2111        .unwrap();
2112        let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2113            "seriesId": "BTC-ABOVE-DAILY",
2114            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2115            "fixTime": "",
2116            "expTime": "1769697132335",
2117            "state": "live"
2118        }))
2119        .unwrap();
2120        let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2121            "seriesId": "BTC-ABOVE-DAILY",
2122            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2123            "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2124            "listTime": "1769697132335",
2125            "fixTime": "",
2126            "expTime": "1769697132335",
2127            "state": "live",
2128            "disputed": false,
2129            "outcome": "0",
2130            "floorStrike": "120000",
2131            "settleValue": ""
2132        }))
2133        .unwrap();
2134
2135        assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2136        assert_eq!(series.settlement.underlying, "BTC-USD");
2137        assert_eq!(event.fix_time, None);
2138        assert_eq!(event.exp_time, Some(1_769_697_132_335));
2139        assert_eq!(
2140            market.inst_id,
2141            Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2142        );
2143        assert_eq!(market.list_time, Some(1_769_697_132_335));
2144        assert_eq!(market.exp_time, Some(1_769_697_132_335));
2145        assert_eq!(market.outcome, "0");
2146    }
2147
2148    #[rstest]
2149    fn test_event_contract_models_accept_missing_optional_fields() {
2150        let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2151            "seriesId": "BTC-ABOVE-DAILY"
2152        }))
2153        .unwrap();
2154        let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2155            "seriesId": "BTC-ABOVE-DAILY",
2156            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2157            "fixTime": "",
2158            "expTime": "",
2159            "state": "live"
2160        }))
2161        .unwrap();
2162        let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2163            "seriesId": "BTC-ABOVE-DAILY",
2164            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2165            "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2166            "listTime": "",
2167            "fixTime": "",
2168            "expTime": "",
2169            "state": "live",
2170            "disputed": false,
2171            "outcome": "0",
2172            "floorStrike": "120000",
2173            "settleValue": ""
2174        }))
2175        .unwrap();
2176
2177        assert_eq!(series.freq, "");
2178        assert_eq!(series.settlement.underlying, "");
2179        assert_eq!(event.exp_time, None);
2180        assert_eq!(market.list_time, None);
2181        assert_eq!(market.exp_time, None);
2182    }
2183
2184    #[rstest]
2185    fn test_place_order_request_serializes_event_contract_fields() {
2186        let request = OKXPlaceOrderRequest {
2187            inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2188            td_mode: OKXTradeMode::Cash,
2189            ccy: None,
2190            cl_ord_id: Some("event-1".to_string()),
2191            tag: None,
2192            side: OKXSide::Buy,
2193            pos_side: None,
2194            ord_type: OKXOrderType::Limit,
2195            sz: "10".to_string(),
2196            px: Some("0.42".to_string()),
2197            px_usd: None,
2198            px_vol: None,
2199            reduce_only: None,
2200            tgt_ccy: None,
2201            attach_algo_ords: None,
2202            speed_bump: Some("1".to_string()),
2203            outcome: Some("yes".to_string()),
2204            slippage_pct: None,
2205        };
2206
2207        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2208
2209        assert_eq!(json["speedBump"], "1");
2210        assert_eq!(json["outcome"], "yes");
2211    }
2212
2213    #[rstest]
2214    fn test_attach_algo_ord_request_serializes_trailing_fields() {
2215        let request = OKXAttachAlgoOrdRequest {
2216            attach_algo_cl_ord_id: Some("trail-1".to_string()),
2217            sl_trigger_px: None,
2218            sl_ord_px: None,
2219            sl_trigger_px_type: None,
2220            tp_trigger_px: None,
2221            tp_ord_px: None,
2222            tp_trigger_px_type: None,
2223            callback_ratio: Some("0.01".to_string()),
2224            callback_spread: None,
2225            active_px: Some("64000".to_string()),
2226            new_callback_ratio: Some("0.02".to_string()),
2227            new_callback_spread: Some("25".to_string()),
2228            new_active_px: Some("65000".to_string()),
2229        };
2230
2231        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2232
2233        assert_eq!(json["callbackRatio"], "0.01");
2234        assert_eq!(json["activePx"], "64000");
2235        assert_eq!(json["newCallbackRatio"], "0.02");
2236        assert_eq!(json["newCallbackSpread"], "25");
2237        assert_eq!(json["newActivePx"], "65000");
2238        assert!(json.get("callbackSpread").is_none());
2239    }
2240
2241    #[rstest]
2242    fn test_place_order_response_deserializes_sub_code() {
2243        let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2244            "ordId": "",
2245            "clOrdId": "event-1",
2246            "sCode": "51000",
2247            "sMsg": "Parameter error",
2248            "subCode": "51004"
2249        }))
2250        .unwrap();
2251
2252        assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2253        assert_eq!(response.s_code, Some("51000".to_string()));
2254        assert_eq!(response.sub_code, Some("51004".to_string()));
2255    }
2256
2257    #[rstest]
2258    fn test_fee_rate_deserializes_settle() {
2259        let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2260            "level": "VIP1",
2261            "taker": "-0.0005",
2262            "maker": "-0.0002",
2263            "takerU": "-0.0005",
2264            "makerU": "-0.0002",
2265            "settle": "-0.001",
2266            "instType": "EVENTS",
2267            "category": "1",
2268            "ts": "1769697132335"
2269        }))
2270        .unwrap();
2271
2272        assert_eq!(fee_rate.settle, "-0.001");
2273        assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2274    }
2275}