nautilus_hyperliquid/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{fmt::Display, str::FromStr};
17
18use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus, OrderType, TriggerType};
19use serde::{Deserialize, Serialize};
20use strum::{AsRefStr, Display, EnumIter, EnumString};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum HyperliquidBarInterval {
24    #[serde(rename = "1m")]
25    OneMinute,
26    #[serde(rename = "3m")]
27    ThreeMinutes,
28    #[serde(rename = "5m")]
29    FiveMinutes,
30    #[serde(rename = "15m")]
31    FifteenMinutes,
32    #[serde(rename = "30m")]
33    ThirtyMinutes,
34    #[serde(rename = "1h")]
35    OneHour,
36    #[serde(rename = "2h")]
37    TwoHours,
38    #[serde(rename = "4h")]
39    FourHours,
40    #[serde(rename = "8h")]
41    EightHours,
42    #[serde(rename = "12h")]
43    TwelveHours,
44    #[serde(rename = "1d")]
45    OneDay,
46    #[serde(rename = "3d")]
47    ThreeDays,
48    #[serde(rename = "1w")]
49    OneWeek,
50    #[serde(rename = "1M")]
51    OneMonth,
52}
53
54impl HyperliquidBarInterval {
55    pub fn as_str(&self) -> &'static str {
56        match self {
57            Self::OneMinute => "1m",
58            Self::ThreeMinutes => "3m",
59            Self::FiveMinutes => "5m",
60            Self::FifteenMinutes => "15m",
61            Self::ThirtyMinutes => "30m",
62            Self::OneHour => "1h",
63            Self::TwoHours => "2h",
64            Self::FourHours => "4h",
65            Self::EightHours => "8h",
66            Self::TwelveHours => "12h",
67            Self::OneDay => "1d",
68            Self::ThreeDays => "3d",
69            Self::OneWeek => "1w",
70            Self::OneMonth => "1M",
71        }
72    }
73}
74
75impl FromStr for HyperliquidBarInterval {
76    type Err = anyhow::Error;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s {
80            "1m" => Ok(Self::OneMinute),
81            "3m" => Ok(Self::ThreeMinutes),
82            "5m" => Ok(Self::FiveMinutes),
83            "15m" => Ok(Self::FifteenMinutes),
84            "30m" => Ok(Self::ThirtyMinutes),
85            "1h" => Ok(Self::OneHour),
86            "2h" => Ok(Self::TwoHours),
87            "4h" => Ok(Self::FourHours),
88            "8h" => Ok(Self::EightHours),
89            "12h" => Ok(Self::TwelveHours),
90            "1d" => Ok(Self::OneDay),
91            "3d" => Ok(Self::ThreeDays),
92            "1w" => Ok(Self::OneWeek),
93            "1M" => Ok(Self::OneMonth),
94            _ => anyhow::bail!("Invalid Hyperliquid bar interval: {s}"),
95        }
96    }
97}
98
99impl Display for HyperliquidBarInterval {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}", self.as_str())
102    }
103}
104
105/// Represents the order side (Buy or Sell).
106#[derive(
107    Copy,
108    Clone,
109    Debug,
110    Display,
111    PartialEq,
112    Eq,
113    Hash,
114    AsRefStr,
115    EnumIter,
116    EnumString,
117    Serialize,
118    Deserialize,
119)]
120#[serde(rename_all = "UPPERCASE")]
121#[strum(serialize_all = "UPPERCASE")]
122pub enum HyperliquidSide {
123    #[serde(rename = "B")]
124    Buy,
125    #[serde(rename = "A")]
126    Sell,
127}
128
129impl From<OrderSide> for HyperliquidSide {
130    fn from(value: OrderSide) -> Self {
131        match value {
132            OrderSide::Buy => Self::Buy,
133            OrderSide::Sell => Self::Sell,
134            _ => panic!("Invalid `OrderSide`"),
135        }
136    }
137}
138
139impl From<HyperliquidSide> for OrderSide {
140    fn from(value: HyperliquidSide) -> Self {
141        match value {
142            HyperliquidSide::Buy => Self::Buy,
143            HyperliquidSide::Sell => Self::Sell,
144        }
145    }
146}
147
148impl From<HyperliquidSide> for AggressorSide {
149    fn from(value: HyperliquidSide) -> Self {
150        match value {
151            HyperliquidSide::Buy => Self::Buyer,
152            HyperliquidSide::Sell => Self::Seller,
153        }
154    }
155}
156
157/// Represents the time in force for limit orders.
158#[derive(
159    Copy,
160    Clone,
161    Debug,
162    Display,
163    PartialEq,
164    Eq,
165    Hash,
166    AsRefStr,
167    EnumIter,
168    EnumString,
169    Serialize,
170    Deserialize,
171)]
172#[serde(rename_all = "PascalCase")]
173#[strum(serialize_all = "PascalCase")]
174pub enum HyperliquidTimeInForce {
175    /// Add Liquidity Only - post-only order.
176    Alo,
177    /// Immediate or Cancel - fill immediately or cancel.
178    Ioc,
179    /// Good Till Cancel - remain on book until filled or cancelled.
180    Gtc,
181}
182
183/// Represents the order type configuration.
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185#[serde(tag = "type", rename_all = "lowercase")]
186pub enum HyperliquidOrderType {
187    /// Limit order with time-in-force.
188    #[serde(rename = "limit")]
189    Limit { tif: HyperliquidTimeInForce },
190
191    /// Trigger order (stop or take profit).
192    #[serde(rename = "trigger")]
193    Trigger {
194        #[serde(rename = "isMarket")]
195        is_market: bool,
196        #[serde(rename = "triggerPx")]
197        trigger_px: String,
198        tpsl: HyperliquidTpSl,
199    },
200}
201
202/// Represents the take profit / stop loss type.
203#[derive(
204    Copy,
205    Clone,
206    Debug,
207    Display,
208    PartialEq,
209    Eq,
210    Hash,
211    AsRefStr,
212    EnumIter,
213    EnumString,
214    Serialize,
215    Deserialize,
216)]
217#[cfg_attr(
218    feature = "python",
219    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
220)]
221#[serde(rename_all = "lowercase")]
222#[strum(serialize_all = "lowercase")]
223pub enum HyperliquidTpSl {
224    /// Take Profit.
225    Tp,
226    /// Stop Loss.
227    Sl,
228}
229
230/// Represents trigger price types for conditional orders.
231///
232/// Hyperliquid supports different price references for trigger evaluation:
233/// - Last: Last traded price (most common)
234/// - Mark: Mark price (for risk management)
235/// - Oracle: Oracle/index price (for some perpetuals)
236#[derive(
237    Copy,
238    Clone,
239    Debug,
240    Display,
241    PartialEq,
242    Eq,
243    Hash,
244    AsRefStr,
245    EnumIter,
246    EnumString,
247    Serialize,
248    Deserialize,
249)]
250#[cfg_attr(
251    feature = "python",
252    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
253)]
254#[serde(rename_all = "lowercase")]
255#[strum(serialize_all = "lowercase")]
256pub enum HyperliquidTriggerPriceType {
257    /// Last traded price.
258    Last,
259    /// Mark price.
260    Mark,
261    /// Oracle/index price.
262    Oracle,
263}
264
265impl From<HyperliquidTriggerPriceType> for TriggerType {
266    fn from(value: HyperliquidTriggerPriceType) -> Self {
267        match value {
268            HyperliquidTriggerPriceType::Last => Self::LastPrice,
269            HyperliquidTriggerPriceType::Mark => Self::MarkPrice,
270            HyperliquidTriggerPriceType::Oracle => Self::IndexPrice,
271        }
272    }
273}
274
275impl From<TriggerType> for HyperliquidTriggerPriceType {
276    fn from(value: TriggerType) -> Self {
277        match value {
278            TriggerType::LastPrice => Self::Last,
279            TriggerType::MarkPrice => Self::Mark,
280            TriggerType::IndexPrice => Self::Oracle,
281            _ => Self::Last, // Default fallback
282        }
283    }
284}
285
286/// Represents conditional/trigger order types.
287///
288/// Hyperliquid supports various conditional order types that trigger
289/// based on market conditions. These map to Nautilus OrderType variants.
290#[derive(
291    Copy,
292    Clone,
293    Debug,
294    Display,
295    PartialEq,
296    Eq,
297    Hash,
298    AsRefStr,
299    EnumIter,
300    EnumString,
301    Serialize,
302    Deserialize,
303)]
304#[cfg_attr(
305    feature = "python",
306    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
307)]
308#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
309#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
310pub enum HyperliquidConditionalOrderType {
311    /// Stop market order (protective stop with market execution).
312    StopMarket,
313    /// Stop limit order (protective stop with limit price).
314    StopLimit,
315    /// Take profit market order (profit-taking with market execution).
316    TakeProfitMarket,
317    /// Take profit limit order (profit-taking with limit price).
318    TakeProfitLimit,
319    /// Trailing stop market order (dynamic stop with market execution).
320    TrailingStopMarket,
321    /// Trailing stop limit order (dynamic stop with limit price).
322    TrailingStopLimit,
323}
324
325impl From<HyperliquidConditionalOrderType> for OrderType {
326    fn from(value: HyperliquidConditionalOrderType) -> Self {
327        match value {
328            HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
329            HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
330            HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
331            HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
332            HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
333            HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
334        }
335    }
336}
337
338impl From<OrderType> for HyperliquidConditionalOrderType {
339    fn from(value: OrderType) -> Self {
340        match value {
341            OrderType::StopMarket => Self::StopMarket,
342            OrderType::StopLimit => Self::StopLimit,
343            OrderType::MarketIfTouched => Self::TakeProfitMarket,
344            OrderType::LimitIfTouched => Self::TakeProfitLimit,
345            OrderType::TrailingStopMarket => Self::TrailingStopMarket,
346            OrderType::TrailingStopLimit => Self::TrailingStopLimit,
347            _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
348        }
349    }
350}
351
352/// Represents trailing offset types for trailing stop orders.
353///
354/// Trailing stops adjust dynamically based on market movement:
355/// - Price: Fixed price offset (e.g., $100)
356/// - Percentage: Percentage offset (e.g., 5%)
357/// - BasisPoints: Basis points offset (e.g., 250 bps = 2.5%)
358#[derive(
359    Copy,
360    Clone,
361    Debug,
362    Display,
363    PartialEq,
364    Eq,
365    Hash,
366    AsRefStr,
367    EnumIter,
368    EnumString,
369    Serialize,
370    Deserialize,
371)]
372#[cfg_attr(
373    feature = "python",
374    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
375)]
376#[serde(rename_all = "lowercase")]
377#[strum(serialize_all = "lowercase")]
378pub enum HyperliquidTrailingOffsetType {
379    /// Fixed price offset.
380    Price,
381    /// Percentage offset.
382    Percentage,
383    /// Basis points offset (1 bp = 0.01%).
384    #[serde(rename = "basispoints")]
385    #[strum(serialize = "basispoints")]
386    BasisPoints,
387}
388
389/// Represents the reduce only flag wrapper.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(transparent)]
392pub struct HyperliquidReduceOnly(pub bool);
393
394impl HyperliquidReduceOnly {
395    /// Creates a new reduce only flag.
396    pub fn new(reduce_only: bool) -> Self {
397        Self(reduce_only)
398    }
399
400    /// Returns whether this is a reduce only order.
401    pub fn is_reduce_only(&self) -> bool {
402        self.0
403    }
404}
405
406/// Represents the liquidity flag indicating maker or taker.
407#[derive(
408    Copy,
409    Clone,
410    Debug,
411    Display,
412    PartialEq,
413    Eq,
414    Hash,
415    AsRefStr,
416    EnumIter,
417    EnumString,
418    Serialize,
419    Deserialize,
420)]
421#[serde(rename_all = "lowercase")]
422#[strum(serialize_all = "lowercase")]
423pub enum HyperliquidLiquidityFlag {
424    Maker,
425    Taker,
426}
427
428impl From<bool> for HyperliquidLiquidityFlag {
429    /// Converts from `crossed` field in fill responses.
430    ///
431    /// `true` (crossed) -> Taker, `false` -> Maker
432    fn from(crossed: bool) -> Self {
433        if crossed { Self::Taker } else { Self::Maker }
434    }
435}
436
437/// Hyperliquid liquidation method.
438#[derive(
439    Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
440)]
441#[serde(rename_all = "lowercase")]
442#[strum(serialize_all = "lowercase")]
443pub enum HyperliquidLiquidationMethod {
444    Market,
445    Backstop,
446}
447
448/// Hyperliquid position type/mode.
449#[derive(
450    Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
451)]
452#[serde(rename_all = "camelCase")]
453#[strum(serialize_all = "camelCase")]
454pub enum HyperliquidPositionType {
455    OneWay,
456}
457
458/// Hyperliquid TWAP order status.
459#[derive(
460    Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
461)]
462#[serde(rename_all = "lowercase")]
463#[strum(serialize_all = "lowercase")]
464pub enum HyperliquidTwapStatus {
465    Activated,
466    Terminated,
467    Finished,
468    Error,
469}
470
471#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
472#[serde(untagged)]
473pub enum HyperliquidRejectCode {
474    /// Price must be divisible by tick size.
475    Tick,
476    /// Order must have minimum value of $10.
477    MinTradeNtl,
478    /// Order must have minimum value of 10 {quote_token}.
479    MinTradeSpotNtl,
480    /// Insufficient margin to place order.
481    PerpMargin,
482    /// Reduce only order would increase position.
483    ReduceOnly,
484    /// Post only order would have immediately matched.
485    BadAloPx,
486    /// Order could not immediately match.
487    IocCancel,
488    /// Invalid TP/SL price.
489    BadTriggerPx,
490    /// No liquidity available for market order.
491    MarketOrderNoLiquidity,
492    /// Position increase at open interest cap.
493    PositionIncreaseAtOpenInterestCap,
494    /// Position flip at open interest cap.
495    PositionFlipAtOpenInterestCap,
496    /// Too aggressive at open interest cap.
497    TooAggressiveAtOpenInterestCap,
498    /// Open interest increase.
499    OpenInterestIncrease,
500    /// Insufficient spot balance.
501    InsufficientSpotBalance,
502    /// Oracle issue.
503    Oracle,
504    /// Perp max position.
505    PerpMaxPosition,
506    /// Missing order.
507    MissingOrder,
508    /// Unknown reject reason with raw error message.
509    Unknown(String),
510}
511
512impl HyperliquidRejectCode {
513    /// Parse reject code from Hyperliquid API error message.
514    pub fn from_api_error(error_message: &str) -> Self {
515        Self::from_error_string_internal(error_message)
516    }
517
518    fn from_error_string_internal(error: &str) -> Self {
519        // Normalize: trim whitespace and convert to lowercase for robust matching
520        let normalized = error.trim().to_lowercase();
521
522        match normalized.as_str() {
523            // Tick size validation errors
524            s if s.contains("tick size") => Self::Tick,
525
526            // Minimum notional value errors (perp: $10, spot: 10 USDC)
527            s if s.contains("minimum value of $10") => Self::MinTradeNtl,
528            s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
529
530            // Margin errors
531            s if s.contains("insufficient margin") => Self::PerpMargin,
532
533            // Reduce-only order violations
534            s if s.contains("reduce only order would increase")
535                || s.contains("reduce-only order would increase") =>
536            {
537                Self::ReduceOnly
538            }
539
540            // Post-only order matching errors
541            s if s.contains("post only order would have immediately matched")
542                || s.contains("post-only order would have immediately matched") =>
543            {
544                Self::BadAloPx
545            }
546
547            // IOC (Immediate-or-Cancel) order errors
548            s if s.contains("could not immediately match") => Self::IocCancel,
549
550            // TP/SL trigger price errors
551            s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
552
553            // Market order liquidity errors
554            s if s.contains("no liquidity available for market order") => {
555                Self::MarketOrderNoLiquidity
556            }
557
558            // Open interest cap errors (various types)
559            // Note: These patterns are case-insensitive due to normalization
560            s if s.contains("positionincreaseatopeninterestcap") => {
561                Self::PositionIncreaseAtOpenInterestCap
562            }
563            s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
564            s if s.contains("tooaggressiveatopeninterestcap") => {
565                Self::TooAggressiveAtOpenInterestCap
566            }
567            s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
568
569            // Spot balance errors
570            s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
571
572            // Oracle errors
573            s if s.contains("oracle") => Self::Oracle,
574
575            // Position size limit errors
576            s if s.contains("max position") => Self::PerpMaxPosition,
577
578            // Missing order errors (cancel/modify non-existent order)
579            s if s.contains("missingorder") => Self::MissingOrder,
580
581            // Unknown error - log for monitoring and return with original message
582            _ => {
583                tracing::warn!(
584                    "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
585                    error // Use original error, not normalized
586                );
587                Self::Unknown(error.to_string())
588            }
589        }
590    }
591
592    /// Parses reject code from error string.
593    ///
594    /// **Deprecated**: This method uses substring matching which is fragile and not robust.
595    /// Use `from_api_error()` instead, which provides a migration path for structured error handling.
596    #[deprecated(
597        since = "0.50.0",
598        note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
599    )]
600    pub fn from_error_string(error: &str) -> Self {
601        Self::from_error_string_internal(error)
602    }
603}
604
605/// Represents Hyperliquid order status from API responses
606#[derive(
607    Copy,
608    Clone,
609    Debug,
610    Display,
611    PartialEq,
612    Eq,
613    Hash,
614    AsRefStr,
615    EnumIter,
616    EnumString,
617    Serialize,
618    Deserialize,
619)]
620#[serde(rename_all = "snake_case")]
621#[strum(serialize_all = "snake_case")]
622pub enum HyperliquidOrderStatus {
623    /// Order has been accepted and is open
624    Open,
625    /// Order has been accepted and is open (alternative representation)
626    Accepted,
627    /// Order has been partially filled
628    PartiallyFilled,
629    /// Order has been completely filled
630    Filled,
631    /// Order has been canceled
632    Canceled,
633    /// Order has been canceled (alternative spelling)
634    Cancelled,
635    /// Order was rejected by the exchange
636    Rejected,
637    /// Order has expired
638    Expired,
639}
640
641impl From<HyperliquidOrderStatus> for OrderStatus {
642    fn from(status: HyperliquidOrderStatus) -> Self {
643        match status {
644            HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
645            HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
646            HyperliquidOrderStatus::Filled => Self::Filled,
647            HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
648            HyperliquidOrderStatus::Rejected => Self::Rejected,
649            HyperliquidOrderStatus::Expired => Self::Expired,
650        }
651    }
652}
653
654pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
655    match status {
656        "open" | "accepted" => OrderStatus::Accepted,
657        "partially_filled" => OrderStatus::PartiallyFilled,
658        "filled" => OrderStatus::Filled,
659        "canceled" | "cancelled" => OrderStatus::Canceled,
660        "rejected" => OrderStatus::Rejected,
661        "expired" => OrderStatus::Expired,
662        _ => OrderStatus::Rejected,
663    }
664}
665
666/// Represents the direction of a fill (open/close position).
667///
668/// For perpetuals:
669/// - OpenLong: Opening a long position
670/// - OpenShort: Opening a short position
671/// - CloseLong: Closing an existing long position
672/// - CloseShort: Closing an existing short position
673///
674/// For spot:
675/// - Sell: Selling an asset
676#[derive(
677    Copy,
678    Clone,
679    Debug,
680    Display,
681    PartialEq,
682    Eq,
683    Hash,
684    AsRefStr,
685    EnumIter,
686    EnumString,
687    Serialize,
688    Deserialize,
689)]
690#[serde(rename_all = "PascalCase")]
691#[strum(serialize_all = "PascalCase")]
692pub enum HyperliquidFillDirection {
693    /// Opening a long position.
694    #[serde(rename = "Open Long")]
695    #[strum(serialize = "Open Long")]
696    OpenLong,
697    /// Opening a short position.
698    #[serde(rename = "Open Short")]
699    #[strum(serialize = "Open Short")]
700    OpenShort,
701    /// Closing an existing long position.
702    #[serde(rename = "Close Long")]
703    #[strum(serialize = "Close Long")]
704    CloseLong,
705    /// Closing an existing short position.
706    #[serde(rename = "Close Short")]
707    #[strum(serialize = "Close Short")]
708    CloseShort,
709    /// Selling an asset (spot only).
710    Sell,
711}
712
713/// Represents info request types for the Hyperliquid info endpoint.
714///
715/// These correspond to the "type" field in info endpoint requests.
716#[derive(
717    Copy,
718    Clone,
719    Debug,
720    Display,
721    PartialEq,
722    Eq,
723    Hash,
724    AsRefStr,
725    EnumIter,
726    EnumString,
727    Serialize,
728    Deserialize,
729)]
730#[serde(rename_all = "camelCase")]
731#[strum(serialize_all = "camelCase")]
732pub enum HyperliquidInfoRequestType {
733    /// Get metadata about available markets.
734    Meta,
735    /// Get spot metadata (tokens and pairs).
736    SpotMeta,
737    /// Get metadata with asset contexts (for price precision).
738    MetaAndAssetCtxs,
739    /// Get spot metadata with asset contexts.
740    SpotMetaAndAssetCtxs,
741    /// Get L2 order book for a coin.
742    L2Book,
743    /// Get user fills.
744    UserFills,
745    /// Get order status for a user.
746    OrderStatus,
747    /// Get all open orders for a user.
748    OpenOrders,
749    /// Get frontend open orders (includes more detail).
750    FrontendOpenOrders,
751    /// Get user state (balances, positions, margin).
752    ClearinghouseState,
753    /// Get candle/bar data.
754    CandleSnapshot,
755}
756
757impl HyperliquidInfoRequestType {
758    pub fn as_str(&self) -> &'static str {
759        match self {
760            Self::Meta => "meta",
761            Self::SpotMeta => "spotMeta",
762            Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
763            Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
764            Self::L2Book => "l2Book",
765            Self::UserFills => "userFills",
766            Self::OrderStatus => "orderStatus",
767            Self::OpenOrders => "openOrders",
768            Self::FrontendOpenOrders => "frontendOpenOrders",
769            Self::ClearinghouseState => "clearinghouseState",
770            Self::CandleSnapshot => "candleSnapshot",
771        }
772    }
773}
774
775/// Hyperliquid product type.
776#[derive(
777    Copy,
778    Clone,
779    Debug,
780    Display,
781    PartialEq,
782    Eq,
783    Hash,
784    AsRefStr,
785    EnumIter,
786    EnumString,
787    Serialize,
788    Deserialize,
789)]
790#[cfg_attr(
791    feature = "python",
792    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
793)]
794#[serde(rename_all = "UPPERCASE")]
795#[strum(serialize_all = "UPPERCASE")]
796pub enum HyperliquidProductType {
797    /// Perpetual futures.
798    Perp,
799    /// Spot markets.
800    Spot,
801}
802
803impl HyperliquidProductType {
804    /// Extract product type from an instrument symbol.
805    ///
806    /// # Errors
807    ///
808    /// Returns error if symbol doesn't match expected format.
809    pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
810        if symbol.ends_with("-PERP") {
811            Ok(Self::Perp)
812        } else if symbol.ends_with("-SPOT") {
813            Ok(Self::Spot)
814        } else {
815            anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
816        }
817    }
818}
819
820#[cfg(test)]
821mod tests {
822    use nautilus_model::enums::{OrderType, TriggerType};
823    use rstest::rstest;
824    use serde_json;
825
826    use super::*;
827
828    #[rstest]
829    fn test_side_serde() {
830        let buy_side = HyperliquidSide::Buy;
831        let sell_side = HyperliquidSide::Sell;
832
833        assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
834        assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
835
836        assert_eq!(
837            serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
838            HyperliquidSide::Buy
839        );
840        assert_eq!(
841            serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
842            HyperliquidSide::Sell
843        );
844    }
845
846    #[rstest]
847    fn test_side_from_order_side() {
848        // Test conversion from OrderSide to HyperliquidSide
849        assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
850        assert_eq!(
851            HyperliquidSide::from(OrderSide::Sell),
852            HyperliquidSide::Sell
853        );
854    }
855
856    #[rstest]
857    fn test_order_side_from_hyperliquid_side() {
858        // Test conversion from HyperliquidSide to OrderSide
859        assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
860        assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
861    }
862
863    #[rstest]
864    fn test_aggressor_side_from_hyperliquid_side() {
865        // Test conversion from HyperliquidSide to AggressorSide
866        assert_eq!(
867            AggressorSide::from(HyperliquidSide::Buy),
868            AggressorSide::Buyer
869        );
870        assert_eq!(
871            AggressorSide::from(HyperliquidSide::Sell),
872            AggressorSide::Seller
873        );
874    }
875
876    #[rstest]
877    fn test_time_in_force_serde() {
878        let test_cases = [
879            (HyperliquidTimeInForce::Alo, "\"Alo\""),
880            (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
881            (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
882        ];
883
884        for (tif, expected_json) in test_cases {
885            assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
886            assert_eq!(
887                serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
888                tif
889            );
890        }
891    }
892
893    #[rstest]
894    fn test_liquidity_flag_from_crossed() {
895        assert_eq!(
896            HyperliquidLiquidityFlag::from(true),
897            HyperliquidLiquidityFlag::Taker
898        );
899        assert_eq!(
900            HyperliquidLiquidityFlag::from(false),
901            HyperliquidLiquidityFlag::Maker
902        );
903    }
904
905    #[rstest]
906    #[allow(deprecated)]
907    fn test_reject_code_from_error_string() {
908        let test_cases = [
909            (
910                "Price must be divisible by tick size.",
911                HyperliquidRejectCode::Tick,
912            ),
913            (
914                "Order must have minimum value of $10.",
915                HyperliquidRejectCode::MinTradeNtl,
916            ),
917            (
918                "Insufficient margin to place order.",
919                HyperliquidRejectCode::PerpMargin,
920            ),
921            (
922                "Post only order would have immediately matched, bbo was 1.23",
923                HyperliquidRejectCode::BadAloPx,
924            ),
925            (
926                "Some unknown error",
927                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
928            ),
929        ];
930
931        for (error_str, expected_code) in test_cases {
932            assert_eq!(
933                HyperliquidRejectCode::from_error_string(error_str),
934                expected_code
935            );
936        }
937    }
938
939    #[rstest]
940    fn test_reject_code_from_api_error() {
941        let test_cases = [
942            (
943                "Price must be divisible by tick size.",
944                HyperliquidRejectCode::Tick,
945            ),
946            (
947                "Order must have minimum value of $10.",
948                HyperliquidRejectCode::MinTradeNtl,
949            ),
950            (
951                "Insufficient margin to place order.",
952                HyperliquidRejectCode::PerpMargin,
953            ),
954            (
955                "Post only order would have immediately matched, bbo was 1.23",
956                HyperliquidRejectCode::BadAloPx,
957            ),
958            (
959                "Some unknown error",
960                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
961            ),
962        ];
963
964        for (error_str, expected_code) in test_cases {
965            assert_eq!(
966                HyperliquidRejectCode::from_api_error(error_str),
967                expected_code
968            );
969        }
970    }
971
972    #[rstest]
973    fn test_reduce_only() {
974        let reduce_only = HyperliquidReduceOnly::new(true);
975
976        assert!(reduce_only.is_reduce_only());
977
978        let json = serde_json::to_string(&reduce_only).unwrap();
979        assert_eq!(json, "true");
980
981        let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
982        assert_eq!(parsed, reduce_only);
983    }
984
985    #[rstest]
986    fn test_order_status_conversion() {
987        // Test HyperliquidOrderStatus to OrderState conversion
988        assert_eq!(
989            OrderStatus::from(HyperliquidOrderStatus::Open),
990            OrderStatus::Accepted
991        );
992        assert_eq!(
993            OrderStatus::from(HyperliquidOrderStatus::Accepted),
994            OrderStatus::Accepted
995        );
996        assert_eq!(
997            OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
998            OrderStatus::PartiallyFilled
999        );
1000        assert_eq!(
1001            OrderStatus::from(HyperliquidOrderStatus::Filled),
1002            OrderStatus::Filled
1003        );
1004        assert_eq!(
1005            OrderStatus::from(HyperliquidOrderStatus::Canceled),
1006            OrderStatus::Canceled
1007        );
1008        assert_eq!(
1009            OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1010            OrderStatus::Canceled
1011        );
1012        assert_eq!(
1013            OrderStatus::from(HyperliquidOrderStatus::Rejected),
1014            OrderStatus::Rejected
1015        );
1016        assert_eq!(
1017            OrderStatus::from(HyperliquidOrderStatus::Expired),
1018            OrderStatus::Expired
1019        );
1020    }
1021
1022    #[rstest]
1023    fn test_order_status_string_mapping() {
1024        // Test direct string to OrderState conversion
1025        assert_eq!(
1026            hyperliquid_status_to_order_status("open"),
1027            OrderStatus::Accepted
1028        );
1029        assert_eq!(
1030            hyperliquid_status_to_order_status("accepted"),
1031            OrderStatus::Accepted
1032        );
1033        assert_eq!(
1034            hyperliquid_status_to_order_status("partially_filled"),
1035            OrderStatus::PartiallyFilled
1036        );
1037        assert_eq!(
1038            hyperliquid_status_to_order_status("filled"),
1039            OrderStatus::Filled
1040        );
1041        assert_eq!(
1042            hyperliquid_status_to_order_status("canceled"),
1043            OrderStatus::Canceled
1044        );
1045        assert_eq!(
1046            hyperliquid_status_to_order_status("cancelled"),
1047            OrderStatus::Canceled
1048        );
1049        assert_eq!(
1050            hyperliquid_status_to_order_status("rejected"),
1051            OrderStatus::Rejected
1052        );
1053        assert_eq!(
1054            hyperliquid_status_to_order_status("expired"),
1055            OrderStatus::Expired
1056        );
1057        assert_eq!(
1058            hyperliquid_status_to_order_status("unknown_status"),
1059            OrderStatus::Rejected
1060        );
1061    }
1062
1063    // ========================================================================
1064    // Conditional Order Tests
1065    // ========================================================================
1066
1067    #[rstest]
1068    fn test_hyperliquid_tpsl_serialization() {
1069        let tp = HyperliquidTpSl::Tp;
1070        let sl = HyperliquidTpSl::Sl;
1071
1072        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1073        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1074    }
1075
1076    #[rstest]
1077    fn test_hyperliquid_tpsl_deserialization() {
1078        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1079        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1080
1081        assert_eq!(tp, HyperliquidTpSl::Tp);
1082        assert_eq!(sl, HyperliquidTpSl::Sl);
1083    }
1084
1085    #[rstest]
1086    fn test_hyperliquid_trigger_price_type_serialization() {
1087        let last = HyperliquidTriggerPriceType::Last;
1088        let mark = HyperliquidTriggerPriceType::Mark;
1089        let oracle = HyperliquidTriggerPriceType::Oracle;
1090
1091        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1092        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1093        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1094    }
1095
1096    #[rstest]
1097    fn test_hyperliquid_trigger_price_type_to_nautilus() {
1098        assert_eq!(
1099            TriggerType::from(HyperliquidTriggerPriceType::Last),
1100            TriggerType::LastPrice
1101        );
1102        assert_eq!(
1103            TriggerType::from(HyperliquidTriggerPriceType::Mark),
1104            TriggerType::MarkPrice
1105        );
1106        assert_eq!(
1107            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1108            TriggerType::IndexPrice
1109        );
1110    }
1111
1112    #[rstest]
1113    fn test_nautilus_trigger_type_to_hyperliquid() {
1114        assert_eq!(
1115            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1116            HyperliquidTriggerPriceType::Last
1117        );
1118        assert_eq!(
1119            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1120            HyperliquidTriggerPriceType::Mark
1121        );
1122        assert_eq!(
1123            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1124            HyperliquidTriggerPriceType::Oracle
1125        );
1126    }
1127
1128    #[rstest]
1129    fn test_conditional_order_type_conversions() {
1130        // Test all conditional order types
1131        assert_eq!(
1132            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1133            OrderType::StopMarket
1134        );
1135        assert_eq!(
1136            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1137            OrderType::StopLimit
1138        );
1139        assert_eq!(
1140            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1141            OrderType::MarketIfTouched
1142        );
1143        assert_eq!(
1144            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1145            OrderType::LimitIfTouched
1146        );
1147        assert_eq!(
1148            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1149            OrderType::TrailingStopMarket
1150        );
1151    }
1152
1153    // Tests for error parsing with real and simulated error messages
1154    mod error_parsing_tests {
1155        use super::*;
1156
1157        #[rstest]
1158        fn test_parse_tick_size_error() {
1159            let error = "Price must be divisible by tick size 0.01";
1160            let code = HyperliquidRejectCode::from_api_error(error);
1161            assert_eq!(code, HyperliquidRejectCode::Tick);
1162        }
1163
1164        #[rstest]
1165        fn test_parse_tick_size_error_case_insensitive() {
1166            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1167            let code = HyperliquidRejectCode::from_api_error(error);
1168            assert_eq!(code, HyperliquidRejectCode::Tick);
1169        }
1170
1171        #[rstest]
1172        fn test_parse_min_notional_perp() {
1173            let error = "Order must have minimum value of $10";
1174            let code = HyperliquidRejectCode::from_api_error(error);
1175            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1176        }
1177
1178        #[rstest]
1179        fn test_parse_min_notional_spot() {
1180            let error = "Order must have minimum value of 10 USDC";
1181            let code = HyperliquidRejectCode::from_api_error(error);
1182            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1183        }
1184
1185        #[rstest]
1186        fn test_parse_insufficient_margin() {
1187            let error = "Insufficient margin to place order";
1188            let code = HyperliquidRejectCode::from_api_error(error);
1189            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1190        }
1191
1192        #[rstest]
1193        fn test_parse_insufficient_margin_case_variations() {
1194            let variations = vec![
1195                "insufficient margin to place order",
1196                "INSUFFICIENT MARGIN TO PLACE ORDER",
1197                "  Insufficient margin to place order  ", // with whitespace
1198            ];
1199
1200            for error in variations {
1201                let code = HyperliquidRejectCode::from_api_error(error);
1202                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1203            }
1204        }
1205
1206        #[rstest]
1207        fn test_parse_reduce_only_violation() {
1208            let error = "Reduce only order would increase position";
1209            let code = HyperliquidRejectCode::from_api_error(error);
1210            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1211        }
1212
1213        #[rstest]
1214        fn test_parse_reduce_only_with_hyphen() {
1215            let error = "Reduce-only order would increase position";
1216            let code = HyperliquidRejectCode::from_api_error(error);
1217            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1218        }
1219
1220        #[rstest]
1221        fn test_parse_post_only_match() {
1222            let error = "Post only order would have immediately matched";
1223            let code = HyperliquidRejectCode::from_api_error(error);
1224            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1225        }
1226
1227        #[rstest]
1228        fn test_parse_post_only_with_hyphen() {
1229            let error = "Post-only order would have immediately matched";
1230            let code = HyperliquidRejectCode::from_api_error(error);
1231            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1232        }
1233
1234        #[rstest]
1235        fn test_parse_ioc_no_match() {
1236            let error = "Order could not immediately match";
1237            let code = HyperliquidRejectCode::from_api_error(error);
1238            assert_eq!(code, HyperliquidRejectCode::IocCancel);
1239        }
1240
1241        #[rstest]
1242        fn test_parse_invalid_trigger_price() {
1243            let error = "Invalid TP/SL price";
1244            let code = HyperliquidRejectCode::from_api_error(error);
1245            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1246        }
1247
1248        #[rstest]
1249        fn test_parse_no_liquidity() {
1250            let error = "No liquidity available for market order";
1251            let code = HyperliquidRejectCode::from_api_error(error);
1252            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1253        }
1254
1255        #[rstest]
1256        fn test_parse_position_increase_at_oi_cap() {
1257            let error = "PositionIncreaseAtOpenInterestCap";
1258            let code = HyperliquidRejectCode::from_api_error(error);
1259            assert_eq!(
1260                code,
1261                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1262            );
1263        }
1264
1265        #[rstest]
1266        fn test_parse_position_flip_at_oi_cap() {
1267            let error = "PositionFlipAtOpenInterestCap";
1268            let code = HyperliquidRejectCode::from_api_error(error);
1269            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1270        }
1271
1272        #[rstest]
1273        fn test_parse_too_aggressive_at_oi_cap() {
1274            let error = "TooAggressiveAtOpenInterestCap";
1275            let code = HyperliquidRejectCode::from_api_error(error);
1276            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1277        }
1278
1279        #[rstest]
1280        fn test_parse_open_interest_increase() {
1281            let error = "OpenInterestIncrease";
1282            let code = HyperliquidRejectCode::from_api_error(error);
1283            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1284        }
1285
1286        #[rstest]
1287        fn test_parse_insufficient_spot_balance() {
1288            let error = "Insufficient spot balance";
1289            let code = HyperliquidRejectCode::from_api_error(error);
1290            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1291        }
1292
1293        #[rstest]
1294        fn test_parse_oracle_error() {
1295            let error = "Oracle price unavailable";
1296            let code = HyperliquidRejectCode::from_api_error(error);
1297            assert_eq!(code, HyperliquidRejectCode::Oracle);
1298        }
1299
1300        #[rstest]
1301        fn test_parse_max_position() {
1302            let error = "Exceeds max position size";
1303            let code = HyperliquidRejectCode::from_api_error(error);
1304            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1305        }
1306
1307        #[rstest]
1308        fn test_parse_missing_order() {
1309            let error = "MissingOrder";
1310            let code = HyperliquidRejectCode::from_api_error(error);
1311            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1312        }
1313
1314        #[rstest]
1315        fn test_parse_unknown_error() {
1316            let error = "This is a completely new error message";
1317            let code = HyperliquidRejectCode::from_api_error(error);
1318            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1319
1320            // Verify the original message is preserved
1321            if let HyperliquidRejectCode::Unknown(msg) = code {
1322                assert_eq!(msg, error);
1323            }
1324        }
1325
1326        #[rstest]
1327        fn test_parse_empty_error() {
1328            let error = "";
1329            let code = HyperliquidRejectCode::from_api_error(error);
1330            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1331        }
1332
1333        #[rstest]
1334        fn test_parse_whitespace_only() {
1335            let error = "   ";
1336            let code = HyperliquidRejectCode::from_api_error(error);
1337            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1338        }
1339
1340        #[rstest]
1341        fn test_normalization_preserves_original_in_unknown() {
1342            let error = "  UNKNOWN ERROR MESSAGE  ";
1343            let code = HyperliquidRejectCode::from_api_error(error);
1344
1345            // Should be Unknown, and should contain original message (not normalized)
1346            if let HyperliquidRejectCode::Unknown(msg) = code {
1347                assert_eq!(msg, error);
1348            } else {
1349                panic!("Expected Unknown variant");
1350            }
1351        }
1352    }
1353
1354    #[rstest]
1355    fn test_conditional_order_type_round_trip() {
1356        assert_eq!(
1357            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1358            OrderType::TrailingStopLimit
1359        );
1360
1361        // Test reverse conversions
1362        assert_eq!(
1363            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1364            HyperliquidConditionalOrderType::StopMarket
1365        );
1366        assert_eq!(
1367            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1368            HyperliquidConditionalOrderType::StopLimit
1369        );
1370    }
1371
1372    #[rstest]
1373    fn test_trailing_offset_type_serialization() {
1374        let price = HyperliquidTrailingOffsetType::Price;
1375        let percentage = HyperliquidTrailingOffsetType::Percentage;
1376        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1377
1378        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1379        assert_eq!(
1380            serde_json::to_string(&percentage).unwrap(),
1381            r#""percentage""#
1382        );
1383        assert_eq!(
1384            serde_json::to_string(&basis_points).unwrap(),
1385            r#""basispoints""#
1386        );
1387    }
1388
1389    #[rstest]
1390    fn test_conditional_order_type_serialization() {
1391        assert_eq!(
1392            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1393            r#""STOP_MARKET""#
1394        );
1395        assert_eq!(
1396            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1397            r#""STOP_LIMIT""#
1398        );
1399        assert_eq!(
1400            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1401            r#""TAKE_PROFIT_MARKET""#
1402        );
1403        assert_eq!(
1404            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1405            r#""TAKE_PROFIT_LIMIT""#
1406        );
1407        assert_eq!(
1408            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1409            r#""TRAILING_STOP_MARKET""#
1410        );
1411        assert_eq!(
1412            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1413            r#""TRAILING_STOP_LIMIT""#
1414        );
1415    }
1416
1417    #[rstest]
1418    fn test_order_type_enum_coverage() {
1419        // Ensure all conditional order types roundtrip correctly
1420        let conditional_types = vec![
1421            HyperliquidConditionalOrderType::StopMarket,
1422            HyperliquidConditionalOrderType::StopLimit,
1423            HyperliquidConditionalOrderType::TakeProfitMarket,
1424            HyperliquidConditionalOrderType::TakeProfitLimit,
1425            HyperliquidConditionalOrderType::TrailingStopMarket,
1426            HyperliquidConditionalOrderType::TrailingStopLimit,
1427        ];
1428
1429        for cond_type in conditional_types {
1430            let order_type = OrderType::from(cond_type);
1431            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1432            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1433        }
1434    }
1435
1436    #[rstest]
1437    fn test_all_trigger_price_types() {
1438        let trigger_types = vec![
1439            HyperliquidTriggerPriceType::Last,
1440            HyperliquidTriggerPriceType::Mark,
1441            HyperliquidTriggerPriceType::Oracle,
1442        ];
1443
1444        for trigger_type in trigger_types {
1445            let nautilus_type = TriggerType::from(trigger_type);
1446            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1447            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1448        }
1449    }
1450}