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]
1068 fn test_hyperliquid_tpsl_serialization() {
1069 let tp = HyperliquidTpSl::Tp;
1070 let sl = HyperliquidTpSl::Sl;
1071
1072 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1073 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1074 }
1075
1076 #[rstest]
1077 fn test_hyperliquid_tpsl_deserialization() {
1078 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1079 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1080
1081 assert_eq!(tp, HyperliquidTpSl::Tp);
1082 assert_eq!(sl, HyperliquidTpSl::Sl);
1083 }
1084
1085 #[rstest]
1086 fn test_hyperliquid_trigger_price_type_serialization() {
1087 let last = HyperliquidTriggerPriceType::Last;
1088 let mark = HyperliquidTriggerPriceType::Mark;
1089 let oracle = HyperliquidTriggerPriceType::Oracle;
1090
1091 assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1092 assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1093 assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1094 }
1095
1096 #[rstest]
1097 fn test_hyperliquid_trigger_price_type_to_nautilus() {
1098 assert_eq!(
1099 TriggerType::from(HyperliquidTriggerPriceType::Last),
1100 TriggerType::LastPrice
1101 );
1102 assert_eq!(
1103 TriggerType::from(HyperliquidTriggerPriceType::Mark),
1104 TriggerType::MarkPrice
1105 );
1106 assert_eq!(
1107 TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1108 TriggerType::IndexPrice
1109 );
1110 }
1111
1112 #[rstest]
1113 fn test_nautilus_trigger_type_to_hyperliquid() {
1114 assert_eq!(
1115 HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1116 HyperliquidTriggerPriceType::Last
1117 );
1118 assert_eq!(
1119 HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1120 HyperliquidTriggerPriceType::Mark
1121 );
1122 assert_eq!(
1123 HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1124 HyperliquidTriggerPriceType::Oracle
1125 );
1126 }
1127
1128 #[rstest]
1129 fn test_conditional_order_type_conversions() {
1130 assert_eq!(
1132 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1133 OrderType::StopMarket
1134 );
1135 assert_eq!(
1136 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1137 OrderType::StopLimit
1138 );
1139 assert_eq!(
1140 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1141 OrderType::MarketIfTouched
1142 );
1143 assert_eq!(
1144 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1145 OrderType::LimitIfTouched
1146 );
1147 assert_eq!(
1148 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1149 OrderType::TrailingStopMarket
1150 );
1151 }
1152
1153 mod error_parsing_tests {
1155 use super::*;
1156
1157 #[rstest]
1158 fn test_parse_tick_size_error() {
1159 let error = "Price must be divisible by tick size 0.01";
1160 let code = HyperliquidRejectCode::from_api_error(error);
1161 assert_eq!(code, HyperliquidRejectCode::Tick);
1162 }
1163
1164 #[rstest]
1165 fn test_parse_tick_size_error_case_insensitive() {
1166 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1167 let code = HyperliquidRejectCode::from_api_error(error);
1168 assert_eq!(code, HyperliquidRejectCode::Tick);
1169 }
1170
1171 #[rstest]
1172 fn test_parse_min_notional_perp() {
1173 let error = "Order must have minimum value of $10";
1174 let code = HyperliquidRejectCode::from_api_error(error);
1175 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1176 }
1177
1178 #[rstest]
1179 fn test_parse_min_notional_spot() {
1180 let error = "Order must have minimum value of 10 USDC";
1181 let code = HyperliquidRejectCode::from_api_error(error);
1182 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1183 }
1184
1185 #[rstest]
1186 fn test_parse_insufficient_margin() {
1187 let error = "Insufficient margin to place order";
1188 let code = HyperliquidRejectCode::from_api_error(error);
1189 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1190 }
1191
1192 #[rstest]
1193 fn test_parse_insufficient_margin_case_variations() {
1194 let variations = vec![
1195 "insufficient margin to place order",
1196 "INSUFFICIENT MARGIN TO PLACE ORDER",
1197 " Insufficient margin to place order ", ];
1199
1200 for error in variations {
1201 let code = HyperliquidRejectCode::from_api_error(error);
1202 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1203 }
1204 }
1205
1206 #[rstest]
1207 fn test_parse_reduce_only_violation() {
1208 let error = "Reduce only order would increase position";
1209 let code = HyperliquidRejectCode::from_api_error(error);
1210 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1211 }
1212
1213 #[rstest]
1214 fn test_parse_reduce_only_with_hyphen() {
1215 let error = "Reduce-only order would increase position";
1216 let code = HyperliquidRejectCode::from_api_error(error);
1217 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1218 }
1219
1220 #[rstest]
1221 fn test_parse_post_only_match() {
1222 let error = "Post only order would have immediately matched";
1223 let code = HyperliquidRejectCode::from_api_error(error);
1224 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1225 }
1226
1227 #[rstest]
1228 fn test_parse_post_only_with_hyphen() {
1229 let error = "Post-only order would have immediately matched";
1230 let code = HyperliquidRejectCode::from_api_error(error);
1231 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1232 }
1233
1234 #[rstest]
1235 fn test_parse_ioc_no_match() {
1236 let error = "Order could not immediately match";
1237 let code = HyperliquidRejectCode::from_api_error(error);
1238 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1239 }
1240
1241 #[rstest]
1242 fn test_parse_invalid_trigger_price() {
1243 let error = "Invalid TP/SL price";
1244 let code = HyperliquidRejectCode::from_api_error(error);
1245 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1246 }
1247
1248 #[rstest]
1249 fn test_parse_no_liquidity() {
1250 let error = "No liquidity available for market order";
1251 let code = HyperliquidRejectCode::from_api_error(error);
1252 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1253 }
1254
1255 #[rstest]
1256 fn test_parse_position_increase_at_oi_cap() {
1257 let error = "PositionIncreaseAtOpenInterestCap";
1258 let code = HyperliquidRejectCode::from_api_error(error);
1259 assert_eq!(
1260 code,
1261 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1262 );
1263 }
1264
1265 #[rstest]
1266 fn test_parse_position_flip_at_oi_cap() {
1267 let error = "PositionFlipAtOpenInterestCap";
1268 let code = HyperliquidRejectCode::from_api_error(error);
1269 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1270 }
1271
1272 #[rstest]
1273 fn test_parse_too_aggressive_at_oi_cap() {
1274 let error = "TooAggressiveAtOpenInterestCap";
1275 let code = HyperliquidRejectCode::from_api_error(error);
1276 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1277 }
1278
1279 #[rstest]
1280 fn test_parse_open_interest_increase() {
1281 let error = "OpenInterestIncrease";
1282 let code = HyperliquidRejectCode::from_api_error(error);
1283 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1284 }
1285
1286 #[rstest]
1287 fn test_parse_insufficient_spot_balance() {
1288 let error = "Insufficient spot balance";
1289 let code = HyperliquidRejectCode::from_api_error(error);
1290 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1291 }
1292
1293 #[rstest]
1294 fn test_parse_oracle_error() {
1295 let error = "Oracle price unavailable";
1296 let code = HyperliquidRejectCode::from_api_error(error);
1297 assert_eq!(code, HyperliquidRejectCode::Oracle);
1298 }
1299
1300 #[rstest]
1301 fn test_parse_max_position() {
1302 let error = "Exceeds max position size";
1303 let code = HyperliquidRejectCode::from_api_error(error);
1304 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1305 }
1306
1307 #[rstest]
1308 fn test_parse_missing_order() {
1309 let error = "MissingOrder";
1310 let code = HyperliquidRejectCode::from_api_error(error);
1311 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1312 }
1313
1314 #[rstest]
1315 fn test_parse_unknown_error() {
1316 let error = "This is a completely new error message";
1317 let code = HyperliquidRejectCode::from_api_error(error);
1318 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1319
1320 if let HyperliquidRejectCode::Unknown(msg) = code {
1322 assert_eq!(msg, error);
1323 }
1324 }
1325
1326 #[rstest]
1327 fn test_parse_empty_error() {
1328 let error = "";
1329 let code = HyperliquidRejectCode::from_api_error(error);
1330 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1331 }
1332
1333 #[rstest]
1334 fn test_parse_whitespace_only() {
1335 let error = " ";
1336 let code = HyperliquidRejectCode::from_api_error(error);
1337 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1338 }
1339
1340 #[rstest]
1341 fn test_normalization_preserves_original_in_unknown() {
1342 let error = " UNKNOWN ERROR MESSAGE ";
1343 let code = HyperliquidRejectCode::from_api_error(error);
1344
1345 if let HyperliquidRejectCode::Unknown(msg) = code {
1347 assert_eq!(msg, error);
1348 } else {
1349 panic!("Expected Unknown variant");
1350 }
1351 }
1352 }
1353
1354 #[rstest]
1355 fn test_conditional_order_type_round_trip() {
1356 assert_eq!(
1357 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1358 OrderType::TrailingStopLimit
1359 );
1360
1361 assert_eq!(
1363 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1364 HyperliquidConditionalOrderType::StopMarket
1365 );
1366 assert_eq!(
1367 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1368 HyperliquidConditionalOrderType::StopLimit
1369 );
1370 }
1371
1372 #[rstest]
1373 fn test_trailing_offset_type_serialization() {
1374 let price = HyperliquidTrailingOffsetType::Price;
1375 let percentage = HyperliquidTrailingOffsetType::Percentage;
1376 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1377
1378 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1379 assert_eq!(
1380 serde_json::to_string(&percentage).unwrap(),
1381 r#""percentage""#
1382 );
1383 assert_eq!(
1384 serde_json::to_string(&basis_points).unwrap(),
1385 r#""basispoints""#
1386 );
1387 }
1388
1389 #[rstest]
1390 fn test_conditional_order_type_serialization() {
1391 assert_eq!(
1392 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1393 r#""STOP_MARKET""#
1394 );
1395 assert_eq!(
1396 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1397 r#""STOP_LIMIT""#
1398 );
1399 assert_eq!(
1400 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1401 r#""TAKE_PROFIT_MARKET""#
1402 );
1403 assert_eq!(
1404 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1405 r#""TAKE_PROFIT_LIMIT""#
1406 );
1407 assert_eq!(
1408 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1409 r#""TRAILING_STOP_MARKET""#
1410 );
1411 assert_eq!(
1412 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1413 r#""TRAILING_STOP_LIMIT""#
1414 );
1415 }
1416
1417 #[rstest]
1418 fn test_order_type_enum_coverage() {
1419 let conditional_types = vec![
1421 HyperliquidConditionalOrderType::StopMarket,
1422 HyperliquidConditionalOrderType::StopLimit,
1423 HyperliquidConditionalOrderType::TakeProfitMarket,
1424 HyperliquidConditionalOrderType::TakeProfitLimit,
1425 HyperliquidConditionalOrderType::TrailingStopMarket,
1426 HyperliquidConditionalOrderType::TrailingStopLimit,
1427 ];
1428
1429 for cond_type in conditional_types {
1430 let order_type = OrderType::from(cond_type);
1431 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1432 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1433 }
1434 }
1435
1436 #[rstest]
1437 fn test_all_trigger_price_types() {
1438 let trigger_types = vec![
1439 HyperliquidTriggerPriceType::Last,
1440 HyperliquidTriggerPriceType::Mark,
1441 HyperliquidTriggerPriceType::Oracle,
1442 ];
1443
1444 for trigger_type in trigger_types {
1445 let nautilus_type = TriggerType::from(trigger_type);
1446 let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1447 assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1448 }
1449 }
1450}