1use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus};
17use serde::{Deserialize, Serialize};
18use strum::{AsRefStr, Display, EnumIter, EnumString};
19
20#[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#[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 Alo,
92 Ioc,
94 Gtc,
96}
97
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100#[serde(tag = "type", rename_all = "lowercase")]
101pub enum HyperliquidOrderType {
102 #[serde(rename = "limit")]
104 Limit { tif: HyperliquidTimeInForce },
105
106 #[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#[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 Tp,
141 Sl,
143}
144
145#[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,
174 Mark,
176 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, }
198 }
199}
200
201#[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 StopMarket,
228 StopLimit,
230 TakeProfitMarket,
232 TakeProfitLimit,
234 TrailingStopMarket,
236 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#[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 Price,
296 Percentage,
298 #[serde(rename = "basispoints")]
300 #[strum(serialize = "basispoints")]
301 BasisPoints,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[serde(transparent)]
307pub struct HyperliquidReduceOnly(pub bool);
308
309impl HyperliquidReduceOnly {
310 pub fn new(reduce_only: bool) -> Self {
312 Self(reduce_only)
313 }
314
315 pub fn is_reduce_only(&self) -> bool {
317 self.0
318 }
319}
320
321#[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 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 Tick,
357 MinTradeNtl,
359 MinTradeSpotNtl,
361 PerpMargin,
363 ReduceOnly,
365 BadAloPx,
367 IocCancel,
369 BadTriggerPx,
371 MarketOrderNoLiquidity,
373 PositionIncreaseAtOpenInterestCap,
375 PositionFlipAtOpenInterestCap,
377 TooAggressiveAtOpenInterestCap,
379 OpenInterestIncrease,
381 InsufficientSpotBalance,
383 Oracle,
385 PerpMaxPosition,
387 MissingOrder,
389 Unknown(String),
391}
392
393impl HyperliquidRejectCode {
394 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 let normalized = error.trim().to_lowercase();
402
403 match normalized.as_str() {
404 s if s.contains("tick size") => Self::Tick,
406
407 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
409 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
410
411 s if s.contains("insufficient margin") => Self::PerpMargin,
413
414 s if s.contains("reduce only order would increase")
416 || s.contains("reduce-only order would increase") =>
417 {
418 Self::ReduceOnly
419 }
420
421 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 s if s.contains("could not immediately match") => Self::IocCancel,
430
431 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
433
434 s if s.contains("no liquidity available for market order") => {
436 Self::MarketOrderNoLiquidity
437 }
438
439 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 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
452
453 s if s.contains("oracle") => Self::Oracle,
455
456 s if s.contains("max position") => Self::PerpMaxPosition,
458
459 s if s.contains("missingorder") => Self::MissingOrder,
461
462 _ => {
464 tracing::warn!(
465 "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
466 error );
468 Self::Unknown(error.to_string())
469 }
470 }
471 }
472
473 #[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#[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 Open,
506 Accepted,
508 PartiallyFilled,
510 Filled,
512 Canceled,
514 Cancelled,
516 Rejected,
518 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#[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 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 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 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 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 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 #[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 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 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 ", ];
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 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 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 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 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}