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 { Self::Taker } else { Self::Maker }
349    }
350}
351
352#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
353#[serde(untagged)]
354pub enum HyperliquidRejectCode {
355    /// Price must be divisible by tick size.
356    Tick,
357    /// Order must have minimum value of $10.
358    MinTradeNtl,
359    /// Order must have minimum value of 10 {quote_token}.
360    MinTradeSpotNtl,
361    /// Insufficient margin to place order.
362    PerpMargin,
363    /// Reduce only order would increase position.
364    ReduceOnly,
365    /// Post only order would have immediately matched.
366    BadAloPx,
367    /// Order could not immediately match.
368    IocCancel,
369    /// Invalid TP/SL price.
370    BadTriggerPx,
371    /// No liquidity available for market order.
372    MarketOrderNoLiquidity,
373    /// Position increase at open interest cap.
374    PositionIncreaseAtOpenInterestCap,
375    /// Position flip at open interest cap.
376    PositionFlipAtOpenInterestCap,
377    /// Too aggressive at open interest cap.
378    TooAggressiveAtOpenInterestCap,
379    /// Open interest increase.
380    OpenInterestIncrease,
381    /// Insufficient spot balance.
382    InsufficientSpotBalance,
383    /// Oracle issue.
384    Oracle,
385    /// Perp max position.
386    PerpMaxPosition,
387    /// Missing order.
388    MissingOrder,
389    /// Unknown reject reason with raw error message.
390    Unknown(String),
391}
392
393impl HyperliquidRejectCode {
394    /// Parse reject code from Hyperliquid API error message.
395    pub fn from_api_error(error_message: &str) -> Self {
396        Self::from_error_string_internal(error_message)
397    }
398
399    fn from_error_string_internal(error: &str) -> Self {
400        // Normalize: trim whitespace and convert to lowercase for robust matching
401        let normalized = error.trim().to_lowercase();
402
403        match normalized.as_str() {
404            // Tick size validation errors
405            s if s.contains("tick size") => Self::Tick,
406
407            // Minimum notional value errors (perp: $10, spot: 10 USDC)
408            s if s.contains("minimum value of $10") => Self::MinTradeNtl,
409            s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
410
411            // Margin errors
412            s if s.contains("insufficient margin") => Self::PerpMargin,
413
414            // Reduce-only order violations
415            s if s.contains("reduce only order would increase")
416                || s.contains("reduce-only order would increase") =>
417            {
418                Self::ReduceOnly
419            }
420
421            // Post-only order matching errors
422            s if s.contains("post only order would have immediately matched")
423                || s.contains("post-only order would have immediately matched") =>
424            {
425                Self::BadAloPx
426            }
427
428            // IOC (Immediate-or-Cancel) order errors
429            s if s.contains("could not immediately match") => Self::IocCancel,
430
431            // TP/SL trigger price errors
432            s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
433
434            // Market order liquidity errors
435            s if s.contains("no liquidity available for market order") => {
436                Self::MarketOrderNoLiquidity
437            }
438
439            // Open interest cap errors (various types)
440            // Note: These patterns are case-insensitive due to normalization
441            s if s.contains("positionincreaseatopeninterestcap") => {
442                Self::PositionIncreaseAtOpenInterestCap
443            }
444            s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
445            s if s.contains("tooaggressiveatopeninterestcap") => {
446                Self::TooAggressiveAtOpenInterestCap
447            }
448            s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
449
450            // Spot balance errors
451            s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
452
453            // Oracle errors
454            s if s.contains("oracle") => Self::Oracle,
455
456            // Position size limit errors
457            s if s.contains("max position") => Self::PerpMaxPosition,
458
459            // Missing order errors (cancel/modify non-existent order)
460            s if s.contains("missingorder") => Self::MissingOrder,
461
462            // Unknown error - log for monitoring and return with original message
463            _ => {
464                tracing::warn!(
465                    "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
466                    error // Use original error, not normalized
467                );
468                Self::Unknown(error.to_string())
469            }
470        }
471    }
472
473    /// Parses reject code from error string.
474    ///
475    /// **Deprecated**: This method uses substring matching which is fragile and not robust.
476    /// Use `from_api_error()` instead, which provides a migration path for structured error handling.
477    #[deprecated(
478        since = "0.50.0",
479        note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
480    )]
481    pub fn from_error_string(error: &str) -> Self {
482        Self::from_error_string_internal(error)
483    }
484}
485
486/// Represents Hyperliquid order status from API responses
487#[derive(
488    Copy,
489    Clone,
490    Debug,
491    Display,
492    PartialEq,
493    Eq,
494    Hash,
495    AsRefStr,
496    EnumIter,
497    EnumString,
498    Serialize,
499    Deserialize,
500)]
501#[serde(rename_all = "snake_case")]
502#[strum(serialize_all = "snake_case")]
503pub enum HyperliquidOrderStatus {
504    /// Order has been accepted and is open
505    Open,
506    /// Order has been accepted and is open (alternative representation)
507    Accepted,
508    /// Order has been partially filled
509    PartiallyFilled,
510    /// Order has been completely filled
511    Filled,
512    /// Order has been canceled
513    Canceled,
514    /// Order has been canceled (alternative spelling)
515    Cancelled,
516    /// Order was rejected by the exchange
517    Rejected,
518    /// Order has expired
519    Expired,
520}
521
522impl From<HyperliquidOrderStatus> for OrderStatus {
523    fn from(status: HyperliquidOrderStatus) -> Self {
524        match status {
525            HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
526            HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
527            HyperliquidOrderStatus::Filled => Self::Filled,
528            HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
529            HyperliquidOrderStatus::Rejected => Self::Rejected,
530            HyperliquidOrderStatus::Expired => Self::Expired,
531        }
532    }
533}
534
535pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
536    match status {
537        "open" | "accepted" => OrderStatus::Accepted,
538        "partially_filled" => OrderStatus::PartiallyFilled,
539        "filled" => OrderStatus::Filled,
540        "canceled" | "cancelled" => OrderStatus::Canceled,
541        "rejected" => OrderStatus::Rejected,
542        "expired" => OrderStatus::Expired,
543        _ => OrderStatus::Rejected,
544    }
545}
546
547////////////////////////////////////////////////////////////////////////////////
548// Tests
549////////////////////////////////////////////////////////////////////////////////
550
551#[cfg(test)]
552mod tests {
553    use nautilus_model::enums::{OrderType, TriggerType};
554    use rstest::rstest;
555    use serde_json;
556
557    use super::*;
558
559    #[rstest]
560    fn test_side_serde() {
561        let buy_side = HyperliquidSide::Buy;
562        let sell_side = HyperliquidSide::Sell;
563
564        assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
565        assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
566
567        assert_eq!(
568            serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
569            HyperliquidSide::Buy
570        );
571        assert_eq!(
572            serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
573            HyperliquidSide::Sell
574        );
575    }
576
577    #[rstest]
578    fn test_side_from_order_side() {
579        // Test conversion from OrderSide to HyperliquidSide
580        assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
581        assert_eq!(
582            HyperliquidSide::from(OrderSide::Sell),
583            HyperliquidSide::Sell
584        );
585    }
586
587    #[rstest]
588    fn test_order_side_from_hyperliquid_side() {
589        // Test conversion from HyperliquidSide to OrderSide
590        assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
591        assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
592    }
593
594    #[rstest]
595    fn test_aggressor_side_from_hyperliquid_side() {
596        // Test conversion from HyperliquidSide to AggressorSide
597        assert_eq!(
598            AggressorSide::from(HyperliquidSide::Buy),
599            AggressorSide::Buyer
600        );
601        assert_eq!(
602            AggressorSide::from(HyperliquidSide::Sell),
603            AggressorSide::Seller
604        );
605    }
606
607    #[rstest]
608    fn test_time_in_force_serde() {
609        let test_cases = [
610            (HyperliquidTimeInForce::Alo, "\"Alo\""),
611            (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
612            (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
613        ];
614
615        for (tif, expected_json) in test_cases {
616            assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
617            assert_eq!(
618                serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
619                tif
620            );
621        }
622    }
623
624    #[rstest]
625    fn test_liquidity_flag_from_crossed() {
626        assert_eq!(
627            HyperliquidLiquidityFlag::from(true),
628            HyperliquidLiquidityFlag::Taker
629        );
630        assert_eq!(
631            HyperliquidLiquidityFlag::from(false),
632            HyperliquidLiquidityFlag::Maker
633        );
634    }
635
636    #[rstest]
637    #[allow(deprecated)]
638    fn test_reject_code_from_error_string() {
639        let test_cases = [
640            (
641                "Price must be divisible by tick size.",
642                HyperliquidRejectCode::Tick,
643            ),
644            (
645                "Order must have minimum value of $10.",
646                HyperliquidRejectCode::MinTradeNtl,
647            ),
648            (
649                "Insufficient margin to place order.",
650                HyperliquidRejectCode::PerpMargin,
651            ),
652            (
653                "Post only order would have immediately matched, bbo was 1.23",
654                HyperliquidRejectCode::BadAloPx,
655            ),
656            (
657                "Some unknown error",
658                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
659            ),
660        ];
661
662        for (error_str, expected_code) in test_cases {
663            assert_eq!(
664                HyperliquidRejectCode::from_error_string(error_str),
665                expected_code
666            );
667        }
668    }
669
670    #[rstest]
671    fn test_reject_code_from_api_error() {
672        let test_cases = [
673            (
674                "Price must be divisible by tick size.",
675                HyperliquidRejectCode::Tick,
676            ),
677            (
678                "Order must have minimum value of $10.",
679                HyperliquidRejectCode::MinTradeNtl,
680            ),
681            (
682                "Insufficient margin to place order.",
683                HyperliquidRejectCode::PerpMargin,
684            ),
685            (
686                "Post only order would have immediately matched, bbo was 1.23",
687                HyperliquidRejectCode::BadAloPx,
688            ),
689            (
690                "Some unknown error",
691                HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
692            ),
693        ];
694
695        for (error_str, expected_code) in test_cases {
696            assert_eq!(
697                HyperliquidRejectCode::from_api_error(error_str),
698                expected_code
699            );
700        }
701    }
702
703    #[rstest]
704    fn test_reduce_only() {
705        let reduce_only = HyperliquidReduceOnly::new(true);
706
707        assert!(reduce_only.is_reduce_only());
708
709        let json = serde_json::to_string(&reduce_only).unwrap();
710        assert_eq!(json, "true");
711
712        let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
713        assert_eq!(parsed, reduce_only);
714    }
715
716    #[rstest]
717    fn test_order_status_conversion() {
718        // Test HyperliquidOrderStatus to OrderState conversion
719        assert_eq!(
720            OrderStatus::from(HyperliquidOrderStatus::Open),
721            OrderStatus::Accepted
722        );
723        assert_eq!(
724            OrderStatus::from(HyperliquidOrderStatus::Accepted),
725            OrderStatus::Accepted
726        );
727        assert_eq!(
728            OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
729            OrderStatus::PartiallyFilled
730        );
731        assert_eq!(
732            OrderStatus::from(HyperliquidOrderStatus::Filled),
733            OrderStatus::Filled
734        );
735        assert_eq!(
736            OrderStatus::from(HyperliquidOrderStatus::Canceled),
737            OrderStatus::Canceled
738        );
739        assert_eq!(
740            OrderStatus::from(HyperliquidOrderStatus::Cancelled),
741            OrderStatus::Canceled
742        );
743        assert_eq!(
744            OrderStatus::from(HyperliquidOrderStatus::Rejected),
745            OrderStatus::Rejected
746        );
747        assert_eq!(
748            OrderStatus::from(HyperliquidOrderStatus::Expired),
749            OrderStatus::Expired
750        );
751    }
752
753    #[rstest]
754    fn test_order_status_string_mapping() {
755        // Test direct string to OrderState conversion
756        assert_eq!(
757            hyperliquid_status_to_order_status("open"),
758            OrderStatus::Accepted
759        );
760        assert_eq!(
761            hyperliquid_status_to_order_status("accepted"),
762            OrderStatus::Accepted
763        );
764        assert_eq!(
765            hyperliquid_status_to_order_status("partially_filled"),
766            OrderStatus::PartiallyFilled
767        );
768        assert_eq!(
769            hyperliquid_status_to_order_status("filled"),
770            OrderStatus::Filled
771        );
772        assert_eq!(
773            hyperliquid_status_to_order_status("canceled"),
774            OrderStatus::Canceled
775        );
776        assert_eq!(
777            hyperliquid_status_to_order_status("cancelled"),
778            OrderStatus::Canceled
779        );
780        assert_eq!(
781            hyperliquid_status_to_order_status("rejected"),
782            OrderStatus::Rejected
783        );
784        assert_eq!(
785            hyperliquid_status_to_order_status("expired"),
786            OrderStatus::Expired
787        );
788        assert_eq!(
789            hyperliquid_status_to_order_status("unknown_status"),
790            OrderStatus::Rejected
791        );
792    }
793
794    // ========================================================================
795    // Conditional Order Tests
796    // ========================================================================
797
798    #[rstest]
799    fn test_hyperliquid_tpsl_serialization() {
800        let tp = HyperliquidTpSl::Tp;
801        let sl = HyperliquidTpSl::Sl;
802
803        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
804        assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
805    }
806
807    #[rstest]
808    fn test_hyperliquid_tpsl_deserialization() {
809        let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
810        let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
811
812        assert_eq!(tp, HyperliquidTpSl::Tp);
813        assert_eq!(sl, HyperliquidTpSl::Sl);
814    }
815
816    #[rstest]
817    fn test_hyperliquid_trigger_price_type_serialization() {
818        let last = HyperliquidTriggerPriceType::Last;
819        let mark = HyperliquidTriggerPriceType::Mark;
820        let oracle = HyperliquidTriggerPriceType::Oracle;
821
822        assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
823        assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
824        assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
825    }
826
827    #[rstest]
828    fn test_hyperliquid_trigger_price_type_to_nautilus() {
829        assert_eq!(
830            TriggerType::from(HyperliquidTriggerPriceType::Last),
831            TriggerType::LastPrice
832        );
833        assert_eq!(
834            TriggerType::from(HyperliquidTriggerPriceType::Mark),
835            TriggerType::MarkPrice
836        );
837        assert_eq!(
838            TriggerType::from(HyperliquidTriggerPriceType::Oracle),
839            TriggerType::IndexPrice
840        );
841    }
842
843    #[rstest]
844    fn test_nautilus_trigger_type_to_hyperliquid() {
845        assert_eq!(
846            HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
847            HyperliquidTriggerPriceType::Last
848        );
849        assert_eq!(
850            HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
851            HyperliquidTriggerPriceType::Mark
852        );
853        assert_eq!(
854            HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
855            HyperliquidTriggerPriceType::Oracle
856        );
857    }
858
859    #[rstest]
860    fn test_conditional_order_type_conversions() {
861        // Test all conditional order types
862        assert_eq!(
863            OrderType::from(HyperliquidConditionalOrderType::StopMarket),
864            OrderType::StopMarket
865        );
866        assert_eq!(
867            OrderType::from(HyperliquidConditionalOrderType::StopLimit),
868            OrderType::StopLimit
869        );
870        assert_eq!(
871            OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
872            OrderType::MarketIfTouched
873        );
874        assert_eq!(
875            OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
876            OrderType::LimitIfTouched
877        );
878        assert_eq!(
879            OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
880            OrderType::TrailingStopMarket
881        );
882    }
883
884    // Tests for error parsing with real and simulated error messages
885    mod error_parsing_tests {
886        use super::*;
887
888        #[rstest]
889        fn test_parse_tick_size_error() {
890            let error = "Price must be divisible by tick size 0.01";
891            let code = HyperliquidRejectCode::from_api_error(error);
892            assert_eq!(code, HyperliquidRejectCode::Tick);
893        }
894
895        #[rstest]
896        fn test_parse_tick_size_error_case_insensitive() {
897            let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
898            let code = HyperliquidRejectCode::from_api_error(error);
899            assert_eq!(code, HyperliquidRejectCode::Tick);
900        }
901
902        #[rstest]
903        fn test_parse_min_notional_perp() {
904            let error = "Order must have minimum value of $10";
905            let code = HyperliquidRejectCode::from_api_error(error);
906            assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
907        }
908
909        #[rstest]
910        fn test_parse_min_notional_spot() {
911            let error = "Order must have minimum value of 10 USDC";
912            let code = HyperliquidRejectCode::from_api_error(error);
913            assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
914        }
915
916        #[rstest]
917        fn test_parse_insufficient_margin() {
918            let error = "Insufficient margin to place order";
919            let code = HyperliquidRejectCode::from_api_error(error);
920            assert_eq!(code, HyperliquidRejectCode::PerpMargin);
921        }
922
923        #[rstest]
924        fn test_parse_insufficient_margin_case_variations() {
925            let variations = vec![
926                "insufficient margin to place order",
927                "INSUFFICIENT MARGIN TO PLACE ORDER",
928                "  Insufficient margin to place order  ", // with whitespace
929            ];
930
931            for error in variations {
932                let code = HyperliquidRejectCode::from_api_error(error);
933                assert_eq!(code, HyperliquidRejectCode::PerpMargin);
934            }
935        }
936
937        #[rstest]
938        fn test_parse_reduce_only_violation() {
939            let error = "Reduce only order would increase position";
940            let code = HyperliquidRejectCode::from_api_error(error);
941            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
942        }
943
944        #[rstest]
945        fn test_parse_reduce_only_with_hyphen() {
946            let error = "Reduce-only order would increase position";
947            let code = HyperliquidRejectCode::from_api_error(error);
948            assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
949        }
950
951        #[rstest]
952        fn test_parse_post_only_match() {
953            let error = "Post only order would have immediately matched";
954            let code = HyperliquidRejectCode::from_api_error(error);
955            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
956        }
957
958        #[rstest]
959        fn test_parse_post_only_with_hyphen() {
960            let error = "Post-only order would have immediately matched";
961            let code = HyperliquidRejectCode::from_api_error(error);
962            assert_eq!(code, HyperliquidRejectCode::BadAloPx);
963        }
964
965        #[rstest]
966        fn test_parse_ioc_no_match() {
967            let error = "Order could not immediately match";
968            let code = HyperliquidRejectCode::from_api_error(error);
969            assert_eq!(code, HyperliquidRejectCode::IocCancel);
970        }
971
972        #[rstest]
973        fn test_parse_invalid_trigger_price() {
974            let error = "Invalid TP/SL price";
975            let code = HyperliquidRejectCode::from_api_error(error);
976            assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
977        }
978
979        #[rstest]
980        fn test_parse_no_liquidity() {
981            let error = "No liquidity available for market order";
982            let code = HyperliquidRejectCode::from_api_error(error);
983            assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
984        }
985
986        #[rstest]
987        fn test_parse_position_increase_at_oi_cap() {
988            let error = "PositionIncreaseAtOpenInterestCap";
989            let code = HyperliquidRejectCode::from_api_error(error);
990            assert_eq!(
991                code,
992                HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
993            );
994        }
995
996        #[rstest]
997        fn test_parse_position_flip_at_oi_cap() {
998            let error = "PositionFlipAtOpenInterestCap";
999            let code = HyperliquidRejectCode::from_api_error(error);
1000            assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1001        }
1002
1003        #[rstest]
1004        fn test_parse_too_aggressive_at_oi_cap() {
1005            let error = "TooAggressiveAtOpenInterestCap";
1006            let code = HyperliquidRejectCode::from_api_error(error);
1007            assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1008        }
1009
1010        #[rstest]
1011        fn test_parse_open_interest_increase() {
1012            let error = "OpenInterestIncrease";
1013            let code = HyperliquidRejectCode::from_api_error(error);
1014            assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1015        }
1016
1017        #[rstest]
1018        fn test_parse_insufficient_spot_balance() {
1019            let error = "Insufficient spot balance";
1020            let code = HyperliquidRejectCode::from_api_error(error);
1021            assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1022        }
1023
1024        #[rstest]
1025        fn test_parse_oracle_error() {
1026            let error = "Oracle price unavailable";
1027            let code = HyperliquidRejectCode::from_api_error(error);
1028            assert_eq!(code, HyperliquidRejectCode::Oracle);
1029        }
1030
1031        #[rstest]
1032        fn test_parse_max_position() {
1033            let error = "Exceeds max position size";
1034            let code = HyperliquidRejectCode::from_api_error(error);
1035            assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1036        }
1037
1038        #[rstest]
1039        fn test_parse_missing_order() {
1040            let error = "MissingOrder";
1041            let code = HyperliquidRejectCode::from_api_error(error);
1042            assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1043        }
1044
1045        #[rstest]
1046        fn test_parse_unknown_error() {
1047            let error = "This is a completely new error message";
1048            let code = HyperliquidRejectCode::from_api_error(error);
1049            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1050
1051            // Verify the original message is preserved
1052            if let HyperliquidRejectCode::Unknown(msg) = code {
1053                assert_eq!(msg, error);
1054            }
1055        }
1056
1057        #[rstest]
1058        fn test_parse_empty_error() {
1059            let error = "";
1060            let code = HyperliquidRejectCode::from_api_error(error);
1061            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1062        }
1063
1064        #[rstest]
1065        fn test_parse_whitespace_only() {
1066            let error = "   ";
1067            let code = HyperliquidRejectCode::from_api_error(error);
1068            assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1069        }
1070
1071        #[rstest]
1072        fn test_normalization_preserves_original_in_unknown() {
1073            let error = "  UNKNOWN ERROR MESSAGE  ";
1074            let code = HyperliquidRejectCode::from_api_error(error);
1075
1076            // Should be Unknown, and should contain original message (not normalized)
1077            if let HyperliquidRejectCode::Unknown(msg) = code {
1078                assert_eq!(msg, error);
1079            } else {
1080                panic!("Expected Unknown variant");
1081            }
1082        }
1083    }
1084
1085    #[rstest]
1086    fn test_conditional_order_type_round_trip() {
1087        assert_eq!(
1088            OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1089            OrderType::TrailingStopLimit
1090        );
1091
1092        // Test reverse conversions
1093        assert_eq!(
1094            HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1095            HyperliquidConditionalOrderType::StopMarket
1096        );
1097        assert_eq!(
1098            HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1099            HyperliquidConditionalOrderType::StopLimit
1100        );
1101    }
1102
1103    #[rstest]
1104    fn test_trailing_offset_type_serialization() {
1105        let price = HyperliquidTrailingOffsetType::Price;
1106        let percentage = HyperliquidTrailingOffsetType::Percentage;
1107        let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1108
1109        assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1110        assert_eq!(
1111            serde_json::to_string(&percentage).unwrap(),
1112            r#""percentage""#
1113        );
1114        assert_eq!(
1115            serde_json::to_string(&basis_points).unwrap(),
1116            r#""basispoints""#
1117        );
1118    }
1119
1120    #[rstest]
1121    fn test_conditional_order_type_serialization() {
1122        assert_eq!(
1123            serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1124            r#""STOP_MARKET""#
1125        );
1126        assert_eq!(
1127            serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1128            r#""STOP_LIMIT""#
1129        );
1130        assert_eq!(
1131            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1132            r#""TAKE_PROFIT_MARKET""#
1133        );
1134        assert_eq!(
1135            serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1136            r#""TAKE_PROFIT_LIMIT""#
1137        );
1138        assert_eq!(
1139            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1140            r#""TRAILING_STOP_MARKET""#
1141        );
1142        assert_eq!(
1143            serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1144            r#""TRAILING_STOP_LIMIT""#
1145        );
1146    }
1147
1148    #[rstest]
1149    fn test_order_type_enum_coverage() {
1150        // Ensure all conditional order types roundtrip correctly
1151        let conditional_types = vec![
1152            HyperliquidConditionalOrderType::StopMarket,
1153            HyperliquidConditionalOrderType::StopLimit,
1154            HyperliquidConditionalOrderType::TakeProfitMarket,
1155            HyperliquidConditionalOrderType::TakeProfitLimit,
1156            HyperliquidConditionalOrderType::TrailingStopMarket,
1157            HyperliquidConditionalOrderType::TrailingStopLimit,
1158        ];
1159
1160        for cond_type in conditional_types {
1161            let order_type = OrderType::from(cond_type);
1162            let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1163            assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1164        }
1165    }
1166
1167    #[rstest]
1168    fn test_all_trigger_price_types() {
1169        let trigger_types = vec![
1170            HyperliquidTriggerPriceType::Last,
1171            HyperliquidTriggerPriceType::Mark,
1172            HyperliquidTriggerPriceType::Oracle,
1173        ];
1174
1175        for trigger_type in trigger_types {
1176            let nautilus_type = TriggerType::from(trigger_type);
1177            let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1178            assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1179        }
1180    }
1181}