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