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, parse::OUTCOME_SYMBOL_SUFFIX};
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::Buy,
154 HyperliquidSide::Sell => Self::Sell,
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 FrontendMarket,
185 LiquidationMarket,
187}
188
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
191#[serde(tag = "type", rename_all = "lowercase")]
192pub enum HyperliquidOrderType {
193 #[serde(rename = "limit")]
195 Limit { tif: HyperliquidTimeInForce },
196
197 #[serde(rename = "trigger")]
199 Trigger {
200 #[serde(rename = "isMarket")]
201 is_market: bool,
202 #[serde(rename = "triggerPx")]
203 trigger_px: String,
204 tpsl: HyperliquidTpSl,
205 },
206}
207
208#[derive(
210 Copy,
211 Clone,
212 Debug,
213 Display,
214 PartialEq,
215 Eq,
216 Hash,
217 AsRefStr,
218 EnumIter,
219 EnumString,
220 Serialize,
221 Deserialize,
222)]
223#[cfg_attr(
224 feature = "python",
225 pyo3::pyclass(
226 module = "nautilus_trader.adapters.hyperliquid",
227 from_py_object,
228 rename_all = "SCREAMING_SNAKE_CASE",
229 )
230)]
231#[cfg_attr(
232 feature = "python",
233 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
234)]
235#[serde(rename_all = "lowercase")]
236#[strum(serialize_all = "lowercase")]
237pub enum HyperliquidTpSl {
238 Tp,
240 Sl,
242}
243
244#[derive(
249 Copy,
250 Clone,
251 Debug,
252 Display,
253 PartialEq,
254 Eq,
255 Hash,
256 AsRefStr,
257 EnumIter,
258 EnumString,
259 Serialize,
260 Deserialize,
261)]
262#[cfg_attr(
263 feature = "python",
264 pyo3::pyclass(
265 module = "nautilus_trader.adapters.hyperliquid",
266 from_py_object,
267 rename_all = "SCREAMING_SNAKE_CASE",
268 )
269)]
270#[cfg_attr(
271 feature = "python",
272 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
273)]
274#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
275#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
276pub enum HyperliquidConditionalOrderType {
277 StopMarket,
279 StopLimit,
281 TakeProfitMarket,
283 TakeProfitLimit,
285 TrailingStopMarket,
287 TrailingStopLimit,
289}
290
291impl From<HyperliquidConditionalOrderType> for OrderType {
292 fn from(value: HyperliquidConditionalOrderType) -> Self {
293 match value {
294 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
295 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
296 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
297 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
298 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
299 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
300 }
301 }
302}
303
304impl From<OrderType> for HyperliquidConditionalOrderType {
305 fn from(value: OrderType) -> Self {
306 match value {
307 OrderType::StopMarket => Self::StopMarket,
308 OrderType::StopLimit => Self::StopLimit,
309 OrderType::MarketIfTouched => Self::TakeProfitMarket,
310 OrderType::LimitIfTouched => Self::TakeProfitLimit,
311 OrderType::TrailingStopMarket => Self::TrailingStopMarket,
312 OrderType::TrailingStopLimit => Self::TrailingStopLimit,
313 _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
314 }
315 }
316}
317
318#[derive(
325 Copy,
326 Clone,
327 Debug,
328 Display,
329 PartialEq,
330 Eq,
331 Hash,
332 AsRefStr,
333 EnumIter,
334 EnumString,
335 Serialize,
336 Deserialize,
337)]
338#[cfg_attr(
339 feature = "python",
340 pyo3::pyclass(
341 module = "nautilus_trader.adapters.hyperliquid",
342 from_py_object,
343 rename_all = "SCREAMING_SNAKE_CASE",
344 )
345)]
346#[cfg_attr(
347 feature = "python",
348 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
349)]
350#[serde(rename_all = "lowercase")]
351#[strum(serialize_all = "lowercase")]
352pub enum HyperliquidTrailingOffsetType {
353 Price,
355 Percentage,
357 #[serde(rename = "basispoints")]
359 #[strum(serialize = "basispoints")]
360 BasisPoints,
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
365#[serde(transparent)]
366pub struct HyperliquidReduceOnly(pub bool);
367
368impl HyperliquidReduceOnly {
369 pub fn new(reduce_only: bool) -> Self {
371 Self(reduce_only)
372 }
373
374 pub fn is_reduce_only(&self) -> bool {
376 self.0
377 }
378}
379
380#[derive(
382 Copy,
383 Clone,
384 Debug,
385 Display,
386 PartialEq,
387 Eq,
388 Hash,
389 AsRefStr,
390 EnumIter,
391 EnumString,
392 Serialize,
393 Deserialize,
394)]
395#[serde(rename_all = "lowercase")]
396#[strum(serialize_all = "lowercase")]
397pub enum HyperliquidLiquidityFlag {
398 Maker,
399 Taker,
400}
401
402impl From<bool> for HyperliquidLiquidityFlag {
403 fn from(crossed: bool) -> Self {
407 if crossed { Self::Taker } else { Self::Maker }
408 }
409}
410
411#[derive(
413 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
414)]
415#[serde(rename_all = "lowercase")]
416#[strum(serialize_all = "lowercase")]
417pub enum HyperliquidLiquidationMethod {
418 Market,
419 Backstop,
420 #[serde(other)]
421 Unknown,
422}
423
424#[derive(
426 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
427)]
428#[serde(rename_all = "camelCase")]
429#[strum(serialize_all = "camelCase")]
430pub enum HyperliquidPositionType {
431 OneWay,
432 #[serde(other)]
433 Unknown,
434}
435
436#[derive(
438 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
439)]
440#[serde(rename_all = "lowercase")]
441#[strum(serialize_all = "lowercase")]
442pub enum HyperliquidTwapStatus {
443 Activated,
444 Terminated,
445 Finished,
446 Error,
447 #[serde(other)]
448 Unknown,
449}
450
451#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
452#[serde(untagged)]
453pub enum HyperliquidRejectCode {
454 Tick,
456 MinTradeNtl,
458 MinTradeSpotNtl,
460 PerpMargin,
462 ReduceOnly,
464 BadAloPx,
466 IocCancel,
468 BadTriggerPx,
470 MarketOrderNoLiquidity,
472 PositionIncreaseAtOpenInterestCap,
474 PositionFlipAtOpenInterestCap,
476 TooAggressiveAtOpenInterestCap,
478 OpenInterestIncrease,
480 InsufficientSpotBalance,
482 Oracle,
484 PerpMaxPosition,
486 MissingOrder,
488 Unknown(String),
490}
491
492impl HyperliquidRejectCode {
493 pub fn from_api_error(error_message: &str) -> Self {
495 Self::from_error_string_internal(error_message)
496 }
497
498 fn from_error_string_internal(error: &str) -> Self {
499 let normalized = error.trim().to_lowercase();
501
502 match normalized.as_str() {
503 s if s.contains("tick size") => Self::Tick,
505
506 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
508 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
509
510 s if s.contains("insufficient margin") => Self::PerpMargin,
512
513 s if s.contains("reduce only order would increase")
515 || s.contains("reduce-only order would increase") =>
516 {
517 Self::ReduceOnly
518 }
519
520 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
522 || s.contains("post-only order would have immediately matched") =>
523 {
524 Self::BadAloPx
525 }
526
527 s if s.contains("could not immediately match") => Self::IocCancel,
529
530 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
532
533 s if s.contains("no liquidity available for market order") => {
535 Self::MarketOrderNoLiquidity
536 }
537
538 s if s.contains("positionincreaseatopeninterestcap") => {
541 Self::PositionIncreaseAtOpenInterestCap
542 }
543 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
544 s if s.contains("tooaggressiveatopeninterestcap") => {
545 Self::TooAggressiveAtOpenInterestCap
546 }
547 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
548
549 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
551
552 s if s.contains("oracle") => Self::Oracle,
554
555 s if s.contains("max position") => Self::PerpMaxPosition,
557
558 s if s.contains("missingorder") => Self::MissingOrder,
560
561 _ => {
563 log::warn!(
564 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
566 Self::Unknown(error.to_string())
567 }
568 }
569 }
570
571 #[deprecated(
576 since = "0.50.0",
577 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
578 )]
579 pub fn from_error_string(error: &str) -> Self {
580 Self::from_error_string_internal(error)
581 }
582}
583
584#[derive(
588 Copy,
589 Clone,
590 Debug,
591 Display,
592 PartialEq,
593 Eq,
594 Hash,
595 AsRefStr,
596 EnumIter,
597 EnumString,
598 Serialize,
599 Deserialize,
600)]
601pub enum HyperliquidOrderStatus {
602 #[serde(rename = "open")]
604 Open,
605 #[serde(rename = "accepted")]
607 Accepted,
608 #[serde(rename = "triggered")]
610 Triggered,
611 #[serde(rename = "filled")]
613 Filled,
614 #[serde(rename = "canceled")]
616 Canceled,
617 #[serde(rename = "rejected")]
619 Rejected,
620 #[serde(rename = "marginCanceled")]
623 MarginCanceled,
624 #[serde(rename = "vaultWithdrawalCanceled")]
626 VaultWithdrawalCanceled,
627 #[serde(rename = "openInterestCapCanceled")]
629 OpenInterestCapCanceled,
630 #[serde(rename = "selfTradeCanceled")]
632 SelfTradeCanceled,
633 #[serde(rename = "reduceOnlyCanceled")]
635 ReduceOnlyCanceled,
636 #[serde(rename = "siblingFilledCanceled")]
638 SiblingFilledCanceled,
639 #[serde(rename = "delistedCanceled")]
641 DelistedCanceled,
642 #[serde(rename = "liquidatedCanceled")]
644 LiquidatedCanceled,
645 #[serde(rename = "scheduledCancel")]
647 ScheduledCancel,
648 #[serde(rename = "tickRejected")]
651 TickRejected,
652 #[serde(rename = "minTradeNtlRejected")]
654 MinTradeNtlRejected,
655 #[serde(rename = "minTradeSpotNtlRejected")]
657 MinTradeSpotNtlRejected,
658 #[serde(rename = "perpMarginRejected")]
660 PerpMarginRejected,
661 #[serde(rename = "reduceOnlyRejected")]
663 ReduceOnlyRejected,
664 #[serde(rename = "badAloPxRejected")]
666 BadAloPxRejected,
667 #[serde(rename = "iocCancelRejected")]
669 IocCancelRejected,
670 #[serde(rename = "badTriggerPxRejected")]
672 BadTriggerPxRejected,
673 #[serde(rename = "marketOrderNoLiquidityRejected")]
675 MarketOrderNoLiquidityRejected,
676 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
678 PositionIncreaseAtOpenInterestCapRejected,
679 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
681 PositionFlipAtOpenInterestCapRejected,
682 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
684 TooAggressiveAtOpenInterestCapRejected,
685 #[serde(rename = "openInterestIncreaseRejected")]
687 OpenInterestIncreaseRejected,
688 #[serde(rename = "insufficientSpotBalanceRejected")]
690 InsufficientSpotBalanceRejected,
691 #[serde(rename = "oracleRejected")]
693 OracleRejected,
694 #[serde(rename = "perpMaxPositionRejected")]
696 PerpMaxPositionRejected,
697}
698
699impl From<HyperliquidOrderStatus> for OrderStatus {
700 fn from(status: HyperliquidOrderStatus) -> Self {
701 match status {
702 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
703 HyperliquidOrderStatus::Triggered => Self::Triggered,
704 HyperliquidOrderStatus::Filled => Self::Filled,
705 HyperliquidOrderStatus::Canceled
707 | HyperliquidOrderStatus::MarginCanceled
708 | HyperliquidOrderStatus::VaultWithdrawalCanceled
709 | HyperliquidOrderStatus::OpenInterestCapCanceled
710 | HyperliquidOrderStatus::SelfTradeCanceled
711 | HyperliquidOrderStatus::ReduceOnlyCanceled
712 | HyperliquidOrderStatus::SiblingFilledCanceled
713 | HyperliquidOrderStatus::DelistedCanceled
714 | HyperliquidOrderStatus::LiquidatedCanceled
715 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
716 HyperliquidOrderStatus::Rejected
718 | HyperliquidOrderStatus::TickRejected
719 | HyperliquidOrderStatus::MinTradeNtlRejected
720 | HyperliquidOrderStatus::MinTradeSpotNtlRejected
721 | HyperliquidOrderStatus::PerpMarginRejected
722 | HyperliquidOrderStatus::ReduceOnlyRejected
723 | HyperliquidOrderStatus::BadAloPxRejected
724 | HyperliquidOrderStatus::IocCancelRejected
725 | HyperliquidOrderStatus::BadTriggerPxRejected
726 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
727 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
728 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
729 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
730 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
731 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
732 | HyperliquidOrderStatus::OracleRejected
733 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
734 }
735 }
736}
737
738impl HyperliquidOrderStatus {
739 #[must_use]
741 pub const fn rejection_reason(self) -> Option<&'static str> {
742 match self {
743 Self::BadAloPxRejected => Some(HYPERLIQUID_POST_ONLY_WOULD_MATCH),
744 Self::ReduceOnlyRejected => Some("Reduce only order would increase position."),
745 Self::IocCancelRejected => {
746 Some("Order could not immediately match against any resting orders")
747 }
748 _ => None,
749 }
750 }
751}
752
753#[derive(
764 Copy,
765 Clone,
766 Debug,
767 Display,
768 PartialEq,
769 Eq,
770 Hash,
771 AsRefStr,
772 EnumIter,
773 EnumString,
774 Serialize,
775 Deserialize,
776)]
777#[serde(rename_all = "PascalCase")]
778#[strum(serialize_all = "PascalCase")]
779pub enum HyperliquidFillDirection {
780 #[serde(rename = "Open Long")]
782 #[strum(serialize = "Open Long")]
783 OpenLong,
784 #[serde(rename = "Open Short")]
786 #[strum(serialize = "Open Short")]
787 OpenShort,
788 #[serde(rename = "Close Long")]
790 #[strum(serialize = "Close Long")]
791 CloseLong,
792 #[serde(rename = "Close Short")]
794 #[strum(serialize = "Close Short")]
795 CloseShort,
796 #[serde(rename = "Long > Short")]
798 #[strum(serialize = "Long > Short")]
799 LongToShort,
800 #[serde(rename = "Short > Long")]
802 #[strum(serialize = "Short > Long")]
803 ShortToLong,
804 #[serde(rename = "Auto-Deleveraging")]
806 #[strum(serialize = "Auto-Deleveraging")]
807 AutoDeleveraging,
808 #[serde(rename = "Net Child Vaults")]
810 #[strum(serialize = "Net Child Vaults")]
811 NetChildVaults,
812 Buy,
814 Sell,
816 #[serde(rename = "Spot Dust Conversion")]
818 #[strum(serialize = "Spot Dust Conversion")]
819 SpotDustConversion,
820 #[serde(rename = "Settlement")]
823 #[strum(serialize = "Settlement")]
824 Settlement,
825 #[serde(rename = "Split Outcome")]
828 #[strum(serialize = "Split Outcome")]
829 SplitOutcome,
830 #[serde(rename = "Merge Outcome")]
833 #[strum(serialize = "Merge Outcome")]
834 MergeOutcome,
835 #[serde(rename = "Merge Question")]
838 #[strum(serialize = "Merge Question")]
839 MergeQuestion,
840 #[serde(rename = "Negate Outcome")]
843 #[strum(serialize = "Negate Outcome")]
844 NegateOutcome,
845 #[serde(other)]
847 Unknown,
848}
849
850#[derive(
854 Copy,
855 Clone,
856 Debug,
857 Display,
858 PartialEq,
859 Eq,
860 Hash,
861 AsRefStr,
862 EnumIter,
863 EnumString,
864 Serialize,
865 Deserialize,
866)]
867#[serde(rename_all = "camelCase")]
868#[strum(serialize_all = "camelCase")]
869pub enum HyperliquidInfoRequestType {
870 Meta,
872 SpotMeta,
874 MetaAndAssetCtxs,
876 SpotMetaAndAssetCtxs,
878 OutcomeMeta,
880 L2Book,
882 AllMids,
884 RecentTrades,
886 UserFills,
888 UserFillsByTime,
890 OrderStatus,
892 OpenOrders,
894 FrontendOpenOrders,
896 ClearinghouseState,
898 SpotClearinghouseState,
900 ExchangeStatus,
902 CandleSnapshot,
904 Candle,
906 HistoricalOrders,
908 FundingHistory,
910 UserFunding,
912 NonUserFundingUpdates,
914 TwapHistory,
916 UserTwapSliceFills,
918 UserTwapSliceFillsByTime,
920 UserRateLimit,
922 UserRole,
924 DelegatorHistory,
926 DelegatorRewards,
928 ValidatorStats,
930 UserFees,
932 PerpDexs,
934 AllPerpMetas,
936}
937
938impl HyperliquidInfoRequestType {
939 pub fn as_str(&self) -> &'static str {
940 match self {
941 Self::Meta => "meta",
942 Self::SpotMeta => "spotMeta",
943 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
944 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
945 Self::OutcomeMeta => "outcomeMeta",
946 Self::L2Book => "l2Book",
947 Self::AllMids => "allMids",
948 Self::RecentTrades => "recentTrades",
949 Self::UserFills => "userFills",
950 Self::UserFillsByTime => "userFillsByTime",
951 Self::OrderStatus => "orderStatus",
952 Self::OpenOrders => "openOrders",
953 Self::FrontendOpenOrders => "frontendOpenOrders",
954 Self::ClearinghouseState => "clearinghouseState",
955 Self::SpotClearinghouseState => "spotClearinghouseState",
956 Self::ExchangeStatus => "exchangeStatus",
957 Self::CandleSnapshot => "candleSnapshot",
958 Self::Candle => "candle",
959 Self::HistoricalOrders => "historicalOrders",
960 Self::FundingHistory => "fundingHistory",
961 Self::UserFunding => "userFunding",
962 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
963 Self::TwapHistory => "twapHistory",
964 Self::UserTwapSliceFills => "userTwapSliceFills",
965 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
966 Self::UserRateLimit => "userRateLimit",
967 Self::UserRole => "userRole",
968 Self::DelegatorHistory => "delegatorHistory",
969 Self::DelegatorRewards => "delegatorRewards",
970 Self::ValidatorStats => "validatorStats",
971 Self::UserFees => "userFees",
972 Self::PerpDexs => "perpDexs",
973 Self::AllPerpMetas => "allPerpMetas",
974 }
975 }
976}
977
978#[derive(
979 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
980)]
981#[serde(rename_all = "lowercase")]
982#[strum(serialize_all = "lowercase")]
983pub enum HyperliquidLeverageType {
984 Cross,
985 Isolated,
986 #[serde(other)]
987 Unknown,
988}
989
990#[derive(
992 Copy,
993 Clone,
994 Debug,
995 Display,
996 PartialEq,
997 Eq,
998 Hash,
999 AsRefStr,
1000 EnumIter,
1001 EnumString,
1002 Serialize,
1003 Deserialize,
1004)]
1005#[cfg_attr(
1006 feature = "python",
1007 pyo3::pyclass(
1008 module = "nautilus_trader.adapters.hyperliquid",
1009 from_py_object,
1010 rename_all = "SCREAMING_SNAKE_CASE",
1011 )
1012)]
1013#[cfg_attr(
1014 feature = "python",
1015 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1016)]
1017#[serde(rename_all = "UPPERCASE")]
1018#[strum(serialize_all = "UPPERCASE")]
1019pub enum HyperliquidProductType {
1020 Perp,
1022 Spot,
1024 Outcome,
1026}
1027
1028impl HyperliquidProductType {
1029 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
1040 if symbol.ends_with("-PERP") {
1041 Ok(Self::Perp)
1042 } else if symbol.ends_with("-SPOT") {
1043 Ok(Self::Spot)
1044 } else if symbol.ends_with(OUTCOME_SYMBOL_SUFFIX) || is_outcome_wire_symbol(symbol) {
1045 Ok(Self::Outcome)
1046 } else {
1047 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
1048 }
1049 }
1050}
1051
1052fn is_outcome_wire_symbol(symbol: &str) -> bool {
1055 let Some(rest) = symbol
1056 .strip_prefix('#')
1057 .or_else(|| symbol.strip_prefix('+'))
1058 else {
1059 return false;
1060 };
1061 !rest.is_empty() && rest.parse::<u32>().is_ok()
1062}
1063
1064#[derive(
1066 Copy,
1067 Clone,
1068 Debug,
1069 Default,
1070 Display,
1071 PartialEq,
1072 Eq,
1073 Hash,
1074 AsRefStr,
1075 EnumIter,
1076 EnumString,
1077 Serialize,
1078 Deserialize,
1079)]
1080#[serde(rename_all = "lowercase")]
1081#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1082#[cfg_attr(
1083 feature = "python",
1084 pyo3::pyclass(
1085 eq,
1086 eq_int,
1087 module = "nautilus_trader.adapters.hyperliquid",
1088 from_py_object,
1089 rename_all = "SCREAMING_SNAKE_CASE",
1090 )
1091)]
1092#[cfg_attr(
1093 feature = "python",
1094 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1095)]
1096pub enum HyperliquidEnvironment {
1097 #[default]
1099 Mainnet,
1100 Testnet,
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use nautilus_model::enums::OrderType;
1107 use rstest::rstest;
1108 use serde_json;
1109
1110 use super::*;
1111
1112 #[rstest]
1113 fn test_side_serde() {
1114 let buy_side = HyperliquidSide::Buy;
1115 let sell_side = HyperliquidSide::Sell;
1116
1117 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
1118 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
1119
1120 assert_eq!(
1121 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
1122 HyperliquidSide::Buy
1123 );
1124 assert_eq!(
1125 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
1126 HyperliquidSide::Sell
1127 );
1128 }
1129
1130 #[rstest]
1131 fn test_side_from_order_side() {
1132 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
1134 assert_eq!(
1135 HyperliquidSide::from(OrderSide::Sell),
1136 HyperliquidSide::Sell
1137 );
1138 }
1139
1140 #[rstest]
1141 fn test_order_side_from_hyperliquid_side() {
1142 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
1144 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
1145 }
1146
1147 #[rstest]
1148 fn test_aggressor_side_from_hyperliquid_side() {
1149 assert_eq!(
1151 AggressorSide::from(HyperliquidSide::Buy),
1152 AggressorSide::Buy
1153 );
1154 assert_eq!(
1155 AggressorSide::from(HyperliquidSide::Sell),
1156 AggressorSide::Sell
1157 );
1158 }
1159
1160 #[rstest]
1161 fn test_time_in_force_serde() {
1162 let test_cases = [
1163 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1164 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1165 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1166 (HyperliquidTimeInForce::FrontendMarket, "\"FrontendMarket\""),
1167 (
1168 HyperliquidTimeInForce::LiquidationMarket,
1169 "\"LiquidationMarket\"",
1170 ),
1171 ];
1172
1173 for (tif, expected_json) in test_cases {
1174 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1175 assert_eq!(
1176 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1177 tif
1178 );
1179 }
1180 }
1181
1182 #[rstest]
1183 fn test_info_request_type_outcome_meta_as_str() {
1184 assert_eq!(
1185 HyperliquidInfoRequestType::OutcomeMeta.as_str(),
1186 "outcomeMeta"
1187 );
1188 }
1189
1190 #[rstest]
1191 fn test_info_request_type_recent_trades_as_str() {
1192 assert_eq!(
1193 HyperliquidInfoRequestType::RecentTrades.as_str(),
1194 "recentTrades"
1195 );
1196 }
1197
1198 #[rstest]
1199 fn test_fill_direction_serde() {
1200 let cases = [
1201 (HyperliquidFillDirection::OpenLong, "\"Open Long\""),
1202 (HyperliquidFillDirection::CloseShort, "\"Close Short\""),
1203 (HyperliquidFillDirection::LongToShort, "\"Long > Short\""),
1204 (
1205 HyperliquidFillDirection::AutoDeleveraging,
1206 "\"Auto-Deleveraging\"",
1207 ),
1208 (
1209 HyperliquidFillDirection::NetChildVaults,
1210 "\"Net Child Vaults\"",
1211 ),
1212 (HyperliquidFillDirection::Buy, "\"Buy\""),
1213 (
1214 HyperliquidFillDirection::SpotDustConversion,
1215 "\"Spot Dust Conversion\"",
1216 ),
1217 (HyperliquidFillDirection::Settlement, "\"Settlement\""),
1218 (HyperliquidFillDirection::SplitOutcome, "\"Split Outcome\""),
1219 (HyperliquidFillDirection::MergeOutcome, "\"Merge Outcome\""),
1220 (
1221 HyperliquidFillDirection::MergeQuestion,
1222 "\"Merge Question\"",
1223 ),
1224 (
1225 HyperliquidFillDirection::NegateOutcome,
1226 "\"Negate Outcome\"",
1227 ),
1228 ];
1229
1230 for (variant, expected) in cases {
1231 assert_eq!(serde_json::to_string(&variant).unwrap(), expected);
1232 assert_eq!(
1233 serde_json::from_str::<HyperliquidFillDirection>(expected).unwrap(),
1234 variant
1235 );
1236 }
1237 }
1238
1239 #[rstest]
1240 fn test_fill_direction_unknown_is_lenient() {
1241 assert_eq!(
1242 serde_json::from_str::<HyperliquidFillDirection>("\"Some New Direction\"").unwrap(),
1243 HyperliquidFillDirection::Unknown,
1244 );
1245 }
1246
1247 #[rstest]
1248 fn test_position_type_unknown_is_lenient() {
1249 assert_eq!(
1250 serde_json::from_str::<HyperliquidPositionType>("\"hedge\"").unwrap(),
1251 HyperliquidPositionType::Unknown,
1252 );
1253 }
1254
1255 #[rstest]
1256 fn test_twap_status_unknown_is_lenient() {
1257 assert_eq!(
1258 serde_json::from_str::<HyperliquidTwapStatus>("\"paused\"").unwrap(),
1259 HyperliquidTwapStatus::Unknown,
1260 );
1261 assert_eq!(
1262 serde_json::from_str::<HyperliquidTwapStatus>("\"waitingForTrigger\"").unwrap(),
1263 HyperliquidTwapStatus::Unknown,
1264 );
1265 }
1266
1267 #[rstest]
1268 fn test_liquidity_flag_from_crossed() {
1269 assert_eq!(
1270 HyperliquidLiquidityFlag::from(true),
1271 HyperliquidLiquidityFlag::Taker
1272 );
1273 assert_eq!(
1274 HyperliquidLiquidityFlag::from(false),
1275 HyperliquidLiquidityFlag::Maker
1276 );
1277 }
1278
1279 #[rstest]
1280 #[allow(deprecated)]
1281 fn test_reject_code_from_error_string() {
1282 let test_cases = [
1283 (
1284 "Price must be divisible by tick size.",
1285 HyperliquidRejectCode::Tick,
1286 ),
1287 (
1288 "Order must have minimum value of $10.",
1289 HyperliquidRejectCode::MinTradeNtl,
1290 ),
1291 (
1292 "Insufficient margin to place order.",
1293 HyperliquidRejectCode::PerpMargin,
1294 ),
1295 (
1296 "Post only order would have immediately matched, bbo was 1.23",
1297 HyperliquidRejectCode::BadAloPx,
1298 ),
1299 (
1300 "Some unknown error",
1301 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1302 ),
1303 ];
1304
1305 for (error_str, expected_code) in test_cases {
1306 assert_eq!(
1307 HyperliquidRejectCode::from_error_string(error_str),
1308 expected_code
1309 );
1310 }
1311 }
1312
1313 #[rstest]
1314 fn test_reject_code_from_api_error() {
1315 let test_cases = [
1316 (
1317 "Price must be divisible by tick size.",
1318 HyperliquidRejectCode::Tick,
1319 ),
1320 (
1321 "Order must have minimum value of $10.",
1322 HyperliquidRejectCode::MinTradeNtl,
1323 ),
1324 (
1325 "Insufficient margin to place order.",
1326 HyperliquidRejectCode::PerpMargin,
1327 ),
1328 (
1329 "Post only order would have immediately matched, bbo was 1.23",
1330 HyperliquidRejectCode::BadAloPx,
1331 ),
1332 (
1333 "Some unknown error",
1334 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1335 ),
1336 ];
1337
1338 for (error_str, expected_code) in test_cases {
1339 assert_eq!(
1340 HyperliquidRejectCode::from_api_error(error_str),
1341 expected_code
1342 );
1343 }
1344 }
1345
1346 #[rstest]
1347 fn test_reduce_only() {
1348 let reduce_only = HyperliquidReduceOnly::new(true);
1349
1350 assert!(reduce_only.is_reduce_only());
1351
1352 let json = serde_json::to_string(&reduce_only).unwrap();
1353 assert_eq!(json, "true");
1354
1355 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1356 assert_eq!(parsed, reduce_only);
1357 }
1358
1359 #[rstest]
1360 fn test_order_status_conversion() {
1361 assert_eq!(
1363 OrderStatus::from(HyperliquidOrderStatus::Open),
1364 OrderStatus::Accepted
1365 );
1366 assert_eq!(
1367 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1368 OrderStatus::Accepted
1369 );
1370 assert_eq!(
1371 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1372 OrderStatus::Triggered
1373 );
1374 assert_eq!(
1375 OrderStatus::from(HyperliquidOrderStatus::Filled),
1376 OrderStatus::Filled
1377 );
1378 assert_eq!(
1379 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1380 OrderStatus::Canceled
1381 );
1382 assert_eq!(
1383 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1384 OrderStatus::Rejected
1385 );
1386
1387 assert_eq!(
1389 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1390 OrderStatus::Canceled
1391 );
1392 assert_eq!(
1393 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1394 OrderStatus::Canceled
1395 );
1396 assert_eq!(
1397 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1398 OrderStatus::Canceled
1399 );
1400
1401 assert_eq!(
1403 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1404 OrderStatus::Rejected
1405 );
1406 assert_eq!(
1407 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1408 OrderStatus::Rejected
1409 );
1410 }
1411
1412 #[rstest]
1413 fn test_order_status_serde_deserialization() {
1414 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1416 assert_eq!(open, HyperliquidOrderStatus::Open);
1417
1418 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1419 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1420
1421 let margin_canceled: HyperliquidOrderStatus =
1422 serde_json::from_str(r#""marginCanceled""#).unwrap();
1423 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1424
1425 let self_trade_canceled: HyperliquidOrderStatus =
1426 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1427 assert_eq!(
1428 self_trade_canceled,
1429 HyperliquidOrderStatus::SelfTradeCanceled
1430 );
1431
1432 let reduce_only_canceled: HyperliquidOrderStatus =
1433 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1434 assert_eq!(
1435 reduce_only_canceled,
1436 HyperliquidOrderStatus::ReduceOnlyCanceled
1437 );
1438
1439 let tick_rejected: HyperliquidOrderStatus =
1440 serde_json::from_str(r#""tickRejected""#).unwrap();
1441 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1442 }
1443
1444 #[rstest]
1445 fn test_hyperliquid_tpsl_serialization() {
1446 let tp = HyperliquidTpSl::Tp;
1447 let sl = HyperliquidTpSl::Sl;
1448
1449 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1450 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1451 }
1452
1453 #[rstest]
1454 fn test_hyperliquid_tpsl_deserialization() {
1455 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1456 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1457
1458 assert_eq!(tp, HyperliquidTpSl::Tp);
1459 assert_eq!(sl, HyperliquidTpSl::Sl);
1460 }
1461
1462 #[rstest]
1463 fn test_conditional_order_type_conversions() {
1464 assert_eq!(
1466 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1467 OrderType::StopMarket
1468 );
1469 assert_eq!(
1470 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1471 OrderType::StopLimit
1472 );
1473 assert_eq!(
1474 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1475 OrderType::MarketIfTouched
1476 );
1477 assert_eq!(
1478 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1479 OrderType::LimitIfTouched
1480 );
1481 assert_eq!(
1482 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1483 OrderType::TrailingStopMarket
1484 );
1485 }
1486
1487 mod error_parsing_tests {
1489 use super::*;
1490
1491 #[rstest]
1492 fn test_parse_tick_size_error() {
1493 let error = "Price must be divisible by tick size 0.01";
1494 let code = HyperliquidRejectCode::from_api_error(error);
1495 assert_eq!(code, HyperliquidRejectCode::Tick);
1496 }
1497
1498 #[rstest]
1499 fn test_parse_tick_size_error_case_insensitive() {
1500 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1501 let code = HyperliquidRejectCode::from_api_error(error);
1502 assert_eq!(code, HyperliquidRejectCode::Tick);
1503 }
1504
1505 #[rstest]
1506 fn test_parse_min_notional_perp() {
1507 let error = "Order must have minimum value of $10";
1508 let code = HyperliquidRejectCode::from_api_error(error);
1509 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1510 }
1511
1512 #[rstest]
1513 fn test_parse_min_notional_spot() {
1514 let error = "Order must have minimum value of 10 USDC";
1515 let code = HyperliquidRejectCode::from_api_error(error);
1516 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1517 }
1518
1519 #[rstest]
1520 fn test_parse_insufficient_margin() {
1521 let error = "Insufficient margin to place order";
1522 let code = HyperliquidRejectCode::from_api_error(error);
1523 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1524 }
1525
1526 #[rstest]
1527 fn test_parse_insufficient_margin_case_variations() {
1528 let variations = vec![
1529 "insufficient margin to place order",
1530 "INSUFFICIENT MARGIN TO PLACE ORDER",
1531 " Insufficient margin to place order ", ];
1533
1534 for error in variations {
1535 let code = HyperliquidRejectCode::from_api_error(error);
1536 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1537 }
1538 }
1539
1540 #[rstest]
1541 fn test_parse_reduce_only_violation() {
1542 let error = "Reduce only order would increase position";
1543 let code = HyperliquidRejectCode::from_api_error(error);
1544 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1545 }
1546
1547 #[rstest]
1548 fn test_parse_reduce_only_with_hyphen() {
1549 let error = "Reduce-only order would increase position";
1550 let code = HyperliquidRejectCode::from_api_error(error);
1551 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1552 }
1553
1554 #[rstest]
1555 fn test_parse_post_only_match() {
1556 let error = "Post only order would have immediately matched";
1557 let code = HyperliquidRejectCode::from_api_error(error);
1558 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1559 }
1560
1561 #[rstest]
1562 fn test_parse_post_only_with_hyphen() {
1563 let error = "Post-only order would have immediately matched";
1564 let code = HyperliquidRejectCode::from_api_error(error);
1565 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1566 }
1567
1568 #[rstest]
1569 fn test_parse_ioc_no_match() {
1570 let error = "Order could not immediately match";
1571 let code = HyperliquidRejectCode::from_api_error(error);
1572 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1573 }
1574
1575 #[rstest]
1576 fn test_parse_invalid_trigger_price() {
1577 let error = "Invalid TP/SL price";
1578 let code = HyperliquidRejectCode::from_api_error(error);
1579 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1580 }
1581
1582 #[rstest]
1583 fn test_parse_no_liquidity() {
1584 let error = "No liquidity available for market order";
1585 let code = HyperliquidRejectCode::from_api_error(error);
1586 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1587 }
1588
1589 #[rstest]
1590 fn test_parse_position_increase_at_oi_cap() {
1591 let error = "PositionIncreaseAtOpenInterestCap";
1592 let code = HyperliquidRejectCode::from_api_error(error);
1593 assert_eq!(
1594 code,
1595 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1596 );
1597 }
1598
1599 #[rstest]
1600 fn test_parse_position_flip_at_oi_cap() {
1601 let error = "PositionFlipAtOpenInterestCap";
1602 let code = HyperliquidRejectCode::from_api_error(error);
1603 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1604 }
1605
1606 #[rstest]
1607 fn test_parse_too_aggressive_at_oi_cap() {
1608 let error = "TooAggressiveAtOpenInterestCap";
1609 let code = HyperliquidRejectCode::from_api_error(error);
1610 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1611 }
1612
1613 #[rstest]
1614 fn test_parse_open_interest_increase() {
1615 let error = "OpenInterestIncrease";
1616 let code = HyperliquidRejectCode::from_api_error(error);
1617 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1618 }
1619
1620 #[rstest]
1621 fn test_parse_insufficient_spot_balance() {
1622 let error = "Insufficient spot balance";
1623 let code = HyperliquidRejectCode::from_api_error(error);
1624 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1625 }
1626
1627 #[rstest]
1628 fn test_parse_oracle_error() {
1629 let error = "Oracle price unavailable";
1630 let code = HyperliquidRejectCode::from_api_error(error);
1631 assert_eq!(code, HyperliquidRejectCode::Oracle);
1632 }
1633
1634 #[rstest]
1635 fn test_parse_max_position() {
1636 let error = "Exceeds max position size";
1637 let code = HyperliquidRejectCode::from_api_error(error);
1638 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1639 }
1640
1641 #[rstest]
1642 fn test_parse_missing_order() {
1643 let error = "MissingOrder";
1644 let code = HyperliquidRejectCode::from_api_error(error);
1645 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1646 }
1647
1648 #[rstest]
1649 fn test_parse_unknown_error() {
1650 let error = "This is a completely new error message";
1651 let code = HyperliquidRejectCode::from_api_error(error);
1652 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1653
1654 if let HyperliquidRejectCode::Unknown(msg) = code {
1656 assert_eq!(msg, error);
1657 }
1658 }
1659
1660 #[rstest]
1661 fn test_parse_empty_error() {
1662 let error = "";
1663 let code = HyperliquidRejectCode::from_api_error(error);
1664 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1665 }
1666
1667 #[rstest]
1668 fn test_parse_whitespace_only() {
1669 let error = " ";
1670 let code = HyperliquidRejectCode::from_api_error(error);
1671 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1672 }
1673
1674 #[rstest]
1675 fn test_normalization_preserves_original_in_unknown() {
1676 let error = " UNKNOWN ERROR MESSAGE ";
1677 let code = HyperliquidRejectCode::from_api_error(error);
1678
1679 if let HyperliquidRejectCode::Unknown(msg) = code {
1681 assert_eq!(msg, error);
1682 } else {
1683 panic!("Expected Unknown variant");
1684 }
1685 }
1686 }
1687
1688 #[rstest]
1689 fn test_conditional_order_type_round_trip() {
1690 assert_eq!(
1691 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1692 OrderType::TrailingStopLimit
1693 );
1694
1695 assert_eq!(
1697 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1698 HyperliquidConditionalOrderType::StopMarket
1699 );
1700 assert_eq!(
1701 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1702 HyperliquidConditionalOrderType::StopLimit
1703 );
1704 }
1705
1706 #[rstest]
1707 fn test_trailing_offset_type_serialization() {
1708 let price = HyperliquidTrailingOffsetType::Price;
1709 let percentage = HyperliquidTrailingOffsetType::Percentage;
1710 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1711
1712 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1713 assert_eq!(
1714 serde_json::to_string(&percentage).unwrap(),
1715 r#""percentage""#
1716 );
1717 assert_eq!(
1718 serde_json::to_string(&basis_points).unwrap(),
1719 r#""basispoints""#
1720 );
1721 }
1722
1723 #[rstest]
1724 fn test_conditional_order_type_serialization() {
1725 assert_eq!(
1726 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1727 r#""STOP_MARKET""#
1728 );
1729 assert_eq!(
1730 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1731 r#""STOP_LIMIT""#
1732 );
1733 assert_eq!(
1734 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1735 r#""TAKE_PROFIT_MARKET""#
1736 );
1737 assert_eq!(
1738 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1739 r#""TAKE_PROFIT_LIMIT""#
1740 );
1741 assert_eq!(
1742 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1743 r#""TRAILING_STOP_MARKET""#
1744 );
1745 assert_eq!(
1746 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1747 r#""TRAILING_STOP_LIMIT""#
1748 );
1749 }
1750
1751 #[rstest]
1752 fn test_order_type_enum_coverage() {
1753 let conditional_types = vec![
1755 HyperliquidConditionalOrderType::StopMarket,
1756 HyperliquidConditionalOrderType::StopLimit,
1757 HyperliquidConditionalOrderType::TakeProfitMarket,
1758 HyperliquidConditionalOrderType::TakeProfitLimit,
1759 HyperliquidConditionalOrderType::TrailingStopMarket,
1760 HyperliquidConditionalOrderType::TrailingStopLimit,
1761 ];
1762
1763 for cond_type in conditional_types {
1764 let order_type = OrderType::from(cond_type);
1765 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1766 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1767 }
1768 }
1769
1770 #[rstest]
1771 #[case("BTC-USD-PERP", HyperliquidProductType::Perp)]
1772 #[case("HYPE-USDC-SPOT", HyperliquidProductType::Spot)]
1773 #[case("25-YES-OUTCOME", HyperliquidProductType::Outcome)]
1774 #[case("25-NO-OUTCOME", HyperliquidProductType::Outcome)]
1775 #[case("0-YES-OUTCOME", HyperliquidProductType::Outcome)]
1776 #[case("#10", HyperliquidProductType::Outcome)]
1777 #[case("+31", HyperliquidProductType::Outcome)]
1778 #[case("#0", HyperliquidProductType::Outcome)]
1779 fn test_product_type_from_symbol(
1780 #[case] symbol: &str,
1781 #[case] expected: HyperliquidProductType,
1782 ) {
1783 assert_eq!(
1784 HyperliquidProductType::from_symbol(symbol).unwrap(),
1785 expected
1786 );
1787 }
1788
1789 #[rstest]
1790 #[case("")]
1791 #[case("BTC")]
1792 #[case("#")]
1793 #[case("+")]
1794 #[case("#abc")]
1795 #[case("+12.5")]
1796 #[case("@1")]
1797 #[case("#-1")]
1798 #[case("+-1")]
1799 #[case("25-YES")]
1800 #[case("OUTCOME")]
1801 #[case("25-YES-outcome")]
1802 fn test_product_type_from_symbol_rejects_invalid(#[case] symbol: &str) {
1803 assert!(HyperliquidProductType::from_symbol(symbol).is_err());
1804 }
1805}