Skip to main content

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