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#[cfg_attr(
228 feature = "python",
229 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
230)]
231#[serde(rename_all = "lowercase")]
232#[strum(serialize_all = "lowercase")]
233pub enum HyperliquidTpSl {
234 Tp,
236 Sl,
238}
239
240#[derive(
245 Copy,
246 Clone,
247 Debug,
248 Display,
249 PartialEq,
250 Eq,
251 Hash,
252 AsRefStr,
253 EnumIter,
254 EnumString,
255 Serialize,
256 Deserialize,
257)]
258#[cfg_attr(
259 feature = "python",
260 pyo3::pyclass(
261 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
262 from_py_object,
263 rename_all = "SCREAMING_SNAKE_CASE",
264 )
265)]
266#[cfg_attr(
267 feature = "python",
268 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
269)]
270#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
271#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
272pub enum HyperliquidConditionalOrderType {
273 StopMarket,
275 StopLimit,
277 TakeProfitMarket,
279 TakeProfitLimit,
281 TrailingStopMarket,
283 TrailingStopLimit,
285}
286
287impl From<HyperliquidConditionalOrderType> for OrderType {
288 fn from(value: HyperliquidConditionalOrderType) -> Self {
289 match value {
290 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
291 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
292 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
293 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
294 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
295 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
296 }
297 }
298}
299
300impl From<OrderType> for HyperliquidConditionalOrderType {
301 fn from(value: OrderType) -> Self {
302 match value {
303 OrderType::StopMarket => Self::StopMarket,
304 OrderType::StopLimit => Self::StopLimit,
305 OrderType::MarketIfTouched => Self::TakeProfitMarket,
306 OrderType::LimitIfTouched => Self::TakeProfitLimit,
307 OrderType::TrailingStopMarket => Self::TrailingStopMarket,
308 OrderType::TrailingStopLimit => Self::TrailingStopLimit,
309 _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
310 }
311 }
312}
313
314#[derive(
321 Copy,
322 Clone,
323 Debug,
324 Display,
325 PartialEq,
326 Eq,
327 Hash,
328 AsRefStr,
329 EnumIter,
330 EnumString,
331 Serialize,
332 Deserialize,
333)]
334#[cfg_attr(
335 feature = "python",
336 pyo3::pyclass(
337 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
338 from_py_object,
339 rename_all = "SCREAMING_SNAKE_CASE",
340 )
341)]
342#[cfg_attr(
343 feature = "python",
344 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
345)]
346#[serde(rename_all = "lowercase")]
347#[strum(serialize_all = "lowercase")]
348pub enum HyperliquidTrailingOffsetType {
349 Price,
351 Percentage,
353 #[serde(rename = "basispoints")]
355 #[strum(serialize = "basispoints")]
356 BasisPoints,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
361#[serde(transparent)]
362pub struct HyperliquidReduceOnly(pub bool);
363
364impl HyperliquidReduceOnly {
365 pub fn new(reduce_only: bool) -> Self {
367 Self(reduce_only)
368 }
369
370 pub fn is_reduce_only(&self) -> bool {
372 self.0
373 }
374}
375
376#[derive(
378 Copy,
379 Clone,
380 Debug,
381 Display,
382 PartialEq,
383 Eq,
384 Hash,
385 AsRefStr,
386 EnumIter,
387 EnumString,
388 Serialize,
389 Deserialize,
390)]
391#[serde(rename_all = "lowercase")]
392#[strum(serialize_all = "lowercase")]
393pub enum HyperliquidLiquidityFlag {
394 Maker,
395 Taker,
396}
397
398impl From<bool> for HyperliquidLiquidityFlag {
399 fn from(crossed: bool) -> Self {
403 if crossed { Self::Taker } else { Self::Maker }
404 }
405}
406
407#[derive(
409 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
410)]
411#[serde(rename_all = "lowercase")]
412#[strum(serialize_all = "lowercase")]
413pub enum HyperliquidLiquidationMethod {
414 Market,
415 Backstop,
416}
417
418#[derive(
420 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
421)]
422#[serde(rename_all = "camelCase")]
423#[strum(serialize_all = "camelCase")]
424pub enum HyperliquidPositionType {
425 OneWay,
426}
427
428#[derive(
430 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
431)]
432#[serde(rename_all = "lowercase")]
433#[strum(serialize_all = "lowercase")]
434pub enum HyperliquidTwapStatus {
435 Activated,
436 Terminated,
437 Finished,
438 Error,
439}
440
441#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
442#[serde(untagged)]
443pub enum HyperliquidRejectCode {
444 Tick,
446 MinTradeNtl,
448 MinTradeSpotNtl,
450 PerpMargin,
452 ReduceOnly,
454 BadAloPx,
456 IocCancel,
458 BadTriggerPx,
460 MarketOrderNoLiquidity,
462 PositionIncreaseAtOpenInterestCap,
464 PositionFlipAtOpenInterestCap,
466 TooAggressiveAtOpenInterestCap,
468 OpenInterestIncrease,
470 InsufficientSpotBalance,
472 Oracle,
474 PerpMaxPosition,
476 MissingOrder,
478 Unknown(String),
480}
481
482impl HyperliquidRejectCode {
483 pub fn from_api_error(error_message: &str) -> Self {
485 Self::from_error_string_internal(error_message)
486 }
487
488 fn from_error_string_internal(error: &str) -> Self {
489 let normalized = error.trim().to_lowercase();
491
492 match normalized.as_str() {
493 s if s.contains("tick size") => Self::Tick,
495
496 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
498 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
499
500 s if s.contains("insufficient margin") => Self::PerpMargin,
502
503 s if s.contains("reduce only order would increase")
505 || s.contains("reduce-only order would increase") =>
506 {
507 Self::ReduceOnly
508 }
509
510 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
512 || s.contains("post-only order would have immediately matched") =>
513 {
514 Self::BadAloPx
515 }
516
517 s if s.contains("could not immediately match") => Self::IocCancel,
519
520 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
522
523 s if s.contains("no liquidity available for market order") => {
525 Self::MarketOrderNoLiquidity
526 }
527
528 s if s.contains("positionincreaseatopeninterestcap") => {
531 Self::PositionIncreaseAtOpenInterestCap
532 }
533 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
534 s if s.contains("tooaggressiveatopeninterestcap") => {
535 Self::TooAggressiveAtOpenInterestCap
536 }
537 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
538
539 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
541
542 s if s.contains("oracle") => Self::Oracle,
544
545 s if s.contains("max position") => Self::PerpMaxPosition,
547
548 s if s.contains("missingorder") => Self::MissingOrder,
550
551 _ => {
553 log::warn!(
554 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
556 Self::Unknown(error.to_string())
557 }
558 }
559 }
560
561 #[deprecated(
566 since = "0.50.0",
567 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
568 )]
569 pub fn from_error_string(error: &str) -> Self {
570 Self::from_error_string_internal(error)
571 }
572}
573
574#[derive(
578 Copy,
579 Clone,
580 Debug,
581 Display,
582 PartialEq,
583 Eq,
584 Hash,
585 AsRefStr,
586 EnumIter,
587 EnumString,
588 Serialize,
589 Deserialize,
590)]
591pub enum HyperliquidOrderStatus {
592 #[serde(rename = "open")]
594 Open,
595 #[serde(rename = "accepted")]
597 Accepted,
598 #[serde(rename = "triggered")]
600 Triggered,
601 #[serde(rename = "filled")]
603 Filled,
604 #[serde(rename = "canceled")]
606 Canceled,
607 #[serde(rename = "rejected")]
609 Rejected,
610 #[serde(rename = "marginCanceled")]
613 MarginCanceled,
614 #[serde(rename = "vaultWithdrawalCanceled")]
616 VaultWithdrawalCanceled,
617 #[serde(rename = "openInterestCapCanceled")]
619 OpenInterestCapCanceled,
620 #[serde(rename = "selfTradeCanceled")]
622 SelfTradeCanceled,
623 #[serde(rename = "reduceOnlyCanceled")]
625 ReduceOnlyCanceled,
626 #[serde(rename = "siblingFilledCanceled")]
628 SiblingFilledCanceled,
629 #[serde(rename = "delistedCanceled")]
631 DelistedCanceled,
632 #[serde(rename = "liquidatedCanceled")]
634 LiquidatedCanceled,
635 #[serde(rename = "scheduledCancel")]
637 ScheduledCancel,
638 #[serde(rename = "tickRejected")]
641 TickRejected,
642 #[serde(rename = "minTradeNtlRejected")]
644 MinTradeNtlRejected,
645 #[serde(rename = "perpMarginRejected")]
647 PerpMarginRejected,
648 #[serde(rename = "reduceOnlyRejected")]
650 ReduceOnlyRejected,
651 #[serde(rename = "badAloPxRejected")]
653 BadAloPxRejected,
654 #[serde(rename = "iocCancelRejected")]
656 IocCancelRejected,
657 #[serde(rename = "badTriggerPxRejected")]
659 BadTriggerPxRejected,
660 #[serde(rename = "marketOrderNoLiquidityRejected")]
662 MarketOrderNoLiquidityRejected,
663 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
665 PositionIncreaseAtOpenInterestCapRejected,
666 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
668 PositionFlipAtOpenInterestCapRejected,
669 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
671 TooAggressiveAtOpenInterestCapRejected,
672 #[serde(rename = "openInterestIncreaseRejected")]
674 OpenInterestIncreaseRejected,
675 #[serde(rename = "insufficientSpotBalanceRejected")]
677 InsufficientSpotBalanceRejected,
678 #[serde(rename = "oracleRejected")]
680 OracleRejected,
681 #[serde(rename = "perpMaxPositionRejected")]
683 PerpMaxPositionRejected,
684}
685
686impl From<HyperliquidOrderStatus> for OrderStatus {
687 fn from(status: HyperliquidOrderStatus) -> Self {
688 match status {
689 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
690 HyperliquidOrderStatus::Triggered => Self::Triggered,
691 HyperliquidOrderStatus::Filled => Self::Filled,
692 HyperliquidOrderStatus::Canceled
694 | HyperliquidOrderStatus::MarginCanceled
695 | HyperliquidOrderStatus::VaultWithdrawalCanceled
696 | HyperliquidOrderStatus::OpenInterestCapCanceled
697 | HyperliquidOrderStatus::SelfTradeCanceled
698 | HyperliquidOrderStatus::ReduceOnlyCanceled
699 | HyperliquidOrderStatus::SiblingFilledCanceled
700 | HyperliquidOrderStatus::DelistedCanceled
701 | HyperliquidOrderStatus::LiquidatedCanceled
702 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
703 HyperliquidOrderStatus::Rejected
705 | HyperliquidOrderStatus::TickRejected
706 | HyperliquidOrderStatus::MinTradeNtlRejected
707 | HyperliquidOrderStatus::PerpMarginRejected
708 | HyperliquidOrderStatus::ReduceOnlyRejected
709 | HyperliquidOrderStatus::BadAloPxRejected
710 | HyperliquidOrderStatus::IocCancelRejected
711 | HyperliquidOrderStatus::BadTriggerPxRejected
712 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
713 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
714 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
715 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
716 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
717 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
718 | HyperliquidOrderStatus::OracleRejected
719 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
720 }
721 }
722}
723
724#[derive(
735 Copy,
736 Clone,
737 Debug,
738 Display,
739 PartialEq,
740 Eq,
741 Hash,
742 AsRefStr,
743 EnumIter,
744 EnumString,
745 Serialize,
746 Deserialize,
747)]
748#[serde(rename_all = "PascalCase")]
749#[strum(serialize_all = "PascalCase")]
750pub enum HyperliquidFillDirection {
751 #[serde(rename = "Open Long")]
753 #[strum(serialize = "Open Long")]
754 OpenLong,
755 #[serde(rename = "Open Short")]
757 #[strum(serialize = "Open Short")]
758 OpenShort,
759 #[serde(rename = "Close Long")]
761 #[strum(serialize = "Close Long")]
762 CloseLong,
763 #[serde(rename = "Close Short")]
765 #[strum(serialize = "Close Short")]
766 CloseShort,
767 #[serde(rename = "Long > Short")]
769 #[strum(serialize = "Long > Short")]
770 LongToShort,
771 #[serde(rename = "Short > Long")]
773 #[strum(serialize = "Short > Long")]
774 ShortToLong,
775 Buy,
777 Sell,
779}
780
781#[derive(
785 Copy,
786 Clone,
787 Debug,
788 Display,
789 PartialEq,
790 Eq,
791 Hash,
792 AsRefStr,
793 EnumIter,
794 EnumString,
795 Serialize,
796 Deserialize,
797)]
798#[serde(rename_all = "camelCase")]
799#[strum(serialize_all = "camelCase")]
800pub enum HyperliquidInfoRequestType {
801 Meta,
803 SpotMeta,
805 MetaAndAssetCtxs,
807 SpotMetaAndAssetCtxs,
809 L2Book,
811 AllMids,
813 UserFills,
815 UserFillsByTime,
817 OrderStatus,
819 OpenOrders,
821 FrontendOpenOrders,
823 ClearinghouseState,
825 SpotClearinghouseState,
827 ExchangeStatus,
829 CandleSnapshot,
831 Candle,
833 RecentTrades,
835 HistoricalOrders,
837 FundingHistory,
839 UserFunding,
841 NonUserFundingUpdates,
843 TwapHistory,
845 UserTwapSliceFills,
847 UserTwapSliceFillsByTime,
849 UserRateLimit,
851 UserRole,
853 DelegatorHistory,
855 DelegatorRewards,
857 ValidatorStats,
859 UserFees,
861 AllPerpMetas,
863}
864
865impl HyperliquidInfoRequestType {
866 pub fn as_str(&self) -> &'static str {
867 match self {
868 Self::Meta => "meta",
869 Self::SpotMeta => "spotMeta",
870 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
871 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
872 Self::L2Book => "l2Book",
873 Self::AllMids => "allMids",
874 Self::UserFills => "userFills",
875 Self::UserFillsByTime => "userFillsByTime",
876 Self::OrderStatus => "orderStatus",
877 Self::OpenOrders => "openOrders",
878 Self::FrontendOpenOrders => "frontendOpenOrders",
879 Self::ClearinghouseState => "clearinghouseState",
880 Self::SpotClearinghouseState => "spotClearinghouseState",
881 Self::ExchangeStatus => "exchangeStatus",
882 Self::CandleSnapshot => "candleSnapshot",
883 Self::Candle => "candle",
884 Self::RecentTrades => "recentTrades",
885 Self::HistoricalOrders => "historicalOrders",
886 Self::FundingHistory => "fundingHistory",
887 Self::UserFunding => "userFunding",
888 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
889 Self::TwapHistory => "twapHistory",
890 Self::UserTwapSliceFills => "userTwapSliceFills",
891 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
892 Self::UserRateLimit => "userRateLimit",
893 Self::UserRole => "userRole",
894 Self::DelegatorHistory => "delegatorHistory",
895 Self::DelegatorRewards => "delegatorRewards",
896 Self::ValidatorStats => "validatorStats",
897 Self::UserFees => "userFees",
898 Self::AllPerpMetas => "allPerpMetas",
899 }
900 }
901}
902
903#[derive(
904 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
905)]
906#[serde(rename_all = "lowercase")]
907#[strum(serialize_all = "lowercase")]
908pub enum HyperliquidLeverageType {
909 Cross,
910 Isolated,
911 #[serde(other)]
912 Unknown,
913}
914
915#[derive(
917 Copy,
918 Clone,
919 Debug,
920 Display,
921 PartialEq,
922 Eq,
923 Hash,
924 AsRefStr,
925 EnumIter,
926 EnumString,
927 Serialize,
928 Deserialize,
929)]
930#[cfg_attr(
931 feature = "python",
932 pyo3::pyclass(
933 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
934 from_py_object,
935 rename_all = "SCREAMING_SNAKE_CASE",
936 )
937)]
938#[cfg_attr(
939 feature = "python",
940 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
941)]
942#[serde(rename_all = "UPPERCASE")]
943#[strum(serialize_all = "UPPERCASE")]
944pub enum HyperliquidProductType {
945 Perp,
947 Spot,
949}
950
951impl HyperliquidProductType {
952 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
958 if symbol.ends_with("-PERP") {
959 Ok(Self::Perp)
960 } else if symbol.ends_with("-SPOT") {
961 Ok(Self::Spot)
962 } else {
963 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
964 }
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use nautilus_model::enums::OrderType;
971 use rstest::rstest;
972 use serde_json;
973
974 use super::*;
975
976 #[rstest]
977 fn test_side_serde() {
978 let buy_side = HyperliquidSide::Buy;
979 let sell_side = HyperliquidSide::Sell;
980
981 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
982 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
983
984 assert_eq!(
985 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
986 HyperliquidSide::Buy
987 );
988 assert_eq!(
989 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
990 HyperliquidSide::Sell
991 );
992 }
993
994 #[rstest]
995 fn test_side_from_order_side() {
996 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
998 assert_eq!(
999 HyperliquidSide::from(OrderSide::Sell),
1000 HyperliquidSide::Sell
1001 );
1002 }
1003
1004 #[rstest]
1005 fn test_order_side_from_hyperliquid_side() {
1006 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
1008 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
1009 }
1010
1011 #[rstest]
1012 fn test_aggressor_side_from_hyperliquid_side() {
1013 assert_eq!(
1015 AggressorSide::from(HyperliquidSide::Buy),
1016 AggressorSide::Buyer
1017 );
1018 assert_eq!(
1019 AggressorSide::from(HyperliquidSide::Sell),
1020 AggressorSide::Seller
1021 );
1022 }
1023
1024 #[rstest]
1025 fn test_time_in_force_serde() {
1026 let test_cases = [
1027 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1028 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1029 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1030 ];
1031
1032 for (tif, expected_json) in test_cases {
1033 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1034 assert_eq!(
1035 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1036 tif
1037 );
1038 }
1039 }
1040
1041 #[rstest]
1042 fn test_liquidity_flag_from_crossed() {
1043 assert_eq!(
1044 HyperliquidLiquidityFlag::from(true),
1045 HyperliquidLiquidityFlag::Taker
1046 );
1047 assert_eq!(
1048 HyperliquidLiquidityFlag::from(false),
1049 HyperliquidLiquidityFlag::Maker
1050 );
1051 }
1052
1053 #[rstest]
1054 #[allow(deprecated)]
1055 fn test_reject_code_from_error_string() {
1056 let test_cases = [
1057 (
1058 "Price must be divisible by tick size.",
1059 HyperliquidRejectCode::Tick,
1060 ),
1061 (
1062 "Order must have minimum value of $10.",
1063 HyperliquidRejectCode::MinTradeNtl,
1064 ),
1065 (
1066 "Insufficient margin to place order.",
1067 HyperliquidRejectCode::PerpMargin,
1068 ),
1069 (
1070 "Post only order would have immediately matched, bbo was 1.23",
1071 HyperliquidRejectCode::BadAloPx,
1072 ),
1073 (
1074 "Some unknown error",
1075 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1076 ),
1077 ];
1078
1079 for (error_str, expected_code) in test_cases {
1080 assert_eq!(
1081 HyperliquidRejectCode::from_error_string(error_str),
1082 expected_code
1083 );
1084 }
1085 }
1086
1087 #[rstest]
1088 fn test_reject_code_from_api_error() {
1089 let test_cases = [
1090 (
1091 "Price must be divisible by tick size.",
1092 HyperliquidRejectCode::Tick,
1093 ),
1094 (
1095 "Order must have minimum value of $10.",
1096 HyperliquidRejectCode::MinTradeNtl,
1097 ),
1098 (
1099 "Insufficient margin to place order.",
1100 HyperliquidRejectCode::PerpMargin,
1101 ),
1102 (
1103 "Post only order would have immediately matched, bbo was 1.23",
1104 HyperliquidRejectCode::BadAloPx,
1105 ),
1106 (
1107 "Some unknown error",
1108 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1109 ),
1110 ];
1111
1112 for (error_str, expected_code) in test_cases {
1113 assert_eq!(
1114 HyperliquidRejectCode::from_api_error(error_str),
1115 expected_code
1116 );
1117 }
1118 }
1119
1120 #[rstest]
1121 fn test_reduce_only() {
1122 let reduce_only = HyperliquidReduceOnly::new(true);
1123
1124 assert!(reduce_only.is_reduce_only());
1125
1126 let json = serde_json::to_string(&reduce_only).unwrap();
1127 assert_eq!(json, "true");
1128
1129 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1130 assert_eq!(parsed, reduce_only);
1131 }
1132
1133 #[rstest]
1134 fn test_order_status_conversion() {
1135 assert_eq!(
1137 OrderStatus::from(HyperliquidOrderStatus::Open),
1138 OrderStatus::Accepted
1139 );
1140 assert_eq!(
1141 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1142 OrderStatus::Accepted
1143 );
1144 assert_eq!(
1145 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1146 OrderStatus::Triggered
1147 );
1148 assert_eq!(
1149 OrderStatus::from(HyperliquidOrderStatus::Filled),
1150 OrderStatus::Filled
1151 );
1152 assert_eq!(
1153 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1154 OrderStatus::Canceled
1155 );
1156 assert_eq!(
1157 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1158 OrderStatus::Rejected
1159 );
1160
1161 assert_eq!(
1163 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1164 OrderStatus::Canceled
1165 );
1166 assert_eq!(
1167 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1168 OrderStatus::Canceled
1169 );
1170 assert_eq!(
1171 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1172 OrderStatus::Canceled
1173 );
1174
1175 assert_eq!(
1177 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1178 OrderStatus::Rejected
1179 );
1180 assert_eq!(
1181 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1182 OrderStatus::Rejected
1183 );
1184 }
1185
1186 #[rstest]
1187 fn test_order_status_serde_deserialization() {
1188 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1190 assert_eq!(open, HyperliquidOrderStatus::Open);
1191
1192 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1193 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1194
1195 let margin_canceled: HyperliquidOrderStatus =
1196 serde_json::from_str(r#""marginCanceled""#).unwrap();
1197 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1198
1199 let self_trade_canceled: HyperliquidOrderStatus =
1200 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1201 assert_eq!(
1202 self_trade_canceled,
1203 HyperliquidOrderStatus::SelfTradeCanceled
1204 );
1205
1206 let reduce_only_canceled: HyperliquidOrderStatus =
1207 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1208 assert_eq!(
1209 reduce_only_canceled,
1210 HyperliquidOrderStatus::ReduceOnlyCanceled
1211 );
1212
1213 let tick_rejected: HyperliquidOrderStatus =
1214 serde_json::from_str(r#""tickRejected""#).unwrap();
1215 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1216 }
1217
1218 #[rstest]
1219 fn test_hyperliquid_tpsl_serialization() {
1220 let tp = HyperliquidTpSl::Tp;
1221 let sl = HyperliquidTpSl::Sl;
1222
1223 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1224 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1225 }
1226
1227 #[rstest]
1228 fn test_hyperliquid_tpsl_deserialization() {
1229 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1230 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1231
1232 assert_eq!(tp, HyperliquidTpSl::Tp);
1233 assert_eq!(sl, HyperliquidTpSl::Sl);
1234 }
1235
1236 #[rstest]
1237 fn test_conditional_order_type_conversions() {
1238 assert_eq!(
1240 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1241 OrderType::StopMarket
1242 );
1243 assert_eq!(
1244 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1245 OrderType::StopLimit
1246 );
1247 assert_eq!(
1248 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1249 OrderType::MarketIfTouched
1250 );
1251 assert_eq!(
1252 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1253 OrderType::LimitIfTouched
1254 );
1255 assert_eq!(
1256 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1257 OrderType::TrailingStopMarket
1258 );
1259 }
1260
1261 mod error_parsing_tests {
1263 use super::*;
1264
1265 #[rstest]
1266 fn test_parse_tick_size_error() {
1267 let error = "Price must be divisible by tick size 0.01";
1268 let code = HyperliquidRejectCode::from_api_error(error);
1269 assert_eq!(code, HyperliquidRejectCode::Tick);
1270 }
1271
1272 #[rstest]
1273 fn test_parse_tick_size_error_case_insensitive() {
1274 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1275 let code = HyperliquidRejectCode::from_api_error(error);
1276 assert_eq!(code, HyperliquidRejectCode::Tick);
1277 }
1278
1279 #[rstest]
1280 fn test_parse_min_notional_perp() {
1281 let error = "Order must have minimum value of $10";
1282 let code = HyperliquidRejectCode::from_api_error(error);
1283 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1284 }
1285
1286 #[rstest]
1287 fn test_parse_min_notional_spot() {
1288 let error = "Order must have minimum value of 10 USDC";
1289 let code = HyperliquidRejectCode::from_api_error(error);
1290 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1291 }
1292
1293 #[rstest]
1294 fn test_parse_insufficient_margin() {
1295 let error = "Insufficient margin to place order";
1296 let code = HyperliquidRejectCode::from_api_error(error);
1297 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1298 }
1299
1300 #[rstest]
1301 fn test_parse_insufficient_margin_case_variations() {
1302 let variations = vec![
1303 "insufficient margin to place order",
1304 "INSUFFICIENT MARGIN TO PLACE ORDER",
1305 " Insufficient margin to place order ", ];
1307
1308 for error in variations {
1309 let code = HyperliquidRejectCode::from_api_error(error);
1310 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1311 }
1312 }
1313
1314 #[rstest]
1315 fn test_parse_reduce_only_violation() {
1316 let error = "Reduce only order would increase position";
1317 let code = HyperliquidRejectCode::from_api_error(error);
1318 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1319 }
1320
1321 #[rstest]
1322 fn test_parse_reduce_only_with_hyphen() {
1323 let error = "Reduce-only order would increase position";
1324 let code = HyperliquidRejectCode::from_api_error(error);
1325 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1326 }
1327
1328 #[rstest]
1329 fn test_parse_post_only_match() {
1330 let error = "Post only order would have immediately matched";
1331 let code = HyperliquidRejectCode::from_api_error(error);
1332 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1333 }
1334
1335 #[rstest]
1336 fn test_parse_post_only_with_hyphen() {
1337 let error = "Post-only order would have immediately matched";
1338 let code = HyperliquidRejectCode::from_api_error(error);
1339 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1340 }
1341
1342 #[rstest]
1343 fn test_parse_ioc_no_match() {
1344 let error = "Order could not immediately match";
1345 let code = HyperliquidRejectCode::from_api_error(error);
1346 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1347 }
1348
1349 #[rstest]
1350 fn test_parse_invalid_trigger_price() {
1351 let error = "Invalid TP/SL price";
1352 let code = HyperliquidRejectCode::from_api_error(error);
1353 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1354 }
1355
1356 #[rstest]
1357 fn test_parse_no_liquidity() {
1358 let error = "No liquidity available for market order";
1359 let code = HyperliquidRejectCode::from_api_error(error);
1360 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1361 }
1362
1363 #[rstest]
1364 fn test_parse_position_increase_at_oi_cap() {
1365 let error = "PositionIncreaseAtOpenInterestCap";
1366 let code = HyperliquidRejectCode::from_api_error(error);
1367 assert_eq!(
1368 code,
1369 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1370 );
1371 }
1372
1373 #[rstest]
1374 fn test_parse_position_flip_at_oi_cap() {
1375 let error = "PositionFlipAtOpenInterestCap";
1376 let code = HyperliquidRejectCode::from_api_error(error);
1377 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1378 }
1379
1380 #[rstest]
1381 fn test_parse_too_aggressive_at_oi_cap() {
1382 let error = "TooAggressiveAtOpenInterestCap";
1383 let code = HyperliquidRejectCode::from_api_error(error);
1384 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1385 }
1386
1387 #[rstest]
1388 fn test_parse_open_interest_increase() {
1389 let error = "OpenInterestIncrease";
1390 let code = HyperliquidRejectCode::from_api_error(error);
1391 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1392 }
1393
1394 #[rstest]
1395 fn test_parse_insufficient_spot_balance() {
1396 let error = "Insufficient spot balance";
1397 let code = HyperliquidRejectCode::from_api_error(error);
1398 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1399 }
1400
1401 #[rstest]
1402 fn test_parse_oracle_error() {
1403 let error = "Oracle price unavailable";
1404 let code = HyperliquidRejectCode::from_api_error(error);
1405 assert_eq!(code, HyperliquidRejectCode::Oracle);
1406 }
1407
1408 #[rstest]
1409 fn test_parse_max_position() {
1410 let error = "Exceeds max position size";
1411 let code = HyperliquidRejectCode::from_api_error(error);
1412 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1413 }
1414
1415 #[rstest]
1416 fn test_parse_missing_order() {
1417 let error = "MissingOrder";
1418 let code = HyperliquidRejectCode::from_api_error(error);
1419 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1420 }
1421
1422 #[rstest]
1423 fn test_parse_unknown_error() {
1424 let error = "This is a completely new error message";
1425 let code = HyperliquidRejectCode::from_api_error(error);
1426 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1427
1428 if let HyperliquidRejectCode::Unknown(msg) = code {
1430 assert_eq!(msg, error);
1431 }
1432 }
1433
1434 #[rstest]
1435 fn test_parse_empty_error() {
1436 let error = "";
1437 let code = HyperliquidRejectCode::from_api_error(error);
1438 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1439 }
1440
1441 #[rstest]
1442 fn test_parse_whitespace_only() {
1443 let error = " ";
1444 let code = HyperliquidRejectCode::from_api_error(error);
1445 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1446 }
1447
1448 #[rstest]
1449 fn test_normalization_preserves_original_in_unknown() {
1450 let error = " UNKNOWN ERROR MESSAGE ";
1451 let code = HyperliquidRejectCode::from_api_error(error);
1452
1453 if let HyperliquidRejectCode::Unknown(msg) = code {
1455 assert_eq!(msg, error);
1456 } else {
1457 panic!("Expected Unknown variant");
1458 }
1459 }
1460 }
1461
1462 #[rstest]
1463 fn test_conditional_order_type_round_trip() {
1464 assert_eq!(
1465 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1466 OrderType::TrailingStopLimit
1467 );
1468
1469 assert_eq!(
1471 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1472 HyperliquidConditionalOrderType::StopMarket
1473 );
1474 assert_eq!(
1475 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1476 HyperliquidConditionalOrderType::StopLimit
1477 );
1478 }
1479
1480 #[rstest]
1481 fn test_trailing_offset_type_serialization() {
1482 let price = HyperliquidTrailingOffsetType::Price;
1483 let percentage = HyperliquidTrailingOffsetType::Percentage;
1484 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1485
1486 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1487 assert_eq!(
1488 serde_json::to_string(&percentage).unwrap(),
1489 r#""percentage""#
1490 );
1491 assert_eq!(
1492 serde_json::to_string(&basis_points).unwrap(),
1493 r#""basispoints""#
1494 );
1495 }
1496
1497 #[rstest]
1498 fn test_conditional_order_type_serialization() {
1499 assert_eq!(
1500 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1501 r#""STOP_MARKET""#
1502 );
1503 assert_eq!(
1504 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1505 r#""STOP_LIMIT""#
1506 );
1507 assert_eq!(
1508 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1509 r#""TAKE_PROFIT_MARKET""#
1510 );
1511 assert_eq!(
1512 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1513 r#""TAKE_PROFIT_LIMIT""#
1514 );
1515 assert_eq!(
1516 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1517 r#""TRAILING_STOP_MARKET""#
1518 );
1519 assert_eq!(
1520 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1521 r#""TRAILING_STOP_LIMIT""#
1522 );
1523 }
1524
1525 #[rstest]
1526 fn test_order_type_enum_coverage() {
1527 let conditional_types = vec![
1529 HyperliquidConditionalOrderType::StopMarket,
1530 HyperliquidConditionalOrderType::StopLimit,
1531 HyperliquidConditionalOrderType::TakeProfitMarket,
1532 HyperliquidConditionalOrderType::TakeProfitLimit,
1533 HyperliquidConditionalOrderType::TrailingStopMarket,
1534 HyperliquidConditionalOrderType::TrailingStopLimit,
1535 ];
1536
1537 for cond_type in conditional_types {
1538 let order_type = OrderType::from(cond_type);
1539 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1540 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1541 }
1542 }
1543}