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                log::warn!(
584                    "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" // Use original error, not normalized
585                );
586                Self::Unknown(error.to_string())
587            }
588        }
589    }
590
591    /// Parses reject code from error string.
592    ///
593    /// **Deprecated**: This method uses substring matching which is fragile and not robust.
594    /// Use `from_api_error()` instead, which provides a migration path for structured error handling.
595    #[deprecated(
596        since = "0.50.0",
597        note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
598    )]
599    pub fn from_error_string(error: &str) -> Self {
600        Self::from_error_string_internal(error)
601    }
602}
603
604/// Represents Hyperliquid order status from API responses
605#[derive(
606    Copy,
607    Clone,
608    Debug,
609    Display,
610    PartialEq,
611    Eq,
612    Hash,
613    AsRefStr,
614    EnumIter,
615    EnumString,
616    Serialize,
617    Deserialize,
618)]
619#[serde(rename_all = "snake_case")]
620#[strum(serialize_all = "snake_case")]
621pub enum HyperliquidOrderStatus {
622    /// Order has been accepted and is open
623    Open,
624    /// Order has been accepted and is open (alternative representation)
625    Accepted,
626    /// Order has been partially filled
627    PartiallyFilled,
628    /// Order has been completely filled
629    Filled,
630    /// Order has been canceled
631    Canceled,
632    /// Order has been canceled (alternative spelling)
633    Cancelled,
634    /// Order was rejected by the exchange
635    Rejected,
636    /// Order has expired
637    Expired,
638}
639
640impl From<HyperliquidOrderStatus> for OrderStatus {
641    fn from(status: HyperliquidOrderStatus) -> Self {
642        match status {
643            HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
644            HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
645            HyperliquidOrderStatus::Filled => Self::Filled,
646            HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
647            HyperliquidOrderStatus::Rejected => Self::Rejected,
648            HyperliquidOrderStatus::Expired => Self::Expired,
649        }
650    }
651}
652
653pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
654    match status {
655        "open" | "accepted" => OrderStatus::Accepted,
656        "partially_filled" => OrderStatus::PartiallyFilled,
657        "filled" => OrderStatus::Filled,
658        "canceled" | "cancelled" => OrderStatus::Canceled,
659        "rejected" => OrderStatus::Rejected,
660        "expired" => OrderStatus::Expired,
661        _ => OrderStatus::Rejected,
662    }
663}
664
665/// Represents the direction of a fill (open/close position).
666///
667/// For perpetuals:
668/// - OpenLong: Opening a long position
669/// - OpenShort: Opening a short position
670/// - CloseLong: Closing an existing long position
671/// - CloseShort: Closing an existing short position
672///
673/// For spot:
674/// - Sell: Selling an asset
675#[derive(
676    Copy,
677    Clone,
678    Debug,
679    Display,
680    PartialEq,
681    Eq,
682    Hash,
683    AsRefStr,
684    EnumIter,
685    EnumString,
686    Serialize,
687    Deserialize,
688)]
689#[serde(rename_all = "PascalCase")]
690#[strum(serialize_all = "PascalCase")]
691pub enum HyperliquidFillDirection {
692    /// Opening a long position.
693    #[serde(rename = "Open Long")]
694    #[strum(serialize = "Open Long")]
695    OpenLong,
696    /// Opening a short position.
697    #[serde(rename = "Open Short")]
698    #[strum(serialize = "Open Short")]
699    OpenShort,
700    /// Closing an existing long position.
701    #[serde(rename = "Close Long")]
702    #[strum(serialize = "Close Long")]
703    CloseLong,
704    /// Closing an existing short position.
705    #[serde(rename = "Close Short")]
706    #[strum(serialize = "Close Short")]
707    CloseShort,
708    /// Selling an asset (spot only).
709    Sell,
710}
711
712/// Represents info request types for the Hyperliquid info endpoint.
713///
714/// These correspond to the "type" field in info endpoint requests.
715#[derive(
716    Copy,
717    Clone,
718    Debug,
719    Display,
720    PartialEq,
721    Eq,
722    Hash,
723    AsRefStr,
724    EnumIter,
725    EnumString,
726    Serialize,
727    Deserialize,
728)]
729#[serde(rename_all = "camelCase")]
730#[strum(serialize_all = "camelCase")]
731pub enum HyperliquidInfoRequestType {
732    /// Get metadata about available markets.
733    Meta,
734    /// Get spot metadata (tokens and pairs).
735    SpotMeta,
736    /// Get metadata with asset contexts (for price precision).
737    MetaAndAssetCtxs,
738    /// Get spot metadata with asset contexts.
739    SpotMetaAndAssetCtxs,
740    /// Get L2 order book for a coin.
741    L2Book,
742    /// Get user fills.
743    UserFills,
744    /// Get order status for a user.
745    OrderStatus,
746    /// Get all open orders for a user.
747    OpenOrders,
748    /// Get frontend open orders (includes more detail).
749    FrontendOpenOrders,
750    /// Get user state (balances, positions, margin).
751    ClearinghouseState,
752    /// Get candle/bar data.
753    CandleSnapshot,
754}
755
756impl HyperliquidInfoRequestType {
757    pub fn as_str(&self) -> &'static str {
758        match self {
759            Self::Meta => "meta",
760            Self::SpotMeta => "spotMeta",
761            Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
762            Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
763            Self::L2Book => "l2Book",
764            Self::UserFills => "userFills",
765            Self::OrderStatus => "orderStatus",
766            Self::OpenOrders => "openOrders",
767            Self::FrontendOpenOrders => "frontendOpenOrders",
768            Self::ClearinghouseState => "clearinghouseState",
769            Self::CandleSnapshot => "candleSnapshot",
770        }
771    }
772}
773
774/// Hyperliquid product type.
775#[derive(
776    Copy,
777    Clone,
778    Debug,
779    Display,
780    PartialEq,
781    Eq,
782    Hash,
783    AsRefStr,
784    EnumIter,
785    EnumString,
786    Serialize,
787    Deserialize,
788)]
789#[cfg_attr(
790    feature = "python",
791    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
792)]
793#[serde(rename_all = "UPPERCASE")]
794#[strum(serialize_all = "UPPERCASE")]
795pub enum HyperliquidProductType {
796    /// Perpetual futures.
797    Perp,
798    /// Spot markets.
799    Spot,
800}
801
802impl HyperliquidProductType {
803    /// Extract product type from an instrument symbol.
804    ///
805    /// # Errors
806    ///
807    /// Returns error if symbol doesn't match expected format.
808    pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
809        if symbol.ends_with("-PERP") {
810            Ok(Self::Perp)
811        } else if symbol.ends_with("-SPOT") {
812            Ok(Self::Spot)
813        } else {
814            anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
815        }
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use nautilus_model::enums::{OrderType, TriggerType};
822    use rstest::rstest;
823    use serde_json;
824
825    use super::*;
826
827    #[rstest]
828    fn test_side_serde() {
829        let buy_side = HyperliquidSide::Buy;
830        let sell_side = HyperliquidSide::Sell;
831
832        assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
833        assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
834
835        assert_eq!(
836            serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
837            HyperliquidSide::Buy
838        );
839        assert_eq!(
840            serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
841            HyperliquidSide::Sell
842        );
843    }
844
845    #[rstest]
846    fn test_side_from_order_side() {
847        // Test conversion from OrderSide to HyperliquidSide
848        assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
849        assert_eq!(
850            HyperliquidSide::from(OrderSide::Sell),
851            HyperliquidSide::Sell
852        );
853    }
854
855    #[rstest]
856    fn test_order_side_from_hyperliquid_side() {
857        // Test conversion from HyperliquidSide to OrderSide
858        assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
859        assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
860    }
861
862    #[rstest]
863    fn test_aggressor_side_from_hyperliquid_side() {
864        // Test conversion from HyperliquidSide to AggressorSide
865        assert_eq!(
866            AggressorSide::from(HyperliquidSide::Buy),
867            AggressorSide::Buyer
868        );
869        assert_eq!(
870            AggressorSide::from(HyperliquidSide::Sell),
871            AggressorSide::Seller
872        );
873    }
874
875    #[rstest]
876    fn test_time_in_force_serde() {
877        let test_cases = [
878            (HyperliquidTimeInForce::Alo, "\"Alo\""),
879            (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
880            (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
881        ];
882
883        for (tif, expected_json) in test_cases {
884            assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
885            assert_eq!(
886                serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
887                tif
888            );
889        }
890    }
891
892    #[rstest]
893    fn test_liquidity_flag_from_crossed() {
894        assert_eq!(
895            HyperliquidLiquidityFlag::from(true),
896            HyperliquidLiquidityFlag::Taker
897        );
898        assert_eq!(
899            HyperliquidLiquidityFlag::from(false),
900            HyperliquidLiquidityFlag::Maker
901        );
902    }
903
904    #[rstest]
905    #[allow(deprecated)]
906    fn test_reject_code_from_error_string() {
907        let test_cases = [
908            (
909                "Price must be divisible by tick size.",
910                HyperliquidRejectCode::Tick,
911            ),
912            (
913                "Order must have minimum value of $10.",
914                HyperliquidRejectCode::MinTradeNtl,
915            ),
916            (
917                "Insufficient margin to place order.",
918                HyperliquidRejectCode::PerpMargin,
919            ),
920            (
921                "Post only order would have immediately matched, bbo was 1.23",
922                HyperliquidRejectCode::BadAloPx,
923            ),
924            (
925                "Some unknown error",
926                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
927            ),
928        ];
929
930        for (error_str, expected_code) in test_cases {
931            assert_eq!(
932                HyperliquidRejectCode::from_error_string(error_str),
933                expected_code
934            );
935        }
936    }
937
938    #[rstest]
939    fn test_reject_code_from_api_error() {
940        let test_cases = [
941            (
942                "Price must be divisible by tick size.",
943                HyperliquidRejectCode::Tick,
944            ),
945            (
946                "Order must have minimum value of $10.",
947                HyperliquidRejectCode::MinTradeNtl,
948            ),
949            (
950                "Insufficient margin to place order.",
951                HyperliquidRejectCode::PerpMargin,
952            ),
953            (
954                "Post only order would have immediately matched, bbo was 1.23",
955                HyperliquidRejectCode::BadAloPx,
956            ),
957            (
958                "Some unknown error",
959                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
960            ),
961        ];
962
963        for (error_str, expected_code) in test_cases {
964            assert_eq!(
965                HyperliquidRejectCode::from_api_error(error_str),
966                expected_code
967            );
968        }
969    }
970
971    #[rstest]
972    fn test_reduce_only() {
973        let reduce_only = HyperliquidReduceOnly::new(true);
974
975        assert!(reduce_only.is_reduce_only());
976
977        let json = serde_json::to_string(&reduce_only).unwrap();
978        assert_eq!(json, "true");
979
980        let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
981        assert_eq!(parsed, reduce_only);
982    }
983
984    #[rstest]
985    fn test_order_status_conversion() {
986        // Test HyperliquidOrderStatus to OrderState conversion
987        assert_eq!(
988            OrderStatus::from(HyperliquidOrderStatus::Open),
989            OrderStatus::Accepted
990        );
991        assert_eq!(
992            OrderStatus::from(HyperliquidOrderStatus::Accepted),
993            OrderStatus::Accepted
994        );
995        assert_eq!(
996            OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
997            OrderStatus::PartiallyFilled
998        );
999        assert_eq!(
1000            OrderStatus::from(HyperliquidOrderStatus::Filled),
1001            OrderStatus::Filled
1002        );
1003        assert_eq!(
1004            OrderStatus::from(HyperliquidOrderStatus::Canceled),
1005            OrderStatus::Canceled
1006        );
1007        assert_eq!(
1008            OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1009            OrderStatus::Canceled
1010        );
1011        assert_eq!(
1012            OrderStatus::from(HyperliquidOrderStatus::Rejected),
1013            OrderStatus::Rejected
1014        );
1015        assert_eq!(
1016            OrderStatus::from(HyperliquidOrderStatus::Expired),
1017            OrderStatus::Expired
1018        );
1019    }
1020
1021    #[rstest]
1022    fn test_order_status_string_mapping() {
1023        // Test direct string to OrderState conversion
1024        assert_eq!(
1025            hyperliquid_status_to_order_status("open"),
1026            OrderStatus::Accepted
1027        );
1028        assert_eq!(
1029            hyperliquid_status_to_order_status("accepted"),
1030            OrderStatus::Accepted
1031        );
1032        assert_eq!(
1033            hyperliquid_status_to_order_status("partially_filled"),
1034            OrderStatus::PartiallyFilled
1035        );
1036        assert_eq!(
1037            hyperliquid_status_to_order_status("filled"),
1038            OrderStatus::Filled
1039        );
1040        assert_eq!(
1041            hyperliquid_status_to_order_status("canceled"),
1042            OrderStatus::Canceled
1043        );
1044        assert_eq!(
1045            hyperliquid_status_to_order_status("cancelled"),
1046            OrderStatus::Canceled
1047        );
1048        assert_eq!(
1049            hyperliquid_status_to_order_status("rejected"),
1050            OrderStatus::Rejected
1051        );
1052        assert_eq!(
1053            hyperliquid_status_to_order_status("expired"),
1054            OrderStatus::Expired
1055        );
1056        assert_eq!(
1057            hyperliquid_status_to_order_status("unknown_status"),
1058            OrderStatus::Rejected
1059        );
1060    }
1061
1062    #[rstest]
1063    fn test_hyperliquid_tpsl_serialization() {
1064        let tp = HyperliquidTpSl::Tp;
1065        let sl = HyperliquidTpSl::Sl;
1066
1067        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1068        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1069    }
1070
1071    #[rstest]
1072    fn test_hyperliquid_tpsl_deserialization() {
1073        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1074        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1075
1076        assert_eq!(tp, HyperliquidTpSl::Tp);
1077        assert_eq!(sl, HyperliquidTpSl::Sl);
1078    }
1079
1080    #[rstest]
1081    fn test_hyperliquid_trigger_price_type_serialization() {
1082        let last = HyperliquidTriggerPriceType::Last;
1083        let mark = HyperliquidTriggerPriceType::Mark;
1084        let oracle = HyperliquidTriggerPriceType::Oracle;
1085
1086        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1087        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1088        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1089    }
1090
1091    #[rstest]
1092    fn test_hyperliquid_trigger_price_type_to_nautilus() {
1093        assert_eq!(
1094            TriggerType::from(HyperliquidTriggerPriceType::Last),
1095            TriggerType::LastPrice
1096        );
1097        assert_eq!(
1098            TriggerType::from(HyperliquidTriggerPriceType::Mark),
1099            TriggerType::MarkPrice
1100        );
1101        assert_eq!(
1102            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1103            TriggerType::IndexPrice
1104        );
1105    }
1106
1107    #[rstest]
1108    fn test_nautilus_trigger_type_to_hyperliquid() {
1109        assert_eq!(
1110            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1111            HyperliquidTriggerPriceType::Last
1112        );
1113        assert_eq!(
1114            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1115            HyperliquidTriggerPriceType::Mark
1116        );
1117        assert_eq!(
1118            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1119            HyperliquidTriggerPriceType::Oracle
1120        );
1121    }
1122
1123    #[rstest]
1124    fn test_conditional_order_type_conversions() {
1125        // Test all conditional order types
1126        assert_eq!(
1127            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1128            OrderType::StopMarket
1129        );
1130        assert_eq!(
1131            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1132            OrderType::StopLimit
1133        );
1134        assert_eq!(
1135            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1136            OrderType::MarketIfTouched
1137        );
1138        assert_eq!(
1139            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1140            OrderType::LimitIfTouched
1141        );
1142        assert_eq!(
1143            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1144            OrderType::TrailingStopMarket
1145        );
1146    }
1147
1148    // Tests for error parsing with real and simulated error messages
1149    mod error_parsing_tests {
1150        use super::*;
1151
1152        #[rstest]
1153        fn test_parse_tick_size_error() {
1154            let error = "Price must be divisible by tick size 0.01";
1155            let code = HyperliquidRejectCode::from_api_error(error);
1156            assert_eq!(code, HyperliquidRejectCode::Tick);
1157        }
1158
1159        #[rstest]
1160        fn test_parse_tick_size_error_case_insensitive() {
1161            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1162            let code = HyperliquidRejectCode::from_api_error(error);
1163            assert_eq!(code, HyperliquidRejectCode::Tick);
1164        }
1165
1166        #[rstest]
1167        fn test_parse_min_notional_perp() {
1168            let error = "Order must have minimum value of $10";
1169            let code = HyperliquidRejectCode::from_api_error(error);
1170            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1171        }
1172
1173        #[rstest]
1174        fn test_parse_min_notional_spot() {
1175            let error = "Order must have minimum value of 10 USDC";
1176            let code = HyperliquidRejectCode::from_api_error(error);
1177            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1178        }
1179
1180        #[rstest]
1181        fn test_parse_insufficient_margin() {
1182            let error = "Insufficient margin to place order";
1183            let code = HyperliquidRejectCode::from_api_error(error);
1184            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1185        }
1186
1187        #[rstest]
1188        fn test_parse_insufficient_margin_case_variations() {
1189            let variations = vec![
1190                "insufficient margin to place order",
1191                "INSUFFICIENT MARGIN TO PLACE ORDER",
1192                "  Insufficient margin to place order  ", // with whitespace
1193            ];
1194
1195            for error in variations {
1196                let code = HyperliquidRejectCode::from_api_error(error);
1197                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1198            }
1199        }
1200
1201        #[rstest]
1202        fn test_parse_reduce_only_violation() {
1203            let error = "Reduce only order would increase position";
1204            let code = HyperliquidRejectCode::from_api_error(error);
1205            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1206        }
1207
1208        #[rstest]
1209        fn test_parse_reduce_only_with_hyphen() {
1210            let error = "Reduce-only order would increase position";
1211            let code = HyperliquidRejectCode::from_api_error(error);
1212            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1213        }
1214
1215        #[rstest]
1216        fn test_parse_post_only_match() {
1217            let error = "Post only order would have immediately matched";
1218            let code = HyperliquidRejectCode::from_api_error(error);
1219            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1220        }
1221
1222        #[rstest]
1223        fn test_parse_post_only_with_hyphen() {
1224            let error = "Post-only order would have immediately matched";
1225            let code = HyperliquidRejectCode::from_api_error(error);
1226            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1227        }
1228
1229        #[rstest]
1230        fn test_parse_ioc_no_match() {
1231            let error = "Order could not immediately match";
1232            let code = HyperliquidRejectCode::from_api_error(error);
1233            assert_eq!(code, HyperliquidRejectCode::IocCancel);
1234        }
1235
1236        #[rstest]
1237        fn test_parse_invalid_trigger_price() {
1238            let error = "Invalid TP/SL price";
1239            let code = HyperliquidRejectCode::from_api_error(error);
1240            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1241        }
1242
1243        #[rstest]
1244        fn test_parse_no_liquidity() {
1245            let error = "No liquidity available for market order";
1246            let code = HyperliquidRejectCode::from_api_error(error);
1247            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1248        }
1249
1250        #[rstest]
1251        fn test_parse_position_increase_at_oi_cap() {
1252            let error = "PositionIncreaseAtOpenInterestCap";
1253            let code = HyperliquidRejectCode::from_api_error(error);
1254            assert_eq!(
1255                code,
1256                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1257            );
1258        }
1259
1260        #[rstest]
1261        fn test_parse_position_flip_at_oi_cap() {
1262            let error = "PositionFlipAtOpenInterestCap";
1263            let code = HyperliquidRejectCode::from_api_error(error);
1264            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1265        }
1266
1267        #[rstest]
1268        fn test_parse_too_aggressive_at_oi_cap() {
1269            let error = "TooAggressiveAtOpenInterestCap";
1270            let code = HyperliquidRejectCode::from_api_error(error);
1271            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1272        }
1273
1274        #[rstest]
1275        fn test_parse_open_interest_increase() {
1276            let error = "OpenInterestIncrease";
1277            let code = HyperliquidRejectCode::from_api_error(error);
1278            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1279        }
1280
1281        #[rstest]
1282        fn test_parse_insufficient_spot_balance() {
1283            let error = "Insufficient spot balance";
1284            let code = HyperliquidRejectCode::from_api_error(error);
1285            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1286        }
1287
1288        #[rstest]
1289        fn test_parse_oracle_error() {
1290            let error = "Oracle price unavailable";
1291            let code = HyperliquidRejectCode::from_api_error(error);
1292            assert_eq!(code, HyperliquidRejectCode::Oracle);
1293        }
1294
1295        #[rstest]
1296        fn test_parse_max_position() {
1297            let error = "Exceeds max position size";
1298            let code = HyperliquidRejectCode::from_api_error(error);
1299            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1300        }
1301
1302        #[rstest]
1303        fn test_parse_missing_order() {
1304            let error = "MissingOrder";
1305            let code = HyperliquidRejectCode::from_api_error(error);
1306            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1307        }
1308
1309        #[rstest]
1310        fn test_parse_unknown_error() {
1311            let error = "This is a completely new error message";
1312            let code = HyperliquidRejectCode::from_api_error(error);
1313            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1314
1315            // Verify the original message is preserved
1316            if let HyperliquidRejectCode::Unknown(msg) = code {
1317                assert_eq!(msg, error);
1318            }
1319        }
1320
1321        #[rstest]
1322        fn test_parse_empty_error() {
1323            let error = "";
1324            let code = HyperliquidRejectCode::from_api_error(error);
1325            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1326        }
1327
1328        #[rstest]
1329        fn test_parse_whitespace_only() {
1330            let error = "   ";
1331            let code = HyperliquidRejectCode::from_api_error(error);
1332            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1333        }
1334
1335        #[rstest]
1336        fn test_normalization_preserves_original_in_unknown() {
1337            let error = "  UNKNOWN ERROR MESSAGE  ";
1338            let code = HyperliquidRejectCode::from_api_error(error);
1339
1340            // Should be Unknown, and should contain original message (not normalized)
1341            if let HyperliquidRejectCode::Unknown(msg) = code {
1342                assert_eq!(msg, error);
1343            } else {
1344                panic!("Expected Unknown variant");
1345            }
1346        }
1347    }
1348
1349    #[rstest]
1350    fn test_conditional_order_type_round_trip() {
1351        assert_eq!(
1352            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1353            OrderType::TrailingStopLimit
1354        );
1355
1356        // Test reverse conversions
1357        assert_eq!(
1358            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1359            HyperliquidConditionalOrderType::StopMarket
1360        );
1361        assert_eq!(
1362            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1363            HyperliquidConditionalOrderType::StopLimit
1364        );
1365    }
1366
1367    #[rstest]
1368    fn test_trailing_offset_type_serialization() {
1369        let price = HyperliquidTrailingOffsetType::Price;
1370        let percentage = HyperliquidTrailingOffsetType::Percentage;
1371        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1372
1373        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1374        assert_eq!(
1375            serde_json::to_string(&percentage).unwrap(),
1376            r#""percentage""#
1377        );
1378        assert_eq!(
1379            serde_json::to_string(&basis_points).unwrap(),
1380            r#""basispoints""#
1381        );
1382    }
1383
1384    #[rstest]
1385    fn test_conditional_order_type_serialization() {
1386        assert_eq!(
1387            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1388            r#""STOP_MARKET""#
1389        );
1390        assert_eq!(
1391            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1392            r#""STOP_LIMIT""#
1393        );
1394        assert_eq!(
1395            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1396            r#""TAKE_PROFIT_MARKET""#
1397        );
1398        assert_eq!(
1399            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1400            r#""TAKE_PROFIT_LIMIT""#
1401        );
1402        assert_eq!(
1403            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1404            r#""TRAILING_STOP_MARKET""#
1405        );
1406        assert_eq!(
1407            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1408            r#""TRAILING_STOP_LIMIT""#
1409        );
1410    }
1411
1412    #[rstest]
1413    fn test_order_type_enum_coverage() {
1414        // Ensure all conditional order types roundtrip correctly
1415        let conditional_types = vec![
1416            HyperliquidConditionalOrderType::StopMarket,
1417            HyperliquidConditionalOrderType::StopLimit,
1418            HyperliquidConditionalOrderType::TakeProfitMarket,
1419            HyperliquidConditionalOrderType::TakeProfitLimit,
1420            HyperliquidConditionalOrderType::TrailingStopMarket,
1421            HyperliquidConditionalOrderType::TrailingStopLimit,
1422        ];
1423
1424        for cond_type in conditional_types {
1425            let order_type = OrderType::from(cond_type);
1426            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1427            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1428        }
1429    }
1430
1431    #[rstest]
1432    fn test_all_trigger_price_types() {
1433        let trigger_types = vec![
1434            HyperliquidTriggerPriceType::Last,
1435            HyperliquidTriggerPriceType::Mark,
1436            HyperliquidTriggerPriceType::Oracle,
1437        ];
1438
1439        for trigger_type in trigger_types {
1440            let nautilus_type = TriggerType::from(trigger_type);
1441            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1442            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1443        }
1444    }
1445}