Skip to main content

nautilus_okx/common/
enums.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//! Enumerations mapping OKX concepts onto idiomatic Nautilus variants.
17
18use nautilus_model::enums::{
19    AggressorSide, GreeksConvention, LiquiditySide, OptionKind, OrderSide, OrderSideSpecified,
20    OrderStatus, OrderType, PositionSide, TriggerType,
21};
22use serde::{Deserialize, Serialize};
23use strum::{AsRefStr, Display, EnumIter, EnumString};
24
25use crate::common::consts::{OKX_ADVANCE_ALGO_ORDER_TYPES, OKX_CONDITIONAL_ORDER_TYPES};
26
27/// Represents the type of book action.
28#[derive(
29    Copy,
30    Clone,
31    Debug,
32    Display,
33    PartialEq,
34    Eq,
35    Hash,
36    AsRefStr,
37    EnumIter,
38    EnumString,
39    Serialize,
40    Deserialize,
41)]
42#[serde(rename_all = "lowercase")]
43pub enum OKXBookAction {
44    /// Incremental update.
45    Update,
46    /// Full snapshot.
47    Snapshot,
48}
49
50/// Represents the possible states of an order throughout its lifecycle.
51#[derive(
52    Copy,
53    Clone,
54    Debug,
55    Display,
56    PartialEq,
57    Eq,
58    Hash,
59    AsRefStr,
60    EnumIter,
61    EnumString,
62    Serialize,
63    Deserialize,
64)]
65pub enum OKXCandleConfirm {
66    /// K-line is incomplete.
67    #[serde(rename = "0")]
68    Partial,
69    /// K-line is completed.
70    #[serde(rename = "1")]
71    Closed,
72}
73
74/// Represents the side of an order or trade (Buy/Sell).
75#[derive(
76    Copy,
77    Clone,
78    Debug,
79    Display,
80    PartialEq,
81    Eq,
82    Hash,
83    AsRefStr,
84    EnumIter,
85    EnumString,
86    Serialize,
87    Deserialize,
88)]
89#[serde(rename_all = "snake_case")]
90pub enum OKXSide {
91    /// Buy side of a trade or order.
92    Buy,
93    /// Sell side of a trade or order.
94    Sell,
95}
96
97impl From<OrderSideSpecified> for OKXSide {
98    fn from(value: OrderSideSpecified) -> Self {
99        match value {
100            OrderSideSpecified::Buy => Self::Buy,
101            OrderSideSpecified::Sell => Self::Sell,
102        }
103    }
104}
105
106impl From<OKXSide> for AggressorSide {
107    fn from(value: OKXSide) -> Self {
108        match value {
109            OKXSide::Buy => Self::Buyer,
110            OKXSide::Sell => Self::Seller,
111        }
112    }
113}
114
115/// Represents the available order types on OKX.
116#[derive(
117    Copy,
118    Clone,
119    Debug,
120    Display,
121    PartialEq,
122    Eq,
123    Hash,
124    AsRefStr,
125    EnumIter,
126    EnumString,
127    Serialize,
128    Deserialize,
129)]
130#[serde(rename_all = "snake_case")]
131pub enum OKXOrderType {
132    /// Market order, executed immediately at current market price.
133    Market,
134    /// Limit order, executed only at specified price or better.
135    Limit,
136    PostOnly,        // limit only, requires "px" to be provided
137    Fok,             // Market order if "px" is not provided, otherwise limit order
138    Ioc,             // Market order if "px" is not provided, otherwise limit order
139    OptimalLimitIoc, // Market order with immediate-or-cancel order
140    Mmp,             // Market Maker Protection (only applicable to Option in Portfolio Margin mode)
141    MmpAndPostOnly, // Market Maker Protection and Post-only order(only applicable to Option in Portfolio Margin mode)
142    OpFok,          // Fill-or-Kill for options (only applicable to Option)
143    Trigger,        // Conditional/algo order (stop orders, etc.)
144}
145
146/// Represents the possible states of an order throughout its lifecycle.
147#[derive(
148    Copy,
149    Clone,
150    Debug,
151    Display,
152    PartialEq,
153    Eq,
154    Hash,
155    AsRefStr,
156    EnumIter,
157    EnumString,
158    Serialize,
159    Deserialize,
160)]
161#[serde(rename_all = "snake_case")]
162#[cfg_attr(
163    feature = "python",
164    pyo3::pyclass(
165        eq,
166        eq_int,
167        module = "nautilus_trader.core.nautilus_pyo3.okx",
168        from_py_object,
169        rename_all = "SCREAMING_SNAKE_CASE",
170    )
171)]
172#[cfg_attr(
173    feature = "python",
174    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
175)]
176pub enum OKXOrderStatus {
177    Canceled,
178    Live,
179    PartiallyFilled,
180    Filled,
181    MmpCanceled,
182}
183
184impl TryFrom<OrderStatus> for OKXOrderStatus {
185    type Error = OrderStatus;
186
187    /// Converts a Nautilus [`OrderStatus`] into the matching [`OKXOrderStatus`].
188    ///
189    /// Returns the original [`OrderStatus`] in the error case for any variant
190    /// that has no representable OKX equivalent (e.g. `Submitted`, `PendingNew`,
191    /// `Triggered`, `PendingCancel`, `Expired`, `Rejected`).
192    fn try_from(value: OrderStatus) -> Result<Self, Self::Error> {
193        match value {
194            OrderStatus::Canceled => Ok(Self::Canceled),
195            OrderStatus::Accepted => Ok(Self::Live),
196            OrderStatus::PartiallyFilled => Ok(Self::PartiallyFilled),
197            OrderStatus::Filled => Ok(Self::Filled),
198            other => Err(other),
199        }
200    }
201}
202
203/// Represents the type of execution that generated a trade.
204#[derive(
205    Copy,
206    Clone,
207    Debug,
208    Default,
209    Display,
210    PartialEq,
211    Eq,
212    Hash,
213    AsRefStr,
214    EnumIter,
215    EnumString,
216    Serialize,
217    Deserialize,
218)]
219pub enum OKXExecType {
220    #[serde(rename = "")]
221    #[default]
222    None,
223    #[serde(rename = "T")]
224    Taker,
225    #[serde(rename = "M")]
226    Maker,
227}
228
229impl From<LiquiditySide> for OKXExecType {
230    fn from(value: LiquiditySide) -> Self {
231        match value {
232            LiquiditySide::NoLiquiditySide => Self::None,
233            LiquiditySide::Taker => Self::Taker,
234            LiquiditySide::Maker => Self::Maker,
235        }
236    }
237}
238
239/// Represents instrument types on OKX.
240#[derive(
241    Copy,
242    Clone,
243    Debug,
244    Display,
245    Default,
246    PartialEq,
247    Eq,
248    Hash,
249    AsRefStr,
250    EnumIter,
251    EnumString,
252    Serialize,
253    Deserialize,
254)]
255#[serde(rename_all = "UPPERCASE")]
256#[cfg_attr(
257    feature = "python",
258    pyo3::pyclass(
259        eq,
260        eq_int,
261        module = "nautilus_trader.core.nautilus_pyo3.okx",
262        from_py_object,
263        rename_all = "SCREAMING_SNAKE_CASE",
264    )
265)]
266#[cfg_attr(
267    feature = "python",
268    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
269)]
270pub enum OKXInstrumentType {
271    #[default]
272    /// Any product type.
273    Any,
274    /// Spot products.
275    Spot,
276    /// Margin products.
277    Margin,
278    /// Swap products.
279    Swap,
280    /// Futures products.
281    Futures,
282    /// Option products.
283    Option,
284    /// Event contract products.
285    Events,
286}
287
288/// Represents an OKX instrument category code (the `instCategory` field).
289///
290/// OKX also returns a deprecated `category` field that is effectively always
291/// `"1"`; `instCategory` is the meaningful value that drives asset-class
292/// mapping. Unknown or future codes fall back to
293/// [`OKXInstrumentCategory::Unknown`] rather than failing to parse.
294#[derive(
295    Copy,
296    Clone,
297    Debug,
298    Default,
299    Display,
300    PartialEq,
301    Eq,
302    Hash,
303    AsRefStr,
304    EnumIter,
305    EnumString,
306    Serialize,
307    Deserialize,
308)]
309pub enum OKXInstrumentCategory {
310    /// Cryptocurrency (`"1"`).
311    #[serde(rename = "1")]
312    #[strum(serialize = "1")]
313    Crypto,
314    /// Equity-linked (`"3"`).
315    #[serde(rename = "3")]
316    #[strum(serialize = "3")]
317    Equity,
318    /// Commodity-linked (`"4"`).
319    #[serde(rename = "4")]
320    #[strum(serialize = "4")]
321    Commodity,
322    /// FX-linked (`"5"`).
323    #[serde(rename = "5")]
324    #[strum(serialize = "5")]
325    Fx,
326    /// Debt-linked (`"6"`).
327    #[serde(rename = "6")]
328    #[strum(serialize = "6")]
329    Debt,
330    /// Unknown or future category code.
331    #[default]
332    #[serde(other)]
333    #[strum(serialize = "")]
334    Unknown,
335}
336
337/// Represents an instrument status on OKX.
338#[derive(
339    Copy,
340    Clone,
341    Debug,
342    Display,
343    PartialEq,
344    Eq,
345    Hash,
346    AsRefStr,
347    EnumIter,
348    EnumString,
349    Serialize,
350    Deserialize,
351)]
352#[serde(rename_all = "snake_case")]
353pub enum OKXInstrumentStatus {
354    Live,
355    Suspend,
356    Preopen,
357    Test,
358    PostOnly,
359    Rebase,
360    Settling,
361    /// Unknown or future status.
362    #[serde(other)]
363    Unknown,
364}
365
366/// Represents a spread type on OKX.
367#[derive(
368    Copy,
369    Clone,
370    Debug,
371    Display,
372    PartialEq,
373    Eq,
374    Hash,
375    AsRefStr,
376    EnumIter,
377    EnumString,
378    Serialize,
379    Deserialize,
380)]
381#[serde(rename_all = "snake_case")]
382pub enum OKXSpreadType {
383    Linear,
384    Inverse,
385    Hybrid,
386    /// Unknown or future spread type.
387    #[serde(other)]
388    Unknown,
389}
390
391/// Represents a spread status on OKX.
392#[derive(
393    Copy,
394    Clone,
395    Debug,
396    Display,
397    PartialEq,
398    Eq,
399    Hash,
400    AsRefStr,
401    EnumIter,
402    EnumString,
403    Serialize,
404    Deserialize,
405)]
406#[serde(rename_all = "snake_case")]
407pub enum OKXSpreadState {
408    Live,
409    Suspend,
410    Expired,
411    /// Unknown or future status.
412    #[serde(other)]
413    Unknown,
414}
415
416/// Represents an instrument contract type on OKX.
417#[derive(
418    Copy,
419    Clone,
420    Default,
421    Debug,
422    Display,
423    PartialEq,
424    Eq,
425    Hash,
426    AsRefStr,
427    EnumIter,
428    EnumString,
429    Serialize,
430    Deserialize,
431)]
432#[serde(rename_all = "snake_case")]
433#[cfg_attr(
434    feature = "python",
435    pyo3::pyclass(
436        eq,
437        eq_int,
438        module = "nautilus_trader.core.nautilus_pyo3.okx",
439        from_py_object,
440        rename_all = "SCREAMING_SNAKE_CASE",
441    )
442)]
443#[cfg_attr(
444    feature = "python",
445    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
446)]
447pub enum OKXContractType {
448    #[serde(rename = "")]
449    #[default]
450    None,
451    Linear,
452    Inverse,
453}
454
455/// Represents an option type on OKX.
456#[derive(
457    Copy,
458    Clone,
459    Debug,
460    Display,
461    PartialEq,
462    Eq,
463    Hash,
464    AsRefStr,
465    EnumIter,
466    EnumString,
467    Serialize,
468    Deserialize,
469)]
470pub enum OKXOptionType {
471    #[serde(rename = "")]
472    None,
473    #[serde(rename = "C")]
474    Call,
475    #[serde(rename = "P")]
476    Put,
477}
478
479impl TryFrom<OKXOptionType> for OptionKind {
480    type Error = OKXOptionType;
481
482    /// Converts an OKX option type into the matching Nautilus [`OptionKind`].
483    ///
484    /// Returns the source variant in the error case for [`OKXOptionType::None`]
485    /// (sent by OKX as an empty `optType` for non-option instruments and the
486    /// occasional malformed payload). Callers should skip such instruments
487    /// rather than treating the unknown variant as a default option kind.
488    fn try_from(option_type: OKXOptionType) -> Result<Self, Self::Error> {
489        match option_type {
490            OKXOptionType::Call => Ok(Self::Call),
491            OKXOptionType::Put => Ok(Self::Put),
492            other => Err(other),
493        }
494    }
495}
496
497/// Represents the convention used for option greeks on OKX.
498///
499/// OKX publishes two parallel greek sets on `opt-summary` and related endpoints:
500/// Black-Scholes greeks denominated in USD, and price-adjusted greeks denominated
501/// in the underlying/coin units.
502#[derive(
503    Copy,
504    Clone,
505    Debug,
506    Default,
507    Display,
508    PartialEq,
509    Eq,
510    Hash,
511    AsRefStr,
512    EnumIter,
513    EnumString,
514    Serialize,
515    Deserialize,
516)]
517#[serde(rename_all = "UPPERCASE")]
518#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
519#[cfg_attr(
520    feature = "python",
521    pyo3::pyclass(
522        eq,
523        eq_int,
524        module = "nautilus_trader.core.nautilus_pyo3.okx",
525        from_py_object,
526        rename_all = "SCREAMING_SNAKE_CASE",
527    )
528)]
529#[cfg_attr(
530    feature = "python",
531    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
532)]
533pub enum OKXGreeksType {
534    /// Black-Scholes greeks in USD.
535    #[default]
536    Bs = 0,
537    /// Price-adjusted greeks in the underlying/coin units.
538    Pa = 1,
539}
540
541impl From<u8> for OKXGreeksType {
542    fn from(value: u8) -> Self {
543        match value {
544            0 => Self::Bs,
545            1 => Self::Pa,
546            _ => {
547                log::warn!("Invalid OKXGreeksType {value}, defaulting to Bs");
548                Self::Bs
549            }
550        }
551    }
552}
553
554impl From<GreeksConvention> for OKXGreeksType {
555    fn from(convention: GreeksConvention) -> Self {
556        match convention {
557            GreeksConvention::BlackScholes => Self::Bs,
558            GreeksConvention::PriceAdjusted => Self::Pa,
559        }
560    }
561}
562
563impl From<OKXGreeksType> for GreeksConvention {
564    fn from(greeks_type: OKXGreeksType) -> Self {
565        match greeks_type {
566            OKXGreeksType::Bs => Self::BlackScholes,
567            OKXGreeksType::Pa => Self::PriceAdjusted,
568        }
569    }
570}
571
572/// Represents the trading mode for OKX orders.
573#[derive(
574    Copy,
575    Clone,
576    Debug,
577    Display,
578    Default,
579    PartialEq,
580    Eq,
581    Hash,
582    AsRefStr,
583    EnumIter,
584    EnumString,
585    Serialize,
586    Deserialize,
587)]
588#[serde(rename_all = "snake_case")]
589#[strum(ascii_case_insensitive)]
590#[cfg_attr(
591    feature = "python",
592    pyo3::pyclass(
593        eq,
594        eq_int,
595        module = "nautilus_trader.core.nautilus_pyo3.okx",
596        from_py_object,
597        rename_all = "SCREAMING_SNAKE_CASE",
598    )
599)]
600#[cfg_attr(
601    feature = "python",
602    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
603)]
604pub enum OKXTradeMode {
605    #[default]
606    Cash,
607    Isolated,
608    Cross,
609    #[strum(serialize = "spot_isolated")]
610    SpotIsolated,
611}
612
613/// Represents an OKX account mode.
614///
615/// # References
616///
617/// <https://www.okx.com/docs-v5/en/#overview-account-mode>
618#[derive(
619    Copy,
620    Clone,
621    Debug,
622    Display,
623    PartialEq,
624    Eq,
625    Hash,
626    AsRefStr,
627    EnumIter,
628    EnumString,
629    Serialize,
630    Deserialize,
631)]
632pub enum OKXAccountMode {
633    #[serde(rename = "Spot mode")]
634    Spot,
635    #[serde(rename = "Spot and futures mode")]
636    SpotAndFutures,
637    #[serde(rename = "Multi-currency margin mode")]
638    MultiCurrencyMarginMode,
639    #[serde(rename = "Portfolio margin mode")]
640    PortfolioMarginMode,
641}
642
643/// Represents the margin mode for OKX accounts.
644///
645/// # Reference
646///
647/// - <https://www.okx.com/en-au/help/iv-isolated-margin-mode>
648/// - <https://www.okx.com/en-au/help/iii-single-currency-margin-cross-margin-trading>
649/// - <https://www.okx.com/en-au/help/iv-multi-currency-margin-mode-cross-margin-trading>
650#[derive(
651    Copy,
652    Clone,
653    Default,
654    Debug,
655    Display,
656    PartialEq,
657    Eq,
658    Hash,
659    AsRefStr,
660    EnumIter,
661    EnumString,
662    Serialize,
663    Deserialize,
664)]
665#[serde(rename_all = "snake_case")]
666#[cfg_attr(
667    feature = "python",
668    pyo3::pyclass(
669        eq,
670        eq_int,
671        module = "nautilus_trader.core.nautilus_pyo3.okx",
672        from_py_object,
673        rename_all = "SCREAMING_SNAKE_CASE",
674    )
675)]
676#[cfg_attr(
677    feature = "python",
678    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
679)]
680pub enum OKXMarginMode {
681    #[serde(rename = "")]
682    #[default]
683    None,
684    Isolated,
685    Cross,
686}
687
688/// Represents the position mode for OKX accounts.
689///
690/// # References
691///
692/// <https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-position-mode>
693#[derive(
694    Copy,
695    Clone,
696    Default,
697    Debug,
698    Display,
699    PartialEq,
700    Eq,
701    Hash,
702    AsRefStr,
703    EnumIter,
704    EnumString,
705    Serialize,
706    Deserialize,
707)]
708#[cfg_attr(
709    feature = "python",
710    pyo3::pyclass(
711        eq,
712        eq_int,
713        module = "nautilus_trader.core.nautilus_pyo3.okx",
714        from_py_object,
715        rename_all = "SCREAMING_SNAKE_CASE",
716    )
717)]
718#[cfg_attr(
719    feature = "python",
720    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
721)]
722pub enum OKXPositionMode {
723    #[default]
724    #[serde(rename = "net_mode")]
725    NetMode,
726    #[serde(rename = "long_short_mode")]
727    LongShortMode,
728}
729
730#[derive(
731    Copy,
732    Clone,
733    Debug,
734    Display,
735    PartialEq,
736    Eq,
737    Hash,
738    AsRefStr,
739    EnumIter,
740    EnumString,
741    Serialize,
742    Deserialize,
743)]
744#[serde(rename_all = "snake_case")]
745pub enum OKXPositionSide {
746    #[serde(rename = "")]
747    None,
748    Net,
749    Long,
750    Short,
751}
752
753#[derive(
754    Copy,
755    Clone,
756    Debug,
757    Default,
758    Display,
759    PartialEq,
760    Eq,
761    Hash,
762    AsRefStr,
763    EnumIter,
764    EnumString,
765    Serialize,
766    Deserialize,
767)]
768#[serde(rename_all = "snake_case")]
769pub enum OKXSelfTradePreventionMode {
770    #[default]
771    #[serde(rename = "")]
772    None,
773    CancelMaker,
774    CancelTaker,
775    CancelBoth,
776}
777
778#[derive(
779    Copy,
780    Clone,
781    Debug,
782    Display,
783    PartialEq,
784    Eq,
785    Hash,
786    AsRefStr,
787    EnumIter,
788    EnumString,
789    Serialize,
790    Deserialize,
791)]
792#[serde(rename_all = "snake_case")]
793pub enum OKXTakeProfitKind {
794    #[serde(rename = "")]
795    None,
796    Condition,
797    Limit,
798}
799
800#[derive(
801    Copy,
802    Clone,
803    Debug,
804    Default,
805    Display,
806    PartialEq,
807    Eq,
808    Hash,
809    AsRefStr,
810    EnumIter,
811    EnumString,
812    Serialize,
813    Deserialize,
814)]
815#[serde(rename_all = "snake_case")]
816#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
817pub enum OKXTriggerType {
818    #[default]
819    #[serde(rename = "")]
820    None,
821    Last,
822    Index,
823    Mark,
824}
825
826impl From<TriggerType> for OKXTriggerType {
827    fn from(value: TriggerType) -> Self {
828        match value {
829            TriggerType::LastPrice => Self::Last,
830            TriggerType::MarkPrice => Self::Mark,
831            TriggerType::IndexPrice => Self::Index,
832            _ => Self::Last,
833        }
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use std::str::FromStr;
840
841    use nautilus_model::enums::{GreeksConvention, OptionKind, OrderStatus};
842    use rstest::rstest;
843
844    use super::{OKXGreeksType, OKXOptionType, OKXOrderStatus, OKXOrderType, OKXTriggerType};
845
846    #[rstest]
847    fn test_okx_trigger_type_from_str_accepts_snake_case_values() {
848        assert_eq!(
849            OKXTriggerType::from_str("last").unwrap(),
850            OKXTriggerType::Last
851        );
852        assert_eq!(
853            OKXTriggerType::from_str("mark").unwrap(),
854            OKXTriggerType::Mark
855        );
856        assert_eq!(
857            OKXTriggerType::from_str("index").unwrap(),
858            OKXTriggerType::Index
859        );
860    }
861
862    #[rstest]
863    #[case(OKXGreeksType::Bs, "\"BS\"")]
864    #[case(OKXGreeksType::Pa, "\"PA\"")]
865    fn test_greeks_type_serde_roundtrip(#[case] input: OKXGreeksType, #[case] expected: &str) {
866        let json = serde_json::to_string(&input).unwrap();
867        assert_eq!(json, expected);
868        let parsed: OKXGreeksType = serde_json::from_str(expected).unwrap();
869        assert_eq!(parsed, input);
870    }
871
872    #[rstest]
873    fn test_greeks_type_default_is_bs() {
874        assert_eq!(OKXGreeksType::default(), OKXGreeksType::Bs);
875    }
876
877    #[rstest]
878    fn test_greeks_type_from_u8() {
879        assert_eq!(OKXGreeksType::from(0_u8), OKXGreeksType::Bs);
880        assert_eq!(OKXGreeksType::from(1_u8), OKXGreeksType::Pa);
881        assert_eq!(OKXGreeksType::from(99_u8), OKXGreeksType::Bs);
882    }
883
884    #[rstest]
885    #[case(GreeksConvention::BlackScholes, OKXGreeksType::Bs)]
886    #[case(GreeksConvention::PriceAdjusted, OKXGreeksType::Pa)]
887    fn test_greeks_type_convention_roundtrip(
888        #[case] convention: GreeksConvention,
889        #[case] expected: OKXGreeksType,
890    ) {
891        let mapped: OKXGreeksType = convention.into();
892        assert_eq!(mapped, expected);
893        let back: GreeksConvention = mapped.into();
894        assert_eq!(back, convention);
895    }
896
897    #[rstest]
898    fn test_op_fok_serializes_to_snake_case() {
899        let json = serde_json::to_string(&OKXOrderType::OpFok).unwrap();
900        assert_eq!(json, "\"op_fok\"");
901    }
902
903    #[rstest]
904    fn test_op_fok_deserializes_from_snake_case() {
905        let parsed: OKXOrderType = serde_json::from_str("\"op_fok\"").unwrap();
906        assert_eq!(parsed, OKXOrderType::OpFok);
907    }
908
909    #[rstest]
910    fn test_op_fok_converts_to_limit_order_type() {
911        use nautilus_model::enums::OrderType;
912        let order_type: OrderType = OKXOrderType::OpFok.into();
913        assert_eq!(order_type, OrderType::Limit);
914    }
915
916    #[rstest]
917    #[case::call(OKXOptionType::Call, Ok(OptionKind::Call))]
918    #[case::put(OKXOptionType::Put, Ok(OptionKind::Put))]
919    #[case::none(OKXOptionType::None, Err(OKXOptionType::None))]
920    fn test_try_from_okx_option_type(
921        #[case] input: OKXOptionType,
922        #[case] expected: Result<OptionKind, OKXOptionType>,
923    ) {
924        let actual: Result<OptionKind, OKXOptionType> = input.try_into();
925        assert_eq!(actual, expected);
926    }
927
928    #[rstest]
929    #[case::canceled(OrderStatus::Canceled, Ok(OKXOrderStatus::Canceled))]
930    #[case::accepted(OrderStatus::Accepted, Ok(OKXOrderStatus::Live))]
931    #[case::partially_filled(OrderStatus::PartiallyFilled, Ok(OKXOrderStatus::PartiallyFilled))]
932    #[case::filled(OrderStatus::Filled, Ok(OKXOrderStatus::Filled))]
933    #[case::submitted(OrderStatus::Submitted, Err(OrderStatus::Submitted))]
934    #[case::pending_update(OrderStatus::PendingUpdate, Err(OrderStatus::PendingUpdate))]
935    #[case::pending_cancel(OrderStatus::PendingCancel, Err(OrderStatus::PendingCancel))]
936    #[case::triggered(OrderStatus::Triggered, Err(OrderStatus::Triggered))]
937    #[case::expired(OrderStatus::Expired, Err(OrderStatus::Expired))]
938    #[case::rejected(OrderStatus::Rejected, Err(OrderStatus::Rejected))]
939    fn test_try_from_order_status(
940        #[case] input: OrderStatus,
941        #[case] expected: Result<OKXOrderStatus, OrderStatus>,
942    ) {
943        let actual: Result<OKXOrderStatus, OrderStatus> = input.try_into();
944        assert_eq!(actual, expected);
945    }
946}
947
948/// Represents the target currency for order quantity.
949#[derive(
950    Copy,
951    Clone,
952    Debug,
953    Display,
954    PartialEq,
955    Eq,
956    Hash,
957    AsRefStr,
958    EnumIter,
959    EnumString,
960    Serialize,
961    Deserialize,
962)]
963#[serde(rename_all = "snake_case")]
964#[strum(serialize_all = "snake_case")]
965pub enum OKXTargetCurrency {
966    /// Base currency.
967    BaseCcy,
968    /// Quote currency.
969    QuoteCcy,
970}
971
972/// Represents an OKX order book channel.
973#[derive(Copy, Clone, Debug, PartialEq, Eq)]
974pub enum OKXBookChannel {
975    /// Standard depth-first book channel (`books`).
976    Book,
977    /// Low-latency 400-depth channel (`books-l2-tbt`).
978    BookL2Tbt,
979    /// Low-latency 50-depth channel (`books50-l2-tbt`).
980    Books50L2Tbt,
981}
982
983/// Represents OKX VIP level tiers for trading fee structure and API limits.
984///
985/// VIP levels determine:
986/// - Trading fee discounts.
987/// - API rate limits.
988/// - Access to advanced order book channels (L2/L3 depth).
989///
990/// Higher VIP levels (VIP4+) get access to:
991/// - "books50-l2-tbt" channel (50 depth, 10ms updates).
992/// - "bbo-tbt" channel (1 depth, 10ms updates).
993///
994/// VIP5+ get access to:
995/// - "books-l2-tbt" channel (400 depth, 10ms updates).
996#[derive(
997    Copy,
998    Clone,
999    Debug,
1000    Display,
1001    PartialEq,
1002    Eq,
1003    PartialOrd,
1004    Ord,
1005    Hash,
1006    AsRefStr,
1007    EnumIter,
1008    EnumString,
1009    Serialize,
1010    Deserialize,
1011)]
1012#[cfg_attr(
1013    feature = "python",
1014    pyo3::pyclass(
1015        module = "nautilus_trader.core.nautilus_pyo3.okx",
1016        from_py_object,
1017        rename_all = "SCREAMING_SNAKE_CASE",
1018    )
1019)]
1020#[cfg_attr(
1021    feature = "python",
1022    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
1023)]
1024pub enum OKXVipLevel {
1025    /// VIP level 0 (default tier).
1026    #[serde(rename = "0")]
1027    #[strum(serialize = "0")]
1028    Vip0 = 0,
1029    /// VIP level 1.
1030    #[serde(rename = "1")]
1031    #[strum(serialize = "1")]
1032    Vip1 = 1,
1033    /// VIP level 2.
1034    #[serde(rename = "2")]
1035    #[strum(serialize = "2")]
1036    Vip2 = 2,
1037    /// VIP level 3.
1038    #[serde(rename = "3")]
1039    #[strum(serialize = "3")]
1040    Vip3 = 3,
1041    /// VIP level 4 (can access books50-l2-tbt channel).
1042    #[serde(rename = "4")]
1043    #[strum(serialize = "4")]
1044    Vip4 = 4,
1045    /// VIP level 5 (can access books-l2-tbt channel).
1046    #[serde(rename = "5")]
1047    #[strum(serialize = "5")]
1048    Vip5 = 5,
1049    /// VIP level 6.
1050    #[serde(rename = "6")]
1051    #[strum(serialize = "6")]
1052    Vip6 = 6,
1053    /// VIP level 7.
1054    #[serde(rename = "7")]
1055    #[strum(serialize = "7")]
1056    Vip7 = 7,
1057    /// VIP level 8.
1058    #[serde(rename = "8")]
1059    #[strum(serialize = "8")]
1060    Vip8 = 8,
1061    /// VIP level 9 (highest tier).
1062    #[serde(rename = "9")]
1063    #[strum(serialize = "9")]
1064    Vip9 = 9,
1065}
1066
1067impl From<u8> for OKXVipLevel {
1068    fn from(value: u8) -> Self {
1069        match value {
1070            0 => Self::Vip0,
1071            1 => Self::Vip1,
1072            2 => Self::Vip2,
1073            3 => Self::Vip3,
1074            4 => Self::Vip4,
1075            5 => Self::Vip5,
1076            6 => Self::Vip6,
1077            7 => Self::Vip7,
1078            8 => Self::Vip8,
1079            9 => Self::Vip9,
1080            _ => {
1081                log::warn!("Invalid VIP level {value}, defaulting to Vip0");
1082                Self::Vip0
1083            }
1084        }
1085    }
1086}
1087
1088impl From<OKXSide> for OrderSide {
1089    fn from(side: OKXSide) -> Self {
1090        match side {
1091            OKXSide::Buy => Self::Buy,
1092            OKXSide::Sell => Self::Sell,
1093        }
1094    }
1095}
1096
1097impl From<OKXExecType> for LiquiditySide {
1098    fn from(exec: OKXExecType) -> Self {
1099        match exec {
1100            OKXExecType::Maker => Self::Maker,
1101            OKXExecType::Taker => Self::Taker,
1102            OKXExecType::None => Self::NoLiquiditySide,
1103        }
1104    }
1105}
1106
1107impl From<OKXPositionSide> for PositionSide {
1108    fn from(side: OKXPositionSide) -> Self {
1109        match side {
1110            OKXPositionSide::Long => Self::Long,
1111            OKXPositionSide::Short => Self::Short,
1112            _ => Self::Flat,
1113        }
1114    }
1115}
1116
1117impl From<OKXOrderStatus> for OrderStatus {
1118    fn from(status: OKXOrderStatus) -> Self {
1119        match status {
1120            OKXOrderStatus::Live => Self::Accepted,
1121            OKXOrderStatus::PartiallyFilled => Self::PartiallyFilled,
1122            OKXOrderStatus::Filled => Self::Filled,
1123            OKXOrderStatus::Canceled | OKXOrderStatus::MmpCanceled => Self::Canceled,
1124        }
1125    }
1126}
1127
1128impl From<OKXAlgoOrderStatus> for OrderStatus {
1129    fn from(status: OKXAlgoOrderStatus) -> Self {
1130        match status {
1131            OKXAlgoOrderStatus::Live | OKXAlgoOrderStatus::Pause => Self::Accepted,
1132            OKXAlgoOrderStatus::Effective
1133            | OKXAlgoOrderStatus::OrderPlaced
1134            | OKXAlgoOrderStatus::PartiallyEffective => Self::Triggered,
1135            OKXAlgoOrderStatus::Filled => Self::Filled,
1136            OKXAlgoOrderStatus::Canceled => Self::Canceled,
1137            OKXAlgoOrderStatus::OrderFailed | OKXAlgoOrderStatus::PartiallyFailed => Self::Rejected,
1138        }
1139    }
1140}
1141
1142impl From<OKXOrderType> for OrderType {
1143    fn from(ord_type: OKXOrderType) -> Self {
1144        match ord_type {
1145            OKXOrderType::Market => Self::Market,
1146            OKXOrderType::Limit
1147            | OKXOrderType::PostOnly
1148            | OKXOrderType::OptimalLimitIoc
1149            | OKXOrderType::Mmp
1150            | OKXOrderType::MmpAndPostOnly
1151            | OKXOrderType::Fok
1152            | OKXOrderType::OpFok
1153            | OKXOrderType::Ioc => Self::Limit,
1154            OKXOrderType::Trigger => Self::StopMarket,
1155        }
1156    }
1157}
1158
1159impl From<OrderType> for OKXOrderType {
1160    fn from(value: OrderType) -> Self {
1161        match value {
1162            OrderType::Market => Self::Market,
1163            OrderType::Limit => Self::Limit,
1164            OrderType::MarketToLimit => Self::Ioc,
1165            // Conditional orders will be handled separately via algo orders
1166            OrderType::StopMarket
1167            | OrderType::StopLimit
1168            | OrderType::MarketIfTouched
1169            | OrderType::LimitIfTouched
1170            | OrderType::TrailingStopMarket => {
1171                panic!("Conditional order types must use OKXAlgoOrderType")
1172            }
1173            _ => panic!("Invalid `OrderType` cannot be represented on OKX: {value:?}"),
1174        }
1175    }
1176}
1177
1178impl From<PositionSide> for OKXPositionSide {
1179    fn from(value: PositionSide) -> Self {
1180        match value {
1181            PositionSide::Long => Self::Long,
1182            PositionSide::Short => Self::Short,
1183            _ => Self::None,
1184        }
1185    }
1186}
1187
1188#[derive(
1189    Copy,
1190    Clone,
1191    Debug,
1192    Display,
1193    PartialEq,
1194    Eq,
1195    Hash,
1196    AsRefStr,
1197    EnumIter,
1198    EnumString,
1199    Serialize,
1200    Deserialize,
1201)]
1202#[serde(rename_all = "snake_case")]
1203pub enum OKXAlgoOrderType {
1204    Conditional,
1205    Oco,
1206    Trigger,
1207    MoveOrderStop,
1208    Iceberg,
1209    Twap,
1210}
1211
1212/// Helper to determine if an order type requires algo order handling.
1213pub fn is_conditional_order(order_type: OrderType) -> bool {
1214    OKX_CONDITIONAL_ORDER_TYPES.contains(&order_type)
1215}
1216
1217/// Helper to determine if an order type requires the advance algo cancel endpoint.
1218pub fn is_advance_algo_order(order_type: OrderType) -> bool {
1219    OKX_ADVANCE_ALGO_ORDER_TYPES.contains(&order_type)
1220}
1221
1222/// Converts Nautilus conditional order types to OKX algo order type.
1223///
1224/// # Errors
1225///
1226/// Returns an error if the provided `order_type` is not a conditional order type.
1227pub fn conditional_order_to_algo_type(order_type: OrderType) -> anyhow::Result<OKXAlgoOrderType> {
1228    match order_type {
1229        OrderType::StopMarket
1230        | OrderType::StopLimit
1231        | OrderType::MarketIfTouched
1232        | OrderType::LimitIfTouched => Ok(OKXAlgoOrderType::Trigger),
1233        OrderType::TrailingStopMarket => Ok(OKXAlgoOrderType::MoveOrderStop),
1234        _ => anyhow::bail!("Not a conditional order type: {order_type:?}"),
1235    }
1236}
1237
1238/// Represents the state of an algo (trigger/OCO/conditional) order on OKX.
1239#[derive(
1240    Copy,
1241    Clone,
1242    Debug,
1243    Display,
1244    PartialEq,
1245    Eq,
1246    Hash,
1247    AsRefStr,
1248    EnumIter,
1249    EnumString,
1250    Serialize,
1251    Deserialize,
1252)]
1253#[serde(rename_all = "snake_case")]
1254pub enum OKXAlgoOrderStatus {
1255    Live,
1256    Pause,
1257    Effective,
1258    OrderPlaced,
1259    PartiallyEffective,
1260    Canceled,
1261    Filled,
1262    OrderFailed,
1263    PartiallyFailed,
1264}
1265
1266/// Represents the category of an order on OKX.
1267///
1268/// The category field indicates whether an order is a normal trade, liquidation,
1269/// auto-deleveraging (ADL) event, or algorithmic order type. This is critical for
1270/// risk management and proper handling of exchange-generated orders.
1271///
1272/// # References
1273///
1274/// <https://www.okx.com/docs-v5/en/#order-book-trading-ws-order-channel>
1275#[derive(
1276    Copy,
1277    Clone,
1278    Debug,
1279    Display,
1280    PartialEq,
1281    Eq,
1282    Hash,
1283    AsRefStr,
1284    EnumIter,
1285    EnumString,
1286    Serialize,
1287    Deserialize,
1288)]
1289#[serde(rename_all = "snake_case")]
1290pub enum OKXOrderCategory {
1291    /// Normal trading order.
1292    Normal,
1293    /// Full liquidation order (position completely closed by exchange).
1294    FullLiquidation,
1295    /// Partial liquidation order (position partially closed by exchange).
1296    PartialLiquidation,
1297    /// Auto-deleveraging order (position closed to offset counterparty liquidation).
1298    Adl,
1299    /// Time-Weighted Average Price algorithmic order.
1300    Twap,
1301    /// Iceberg algorithmic order (hidden quantity).
1302    Iceberg,
1303    /// One-Cancels-the-Other algorithmic order.
1304    Oco,
1305    /// Conditional/trigger order.
1306    Conditional,
1307    /// Move order stop algorithmic order.
1308    MoveOrderStop,
1309    /// Delivery and exercise (for futures/options settlement).
1310    Ddh,
1311    /// Event contract settlement fill.
1312    Delivery,
1313    /// Unknown or future category (graceful fallback).
1314    #[serde(other)]
1315    Other,
1316}
1317
1318#[derive(
1319    Copy,
1320    Clone,
1321    Debug,
1322    Display,
1323    PartialEq,
1324    Eq,
1325    Hash,
1326    AsRefStr,
1327    EnumIter,
1328    EnumString,
1329    Serialize,
1330    Deserialize,
1331)]
1332pub enum OKXBarSize {
1333    #[serde(rename = "1s")]
1334    Second1,
1335    #[serde(rename = "1m")]
1336    Minute1,
1337    #[serde(rename = "3m")]
1338    Minute3,
1339    #[serde(rename = "5m")]
1340    Minute5,
1341    #[serde(rename = "15m")]
1342    Minute15,
1343    #[serde(rename = "30m")]
1344    Minute30,
1345    #[serde(rename = "1H")]
1346    Hour1,
1347    #[serde(rename = "2H")]
1348    Hour2,
1349    #[serde(rename = "4H")]
1350    Hour4,
1351    #[serde(rename = "6H")]
1352    Hour6,
1353    #[serde(rename = "12H")]
1354    Hour12,
1355    #[serde(rename = "1D")]
1356    Day1,
1357    #[serde(rename = "2D")]
1358    Day2,
1359    #[serde(rename = "3D")]
1360    Day3,
1361    #[serde(rename = "5D")]
1362    Day5,
1363    #[serde(rename = "1W")]
1364    Week1,
1365    #[serde(rename = "1M")]
1366    Month1,
1367    #[serde(rename = "3M")]
1368    Month3,
1369}
1370
1371/// Options price type for order pricing.
1372#[derive(
1373    Copy,
1374    Clone,
1375    Debug,
1376    Default,
1377    Display,
1378    PartialEq,
1379    Eq,
1380    Hash,
1381    AsRefStr,
1382    EnumIter,
1383    EnumString,
1384    Serialize,
1385    Deserialize,
1386)]
1387#[serde(rename_all = "snake_case")]
1388pub enum OKXPriceType {
1389    /// No price type specified.
1390    #[default]
1391    #[serde(rename = "")]
1392    None,
1393    /// Standard price.
1394    Px,
1395    /// Price in USD.
1396    Usd,
1397    /// Price in implied volatility.
1398    Vol,
1399}
1400
1401/// Funding rate settlement state.
1402#[derive(
1403    Copy,
1404    Clone,
1405    Debug,
1406    Default,
1407    Display,
1408    PartialEq,
1409    Eq,
1410    Hash,
1411    AsRefStr,
1412    EnumIter,
1413    EnumString,
1414    Serialize,
1415    Deserialize,
1416)]
1417#[serde(rename_all = "snake_case")]
1418pub enum OKXSettlementState {
1419    /// No settlement state.
1420    #[default]
1421    #[serde(rename = "")]
1422    None,
1423    /// Settlement in progress.
1424    Processing,
1425    /// Settlement completed.
1426    Settled,
1427}
1428
1429/// Quick margin type for order margin management.
1430#[derive(
1431    Copy,
1432    Clone,
1433    Debug,
1434    Default,
1435    Display,
1436    PartialEq,
1437    Eq,
1438    Hash,
1439    AsRefStr,
1440    EnumIter,
1441    EnumString,
1442    Serialize,
1443    Deserialize,
1444)]
1445#[serde(rename_all = "snake_case")]
1446pub enum OKXQuickMarginType {
1447    /// No quick margin type.
1448    #[default]
1449    #[serde(rename = "")]
1450    None,
1451    /// Manual margin management.
1452    Manual,
1453    /// Auto borrow margin.
1454    AutoBorrow,
1455    /// Auto repay margin.
1456    AutoRepay,
1457}
1458
1459/// OKX API environment.
1460#[derive(
1461    Copy,
1462    Clone,
1463    Debug,
1464    Default,
1465    Display,
1466    PartialEq,
1467    Eq,
1468    Hash,
1469    AsRefStr,
1470    EnumIter,
1471    EnumString,
1472    Serialize,
1473    Deserialize,
1474)]
1475#[serde(rename_all = "lowercase")]
1476#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1477#[cfg_attr(
1478    feature = "python",
1479    pyo3::pyclass(
1480        eq,
1481        eq_int,
1482        module = "nautilus_trader.core.nautilus_pyo3.okx",
1483        from_py_object,
1484        rename_all = "SCREAMING_SNAKE_CASE",
1485    )
1486)]
1487#[cfg_attr(
1488    feature = "python",
1489    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
1490)]
1491pub enum OKXEnvironment {
1492    /// Live trading environment.
1493    #[default]
1494    Live,
1495    /// Demo trading environment.
1496    Demo,
1497}
1498
1499/// OKX API region.
1500///
1501/// Selects the regional endpoint set. OKX serves region-specific hosts and an
1502/// API key registered in one region is rejected by another region's endpoints
1503/// (returning `API key doesn't exist`).
1504#[derive(
1505    Copy,
1506    Clone,
1507    Debug,
1508    Default,
1509    Display,
1510    PartialEq,
1511    Eq,
1512    Hash,
1513    AsRefStr,
1514    EnumIter,
1515    EnumString,
1516    Serialize,
1517    Deserialize,
1518)]
1519#[serde(rename_all = "lowercase")]
1520#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1521#[cfg_attr(
1522    feature = "python",
1523    pyo3::pyclass(
1524        eq,
1525        eq_int,
1526        module = "nautilus_trader.core.nautilus_pyo3.okx",
1527        from_py_object,
1528        rename_all = "SCREAMING_SNAKE_CASE",
1529    )
1530)]
1531#[cfg_attr(
1532    feature = "python",
1533    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.okx")
1534)]
1535pub enum OKXRegion {
1536    /// Global endpoints (accounts registered on www.okx.com).
1537    #[default]
1538    Global,
1539    /// European Economic Area endpoints (accounts registered on my.okx.com).
1540    Eea,
1541    /// United States and Australia endpoints (accounts registered on app.okx.com).
1542    Us,
1543}