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