1use std::{fmt::Display, str::FromStr};
17
18use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus, OrderType, TriggerType};
19use serde::{Deserialize, Serialize};
20use strum::{AsRefStr, Display, EnumIter, EnumString};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum HyperliquidBarInterval {
24 #[serde(rename = "1m")]
25 OneMinute,
26 #[serde(rename = "3m")]
27 ThreeMinutes,
28 #[serde(rename = "5m")]
29 FiveMinutes,
30 #[serde(rename = "15m")]
31 FifteenMinutes,
32 #[serde(rename = "30m")]
33 ThirtyMinutes,
34 #[serde(rename = "1h")]
35 OneHour,
36 #[serde(rename = "2h")]
37 TwoHours,
38 #[serde(rename = "4h")]
39 FourHours,
40 #[serde(rename = "8h")]
41 EightHours,
42 #[serde(rename = "12h")]
43 TwelveHours,
44 #[serde(rename = "1d")]
45 OneDay,
46 #[serde(rename = "3d")]
47 ThreeDays,
48 #[serde(rename = "1w")]
49 OneWeek,
50 #[serde(rename = "1M")]
51 OneMonth,
52}
53
54impl HyperliquidBarInterval {
55 pub fn as_str(&self) -> &'static str {
56 match self {
57 Self::OneMinute => "1m",
58 Self::ThreeMinutes => "3m",
59 Self::FiveMinutes => "5m",
60 Self::FifteenMinutes => "15m",
61 Self::ThirtyMinutes => "30m",
62 Self::OneHour => "1h",
63 Self::TwoHours => "2h",
64 Self::FourHours => "4h",
65 Self::EightHours => "8h",
66 Self::TwelveHours => "12h",
67 Self::OneDay => "1d",
68 Self::ThreeDays => "3d",
69 Self::OneWeek => "1w",
70 Self::OneMonth => "1M",
71 }
72 }
73}
74
75impl FromStr for HyperliquidBarInterval {
76 type Err = anyhow::Error;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 match s {
80 "1m" => Ok(Self::OneMinute),
81 "3m" => Ok(Self::ThreeMinutes),
82 "5m" => Ok(Self::FiveMinutes),
83 "15m" => Ok(Self::FifteenMinutes),
84 "30m" => Ok(Self::ThirtyMinutes),
85 "1h" => Ok(Self::OneHour),
86 "2h" => Ok(Self::TwoHours),
87 "4h" => Ok(Self::FourHours),
88 "8h" => Ok(Self::EightHours),
89 "12h" => Ok(Self::TwelveHours),
90 "1d" => Ok(Self::OneDay),
91 "3d" => Ok(Self::ThreeDays),
92 "1w" => Ok(Self::OneWeek),
93 "1M" => Ok(Self::OneMonth),
94 _ => anyhow::bail!("Invalid Hyperliquid bar interval: {s}"),
95 }
96 }
97}
98
99impl Display for HyperliquidBarInterval {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(f, "{}", self.as_str())
102 }
103}
104
105#[derive(
107 Copy,
108 Clone,
109 Debug,
110 Display,
111 PartialEq,
112 Eq,
113 Hash,
114 AsRefStr,
115 EnumIter,
116 EnumString,
117 Serialize,
118 Deserialize,
119)]
120#[serde(rename_all = "UPPERCASE")]
121#[strum(serialize_all = "UPPERCASE")]
122pub enum HyperliquidSide {
123 #[serde(rename = "B")]
124 Buy,
125 #[serde(rename = "A")]
126 Sell,
127}
128
129impl From<OrderSide> for HyperliquidSide {
130 fn from(value: OrderSide) -> Self {
131 match value {
132 OrderSide::Buy => Self::Buy,
133 OrderSide::Sell => Self::Sell,
134 _ => panic!("Invalid `OrderSide`"),
135 }
136 }
137}
138
139impl From<HyperliquidSide> for OrderSide {
140 fn from(value: HyperliquidSide) -> Self {
141 match value {
142 HyperliquidSide::Buy => Self::Buy,
143 HyperliquidSide::Sell => Self::Sell,
144 }
145 }
146}
147
148impl From<HyperliquidSide> for AggressorSide {
149 fn from(value: HyperliquidSide) -> Self {
150 match value {
151 HyperliquidSide::Buy => Self::Buyer,
152 HyperliquidSide::Sell => Self::Seller,
153 }
154 }
155}
156
157#[derive(
159 Copy,
160 Clone,
161 Debug,
162 Display,
163 PartialEq,
164 Eq,
165 Hash,
166 AsRefStr,
167 EnumIter,
168 EnumString,
169 Serialize,
170 Deserialize,
171)]
172#[serde(rename_all = "PascalCase")]
173#[strum(serialize_all = "PascalCase")]
174pub enum HyperliquidTimeInForce {
175 Alo,
177 Ioc,
179 Gtc,
181}
182
183#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185#[serde(tag = "type", rename_all = "lowercase")]
186pub enum HyperliquidOrderType {
187 #[serde(rename = "limit")]
189 Limit { tif: HyperliquidTimeInForce },
190
191 #[serde(rename = "trigger")]
193 Trigger {
194 #[serde(rename = "isMarket")]
195 is_market: bool,
196 #[serde(rename = "triggerPx")]
197 trigger_px: String,
198 tpsl: HyperliquidTpSl,
199 },
200}
201
202#[derive(
204 Copy,
205 Clone,
206 Debug,
207 Display,
208 PartialEq,
209 Eq,
210 Hash,
211 AsRefStr,
212 EnumIter,
213 EnumString,
214 Serialize,
215 Deserialize,
216)]
217#[cfg_attr(
218 feature = "python",
219 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
220)]
221#[serde(rename_all = "lowercase")]
222#[strum(serialize_all = "lowercase")]
223pub enum HyperliquidTpSl {
224 Tp,
226 Sl,
228}
229
230#[derive(
237 Copy,
238 Clone,
239 Debug,
240 Display,
241 PartialEq,
242 Eq,
243 Hash,
244 AsRefStr,
245 EnumIter,
246 EnumString,
247 Serialize,
248 Deserialize,
249)]
250#[cfg_attr(
251 feature = "python",
252 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
253)]
254#[serde(rename_all = "lowercase")]
255#[strum(serialize_all = "lowercase")]
256pub enum HyperliquidTriggerPriceType {
257 Last,
259 Mark,
261 Oracle,
263}
264
265impl From<HyperliquidTriggerPriceType> for TriggerType {
266 fn from(value: HyperliquidTriggerPriceType) -> Self {
267 match value {
268 HyperliquidTriggerPriceType::Last => Self::LastPrice,
269 HyperliquidTriggerPriceType::Mark => Self::MarkPrice,
270 HyperliquidTriggerPriceType::Oracle => Self::IndexPrice,
271 }
272 }
273}
274
275impl From<TriggerType> for HyperliquidTriggerPriceType {
276 fn from(value: TriggerType) -> Self {
277 match value {
278 TriggerType::LastPrice => Self::Last,
279 TriggerType::MarkPrice => Self::Mark,
280 TriggerType::IndexPrice => Self::Oracle,
281 _ => Self::Last, }
283 }
284}
285
286#[derive(
291 Copy,
292 Clone,
293 Debug,
294 Display,
295 PartialEq,
296 Eq,
297 Hash,
298 AsRefStr,
299 EnumIter,
300 EnumString,
301 Serialize,
302 Deserialize,
303)]
304#[cfg_attr(
305 feature = "python",
306 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
307)]
308#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
309#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
310pub enum HyperliquidConditionalOrderType {
311 StopMarket,
313 StopLimit,
315 TakeProfitMarket,
317 TakeProfitLimit,
319 TrailingStopMarket,
321 TrailingStopLimit,
323}
324
325impl From<HyperliquidConditionalOrderType> for OrderType {
326 fn from(value: HyperliquidConditionalOrderType) -> Self {
327 match value {
328 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
329 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
330 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
331 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
332 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
333 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
334 }
335 }
336}
337
338impl From<OrderType> for HyperliquidConditionalOrderType {
339 fn from(value: OrderType) -> Self {
340 match value {
341 OrderType::StopMarket => Self::StopMarket,
342 OrderType::StopLimit => Self::StopLimit,
343 OrderType::MarketIfTouched => Self::TakeProfitMarket,
344 OrderType::LimitIfTouched => Self::TakeProfitLimit,
345 OrderType::TrailingStopMarket => Self::TrailingStopMarket,
346 OrderType::TrailingStopLimit => Self::TrailingStopLimit,
347 _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
348 }
349 }
350}
351
352#[derive(
359 Copy,
360 Clone,
361 Debug,
362 Display,
363 PartialEq,
364 Eq,
365 Hash,
366 AsRefStr,
367 EnumIter,
368 EnumString,
369 Serialize,
370 Deserialize,
371)]
372#[cfg_attr(
373 feature = "python",
374 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
375)]
376#[serde(rename_all = "lowercase")]
377#[strum(serialize_all = "lowercase")]
378pub enum HyperliquidTrailingOffsetType {
379 Price,
381 Percentage,
383 #[serde(rename = "basispoints")]
385 #[strum(serialize = "basispoints")]
386 BasisPoints,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(transparent)]
392pub struct HyperliquidReduceOnly(pub bool);
393
394impl HyperliquidReduceOnly {
395 pub fn new(reduce_only: bool) -> Self {
397 Self(reduce_only)
398 }
399
400 pub fn is_reduce_only(&self) -> bool {
402 self.0
403 }
404}
405
406#[derive(
408 Copy,
409 Clone,
410 Debug,
411 Display,
412 PartialEq,
413 Eq,
414 Hash,
415 AsRefStr,
416 EnumIter,
417 EnumString,
418 Serialize,
419 Deserialize,
420)]
421#[serde(rename_all = "lowercase")]
422#[strum(serialize_all = "lowercase")]
423pub enum HyperliquidLiquidityFlag {
424 Maker,
425 Taker,
426}
427
428impl From<bool> for HyperliquidLiquidityFlag {
429 fn from(crossed: bool) -> Self {
433 if crossed { Self::Taker } else { Self::Maker }
434 }
435}
436
437#[derive(
439 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
440)]
441#[serde(rename_all = "lowercase")]
442#[strum(serialize_all = "lowercase")]
443pub enum HyperliquidLiquidationMethod {
444 Market,
445 Backstop,
446}
447
448#[derive(
450 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
451)]
452#[serde(rename_all = "camelCase")]
453#[strum(serialize_all = "camelCase")]
454pub enum HyperliquidPositionType {
455 OneWay,
456}
457
458#[derive(
460 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
461)]
462#[serde(rename_all = "lowercase")]
463#[strum(serialize_all = "lowercase")]
464pub enum HyperliquidTwapStatus {
465 Activated,
466 Terminated,
467 Finished,
468 Error,
469}
470
471#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
472#[serde(untagged)]
473pub enum HyperliquidRejectCode {
474 Tick,
476 MinTradeNtl,
478 MinTradeSpotNtl,
480 PerpMargin,
482 ReduceOnly,
484 BadAloPx,
486 IocCancel,
488 BadTriggerPx,
490 MarketOrderNoLiquidity,
492 PositionIncreaseAtOpenInterestCap,
494 PositionFlipAtOpenInterestCap,
496 TooAggressiveAtOpenInterestCap,
498 OpenInterestIncrease,
500 InsufficientSpotBalance,
502 Oracle,
504 PerpMaxPosition,
506 MissingOrder,
508 Unknown(String),
510}
511
512impl HyperliquidRejectCode {
513 pub fn from_api_error(error_message: &str) -> Self {
515 Self::from_error_string_internal(error_message)
516 }
517
518 fn from_error_string_internal(error: &str) -> Self {
519 let normalized = error.trim().to_lowercase();
521
522 match normalized.as_str() {
523 s if s.contains("tick size") => Self::Tick,
525
526 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
528 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
529
530 s if s.contains("insufficient margin") => Self::PerpMargin,
532
533 s if s.contains("reduce only order would increase")
535 || s.contains("reduce-only order would increase") =>
536 {
537 Self::ReduceOnly
538 }
539
540 s if s.contains("post only order would have immediately matched")
542 || s.contains("post-only order would have immediately matched") =>
543 {
544 Self::BadAloPx
545 }
546
547 s if s.contains("could not immediately match") => Self::IocCancel,
549
550 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
552
553 s if s.contains("no liquidity available for market order") => {
555 Self::MarketOrderNoLiquidity
556 }
557
558 s if s.contains("positionincreaseatopeninterestcap") => {
561 Self::PositionIncreaseAtOpenInterestCap
562 }
563 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
564 s if s.contains("tooaggressiveatopeninterestcap") => {
565 Self::TooAggressiveAtOpenInterestCap
566 }
567 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
568
569 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
571
572 s if s.contains("oracle") => Self::Oracle,
574
575 s if s.contains("max position") => Self::PerpMaxPosition,
577
578 s if s.contains("missingorder") => Self::MissingOrder,
580
581 _ => {
583 log::warn!(
584 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
586 Self::Unknown(error.to_string())
587 }
588 }
589 }
590
591 #[deprecated(
596 since = "0.50.0",
597 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
598 )]
599 pub fn from_error_string(error: &str) -> Self {
600 Self::from_error_string_internal(error)
601 }
602}
603
604#[derive(
606 Copy,
607 Clone,
608 Debug,
609 Display,
610 PartialEq,
611 Eq,
612 Hash,
613 AsRefStr,
614 EnumIter,
615 EnumString,
616 Serialize,
617 Deserialize,
618)]
619#[serde(rename_all = "snake_case")]
620#[strum(serialize_all = "snake_case")]
621pub enum HyperliquidOrderStatus {
622 Open,
624 Accepted,
626 PartiallyFilled,
628 Filled,
630 Canceled,
632 Cancelled,
634 Rejected,
636 Expired,
638}
639
640impl From<HyperliquidOrderStatus> for OrderStatus {
641 fn from(status: HyperliquidOrderStatus) -> Self {
642 match status {
643 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
644 HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
645 HyperliquidOrderStatus::Filled => Self::Filled,
646 HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
647 HyperliquidOrderStatus::Rejected => Self::Rejected,
648 HyperliquidOrderStatus::Expired => Self::Expired,
649 }
650 }
651}
652
653pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
654 match status {
655 "open" | "accepted" => OrderStatus::Accepted,
656 "partially_filled" => OrderStatus::PartiallyFilled,
657 "filled" => OrderStatus::Filled,
658 "canceled" | "cancelled" => OrderStatus::Canceled,
659 "rejected" => OrderStatus::Rejected,
660 "expired" => OrderStatus::Expired,
661 _ => OrderStatus::Rejected,
662 }
663}
664
665#[derive(
676 Copy,
677 Clone,
678 Debug,
679 Display,
680 PartialEq,
681 Eq,
682 Hash,
683 AsRefStr,
684 EnumIter,
685 EnumString,
686 Serialize,
687 Deserialize,
688)]
689#[serde(rename_all = "PascalCase")]
690#[strum(serialize_all = "PascalCase")]
691pub enum HyperliquidFillDirection {
692 #[serde(rename = "Open Long")]
694 #[strum(serialize = "Open Long")]
695 OpenLong,
696 #[serde(rename = "Open Short")]
698 #[strum(serialize = "Open Short")]
699 OpenShort,
700 #[serde(rename = "Close Long")]
702 #[strum(serialize = "Close Long")]
703 CloseLong,
704 #[serde(rename = "Close Short")]
706 #[strum(serialize = "Close Short")]
707 CloseShort,
708 Sell,
710}
711
712#[derive(
716 Copy,
717 Clone,
718 Debug,
719 Display,
720 PartialEq,
721 Eq,
722 Hash,
723 AsRefStr,
724 EnumIter,
725 EnumString,
726 Serialize,
727 Deserialize,
728)]
729#[serde(rename_all = "camelCase")]
730#[strum(serialize_all = "camelCase")]
731pub enum HyperliquidInfoRequestType {
732 Meta,
734 SpotMeta,
736 MetaAndAssetCtxs,
738 SpotMetaAndAssetCtxs,
740 L2Book,
742 UserFills,
744 OrderStatus,
746 OpenOrders,
748 FrontendOpenOrders,
750 ClearinghouseState,
752 CandleSnapshot,
754}
755
756impl HyperliquidInfoRequestType {
757 pub fn as_str(&self) -> &'static str {
758 match self {
759 Self::Meta => "meta",
760 Self::SpotMeta => "spotMeta",
761 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
762 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
763 Self::L2Book => "l2Book",
764 Self::UserFills => "userFills",
765 Self::OrderStatus => "orderStatus",
766 Self::OpenOrders => "openOrders",
767 Self::FrontendOpenOrders => "frontendOpenOrders",
768 Self::ClearinghouseState => "clearinghouseState",
769 Self::CandleSnapshot => "candleSnapshot",
770 }
771 }
772}
773
774#[derive(
776 Copy,
777 Clone,
778 Debug,
779 Display,
780 PartialEq,
781 Eq,
782 Hash,
783 AsRefStr,
784 EnumIter,
785 EnumString,
786 Serialize,
787 Deserialize,
788)]
789#[cfg_attr(
790 feature = "python",
791 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
792)]
793#[serde(rename_all = "UPPERCASE")]
794#[strum(serialize_all = "UPPERCASE")]
795pub enum HyperliquidProductType {
796 Perp,
798 Spot,
800}
801
802impl HyperliquidProductType {
803 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
809 if symbol.ends_with("-PERP") {
810 Ok(Self::Perp)
811 } else if symbol.ends_with("-SPOT") {
812 Ok(Self::Spot)
813 } else {
814 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
815 }
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use nautilus_model::enums::{OrderType, TriggerType};
822 use rstest::rstest;
823 use serde_json;
824
825 use super::*;
826
827 #[rstest]
828 fn test_side_serde() {
829 let buy_side = HyperliquidSide::Buy;
830 let sell_side = HyperliquidSide::Sell;
831
832 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
833 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
834
835 assert_eq!(
836 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
837 HyperliquidSide::Buy
838 );
839 assert_eq!(
840 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
841 HyperliquidSide::Sell
842 );
843 }
844
845 #[rstest]
846 fn test_side_from_order_side() {
847 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
849 assert_eq!(
850 HyperliquidSide::from(OrderSide::Sell),
851 HyperliquidSide::Sell
852 );
853 }
854
855 #[rstest]
856 fn test_order_side_from_hyperliquid_side() {
857 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
859 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
860 }
861
862 #[rstest]
863 fn test_aggressor_side_from_hyperliquid_side() {
864 assert_eq!(
866 AggressorSide::from(HyperliquidSide::Buy),
867 AggressorSide::Buyer
868 );
869 assert_eq!(
870 AggressorSide::from(HyperliquidSide::Sell),
871 AggressorSide::Seller
872 );
873 }
874
875 #[rstest]
876 fn test_time_in_force_serde() {
877 let test_cases = [
878 (HyperliquidTimeInForce::Alo, "\"Alo\""),
879 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
880 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
881 ];
882
883 for (tif, expected_json) in test_cases {
884 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
885 assert_eq!(
886 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
887 tif
888 );
889 }
890 }
891
892 #[rstest]
893 fn test_liquidity_flag_from_crossed() {
894 assert_eq!(
895 HyperliquidLiquidityFlag::from(true),
896 HyperliquidLiquidityFlag::Taker
897 );
898 assert_eq!(
899 HyperliquidLiquidityFlag::from(false),
900 HyperliquidLiquidityFlag::Maker
901 );
902 }
903
904 #[rstest]
905 #[allow(deprecated)]
906 fn test_reject_code_from_error_string() {
907 let test_cases = [
908 (
909 "Price must be divisible by tick size.",
910 HyperliquidRejectCode::Tick,
911 ),
912 (
913 "Order must have minimum value of $10.",
914 HyperliquidRejectCode::MinTradeNtl,
915 ),
916 (
917 "Insufficient margin to place order.",
918 HyperliquidRejectCode::PerpMargin,
919 ),
920 (
921 "Post only order would have immediately matched, bbo was 1.23",
922 HyperliquidRejectCode::BadAloPx,
923 ),
924 (
925 "Some unknown error",
926 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
927 ),
928 ];
929
930 for (error_str, expected_code) in test_cases {
931 assert_eq!(
932 HyperliquidRejectCode::from_error_string(error_str),
933 expected_code
934 );
935 }
936 }
937
938 #[rstest]
939 fn test_reject_code_from_api_error() {
940 let test_cases = [
941 (
942 "Price must be divisible by tick size.",
943 HyperliquidRejectCode::Tick,
944 ),
945 (
946 "Order must have minimum value of $10.",
947 HyperliquidRejectCode::MinTradeNtl,
948 ),
949 (
950 "Insufficient margin to place order.",
951 HyperliquidRejectCode::PerpMargin,
952 ),
953 (
954 "Post only order would have immediately matched, bbo was 1.23",
955 HyperliquidRejectCode::BadAloPx,
956 ),
957 (
958 "Some unknown error",
959 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
960 ),
961 ];
962
963 for (error_str, expected_code) in test_cases {
964 assert_eq!(
965 HyperliquidRejectCode::from_api_error(error_str),
966 expected_code
967 );
968 }
969 }
970
971 #[rstest]
972 fn test_reduce_only() {
973 let reduce_only = HyperliquidReduceOnly::new(true);
974
975 assert!(reduce_only.is_reduce_only());
976
977 let json = serde_json::to_string(&reduce_only).unwrap();
978 assert_eq!(json, "true");
979
980 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
981 assert_eq!(parsed, reduce_only);
982 }
983
984 #[rstest]
985 fn test_order_status_conversion() {
986 assert_eq!(
988 OrderStatus::from(HyperliquidOrderStatus::Open),
989 OrderStatus::Accepted
990 );
991 assert_eq!(
992 OrderStatus::from(HyperliquidOrderStatus::Accepted),
993 OrderStatus::Accepted
994 );
995 assert_eq!(
996 OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
997 OrderStatus::PartiallyFilled
998 );
999 assert_eq!(
1000 OrderStatus::from(HyperliquidOrderStatus::Filled),
1001 OrderStatus::Filled
1002 );
1003 assert_eq!(
1004 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1005 OrderStatus::Canceled
1006 );
1007 assert_eq!(
1008 OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1009 OrderStatus::Canceled
1010 );
1011 assert_eq!(
1012 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1013 OrderStatus::Rejected
1014 );
1015 assert_eq!(
1016 OrderStatus::from(HyperliquidOrderStatus::Expired),
1017 OrderStatus::Expired
1018 );
1019 }
1020
1021 #[rstest]
1022 fn test_order_status_string_mapping() {
1023 assert_eq!(
1025 hyperliquid_status_to_order_status("open"),
1026 OrderStatus::Accepted
1027 );
1028 assert_eq!(
1029 hyperliquid_status_to_order_status("accepted"),
1030 OrderStatus::Accepted
1031 );
1032 assert_eq!(
1033 hyperliquid_status_to_order_status("partially_filled"),
1034 OrderStatus::PartiallyFilled
1035 );
1036 assert_eq!(
1037 hyperliquid_status_to_order_status("filled"),
1038 OrderStatus::Filled
1039 );
1040 assert_eq!(
1041 hyperliquid_status_to_order_status("canceled"),
1042 OrderStatus::Canceled
1043 );
1044 assert_eq!(
1045 hyperliquid_status_to_order_status("cancelled"),
1046 OrderStatus::Canceled
1047 );
1048 assert_eq!(
1049 hyperliquid_status_to_order_status("rejected"),
1050 OrderStatus::Rejected
1051 );
1052 assert_eq!(
1053 hyperliquid_status_to_order_status("expired"),
1054 OrderStatus::Expired
1055 );
1056 assert_eq!(
1057 hyperliquid_status_to_order_status("unknown_status"),
1058 OrderStatus::Rejected
1059 );
1060 }
1061
1062 #[rstest]
1063 fn test_hyperliquid_tpsl_serialization() {
1064 let tp = HyperliquidTpSl::Tp;
1065 let sl = HyperliquidTpSl::Sl;
1066
1067 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1068 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1069 }
1070
1071 #[rstest]
1072 fn test_hyperliquid_tpsl_deserialization() {
1073 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1074 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1075
1076 assert_eq!(tp, HyperliquidTpSl::Tp);
1077 assert_eq!(sl, HyperliquidTpSl::Sl);
1078 }
1079
1080 #[rstest]
1081 fn test_hyperliquid_trigger_price_type_serialization() {
1082 let last = HyperliquidTriggerPriceType::Last;
1083 let mark = HyperliquidTriggerPriceType::Mark;
1084 let oracle = HyperliquidTriggerPriceType::Oracle;
1085
1086 assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1087 assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1088 assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1089 }
1090
1091 #[rstest]
1092 fn test_hyperliquid_trigger_price_type_to_nautilus() {
1093 assert_eq!(
1094 TriggerType::from(HyperliquidTriggerPriceType::Last),
1095 TriggerType::LastPrice
1096 );
1097 assert_eq!(
1098 TriggerType::from(HyperliquidTriggerPriceType::Mark),
1099 TriggerType::MarkPrice
1100 );
1101 assert_eq!(
1102 TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1103 TriggerType::IndexPrice
1104 );
1105 }
1106
1107 #[rstest]
1108 fn test_nautilus_trigger_type_to_hyperliquid() {
1109 assert_eq!(
1110 HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1111 HyperliquidTriggerPriceType::Last
1112 );
1113 assert_eq!(
1114 HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1115 HyperliquidTriggerPriceType::Mark
1116 );
1117 assert_eq!(
1118 HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1119 HyperliquidTriggerPriceType::Oracle
1120 );
1121 }
1122
1123 #[rstest]
1124 fn test_conditional_order_type_conversions() {
1125 assert_eq!(
1127 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1128 OrderType::StopMarket
1129 );
1130 assert_eq!(
1131 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1132 OrderType::StopLimit
1133 );
1134 assert_eq!(
1135 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1136 OrderType::MarketIfTouched
1137 );
1138 assert_eq!(
1139 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1140 OrderType::LimitIfTouched
1141 );
1142 assert_eq!(
1143 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1144 OrderType::TrailingStopMarket
1145 );
1146 }
1147
1148 mod error_parsing_tests {
1150 use super::*;
1151
1152 #[rstest]
1153 fn test_parse_tick_size_error() {
1154 let error = "Price must be divisible by tick size 0.01";
1155 let code = HyperliquidRejectCode::from_api_error(error);
1156 assert_eq!(code, HyperliquidRejectCode::Tick);
1157 }
1158
1159 #[rstest]
1160 fn test_parse_tick_size_error_case_insensitive() {
1161 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1162 let code = HyperliquidRejectCode::from_api_error(error);
1163 assert_eq!(code, HyperliquidRejectCode::Tick);
1164 }
1165
1166 #[rstest]
1167 fn test_parse_min_notional_perp() {
1168 let error = "Order must have minimum value of $10";
1169 let code = HyperliquidRejectCode::from_api_error(error);
1170 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1171 }
1172
1173 #[rstest]
1174 fn test_parse_min_notional_spot() {
1175 let error = "Order must have minimum value of 10 USDC";
1176 let code = HyperliquidRejectCode::from_api_error(error);
1177 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1178 }
1179
1180 #[rstest]
1181 fn test_parse_insufficient_margin() {
1182 let error = "Insufficient margin to place order";
1183 let code = HyperliquidRejectCode::from_api_error(error);
1184 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1185 }
1186
1187 #[rstest]
1188 fn test_parse_insufficient_margin_case_variations() {
1189 let variations = vec![
1190 "insufficient margin to place order",
1191 "INSUFFICIENT MARGIN TO PLACE ORDER",
1192 " Insufficient margin to place order ", ];
1194
1195 for error in variations {
1196 let code = HyperliquidRejectCode::from_api_error(error);
1197 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1198 }
1199 }
1200
1201 #[rstest]
1202 fn test_parse_reduce_only_violation() {
1203 let error = "Reduce only order would increase position";
1204 let code = HyperliquidRejectCode::from_api_error(error);
1205 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1206 }
1207
1208 #[rstest]
1209 fn test_parse_reduce_only_with_hyphen() {
1210 let error = "Reduce-only order would increase position";
1211 let code = HyperliquidRejectCode::from_api_error(error);
1212 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1213 }
1214
1215 #[rstest]
1216 fn test_parse_post_only_match() {
1217 let error = "Post only order would have immediately matched";
1218 let code = HyperliquidRejectCode::from_api_error(error);
1219 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1220 }
1221
1222 #[rstest]
1223 fn test_parse_post_only_with_hyphen() {
1224 let error = "Post-only order would have immediately matched";
1225 let code = HyperliquidRejectCode::from_api_error(error);
1226 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1227 }
1228
1229 #[rstest]
1230 fn test_parse_ioc_no_match() {
1231 let error = "Order could not immediately match";
1232 let code = HyperliquidRejectCode::from_api_error(error);
1233 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1234 }
1235
1236 #[rstest]
1237 fn test_parse_invalid_trigger_price() {
1238 let error = "Invalid TP/SL price";
1239 let code = HyperliquidRejectCode::from_api_error(error);
1240 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1241 }
1242
1243 #[rstest]
1244 fn test_parse_no_liquidity() {
1245 let error = "No liquidity available for market order";
1246 let code = HyperliquidRejectCode::from_api_error(error);
1247 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1248 }
1249
1250 #[rstest]
1251 fn test_parse_position_increase_at_oi_cap() {
1252 let error = "PositionIncreaseAtOpenInterestCap";
1253 let code = HyperliquidRejectCode::from_api_error(error);
1254 assert_eq!(
1255 code,
1256 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1257 );
1258 }
1259
1260 #[rstest]
1261 fn test_parse_position_flip_at_oi_cap() {
1262 let error = "PositionFlipAtOpenInterestCap";
1263 let code = HyperliquidRejectCode::from_api_error(error);
1264 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1265 }
1266
1267 #[rstest]
1268 fn test_parse_too_aggressive_at_oi_cap() {
1269 let error = "TooAggressiveAtOpenInterestCap";
1270 let code = HyperliquidRejectCode::from_api_error(error);
1271 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1272 }
1273
1274 #[rstest]
1275 fn test_parse_open_interest_increase() {
1276 let error = "OpenInterestIncrease";
1277 let code = HyperliquidRejectCode::from_api_error(error);
1278 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1279 }
1280
1281 #[rstest]
1282 fn test_parse_insufficient_spot_balance() {
1283 let error = "Insufficient spot balance";
1284 let code = HyperliquidRejectCode::from_api_error(error);
1285 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1286 }
1287
1288 #[rstest]
1289 fn test_parse_oracle_error() {
1290 let error = "Oracle price unavailable";
1291 let code = HyperliquidRejectCode::from_api_error(error);
1292 assert_eq!(code, HyperliquidRejectCode::Oracle);
1293 }
1294
1295 #[rstest]
1296 fn test_parse_max_position() {
1297 let error = "Exceeds max position size";
1298 let code = HyperliquidRejectCode::from_api_error(error);
1299 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1300 }
1301
1302 #[rstest]
1303 fn test_parse_missing_order() {
1304 let error = "MissingOrder";
1305 let code = HyperliquidRejectCode::from_api_error(error);
1306 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1307 }
1308
1309 #[rstest]
1310 fn test_parse_unknown_error() {
1311 let error = "This is a completely new error message";
1312 let code = HyperliquidRejectCode::from_api_error(error);
1313 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1314
1315 if let HyperliquidRejectCode::Unknown(msg) = code {
1317 assert_eq!(msg, error);
1318 }
1319 }
1320
1321 #[rstest]
1322 fn test_parse_empty_error() {
1323 let error = "";
1324 let code = HyperliquidRejectCode::from_api_error(error);
1325 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1326 }
1327
1328 #[rstest]
1329 fn test_parse_whitespace_only() {
1330 let error = " ";
1331 let code = HyperliquidRejectCode::from_api_error(error);
1332 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1333 }
1334
1335 #[rstest]
1336 fn test_normalization_preserves_original_in_unknown() {
1337 let error = " UNKNOWN ERROR MESSAGE ";
1338 let code = HyperliquidRejectCode::from_api_error(error);
1339
1340 if let HyperliquidRejectCode::Unknown(msg) = code {
1342 assert_eq!(msg, error);
1343 } else {
1344 panic!("Expected Unknown variant");
1345 }
1346 }
1347 }
1348
1349 #[rstest]
1350 fn test_conditional_order_type_round_trip() {
1351 assert_eq!(
1352 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1353 OrderType::TrailingStopLimit
1354 );
1355
1356 assert_eq!(
1358 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1359 HyperliquidConditionalOrderType::StopMarket
1360 );
1361 assert_eq!(
1362 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1363 HyperliquidConditionalOrderType::StopLimit
1364 );
1365 }
1366
1367 #[rstest]
1368 fn test_trailing_offset_type_serialization() {
1369 let price = HyperliquidTrailingOffsetType::Price;
1370 let percentage = HyperliquidTrailingOffsetType::Percentage;
1371 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1372
1373 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1374 assert_eq!(
1375 serde_json::to_string(&percentage).unwrap(),
1376 r#""percentage""#
1377 );
1378 assert_eq!(
1379 serde_json::to_string(&basis_points).unwrap(),
1380 r#""basispoints""#
1381 );
1382 }
1383
1384 #[rstest]
1385 fn test_conditional_order_type_serialization() {
1386 assert_eq!(
1387 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1388 r#""STOP_MARKET""#
1389 );
1390 assert_eq!(
1391 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1392 r#""STOP_LIMIT""#
1393 );
1394 assert_eq!(
1395 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1396 r#""TAKE_PROFIT_MARKET""#
1397 );
1398 assert_eq!(
1399 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1400 r#""TAKE_PROFIT_LIMIT""#
1401 );
1402 assert_eq!(
1403 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1404 r#""TRAILING_STOP_MARKET""#
1405 );
1406 assert_eq!(
1407 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1408 r#""TRAILING_STOP_LIMIT""#
1409 );
1410 }
1411
1412 #[rstest]
1413 fn test_order_type_enum_coverage() {
1414 let conditional_types = vec![
1416 HyperliquidConditionalOrderType::StopMarket,
1417 HyperliquidConditionalOrderType::StopLimit,
1418 HyperliquidConditionalOrderType::TakeProfitMarket,
1419 HyperliquidConditionalOrderType::TakeProfitLimit,
1420 HyperliquidConditionalOrderType::TrailingStopMarket,
1421 HyperliquidConditionalOrderType::TrailingStopLimit,
1422 ];
1423
1424 for cond_type in conditional_types {
1425 let order_type = OrderType::from(cond_type);
1426 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1427 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1428 }
1429 }
1430
1431 #[rstest]
1432 fn test_all_trigger_price_types() {
1433 let trigger_types = vec![
1434 HyperliquidTriggerPriceType::Last,
1435 HyperliquidTriggerPriceType::Mark,
1436 HyperliquidTriggerPriceType::Oracle,
1437 ];
1438
1439 for trigger_type in trigger_types {
1440 let nautilus_type = TriggerType::from(trigger_type);
1441 let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1442 assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1443 }
1444 }
1445}