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 tracing::warn!(
584 "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
585 error );
587 Self::Unknown(error.to_string())
588 }
589 }
590 }
591
592 #[deprecated(
597 since = "0.50.0",
598 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
599 )]
600 pub fn from_error_string(error: &str) -> Self {
601 Self::from_error_string_internal(error)
602 }
603}
604
605#[derive(
607 Copy,
608 Clone,
609 Debug,
610 Display,
611 PartialEq,
612 Eq,
613 Hash,
614 AsRefStr,
615 EnumIter,
616 EnumString,
617 Serialize,
618 Deserialize,
619)]
620#[serde(rename_all = "snake_case")]
621#[strum(serialize_all = "snake_case")]
622pub enum HyperliquidOrderStatus {
623 Open,
625 Accepted,
627 PartiallyFilled,
629 Filled,
631 Canceled,
633 Cancelled,
635 Rejected,
637 Expired,
639}
640
641impl From<HyperliquidOrderStatus> for OrderStatus {
642 fn from(status: HyperliquidOrderStatus) -> Self {
643 match status {
644 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
645 HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
646 HyperliquidOrderStatus::Filled => Self::Filled,
647 HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
648 HyperliquidOrderStatus::Rejected => Self::Rejected,
649 HyperliquidOrderStatus::Expired => Self::Expired,
650 }
651 }
652}
653
654pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
655 match status {
656 "open" | "accepted" => OrderStatus::Accepted,
657 "partially_filled" => OrderStatus::PartiallyFilled,
658 "filled" => OrderStatus::Filled,
659 "canceled" | "cancelled" => OrderStatus::Canceled,
660 "rejected" => OrderStatus::Rejected,
661 "expired" => OrderStatus::Expired,
662 _ => OrderStatus::Rejected,
663 }
664}
665
666#[derive(
677 Copy,
678 Clone,
679 Debug,
680 Display,
681 PartialEq,
682 Eq,
683 Hash,
684 AsRefStr,
685 EnumIter,
686 EnumString,
687 Serialize,
688 Deserialize,
689)]
690#[serde(rename_all = "PascalCase")]
691#[strum(serialize_all = "PascalCase")]
692pub enum HyperliquidFillDirection {
693 #[serde(rename = "Open Long")]
695 #[strum(serialize = "Open Long")]
696 OpenLong,
697 #[serde(rename = "Open Short")]
699 #[strum(serialize = "Open Short")]
700 OpenShort,
701 #[serde(rename = "Close Long")]
703 #[strum(serialize = "Close Long")]
704 CloseLong,
705 #[serde(rename = "Close Short")]
707 #[strum(serialize = "Close Short")]
708 CloseShort,
709 Sell,
711}
712
713#[derive(
717 Copy,
718 Clone,
719 Debug,
720 Display,
721 PartialEq,
722 Eq,
723 Hash,
724 AsRefStr,
725 EnumIter,
726 EnumString,
727 Serialize,
728 Deserialize,
729)]
730#[serde(rename_all = "camelCase")]
731#[strum(serialize_all = "camelCase")]
732pub enum HyperliquidInfoRequestType {
733 Meta,
735 SpotMeta,
737 MetaAndAssetCtxs,
739 SpotMetaAndAssetCtxs,
741 L2Book,
743 UserFills,
745 OrderStatus,
747 OpenOrders,
749 FrontendOpenOrders,
751 ClearinghouseState,
753 CandleSnapshot,
755}
756
757impl HyperliquidInfoRequestType {
758 pub fn as_str(&self) -> &'static str {
759 match self {
760 Self::Meta => "meta",
761 Self::SpotMeta => "spotMeta",
762 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
763 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
764 Self::L2Book => "l2Book",
765 Self::UserFills => "userFills",
766 Self::OrderStatus => "orderStatus",
767 Self::OpenOrders => "openOrders",
768 Self::FrontendOpenOrders => "frontendOpenOrders",
769 Self::ClearinghouseState => "clearinghouseState",
770 Self::CandleSnapshot => "candleSnapshot",
771 }
772 }
773}
774
775#[derive(
777 Copy,
778 Clone,
779 Debug,
780 Display,
781 PartialEq,
782 Eq,
783 Hash,
784 AsRefStr,
785 EnumIter,
786 EnumString,
787 Serialize,
788 Deserialize,
789)]
790#[cfg_attr(
791 feature = "python",
792 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
793)]
794#[serde(rename_all = "UPPERCASE")]
795#[strum(serialize_all = "UPPERCASE")]
796pub enum HyperliquidProductType {
797 Perp,
799 Spot,
801}
802
803impl HyperliquidProductType {
804 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
810 if symbol.ends_with("-PERP") {
811 Ok(Self::Perp)
812 } else if symbol.ends_with("-SPOT") {
813 Ok(Self::Spot)
814 } else {
815 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
816 }
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use nautilus_model::enums::{OrderType, TriggerType};
823 use rstest::rstest;
824 use serde_json;
825
826 use super::*;
827
828 #[rstest]
829 fn test_side_serde() {
830 let buy_side = HyperliquidSide::Buy;
831 let sell_side = HyperliquidSide::Sell;
832
833 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
834 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
835
836 assert_eq!(
837 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
838 HyperliquidSide::Buy
839 );
840 assert_eq!(
841 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
842 HyperliquidSide::Sell
843 );
844 }
845
846 #[rstest]
847 fn test_side_from_order_side() {
848 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
850 assert_eq!(
851 HyperliquidSide::from(OrderSide::Sell),
852 HyperliquidSide::Sell
853 );
854 }
855
856 #[rstest]
857 fn test_order_side_from_hyperliquid_side() {
858 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
860 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
861 }
862
863 #[rstest]
864 fn test_aggressor_side_from_hyperliquid_side() {
865 assert_eq!(
867 AggressorSide::from(HyperliquidSide::Buy),
868 AggressorSide::Buyer
869 );
870 assert_eq!(
871 AggressorSide::from(HyperliquidSide::Sell),
872 AggressorSide::Seller
873 );
874 }
875
876 #[rstest]
877 fn test_time_in_force_serde() {
878 let test_cases = [
879 (HyperliquidTimeInForce::Alo, "\"Alo\""),
880 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
881 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
882 ];
883
884 for (tif, expected_json) in test_cases {
885 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
886 assert_eq!(
887 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
888 tif
889 );
890 }
891 }
892
893 #[rstest]
894 fn test_liquidity_flag_from_crossed() {
895 assert_eq!(
896 HyperliquidLiquidityFlag::from(true),
897 HyperliquidLiquidityFlag::Taker
898 );
899 assert_eq!(
900 HyperliquidLiquidityFlag::from(false),
901 HyperliquidLiquidityFlag::Maker
902 );
903 }
904
905 #[rstest]
906 #[allow(deprecated)]
907 fn test_reject_code_from_error_string() {
908 let test_cases = [
909 (
910 "Price must be divisible by tick size.",
911 HyperliquidRejectCode::Tick,
912 ),
913 (
914 "Order must have minimum value of $10.",
915 HyperliquidRejectCode::MinTradeNtl,
916 ),
917 (
918 "Insufficient margin to place order.",
919 HyperliquidRejectCode::PerpMargin,
920 ),
921 (
922 "Post only order would have immediately matched, bbo was 1.23",
923 HyperliquidRejectCode::BadAloPx,
924 ),
925 (
926 "Some unknown error",
927 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
928 ),
929 ];
930
931 for (error_str, expected_code) in test_cases {
932 assert_eq!(
933 HyperliquidRejectCode::from_error_string(error_str),
934 expected_code
935 );
936 }
937 }
938
939 #[rstest]
940 fn test_reject_code_from_api_error() {
941 let test_cases = [
942 (
943 "Price must be divisible by tick size.",
944 HyperliquidRejectCode::Tick,
945 ),
946 (
947 "Order must have minimum value of $10.",
948 HyperliquidRejectCode::MinTradeNtl,
949 ),
950 (
951 "Insufficient margin to place order.",
952 HyperliquidRejectCode::PerpMargin,
953 ),
954 (
955 "Post only order would have immediately matched, bbo was 1.23",
956 HyperliquidRejectCode::BadAloPx,
957 ),
958 (
959 "Some unknown error",
960 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
961 ),
962 ];
963
964 for (error_str, expected_code) in test_cases {
965 assert_eq!(
966 HyperliquidRejectCode::from_api_error(error_str),
967 expected_code
968 );
969 }
970 }
971
972 #[rstest]
973 fn test_reduce_only() {
974 let reduce_only = HyperliquidReduceOnly::new(true);
975
976 assert!(reduce_only.is_reduce_only());
977
978 let json = serde_json::to_string(&reduce_only).unwrap();
979 assert_eq!(json, "true");
980
981 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
982 assert_eq!(parsed, reduce_only);
983 }
984
985 #[rstest]
986 fn test_order_status_conversion() {
987 assert_eq!(
989 OrderStatus::from(HyperliquidOrderStatus::Open),
990 OrderStatus::Accepted
991 );
992 assert_eq!(
993 OrderStatus::from(HyperliquidOrderStatus::Accepted),
994 OrderStatus::Accepted
995 );
996 assert_eq!(
997 OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
998 OrderStatus::PartiallyFilled
999 );
1000 assert_eq!(
1001 OrderStatus::from(HyperliquidOrderStatus::Filled),
1002 OrderStatus::Filled
1003 );
1004 assert_eq!(
1005 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1006 OrderStatus::Canceled
1007 );
1008 assert_eq!(
1009 OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1010 OrderStatus::Canceled
1011 );
1012 assert_eq!(
1013 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1014 OrderStatus::Rejected
1015 );
1016 assert_eq!(
1017 OrderStatus::from(HyperliquidOrderStatus::Expired),
1018 OrderStatus::Expired
1019 );
1020 }
1021
1022 #[rstest]
1023 fn test_order_status_string_mapping() {
1024 assert_eq!(
1026 hyperliquid_status_to_order_status("open"),
1027 OrderStatus::Accepted
1028 );
1029 assert_eq!(
1030 hyperliquid_status_to_order_status("accepted"),
1031 OrderStatus::Accepted
1032 );
1033 assert_eq!(
1034 hyperliquid_status_to_order_status("partially_filled"),
1035 OrderStatus::PartiallyFilled
1036 );
1037 assert_eq!(
1038 hyperliquid_status_to_order_status("filled"),
1039 OrderStatus::Filled
1040 );
1041 assert_eq!(
1042 hyperliquid_status_to_order_status("canceled"),
1043 OrderStatus::Canceled
1044 );
1045 assert_eq!(
1046 hyperliquid_status_to_order_status("cancelled"),
1047 OrderStatus::Canceled
1048 );
1049 assert_eq!(
1050 hyperliquid_status_to_order_status("rejected"),
1051 OrderStatus::Rejected
1052 );
1053 assert_eq!(
1054 hyperliquid_status_to_order_status("expired"),
1055 OrderStatus::Expired
1056 );
1057 assert_eq!(
1058 hyperliquid_status_to_order_status("unknown_status"),
1059 OrderStatus::Rejected
1060 );
1061 }
1062
1063 #[rstest]
1064 fn test_hyperliquid_tpsl_serialization() {
1065 let tp = HyperliquidTpSl::Tp;
1066 let sl = HyperliquidTpSl::Sl;
1067
1068 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1069 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1070 }
1071
1072 #[rstest]
1073 fn test_hyperliquid_tpsl_deserialization() {
1074 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1075 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1076
1077 assert_eq!(tp, HyperliquidTpSl::Tp);
1078 assert_eq!(sl, HyperliquidTpSl::Sl);
1079 }
1080
1081 #[rstest]
1082 fn test_hyperliquid_trigger_price_type_serialization() {
1083 let last = HyperliquidTriggerPriceType::Last;
1084 let mark = HyperliquidTriggerPriceType::Mark;
1085 let oracle = HyperliquidTriggerPriceType::Oracle;
1086
1087 assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1088 assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1089 assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1090 }
1091
1092 #[rstest]
1093 fn test_hyperliquid_trigger_price_type_to_nautilus() {
1094 assert_eq!(
1095 TriggerType::from(HyperliquidTriggerPriceType::Last),
1096 TriggerType::LastPrice
1097 );
1098 assert_eq!(
1099 TriggerType::from(HyperliquidTriggerPriceType::Mark),
1100 TriggerType::MarkPrice
1101 );
1102 assert_eq!(
1103 TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1104 TriggerType::IndexPrice
1105 );
1106 }
1107
1108 #[rstest]
1109 fn test_nautilus_trigger_type_to_hyperliquid() {
1110 assert_eq!(
1111 HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1112 HyperliquidTriggerPriceType::Last
1113 );
1114 assert_eq!(
1115 HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1116 HyperliquidTriggerPriceType::Mark
1117 );
1118 assert_eq!(
1119 HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1120 HyperliquidTriggerPriceType::Oracle
1121 );
1122 }
1123
1124 #[rstest]
1125 fn test_conditional_order_type_conversions() {
1126 assert_eq!(
1128 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1129 OrderType::StopMarket
1130 );
1131 assert_eq!(
1132 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1133 OrderType::StopLimit
1134 );
1135 assert_eq!(
1136 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1137 OrderType::MarketIfTouched
1138 );
1139 assert_eq!(
1140 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1141 OrderType::LimitIfTouched
1142 );
1143 assert_eq!(
1144 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1145 OrderType::TrailingStopMarket
1146 );
1147 }
1148
1149 mod error_parsing_tests {
1151 use super::*;
1152
1153 #[rstest]
1154 fn test_parse_tick_size_error() {
1155 let error = "Price must be divisible by tick size 0.01";
1156 let code = HyperliquidRejectCode::from_api_error(error);
1157 assert_eq!(code, HyperliquidRejectCode::Tick);
1158 }
1159
1160 #[rstest]
1161 fn test_parse_tick_size_error_case_insensitive() {
1162 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1163 let code = HyperliquidRejectCode::from_api_error(error);
1164 assert_eq!(code, HyperliquidRejectCode::Tick);
1165 }
1166
1167 #[rstest]
1168 fn test_parse_min_notional_perp() {
1169 let error = "Order must have minimum value of $10";
1170 let code = HyperliquidRejectCode::from_api_error(error);
1171 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1172 }
1173
1174 #[rstest]
1175 fn test_parse_min_notional_spot() {
1176 let error = "Order must have minimum value of 10 USDC";
1177 let code = HyperliquidRejectCode::from_api_error(error);
1178 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1179 }
1180
1181 #[rstest]
1182 fn test_parse_insufficient_margin() {
1183 let error = "Insufficient margin to place order";
1184 let code = HyperliquidRejectCode::from_api_error(error);
1185 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1186 }
1187
1188 #[rstest]
1189 fn test_parse_insufficient_margin_case_variations() {
1190 let variations = vec![
1191 "insufficient margin to place order",
1192 "INSUFFICIENT MARGIN TO PLACE ORDER",
1193 " Insufficient margin to place order ", ];
1195
1196 for error in variations {
1197 let code = HyperliquidRejectCode::from_api_error(error);
1198 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1199 }
1200 }
1201
1202 #[rstest]
1203 fn test_parse_reduce_only_violation() {
1204 let error = "Reduce only order would increase position";
1205 let code = HyperliquidRejectCode::from_api_error(error);
1206 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1207 }
1208
1209 #[rstest]
1210 fn test_parse_reduce_only_with_hyphen() {
1211 let error = "Reduce-only order would increase position";
1212 let code = HyperliquidRejectCode::from_api_error(error);
1213 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1214 }
1215
1216 #[rstest]
1217 fn test_parse_post_only_match() {
1218 let error = "Post only order would have immediately matched";
1219 let code = HyperliquidRejectCode::from_api_error(error);
1220 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1221 }
1222
1223 #[rstest]
1224 fn test_parse_post_only_with_hyphen() {
1225 let error = "Post-only order would have immediately matched";
1226 let code = HyperliquidRejectCode::from_api_error(error);
1227 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1228 }
1229
1230 #[rstest]
1231 fn test_parse_ioc_no_match() {
1232 let error = "Order could not immediately match";
1233 let code = HyperliquidRejectCode::from_api_error(error);
1234 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1235 }
1236
1237 #[rstest]
1238 fn test_parse_invalid_trigger_price() {
1239 let error = "Invalid TP/SL price";
1240 let code = HyperliquidRejectCode::from_api_error(error);
1241 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1242 }
1243
1244 #[rstest]
1245 fn test_parse_no_liquidity() {
1246 let error = "No liquidity available for market order";
1247 let code = HyperliquidRejectCode::from_api_error(error);
1248 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1249 }
1250
1251 #[rstest]
1252 fn test_parse_position_increase_at_oi_cap() {
1253 let error = "PositionIncreaseAtOpenInterestCap";
1254 let code = HyperliquidRejectCode::from_api_error(error);
1255 assert_eq!(
1256 code,
1257 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1258 );
1259 }
1260
1261 #[rstest]
1262 fn test_parse_position_flip_at_oi_cap() {
1263 let error = "PositionFlipAtOpenInterestCap";
1264 let code = HyperliquidRejectCode::from_api_error(error);
1265 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1266 }
1267
1268 #[rstest]
1269 fn test_parse_too_aggressive_at_oi_cap() {
1270 let error = "TooAggressiveAtOpenInterestCap";
1271 let code = HyperliquidRejectCode::from_api_error(error);
1272 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1273 }
1274
1275 #[rstest]
1276 fn test_parse_open_interest_increase() {
1277 let error = "OpenInterestIncrease";
1278 let code = HyperliquidRejectCode::from_api_error(error);
1279 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1280 }
1281
1282 #[rstest]
1283 fn test_parse_insufficient_spot_balance() {
1284 let error = "Insufficient spot balance";
1285 let code = HyperliquidRejectCode::from_api_error(error);
1286 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1287 }
1288
1289 #[rstest]
1290 fn test_parse_oracle_error() {
1291 let error = "Oracle price unavailable";
1292 let code = HyperliquidRejectCode::from_api_error(error);
1293 assert_eq!(code, HyperliquidRejectCode::Oracle);
1294 }
1295
1296 #[rstest]
1297 fn test_parse_max_position() {
1298 let error = "Exceeds max position size";
1299 let code = HyperliquidRejectCode::from_api_error(error);
1300 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1301 }
1302
1303 #[rstest]
1304 fn test_parse_missing_order() {
1305 let error = "MissingOrder";
1306 let code = HyperliquidRejectCode::from_api_error(error);
1307 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1308 }
1309
1310 #[rstest]
1311 fn test_parse_unknown_error() {
1312 let error = "This is a completely new error message";
1313 let code = HyperliquidRejectCode::from_api_error(error);
1314 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1315
1316 if let HyperliquidRejectCode::Unknown(msg) = code {
1318 assert_eq!(msg, error);
1319 }
1320 }
1321
1322 #[rstest]
1323 fn test_parse_empty_error() {
1324 let error = "";
1325 let code = HyperliquidRejectCode::from_api_error(error);
1326 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1327 }
1328
1329 #[rstest]
1330 fn test_parse_whitespace_only() {
1331 let error = " ";
1332 let code = HyperliquidRejectCode::from_api_error(error);
1333 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1334 }
1335
1336 #[rstest]
1337 fn test_normalization_preserves_original_in_unknown() {
1338 let error = " UNKNOWN ERROR MESSAGE ";
1339 let code = HyperliquidRejectCode::from_api_error(error);
1340
1341 if let HyperliquidRejectCode::Unknown(msg) = code {
1343 assert_eq!(msg, error);
1344 } else {
1345 panic!("Expected Unknown variant");
1346 }
1347 }
1348 }
1349
1350 #[rstest]
1351 fn test_conditional_order_type_round_trip() {
1352 assert_eq!(
1353 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1354 OrderType::TrailingStopLimit
1355 );
1356
1357 assert_eq!(
1359 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1360 HyperliquidConditionalOrderType::StopMarket
1361 );
1362 assert_eq!(
1363 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1364 HyperliquidConditionalOrderType::StopLimit
1365 );
1366 }
1367
1368 #[rstest]
1369 fn test_trailing_offset_type_serialization() {
1370 let price = HyperliquidTrailingOffsetType::Price;
1371 let percentage = HyperliquidTrailingOffsetType::Percentage;
1372 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1373
1374 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1375 assert_eq!(
1376 serde_json::to_string(&percentage).unwrap(),
1377 r#""percentage""#
1378 );
1379 assert_eq!(
1380 serde_json::to_string(&basis_points).unwrap(),
1381 r#""basispoints""#
1382 );
1383 }
1384
1385 #[rstest]
1386 fn test_conditional_order_type_serialization() {
1387 assert_eq!(
1388 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1389 r#""STOP_MARKET""#
1390 );
1391 assert_eq!(
1392 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1393 r#""STOP_LIMIT""#
1394 );
1395 assert_eq!(
1396 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1397 r#""TAKE_PROFIT_MARKET""#
1398 );
1399 assert_eq!(
1400 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1401 r#""TAKE_PROFIT_LIMIT""#
1402 );
1403 assert_eq!(
1404 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1405 r#""TRAILING_STOP_MARKET""#
1406 );
1407 assert_eq!(
1408 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1409 r#""TRAILING_STOP_LIMIT""#
1410 );
1411 }
1412
1413 #[rstest]
1414 fn test_order_type_enum_coverage() {
1415 let conditional_types = vec![
1417 HyperliquidConditionalOrderType::StopMarket,
1418 HyperliquidConditionalOrderType::StopLimit,
1419 HyperliquidConditionalOrderType::TakeProfitMarket,
1420 HyperliquidConditionalOrderType::TakeProfitLimit,
1421 HyperliquidConditionalOrderType::TrailingStopMarket,
1422 HyperliquidConditionalOrderType::TrailingStopLimit,
1423 ];
1424
1425 for cond_type in conditional_types {
1426 let order_type = OrderType::from(cond_type);
1427 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1428 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1429 }
1430 }
1431
1432 #[rstest]
1433 fn test_all_trigger_price_types() {
1434 let trigger_types = vec![
1435 HyperliquidTriggerPriceType::Last,
1436 HyperliquidTriggerPriceType::Mark,
1437 HyperliquidTriggerPriceType::Oracle,
1438 ];
1439
1440 for trigger_type in trigger_types {
1441 let nautilus_type = TriggerType::from(trigger_type);
1442 let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1443 assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1444 }
1445 }
1446}