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 {
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 Tick,
361 MinTradeNtl,
363 MinTradeSpotNtl,
365 PerpMargin,
367 ReduceOnly,
369 BadAloPx,
371 IocCancel,
373 BadTriggerPx,
375 MarketOrderNoLiquidity,
377 PositionIncreaseAtOpenInterestCap,
379 PositionFlipAtOpenInterestCap,
381 TooAggressiveAtOpenInterestCap,
383 OpenInterestIncrease,
385 InsufficientSpotBalance,
387 Oracle,
389 PerpMaxPosition,
391 MissingOrder,
393 Unknown(String),
395}
396
397impl HyperliquidRejectCode {
398 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 let normalized = error.trim().to_lowercase();
406
407 match normalized.as_str() {
408 s if s.contains("tick size") => HyperliquidRejectCode::Tick,
410
411 s if s.contains("minimum value of $10") => HyperliquidRejectCode::MinTradeNtl,
413 s if s.contains("minimum value of 10") => HyperliquidRejectCode::MinTradeSpotNtl,
414
415 s if s.contains("insufficient margin") => HyperliquidRejectCode::PerpMargin,
417
418 s if s.contains("reduce only order would increase")
420 || s.contains("reduce-only order would increase") =>
421 {
422 HyperliquidRejectCode::ReduceOnly
423 }
424
425 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 s if s.contains("could not immediately match") => HyperliquidRejectCode::IocCancel,
434
435 s if s.contains("invalid tp/sl price") => HyperliquidRejectCode::BadTriggerPx,
437
438 s if s.contains("no liquidity available for market order") => {
440 HyperliquidRejectCode::MarketOrderNoLiquidity
441 }
442
443 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 s if s.contains("insufficient spot balance") => {
458 HyperliquidRejectCode::InsufficientSpotBalance
459 }
460
461 s if s.contains("oracle") => HyperliquidRejectCode::Oracle,
463
464 s if s.contains("max position") => HyperliquidRejectCode::PerpMaxPosition,
466
467 s if s.contains("missingorder") => HyperliquidRejectCode::MissingOrder,
469
470 _ => {
472 tracing::warn!(
473 "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
474 error );
476 HyperliquidRejectCode::Unknown(error.to_string())
477 }
478 }
479 }
480
481 #[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#[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 Open,
514 Accepted,
516 PartiallyFilled,
518 Filled,
520 Canceled,
522 Cancelled,
524 Rejected,
526 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#[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 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 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 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 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 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 #[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 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 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 ", ];
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 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 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 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 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}