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 #[serde(other)]
417 Unknown,
418}
419
420#[derive(
422 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
423)]
424#[serde(rename_all = "camelCase")]
425#[strum(serialize_all = "camelCase")]
426pub enum HyperliquidPositionType {
427 OneWay,
428}
429
430#[derive(
432 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
433)]
434#[serde(rename_all = "lowercase")]
435#[strum(serialize_all = "lowercase")]
436pub enum HyperliquidTwapStatus {
437 Activated,
438 Terminated,
439 Finished,
440 Error,
441}
442
443#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
444#[serde(untagged)]
445pub enum HyperliquidRejectCode {
446 Tick,
448 MinTradeNtl,
450 MinTradeSpotNtl,
452 PerpMargin,
454 ReduceOnly,
456 BadAloPx,
458 IocCancel,
460 BadTriggerPx,
462 MarketOrderNoLiquidity,
464 PositionIncreaseAtOpenInterestCap,
466 PositionFlipAtOpenInterestCap,
468 TooAggressiveAtOpenInterestCap,
470 OpenInterestIncrease,
472 InsufficientSpotBalance,
474 Oracle,
476 PerpMaxPosition,
478 MissingOrder,
480 Unknown(String),
482}
483
484impl HyperliquidRejectCode {
485 pub fn from_api_error(error_message: &str) -> Self {
487 Self::from_error_string_internal(error_message)
488 }
489
490 fn from_error_string_internal(error: &str) -> Self {
491 let normalized = error.trim().to_lowercase();
493
494 match normalized.as_str() {
495 s if s.contains("tick size") => Self::Tick,
497
498 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
500 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
501
502 s if s.contains("insufficient margin") => Self::PerpMargin,
504
505 s if s.contains("reduce only order would increase")
507 || s.contains("reduce-only order would increase") =>
508 {
509 Self::ReduceOnly
510 }
511
512 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
514 || s.contains("post-only order would have immediately matched") =>
515 {
516 Self::BadAloPx
517 }
518
519 s if s.contains("could not immediately match") => Self::IocCancel,
521
522 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
524
525 s if s.contains("no liquidity available for market order") => {
527 Self::MarketOrderNoLiquidity
528 }
529
530 s if s.contains("positionincreaseatopeninterestcap") => {
533 Self::PositionIncreaseAtOpenInterestCap
534 }
535 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
536 s if s.contains("tooaggressiveatopeninterestcap") => {
537 Self::TooAggressiveAtOpenInterestCap
538 }
539 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
540
541 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
543
544 s if s.contains("oracle") => Self::Oracle,
546
547 s if s.contains("max position") => Self::PerpMaxPosition,
549
550 s if s.contains("missingorder") => Self::MissingOrder,
552
553 _ => {
555 log::warn!(
556 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
558 Self::Unknown(error.to_string())
559 }
560 }
561 }
562
563 #[deprecated(
568 since = "0.50.0",
569 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
570 )]
571 pub fn from_error_string(error: &str) -> Self {
572 Self::from_error_string_internal(error)
573 }
574}
575
576#[derive(
580 Copy,
581 Clone,
582 Debug,
583 Display,
584 PartialEq,
585 Eq,
586 Hash,
587 AsRefStr,
588 EnumIter,
589 EnumString,
590 Serialize,
591 Deserialize,
592)]
593pub enum HyperliquidOrderStatus {
594 #[serde(rename = "open")]
596 Open,
597 #[serde(rename = "accepted")]
599 Accepted,
600 #[serde(rename = "triggered")]
602 Triggered,
603 #[serde(rename = "filled")]
605 Filled,
606 #[serde(rename = "canceled")]
608 Canceled,
609 #[serde(rename = "rejected")]
611 Rejected,
612 #[serde(rename = "marginCanceled")]
615 MarginCanceled,
616 #[serde(rename = "vaultWithdrawalCanceled")]
618 VaultWithdrawalCanceled,
619 #[serde(rename = "openInterestCapCanceled")]
621 OpenInterestCapCanceled,
622 #[serde(rename = "selfTradeCanceled")]
624 SelfTradeCanceled,
625 #[serde(rename = "reduceOnlyCanceled")]
627 ReduceOnlyCanceled,
628 #[serde(rename = "siblingFilledCanceled")]
630 SiblingFilledCanceled,
631 #[serde(rename = "delistedCanceled")]
633 DelistedCanceled,
634 #[serde(rename = "liquidatedCanceled")]
636 LiquidatedCanceled,
637 #[serde(rename = "scheduledCancel")]
639 ScheduledCancel,
640 #[serde(rename = "tickRejected")]
643 TickRejected,
644 #[serde(rename = "minTradeNtlRejected")]
646 MinTradeNtlRejected,
647 #[serde(rename = "minTradeSpotNtlRejected")]
649 MinTradeSpotNtlRejected,
650 #[serde(rename = "perpMarginRejected")]
652 PerpMarginRejected,
653 #[serde(rename = "reduceOnlyRejected")]
655 ReduceOnlyRejected,
656 #[serde(rename = "badAloPxRejected")]
658 BadAloPxRejected,
659 #[serde(rename = "iocCancelRejected")]
661 IocCancelRejected,
662 #[serde(rename = "badTriggerPxRejected")]
664 BadTriggerPxRejected,
665 #[serde(rename = "marketOrderNoLiquidityRejected")]
667 MarketOrderNoLiquidityRejected,
668 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
670 PositionIncreaseAtOpenInterestCapRejected,
671 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
673 PositionFlipAtOpenInterestCapRejected,
674 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
676 TooAggressiveAtOpenInterestCapRejected,
677 #[serde(rename = "openInterestIncreaseRejected")]
679 OpenInterestIncreaseRejected,
680 #[serde(rename = "insufficientSpotBalanceRejected")]
682 InsufficientSpotBalanceRejected,
683 #[serde(rename = "oracleRejected")]
685 OracleRejected,
686 #[serde(rename = "perpMaxPositionRejected")]
688 PerpMaxPositionRejected,
689}
690
691impl From<HyperliquidOrderStatus> for OrderStatus {
692 fn from(status: HyperliquidOrderStatus) -> Self {
693 match status {
694 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
695 HyperliquidOrderStatus::Triggered => Self::Triggered,
696 HyperliquidOrderStatus::Filled => Self::Filled,
697 HyperliquidOrderStatus::Canceled
699 | HyperliquidOrderStatus::MarginCanceled
700 | HyperliquidOrderStatus::VaultWithdrawalCanceled
701 | HyperliquidOrderStatus::OpenInterestCapCanceled
702 | HyperliquidOrderStatus::SelfTradeCanceled
703 | HyperliquidOrderStatus::ReduceOnlyCanceled
704 | HyperliquidOrderStatus::SiblingFilledCanceled
705 | HyperliquidOrderStatus::DelistedCanceled
706 | HyperliquidOrderStatus::LiquidatedCanceled
707 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
708 HyperliquidOrderStatus::Rejected
710 | HyperliquidOrderStatus::TickRejected
711 | HyperliquidOrderStatus::MinTradeNtlRejected
712 | HyperliquidOrderStatus::MinTradeSpotNtlRejected
713 | HyperliquidOrderStatus::PerpMarginRejected
714 | HyperliquidOrderStatus::ReduceOnlyRejected
715 | HyperliquidOrderStatus::BadAloPxRejected
716 | HyperliquidOrderStatus::IocCancelRejected
717 | HyperliquidOrderStatus::BadTriggerPxRejected
718 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
719 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
720 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
721 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
722 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
723 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
724 | HyperliquidOrderStatus::OracleRejected
725 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
726 }
727 }
728}
729
730#[derive(
741 Copy,
742 Clone,
743 Debug,
744 Display,
745 PartialEq,
746 Eq,
747 Hash,
748 AsRefStr,
749 EnumIter,
750 EnumString,
751 Serialize,
752 Deserialize,
753)]
754#[serde(rename_all = "PascalCase")]
755#[strum(serialize_all = "PascalCase")]
756pub enum HyperliquidFillDirection {
757 #[serde(rename = "Open Long")]
759 #[strum(serialize = "Open Long")]
760 OpenLong,
761 #[serde(rename = "Open Short")]
763 #[strum(serialize = "Open Short")]
764 OpenShort,
765 #[serde(rename = "Close Long")]
767 #[strum(serialize = "Close Long")]
768 CloseLong,
769 #[serde(rename = "Close Short")]
771 #[strum(serialize = "Close Short")]
772 CloseShort,
773 #[serde(rename = "Long > Short")]
775 #[strum(serialize = "Long > Short")]
776 LongToShort,
777 #[serde(rename = "Short > Long")]
779 #[strum(serialize = "Short > Long")]
780 ShortToLong,
781 #[serde(rename = "Auto-Deleveraging")]
783 #[strum(serialize = "Auto-Deleveraging")]
784 AutoDeleveraging,
785 Buy,
787 Sell,
789}
790
791#[derive(
795 Copy,
796 Clone,
797 Debug,
798 Display,
799 PartialEq,
800 Eq,
801 Hash,
802 AsRefStr,
803 EnumIter,
804 EnumString,
805 Serialize,
806 Deserialize,
807)]
808#[serde(rename_all = "camelCase")]
809#[strum(serialize_all = "camelCase")]
810pub enum HyperliquidInfoRequestType {
811 Meta,
813 SpotMeta,
815 MetaAndAssetCtxs,
817 SpotMetaAndAssetCtxs,
819 L2Book,
821 AllMids,
823 UserFills,
825 UserFillsByTime,
827 OrderStatus,
829 OpenOrders,
831 FrontendOpenOrders,
833 ClearinghouseState,
835 SpotClearinghouseState,
837 ExchangeStatus,
839 CandleSnapshot,
841 Candle,
843 HistoricalOrders,
845 FundingHistory,
847 UserFunding,
849 NonUserFundingUpdates,
851 TwapHistory,
853 UserTwapSliceFills,
855 UserTwapSliceFillsByTime,
857 UserRateLimit,
859 UserRole,
861 DelegatorHistory,
863 DelegatorRewards,
865 ValidatorStats,
867 UserFees,
869 AllPerpMetas,
871}
872
873impl HyperliquidInfoRequestType {
874 pub fn as_str(&self) -> &'static str {
875 match self {
876 Self::Meta => "meta",
877 Self::SpotMeta => "spotMeta",
878 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
879 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
880 Self::L2Book => "l2Book",
881 Self::AllMids => "allMids",
882 Self::UserFills => "userFills",
883 Self::UserFillsByTime => "userFillsByTime",
884 Self::OrderStatus => "orderStatus",
885 Self::OpenOrders => "openOrders",
886 Self::FrontendOpenOrders => "frontendOpenOrders",
887 Self::ClearinghouseState => "clearinghouseState",
888 Self::SpotClearinghouseState => "spotClearinghouseState",
889 Self::ExchangeStatus => "exchangeStatus",
890 Self::CandleSnapshot => "candleSnapshot",
891 Self::Candle => "candle",
892 Self::HistoricalOrders => "historicalOrders",
893 Self::FundingHistory => "fundingHistory",
894 Self::UserFunding => "userFunding",
895 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
896 Self::TwapHistory => "twapHistory",
897 Self::UserTwapSliceFills => "userTwapSliceFills",
898 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
899 Self::UserRateLimit => "userRateLimit",
900 Self::UserRole => "userRole",
901 Self::DelegatorHistory => "delegatorHistory",
902 Self::DelegatorRewards => "delegatorRewards",
903 Self::ValidatorStats => "validatorStats",
904 Self::UserFees => "userFees",
905 Self::AllPerpMetas => "allPerpMetas",
906 }
907 }
908}
909
910#[derive(
911 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
912)]
913#[serde(rename_all = "lowercase")]
914#[strum(serialize_all = "lowercase")]
915pub enum HyperliquidLeverageType {
916 Cross,
917 Isolated,
918 #[serde(other)]
919 Unknown,
920}
921
922#[derive(
924 Copy,
925 Clone,
926 Debug,
927 Display,
928 PartialEq,
929 Eq,
930 Hash,
931 AsRefStr,
932 EnumIter,
933 EnumString,
934 Serialize,
935 Deserialize,
936)]
937#[cfg_attr(
938 feature = "python",
939 pyo3::pyclass(
940 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
941 from_py_object,
942 rename_all = "SCREAMING_SNAKE_CASE",
943 )
944)]
945#[cfg_attr(
946 feature = "python",
947 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
948)]
949#[serde(rename_all = "UPPERCASE")]
950#[strum(serialize_all = "UPPERCASE")]
951pub enum HyperliquidProductType {
952 Perp,
954 Spot,
956}
957
958impl HyperliquidProductType {
959 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
965 if symbol.ends_with("-PERP") {
966 Ok(Self::Perp)
967 } else if symbol.ends_with("-SPOT") {
968 Ok(Self::Spot)
969 } else {
970 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
971 }
972 }
973}
974
975#[derive(
977 Copy,
978 Clone,
979 Debug,
980 Default,
981 Display,
982 PartialEq,
983 Eq,
984 Hash,
985 AsRefStr,
986 EnumIter,
987 EnumString,
988 Serialize,
989 Deserialize,
990)]
991#[serde(rename_all = "lowercase")]
992#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
993#[cfg_attr(
994 feature = "python",
995 pyo3::pyclass(
996 eq,
997 eq_int,
998 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
999 from_py_object,
1000 rename_all = "SCREAMING_SNAKE_CASE",
1001 )
1002)]
1003#[cfg_attr(
1004 feature = "python",
1005 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
1006)]
1007pub enum HyperliquidEnvironment {
1008 #[default]
1010 Mainnet,
1011 Testnet,
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use nautilus_model::enums::OrderType;
1018 use rstest::rstest;
1019 use serde_json;
1020
1021 use super::*;
1022
1023 #[rstest]
1024 fn test_side_serde() {
1025 let buy_side = HyperliquidSide::Buy;
1026 let sell_side = HyperliquidSide::Sell;
1027
1028 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
1029 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
1030
1031 assert_eq!(
1032 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
1033 HyperliquidSide::Buy
1034 );
1035 assert_eq!(
1036 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
1037 HyperliquidSide::Sell
1038 );
1039 }
1040
1041 #[rstest]
1042 fn test_side_from_order_side() {
1043 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
1045 assert_eq!(
1046 HyperliquidSide::from(OrderSide::Sell),
1047 HyperliquidSide::Sell
1048 );
1049 }
1050
1051 #[rstest]
1052 fn test_order_side_from_hyperliquid_side() {
1053 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
1055 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
1056 }
1057
1058 #[rstest]
1059 fn test_aggressor_side_from_hyperliquid_side() {
1060 assert_eq!(
1062 AggressorSide::from(HyperliquidSide::Buy),
1063 AggressorSide::Buyer
1064 );
1065 assert_eq!(
1066 AggressorSide::from(HyperliquidSide::Sell),
1067 AggressorSide::Seller
1068 );
1069 }
1070
1071 #[rstest]
1072 fn test_time_in_force_serde() {
1073 let test_cases = [
1074 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1075 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1076 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1077 ];
1078
1079 for (tif, expected_json) in test_cases {
1080 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1081 assert_eq!(
1082 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1083 tif
1084 );
1085 }
1086 }
1087
1088 #[rstest]
1089 fn test_liquidity_flag_from_crossed() {
1090 assert_eq!(
1091 HyperliquidLiquidityFlag::from(true),
1092 HyperliquidLiquidityFlag::Taker
1093 );
1094 assert_eq!(
1095 HyperliquidLiquidityFlag::from(false),
1096 HyperliquidLiquidityFlag::Maker
1097 );
1098 }
1099
1100 #[rstest]
1101 #[allow(deprecated)]
1102 fn test_reject_code_from_error_string() {
1103 let test_cases = [
1104 (
1105 "Price must be divisible by tick size.",
1106 HyperliquidRejectCode::Tick,
1107 ),
1108 (
1109 "Order must have minimum value of $10.",
1110 HyperliquidRejectCode::MinTradeNtl,
1111 ),
1112 (
1113 "Insufficient margin to place order.",
1114 HyperliquidRejectCode::PerpMargin,
1115 ),
1116 (
1117 "Post only order would have immediately matched, bbo was 1.23",
1118 HyperliquidRejectCode::BadAloPx,
1119 ),
1120 (
1121 "Some unknown error",
1122 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1123 ),
1124 ];
1125
1126 for (error_str, expected_code) in test_cases {
1127 assert_eq!(
1128 HyperliquidRejectCode::from_error_string(error_str),
1129 expected_code
1130 );
1131 }
1132 }
1133
1134 #[rstest]
1135 fn test_reject_code_from_api_error() {
1136 let test_cases = [
1137 (
1138 "Price must be divisible by tick size.",
1139 HyperliquidRejectCode::Tick,
1140 ),
1141 (
1142 "Order must have minimum value of $10.",
1143 HyperliquidRejectCode::MinTradeNtl,
1144 ),
1145 (
1146 "Insufficient margin to place order.",
1147 HyperliquidRejectCode::PerpMargin,
1148 ),
1149 (
1150 "Post only order would have immediately matched, bbo was 1.23",
1151 HyperliquidRejectCode::BadAloPx,
1152 ),
1153 (
1154 "Some unknown error",
1155 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1156 ),
1157 ];
1158
1159 for (error_str, expected_code) in test_cases {
1160 assert_eq!(
1161 HyperliquidRejectCode::from_api_error(error_str),
1162 expected_code
1163 );
1164 }
1165 }
1166
1167 #[rstest]
1168 fn test_reduce_only() {
1169 let reduce_only = HyperliquidReduceOnly::new(true);
1170
1171 assert!(reduce_only.is_reduce_only());
1172
1173 let json = serde_json::to_string(&reduce_only).unwrap();
1174 assert_eq!(json, "true");
1175
1176 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1177 assert_eq!(parsed, reduce_only);
1178 }
1179
1180 #[rstest]
1181 fn test_order_status_conversion() {
1182 assert_eq!(
1184 OrderStatus::from(HyperliquidOrderStatus::Open),
1185 OrderStatus::Accepted
1186 );
1187 assert_eq!(
1188 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1189 OrderStatus::Accepted
1190 );
1191 assert_eq!(
1192 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1193 OrderStatus::Triggered
1194 );
1195 assert_eq!(
1196 OrderStatus::from(HyperliquidOrderStatus::Filled),
1197 OrderStatus::Filled
1198 );
1199 assert_eq!(
1200 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1201 OrderStatus::Canceled
1202 );
1203 assert_eq!(
1204 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1205 OrderStatus::Rejected
1206 );
1207
1208 assert_eq!(
1210 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1211 OrderStatus::Canceled
1212 );
1213 assert_eq!(
1214 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1215 OrderStatus::Canceled
1216 );
1217 assert_eq!(
1218 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1219 OrderStatus::Canceled
1220 );
1221
1222 assert_eq!(
1224 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1225 OrderStatus::Rejected
1226 );
1227 assert_eq!(
1228 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1229 OrderStatus::Rejected
1230 );
1231 }
1232
1233 #[rstest]
1234 fn test_order_status_serde_deserialization() {
1235 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1237 assert_eq!(open, HyperliquidOrderStatus::Open);
1238
1239 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1240 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1241
1242 let margin_canceled: HyperliquidOrderStatus =
1243 serde_json::from_str(r#""marginCanceled""#).unwrap();
1244 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1245
1246 let self_trade_canceled: HyperliquidOrderStatus =
1247 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1248 assert_eq!(
1249 self_trade_canceled,
1250 HyperliquidOrderStatus::SelfTradeCanceled
1251 );
1252
1253 let reduce_only_canceled: HyperliquidOrderStatus =
1254 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1255 assert_eq!(
1256 reduce_only_canceled,
1257 HyperliquidOrderStatus::ReduceOnlyCanceled
1258 );
1259
1260 let tick_rejected: HyperliquidOrderStatus =
1261 serde_json::from_str(r#""tickRejected""#).unwrap();
1262 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1263 }
1264
1265 #[rstest]
1266 fn test_hyperliquid_tpsl_serialization() {
1267 let tp = HyperliquidTpSl::Tp;
1268 let sl = HyperliquidTpSl::Sl;
1269
1270 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1271 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1272 }
1273
1274 #[rstest]
1275 fn test_hyperliquid_tpsl_deserialization() {
1276 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1277 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1278
1279 assert_eq!(tp, HyperliquidTpSl::Tp);
1280 assert_eq!(sl, HyperliquidTpSl::Sl);
1281 }
1282
1283 #[rstest]
1284 fn test_conditional_order_type_conversions() {
1285 assert_eq!(
1287 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1288 OrderType::StopMarket
1289 );
1290 assert_eq!(
1291 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1292 OrderType::StopLimit
1293 );
1294 assert_eq!(
1295 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1296 OrderType::MarketIfTouched
1297 );
1298 assert_eq!(
1299 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1300 OrderType::LimitIfTouched
1301 );
1302 assert_eq!(
1303 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1304 OrderType::TrailingStopMarket
1305 );
1306 }
1307
1308 mod error_parsing_tests {
1310 use super::*;
1311
1312 #[rstest]
1313 fn test_parse_tick_size_error() {
1314 let error = "Price must be divisible by tick size 0.01";
1315 let code = HyperliquidRejectCode::from_api_error(error);
1316 assert_eq!(code, HyperliquidRejectCode::Tick);
1317 }
1318
1319 #[rstest]
1320 fn test_parse_tick_size_error_case_insensitive() {
1321 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1322 let code = HyperliquidRejectCode::from_api_error(error);
1323 assert_eq!(code, HyperliquidRejectCode::Tick);
1324 }
1325
1326 #[rstest]
1327 fn test_parse_min_notional_perp() {
1328 let error = "Order must have minimum value of $10";
1329 let code = HyperliquidRejectCode::from_api_error(error);
1330 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1331 }
1332
1333 #[rstest]
1334 fn test_parse_min_notional_spot() {
1335 let error = "Order must have minimum value of 10 USDC";
1336 let code = HyperliquidRejectCode::from_api_error(error);
1337 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1338 }
1339
1340 #[rstest]
1341 fn test_parse_insufficient_margin() {
1342 let error = "Insufficient margin to place order";
1343 let code = HyperliquidRejectCode::from_api_error(error);
1344 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1345 }
1346
1347 #[rstest]
1348 fn test_parse_insufficient_margin_case_variations() {
1349 let variations = vec![
1350 "insufficient margin to place order",
1351 "INSUFFICIENT MARGIN TO PLACE ORDER",
1352 " Insufficient margin to place order ", ];
1354
1355 for error in variations {
1356 let code = HyperliquidRejectCode::from_api_error(error);
1357 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1358 }
1359 }
1360
1361 #[rstest]
1362 fn test_parse_reduce_only_violation() {
1363 let error = "Reduce only order would increase position";
1364 let code = HyperliquidRejectCode::from_api_error(error);
1365 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1366 }
1367
1368 #[rstest]
1369 fn test_parse_reduce_only_with_hyphen() {
1370 let error = "Reduce-only order would increase position";
1371 let code = HyperliquidRejectCode::from_api_error(error);
1372 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1373 }
1374
1375 #[rstest]
1376 fn test_parse_post_only_match() {
1377 let error = "Post only order would have immediately matched";
1378 let code = HyperliquidRejectCode::from_api_error(error);
1379 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1380 }
1381
1382 #[rstest]
1383 fn test_parse_post_only_with_hyphen() {
1384 let error = "Post-only order would have immediately matched";
1385 let code = HyperliquidRejectCode::from_api_error(error);
1386 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1387 }
1388
1389 #[rstest]
1390 fn test_parse_ioc_no_match() {
1391 let error = "Order could not immediately match";
1392 let code = HyperliquidRejectCode::from_api_error(error);
1393 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1394 }
1395
1396 #[rstest]
1397 fn test_parse_invalid_trigger_price() {
1398 let error = "Invalid TP/SL price";
1399 let code = HyperliquidRejectCode::from_api_error(error);
1400 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1401 }
1402
1403 #[rstest]
1404 fn test_parse_no_liquidity() {
1405 let error = "No liquidity available for market order";
1406 let code = HyperliquidRejectCode::from_api_error(error);
1407 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1408 }
1409
1410 #[rstest]
1411 fn test_parse_position_increase_at_oi_cap() {
1412 let error = "PositionIncreaseAtOpenInterestCap";
1413 let code = HyperliquidRejectCode::from_api_error(error);
1414 assert_eq!(
1415 code,
1416 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1417 );
1418 }
1419
1420 #[rstest]
1421 fn test_parse_position_flip_at_oi_cap() {
1422 let error = "PositionFlipAtOpenInterestCap";
1423 let code = HyperliquidRejectCode::from_api_error(error);
1424 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1425 }
1426
1427 #[rstest]
1428 fn test_parse_too_aggressive_at_oi_cap() {
1429 let error = "TooAggressiveAtOpenInterestCap";
1430 let code = HyperliquidRejectCode::from_api_error(error);
1431 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1432 }
1433
1434 #[rstest]
1435 fn test_parse_open_interest_increase() {
1436 let error = "OpenInterestIncrease";
1437 let code = HyperliquidRejectCode::from_api_error(error);
1438 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1439 }
1440
1441 #[rstest]
1442 fn test_parse_insufficient_spot_balance() {
1443 let error = "Insufficient spot balance";
1444 let code = HyperliquidRejectCode::from_api_error(error);
1445 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1446 }
1447
1448 #[rstest]
1449 fn test_parse_oracle_error() {
1450 let error = "Oracle price unavailable";
1451 let code = HyperliquidRejectCode::from_api_error(error);
1452 assert_eq!(code, HyperliquidRejectCode::Oracle);
1453 }
1454
1455 #[rstest]
1456 fn test_parse_max_position() {
1457 let error = "Exceeds max position size";
1458 let code = HyperliquidRejectCode::from_api_error(error);
1459 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1460 }
1461
1462 #[rstest]
1463 fn test_parse_missing_order() {
1464 let error = "MissingOrder";
1465 let code = HyperliquidRejectCode::from_api_error(error);
1466 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1467 }
1468
1469 #[rstest]
1470 fn test_parse_unknown_error() {
1471 let error = "This is a completely new error message";
1472 let code = HyperliquidRejectCode::from_api_error(error);
1473 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1474
1475 if let HyperliquidRejectCode::Unknown(msg) = code {
1477 assert_eq!(msg, error);
1478 }
1479 }
1480
1481 #[rstest]
1482 fn test_parse_empty_error() {
1483 let error = "";
1484 let code = HyperliquidRejectCode::from_api_error(error);
1485 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1486 }
1487
1488 #[rstest]
1489 fn test_parse_whitespace_only() {
1490 let error = " ";
1491 let code = HyperliquidRejectCode::from_api_error(error);
1492 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1493 }
1494
1495 #[rstest]
1496 fn test_normalization_preserves_original_in_unknown() {
1497 let error = " UNKNOWN ERROR MESSAGE ";
1498 let code = HyperliquidRejectCode::from_api_error(error);
1499
1500 if let HyperliquidRejectCode::Unknown(msg) = code {
1502 assert_eq!(msg, error);
1503 } else {
1504 panic!("Expected Unknown variant");
1505 }
1506 }
1507 }
1508
1509 #[rstest]
1510 fn test_conditional_order_type_round_trip() {
1511 assert_eq!(
1512 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1513 OrderType::TrailingStopLimit
1514 );
1515
1516 assert_eq!(
1518 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1519 HyperliquidConditionalOrderType::StopMarket
1520 );
1521 assert_eq!(
1522 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1523 HyperliquidConditionalOrderType::StopLimit
1524 );
1525 }
1526
1527 #[rstest]
1528 fn test_trailing_offset_type_serialization() {
1529 let price = HyperliquidTrailingOffsetType::Price;
1530 let percentage = HyperliquidTrailingOffsetType::Percentage;
1531 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1532
1533 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1534 assert_eq!(
1535 serde_json::to_string(&percentage).unwrap(),
1536 r#""percentage""#
1537 );
1538 assert_eq!(
1539 serde_json::to_string(&basis_points).unwrap(),
1540 r#""basispoints""#
1541 );
1542 }
1543
1544 #[rstest]
1545 fn test_conditional_order_type_serialization() {
1546 assert_eq!(
1547 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1548 r#""STOP_MARKET""#
1549 );
1550 assert_eq!(
1551 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1552 r#""STOP_LIMIT""#
1553 );
1554 assert_eq!(
1555 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1556 r#""TAKE_PROFIT_MARKET""#
1557 );
1558 assert_eq!(
1559 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1560 r#""TAKE_PROFIT_LIMIT""#
1561 );
1562 assert_eq!(
1563 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1564 r#""TRAILING_STOP_MARKET""#
1565 );
1566 assert_eq!(
1567 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1568 r#""TRAILING_STOP_LIMIT""#
1569 );
1570 }
1571
1572 #[rstest]
1573 fn test_order_type_enum_coverage() {
1574 let conditional_types = vec![
1576 HyperliquidConditionalOrderType::StopMarket,
1577 HyperliquidConditionalOrderType::StopLimit,
1578 HyperliquidConditionalOrderType::TakeProfitMarket,
1579 HyperliquidConditionalOrderType::TakeProfitLimit,
1580 HyperliquidConditionalOrderType::TrailingStopMarket,
1581 HyperliquidConditionalOrderType::TrailingStopLimit,
1582 ];
1583
1584 for cond_type in conditional_types {
1585 let order_type = OrderType::from(cond_type);
1586 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1587 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1588 }
1589 }
1590}