nautilus_hyperliquid/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus};
17use serde::{Deserialize, Serialize};
18use strum::{AsRefStr, Display, EnumIter, EnumString};
19
20/// Represents the order side (Buy or Sell).
21#[derive(
22    Copy,
23    Clone,
24    Debug,
25    Display,
26    PartialEq,
27    Eq,
28    Hash,
29    AsRefStr,
30    EnumIter,
31    EnumString,
32    Serialize,
33    Deserialize,
34)]
35#[serde(rename_all = "UPPERCASE")]
36#[strum(serialize_all = "UPPERCASE")]
37pub enum HyperliquidSide {
38    #[serde(rename = "B")]
39    Buy,
40    #[serde(rename = "A")]
41    Sell,
42}
43
44impl From<OrderSide> for HyperliquidSide {
45    fn from(value: OrderSide) -> Self {
46        match value {
47            OrderSide::Buy => Self::Buy,
48            OrderSide::Sell => Self::Sell,
49            _ => panic!("Invalid `OrderSide`"),
50        }
51    }
52}
53
54impl From<HyperliquidSide> for OrderSide {
55    fn from(value: HyperliquidSide) -> Self {
56        match value {
57            HyperliquidSide::Buy => Self::Buy,
58            HyperliquidSide::Sell => Self::Sell,
59        }
60    }
61}
62
63impl From<HyperliquidSide> for AggressorSide {
64    fn from(value: HyperliquidSide) -> Self {
65        match value {
66            HyperliquidSide::Buy => Self::Buyer,
67            HyperliquidSide::Sell => Self::Seller,
68        }
69    }
70}
71
72/// Represents the time in force for limit orders.
73#[derive(
74    Copy,
75    Clone,
76    Debug,
77    Display,
78    PartialEq,
79    Eq,
80    Hash,
81    AsRefStr,
82    EnumIter,
83    EnumString,
84    Serialize,
85    Deserialize,
86)]
87#[serde(rename_all = "PascalCase")]
88#[strum(serialize_all = "PascalCase")]
89pub enum HyperliquidTimeInForce {
90    /// Add Liquidity Only - post-only order.
91    Alo,
92    /// Immediate or Cancel - fill immediately or cancel.
93    Ioc,
94    /// Good Till Cancel - remain on book until filled or cancelled.
95    Gtc,
96}
97
98/// Represents the order type configuration.
99#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100#[serde(tag = "type", rename_all = "lowercase")]
101pub enum HyperliquidOrderType {
102    /// Limit order with time-in-force.
103    #[serde(rename = "limit")]
104    Limit { tif: HyperliquidTimeInForce },
105
106    /// Trigger order (stop or take profit).
107    #[serde(rename = "trigger")]
108    Trigger {
109        #[serde(rename = "isMarket")]
110        is_market: bool,
111        #[serde(rename = "triggerPx")]
112        trigger_px: String,
113        tpsl: HyperliquidTpSl,
114    },
115}
116
117/// Represents the take profit / stop loss type.
118#[derive(
119    Copy,
120    Clone,
121    Debug,
122    Display,
123    PartialEq,
124    Eq,
125    Hash,
126    AsRefStr,
127    EnumIter,
128    EnumString,
129    Serialize,
130    Deserialize,
131)]
132#[cfg_attr(
133    feature = "python",
134    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
135)]
136#[serde(rename_all = "lowercase")]
137#[strum(serialize_all = "lowercase")]
138pub enum HyperliquidTpSl {
139    /// Take Profit.
140    Tp,
141    /// Stop Loss.
142    Sl,
143}
144
145/// Represents trigger price types for conditional orders.
146///
147/// Hyperliquid supports different price references for trigger evaluation:
148/// - Last: Last traded price (most common)
149/// - Mark: Mark price (for risk management)
150/// - Oracle: Oracle/index price (for some perpetuals)
151#[derive(
152    Copy,
153    Clone,
154    Debug,
155    Display,
156    PartialEq,
157    Eq,
158    Hash,
159    AsRefStr,
160    EnumIter,
161    EnumString,
162    Serialize,
163    Deserialize,
164)]
165#[cfg_attr(
166    feature = "python",
167    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
168)]
169#[serde(rename_all = "lowercase")]
170#[strum(serialize_all = "lowercase")]
171pub enum HyperliquidTriggerPriceType {
172    /// Last traded price.
173    Last,
174    /// Mark price.
175    Mark,
176    /// Oracle/index price.
177    Oracle,
178}
179
180impl From<HyperliquidTriggerPriceType> for nautilus_model::enums::TriggerType {
181    fn from(value: HyperliquidTriggerPriceType) -> Self {
182        match value {
183            HyperliquidTriggerPriceType::Last => Self::LastPrice,
184            HyperliquidTriggerPriceType::Mark => Self::MarkPrice,
185            HyperliquidTriggerPriceType::Oracle => Self::IndexPrice,
186        }
187    }
188}
189
190impl From<nautilus_model::enums::TriggerType> for HyperliquidTriggerPriceType {
191    fn from(value: nautilus_model::enums::TriggerType) -> Self {
192        match value {
193            nautilus_model::enums::TriggerType::LastPrice => Self::Last,
194            nautilus_model::enums::TriggerType::MarkPrice => Self::Mark,
195            nautilus_model::enums::TriggerType::IndexPrice => Self::Oracle,
196            _ => Self::Last, // Default fallback
197        }
198    }
199}
200
201/// Represents conditional/trigger order types.
202///
203/// Hyperliquid supports various conditional order types that trigger
204/// based on market conditions. These map to Nautilus OrderType variants.
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(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
222)]
223#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
224#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
225pub enum HyperliquidConditionalOrderType {
226    /// Stop market order (protective stop with market execution).
227    StopMarket,
228    /// Stop limit order (protective stop with limit price).
229    StopLimit,
230    /// Take profit market order (profit-taking with market execution).
231    TakeProfitMarket,
232    /// Take profit limit order (profit-taking with limit price).
233    TakeProfitLimit,
234    /// Trailing stop market order (dynamic stop with market execution).
235    TrailingStopMarket,
236    /// Trailing stop limit order (dynamic stop with limit price).
237    TrailingStopLimit,
238}
239
240impl From<HyperliquidConditionalOrderType> for nautilus_model::enums::OrderType {
241    fn from(value: HyperliquidConditionalOrderType) -> Self {
242        match value {
243            HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
244            HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
245            HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
246            HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
247            HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
248            HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
249        }
250    }
251}
252
253impl From<nautilus_model::enums::OrderType> for HyperliquidConditionalOrderType {
254    fn from(value: nautilus_model::enums::OrderType) -> Self {
255        match value {
256            nautilus_model::enums::OrderType::StopMarket => Self::StopMarket,
257            nautilus_model::enums::OrderType::StopLimit => Self::StopLimit,
258            nautilus_model::enums::OrderType::MarketIfTouched => Self::TakeProfitMarket,
259            nautilus_model::enums::OrderType::LimitIfTouched => Self::TakeProfitLimit,
260            nautilus_model::enums::OrderType::TrailingStopMarket => Self::TrailingStopMarket,
261            nautilus_model::enums::OrderType::TrailingStopLimit => Self::TrailingStopLimit,
262            _ => panic!("Unsupported OrderType for conditional orders: {:?}", value),
263        }
264    }
265}
266
267/// Represents trailing offset types for trailing stop orders.
268///
269/// Trailing stops adjust dynamically based on market movement:
270/// - Price: Fixed price offset (e.g., $100)
271/// - Percentage: Percentage offset (e.g., 5%)
272/// - BasisPoints: Basis points offset (e.g., 250 bps = 2.5%)
273#[derive(
274    Copy,
275    Clone,
276    Debug,
277    Display,
278    PartialEq,
279    Eq,
280    Hash,
281    AsRefStr,
282    EnumIter,
283    EnumString,
284    Serialize,
285    Deserialize,
286)]
287#[cfg_attr(
288    feature = "python",
289    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
290)]
291#[serde(rename_all = "lowercase")]
292#[strum(serialize_all = "lowercase")]
293pub enum HyperliquidTrailingOffsetType {
294    /// Fixed price offset.
295    Price,
296    /// Percentage offset.
297    Percentage,
298    /// Basis points offset (1 bp = 0.01%).
299    #[serde(rename = "basispoints")]
300    #[strum(serialize = "basispoints")]
301    BasisPoints,
302}
303
304/// Represents the reduce only flag wrapper.
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[serde(transparent)]
307pub struct HyperliquidReduceOnly(pub bool);
308
309impl HyperliquidReduceOnly {
310    /// Creates a new reduce only flag.
311    pub fn new(reduce_only: bool) -> Self {
312        Self(reduce_only)
313    }
314
315    /// Returns whether this is a reduce only order.
316    pub fn is_reduce_only(&self) -> bool {
317        self.0
318    }
319}
320
321/// Represents the liquidity flag indicating maker or taker.
322#[derive(
323    Copy,
324    Clone,
325    Debug,
326    Display,
327    PartialEq,
328    Eq,
329    Hash,
330    AsRefStr,
331    EnumIter,
332    EnumString,
333    Serialize,
334    Deserialize,
335)]
336#[serde(rename_all = "lowercase")]
337#[strum(serialize_all = "lowercase")]
338pub enum HyperliquidLiquidityFlag {
339    Maker,
340    Taker,
341}
342
343impl From<bool> for HyperliquidLiquidityFlag {
344    /// Converts from `crossed` field in fill responses.
345    ///
346    /// `true` (crossed) -> Taker, `false` -> Maker
347    fn from(crossed: bool) -> Self {
348        if crossed {
349            HyperliquidLiquidityFlag::Taker
350        } else {
351            HyperliquidLiquidityFlag::Maker
352        }
353    }
354}
355
356#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
357#[serde(untagged)]
358pub enum HyperliquidRejectCode {
359    /// Price must be divisible by tick size.
360    Tick,
361    /// Order must have minimum value of $10.
362    MinTradeNtl,
363    /// Order must have minimum value of 10 {quote_token}.
364    MinTradeSpotNtl,
365    /// Insufficient margin to place order.
366    PerpMargin,
367    /// Reduce only order would increase position.
368    ReduceOnly,
369    /// Post only order would have immediately matched.
370    BadAloPx,
371    /// Order could not immediately match.
372    IocCancel,
373    /// Invalid TP/SL price.
374    BadTriggerPx,
375    /// No liquidity available for market order.
376    MarketOrderNoLiquidity,
377    /// Position increase at open interest cap.
378    PositionIncreaseAtOpenInterestCap,
379    /// Position flip at open interest cap.
380    PositionFlipAtOpenInterestCap,
381    /// Too aggressive at open interest cap.
382    TooAggressiveAtOpenInterestCap,
383    /// Open interest increase.
384    OpenInterestIncrease,
385    /// Insufficient spot balance.
386    InsufficientSpotBalance,
387    /// Oracle issue.
388    Oracle,
389    /// Perp max position.
390    PerpMaxPosition,
391    /// Missing order.
392    MissingOrder,
393    /// Unknown reject reason with raw error message.
394    Unknown(String),
395}
396
397impl HyperliquidRejectCode {
398    /// Parse reject code from Hyperliquid API error message.
399    pub fn from_api_error(error_message: &str) -> Self {
400        Self::from_error_string_internal(error_message)
401    }
402
403    fn from_error_string_internal(error: &str) -> Self {
404        // Normalize: trim whitespace and convert to lowercase for robust matching
405        let normalized = error.trim().to_lowercase();
406
407        match normalized.as_str() {
408            // Tick size validation errors
409            s if s.contains("tick size") => HyperliquidRejectCode::Tick,
410
411            // Minimum notional value errors (perp: $10, spot: 10 USDC)
412            s if s.contains("minimum value of $10") => HyperliquidRejectCode::MinTradeNtl,
413            s if s.contains("minimum value of 10") => HyperliquidRejectCode::MinTradeSpotNtl,
414
415            // Margin errors
416            s if s.contains("insufficient margin") => HyperliquidRejectCode::PerpMargin,
417
418            // Reduce-only order violations
419            s if s.contains("reduce only order would increase")
420                || s.contains("reduce-only order would increase") =>
421            {
422                HyperliquidRejectCode::ReduceOnly
423            }
424
425            // Post-only order matching errors
426            s if s.contains("post only order would have immediately matched")
427                || s.contains("post-only order would have immediately matched") =>
428            {
429                HyperliquidRejectCode::BadAloPx
430            }
431
432            // IOC (Immediate-or-Cancel) order errors
433            s if s.contains("could not immediately match") => HyperliquidRejectCode::IocCancel,
434
435            // TP/SL trigger price errors
436            s if s.contains("invalid tp/sl price") => HyperliquidRejectCode::BadTriggerPx,
437
438            // Market order liquidity errors
439            s if s.contains("no liquidity available for market order") => {
440                HyperliquidRejectCode::MarketOrderNoLiquidity
441            }
442
443            // Open interest cap errors (various types)
444            // Note: These patterns are case-insensitive due to normalization
445            s if s.contains("positionincreaseatopeninterestcap") => {
446                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
447            }
448            s if s.contains("positionflipatopeninterestcap") => {
449                HyperliquidRejectCode::PositionFlipAtOpenInterestCap
450            }
451            s if s.contains("tooaggressiveatopeninterestcap") => {
452                HyperliquidRejectCode::TooAggressiveAtOpenInterestCap
453            }
454            s if s.contains("openinterestincrease") => HyperliquidRejectCode::OpenInterestIncrease,
455
456            // Spot balance errors
457            s if s.contains("insufficient spot balance") => {
458                HyperliquidRejectCode::InsufficientSpotBalance
459            }
460
461            // Oracle errors
462            s if s.contains("oracle") => HyperliquidRejectCode::Oracle,
463
464            // Position size limit errors
465            s if s.contains("max position") => HyperliquidRejectCode::PerpMaxPosition,
466
467            // Missing order errors (cancel/modify non-existent order)
468            s if s.contains("missingorder") => HyperliquidRejectCode::MissingOrder,
469
470            // Unknown error - log for monitoring and return with original message
471            _ => {
472                tracing::warn!(
473                    "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
474                    error // Use original error, not normalized
475                );
476                HyperliquidRejectCode::Unknown(error.to_string())
477            }
478        }
479    }
480
481    /// Parses reject code from error string.
482    ///
483    /// **Deprecated**: This method uses substring matching which is fragile and not robust.
484    /// Use `from_api_error()` instead, which provides a migration path for structured error handling.
485    #[deprecated(
486        since = "0.50.0",
487        note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
488    )]
489    pub fn from_error_string(error: &str) -> Self {
490        Self::from_error_string_internal(error)
491    }
492}
493
494/// Represents Hyperliquid order status from API responses
495#[derive(
496    Copy,
497    Clone,
498    Debug,
499    Display,
500    PartialEq,
501    Eq,
502    Hash,
503    AsRefStr,
504    EnumIter,
505    EnumString,
506    Serialize,
507    Deserialize,
508)]
509#[serde(rename_all = "snake_case")]
510#[strum(serialize_all = "snake_case")]
511pub enum HyperliquidOrderStatus {
512    /// Order has been accepted and is open
513    Open,
514    /// Order has been accepted and is open (alternative representation)
515    Accepted,
516    /// Order has been partially filled
517    PartiallyFilled,
518    /// Order has been completely filled
519    Filled,
520    /// Order has been canceled
521    Canceled,
522    /// Order has been canceled (alternative spelling)
523    Cancelled,
524    /// Order was rejected by the exchange
525    Rejected,
526    /// Order has expired
527    Expired,
528}
529
530impl From<HyperliquidOrderStatus> for OrderStatus {
531    fn from(status: HyperliquidOrderStatus) -> Self {
532        match status {
533            HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
534            HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
535            HyperliquidOrderStatus::Filled => Self::Filled,
536            HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
537            HyperliquidOrderStatus::Rejected => Self::Rejected,
538            HyperliquidOrderStatus::Expired => Self::Expired,
539        }
540    }
541}
542
543pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
544    match status {
545        "open" | "accepted" => OrderStatus::Accepted,
546        "partially_filled" => OrderStatus::PartiallyFilled,
547        "filled" => OrderStatus::Filled,
548        "canceled" | "cancelled" => OrderStatus::Canceled,
549        "rejected" => OrderStatus::Rejected,
550        "expired" => OrderStatus::Expired,
551        _ => OrderStatus::Rejected,
552    }
553}
554
555////////////////////////////////////////////////////////////////////////////////
556// Tests
557////////////////////////////////////////////////////////////////////////////////
558
559#[cfg(test)]
560mod tests {
561    use nautilus_model::enums::{OrderType, TriggerType};
562    use rstest::rstest;
563    use serde_json;
564
565    use super::*;
566
567    #[rstest]
568    fn test_side_serde() {
569        let buy_side = HyperliquidSide::Buy;
570        let sell_side = HyperliquidSide::Sell;
571
572        assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
573        assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
574
575        assert_eq!(
576            serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
577            HyperliquidSide::Buy
578        );
579        assert_eq!(
580            serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
581            HyperliquidSide::Sell
582        );
583    }
584
585    #[rstest]
586    fn test_side_from_order_side() {
587        // Test conversion from OrderSide to HyperliquidSide
588        assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
589        assert_eq!(
590            HyperliquidSide::from(OrderSide::Sell),
591            HyperliquidSide::Sell
592        );
593    }
594
595    #[rstest]
596    fn test_order_side_from_hyperliquid_side() {
597        // Test conversion from HyperliquidSide to OrderSide
598        assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
599        assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
600    }
601
602    #[rstest]
603    fn test_aggressor_side_from_hyperliquid_side() {
604        // Test conversion from HyperliquidSide to AggressorSide
605        assert_eq!(
606            AggressorSide::from(HyperliquidSide::Buy),
607            AggressorSide::Buyer
608        );
609        assert_eq!(
610            AggressorSide::from(HyperliquidSide::Sell),
611            AggressorSide::Seller
612        );
613    }
614
615    #[rstest]
616    fn test_time_in_force_serde() {
617        let test_cases = [
618            (HyperliquidTimeInForce::Alo, "\"Alo\""),
619            (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
620            (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
621        ];
622
623        for (tif, expected_json) in test_cases {
624            assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
625            assert_eq!(
626                serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
627                tif
628            );
629        }
630    }
631
632    #[rstest]
633    fn test_liquidity_flag_from_crossed() {
634        assert_eq!(
635            HyperliquidLiquidityFlag::from(true),
636            HyperliquidLiquidityFlag::Taker
637        );
638        assert_eq!(
639            HyperliquidLiquidityFlag::from(false),
640            HyperliquidLiquidityFlag::Maker
641        );
642    }
643
644    #[rstest]
645    #[allow(deprecated)]
646    fn test_reject_code_from_error_string() {
647        let test_cases = [
648            (
649                "Price must be divisible by tick size.",
650                HyperliquidRejectCode::Tick,
651            ),
652            (
653                "Order must have minimum value of $10.",
654                HyperliquidRejectCode::MinTradeNtl,
655            ),
656            (
657                "Insufficient margin to place order.",
658                HyperliquidRejectCode::PerpMargin,
659            ),
660            (
661                "Post only order would have immediately matched, bbo was 1.23",
662                HyperliquidRejectCode::BadAloPx,
663            ),
664            (
665                "Some unknown error",
666                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
667            ),
668        ];
669
670        for (error_str, expected_code) in test_cases {
671            assert_eq!(
672                HyperliquidRejectCode::from_error_string(error_str),
673                expected_code
674            );
675        }
676    }
677
678    #[rstest]
679    fn test_reject_code_from_api_error() {
680        let test_cases = [
681            (
682                "Price must be divisible by tick size.",
683                HyperliquidRejectCode::Tick,
684            ),
685            (
686                "Order must have minimum value of $10.",
687                HyperliquidRejectCode::MinTradeNtl,
688            ),
689            (
690                "Insufficient margin to place order.",
691                HyperliquidRejectCode::PerpMargin,
692            ),
693            (
694                "Post only order would have immediately matched, bbo was 1.23",
695                HyperliquidRejectCode::BadAloPx,
696            ),
697            (
698                "Some unknown error",
699                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
700            ),
701        ];
702
703        for (error_str, expected_code) in test_cases {
704            assert_eq!(
705                HyperliquidRejectCode::from_api_error(error_str),
706                expected_code
707            );
708        }
709    }
710
711    #[rstest]
712    fn test_reduce_only() {
713        let reduce_only = HyperliquidReduceOnly::new(true);
714
715        assert!(reduce_only.is_reduce_only());
716
717        let json = serde_json::to_string(&reduce_only).unwrap();
718        assert_eq!(json, "true");
719
720        let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
721        assert_eq!(parsed, reduce_only);
722    }
723
724    #[rstest]
725    fn test_order_status_conversion() {
726        // Test HyperliquidOrderStatus to OrderState conversion
727        assert_eq!(
728            OrderStatus::from(HyperliquidOrderStatus::Open),
729            OrderStatus::Accepted
730        );
731        assert_eq!(
732            OrderStatus::from(HyperliquidOrderStatus::Accepted),
733            OrderStatus::Accepted
734        );
735        assert_eq!(
736            OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
737            OrderStatus::PartiallyFilled
738        );
739        assert_eq!(
740            OrderStatus::from(HyperliquidOrderStatus::Filled),
741            OrderStatus::Filled
742        );
743        assert_eq!(
744            OrderStatus::from(HyperliquidOrderStatus::Canceled),
745            OrderStatus::Canceled
746        );
747        assert_eq!(
748            OrderStatus::from(HyperliquidOrderStatus::Cancelled),
749            OrderStatus::Canceled
750        );
751        assert_eq!(
752            OrderStatus::from(HyperliquidOrderStatus::Rejected),
753            OrderStatus::Rejected
754        );
755        assert_eq!(
756            OrderStatus::from(HyperliquidOrderStatus::Expired),
757            OrderStatus::Expired
758        );
759    }
760
761    #[rstest]
762    fn test_order_status_string_mapping() {
763        // Test direct string to OrderState conversion
764        assert_eq!(
765            hyperliquid_status_to_order_status("open"),
766            OrderStatus::Accepted
767        );
768        assert_eq!(
769            hyperliquid_status_to_order_status("accepted"),
770            OrderStatus::Accepted
771        );
772        assert_eq!(
773            hyperliquid_status_to_order_status("partially_filled"),
774            OrderStatus::PartiallyFilled
775        );
776        assert_eq!(
777            hyperliquid_status_to_order_status("filled"),
778            OrderStatus::Filled
779        );
780        assert_eq!(
781            hyperliquid_status_to_order_status("canceled"),
782            OrderStatus::Canceled
783        );
784        assert_eq!(
785            hyperliquid_status_to_order_status("cancelled"),
786            OrderStatus::Canceled
787        );
788        assert_eq!(
789            hyperliquid_status_to_order_status("rejected"),
790            OrderStatus::Rejected
791        );
792        assert_eq!(
793            hyperliquid_status_to_order_status("expired"),
794            OrderStatus::Expired
795        );
796        assert_eq!(
797            hyperliquid_status_to_order_status("unknown_status"),
798            OrderStatus::Rejected
799        );
800    }
801
802    // ========================================================================
803    // Conditional Order Tests
804    // ========================================================================
805
806    #[rstest]
807    fn test_hyperliquid_tpsl_serialization() {
808        let tp = HyperliquidTpSl::Tp;
809        let sl = HyperliquidTpSl::Sl;
810
811        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
812        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
813    }
814
815    #[rstest]
816    fn test_hyperliquid_tpsl_deserialization() {
817        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
818        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
819
820        assert_eq!(tp, HyperliquidTpSl::Tp);
821        assert_eq!(sl, HyperliquidTpSl::Sl);
822    }
823
824    #[rstest]
825    fn test_hyperliquid_trigger_price_type_serialization() {
826        let last = HyperliquidTriggerPriceType::Last;
827        let mark = HyperliquidTriggerPriceType::Mark;
828        let oracle = HyperliquidTriggerPriceType::Oracle;
829
830        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
831        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
832        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
833    }
834
835    #[rstest]
836    fn test_hyperliquid_trigger_price_type_to_nautilus() {
837        assert_eq!(
838            TriggerType::from(HyperliquidTriggerPriceType::Last),
839            TriggerType::LastPrice
840        );
841        assert_eq!(
842            TriggerType::from(HyperliquidTriggerPriceType::Mark),
843            TriggerType::MarkPrice
844        );
845        assert_eq!(
846            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
847            TriggerType::IndexPrice
848        );
849    }
850
851    #[rstest]
852    fn test_nautilus_trigger_type_to_hyperliquid() {
853        assert_eq!(
854            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
855            HyperliquidTriggerPriceType::Last
856        );
857        assert_eq!(
858            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
859            HyperliquidTriggerPriceType::Mark
860        );
861        assert_eq!(
862            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
863            HyperliquidTriggerPriceType::Oracle
864        );
865    }
866
867    #[rstest]
868    fn test_conditional_order_type_conversions() {
869        // Test all conditional order types
870        assert_eq!(
871            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
872            OrderType::StopMarket
873        );
874        assert_eq!(
875            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
876            OrderType::StopLimit
877        );
878        assert_eq!(
879            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
880            OrderType::MarketIfTouched
881        );
882        assert_eq!(
883            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
884            OrderType::LimitIfTouched
885        );
886        assert_eq!(
887            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
888            OrderType::TrailingStopMarket
889        );
890    }
891
892    // Tests for error parsing with real and simulated error messages
893    mod error_parsing_tests {
894        use super::*;
895
896        #[rstest]
897        fn test_parse_tick_size_error() {
898            let error = "Price must be divisible by tick size 0.01";
899            let code = HyperliquidRejectCode::from_api_error(error);
900            assert_eq!(code, HyperliquidRejectCode::Tick);
901        }
902
903        #[rstest]
904        fn test_parse_tick_size_error_case_insensitive() {
905            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
906            let code = HyperliquidRejectCode::from_api_error(error);
907            assert_eq!(code, HyperliquidRejectCode::Tick);
908        }
909
910        #[rstest]
911        fn test_parse_min_notional_perp() {
912            let error = "Order must have minimum value of $10";
913            let code = HyperliquidRejectCode::from_api_error(error);
914            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
915        }
916
917        #[rstest]
918        fn test_parse_min_notional_spot() {
919            let error = "Order must have minimum value of 10 USDC";
920            let code = HyperliquidRejectCode::from_api_error(error);
921            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
922        }
923
924        #[rstest]
925        fn test_parse_insufficient_margin() {
926            let error = "Insufficient margin to place order";
927            let code = HyperliquidRejectCode::from_api_error(error);
928            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
929        }
930
931        #[rstest]
932        fn test_parse_insufficient_margin_case_variations() {
933            let variations = vec![
934                "insufficient margin to place order",
935                "INSUFFICIENT MARGIN TO PLACE ORDER",
936                "  Insufficient margin to place order  ", // with whitespace
937            ];
938
939            for error in variations {
940                let code = HyperliquidRejectCode::from_api_error(error);
941                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
942            }
943        }
944
945        #[rstest]
946        fn test_parse_reduce_only_violation() {
947            let error = "Reduce only order would increase position";
948            let code = HyperliquidRejectCode::from_api_error(error);
949            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
950        }
951
952        #[rstest]
953        fn test_parse_reduce_only_with_hyphen() {
954            let error = "Reduce-only order would increase position";
955            let code = HyperliquidRejectCode::from_api_error(error);
956            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
957        }
958
959        #[rstest]
960        fn test_parse_post_only_match() {
961            let error = "Post only order would have immediately matched";
962            let code = HyperliquidRejectCode::from_api_error(error);
963            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
964        }
965
966        #[rstest]
967        fn test_parse_post_only_with_hyphen() {
968            let error = "Post-only order would have immediately matched";
969            let code = HyperliquidRejectCode::from_api_error(error);
970            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
971        }
972
973        #[rstest]
974        fn test_parse_ioc_no_match() {
975            let error = "Order could not immediately match";
976            let code = HyperliquidRejectCode::from_api_error(error);
977            assert_eq!(code, HyperliquidRejectCode::IocCancel);
978        }
979
980        #[rstest]
981        fn test_parse_invalid_trigger_price() {
982            let error = "Invalid TP/SL price";
983            let code = HyperliquidRejectCode::from_api_error(error);
984            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
985        }
986
987        #[rstest]
988        fn test_parse_no_liquidity() {
989            let error = "No liquidity available for market order";
990            let code = HyperliquidRejectCode::from_api_error(error);
991            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
992        }
993
994        #[rstest]
995        fn test_parse_position_increase_at_oi_cap() {
996            let error = "PositionIncreaseAtOpenInterestCap";
997            let code = HyperliquidRejectCode::from_api_error(error);
998            assert_eq!(
999                code,
1000                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1001            );
1002        }
1003
1004        #[rstest]
1005        fn test_parse_position_flip_at_oi_cap() {
1006            let error = "PositionFlipAtOpenInterestCap";
1007            let code = HyperliquidRejectCode::from_api_error(error);
1008            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1009        }
1010
1011        #[rstest]
1012        fn test_parse_too_aggressive_at_oi_cap() {
1013            let error = "TooAggressiveAtOpenInterestCap";
1014            let code = HyperliquidRejectCode::from_api_error(error);
1015            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1016        }
1017
1018        #[rstest]
1019        fn test_parse_open_interest_increase() {
1020            let error = "OpenInterestIncrease";
1021            let code = HyperliquidRejectCode::from_api_error(error);
1022            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1023        }
1024
1025        #[rstest]
1026        fn test_parse_insufficient_spot_balance() {
1027            let error = "Insufficient spot balance";
1028            let code = HyperliquidRejectCode::from_api_error(error);
1029            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1030        }
1031
1032        #[rstest]
1033        fn test_parse_oracle_error() {
1034            let error = "Oracle price unavailable";
1035            let code = HyperliquidRejectCode::from_api_error(error);
1036            assert_eq!(code, HyperliquidRejectCode::Oracle);
1037        }
1038
1039        #[rstest]
1040        fn test_parse_max_position() {
1041            let error = "Exceeds max position size";
1042            let code = HyperliquidRejectCode::from_api_error(error);
1043            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1044        }
1045
1046        #[rstest]
1047        fn test_parse_missing_order() {
1048            let error = "MissingOrder";
1049            let code = HyperliquidRejectCode::from_api_error(error);
1050            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1051        }
1052
1053        #[rstest]
1054        fn test_parse_unknown_error() {
1055            let error = "This is a completely new error message";
1056            let code = HyperliquidRejectCode::from_api_error(error);
1057            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1058
1059            // Verify the original message is preserved
1060            if let HyperliquidRejectCode::Unknown(msg) = code {
1061                assert_eq!(msg, error);
1062            }
1063        }
1064
1065        #[rstest]
1066        fn test_parse_empty_error() {
1067            let error = "";
1068            let code = HyperliquidRejectCode::from_api_error(error);
1069            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1070        }
1071
1072        #[rstest]
1073        fn test_parse_whitespace_only() {
1074            let error = "   ";
1075            let code = HyperliquidRejectCode::from_api_error(error);
1076            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1077        }
1078
1079        #[rstest]
1080        fn test_normalization_preserves_original_in_unknown() {
1081            let error = "  UNKNOWN ERROR MESSAGE  ";
1082            let code = HyperliquidRejectCode::from_api_error(error);
1083
1084            // Should be Unknown, and should contain original message (not normalized)
1085            if let HyperliquidRejectCode::Unknown(msg) = code {
1086                assert_eq!(msg, error);
1087            } else {
1088                panic!("Expected Unknown variant");
1089            }
1090        }
1091    }
1092
1093    #[rstest]
1094    fn test_conditional_order_type_round_trip() {
1095        assert_eq!(
1096            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1097            OrderType::TrailingStopLimit
1098        );
1099
1100        // Test reverse conversions
1101        assert_eq!(
1102            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1103            HyperliquidConditionalOrderType::StopMarket
1104        );
1105        assert_eq!(
1106            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1107            HyperliquidConditionalOrderType::StopLimit
1108        );
1109    }
1110
1111    #[rstest]
1112    fn test_trailing_offset_type_serialization() {
1113        let price = HyperliquidTrailingOffsetType::Price;
1114        let percentage = HyperliquidTrailingOffsetType::Percentage;
1115        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1116
1117        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1118        assert_eq!(
1119            serde_json::to_string(&percentage).unwrap(),
1120            r#""percentage""#
1121        );
1122        assert_eq!(
1123            serde_json::to_string(&basis_points).unwrap(),
1124            r#""basispoints""#
1125        );
1126    }
1127
1128    #[rstest]
1129    fn test_conditional_order_type_serialization() {
1130        assert_eq!(
1131            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1132            r#""STOP_MARKET""#
1133        );
1134        assert_eq!(
1135            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1136            r#""STOP_LIMIT""#
1137        );
1138        assert_eq!(
1139            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1140            r#""TAKE_PROFIT_MARKET""#
1141        );
1142        assert_eq!(
1143            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1144            r#""TAKE_PROFIT_LIMIT""#
1145        );
1146        assert_eq!(
1147            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1148            r#""TRAILING_STOP_MARKET""#
1149        );
1150        assert_eq!(
1151            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1152            r#""TRAILING_STOP_LIMIT""#
1153        );
1154    }
1155
1156    #[rstest]
1157    fn test_order_type_enum_coverage() {
1158        // Ensure all conditional order types roundtrip correctly
1159        let conditional_types = vec![
1160            HyperliquidConditionalOrderType::StopMarket,
1161            HyperliquidConditionalOrderType::StopLimit,
1162            HyperliquidConditionalOrderType::TakeProfitMarket,
1163            HyperliquidConditionalOrderType::TakeProfitLimit,
1164            HyperliquidConditionalOrderType::TrailingStopMarket,
1165            HyperliquidConditionalOrderType::TrailingStopLimit,
1166        ];
1167
1168        for cond_type in conditional_types {
1169            let order_type = OrderType::from(cond_type);
1170            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1171            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1172        }
1173    }
1174
1175    #[rstest]
1176    fn test_all_trigger_price_types() {
1177        let trigger_types = vec![
1178            HyperliquidTriggerPriceType::Last,
1179            HyperliquidTriggerPriceType::Mark,
1180            HyperliquidTriggerPriceType::Oracle,
1181        ];
1182
1183        for trigger_type in trigger_types {
1184            let nautilus_type = TriggerType::from(trigger_type);
1185            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1186            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1187        }
1188    }
1189}