1use std::{fmt::Display, str::FromStr};
17
18use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus, OrderType};
19use serde::{Deserialize, Serialize};
20use strum::{AsRefStr, Display, EnumIter, EnumString};
21
22use super::consts::HYPERLIQUID_POST_ONLY_WOULD_MATCH;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum HyperliquidBarInterval {
26 #[serde(rename = "1m")]
27 OneMinute,
28 #[serde(rename = "3m")]
29 ThreeMinutes,
30 #[serde(rename = "5m")]
31 FiveMinutes,
32 #[serde(rename = "15m")]
33 FifteenMinutes,
34 #[serde(rename = "30m")]
35 ThirtyMinutes,
36 #[serde(rename = "1h")]
37 OneHour,
38 #[serde(rename = "2h")]
39 TwoHours,
40 #[serde(rename = "4h")]
41 FourHours,
42 #[serde(rename = "8h")]
43 EightHours,
44 #[serde(rename = "12h")]
45 TwelveHours,
46 #[serde(rename = "1d")]
47 OneDay,
48 #[serde(rename = "3d")]
49 ThreeDays,
50 #[serde(rename = "1w")]
51 OneWeek,
52 #[serde(rename = "1M")]
53 OneMonth,
54}
55
56impl HyperliquidBarInterval {
57 pub fn as_str(&self) -> &'static str {
58 match self {
59 Self::OneMinute => "1m",
60 Self::ThreeMinutes => "3m",
61 Self::FiveMinutes => "5m",
62 Self::FifteenMinutes => "15m",
63 Self::ThirtyMinutes => "30m",
64 Self::OneHour => "1h",
65 Self::TwoHours => "2h",
66 Self::FourHours => "4h",
67 Self::EightHours => "8h",
68 Self::TwelveHours => "12h",
69 Self::OneDay => "1d",
70 Self::ThreeDays => "3d",
71 Self::OneWeek => "1w",
72 Self::OneMonth => "1M",
73 }
74 }
75}
76
77impl FromStr for HyperliquidBarInterval {
78 type Err = anyhow::Error;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s {
82 "1m" => Ok(Self::OneMinute),
83 "3m" => Ok(Self::ThreeMinutes),
84 "5m" => Ok(Self::FiveMinutes),
85 "15m" => Ok(Self::FifteenMinutes),
86 "30m" => Ok(Self::ThirtyMinutes),
87 "1h" => Ok(Self::OneHour),
88 "2h" => Ok(Self::TwoHours),
89 "4h" => Ok(Self::FourHours),
90 "8h" => Ok(Self::EightHours),
91 "12h" => Ok(Self::TwelveHours),
92 "1d" => Ok(Self::OneDay),
93 "3d" => Ok(Self::ThreeDays),
94 "1w" => Ok(Self::OneWeek),
95 "1M" => Ok(Self::OneMonth),
96 _ => anyhow::bail!("Invalid Hyperliquid bar interval: {s}"),
97 }
98 }
99}
100
101impl Display for HyperliquidBarInterval {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(f, "{}", self.as_str())
104 }
105}
106
107#[derive(
109 Copy,
110 Clone,
111 Debug,
112 Display,
113 PartialEq,
114 Eq,
115 Hash,
116 AsRefStr,
117 EnumIter,
118 EnumString,
119 Serialize,
120 Deserialize,
121)]
122#[serde(rename_all = "UPPERCASE")]
123#[strum(serialize_all = "UPPERCASE")]
124pub enum HyperliquidSide {
125 #[serde(rename = "B")]
126 Buy,
127 #[serde(rename = "A")]
128 Sell,
129}
130
131impl From<OrderSide> for HyperliquidSide {
132 fn from(value: OrderSide) -> Self {
133 match value {
134 OrderSide::Buy => Self::Buy,
135 OrderSide::Sell => Self::Sell,
136 _ => panic!("Invalid `OrderSide`"),
137 }
138 }
139}
140
141impl From<HyperliquidSide> for OrderSide {
142 fn from(value: HyperliquidSide) -> Self {
143 match value {
144 HyperliquidSide::Buy => Self::Buy,
145 HyperliquidSide::Sell => Self::Sell,
146 }
147 }
148}
149
150impl From<HyperliquidSide> for AggressorSide {
151 fn from(value: HyperliquidSide) -> Self {
152 match value {
153 HyperliquidSide::Buy => Self::Buyer,
154 HyperliquidSide::Sell => Self::Seller,
155 }
156 }
157}
158
159#[derive(
161 Copy,
162 Clone,
163 Debug,
164 Display,
165 PartialEq,
166 Eq,
167 Hash,
168 AsRefStr,
169 EnumIter,
170 EnumString,
171 Serialize,
172 Deserialize,
173)]
174#[serde(rename_all = "PascalCase")]
175#[strum(serialize_all = "PascalCase")]
176pub enum HyperliquidTimeInForce {
177 Alo,
179 Ioc,
181 Gtc,
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
187#[serde(tag = "type", rename_all = "lowercase")]
188pub enum HyperliquidOrderType {
189 #[serde(rename = "limit")]
191 Limit { tif: HyperliquidTimeInForce },
192
193 #[serde(rename = "trigger")]
195 Trigger {
196 #[serde(rename = "isMarket")]
197 is_market: bool,
198 #[serde(rename = "triggerPx")]
199 trigger_px: String,
200 tpsl: HyperliquidTpSl,
201 },
202}
203
204#[derive(
206 Copy,
207 Clone,
208 Debug,
209 Display,
210 PartialEq,
211 Eq,
212 Hash,
213 AsRefStr,
214 EnumIter,
215 EnumString,
216 Serialize,
217 Deserialize,
218)]
219#[cfg_attr(
220 feature = "python",
221 pyo3::pyclass(
222 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
223 from_py_object,
224 rename_all = "SCREAMING_SNAKE_CASE",
225 )
226)]
227#[serde(rename_all = "lowercase")]
228#[strum(serialize_all = "lowercase")]
229pub enum HyperliquidTpSl {
230 Tp,
232 Sl,
234}
235
236#[derive(
241 Copy,
242 Clone,
243 Debug,
244 Display,
245 PartialEq,
246 Eq,
247 Hash,
248 AsRefStr,
249 EnumIter,
250 EnumString,
251 Serialize,
252 Deserialize,
253)]
254#[cfg_attr(
255 feature = "python",
256 pyo3::pyclass(
257 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
258 from_py_object,
259 rename_all = "SCREAMING_SNAKE_CASE",
260 )
261)]
262#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
263#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
264pub enum HyperliquidConditionalOrderType {
265 StopMarket,
267 StopLimit,
269 TakeProfitMarket,
271 TakeProfitLimit,
273 TrailingStopMarket,
275 TrailingStopLimit,
277}
278
279impl From<HyperliquidConditionalOrderType> for OrderType {
280 fn from(value: HyperliquidConditionalOrderType) -> Self {
281 match value {
282 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
283 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
284 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
285 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
286 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
287 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
288 }
289 }
290}
291
292impl From<OrderType> for HyperliquidConditionalOrderType {
293 fn from(value: OrderType) -> Self {
294 match value {
295 OrderType::StopMarket => Self::StopMarket,
296 OrderType::StopLimit => Self::StopLimit,
297 OrderType::MarketIfTouched => Self::TakeProfitMarket,
298 OrderType::LimitIfTouched => Self::TakeProfitLimit,
299 OrderType::TrailingStopMarket => Self::TrailingStopMarket,
300 OrderType::TrailingStopLimit => Self::TrailingStopLimit,
301 _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
302 }
303 }
304}
305
306#[derive(
313 Copy,
314 Clone,
315 Debug,
316 Display,
317 PartialEq,
318 Eq,
319 Hash,
320 AsRefStr,
321 EnumIter,
322 EnumString,
323 Serialize,
324 Deserialize,
325)]
326#[cfg_attr(
327 feature = "python",
328 pyo3::pyclass(
329 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
330 from_py_object,
331 rename_all = "SCREAMING_SNAKE_CASE",
332 )
333)]
334#[serde(rename_all = "lowercase")]
335#[strum(serialize_all = "lowercase")]
336pub enum HyperliquidTrailingOffsetType {
337 Price,
339 Percentage,
341 #[serde(rename = "basispoints")]
343 #[strum(serialize = "basispoints")]
344 BasisPoints,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(transparent)]
350pub struct HyperliquidReduceOnly(pub bool);
351
352impl HyperliquidReduceOnly {
353 pub fn new(reduce_only: bool) -> Self {
355 Self(reduce_only)
356 }
357
358 pub fn is_reduce_only(&self) -> bool {
360 self.0
361 }
362}
363
364#[derive(
366 Copy,
367 Clone,
368 Debug,
369 Display,
370 PartialEq,
371 Eq,
372 Hash,
373 AsRefStr,
374 EnumIter,
375 EnumString,
376 Serialize,
377 Deserialize,
378)]
379#[serde(rename_all = "lowercase")]
380#[strum(serialize_all = "lowercase")]
381pub enum HyperliquidLiquidityFlag {
382 Maker,
383 Taker,
384}
385
386impl From<bool> for HyperliquidLiquidityFlag {
387 fn from(crossed: bool) -> Self {
391 if crossed { Self::Taker } else { Self::Maker }
392 }
393}
394
395#[derive(
397 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
398)]
399#[serde(rename_all = "lowercase")]
400#[strum(serialize_all = "lowercase")]
401pub enum HyperliquidLiquidationMethod {
402 Market,
403 Backstop,
404}
405
406#[derive(
408 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
409)]
410#[serde(rename_all = "camelCase")]
411#[strum(serialize_all = "camelCase")]
412pub enum HyperliquidPositionType {
413 OneWay,
414}
415
416#[derive(
418 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
419)]
420#[serde(rename_all = "lowercase")]
421#[strum(serialize_all = "lowercase")]
422pub enum HyperliquidTwapStatus {
423 Activated,
424 Terminated,
425 Finished,
426 Error,
427}
428
429#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
430#[serde(untagged)]
431pub enum HyperliquidRejectCode {
432 Tick,
434 MinTradeNtl,
436 MinTradeSpotNtl,
438 PerpMargin,
440 ReduceOnly,
442 BadAloPx,
444 IocCancel,
446 BadTriggerPx,
448 MarketOrderNoLiquidity,
450 PositionIncreaseAtOpenInterestCap,
452 PositionFlipAtOpenInterestCap,
454 TooAggressiveAtOpenInterestCap,
456 OpenInterestIncrease,
458 InsufficientSpotBalance,
460 Oracle,
462 PerpMaxPosition,
464 MissingOrder,
466 Unknown(String),
468}
469
470impl HyperliquidRejectCode {
471 pub fn from_api_error(error_message: &str) -> Self {
473 Self::from_error_string_internal(error_message)
474 }
475
476 fn from_error_string_internal(error: &str) -> Self {
477 let normalized = error.trim().to_lowercase();
479
480 match normalized.as_str() {
481 s if s.contains("tick size") => Self::Tick,
483
484 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
486 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
487
488 s if s.contains("insufficient margin") => Self::PerpMargin,
490
491 s if s.contains("reduce only order would increase")
493 || s.contains("reduce-only order would increase") =>
494 {
495 Self::ReduceOnly
496 }
497
498 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
500 || s.contains("post-only order would have immediately matched") =>
501 {
502 Self::BadAloPx
503 }
504
505 s if s.contains("could not immediately match") => Self::IocCancel,
507
508 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
510
511 s if s.contains("no liquidity available for market order") => {
513 Self::MarketOrderNoLiquidity
514 }
515
516 s if s.contains("positionincreaseatopeninterestcap") => {
519 Self::PositionIncreaseAtOpenInterestCap
520 }
521 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
522 s if s.contains("tooaggressiveatopeninterestcap") => {
523 Self::TooAggressiveAtOpenInterestCap
524 }
525 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
526
527 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
529
530 s if s.contains("oracle") => Self::Oracle,
532
533 s if s.contains("max position") => Self::PerpMaxPosition,
535
536 s if s.contains("missingorder") => Self::MissingOrder,
538
539 _ => {
541 log::warn!(
542 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
544 Self::Unknown(error.to_string())
545 }
546 }
547 }
548
549 #[deprecated(
554 since = "0.50.0",
555 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
556 )]
557 pub fn from_error_string(error: &str) -> Self {
558 Self::from_error_string_internal(error)
559 }
560}
561
562#[derive(
566 Copy,
567 Clone,
568 Debug,
569 Display,
570 PartialEq,
571 Eq,
572 Hash,
573 AsRefStr,
574 EnumIter,
575 EnumString,
576 Serialize,
577 Deserialize,
578)]
579pub enum HyperliquidOrderStatus {
580 #[serde(rename = "open")]
582 Open,
583 #[serde(rename = "accepted")]
585 Accepted,
586 #[serde(rename = "triggered")]
588 Triggered,
589 #[serde(rename = "filled")]
591 Filled,
592 #[serde(rename = "canceled")]
594 Canceled,
595 #[serde(rename = "rejected")]
597 Rejected,
598 #[serde(rename = "marginCanceled")]
601 MarginCanceled,
602 #[serde(rename = "vaultWithdrawalCanceled")]
604 VaultWithdrawalCanceled,
605 #[serde(rename = "openInterestCapCanceled")]
607 OpenInterestCapCanceled,
608 #[serde(rename = "selfTradeCanceled")]
610 SelfTradeCanceled,
611 #[serde(rename = "reduceOnlyCanceled")]
613 ReduceOnlyCanceled,
614 #[serde(rename = "siblingFilledCanceled")]
616 SiblingFilledCanceled,
617 #[serde(rename = "delistedCanceled")]
619 DelistedCanceled,
620 #[serde(rename = "liquidatedCanceled")]
622 LiquidatedCanceled,
623 #[serde(rename = "scheduledCancel")]
625 ScheduledCancel,
626 #[serde(rename = "tickRejected")]
629 TickRejected,
630 #[serde(rename = "minTradeNtlRejected")]
632 MinTradeNtlRejected,
633 #[serde(rename = "perpMarginRejected")]
635 PerpMarginRejected,
636 #[serde(rename = "reduceOnlyRejected")]
638 ReduceOnlyRejected,
639 #[serde(rename = "badAloPxRejected")]
641 BadAloPxRejected,
642 #[serde(rename = "iocCancelRejected")]
644 IocCancelRejected,
645 #[serde(rename = "badTriggerPxRejected")]
647 BadTriggerPxRejected,
648 #[serde(rename = "marketOrderNoLiquidityRejected")]
650 MarketOrderNoLiquidityRejected,
651 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
653 PositionIncreaseAtOpenInterestCapRejected,
654 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
656 PositionFlipAtOpenInterestCapRejected,
657 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
659 TooAggressiveAtOpenInterestCapRejected,
660 #[serde(rename = "openInterestIncreaseRejected")]
662 OpenInterestIncreaseRejected,
663 #[serde(rename = "insufficientSpotBalanceRejected")]
665 InsufficientSpotBalanceRejected,
666 #[serde(rename = "oracleRejected")]
668 OracleRejected,
669 #[serde(rename = "perpMaxPositionRejected")]
671 PerpMaxPositionRejected,
672}
673
674impl From<HyperliquidOrderStatus> for OrderStatus {
675 fn from(status: HyperliquidOrderStatus) -> Self {
676 match status {
677 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
678 HyperliquidOrderStatus::Triggered => Self::Triggered,
679 HyperliquidOrderStatus::Filled => Self::Filled,
680 HyperliquidOrderStatus::Canceled
682 | HyperliquidOrderStatus::MarginCanceled
683 | HyperliquidOrderStatus::VaultWithdrawalCanceled
684 | HyperliquidOrderStatus::OpenInterestCapCanceled
685 | HyperliquidOrderStatus::SelfTradeCanceled
686 | HyperliquidOrderStatus::ReduceOnlyCanceled
687 | HyperliquidOrderStatus::SiblingFilledCanceled
688 | HyperliquidOrderStatus::DelistedCanceled
689 | HyperliquidOrderStatus::LiquidatedCanceled
690 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
691 HyperliquidOrderStatus::Rejected
693 | HyperliquidOrderStatus::TickRejected
694 | HyperliquidOrderStatus::MinTradeNtlRejected
695 | HyperliquidOrderStatus::PerpMarginRejected
696 | HyperliquidOrderStatus::ReduceOnlyRejected
697 | HyperliquidOrderStatus::BadAloPxRejected
698 | HyperliquidOrderStatus::IocCancelRejected
699 | HyperliquidOrderStatus::BadTriggerPxRejected
700 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
701 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
702 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
703 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
704 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
705 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
706 | HyperliquidOrderStatus::OracleRejected
707 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
708 }
709 }
710}
711
712#[derive(
723 Copy,
724 Clone,
725 Debug,
726 Display,
727 PartialEq,
728 Eq,
729 Hash,
730 AsRefStr,
731 EnumIter,
732 EnumString,
733 Serialize,
734 Deserialize,
735)]
736#[serde(rename_all = "PascalCase")]
737#[strum(serialize_all = "PascalCase")]
738pub enum HyperliquidFillDirection {
739 #[serde(rename = "Open Long")]
741 #[strum(serialize = "Open Long")]
742 OpenLong,
743 #[serde(rename = "Open Short")]
745 #[strum(serialize = "Open Short")]
746 OpenShort,
747 #[serde(rename = "Close Long")]
749 #[strum(serialize = "Close Long")]
750 CloseLong,
751 #[serde(rename = "Close Short")]
753 #[strum(serialize = "Close Short")]
754 CloseShort,
755 #[serde(rename = "Long > Short")]
757 #[strum(serialize = "Long > Short")]
758 LongToShort,
759 #[serde(rename = "Short > Long")]
761 #[strum(serialize = "Short > Long")]
762 ShortToLong,
763 Buy,
765 Sell,
767}
768
769#[derive(
773 Copy,
774 Clone,
775 Debug,
776 Display,
777 PartialEq,
778 Eq,
779 Hash,
780 AsRefStr,
781 EnumIter,
782 EnumString,
783 Serialize,
784 Deserialize,
785)]
786#[serde(rename_all = "camelCase")]
787#[strum(serialize_all = "camelCase")]
788pub enum HyperliquidInfoRequestType {
789 Meta,
791 SpotMeta,
793 MetaAndAssetCtxs,
795 SpotMetaAndAssetCtxs,
797 L2Book,
799 AllMids,
801 UserFills,
803 UserFillsByTime,
805 OrderStatus,
807 OpenOrders,
809 FrontendOpenOrders,
811 ClearinghouseState,
813 SpotClearinghouseState,
815 ExchangeStatus,
817 CandleSnapshot,
819 Candle,
821 RecentTrades,
823 HistoricalOrders,
825 FundingHistory,
827 UserFunding,
829 NonUserFundingUpdates,
831 TwapHistory,
833 UserTwapSliceFills,
835 UserTwapSliceFillsByTime,
837 UserRateLimit,
839 UserRole,
841 DelegatorHistory,
843 DelegatorRewards,
845 ValidatorStats,
847}
848
849impl HyperliquidInfoRequestType {
850 pub fn as_str(&self) -> &'static str {
851 match self {
852 Self::Meta => "meta",
853 Self::SpotMeta => "spotMeta",
854 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
855 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
856 Self::L2Book => "l2Book",
857 Self::AllMids => "allMids",
858 Self::UserFills => "userFills",
859 Self::UserFillsByTime => "userFillsByTime",
860 Self::OrderStatus => "orderStatus",
861 Self::OpenOrders => "openOrders",
862 Self::FrontendOpenOrders => "frontendOpenOrders",
863 Self::ClearinghouseState => "clearinghouseState",
864 Self::SpotClearinghouseState => "spotClearinghouseState",
865 Self::ExchangeStatus => "exchangeStatus",
866 Self::CandleSnapshot => "candleSnapshot",
867 Self::Candle => "candle",
868 Self::RecentTrades => "recentTrades",
869 Self::HistoricalOrders => "historicalOrders",
870 Self::FundingHistory => "fundingHistory",
871 Self::UserFunding => "userFunding",
872 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
873 Self::TwapHistory => "twapHistory",
874 Self::UserTwapSliceFills => "userTwapSliceFills",
875 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
876 Self::UserRateLimit => "userRateLimit",
877 Self::UserRole => "userRole",
878 Self::DelegatorHistory => "delegatorHistory",
879 Self::DelegatorRewards => "delegatorRewards",
880 Self::ValidatorStats => "validatorStats",
881 }
882 }
883}
884
885#[derive(
886 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
887)]
888#[serde(rename_all = "lowercase")]
889#[strum(serialize_all = "lowercase")]
890pub enum HyperliquidLeverageType {
891 Cross,
892 Isolated,
893 #[serde(other)]
894 Unknown,
895}
896
897#[derive(
899 Copy,
900 Clone,
901 Debug,
902 Display,
903 PartialEq,
904 Eq,
905 Hash,
906 AsRefStr,
907 EnumIter,
908 EnumString,
909 Serialize,
910 Deserialize,
911)]
912#[cfg_attr(
913 feature = "python",
914 pyo3::pyclass(
915 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
916 from_py_object,
917 rename_all = "SCREAMING_SNAKE_CASE",
918 )
919)]
920#[serde(rename_all = "UPPERCASE")]
921#[strum(serialize_all = "UPPERCASE")]
922pub enum HyperliquidProductType {
923 Perp,
925 Spot,
927}
928
929impl HyperliquidProductType {
930 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
936 if symbol.ends_with("-PERP") {
937 Ok(Self::Perp)
938 } else if symbol.ends_with("-SPOT") {
939 Ok(Self::Spot)
940 } else {
941 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
942 }
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use nautilus_model::enums::OrderType;
949 use rstest::rstest;
950 use serde_json;
951
952 use super::*;
953
954 #[rstest]
955 fn test_side_serde() {
956 let buy_side = HyperliquidSide::Buy;
957 let sell_side = HyperliquidSide::Sell;
958
959 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
960 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
961
962 assert_eq!(
963 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
964 HyperliquidSide::Buy
965 );
966 assert_eq!(
967 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
968 HyperliquidSide::Sell
969 );
970 }
971
972 #[rstest]
973 fn test_side_from_order_side() {
974 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
976 assert_eq!(
977 HyperliquidSide::from(OrderSide::Sell),
978 HyperliquidSide::Sell
979 );
980 }
981
982 #[rstest]
983 fn test_order_side_from_hyperliquid_side() {
984 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
986 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
987 }
988
989 #[rstest]
990 fn test_aggressor_side_from_hyperliquid_side() {
991 assert_eq!(
993 AggressorSide::from(HyperliquidSide::Buy),
994 AggressorSide::Buyer
995 );
996 assert_eq!(
997 AggressorSide::from(HyperliquidSide::Sell),
998 AggressorSide::Seller
999 );
1000 }
1001
1002 #[rstest]
1003 fn test_time_in_force_serde() {
1004 let test_cases = [
1005 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1006 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1007 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1008 ];
1009
1010 for (tif, expected_json) in test_cases {
1011 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1012 assert_eq!(
1013 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1014 tif
1015 );
1016 }
1017 }
1018
1019 #[rstest]
1020 fn test_liquidity_flag_from_crossed() {
1021 assert_eq!(
1022 HyperliquidLiquidityFlag::from(true),
1023 HyperliquidLiquidityFlag::Taker
1024 );
1025 assert_eq!(
1026 HyperliquidLiquidityFlag::from(false),
1027 HyperliquidLiquidityFlag::Maker
1028 );
1029 }
1030
1031 #[rstest]
1032 #[allow(deprecated)]
1033 fn test_reject_code_from_error_string() {
1034 let test_cases = [
1035 (
1036 "Price must be divisible by tick size.",
1037 HyperliquidRejectCode::Tick,
1038 ),
1039 (
1040 "Order must have minimum value of $10.",
1041 HyperliquidRejectCode::MinTradeNtl,
1042 ),
1043 (
1044 "Insufficient margin to place order.",
1045 HyperliquidRejectCode::PerpMargin,
1046 ),
1047 (
1048 "Post only order would have immediately matched, bbo was 1.23",
1049 HyperliquidRejectCode::BadAloPx,
1050 ),
1051 (
1052 "Some unknown error",
1053 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1054 ),
1055 ];
1056
1057 for (error_str, expected_code) in test_cases {
1058 assert_eq!(
1059 HyperliquidRejectCode::from_error_string(error_str),
1060 expected_code
1061 );
1062 }
1063 }
1064
1065 #[rstest]
1066 fn test_reject_code_from_api_error() {
1067 let test_cases = [
1068 (
1069 "Price must be divisible by tick size.",
1070 HyperliquidRejectCode::Tick,
1071 ),
1072 (
1073 "Order must have minimum value of $10.",
1074 HyperliquidRejectCode::MinTradeNtl,
1075 ),
1076 (
1077 "Insufficient margin to place order.",
1078 HyperliquidRejectCode::PerpMargin,
1079 ),
1080 (
1081 "Post only order would have immediately matched, bbo was 1.23",
1082 HyperliquidRejectCode::BadAloPx,
1083 ),
1084 (
1085 "Some unknown error",
1086 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1087 ),
1088 ];
1089
1090 for (error_str, expected_code) in test_cases {
1091 assert_eq!(
1092 HyperliquidRejectCode::from_api_error(error_str),
1093 expected_code
1094 );
1095 }
1096 }
1097
1098 #[rstest]
1099 fn test_reduce_only() {
1100 let reduce_only = HyperliquidReduceOnly::new(true);
1101
1102 assert!(reduce_only.is_reduce_only());
1103
1104 let json = serde_json::to_string(&reduce_only).unwrap();
1105 assert_eq!(json, "true");
1106
1107 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1108 assert_eq!(parsed, reduce_only);
1109 }
1110
1111 #[rstest]
1112 fn test_order_status_conversion() {
1113 assert_eq!(
1115 OrderStatus::from(HyperliquidOrderStatus::Open),
1116 OrderStatus::Accepted
1117 );
1118 assert_eq!(
1119 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1120 OrderStatus::Accepted
1121 );
1122 assert_eq!(
1123 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1124 OrderStatus::Triggered
1125 );
1126 assert_eq!(
1127 OrderStatus::from(HyperliquidOrderStatus::Filled),
1128 OrderStatus::Filled
1129 );
1130 assert_eq!(
1131 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1132 OrderStatus::Canceled
1133 );
1134 assert_eq!(
1135 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1136 OrderStatus::Rejected
1137 );
1138
1139 assert_eq!(
1141 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1142 OrderStatus::Canceled
1143 );
1144 assert_eq!(
1145 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1146 OrderStatus::Canceled
1147 );
1148 assert_eq!(
1149 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1150 OrderStatus::Canceled
1151 );
1152
1153 assert_eq!(
1155 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1156 OrderStatus::Rejected
1157 );
1158 assert_eq!(
1159 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1160 OrderStatus::Rejected
1161 );
1162 }
1163
1164 #[rstest]
1165 fn test_order_status_serde_deserialization() {
1166 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1168 assert_eq!(open, HyperliquidOrderStatus::Open);
1169
1170 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1171 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1172
1173 let margin_canceled: HyperliquidOrderStatus =
1174 serde_json::from_str(r#""marginCanceled""#).unwrap();
1175 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1176
1177 let self_trade_canceled: HyperliquidOrderStatus =
1178 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1179 assert_eq!(
1180 self_trade_canceled,
1181 HyperliquidOrderStatus::SelfTradeCanceled
1182 );
1183
1184 let reduce_only_canceled: HyperliquidOrderStatus =
1185 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1186 assert_eq!(
1187 reduce_only_canceled,
1188 HyperliquidOrderStatus::ReduceOnlyCanceled
1189 );
1190
1191 let tick_rejected: HyperliquidOrderStatus =
1192 serde_json::from_str(r#""tickRejected""#).unwrap();
1193 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1194 }
1195
1196 #[rstest]
1197 fn test_hyperliquid_tpsl_serialization() {
1198 let tp = HyperliquidTpSl::Tp;
1199 let sl = HyperliquidTpSl::Sl;
1200
1201 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1202 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1203 }
1204
1205 #[rstest]
1206 fn test_hyperliquid_tpsl_deserialization() {
1207 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1208 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1209
1210 assert_eq!(tp, HyperliquidTpSl::Tp);
1211 assert_eq!(sl, HyperliquidTpSl::Sl);
1212 }
1213
1214 #[rstest]
1215 fn test_conditional_order_type_conversions() {
1216 assert_eq!(
1218 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1219 OrderType::StopMarket
1220 );
1221 assert_eq!(
1222 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1223 OrderType::StopLimit
1224 );
1225 assert_eq!(
1226 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1227 OrderType::MarketIfTouched
1228 );
1229 assert_eq!(
1230 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1231 OrderType::LimitIfTouched
1232 );
1233 assert_eq!(
1234 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1235 OrderType::TrailingStopMarket
1236 );
1237 }
1238
1239 mod error_parsing_tests {
1241 use super::*;
1242
1243 #[rstest]
1244 fn test_parse_tick_size_error() {
1245 let error = "Price must be divisible by tick size 0.01";
1246 let code = HyperliquidRejectCode::from_api_error(error);
1247 assert_eq!(code, HyperliquidRejectCode::Tick);
1248 }
1249
1250 #[rstest]
1251 fn test_parse_tick_size_error_case_insensitive() {
1252 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1253 let code = HyperliquidRejectCode::from_api_error(error);
1254 assert_eq!(code, HyperliquidRejectCode::Tick);
1255 }
1256
1257 #[rstest]
1258 fn test_parse_min_notional_perp() {
1259 let error = "Order must have minimum value of $10";
1260 let code = HyperliquidRejectCode::from_api_error(error);
1261 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1262 }
1263
1264 #[rstest]
1265 fn test_parse_min_notional_spot() {
1266 let error = "Order must have minimum value of 10 USDC";
1267 let code = HyperliquidRejectCode::from_api_error(error);
1268 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1269 }
1270
1271 #[rstest]
1272 fn test_parse_insufficient_margin() {
1273 let error = "Insufficient margin to place order";
1274 let code = HyperliquidRejectCode::from_api_error(error);
1275 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1276 }
1277
1278 #[rstest]
1279 fn test_parse_insufficient_margin_case_variations() {
1280 let variations = vec![
1281 "insufficient margin to place order",
1282 "INSUFFICIENT MARGIN TO PLACE ORDER",
1283 " Insufficient margin to place order ", ];
1285
1286 for error in variations {
1287 let code = HyperliquidRejectCode::from_api_error(error);
1288 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1289 }
1290 }
1291
1292 #[rstest]
1293 fn test_parse_reduce_only_violation() {
1294 let error = "Reduce only order would increase position";
1295 let code = HyperliquidRejectCode::from_api_error(error);
1296 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1297 }
1298
1299 #[rstest]
1300 fn test_parse_reduce_only_with_hyphen() {
1301 let error = "Reduce-only order would increase position";
1302 let code = HyperliquidRejectCode::from_api_error(error);
1303 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1304 }
1305
1306 #[rstest]
1307 fn test_parse_post_only_match() {
1308 let error = "Post only order would have immediately matched";
1309 let code = HyperliquidRejectCode::from_api_error(error);
1310 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1311 }
1312
1313 #[rstest]
1314 fn test_parse_post_only_with_hyphen() {
1315 let error = "Post-only order would have immediately matched";
1316 let code = HyperliquidRejectCode::from_api_error(error);
1317 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1318 }
1319
1320 #[rstest]
1321 fn test_parse_ioc_no_match() {
1322 let error = "Order could not immediately match";
1323 let code = HyperliquidRejectCode::from_api_error(error);
1324 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1325 }
1326
1327 #[rstest]
1328 fn test_parse_invalid_trigger_price() {
1329 let error = "Invalid TP/SL price";
1330 let code = HyperliquidRejectCode::from_api_error(error);
1331 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1332 }
1333
1334 #[rstest]
1335 fn test_parse_no_liquidity() {
1336 let error = "No liquidity available for market order";
1337 let code = HyperliquidRejectCode::from_api_error(error);
1338 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1339 }
1340
1341 #[rstest]
1342 fn test_parse_position_increase_at_oi_cap() {
1343 let error = "PositionIncreaseAtOpenInterestCap";
1344 let code = HyperliquidRejectCode::from_api_error(error);
1345 assert_eq!(
1346 code,
1347 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1348 );
1349 }
1350
1351 #[rstest]
1352 fn test_parse_position_flip_at_oi_cap() {
1353 let error = "PositionFlipAtOpenInterestCap";
1354 let code = HyperliquidRejectCode::from_api_error(error);
1355 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1356 }
1357
1358 #[rstest]
1359 fn test_parse_too_aggressive_at_oi_cap() {
1360 let error = "TooAggressiveAtOpenInterestCap";
1361 let code = HyperliquidRejectCode::from_api_error(error);
1362 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1363 }
1364
1365 #[rstest]
1366 fn test_parse_open_interest_increase() {
1367 let error = "OpenInterestIncrease";
1368 let code = HyperliquidRejectCode::from_api_error(error);
1369 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1370 }
1371
1372 #[rstest]
1373 fn test_parse_insufficient_spot_balance() {
1374 let error = "Insufficient spot balance";
1375 let code = HyperliquidRejectCode::from_api_error(error);
1376 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1377 }
1378
1379 #[rstest]
1380 fn test_parse_oracle_error() {
1381 let error = "Oracle price unavailable";
1382 let code = HyperliquidRejectCode::from_api_error(error);
1383 assert_eq!(code, HyperliquidRejectCode::Oracle);
1384 }
1385
1386 #[rstest]
1387 fn test_parse_max_position() {
1388 let error = "Exceeds max position size";
1389 let code = HyperliquidRejectCode::from_api_error(error);
1390 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1391 }
1392
1393 #[rstest]
1394 fn test_parse_missing_order() {
1395 let error = "MissingOrder";
1396 let code = HyperliquidRejectCode::from_api_error(error);
1397 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1398 }
1399
1400 #[rstest]
1401 fn test_parse_unknown_error() {
1402 let error = "This is a completely new error message";
1403 let code = HyperliquidRejectCode::from_api_error(error);
1404 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1405
1406 if let HyperliquidRejectCode::Unknown(msg) = code {
1408 assert_eq!(msg, error);
1409 }
1410 }
1411
1412 #[rstest]
1413 fn test_parse_empty_error() {
1414 let error = "";
1415 let code = HyperliquidRejectCode::from_api_error(error);
1416 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1417 }
1418
1419 #[rstest]
1420 fn test_parse_whitespace_only() {
1421 let error = " ";
1422 let code = HyperliquidRejectCode::from_api_error(error);
1423 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1424 }
1425
1426 #[rstest]
1427 fn test_normalization_preserves_original_in_unknown() {
1428 let error = " UNKNOWN ERROR MESSAGE ";
1429 let code = HyperliquidRejectCode::from_api_error(error);
1430
1431 if let HyperliquidRejectCode::Unknown(msg) = code {
1433 assert_eq!(msg, error);
1434 } else {
1435 panic!("Expected Unknown variant");
1436 }
1437 }
1438 }
1439
1440 #[rstest]
1441 fn test_conditional_order_type_round_trip() {
1442 assert_eq!(
1443 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1444 OrderType::TrailingStopLimit
1445 );
1446
1447 assert_eq!(
1449 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1450 HyperliquidConditionalOrderType::StopMarket
1451 );
1452 assert_eq!(
1453 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1454 HyperliquidConditionalOrderType::StopLimit
1455 );
1456 }
1457
1458 #[rstest]
1459 fn test_trailing_offset_type_serialization() {
1460 let price = HyperliquidTrailingOffsetType::Price;
1461 let percentage = HyperliquidTrailingOffsetType::Percentage;
1462 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1463
1464 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1465 assert_eq!(
1466 serde_json::to_string(&percentage).unwrap(),
1467 r#""percentage""#
1468 );
1469 assert_eq!(
1470 serde_json::to_string(&basis_points).unwrap(),
1471 r#""basispoints""#
1472 );
1473 }
1474
1475 #[rstest]
1476 fn test_conditional_order_type_serialization() {
1477 assert_eq!(
1478 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1479 r#""STOP_MARKET""#
1480 );
1481 assert_eq!(
1482 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1483 r#""STOP_LIMIT""#
1484 );
1485 assert_eq!(
1486 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1487 r#""TAKE_PROFIT_MARKET""#
1488 );
1489 assert_eq!(
1490 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1491 r#""TAKE_PROFIT_LIMIT""#
1492 );
1493 assert_eq!(
1494 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1495 r#""TRAILING_STOP_MARKET""#
1496 );
1497 assert_eq!(
1498 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1499 r#""TRAILING_STOP_LIMIT""#
1500 );
1501 }
1502
1503 #[rstest]
1504 fn test_order_type_enum_coverage() {
1505 let conditional_types = vec![
1507 HyperliquidConditionalOrderType::StopMarket,
1508 HyperliquidConditionalOrderType::StopLimit,
1509 HyperliquidConditionalOrderType::TakeProfitMarket,
1510 HyperliquidConditionalOrderType::TakeProfitLimit,
1511 HyperliquidConditionalOrderType::TrailingStopMarket,
1512 HyperliquidConditionalOrderType::TrailingStopLimit,
1513 ];
1514
1515 for cond_type in conditional_types {
1516 let order_type = OrderType::from(cond_type);
1517 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1518 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1519 }
1520 }
1521}