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
734#[derive(
745 Copy,
746 Clone,
747 Debug,
748 Display,
749 PartialEq,
750 Eq,
751 Hash,
752 AsRefStr,
753 EnumIter,
754 EnumString,
755 Serialize,
756 Deserialize,
757)]
758#[serde(rename_all = "PascalCase")]
759#[strum(serialize_all = "PascalCase")]
760pub enum HyperliquidFillDirection {
761 #[serde(rename = "Open Long")]
763 #[strum(serialize = "Open Long")]
764 OpenLong,
765 #[serde(rename = "Open Short")]
767 #[strum(serialize = "Open Short")]
768 OpenShort,
769 #[serde(rename = "Close Long")]
771 #[strum(serialize = "Close Long")]
772 CloseLong,
773 #[serde(rename = "Close Short")]
775 #[strum(serialize = "Close Short")]
776 CloseShort,
777 #[serde(rename = "Long > Short")]
779 #[strum(serialize = "Long > Short")]
780 LongToShort,
781 #[serde(rename = "Short > Long")]
783 #[strum(serialize = "Short > Long")]
784 ShortToLong,
785 #[serde(rename = "Auto-Deleveraging")]
787 #[strum(serialize = "Auto-Deleveraging")]
788 AutoDeleveraging,
789 #[serde(rename = "Net Child Vaults")]
791 #[strum(serialize = "Net Child Vaults")]
792 NetChildVaults,
793 Buy,
795 Sell,
797 #[serde(rename = "Spot Dust Conversion")]
799 #[strum(serialize = "Spot Dust Conversion")]
800 SpotDustConversion,
801 #[serde(rename = "Settlement")]
804 #[strum(serialize = "Settlement")]
805 Settlement,
806 #[serde(rename = "Split Outcome")]
809 #[strum(serialize = "Split Outcome")]
810 SplitOutcome,
811 #[serde(rename = "Merge Outcome")]
814 #[strum(serialize = "Merge Outcome")]
815 MergeOutcome,
816 #[serde(rename = "Merge Question")]
819 #[strum(serialize = "Merge Question")]
820 MergeQuestion,
821 #[serde(rename = "Negate Outcome")]
824 #[strum(serialize = "Negate Outcome")]
825 NegateOutcome,
826 #[serde(other)]
828 Unknown,
829}
830
831#[derive(
835 Copy,
836 Clone,
837 Debug,
838 Display,
839 PartialEq,
840 Eq,
841 Hash,
842 AsRefStr,
843 EnumIter,
844 EnumString,
845 Serialize,
846 Deserialize,
847)]
848#[serde(rename_all = "camelCase")]
849#[strum(serialize_all = "camelCase")]
850pub enum HyperliquidInfoRequestType {
851 Meta,
853 SpotMeta,
855 MetaAndAssetCtxs,
857 SpotMetaAndAssetCtxs,
859 OutcomeMeta,
861 L2Book,
863 AllMids,
865 RecentTrades,
867 UserFills,
869 UserFillsByTime,
871 OrderStatus,
873 OpenOrders,
875 FrontendOpenOrders,
877 ClearinghouseState,
879 SpotClearinghouseState,
881 ExchangeStatus,
883 CandleSnapshot,
885 Candle,
887 HistoricalOrders,
889 FundingHistory,
891 UserFunding,
893 NonUserFundingUpdates,
895 TwapHistory,
897 UserTwapSliceFills,
899 UserTwapSliceFillsByTime,
901 UserRateLimit,
903 UserRole,
905 DelegatorHistory,
907 DelegatorRewards,
909 ValidatorStats,
911 UserFees,
913 PerpDexs,
915 AllPerpMetas,
917}
918
919impl HyperliquidInfoRequestType {
920 pub fn as_str(&self) -> &'static str {
921 match self {
922 Self::Meta => "meta",
923 Self::SpotMeta => "spotMeta",
924 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
925 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
926 Self::OutcomeMeta => "outcomeMeta",
927 Self::L2Book => "l2Book",
928 Self::AllMids => "allMids",
929 Self::RecentTrades => "recentTrades",
930 Self::UserFills => "userFills",
931 Self::UserFillsByTime => "userFillsByTime",
932 Self::OrderStatus => "orderStatus",
933 Self::OpenOrders => "openOrders",
934 Self::FrontendOpenOrders => "frontendOpenOrders",
935 Self::ClearinghouseState => "clearinghouseState",
936 Self::SpotClearinghouseState => "spotClearinghouseState",
937 Self::ExchangeStatus => "exchangeStatus",
938 Self::CandleSnapshot => "candleSnapshot",
939 Self::Candle => "candle",
940 Self::HistoricalOrders => "historicalOrders",
941 Self::FundingHistory => "fundingHistory",
942 Self::UserFunding => "userFunding",
943 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
944 Self::TwapHistory => "twapHistory",
945 Self::UserTwapSliceFills => "userTwapSliceFills",
946 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
947 Self::UserRateLimit => "userRateLimit",
948 Self::UserRole => "userRole",
949 Self::DelegatorHistory => "delegatorHistory",
950 Self::DelegatorRewards => "delegatorRewards",
951 Self::ValidatorStats => "validatorStats",
952 Self::UserFees => "userFees",
953 Self::PerpDexs => "perpDexs",
954 Self::AllPerpMetas => "allPerpMetas",
955 }
956 }
957}
958
959#[derive(
960 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
961)]
962#[serde(rename_all = "lowercase")]
963#[strum(serialize_all = "lowercase")]
964pub enum HyperliquidLeverageType {
965 Cross,
966 Isolated,
967 #[serde(other)]
968 Unknown,
969}
970
971#[derive(
973 Copy,
974 Clone,
975 Debug,
976 Display,
977 PartialEq,
978 Eq,
979 Hash,
980 AsRefStr,
981 EnumIter,
982 EnumString,
983 Serialize,
984 Deserialize,
985)]
986#[cfg_attr(
987 feature = "python",
988 pyo3::pyclass(
989 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
990 from_py_object,
991 rename_all = "SCREAMING_SNAKE_CASE",
992 )
993)]
994#[cfg_attr(
995 feature = "python",
996 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
997)]
998#[serde(rename_all = "UPPERCASE")]
999#[strum(serialize_all = "UPPERCASE")]
1000pub enum HyperliquidProductType {
1001 Perp,
1003 Spot,
1005 Outcome,
1007}
1008
1009impl HyperliquidProductType {
1010 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
1021 if symbol.ends_with("-PERP") {
1022 Ok(Self::Perp)
1023 } else if symbol.ends_with("-SPOT") {
1024 Ok(Self::Spot)
1025 } else if symbol.ends_with(OUTCOME_SYMBOL_SUFFIX) || is_outcome_wire_symbol(symbol) {
1026 Ok(Self::Outcome)
1027 } else {
1028 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
1029 }
1030 }
1031}
1032
1033fn is_outcome_wire_symbol(symbol: &str) -> bool {
1036 let Some(rest) = symbol
1037 .strip_prefix('#')
1038 .or_else(|| symbol.strip_prefix('+'))
1039 else {
1040 return false;
1041 };
1042 !rest.is_empty() && rest.parse::<u32>().is_ok()
1043}
1044
1045#[derive(
1047 Copy,
1048 Clone,
1049 Debug,
1050 Default,
1051 Display,
1052 PartialEq,
1053 Eq,
1054 Hash,
1055 AsRefStr,
1056 EnumIter,
1057 EnumString,
1058 Serialize,
1059 Deserialize,
1060)]
1061#[serde(rename_all = "lowercase")]
1062#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1063#[cfg_attr(
1064 feature = "python",
1065 pyo3::pyclass(
1066 eq,
1067 eq_int,
1068 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
1069 from_py_object,
1070 rename_all = "SCREAMING_SNAKE_CASE",
1071 )
1072)]
1073#[cfg_attr(
1074 feature = "python",
1075 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1076)]
1077pub enum HyperliquidEnvironment {
1078 #[default]
1080 Mainnet,
1081 Testnet,
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use nautilus_model::enums::OrderType;
1088 use rstest::rstest;
1089 use serde_json;
1090
1091 use super::*;
1092
1093 #[rstest]
1094 fn test_side_serde() {
1095 let buy_side = HyperliquidSide::Buy;
1096 let sell_side = HyperliquidSide::Sell;
1097
1098 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
1099 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
1100
1101 assert_eq!(
1102 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
1103 HyperliquidSide::Buy
1104 );
1105 assert_eq!(
1106 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
1107 HyperliquidSide::Sell
1108 );
1109 }
1110
1111 #[rstest]
1112 fn test_side_from_order_side() {
1113 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
1115 assert_eq!(
1116 HyperliquidSide::from(OrderSide::Sell),
1117 HyperliquidSide::Sell
1118 );
1119 }
1120
1121 #[rstest]
1122 fn test_order_side_from_hyperliquid_side() {
1123 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
1125 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
1126 }
1127
1128 #[rstest]
1129 fn test_aggressor_side_from_hyperliquid_side() {
1130 assert_eq!(
1132 AggressorSide::from(HyperliquidSide::Buy),
1133 AggressorSide::Buyer
1134 );
1135 assert_eq!(
1136 AggressorSide::from(HyperliquidSide::Sell),
1137 AggressorSide::Seller
1138 );
1139 }
1140
1141 #[rstest]
1142 fn test_time_in_force_serde() {
1143 let test_cases = [
1144 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1145 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1146 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1147 ];
1148
1149 for (tif, expected_json) in test_cases {
1150 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1151 assert_eq!(
1152 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1153 tif
1154 );
1155 }
1156 }
1157
1158 #[rstest]
1159 fn test_info_request_type_outcome_meta_as_str() {
1160 assert_eq!(
1161 HyperliquidInfoRequestType::OutcomeMeta.as_str(),
1162 "outcomeMeta"
1163 );
1164 }
1165
1166 #[rstest]
1167 fn test_info_request_type_recent_trades_as_str() {
1168 assert_eq!(
1169 HyperliquidInfoRequestType::RecentTrades.as_str(),
1170 "recentTrades"
1171 );
1172 }
1173
1174 #[rstest]
1175 fn test_fill_direction_serde() {
1176 let cases = [
1177 (HyperliquidFillDirection::OpenLong, "\"Open Long\""),
1178 (HyperliquidFillDirection::CloseShort, "\"Close Short\""),
1179 (HyperliquidFillDirection::LongToShort, "\"Long > Short\""),
1180 (
1181 HyperliquidFillDirection::AutoDeleveraging,
1182 "\"Auto-Deleveraging\"",
1183 ),
1184 (
1185 HyperliquidFillDirection::NetChildVaults,
1186 "\"Net Child Vaults\"",
1187 ),
1188 (HyperliquidFillDirection::Buy, "\"Buy\""),
1189 (
1190 HyperliquidFillDirection::SpotDustConversion,
1191 "\"Spot Dust Conversion\"",
1192 ),
1193 (HyperliquidFillDirection::Settlement, "\"Settlement\""),
1194 (HyperliquidFillDirection::SplitOutcome, "\"Split Outcome\""),
1195 (HyperliquidFillDirection::MergeOutcome, "\"Merge Outcome\""),
1196 (
1197 HyperliquidFillDirection::MergeQuestion,
1198 "\"Merge Question\"",
1199 ),
1200 (
1201 HyperliquidFillDirection::NegateOutcome,
1202 "\"Negate Outcome\"",
1203 ),
1204 ];
1205
1206 for (variant, expected) in cases {
1207 assert_eq!(serde_json::to_string(&variant).unwrap(), expected);
1208 assert_eq!(
1209 serde_json::from_str::<HyperliquidFillDirection>(expected).unwrap(),
1210 variant
1211 );
1212 }
1213 }
1214
1215 #[rstest]
1216 fn test_fill_direction_unknown_is_lenient() {
1217 assert_eq!(
1218 serde_json::from_str::<HyperliquidFillDirection>("\"Some New Direction\"").unwrap(),
1219 HyperliquidFillDirection::Unknown,
1220 );
1221 }
1222
1223 #[rstest]
1224 fn test_position_type_unknown_is_lenient() {
1225 assert_eq!(
1226 serde_json::from_str::<HyperliquidPositionType>("\"hedge\"").unwrap(),
1227 HyperliquidPositionType::Unknown,
1228 );
1229 }
1230
1231 #[rstest]
1232 fn test_twap_status_unknown_is_lenient() {
1233 assert_eq!(
1234 serde_json::from_str::<HyperliquidTwapStatus>("\"paused\"").unwrap(),
1235 HyperliquidTwapStatus::Unknown,
1236 );
1237 }
1238
1239 #[rstest]
1240 fn test_liquidity_flag_from_crossed() {
1241 assert_eq!(
1242 HyperliquidLiquidityFlag::from(true),
1243 HyperliquidLiquidityFlag::Taker
1244 );
1245 assert_eq!(
1246 HyperliquidLiquidityFlag::from(false),
1247 HyperliquidLiquidityFlag::Maker
1248 );
1249 }
1250
1251 #[rstest]
1252 #[allow(deprecated)]
1253 fn test_reject_code_from_error_string() {
1254 let test_cases = [
1255 (
1256 "Price must be divisible by tick size.",
1257 HyperliquidRejectCode::Tick,
1258 ),
1259 (
1260 "Order must have minimum value of $10.",
1261 HyperliquidRejectCode::MinTradeNtl,
1262 ),
1263 (
1264 "Insufficient margin to place order.",
1265 HyperliquidRejectCode::PerpMargin,
1266 ),
1267 (
1268 "Post only order would have immediately matched, bbo was 1.23",
1269 HyperliquidRejectCode::BadAloPx,
1270 ),
1271 (
1272 "Some unknown error",
1273 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1274 ),
1275 ];
1276
1277 for (error_str, expected_code) in test_cases {
1278 assert_eq!(
1279 HyperliquidRejectCode::from_error_string(error_str),
1280 expected_code
1281 );
1282 }
1283 }
1284
1285 #[rstest]
1286 fn test_reject_code_from_api_error() {
1287 let test_cases = [
1288 (
1289 "Price must be divisible by tick size.",
1290 HyperliquidRejectCode::Tick,
1291 ),
1292 (
1293 "Order must have minimum value of $10.",
1294 HyperliquidRejectCode::MinTradeNtl,
1295 ),
1296 (
1297 "Insufficient margin to place order.",
1298 HyperliquidRejectCode::PerpMargin,
1299 ),
1300 (
1301 "Post only order would have immediately matched, bbo was 1.23",
1302 HyperliquidRejectCode::BadAloPx,
1303 ),
1304 (
1305 "Some unknown error",
1306 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1307 ),
1308 ];
1309
1310 for (error_str, expected_code) in test_cases {
1311 assert_eq!(
1312 HyperliquidRejectCode::from_api_error(error_str),
1313 expected_code
1314 );
1315 }
1316 }
1317
1318 #[rstest]
1319 fn test_reduce_only() {
1320 let reduce_only = HyperliquidReduceOnly::new(true);
1321
1322 assert!(reduce_only.is_reduce_only());
1323
1324 let json = serde_json::to_string(&reduce_only).unwrap();
1325 assert_eq!(json, "true");
1326
1327 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1328 assert_eq!(parsed, reduce_only);
1329 }
1330
1331 #[rstest]
1332 fn test_order_status_conversion() {
1333 assert_eq!(
1335 OrderStatus::from(HyperliquidOrderStatus::Open),
1336 OrderStatus::Accepted
1337 );
1338 assert_eq!(
1339 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1340 OrderStatus::Accepted
1341 );
1342 assert_eq!(
1343 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1344 OrderStatus::Triggered
1345 );
1346 assert_eq!(
1347 OrderStatus::from(HyperliquidOrderStatus::Filled),
1348 OrderStatus::Filled
1349 );
1350 assert_eq!(
1351 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1352 OrderStatus::Canceled
1353 );
1354 assert_eq!(
1355 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1356 OrderStatus::Rejected
1357 );
1358
1359 assert_eq!(
1361 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1362 OrderStatus::Canceled
1363 );
1364 assert_eq!(
1365 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1366 OrderStatus::Canceled
1367 );
1368 assert_eq!(
1369 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1370 OrderStatus::Canceled
1371 );
1372
1373 assert_eq!(
1375 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1376 OrderStatus::Rejected
1377 );
1378 assert_eq!(
1379 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1380 OrderStatus::Rejected
1381 );
1382 }
1383
1384 #[rstest]
1385 fn test_order_status_serde_deserialization() {
1386 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1388 assert_eq!(open, HyperliquidOrderStatus::Open);
1389
1390 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1391 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1392
1393 let margin_canceled: HyperliquidOrderStatus =
1394 serde_json::from_str(r#""marginCanceled""#).unwrap();
1395 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1396
1397 let self_trade_canceled: HyperliquidOrderStatus =
1398 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1399 assert_eq!(
1400 self_trade_canceled,
1401 HyperliquidOrderStatus::SelfTradeCanceled
1402 );
1403
1404 let reduce_only_canceled: HyperliquidOrderStatus =
1405 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1406 assert_eq!(
1407 reduce_only_canceled,
1408 HyperliquidOrderStatus::ReduceOnlyCanceled
1409 );
1410
1411 let tick_rejected: HyperliquidOrderStatus =
1412 serde_json::from_str(r#""tickRejected""#).unwrap();
1413 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1414 }
1415
1416 #[rstest]
1417 fn test_hyperliquid_tpsl_serialization() {
1418 let tp = HyperliquidTpSl::Tp;
1419 let sl = HyperliquidTpSl::Sl;
1420
1421 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1422 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1423 }
1424
1425 #[rstest]
1426 fn test_hyperliquid_tpsl_deserialization() {
1427 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1428 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1429
1430 assert_eq!(tp, HyperliquidTpSl::Tp);
1431 assert_eq!(sl, HyperliquidTpSl::Sl);
1432 }
1433
1434 #[rstest]
1435 fn test_conditional_order_type_conversions() {
1436 assert_eq!(
1438 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1439 OrderType::StopMarket
1440 );
1441 assert_eq!(
1442 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1443 OrderType::StopLimit
1444 );
1445 assert_eq!(
1446 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1447 OrderType::MarketIfTouched
1448 );
1449 assert_eq!(
1450 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1451 OrderType::LimitIfTouched
1452 );
1453 assert_eq!(
1454 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1455 OrderType::TrailingStopMarket
1456 );
1457 }
1458
1459 mod error_parsing_tests {
1461 use super::*;
1462
1463 #[rstest]
1464 fn test_parse_tick_size_error() {
1465 let error = "Price must be divisible by tick size 0.01";
1466 let code = HyperliquidRejectCode::from_api_error(error);
1467 assert_eq!(code, HyperliquidRejectCode::Tick);
1468 }
1469
1470 #[rstest]
1471 fn test_parse_tick_size_error_case_insensitive() {
1472 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1473 let code = HyperliquidRejectCode::from_api_error(error);
1474 assert_eq!(code, HyperliquidRejectCode::Tick);
1475 }
1476
1477 #[rstest]
1478 fn test_parse_min_notional_perp() {
1479 let error = "Order must have minimum value of $10";
1480 let code = HyperliquidRejectCode::from_api_error(error);
1481 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1482 }
1483
1484 #[rstest]
1485 fn test_parse_min_notional_spot() {
1486 let error = "Order must have minimum value of 10 USDC";
1487 let code = HyperliquidRejectCode::from_api_error(error);
1488 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1489 }
1490
1491 #[rstest]
1492 fn test_parse_insufficient_margin() {
1493 let error = "Insufficient margin to place order";
1494 let code = HyperliquidRejectCode::from_api_error(error);
1495 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1496 }
1497
1498 #[rstest]
1499 fn test_parse_insufficient_margin_case_variations() {
1500 let variations = vec![
1501 "insufficient margin to place order",
1502 "INSUFFICIENT MARGIN TO PLACE ORDER",
1503 " Insufficient margin to place order ", ];
1505
1506 for error in variations {
1507 let code = HyperliquidRejectCode::from_api_error(error);
1508 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1509 }
1510 }
1511
1512 #[rstest]
1513 fn test_parse_reduce_only_violation() {
1514 let error = "Reduce only order would increase position";
1515 let code = HyperliquidRejectCode::from_api_error(error);
1516 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1517 }
1518
1519 #[rstest]
1520 fn test_parse_reduce_only_with_hyphen() {
1521 let error = "Reduce-only order would increase position";
1522 let code = HyperliquidRejectCode::from_api_error(error);
1523 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1524 }
1525
1526 #[rstest]
1527 fn test_parse_post_only_match() {
1528 let error = "Post only order would have immediately matched";
1529 let code = HyperliquidRejectCode::from_api_error(error);
1530 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1531 }
1532
1533 #[rstest]
1534 fn test_parse_post_only_with_hyphen() {
1535 let error = "Post-only order would have immediately matched";
1536 let code = HyperliquidRejectCode::from_api_error(error);
1537 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1538 }
1539
1540 #[rstest]
1541 fn test_parse_ioc_no_match() {
1542 let error = "Order could not immediately match";
1543 let code = HyperliquidRejectCode::from_api_error(error);
1544 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1545 }
1546
1547 #[rstest]
1548 fn test_parse_invalid_trigger_price() {
1549 let error = "Invalid TP/SL price";
1550 let code = HyperliquidRejectCode::from_api_error(error);
1551 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1552 }
1553
1554 #[rstest]
1555 fn test_parse_no_liquidity() {
1556 let error = "No liquidity available for market order";
1557 let code = HyperliquidRejectCode::from_api_error(error);
1558 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1559 }
1560
1561 #[rstest]
1562 fn test_parse_position_increase_at_oi_cap() {
1563 let error = "PositionIncreaseAtOpenInterestCap";
1564 let code = HyperliquidRejectCode::from_api_error(error);
1565 assert_eq!(
1566 code,
1567 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1568 );
1569 }
1570
1571 #[rstest]
1572 fn test_parse_position_flip_at_oi_cap() {
1573 let error = "PositionFlipAtOpenInterestCap";
1574 let code = HyperliquidRejectCode::from_api_error(error);
1575 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1576 }
1577
1578 #[rstest]
1579 fn test_parse_too_aggressive_at_oi_cap() {
1580 let error = "TooAggressiveAtOpenInterestCap";
1581 let code = HyperliquidRejectCode::from_api_error(error);
1582 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1583 }
1584
1585 #[rstest]
1586 fn test_parse_open_interest_increase() {
1587 let error = "OpenInterestIncrease";
1588 let code = HyperliquidRejectCode::from_api_error(error);
1589 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1590 }
1591
1592 #[rstest]
1593 fn test_parse_insufficient_spot_balance() {
1594 let error = "Insufficient spot balance";
1595 let code = HyperliquidRejectCode::from_api_error(error);
1596 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1597 }
1598
1599 #[rstest]
1600 fn test_parse_oracle_error() {
1601 let error = "Oracle price unavailable";
1602 let code = HyperliquidRejectCode::from_api_error(error);
1603 assert_eq!(code, HyperliquidRejectCode::Oracle);
1604 }
1605
1606 #[rstest]
1607 fn test_parse_max_position() {
1608 let error = "Exceeds max position size";
1609 let code = HyperliquidRejectCode::from_api_error(error);
1610 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1611 }
1612
1613 #[rstest]
1614 fn test_parse_missing_order() {
1615 let error = "MissingOrder";
1616 let code = HyperliquidRejectCode::from_api_error(error);
1617 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1618 }
1619
1620 #[rstest]
1621 fn test_parse_unknown_error() {
1622 let error = "This is a completely new error message";
1623 let code = HyperliquidRejectCode::from_api_error(error);
1624 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1625
1626 if let HyperliquidRejectCode::Unknown(msg) = code {
1628 assert_eq!(msg, error);
1629 }
1630 }
1631
1632 #[rstest]
1633 fn test_parse_empty_error() {
1634 let error = "";
1635 let code = HyperliquidRejectCode::from_api_error(error);
1636 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1637 }
1638
1639 #[rstest]
1640 fn test_parse_whitespace_only() {
1641 let error = " ";
1642 let code = HyperliquidRejectCode::from_api_error(error);
1643 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1644 }
1645
1646 #[rstest]
1647 fn test_normalization_preserves_original_in_unknown() {
1648 let error = " UNKNOWN ERROR MESSAGE ";
1649 let code = HyperliquidRejectCode::from_api_error(error);
1650
1651 if let HyperliquidRejectCode::Unknown(msg) = code {
1653 assert_eq!(msg, error);
1654 } else {
1655 panic!("Expected Unknown variant");
1656 }
1657 }
1658 }
1659
1660 #[rstest]
1661 fn test_conditional_order_type_round_trip() {
1662 assert_eq!(
1663 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1664 OrderType::TrailingStopLimit
1665 );
1666
1667 assert_eq!(
1669 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1670 HyperliquidConditionalOrderType::StopMarket
1671 );
1672 assert_eq!(
1673 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1674 HyperliquidConditionalOrderType::StopLimit
1675 );
1676 }
1677
1678 #[rstest]
1679 fn test_trailing_offset_type_serialization() {
1680 let price = HyperliquidTrailingOffsetType::Price;
1681 let percentage = HyperliquidTrailingOffsetType::Percentage;
1682 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1683
1684 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1685 assert_eq!(
1686 serde_json::to_string(&percentage).unwrap(),
1687 r#""percentage""#
1688 );
1689 assert_eq!(
1690 serde_json::to_string(&basis_points).unwrap(),
1691 r#""basispoints""#
1692 );
1693 }
1694
1695 #[rstest]
1696 fn test_conditional_order_type_serialization() {
1697 assert_eq!(
1698 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1699 r#""STOP_MARKET""#
1700 );
1701 assert_eq!(
1702 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1703 r#""STOP_LIMIT""#
1704 );
1705 assert_eq!(
1706 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1707 r#""TAKE_PROFIT_MARKET""#
1708 );
1709 assert_eq!(
1710 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1711 r#""TAKE_PROFIT_LIMIT""#
1712 );
1713 assert_eq!(
1714 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1715 r#""TRAILING_STOP_MARKET""#
1716 );
1717 assert_eq!(
1718 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1719 r#""TRAILING_STOP_LIMIT""#
1720 );
1721 }
1722
1723 #[rstest]
1724 fn test_order_type_enum_coverage() {
1725 let conditional_types = vec![
1727 HyperliquidConditionalOrderType::StopMarket,
1728 HyperliquidConditionalOrderType::StopLimit,
1729 HyperliquidConditionalOrderType::TakeProfitMarket,
1730 HyperliquidConditionalOrderType::TakeProfitLimit,
1731 HyperliquidConditionalOrderType::TrailingStopMarket,
1732 HyperliquidConditionalOrderType::TrailingStopLimit,
1733 ];
1734
1735 for cond_type in conditional_types {
1736 let order_type = OrderType::from(cond_type);
1737 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1738 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1739 }
1740 }
1741
1742 #[rstest]
1743 #[case("BTC-USD-PERP", HyperliquidProductType::Perp)]
1744 #[case("HYPE-USDC-SPOT", HyperliquidProductType::Spot)]
1745 #[case("25-YES-OUTCOME", HyperliquidProductType::Outcome)]
1746 #[case("25-NO-OUTCOME", HyperliquidProductType::Outcome)]
1747 #[case("0-YES-OUTCOME", HyperliquidProductType::Outcome)]
1748 #[case("#10", HyperliquidProductType::Outcome)]
1749 #[case("+31", HyperliquidProductType::Outcome)]
1750 #[case("#0", HyperliquidProductType::Outcome)]
1751 fn test_product_type_from_symbol(
1752 #[case] symbol: &str,
1753 #[case] expected: HyperliquidProductType,
1754 ) {
1755 assert_eq!(
1756 HyperliquidProductType::from_symbol(symbol).unwrap(),
1757 expected
1758 );
1759 }
1760
1761 #[rstest]
1762 #[case("")]
1763 #[case("BTC")]
1764 #[case("#")]
1765 #[case("+")]
1766 #[case("#abc")]
1767 #[case("+12.5")]
1768 #[case("@1")]
1769 #[case("#-1")]
1770 #[case("+-1")]
1771 #[case("25-YES")]
1772 #[case("OUTCOME")]
1773 #[case("25-YES-outcome")]
1774 fn test_product_type_from_symbol_rejects_invalid(#[case] symbol: &str) {
1775 assert!(HyperliquidProductType::from_symbol(symbol).is_err());
1776 }
1777}