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