nautilus_hyperliquid/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{fmt::Display, str::FromStr};
17
18use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus};
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 nautilus_model::enums::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<nautilus_model::enums::TriggerType> for HyperliquidTriggerPriceType {
276    fn from(value: nautilus_model::enums::TriggerType) -> Self {
277        match value {
278            nautilus_model::enums::TriggerType::LastPrice => Self::Last,
279            nautilus_model::enums::TriggerType::MarkPrice => Self::Mark,
280            nautilus_model::enums::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 nautilus_model::enums::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<nautilus_model::enums::OrderType> for HyperliquidConditionalOrderType {
339    fn from(value: nautilus_model::enums::OrderType) -> Self {
340        match value {
341            nautilus_model::enums::OrderType::StopMarket => Self::StopMarket,
342            nautilus_model::enums::OrderType::StopLimit => Self::StopLimit,
343            nautilus_model::enums::OrderType::MarketIfTouched => Self::TakeProfitMarket,
344            nautilus_model::enums::OrderType::LimitIfTouched => Self::TakeProfitLimit,
345            nautilus_model::enums::OrderType::TrailingStopMarket => Self::TrailingStopMarket,
346            nautilus_model::enums::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////////////////////////////////////////////////////////////////////////////////
821// Tests
822////////////////////////////////////////////////////////////////////////////////
823
824#[cfg(test)]
825mod tests {
826    use nautilus_model::enums::{OrderType, TriggerType};
827    use rstest::rstest;
828    use serde_json;
829
830    use super::*;
831
832    #[rstest]
833    fn test_side_serde() {
834        let buy_side = HyperliquidSide::Buy;
835        let sell_side = HyperliquidSide::Sell;
836
837        assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
838        assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
839
840        assert_eq!(
841            serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
842            HyperliquidSide::Buy
843        );
844        assert_eq!(
845            serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
846            HyperliquidSide::Sell
847        );
848    }
849
850    #[rstest]
851    fn test_side_from_order_side() {
852        // Test conversion from OrderSide to HyperliquidSide
853        assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
854        assert_eq!(
855            HyperliquidSide::from(OrderSide::Sell),
856            HyperliquidSide::Sell
857        );
858    }
859
860    #[rstest]
861    fn test_order_side_from_hyperliquid_side() {
862        // Test conversion from HyperliquidSide to OrderSide
863        assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
864        assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
865    }
866
867    #[rstest]
868    fn test_aggressor_side_from_hyperliquid_side() {
869        // Test conversion from HyperliquidSide to AggressorSide
870        assert_eq!(
871            AggressorSide::from(HyperliquidSide::Buy),
872            AggressorSide::Buyer
873        );
874        assert_eq!(
875            AggressorSide::from(HyperliquidSide::Sell),
876            AggressorSide::Seller
877        );
878    }
879
880    #[rstest]
881    fn test_time_in_force_serde() {
882        let test_cases = [
883            (HyperliquidTimeInForce::Alo, "\"Alo\""),
884            (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
885            (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
886        ];
887
888        for (tif, expected_json) in test_cases {
889            assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
890            assert_eq!(
891                serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
892                tif
893            );
894        }
895    }
896
897    #[rstest]
898    fn test_liquidity_flag_from_crossed() {
899        assert_eq!(
900            HyperliquidLiquidityFlag::from(true),
901            HyperliquidLiquidityFlag::Taker
902        );
903        assert_eq!(
904            HyperliquidLiquidityFlag::from(false),
905            HyperliquidLiquidityFlag::Maker
906        );
907    }
908
909    #[rstest]
910    #[allow(deprecated)]
911    fn test_reject_code_from_error_string() {
912        let test_cases = [
913            (
914                "Price must be divisible by tick size.",
915                HyperliquidRejectCode::Tick,
916            ),
917            (
918                "Order must have minimum value of $10.",
919                HyperliquidRejectCode::MinTradeNtl,
920            ),
921            (
922                "Insufficient margin to place order.",
923                HyperliquidRejectCode::PerpMargin,
924            ),
925            (
926                "Post only order would have immediately matched, bbo was 1.23",
927                HyperliquidRejectCode::BadAloPx,
928            ),
929            (
930                "Some unknown error",
931                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
932            ),
933        ];
934
935        for (error_str, expected_code) in test_cases {
936            assert_eq!(
937                HyperliquidRejectCode::from_error_string(error_str),
938                expected_code
939            );
940        }
941    }
942
943    #[rstest]
944    fn test_reject_code_from_api_error() {
945        let test_cases = [
946            (
947                "Price must be divisible by tick size.",
948                HyperliquidRejectCode::Tick,
949            ),
950            (
951                "Order must have minimum value of $10.",
952                HyperliquidRejectCode::MinTradeNtl,
953            ),
954            (
955                "Insufficient margin to place order.",
956                HyperliquidRejectCode::PerpMargin,
957            ),
958            (
959                "Post only order would have immediately matched, bbo was 1.23",
960                HyperliquidRejectCode::BadAloPx,
961            ),
962            (
963                "Some unknown error",
964                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
965            ),
966        ];
967
968        for (error_str, expected_code) in test_cases {
969            assert_eq!(
970                HyperliquidRejectCode::from_api_error(error_str),
971                expected_code
972            );
973        }
974    }
975
976    #[rstest]
977    fn test_reduce_only() {
978        let reduce_only = HyperliquidReduceOnly::new(true);
979
980        assert!(reduce_only.is_reduce_only());
981
982        let json = serde_json::to_string(&reduce_only).unwrap();
983        assert_eq!(json, "true");
984
985        let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
986        assert_eq!(parsed, reduce_only);
987    }
988
989    #[rstest]
990    fn test_order_status_conversion() {
991        // Test HyperliquidOrderStatus to OrderState conversion
992        assert_eq!(
993            OrderStatus::from(HyperliquidOrderStatus::Open),
994            OrderStatus::Accepted
995        );
996        assert_eq!(
997            OrderStatus::from(HyperliquidOrderStatus::Accepted),
998            OrderStatus::Accepted
999        );
1000        assert_eq!(
1001            OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
1002            OrderStatus::PartiallyFilled
1003        );
1004        assert_eq!(
1005            OrderStatus::from(HyperliquidOrderStatus::Filled),
1006            OrderStatus::Filled
1007        );
1008        assert_eq!(
1009            OrderStatus::from(HyperliquidOrderStatus::Canceled),
1010            OrderStatus::Canceled
1011        );
1012        assert_eq!(
1013            OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1014            OrderStatus::Canceled
1015        );
1016        assert_eq!(
1017            OrderStatus::from(HyperliquidOrderStatus::Rejected),
1018            OrderStatus::Rejected
1019        );
1020        assert_eq!(
1021            OrderStatus::from(HyperliquidOrderStatus::Expired),
1022            OrderStatus::Expired
1023        );
1024    }
1025
1026    #[rstest]
1027    fn test_order_status_string_mapping() {
1028        // Test direct string to OrderState conversion
1029        assert_eq!(
1030            hyperliquid_status_to_order_status("open"),
1031            OrderStatus::Accepted
1032        );
1033        assert_eq!(
1034            hyperliquid_status_to_order_status("accepted"),
1035            OrderStatus::Accepted
1036        );
1037        assert_eq!(
1038            hyperliquid_status_to_order_status("partially_filled"),
1039            OrderStatus::PartiallyFilled
1040        );
1041        assert_eq!(
1042            hyperliquid_status_to_order_status("filled"),
1043            OrderStatus::Filled
1044        );
1045        assert_eq!(
1046            hyperliquid_status_to_order_status("canceled"),
1047            OrderStatus::Canceled
1048        );
1049        assert_eq!(
1050            hyperliquid_status_to_order_status("cancelled"),
1051            OrderStatus::Canceled
1052        );
1053        assert_eq!(
1054            hyperliquid_status_to_order_status("rejected"),
1055            OrderStatus::Rejected
1056        );
1057        assert_eq!(
1058            hyperliquid_status_to_order_status("expired"),
1059            OrderStatus::Expired
1060        );
1061        assert_eq!(
1062            hyperliquid_status_to_order_status("unknown_status"),
1063            OrderStatus::Rejected
1064        );
1065    }
1066
1067    // ========================================================================
1068    // Conditional Order Tests
1069    // ========================================================================
1070
1071    #[rstest]
1072    fn test_hyperliquid_tpsl_serialization() {
1073        let tp = HyperliquidTpSl::Tp;
1074        let sl = HyperliquidTpSl::Sl;
1075
1076        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1077        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1078    }
1079
1080    #[rstest]
1081    fn test_hyperliquid_tpsl_deserialization() {
1082        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1083        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1084
1085        assert_eq!(tp, HyperliquidTpSl::Tp);
1086        assert_eq!(sl, HyperliquidTpSl::Sl);
1087    }
1088
1089    #[rstest]
1090    fn test_hyperliquid_trigger_price_type_serialization() {
1091        let last = HyperliquidTriggerPriceType::Last;
1092        let mark = HyperliquidTriggerPriceType::Mark;
1093        let oracle = HyperliquidTriggerPriceType::Oracle;
1094
1095        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1096        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1097        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1098    }
1099
1100    #[rstest]
1101    fn test_hyperliquid_trigger_price_type_to_nautilus() {
1102        assert_eq!(
1103            TriggerType::from(HyperliquidTriggerPriceType::Last),
1104            TriggerType::LastPrice
1105        );
1106        assert_eq!(
1107            TriggerType::from(HyperliquidTriggerPriceType::Mark),
1108            TriggerType::MarkPrice
1109        );
1110        assert_eq!(
1111            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1112            TriggerType::IndexPrice
1113        );
1114    }
1115
1116    #[rstest]
1117    fn test_nautilus_trigger_type_to_hyperliquid() {
1118        assert_eq!(
1119            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1120            HyperliquidTriggerPriceType::Last
1121        );
1122        assert_eq!(
1123            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1124            HyperliquidTriggerPriceType::Mark
1125        );
1126        assert_eq!(
1127            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1128            HyperliquidTriggerPriceType::Oracle
1129        );
1130    }
1131
1132    #[rstest]
1133    fn test_conditional_order_type_conversions() {
1134        // Test all conditional order types
1135        assert_eq!(
1136            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1137            OrderType::StopMarket
1138        );
1139        assert_eq!(
1140            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1141            OrderType::StopLimit
1142        );
1143        assert_eq!(
1144            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1145            OrderType::MarketIfTouched
1146        );
1147        assert_eq!(
1148            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1149            OrderType::LimitIfTouched
1150        );
1151        assert_eq!(
1152            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1153            OrderType::TrailingStopMarket
1154        );
1155    }
1156
1157    // Tests for error parsing with real and simulated error messages
1158    mod error_parsing_tests {
1159        use super::*;
1160
1161        #[rstest]
1162        fn test_parse_tick_size_error() {
1163            let error = "Price must be divisible by tick size 0.01";
1164            let code = HyperliquidRejectCode::from_api_error(error);
1165            assert_eq!(code, HyperliquidRejectCode::Tick);
1166        }
1167
1168        #[rstest]
1169        fn test_parse_tick_size_error_case_insensitive() {
1170            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1171            let code = HyperliquidRejectCode::from_api_error(error);
1172            assert_eq!(code, HyperliquidRejectCode::Tick);
1173        }
1174
1175        #[rstest]
1176        fn test_parse_min_notional_perp() {
1177            let error = "Order must have minimum value of $10";
1178            let code = HyperliquidRejectCode::from_api_error(error);
1179            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1180        }
1181
1182        #[rstest]
1183        fn test_parse_min_notional_spot() {
1184            let error = "Order must have minimum value of 10 USDC";
1185            let code = HyperliquidRejectCode::from_api_error(error);
1186            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1187        }
1188
1189        #[rstest]
1190        fn test_parse_insufficient_margin() {
1191            let error = "Insufficient margin to place order";
1192            let code = HyperliquidRejectCode::from_api_error(error);
1193            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1194        }
1195
1196        #[rstest]
1197        fn test_parse_insufficient_margin_case_variations() {
1198            let variations = vec![
1199                "insufficient margin to place order",
1200                "INSUFFICIENT MARGIN TO PLACE ORDER",
1201                "  Insufficient margin to place order  ", // with whitespace
1202            ];
1203
1204            for error in variations {
1205                let code = HyperliquidRejectCode::from_api_error(error);
1206                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1207            }
1208        }
1209
1210        #[rstest]
1211        fn test_parse_reduce_only_violation() {
1212            let error = "Reduce only order would increase position";
1213            let code = HyperliquidRejectCode::from_api_error(error);
1214            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1215        }
1216
1217        #[rstest]
1218        fn test_parse_reduce_only_with_hyphen() {
1219            let error = "Reduce-only order would increase position";
1220            let code = HyperliquidRejectCode::from_api_error(error);
1221            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1222        }
1223
1224        #[rstest]
1225        fn test_parse_post_only_match() {
1226            let error = "Post only order would have immediately matched";
1227            let code = HyperliquidRejectCode::from_api_error(error);
1228            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1229        }
1230
1231        #[rstest]
1232        fn test_parse_post_only_with_hyphen() {
1233            let error = "Post-only order would have immediately matched";
1234            let code = HyperliquidRejectCode::from_api_error(error);
1235            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1236        }
1237
1238        #[rstest]
1239        fn test_parse_ioc_no_match() {
1240            let error = "Order could not immediately match";
1241            let code = HyperliquidRejectCode::from_api_error(error);
1242            assert_eq!(code, HyperliquidRejectCode::IocCancel);
1243        }
1244
1245        #[rstest]
1246        fn test_parse_invalid_trigger_price() {
1247            let error = "Invalid TP/SL price";
1248            let code = HyperliquidRejectCode::from_api_error(error);
1249            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1250        }
1251
1252        #[rstest]
1253        fn test_parse_no_liquidity() {
1254            let error = "No liquidity available for market order";
1255            let code = HyperliquidRejectCode::from_api_error(error);
1256            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1257        }
1258
1259        #[rstest]
1260        fn test_parse_position_increase_at_oi_cap() {
1261            let error = "PositionIncreaseAtOpenInterestCap";
1262            let code = HyperliquidRejectCode::from_api_error(error);
1263            assert_eq!(
1264                code,
1265                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1266            );
1267        }
1268
1269        #[rstest]
1270        fn test_parse_position_flip_at_oi_cap() {
1271            let error = "PositionFlipAtOpenInterestCap";
1272            let code = HyperliquidRejectCode::from_api_error(error);
1273            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1274        }
1275
1276        #[rstest]
1277        fn test_parse_too_aggressive_at_oi_cap() {
1278            let error = "TooAggressiveAtOpenInterestCap";
1279            let code = HyperliquidRejectCode::from_api_error(error);
1280            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1281        }
1282
1283        #[rstest]
1284        fn test_parse_open_interest_increase() {
1285            let error = "OpenInterestIncrease";
1286            let code = HyperliquidRejectCode::from_api_error(error);
1287            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1288        }
1289
1290        #[rstest]
1291        fn test_parse_insufficient_spot_balance() {
1292            let error = "Insufficient spot balance";
1293            let code = HyperliquidRejectCode::from_api_error(error);
1294            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1295        }
1296
1297        #[rstest]
1298        fn test_parse_oracle_error() {
1299            let error = "Oracle price unavailable";
1300            let code = HyperliquidRejectCode::from_api_error(error);
1301            assert_eq!(code, HyperliquidRejectCode::Oracle);
1302        }
1303
1304        #[rstest]
1305        fn test_parse_max_position() {
1306            let error = "Exceeds max position size";
1307            let code = HyperliquidRejectCode::from_api_error(error);
1308            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1309        }
1310
1311        #[rstest]
1312        fn test_parse_missing_order() {
1313            let error = "MissingOrder";
1314            let code = HyperliquidRejectCode::from_api_error(error);
1315            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1316        }
1317
1318        #[rstest]
1319        fn test_parse_unknown_error() {
1320            let error = "This is a completely new error message";
1321            let code = HyperliquidRejectCode::from_api_error(error);
1322            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1323
1324            // Verify the original message is preserved
1325            if let HyperliquidRejectCode::Unknown(msg) = code {
1326                assert_eq!(msg, error);
1327            }
1328        }
1329
1330        #[rstest]
1331        fn test_parse_empty_error() {
1332            let error = "";
1333            let code = HyperliquidRejectCode::from_api_error(error);
1334            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1335        }
1336
1337        #[rstest]
1338        fn test_parse_whitespace_only() {
1339            let error = "   ";
1340            let code = HyperliquidRejectCode::from_api_error(error);
1341            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1342        }
1343
1344        #[rstest]
1345        fn test_normalization_preserves_original_in_unknown() {
1346            let error = "  UNKNOWN ERROR MESSAGE  ";
1347            let code = HyperliquidRejectCode::from_api_error(error);
1348
1349            // Should be Unknown, and should contain original message (not normalized)
1350            if let HyperliquidRejectCode::Unknown(msg) = code {
1351                assert_eq!(msg, error);
1352            } else {
1353                panic!("Expected Unknown variant");
1354            }
1355        }
1356    }
1357
1358    #[rstest]
1359    fn test_conditional_order_type_round_trip() {
1360        assert_eq!(
1361            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1362            OrderType::TrailingStopLimit
1363        );
1364
1365        // Test reverse conversions
1366        assert_eq!(
1367            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1368            HyperliquidConditionalOrderType::StopMarket
1369        );
1370        assert_eq!(
1371            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1372            HyperliquidConditionalOrderType::StopLimit
1373        );
1374    }
1375
1376    #[rstest]
1377    fn test_trailing_offset_type_serialization() {
1378        let price = HyperliquidTrailingOffsetType::Price;
1379        let percentage = HyperliquidTrailingOffsetType::Percentage;
1380        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1381
1382        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1383        assert_eq!(
1384            serde_json::to_string(&percentage).unwrap(),
1385            r#""percentage""#
1386        );
1387        assert_eq!(
1388            serde_json::to_string(&basis_points).unwrap(),
1389            r#""basispoints""#
1390        );
1391    }
1392
1393    #[rstest]
1394    fn test_conditional_order_type_serialization() {
1395        assert_eq!(
1396            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1397            r#""STOP_MARKET""#
1398        );
1399        assert_eq!(
1400            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1401            r#""STOP_LIMIT""#
1402        );
1403        assert_eq!(
1404            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1405            r#""TAKE_PROFIT_MARKET""#
1406        );
1407        assert_eq!(
1408            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1409            r#""TAKE_PROFIT_LIMIT""#
1410        );
1411        assert_eq!(
1412            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1413            r#""TRAILING_STOP_MARKET""#
1414        );
1415        assert_eq!(
1416            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1417            r#""TRAILING_STOP_LIMIT""#
1418        );
1419    }
1420
1421    #[rstest]
1422    fn test_order_type_enum_coverage() {
1423        // Ensure all conditional order types roundtrip correctly
1424        let conditional_types = vec![
1425            HyperliquidConditionalOrderType::StopMarket,
1426            HyperliquidConditionalOrderType::StopLimit,
1427            HyperliquidConditionalOrderType::TakeProfitMarket,
1428            HyperliquidConditionalOrderType::TakeProfitLimit,
1429            HyperliquidConditionalOrderType::TrailingStopMarket,
1430            HyperliquidConditionalOrderType::TrailingStopLimit,
1431        ];
1432
1433        for cond_type in conditional_types {
1434            let order_type = OrderType::from(cond_type);
1435            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1436            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1437        }
1438    }
1439
1440    #[rstest]
1441    fn test_all_trigger_price_types() {
1442        let trigger_types = vec![
1443            HyperliquidTriggerPriceType::Last,
1444            HyperliquidTriggerPriceType::Mark,
1445            HyperliquidTriggerPriceType::Oracle,
1446        ];
1447
1448        for trigger_type in trigger_types {
1449            let nautilus_type = TriggerType::from(trigger_type);
1450            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1451            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1452        }
1453    }
1454}