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