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 = "outcomeSettledCanceled")]
646 OutcomeSettledCanceled,
647 #[serde(rename = "scheduledCancel")]
649 ScheduledCancel,
650 #[serde(rename = "tickRejected")]
653 TickRejected,
654 #[serde(rename = "minTradeNtlRejected")]
656 MinTradeNtlRejected,
657 #[serde(rename = "minTradeSpotNtlRejected")]
659 MinTradeSpotNtlRejected,
660 #[serde(rename = "perpMarginRejected")]
662 PerpMarginRejected,
663 #[serde(rename = "reduceOnlyRejected")]
665 ReduceOnlyRejected,
666 #[serde(rename = "badAloPxRejected")]
668 BadAloPxRejected,
669 #[serde(rename = "iocCancelRejected")]
671 IocCancelRejected,
672 #[serde(rename = "badTriggerPxRejected")]
674 BadTriggerPxRejected,
675 #[serde(rename = "marketOrderNoLiquidityRejected")]
677 MarketOrderNoLiquidityRejected,
678 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
680 PositionIncreaseAtOpenInterestCapRejected,
681 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
683 PositionFlipAtOpenInterestCapRejected,
684 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
686 TooAggressiveAtOpenInterestCapRejected,
687 #[serde(rename = "openInterestIncreaseRejected")]
689 OpenInterestIncreaseRejected,
690 #[serde(rename = "insufficientSpotBalanceRejected")]
692 InsufficientSpotBalanceRejected,
693 #[serde(rename = "oracleRejected")]
695 OracleRejected,
696 #[serde(rename = "perpMaxPositionRejected")]
698 PerpMaxPositionRejected,
699}
700
701impl From<HyperliquidOrderStatus> for OrderStatus {
702 fn from(status: HyperliquidOrderStatus) -> Self {
703 match status {
704 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
705 HyperliquidOrderStatus::Triggered => Self::Triggered,
706 HyperliquidOrderStatus::Filled => Self::Filled,
707 HyperliquidOrderStatus::Canceled
709 | HyperliquidOrderStatus::MarginCanceled
710 | HyperliquidOrderStatus::VaultWithdrawalCanceled
711 | HyperliquidOrderStatus::OpenInterestCapCanceled
712 | HyperliquidOrderStatus::SelfTradeCanceled
713 | HyperliquidOrderStatus::ReduceOnlyCanceled
714 | HyperliquidOrderStatus::SiblingFilledCanceled
715 | HyperliquidOrderStatus::DelistedCanceled
716 | HyperliquidOrderStatus::LiquidatedCanceled
717 | HyperliquidOrderStatus::OutcomeSettledCanceled
718 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
719 HyperliquidOrderStatus::Rejected
721 | HyperliquidOrderStatus::TickRejected
722 | HyperliquidOrderStatus::MinTradeNtlRejected
723 | HyperliquidOrderStatus::MinTradeSpotNtlRejected
724 | HyperliquidOrderStatus::PerpMarginRejected
725 | HyperliquidOrderStatus::ReduceOnlyRejected
726 | HyperliquidOrderStatus::BadAloPxRejected
727 | HyperliquidOrderStatus::IocCancelRejected
728 | HyperliquidOrderStatus::BadTriggerPxRejected
729 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
730 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
731 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
732 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
733 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
734 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
735 | HyperliquidOrderStatus::OracleRejected
736 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
737 }
738 }
739}
740
741impl HyperliquidOrderStatus {
742 #[must_use]
744 pub const fn rejection_reason(self) -> Option<&'static str> {
745 match self {
746 Self::BadAloPxRejected => Some(HYPERLIQUID_POST_ONLY_WOULD_MATCH),
747 Self::ReduceOnlyRejected => Some("Reduce only order would increase position."),
748 Self::IocCancelRejected => {
749 Some("Order could not immediately match against any resting orders")
750 }
751 _ => None,
752 }
753 }
754}
755
756#[derive(
767 Copy,
768 Clone,
769 Debug,
770 Display,
771 PartialEq,
772 Eq,
773 Hash,
774 AsRefStr,
775 EnumIter,
776 EnumString,
777 Serialize,
778 Deserialize,
779)]
780#[serde(rename_all = "PascalCase")]
781#[strum(serialize_all = "PascalCase")]
782pub enum HyperliquidFillDirection {
783 #[serde(rename = "Open Long")]
785 #[strum(serialize = "Open Long")]
786 OpenLong,
787 #[serde(rename = "Open Short")]
789 #[strum(serialize = "Open Short")]
790 OpenShort,
791 #[serde(rename = "Close Long")]
793 #[strum(serialize = "Close Long")]
794 CloseLong,
795 #[serde(rename = "Close Short")]
797 #[strum(serialize = "Close Short")]
798 CloseShort,
799 #[serde(rename = "Long > Short")]
801 #[strum(serialize = "Long > Short")]
802 LongToShort,
803 #[serde(rename = "Short > Long")]
805 #[strum(serialize = "Short > Long")]
806 ShortToLong,
807 #[serde(rename = "Auto-Deleveraging")]
809 #[strum(serialize = "Auto-Deleveraging")]
810 AutoDeleveraging,
811 #[serde(rename = "Net Child Vaults")]
813 #[strum(serialize = "Net Child Vaults")]
814 NetChildVaults,
815 Buy,
817 Sell,
819 #[serde(rename = "Spot Dust Conversion")]
821 #[strum(serialize = "Spot Dust Conversion")]
822 SpotDustConversion,
823 #[serde(rename = "Settlement")]
826 #[strum(serialize = "Settlement")]
827 Settlement,
828 #[serde(rename = "Split Outcome")]
831 #[strum(serialize = "Split Outcome")]
832 SplitOutcome,
833 #[serde(rename = "Merge Outcome")]
836 #[strum(serialize = "Merge Outcome")]
837 MergeOutcome,
838 #[serde(rename = "Merge Question")]
841 #[strum(serialize = "Merge Question")]
842 MergeQuestion,
843 #[serde(rename = "Negate Outcome")]
846 #[strum(serialize = "Negate Outcome")]
847 NegateOutcome,
848 #[serde(other)]
850 Unknown,
851}
852
853#[derive(
857 Copy,
858 Clone,
859 Debug,
860 Display,
861 PartialEq,
862 Eq,
863 Hash,
864 AsRefStr,
865 EnumIter,
866 EnumString,
867 Serialize,
868 Deserialize,
869)]
870#[serde(rename_all = "camelCase")]
871#[strum(serialize_all = "camelCase")]
872pub enum HyperliquidInfoRequestType {
873 Meta,
875 SpotMeta,
877 MetaAndAssetCtxs,
879 SpotMetaAndAssetCtxs,
881 OutcomeMeta,
883 L2Book,
885 AllMids,
887 RecentTrades,
889 UserFills,
891 UserFillsByTime,
893 OrderStatus,
895 OpenOrders,
897 FrontendOpenOrders,
899 ClearinghouseState,
901 SpotClearinghouseState,
903 ExchangeStatus,
905 CandleSnapshot,
907 Candle,
909 HistoricalOrders,
911 FundingHistory,
913 UserFunding,
915 NonUserFundingUpdates,
917 TwapHistory,
919 UserTwapSliceFills,
921 UserTwapSliceFillsByTime,
923 UserRateLimit,
925 UserRole,
927 DelegatorHistory,
929 DelegatorRewards,
931 ValidatorStats,
933 UserFees,
935 PerpDexs,
937 AllPerpMetas,
939 UserAbstraction,
941}
942
943impl HyperliquidInfoRequestType {
944 pub fn as_str(&self) -> &'static str {
945 match self {
946 Self::Meta => "meta",
947 Self::SpotMeta => "spotMeta",
948 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
949 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
950 Self::OutcomeMeta => "outcomeMeta",
951 Self::L2Book => "l2Book",
952 Self::AllMids => "allMids",
953 Self::RecentTrades => "recentTrades",
954 Self::UserFills => "userFills",
955 Self::UserFillsByTime => "userFillsByTime",
956 Self::OrderStatus => "orderStatus",
957 Self::OpenOrders => "openOrders",
958 Self::FrontendOpenOrders => "frontendOpenOrders",
959 Self::ClearinghouseState => "clearinghouseState",
960 Self::SpotClearinghouseState => "spotClearinghouseState",
961 Self::ExchangeStatus => "exchangeStatus",
962 Self::CandleSnapshot => "candleSnapshot",
963 Self::Candle => "candle",
964 Self::HistoricalOrders => "historicalOrders",
965 Self::FundingHistory => "fundingHistory",
966 Self::UserFunding => "userFunding",
967 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
968 Self::TwapHistory => "twapHistory",
969 Self::UserTwapSliceFills => "userTwapSliceFills",
970 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
971 Self::UserRateLimit => "userRateLimit",
972 Self::UserRole => "userRole",
973 Self::DelegatorHistory => "delegatorHistory",
974 Self::DelegatorRewards => "delegatorRewards",
975 Self::ValidatorStats => "validatorStats",
976 Self::UserFees => "userFees",
977 Self::PerpDexs => "perpDexs",
978 Self::AllPerpMetas => "allPerpMetas",
979 Self::UserAbstraction => "userAbstraction",
980 }
981 }
982}
983
984#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr)]
990#[serde(rename_all = "camelCase")]
991#[strum(serialize_all = "camelCase")]
992pub enum HyperliquidAccountAbstraction {
993 UnifiedAccount,
994 PortfolioMargin,
995 Disabled,
996 Default,
997 DexAbstraction,
998 #[serde(other)]
999 Unknown,
1000}
1001
1002impl HyperliquidAccountAbstraction {
1003 #[must_use]
1005 pub fn uses_spot_collateral(self) -> bool {
1006 matches!(self, Self::UnifiedAccount | Self::PortfolioMargin)
1007 }
1008}
1009
1010#[derive(
1011 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
1012)]
1013#[serde(rename_all = "lowercase")]
1014#[strum(serialize_all = "lowercase")]
1015pub enum HyperliquidLeverageType {
1016 Cross,
1017 Isolated,
1018 #[serde(other)]
1019 Unknown,
1020}
1021
1022#[derive(
1024 Copy,
1025 Clone,
1026 Debug,
1027 Display,
1028 PartialEq,
1029 Eq,
1030 Hash,
1031 AsRefStr,
1032 EnumIter,
1033 EnumString,
1034 Serialize,
1035 Deserialize,
1036)]
1037#[cfg_attr(
1038 feature = "python",
1039 pyo3::pyclass(
1040 module = "nautilus_trader.adapters.hyperliquid",
1041 from_py_object,
1042 rename_all = "SCREAMING_SNAKE_CASE",
1043 )
1044)]
1045#[cfg_attr(
1046 feature = "python",
1047 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1048)]
1049#[serde(rename_all = "UPPERCASE")]
1050#[strum(serialize_all = "UPPERCASE")]
1051pub enum HyperliquidProductType {
1052 Perp,
1054 Spot,
1056 Outcome,
1058}
1059
1060impl HyperliquidProductType {
1061 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
1072 if symbol.ends_with("-PERP") {
1073 Ok(Self::Perp)
1074 } else if symbol.ends_with("-SPOT") {
1075 Ok(Self::Spot)
1076 } else if symbol.ends_with(OUTCOME_SYMBOL_SUFFIX) || is_outcome_wire_symbol(symbol) {
1077 Ok(Self::Outcome)
1078 } else {
1079 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
1080 }
1081 }
1082}
1083
1084fn is_outcome_wire_symbol(symbol: &str) -> bool {
1087 let Some(rest) = symbol
1088 .strip_prefix('#')
1089 .or_else(|| symbol.strip_prefix('+'))
1090 else {
1091 return false;
1092 };
1093 !rest.is_empty() && rest.parse::<u32>().is_ok()
1094}
1095
1096#[derive(
1098 Copy,
1099 Clone,
1100 Debug,
1101 Default,
1102 Display,
1103 PartialEq,
1104 Eq,
1105 Hash,
1106 AsRefStr,
1107 EnumIter,
1108 EnumString,
1109 Serialize,
1110 Deserialize,
1111)]
1112#[serde(rename_all = "lowercase")]
1113#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1114#[cfg_attr(
1115 feature = "python",
1116 pyo3::pyclass(
1117 eq,
1118 eq_int,
1119 module = "nautilus_trader.adapters.hyperliquid",
1120 from_py_object,
1121 rename_all = "SCREAMING_SNAKE_CASE",
1122 )
1123)]
1124#[cfg_attr(
1125 feature = "python",
1126 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1127)]
1128pub enum HyperliquidEnvironment {
1129 #[default]
1131 Mainnet,
1132 Testnet,
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use nautilus_model::enums::OrderType;
1139 use rstest::rstest;
1140 use serde_json;
1141
1142 use super::*;
1143
1144 #[rstest]
1145 fn test_side_serde() {
1146 let buy_side = HyperliquidSide::Buy;
1147 let sell_side = HyperliquidSide::Sell;
1148
1149 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
1150 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
1151
1152 assert_eq!(
1153 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
1154 HyperliquidSide::Buy
1155 );
1156 assert_eq!(
1157 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
1158 HyperliquidSide::Sell
1159 );
1160 }
1161
1162 #[rstest]
1163 fn test_side_from_order_side() {
1164 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
1166 assert_eq!(
1167 HyperliquidSide::from(OrderSide::Sell),
1168 HyperliquidSide::Sell
1169 );
1170 }
1171
1172 #[rstest]
1173 fn test_order_side_from_hyperliquid_side() {
1174 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy,);
1176 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell,);
1177 }
1178
1179 #[rstest]
1180 fn test_aggressor_side_from_hyperliquid_side() {
1181 assert_eq!(
1183 AggressorSide::from(HyperliquidSide::Buy),
1184 AggressorSide::Buy
1185 );
1186 assert_eq!(
1187 AggressorSide::from(HyperliquidSide::Sell),
1188 AggressorSide::Sell
1189 );
1190 }
1191
1192 #[rstest]
1193 fn test_time_in_force_serde() {
1194 let test_cases = [
1195 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1196 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1197 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1198 (HyperliquidTimeInForce::FrontendMarket, "\"FrontendMarket\""),
1199 (
1200 HyperliquidTimeInForce::LiquidationMarket,
1201 "\"LiquidationMarket\"",
1202 ),
1203 ];
1204
1205 for (tif, expected_json) in test_cases {
1206 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1207 assert_eq!(
1208 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1209 tif
1210 );
1211 }
1212 }
1213
1214 #[rstest]
1215 fn test_info_request_type_outcome_meta_as_str() {
1216 assert_eq!(
1217 HyperliquidInfoRequestType::OutcomeMeta.as_str(),
1218 "outcomeMeta"
1219 );
1220 }
1221
1222 #[rstest]
1223 fn test_info_request_type_recent_trades_as_str() {
1224 assert_eq!(
1225 HyperliquidInfoRequestType::RecentTrades.as_str(),
1226 "recentTrades"
1227 );
1228 }
1229
1230 #[rstest]
1231 fn test_fill_direction_serde() {
1232 let cases = [
1233 (HyperliquidFillDirection::OpenLong, "\"Open Long\""),
1234 (HyperliquidFillDirection::CloseShort, "\"Close Short\""),
1235 (HyperliquidFillDirection::LongToShort, "\"Long > Short\""),
1236 (
1237 HyperliquidFillDirection::AutoDeleveraging,
1238 "\"Auto-Deleveraging\"",
1239 ),
1240 (
1241 HyperliquidFillDirection::NetChildVaults,
1242 "\"Net Child Vaults\"",
1243 ),
1244 (HyperliquidFillDirection::Buy, "\"Buy\""),
1245 (
1246 HyperliquidFillDirection::SpotDustConversion,
1247 "\"Spot Dust Conversion\"",
1248 ),
1249 (HyperliquidFillDirection::Settlement, "\"Settlement\""),
1250 (HyperliquidFillDirection::SplitOutcome, "\"Split Outcome\""),
1251 (HyperliquidFillDirection::MergeOutcome, "\"Merge Outcome\""),
1252 (
1253 HyperliquidFillDirection::MergeQuestion,
1254 "\"Merge Question\"",
1255 ),
1256 (
1257 HyperliquidFillDirection::NegateOutcome,
1258 "\"Negate Outcome\"",
1259 ),
1260 ];
1261
1262 for (variant, expected) in cases {
1263 assert_eq!(serde_json::to_string(&variant).unwrap(), expected);
1264 assert_eq!(
1265 serde_json::from_str::<HyperliquidFillDirection>(expected).unwrap(),
1266 variant
1267 );
1268 }
1269 }
1270
1271 #[rstest]
1272 fn test_fill_direction_unknown_is_lenient() {
1273 assert_eq!(
1274 serde_json::from_str::<HyperliquidFillDirection>("\"Some New Direction\"").unwrap(),
1275 HyperliquidFillDirection::Unknown,
1276 );
1277 }
1278
1279 #[rstest]
1280 fn test_position_type_unknown_is_lenient() {
1281 assert_eq!(
1282 serde_json::from_str::<HyperliquidPositionType>("\"hedge\"").unwrap(),
1283 HyperliquidPositionType::Unknown,
1284 );
1285 }
1286
1287 #[rstest]
1288 fn test_twap_status_unknown_is_lenient() {
1289 assert_eq!(
1290 serde_json::from_str::<HyperliquidTwapStatus>("\"paused\"").unwrap(),
1291 HyperliquidTwapStatus::Unknown,
1292 );
1293 assert_eq!(
1294 serde_json::from_str::<HyperliquidTwapStatus>("\"waitingForTrigger\"").unwrap(),
1295 HyperliquidTwapStatus::Unknown,
1296 );
1297 }
1298
1299 #[rstest]
1300 fn test_liquidity_flag_from_crossed() {
1301 assert_eq!(
1302 HyperliquidLiquidityFlag::from(true),
1303 HyperliquidLiquidityFlag::Taker
1304 );
1305 assert_eq!(
1306 HyperliquidLiquidityFlag::from(false),
1307 HyperliquidLiquidityFlag::Maker
1308 );
1309 }
1310
1311 #[rstest]
1312 #[allow(deprecated)]
1313 fn test_reject_code_from_error_string() {
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_error_string(error_str),
1340 expected_code
1341 );
1342 }
1343 }
1344
1345 #[rstest]
1346 fn test_reject_code_from_api_error() {
1347 let test_cases = [
1348 (
1349 "Price must be divisible by tick size.",
1350 HyperliquidRejectCode::Tick,
1351 ),
1352 (
1353 "Order must have minimum value of $10.",
1354 HyperliquidRejectCode::MinTradeNtl,
1355 ),
1356 (
1357 "Insufficient margin to place order.",
1358 HyperliquidRejectCode::PerpMargin,
1359 ),
1360 (
1361 "Post only order would have immediately matched, bbo was 1.23",
1362 HyperliquidRejectCode::BadAloPx,
1363 ),
1364 (
1365 "Some unknown error",
1366 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1367 ),
1368 ];
1369
1370 for (error_str, expected_code) in test_cases {
1371 assert_eq!(
1372 HyperliquidRejectCode::from_api_error(error_str),
1373 expected_code
1374 );
1375 }
1376 }
1377
1378 #[rstest]
1379 fn test_reduce_only() {
1380 let reduce_only = HyperliquidReduceOnly::new(true);
1381
1382 assert!(reduce_only.is_reduce_only());
1383
1384 let json = serde_json::to_string(&reduce_only).unwrap();
1385 assert_eq!(json, "true");
1386
1387 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1388 assert_eq!(parsed, reduce_only);
1389 }
1390
1391 #[rstest]
1392 fn test_order_status_conversion() {
1393 assert_eq!(
1395 OrderStatus::from(HyperliquidOrderStatus::Open),
1396 OrderStatus::Accepted
1397 );
1398 assert_eq!(
1399 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1400 OrderStatus::Accepted
1401 );
1402 assert_eq!(
1403 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1404 OrderStatus::Triggered
1405 );
1406 assert_eq!(
1407 OrderStatus::from(HyperliquidOrderStatus::Filled),
1408 OrderStatus::Filled
1409 );
1410 assert_eq!(
1411 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1412 OrderStatus::Canceled
1413 );
1414 assert_eq!(
1415 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1416 OrderStatus::Rejected
1417 );
1418
1419 assert_eq!(
1421 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1422 OrderStatus::Canceled
1423 );
1424 assert_eq!(
1425 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1426 OrderStatus::Canceled
1427 );
1428 assert_eq!(
1429 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1430 OrderStatus::Canceled
1431 );
1432 assert_eq!(
1433 OrderStatus::from(HyperliquidOrderStatus::OutcomeSettledCanceled),
1434 OrderStatus::Canceled
1435 );
1436
1437 assert_eq!(
1439 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1440 OrderStatus::Rejected
1441 );
1442 assert_eq!(
1443 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1444 OrderStatus::Rejected
1445 );
1446 }
1447
1448 #[rstest]
1449 fn test_order_status_serde_deserialization() {
1450 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1452 assert_eq!(open, HyperliquidOrderStatus::Open);
1453
1454 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1455 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1456
1457 let margin_canceled: HyperliquidOrderStatus =
1458 serde_json::from_str(r#""marginCanceled""#).unwrap();
1459 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1460
1461 let self_trade_canceled: HyperliquidOrderStatus =
1462 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1463 assert_eq!(
1464 self_trade_canceled,
1465 HyperliquidOrderStatus::SelfTradeCanceled
1466 );
1467
1468 let reduce_only_canceled: HyperliquidOrderStatus =
1469 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1470 assert_eq!(
1471 reduce_only_canceled,
1472 HyperliquidOrderStatus::ReduceOnlyCanceled
1473 );
1474
1475 let outcome_settled_canceled: HyperliquidOrderStatus =
1476 serde_json::from_str(r#""outcomeSettledCanceled""#).unwrap();
1477 assert_eq!(
1478 outcome_settled_canceled,
1479 HyperliquidOrderStatus::OutcomeSettledCanceled
1480 );
1481
1482 let tick_rejected: HyperliquidOrderStatus =
1483 serde_json::from_str(r#""tickRejected""#).unwrap();
1484 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1485 }
1486
1487 #[rstest]
1488 fn test_hyperliquid_tpsl_serialization() {
1489 let tp = HyperliquidTpSl::Tp;
1490 let sl = HyperliquidTpSl::Sl;
1491
1492 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1493 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1494 }
1495
1496 #[rstest]
1497 fn test_hyperliquid_tpsl_deserialization() {
1498 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1499 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1500
1501 assert_eq!(tp, HyperliquidTpSl::Tp);
1502 assert_eq!(sl, HyperliquidTpSl::Sl);
1503 }
1504
1505 #[rstest]
1506 fn test_conditional_order_type_conversions() {
1507 assert_eq!(
1509 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1510 OrderType::StopMarket
1511 );
1512 assert_eq!(
1513 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1514 OrderType::StopLimit
1515 );
1516 assert_eq!(
1517 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1518 OrderType::MarketIfTouched
1519 );
1520 assert_eq!(
1521 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1522 OrderType::LimitIfTouched
1523 );
1524 assert_eq!(
1525 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1526 OrderType::TrailingStopMarket
1527 );
1528 }
1529
1530 mod error_parsing_tests {
1532 use super::*;
1533
1534 #[rstest]
1535 fn test_parse_tick_size_error() {
1536 let error = "Price must be divisible by tick size 0.01";
1537 let code = HyperliquidRejectCode::from_api_error(error);
1538 assert_eq!(code, HyperliquidRejectCode::Tick);
1539 }
1540
1541 #[rstest]
1542 fn test_parse_tick_size_error_case_insensitive() {
1543 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1544 let code = HyperliquidRejectCode::from_api_error(error);
1545 assert_eq!(code, HyperliquidRejectCode::Tick);
1546 }
1547
1548 #[rstest]
1549 fn test_parse_min_notional_perp() {
1550 let error = "Order must have minimum value of $10";
1551 let code = HyperliquidRejectCode::from_api_error(error);
1552 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1553 }
1554
1555 #[rstest]
1556 fn test_parse_min_notional_spot() {
1557 let error = "Order must have minimum value of 10 USDC";
1558 let code = HyperliquidRejectCode::from_api_error(error);
1559 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1560 }
1561
1562 #[rstest]
1563 fn test_parse_insufficient_margin() {
1564 let error = "Insufficient margin to place order";
1565 let code = HyperliquidRejectCode::from_api_error(error);
1566 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1567 }
1568
1569 #[rstest]
1570 fn test_parse_insufficient_margin_case_variations() {
1571 let variations = vec![
1572 "insufficient margin to place order",
1573 "INSUFFICIENT MARGIN TO PLACE ORDER",
1574 " Insufficient margin to place order ", ];
1576
1577 for error in variations {
1578 let code = HyperliquidRejectCode::from_api_error(error);
1579 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1580 }
1581 }
1582
1583 #[rstest]
1584 fn test_parse_reduce_only_violation() {
1585 let error = "Reduce only order would increase position";
1586 let code = HyperliquidRejectCode::from_api_error(error);
1587 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1588 }
1589
1590 #[rstest]
1591 fn test_parse_reduce_only_with_hyphen() {
1592 let error = "Reduce-only order would increase position";
1593 let code = HyperliquidRejectCode::from_api_error(error);
1594 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1595 }
1596
1597 #[rstest]
1598 fn test_parse_post_only_match() {
1599 let error = "Post only order would have immediately matched";
1600 let code = HyperliquidRejectCode::from_api_error(error);
1601 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1602 }
1603
1604 #[rstest]
1605 fn test_parse_post_only_with_hyphen() {
1606 let error = "Post-only order would have immediately matched";
1607 let code = HyperliquidRejectCode::from_api_error(error);
1608 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1609 }
1610
1611 #[rstest]
1612 fn test_parse_ioc_no_match() {
1613 let error = "Order could not immediately match";
1614 let code = HyperliquidRejectCode::from_api_error(error);
1615 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1616 }
1617
1618 #[rstest]
1619 fn test_parse_invalid_trigger_price() {
1620 let error = "Invalid TP/SL price";
1621 let code = HyperliquidRejectCode::from_api_error(error);
1622 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1623 }
1624
1625 #[rstest]
1626 fn test_parse_no_liquidity() {
1627 let error = "No liquidity available for market order";
1628 let code = HyperliquidRejectCode::from_api_error(error);
1629 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1630 }
1631
1632 #[rstest]
1633 fn test_parse_position_increase_at_oi_cap() {
1634 let error = "PositionIncreaseAtOpenInterestCap";
1635 let code = HyperliquidRejectCode::from_api_error(error);
1636 assert_eq!(
1637 code,
1638 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1639 );
1640 }
1641
1642 #[rstest]
1643 fn test_parse_position_flip_at_oi_cap() {
1644 let error = "PositionFlipAtOpenInterestCap";
1645 let code = HyperliquidRejectCode::from_api_error(error);
1646 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1647 }
1648
1649 #[rstest]
1650 fn test_parse_too_aggressive_at_oi_cap() {
1651 let error = "TooAggressiveAtOpenInterestCap";
1652 let code = HyperliquidRejectCode::from_api_error(error);
1653 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1654 }
1655
1656 #[rstest]
1657 fn test_parse_open_interest_increase() {
1658 let error = "OpenInterestIncrease";
1659 let code = HyperliquidRejectCode::from_api_error(error);
1660 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1661 }
1662
1663 #[rstest]
1664 fn test_parse_insufficient_spot_balance() {
1665 let error = "Insufficient spot balance";
1666 let code = HyperliquidRejectCode::from_api_error(error);
1667 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1668 }
1669
1670 #[rstest]
1671 fn test_parse_oracle_error() {
1672 let error = "Oracle price unavailable";
1673 let code = HyperliquidRejectCode::from_api_error(error);
1674 assert_eq!(code, HyperliquidRejectCode::Oracle);
1675 }
1676
1677 #[rstest]
1678 fn test_parse_max_position() {
1679 let error = "Exceeds max position size";
1680 let code = HyperliquidRejectCode::from_api_error(error);
1681 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1682 }
1683
1684 #[rstest]
1685 fn test_parse_missing_order() {
1686 let error = "MissingOrder";
1687 let code = HyperliquidRejectCode::from_api_error(error);
1688 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1689 }
1690
1691 #[rstest]
1692 fn test_parse_unknown_error() {
1693 let error = "This is a completely new error message";
1694 let code = HyperliquidRejectCode::from_api_error(error);
1695 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1696
1697 if let HyperliquidRejectCode::Unknown(msg) = code {
1699 assert_eq!(msg, error);
1700 }
1701 }
1702
1703 #[rstest]
1704 fn test_parse_empty_error() {
1705 let error = "";
1706 let code = HyperliquidRejectCode::from_api_error(error);
1707 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1708 }
1709
1710 #[rstest]
1711 fn test_parse_whitespace_only() {
1712 let error = " ";
1713 let code = HyperliquidRejectCode::from_api_error(error);
1714 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1715 }
1716
1717 #[rstest]
1718 fn test_normalization_preserves_original_in_unknown() {
1719 let error = " UNKNOWN ERROR MESSAGE ";
1720 let code = HyperliquidRejectCode::from_api_error(error);
1721
1722 if let HyperliquidRejectCode::Unknown(msg) = code {
1724 assert_eq!(msg, error);
1725 } else {
1726 panic!("Expected Unknown variant");
1727 }
1728 }
1729 }
1730
1731 #[rstest]
1732 fn test_conditional_order_type_round_trip() {
1733 assert_eq!(
1734 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1735 OrderType::TrailingStopLimit
1736 );
1737
1738 assert_eq!(
1740 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1741 HyperliquidConditionalOrderType::StopMarket
1742 );
1743 assert_eq!(
1744 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1745 HyperliquidConditionalOrderType::StopLimit
1746 );
1747 }
1748
1749 #[rstest]
1750 fn test_trailing_offset_type_serialization() {
1751 let price = HyperliquidTrailingOffsetType::Price;
1752 let percentage = HyperliquidTrailingOffsetType::Percentage;
1753 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1754
1755 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1756 assert_eq!(
1757 serde_json::to_string(&percentage).unwrap(),
1758 r#""percentage""#
1759 );
1760 assert_eq!(
1761 serde_json::to_string(&basis_points).unwrap(),
1762 r#""basispoints""#
1763 );
1764 }
1765
1766 #[rstest]
1767 fn test_conditional_order_type_serialization() {
1768 assert_eq!(
1769 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1770 r#""STOP_MARKET""#
1771 );
1772 assert_eq!(
1773 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1774 r#""STOP_LIMIT""#
1775 );
1776 assert_eq!(
1777 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1778 r#""TAKE_PROFIT_MARKET""#
1779 );
1780 assert_eq!(
1781 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1782 r#""TAKE_PROFIT_LIMIT""#
1783 );
1784 assert_eq!(
1785 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1786 r#""TRAILING_STOP_MARKET""#
1787 );
1788 assert_eq!(
1789 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1790 r#""TRAILING_STOP_LIMIT""#
1791 );
1792 }
1793
1794 #[rstest]
1795 fn test_order_type_enum_coverage() {
1796 let conditional_types = vec![
1798 HyperliquidConditionalOrderType::StopMarket,
1799 HyperliquidConditionalOrderType::StopLimit,
1800 HyperliquidConditionalOrderType::TakeProfitMarket,
1801 HyperliquidConditionalOrderType::TakeProfitLimit,
1802 HyperliquidConditionalOrderType::TrailingStopMarket,
1803 HyperliquidConditionalOrderType::TrailingStopLimit,
1804 ];
1805
1806 for cond_type in conditional_types {
1807 let order_type = OrderType::from(cond_type);
1808 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1809 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1810 }
1811 }
1812
1813 #[rstest]
1814 #[case("BTC-USD-PERP", HyperliquidProductType::Perp)]
1815 #[case("HYPE-USDC-SPOT", HyperliquidProductType::Spot)]
1816 #[case("25-YES-OUTCOME", HyperliquidProductType::Outcome)]
1817 #[case("25-NO-OUTCOME", HyperliquidProductType::Outcome)]
1818 #[case("0-YES-OUTCOME", HyperliquidProductType::Outcome)]
1819 #[case("#10", HyperliquidProductType::Outcome)]
1820 #[case("+31", HyperliquidProductType::Outcome)]
1821 #[case("#0", HyperliquidProductType::Outcome)]
1822 fn test_product_type_from_symbol(
1823 #[case] symbol: &str,
1824 #[case] expected: HyperliquidProductType,
1825 ) {
1826 assert_eq!(
1827 HyperliquidProductType::from_symbol(symbol).unwrap(),
1828 expected
1829 );
1830 }
1831
1832 #[rstest]
1833 #[case("")]
1834 #[case("BTC")]
1835 #[case("#")]
1836 #[case("+")]
1837 #[case("#abc")]
1838 #[case("+12.5")]
1839 #[case("@1")]
1840 #[case("#-1")]
1841 #[case("+-1")]
1842 #[case("25-YES")]
1843 #[case("OUTCOME")]
1844 #[case("25-YES-outcome")]
1845 fn test_product_type_from_symbol_rejects_invalid(#[case] symbol: &str) {
1846 assert!(HyperliquidProductType::from_symbol(symbol).is_err());
1847 }
1848
1849 #[rstest]
1850 #[case("\"unifiedAccount\"", HyperliquidAccountAbstraction::UnifiedAccount)]
1851 #[case("\"portfolioMargin\"", HyperliquidAccountAbstraction::PortfolioMargin)]
1852 #[case("\"disabled\"", HyperliquidAccountAbstraction::Disabled)]
1853 #[case("\"default\"", HyperliquidAccountAbstraction::Default)]
1854 #[case("\"dexAbstraction\"", HyperliquidAccountAbstraction::DexAbstraction)]
1855 #[case("\"someFutureMode\"", HyperliquidAccountAbstraction::Unknown)]
1856 fn test_account_abstraction_deserialize(
1857 #[case] json: &str,
1858 #[case] expected: HyperliquidAccountAbstraction,
1859 ) {
1860 let parsed: HyperliquidAccountAbstraction = serde_json::from_str(json).unwrap();
1861 assert_eq!(parsed, expected);
1862 }
1863
1864 #[rstest]
1865 #[case(HyperliquidAccountAbstraction::UnifiedAccount, true)]
1866 #[case(HyperliquidAccountAbstraction::PortfolioMargin, true)]
1867 #[case(HyperliquidAccountAbstraction::Disabled, false)]
1868 #[case(HyperliquidAccountAbstraction::Default, false)]
1869 #[case(HyperliquidAccountAbstraction::DexAbstraction, false)]
1870 #[case(HyperliquidAccountAbstraction::Unknown, false)]
1871 fn test_account_abstraction_uses_spot_collateral(
1872 #[case] abstraction: HyperliquidAccountAbstraction,
1873 #[case] expected: bool,
1874 ) {
1875 assert_eq!(abstraction.uses_spot_collateral(), expected);
1876 }
1877}