nautilus_hyperliquid/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
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    #[rstest]
1064    fn test_hyperliquid_tpsl_serialization() {
1065        let tp = HyperliquidTpSl::Tp;
1066        let sl = HyperliquidTpSl::Sl;
1067
1068        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1069        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1070    }
1071
1072    #[rstest]
1073    fn test_hyperliquid_tpsl_deserialization() {
1074        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1075        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1076
1077        assert_eq!(tp, HyperliquidTpSl::Tp);
1078        assert_eq!(sl, HyperliquidTpSl::Sl);
1079    }
1080
1081    #[rstest]
1082    fn test_hyperliquid_trigger_price_type_serialization() {
1083        let last = HyperliquidTriggerPriceType::Last;
1084        let mark = HyperliquidTriggerPriceType::Mark;
1085        let oracle = HyperliquidTriggerPriceType::Oracle;
1086
1087        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1088        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1089        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1090    }
1091
1092    #[rstest]
1093    fn test_hyperliquid_trigger_price_type_to_nautilus() {
1094        assert_eq!(
1095            TriggerType::from(HyperliquidTriggerPriceType::Last),
1096            TriggerType::LastPrice
1097        );
1098        assert_eq!(
1099            TriggerType::from(HyperliquidTriggerPriceType::Mark),
1100            TriggerType::MarkPrice
1101        );
1102        assert_eq!(
1103            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1104            TriggerType::IndexPrice
1105        );
1106    }
1107
1108    #[rstest]
1109    fn test_nautilus_trigger_type_to_hyperliquid() {
1110        assert_eq!(
1111            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1112            HyperliquidTriggerPriceType::Last
1113        );
1114        assert_eq!(
1115            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1116            HyperliquidTriggerPriceType::Mark
1117        );
1118        assert_eq!(
1119            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1120            HyperliquidTriggerPriceType::Oracle
1121        );
1122    }
1123
1124    #[rstest]
1125    fn test_conditional_order_type_conversions() {
1126        // Test all conditional order types
1127        assert_eq!(
1128            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1129            OrderType::StopMarket
1130        );
1131        assert_eq!(
1132            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1133            OrderType::StopLimit
1134        );
1135        assert_eq!(
1136            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1137            OrderType::MarketIfTouched
1138        );
1139        assert_eq!(
1140            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1141            OrderType::LimitIfTouched
1142        );
1143        assert_eq!(
1144            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1145            OrderType::TrailingStopMarket
1146        );
1147    }
1148
1149    // Tests for error parsing with real and simulated error messages
1150    mod error_parsing_tests {
1151        use super::*;
1152
1153        #[rstest]
1154        fn test_parse_tick_size_error() {
1155            let error = "Price must be divisible by tick size 0.01";
1156            let code = HyperliquidRejectCode::from_api_error(error);
1157            assert_eq!(code, HyperliquidRejectCode::Tick);
1158        }
1159
1160        #[rstest]
1161        fn test_parse_tick_size_error_case_insensitive() {
1162            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1163            let code = HyperliquidRejectCode::from_api_error(error);
1164            assert_eq!(code, HyperliquidRejectCode::Tick);
1165        }
1166
1167        #[rstest]
1168        fn test_parse_min_notional_perp() {
1169            let error = "Order must have minimum value of $10";
1170            let code = HyperliquidRejectCode::from_api_error(error);
1171            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1172        }
1173
1174        #[rstest]
1175        fn test_parse_min_notional_spot() {
1176            let error = "Order must have minimum value of 10 USDC";
1177            let code = HyperliquidRejectCode::from_api_error(error);
1178            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1179        }
1180
1181        #[rstest]
1182        fn test_parse_insufficient_margin() {
1183            let error = "Insufficient margin to place order";
1184            let code = HyperliquidRejectCode::from_api_error(error);
1185            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1186        }
1187
1188        #[rstest]
1189        fn test_parse_insufficient_margin_case_variations() {
1190            let variations = vec![
1191                "insufficient margin to place order",
1192                "INSUFFICIENT MARGIN TO PLACE ORDER",
1193                "  Insufficient margin to place order  ", // with whitespace
1194            ];
1195
1196            for error in variations {
1197                let code = HyperliquidRejectCode::from_api_error(error);
1198                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1199            }
1200        }
1201
1202        #[rstest]
1203        fn test_parse_reduce_only_violation() {
1204            let error = "Reduce only order would increase position";
1205            let code = HyperliquidRejectCode::from_api_error(error);
1206            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1207        }
1208
1209        #[rstest]
1210        fn test_parse_reduce_only_with_hyphen() {
1211            let error = "Reduce-only order would increase position";
1212            let code = HyperliquidRejectCode::from_api_error(error);
1213            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1214        }
1215
1216        #[rstest]
1217        fn test_parse_post_only_match() {
1218            let error = "Post only order would have immediately matched";
1219            let code = HyperliquidRejectCode::from_api_error(error);
1220            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1221        }
1222
1223        #[rstest]
1224        fn test_parse_post_only_with_hyphen() {
1225            let error = "Post-only order would have immediately matched";
1226            let code = HyperliquidRejectCode::from_api_error(error);
1227            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1228        }
1229
1230        #[rstest]
1231        fn test_parse_ioc_no_match() {
1232            let error = "Order could not immediately match";
1233            let code = HyperliquidRejectCode::from_api_error(error);
1234            assert_eq!(code, HyperliquidRejectCode::IocCancel);
1235        }
1236
1237        #[rstest]
1238        fn test_parse_invalid_trigger_price() {
1239            let error = "Invalid TP/SL price";
1240            let code = HyperliquidRejectCode::from_api_error(error);
1241            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1242        }
1243
1244        #[rstest]
1245        fn test_parse_no_liquidity() {
1246            let error = "No liquidity available for market order";
1247            let code = HyperliquidRejectCode::from_api_error(error);
1248            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1249        }
1250
1251        #[rstest]
1252        fn test_parse_position_increase_at_oi_cap() {
1253            let error = "PositionIncreaseAtOpenInterestCap";
1254            let code = HyperliquidRejectCode::from_api_error(error);
1255            assert_eq!(
1256                code,
1257                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1258            );
1259        }
1260
1261        #[rstest]
1262        fn test_parse_position_flip_at_oi_cap() {
1263            let error = "PositionFlipAtOpenInterestCap";
1264            let code = HyperliquidRejectCode::from_api_error(error);
1265            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1266        }
1267
1268        #[rstest]
1269        fn test_parse_too_aggressive_at_oi_cap() {
1270            let error = "TooAggressiveAtOpenInterestCap";
1271            let code = HyperliquidRejectCode::from_api_error(error);
1272            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1273        }
1274
1275        #[rstest]
1276        fn test_parse_open_interest_increase() {
1277            let error = "OpenInterestIncrease";
1278            let code = HyperliquidRejectCode::from_api_error(error);
1279            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1280        }
1281
1282        #[rstest]
1283        fn test_parse_insufficient_spot_balance() {
1284            let error = "Insufficient spot balance";
1285            let code = HyperliquidRejectCode::from_api_error(error);
1286            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1287        }
1288
1289        #[rstest]
1290        fn test_parse_oracle_error() {
1291            let error = "Oracle price unavailable";
1292            let code = HyperliquidRejectCode::from_api_error(error);
1293            assert_eq!(code, HyperliquidRejectCode::Oracle);
1294        }
1295
1296        #[rstest]
1297        fn test_parse_max_position() {
1298            let error = "Exceeds max position size";
1299            let code = HyperliquidRejectCode::from_api_error(error);
1300            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1301        }
1302
1303        #[rstest]
1304        fn test_parse_missing_order() {
1305            let error = "MissingOrder";
1306            let code = HyperliquidRejectCode::from_api_error(error);
1307            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1308        }
1309
1310        #[rstest]
1311        fn test_parse_unknown_error() {
1312            let error = "This is a completely new error message";
1313            let code = HyperliquidRejectCode::from_api_error(error);
1314            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1315
1316            // Verify the original message is preserved
1317            if let HyperliquidRejectCode::Unknown(msg) = code {
1318                assert_eq!(msg, error);
1319            }
1320        }
1321
1322        #[rstest]
1323        fn test_parse_empty_error() {
1324            let error = "";
1325            let code = HyperliquidRejectCode::from_api_error(error);
1326            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1327        }
1328
1329        #[rstest]
1330        fn test_parse_whitespace_only() {
1331            let error = "   ";
1332            let code = HyperliquidRejectCode::from_api_error(error);
1333            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1334        }
1335
1336        #[rstest]
1337        fn test_normalization_preserves_original_in_unknown() {
1338            let error = "  UNKNOWN ERROR MESSAGE  ";
1339            let code = HyperliquidRejectCode::from_api_error(error);
1340
1341            // Should be Unknown, and should contain original message (not normalized)
1342            if let HyperliquidRejectCode::Unknown(msg) = code {
1343                assert_eq!(msg, error);
1344            } else {
1345                panic!("Expected Unknown variant");
1346            }
1347        }
1348    }
1349
1350    #[rstest]
1351    fn test_conditional_order_type_round_trip() {
1352        assert_eq!(
1353            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1354            OrderType::TrailingStopLimit
1355        );
1356
1357        // Test reverse conversions
1358        assert_eq!(
1359            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1360            HyperliquidConditionalOrderType::StopMarket
1361        );
1362        assert_eq!(
1363            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1364            HyperliquidConditionalOrderType::StopLimit
1365        );
1366    }
1367
1368    #[rstest]
1369    fn test_trailing_offset_type_serialization() {
1370        let price = HyperliquidTrailingOffsetType::Price;
1371        let percentage = HyperliquidTrailingOffsetType::Percentage;
1372        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1373
1374        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1375        assert_eq!(
1376            serde_json::to_string(&percentage).unwrap(),
1377            r#""percentage""#
1378        );
1379        assert_eq!(
1380            serde_json::to_string(&basis_points).unwrap(),
1381            r#""basispoints""#
1382        );
1383    }
1384
1385    #[rstest]
1386    fn test_conditional_order_type_serialization() {
1387        assert_eq!(
1388            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1389            r#""STOP_MARKET""#
1390        );
1391        assert_eq!(
1392            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1393            r#""STOP_LIMIT""#
1394        );
1395        assert_eq!(
1396            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1397            r#""TAKE_PROFIT_MARKET""#
1398        );
1399        assert_eq!(
1400            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1401            r#""TAKE_PROFIT_LIMIT""#
1402        );
1403        assert_eq!(
1404            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1405            r#""TRAILING_STOP_MARKET""#
1406        );
1407        assert_eq!(
1408            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1409            r#""TRAILING_STOP_LIMIT""#
1410        );
1411    }
1412
1413    #[rstest]
1414    fn test_order_type_enum_coverage() {
1415        // Ensure all conditional order types roundtrip correctly
1416        let conditional_types = vec![
1417            HyperliquidConditionalOrderType::StopMarket,
1418            HyperliquidConditionalOrderType::StopLimit,
1419            HyperliquidConditionalOrderType::TakeProfitMarket,
1420            HyperliquidConditionalOrderType::TakeProfitLimit,
1421            HyperliquidConditionalOrderType::TrailingStopMarket,
1422            HyperliquidConditionalOrderType::TrailingStopLimit,
1423        ];
1424
1425        for cond_type in conditional_types {
1426            let order_type = OrderType::from(cond_type);
1427            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1428            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1429        }
1430    }
1431
1432    #[rstest]
1433    fn test_all_trigger_price_types() {
1434        let trigger_types = vec![
1435            HyperliquidTriggerPriceType::Last,
1436            HyperliquidTriggerPriceType::Mark,
1437            HyperliquidTriggerPriceType::Oracle,
1438        ];
1439
1440        for trigger_type in trigger_types {
1441            let nautilus_type = TriggerType::from(trigger_type);
1442            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1443            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1444        }
1445    }
1446}