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}
429
430#[derive(
432 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
433)]
434#[serde(rename_all = "lowercase")]
435#[strum(serialize_all = "lowercase")]
436pub enum HyperliquidTwapStatus {
437 Activated,
438 Terminated,
439 Finished,
440 Error,
441}
442
443#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
444#[serde(untagged)]
445pub enum HyperliquidRejectCode {
446 Tick,
448 MinTradeNtl,
450 MinTradeSpotNtl,
452 PerpMargin,
454 ReduceOnly,
456 BadAloPx,
458 IocCancel,
460 BadTriggerPx,
462 MarketOrderNoLiquidity,
464 PositionIncreaseAtOpenInterestCap,
466 PositionFlipAtOpenInterestCap,
468 TooAggressiveAtOpenInterestCap,
470 OpenInterestIncrease,
472 InsufficientSpotBalance,
474 Oracle,
476 PerpMaxPosition,
478 MissingOrder,
480 Unknown(String),
482}
483
484impl HyperliquidRejectCode {
485 pub fn from_api_error(error_message: &str) -> Self {
487 Self::from_error_string_internal(error_message)
488 }
489
490 fn from_error_string_internal(error: &str) -> Self {
491 let normalized = error.trim().to_lowercase();
493
494 match normalized.as_str() {
495 s if s.contains("tick size") => Self::Tick,
497
498 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
500 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
501
502 s if s.contains("insufficient margin") => Self::PerpMargin,
504
505 s if s.contains("reduce only order would increase")
507 || s.contains("reduce-only order would increase") =>
508 {
509 Self::ReduceOnly
510 }
511
512 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
514 || s.contains("post-only order would have immediately matched") =>
515 {
516 Self::BadAloPx
517 }
518
519 s if s.contains("could not immediately match") => Self::IocCancel,
521
522 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
524
525 s if s.contains("no liquidity available for market order") => {
527 Self::MarketOrderNoLiquidity
528 }
529
530 s if s.contains("positionincreaseatopeninterestcap") => {
533 Self::PositionIncreaseAtOpenInterestCap
534 }
535 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
536 s if s.contains("tooaggressiveatopeninterestcap") => {
537 Self::TooAggressiveAtOpenInterestCap
538 }
539 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
540
541 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
543
544 s if s.contains("oracle") => Self::Oracle,
546
547 s if s.contains("max position") => Self::PerpMaxPosition,
549
550 s if s.contains("missingorder") => Self::MissingOrder,
552
553 _ => {
555 log::warn!(
556 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
558 Self::Unknown(error.to_string())
559 }
560 }
561 }
562
563 #[deprecated(
568 since = "0.50.0",
569 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
570 )]
571 pub fn from_error_string(error: &str) -> Self {
572 Self::from_error_string_internal(error)
573 }
574}
575
576#[derive(
580 Copy,
581 Clone,
582 Debug,
583 Display,
584 PartialEq,
585 Eq,
586 Hash,
587 AsRefStr,
588 EnumIter,
589 EnumString,
590 Serialize,
591 Deserialize,
592)]
593pub enum HyperliquidOrderStatus {
594 #[serde(rename = "open")]
596 Open,
597 #[serde(rename = "accepted")]
599 Accepted,
600 #[serde(rename = "triggered")]
602 Triggered,
603 #[serde(rename = "filled")]
605 Filled,
606 #[serde(rename = "canceled")]
608 Canceled,
609 #[serde(rename = "rejected")]
611 Rejected,
612 #[serde(rename = "marginCanceled")]
615 MarginCanceled,
616 #[serde(rename = "vaultWithdrawalCanceled")]
618 VaultWithdrawalCanceled,
619 #[serde(rename = "openInterestCapCanceled")]
621 OpenInterestCapCanceled,
622 #[serde(rename = "selfTradeCanceled")]
624 SelfTradeCanceled,
625 #[serde(rename = "reduceOnlyCanceled")]
627 ReduceOnlyCanceled,
628 #[serde(rename = "siblingFilledCanceled")]
630 SiblingFilledCanceled,
631 #[serde(rename = "delistedCanceled")]
633 DelistedCanceled,
634 #[serde(rename = "liquidatedCanceled")]
636 LiquidatedCanceled,
637 #[serde(rename = "scheduledCancel")]
639 ScheduledCancel,
640 #[serde(rename = "tickRejected")]
643 TickRejected,
644 #[serde(rename = "minTradeNtlRejected")]
646 MinTradeNtlRejected,
647 #[serde(rename = "minTradeSpotNtlRejected")]
649 MinTradeSpotNtlRejected,
650 #[serde(rename = "perpMarginRejected")]
652 PerpMarginRejected,
653 #[serde(rename = "reduceOnlyRejected")]
655 ReduceOnlyRejected,
656 #[serde(rename = "badAloPxRejected")]
658 BadAloPxRejected,
659 #[serde(rename = "iocCancelRejected")]
661 IocCancelRejected,
662 #[serde(rename = "badTriggerPxRejected")]
664 BadTriggerPxRejected,
665 #[serde(rename = "marketOrderNoLiquidityRejected")]
667 MarketOrderNoLiquidityRejected,
668 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
670 PositionIncreaseAtOpenInterestCapRejected,
671 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
673 PositionFlipAtOpenInterestCapRejected,
674 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
676 TooAggressiveAtOpenInterestCapRejected,
677 #[serde(rename = "openInterestIncreaseRejected")]
679 OpenInterestIncreaseRejected,
680 #[serde(rename = "insufficientSpotBalanceRejected")]
682 InsufficientSpotBalanceRejected,
683 #[serde(rename = "oracleRejected")]
685 OracleRejected,
686 #[serde(rename = "perpMaxPositionRejected")]
688 PerpMaxPositionRejected,
689}
690
691impl From<HyperliquidOrderStatus> for OrderStatus {
692 fn from(status: HyperliquidOrderStatus) -> Self {
693 match status {
694 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
695 HyperliquidOrderStatus::Triggered => Self::Triggered,
696 HyperliquidOrderStatus::Filled => Self::Filled,
697 HyperliquidOrderStatus::Canceled
699 | HyperliquidOrderStatus::MarginCanceled
700 | HyperliquidOrderStatus::VaultWithdrawalCanceled
701 | HyperliquidOrderStatus::OpenInterestCapCanceled
702 | HyperliquidOrderStatus::SelfTradeCanceled
703 | HyperliquidOrderStatus::ReduceOnlyCanceled
704 | HyperliquidOrderStatus::SiblingFilledCanceled
705 | HyperliquidOrderStatus::DelistedCanceled
706 | HyperliquidOrderStatus::LiquidatedCanceled
707 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
708 HyperliquidOrderStatus::Rejected
710 | HyperliquidOrderStatus::TickRejected
711 | HyperliquidOrderStatus::MinTradeNtlRejected
712 | HyperliquidOrderStatus::MinTradeSpotNtlRejected
713 | HyperliquidOrderStatus::PerpMarginRejected
714 | HyperliquidOrderStatus::ReduceOnlyRejected
715 | HyperliquidOrderStatus::BadAloPxRejected
716 | HyperliquidOrderStatus::IocCancelRejected
717 | HyperliquidOrderStatus::BadTriggerPxRejected
718 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
719 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
720 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
721 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
722 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
723 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
724 | HyperliquidOrderStatus::OracleRejected
725 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
726 }
727 }
728}
729
730#[derive(
741 Copy,
742 Clone,
743 Debug,
744 Display,
745 PartialEq,
746 Eq,
747 Hash,
748 AsRefStr,
749 EnumIter,
750 EnumString,
751 Serialize,
752 Deserialize,
753)]
754#[serde(rename_all = "PascalCase")]
755#[strum(serialize_all = "PascalCase")]
756pub enum HyperliquidFillDirection {
757 #[serde(rename = "Open Long")]
759 #[strum(serialize = "Open Long")]
760 OpenLong,
761 #[serde(rename = "Open Short")]
763 #[strum(serialize = "Open Short")]
764 OpenShort,
765 #[serde(rename = "Close Long")]
767 #[strum(serialize = "Close Long")]
768 CloseLong,
769 #[serde(rename = "Close Short")]
771 #[strum(serialize = "Close Short")]
772 CloseShort,
773 #[serde(rename = "Long > Short")]
775 #[strum(serialize = "Long > Short")]
776 LongToShort,
777 #[serde(rename = "Short > Long")]
779 #[strum(serialize = "Short > Long")]
780 ShortToLong,
781 #[serde(rename = "Auto-Deleveraging")]
783 #[strum(serialize = "Auto-Deleveraging")]
784 AutoDeleveraging,
785 Buy,
787 Sell,
789 #[serde(rename = "Settlement")]
792 #[strum(serialize = "Settlement")]
793 Settlement,
794 #[serde(rename = "Split Outcome")]
797 #[strum(serialize = "Split Outcome")]
798 SplitOutcome,
799 #[serde(rename = "Merge Outcome")]
802 #[strum(serialize = "Merge Outcome")]
803 MergeOutcome,
804 #[serde(rename = "Merge Question")]
807 #[strum(serialize = "Merge Question")]
808 MergeQuestion,
809 #[serde(rename = "Negate Outcome")]
812 #[strum(serialize = "Negate Outcome")]
813 NegateOutcome,
814}
815
816#[derive(
820 Copy,
821 Clone,
822 Debug,
823 Display,
824 PartialEq,
825 Eq,
826 Hash,
827 AsRefStr,
828 EnumIter,
829 EnumString,
830 Serialize,
831 Deserialize,
832)]
833#[serde(rename_all = "camelCase")]
834#[strum(serialize_all = "camelCase")]
835pub enum HyperliquidInfoRequestType {
836 Meta,
838 SpotMeta,
840 MetaAndAssetCtxs,
842 SpotMetaAndAssetCtxs,
844 OutcomeMeta,
846 L2Book,
848 AllMids,
850 UserFills,
852 UserFillsByTime,
854 OrderStatus,
856 OpenOrders,
858 FrontendOpenOrders,
860 ClearinghouseState,
862 SpotClearinghouseState,
864 ExchangeStatus,
866 CandleSnapshot,
868 Candle,
870 HistoricalOrders,
872 FundingHistory,
874 UserFunding,
876 NonUserFundingUpdates,
878 TwapHistory,
880 UserTwapSliceFills,
882 UserTwapSliceFillsByTime,
884 UserRateLimit,
886 UserRole,
888 DelegatorHistory,
890 DelegatorRewards,
892 ValidatorStats,
894 UserFees,
896 PerpDexs,
898 AllPerpMetas,
900}
901
902impl HyperliquidInfoRequestType {
903 pub fn as_str(&self) -> &'static str {
904 match self {
905 Self::Meta => "meta",
906 Self::SpotMeta => "spotMeta",
907 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
908 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
909 Self::OutcomeMeta => "outcomeMeta",
910 Self::L2Book => "l2Book",
911 Self::AllMids => "allMids",
912 Self::UserFills => "userFills",
913 Self::UserFillsByTime => "userFillsByTime",
914 Self::OrderStatus => "orderStatus",
915 Self::OpenOrders => "openOrders",
916 Self::FrontendOpenOrders => "frontendOpenOrders",
917 Self::ClearinghouseState => "clearinghouseState",
918 Self::SpotClearinghouseState => "spotClearinghouseState",
919 Self::ExchangeStatus => "exchangeStatus",
920 Self::CandleSnapshot => "candleSnapshot",
921 Self::Candle => "candle",
922 Self::HistoricalOrders => "historicalOrders",
923 Self::FundingHistory => "fundingHistory",
924 Self::UserFunding => "userFunding",
925 Self::NonUserFundingUpdates => "nonUserFundingUpdates",
926 Self::TwapHistory => "twapHistory",
927 Self::UserTwapSliceFills => "userTwapSliceFills",
928 Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
929 Self::UserRateLimit => "userRateLimit",
930 Self::UserRole => "userRole",
931 Self::DelegatorHistory => "delegatorHistory",
932 Self::DelegatorRewards => "delegatorRewards",
933 Self::ValidatorStats => "validatorStats",
934 Self::UserFees => "userFees",
935 Self::PerpDexs => "perpDexs",
936 Self::AllPerpMetas => "allPerpMetas",
937 }
938 }
939}
940
941#[derive(
942 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
943)]
944#[serde(rename_all = "lowercase")]
945#[strum(serialize_all = "lowercase")]
946pub enum HyperliquidLeverageType {
947 Cross,
948 Isolated,
949 #[serde(other)]
950 Unknown,
951}
952
953#[derive(
955 Copy,
956 Clone,
957 Debug,
958 Display,
959 PartialEq,
960 Eq,
961 Hash,
962 AsRefStr,
963 EnumIter,
964 EnumString,
965 Serialize,
966 Deserialize,
967)]
968#[cfg_attr(
969 feature = "python",
970 pyo3::pyclass(
971 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
972 from_py_object,
973 rename_all = "SCREAMING_SNAKE_CASE",
974 )
975)]
976#[cfg_attr(
977 feature = "python",
978 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
979)]
980#[serde(rename_all = "UPPERCASE")]
981#[strum(serialize_all = "UPPERCASE")]
982pub enum HyperliquidProductType {
983 Perp,
985 Spot,
987 Outcome,
989}
990
991impl HyperliquidProductType {
992 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
1003 if symbol.ends_with("-PERP") {
1004 Ok(Self::Perp)
1005 } else if symbol.ends_with("-SPOT") {
1006 Ok(Self::Spot)
1007 } else if symbol.ends_with(OUTCOME_SYMBOL_SUFFIX) || is_outcome_wire_symbol(symbol) {
1008 Ok(Self::Outcome)
1009 } else {
1010 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
1011 }
1012 }
1013}
1014
1015fn is_outcome_wire_symbol(symbol: &str) -> bool {
1018 let Some(rest) = symbol
1019 .strip_prefix('#')
1020 .or_else(|| symbol.strip_prefix('+'))
1021 else {
1022 return false;
1023 };
1024 !rest.is_empty() && rest.parse::<u32>().is_ok()
1025}
1026
1027#[derive(
1029 Copy,
1030 Clone,
1031 Debug,
1032 Default,
1033 Display,
1034 PartialEq,
1035 Eq,
1036 Hash,
1037 AsRefStr,
1038 EnumIter,
1039 EnumString,
1040 Serialize,
1041 Deserialize,
1042)]
1043#[serde(rename_all = "lowercase")]
1044#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
1045#[cfg_attr(
1046 feature = "python",
1047 pyo3::pyclass(
1048 eq,
1049 eq_int,
1050 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
1051 from_py_object,
1052 rename_all = "SCREAMING_SNAKE_CASE",
1053 )
1054)]
1055#[cfg_attr(
1056 feature = "python",
1057 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.hyperliquid")
1058)]
1059pub enum HyperliquidEnvironment {
1060 #[default]
1062 Mainnet,
1063 Testnet,
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use nautilus_model::enums::OrderType;
1070 use rstest::rstest;
1071 use serde_json;
1072
1073 use super::*;
1074
1075 #[rstest]
1076 fn test_side_serde() {
1077 let buy_side = HyperliquidSide::Buy;
1078 let sell_side = HyperliquidSide::Sell;
1079
1080 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
1081 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
1082
1083 assert_eq!(
1084 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
1085 HyperliquidSide::Buy
1086 );
1087 assert_eq!(
1088 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
1089 HyperliquidSide::Sell
1090 );
1091 }
1092
1093 #[rstest]
1094 fn test_side_from_order_side() {
1095 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
1097 assert_eq!(
1098 HyperliquidSide::from(OrderSide::Sell),
1099 HyperliquidSide::Sell
1100 );
1101 }
1102
1103 #[rstest]
1104 fn test_order_side_from_hyperliquid_side() {
1105 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
1107 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
1108 }
1109
1110 #[rstest]
1111 fn test_aggressor_side_from_hyperliquid_side() {
1112 assert_eq!(
1114 AggressorSide::from(HyperliquidSide::Buy),
1115 AggressorSide::Buyer
1116 );
1117 assert_eq!(
1118 AggressorSide::from(HyperliquidSide::Sell),
1119 AggressorSide::Seller
1120 );
1121 }
1122
1123 #[rstest]
1124 fn test_time_in_force_serde() {
1125 let test_cases = [
1126 (HyperliquidTimeInForce::Alo, "\"Alo\""),
1127 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
1128 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
1129 ];
1130
1131 for (tif, expected_json) in test_cases {
1132 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
1133 assert_eq!(
1134 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
1135 tif
1136 );
1137 }
1138 }
1139
1140 #[rstest]
1141 fn test_info_request_type_outcome_meta_as_str() {
1142 assert_eq!(
1143 HyperliquidInfoRequestType::OutcomeMeta.as_str(),
1144 "outcomeMeta"
1145 );
1146 }
1147
1148 #[rstest]
1149 fn test_fill_direction_serde() {
1150 let cases = [
1151 (HyperliquidFillDirection::OpenLong, "\"Open Long\""),
1152 (HyperliquidFillDirection::CloseShort, "\"Close Short\""),
1153 (HyperliquidFillDirection::LongToShort, "\"Long > Short\""),
1154 (
1155 HyperliquidFillDirection::AutoDeleveraging,
1156 "\"Auto-Deleveraging\"",
1157 ),
1158 (HyperliquidFillDirection::Buy, "\"Buy\""),
1159 (HyperliquidFillDirection::Settlement, "\"Settlement\""),
1160 (HyperliquidFillDirection::SplitOutcome, "\"Split Outcome\""),
1161 (HyperliquidFillDirection::MergeOutcome, "\"Merge Outcome\""),
1162 (
1163 HyperliquidFillDirection::MergeQuestion,
1164 "\"Merge Question\"",
1165 ),
1166 (
1167 HyperliquidFillDirection::NegateOutcome,
1168 "\"Negate Outcome\"",
1169 ),
1170 ];
1171
1172 for (variant, expected) in cases {
1173 assert_eq!(serde_json::to_string(&variant).unwrap(), expected);
1174 assert_eq!(
1175 serde_json::from_str::<HyperliquidFillDirection>(expected).unwrap(),
1176 variant
1177 );
1178 }
1179 }
1180
1181 #[rstest]
1182 fn test_liquidity_flag_from_crossed() {
1183 assert_eq!(
1184 HyperliquidLiquidityFlag::from(true),
1185 HyperliquidLiquidityFlag::Taker
1186 );
1187 assert_eq!(
1188 HyperliquidLiquidityFlag::from(false),
1189 HyperliquidLiquidityFlag::Maker
1190 );
1191 }
1192
1193 #[rstest]
1194 #[allow(deprecated)]
1195 fn test_reject_code_from_error_string() {
1196 let test_cases = [
1197 (
1198 "Price must be divisible by tick size.",
1199 HyperliquidRejectCode::Tick,
1200 ),
1201 (
1202 "Order must have minimum value of $10.",
1203 HyperliquidRejectCode::MinTradeNtl,
1204 ),
1205 (
1206 "Insufficient margin to place order.",
1207 HyperliquidRejectCode::PerpMargin,
1208 ),
1209 (
1210 "Post only order would have immediately matched, bbo was 1.23",
1211 HyperliquidRejectCode::BadAloPx,
1212 ),
1213 (
1214 "Some unknown error",
1215 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1216 ),
1217 ];
1218
1219 for (error_str, expected_code) in test_cases {
1220 assert_eq!(
1221 HyperliquidRejectCode::from_error_string(error_str),
1222 expected_code
1223 );
1224 }
1225 }
1226
1227 #[rstest]
1228 fn test_reject_code_from_api_error() {
1229 let test_cases = [
1230 (
1231 "Price must be divisible by tick size.",
1232 HyperliquidRejectCode::Tick,
1233 ),
1234 (
1235 "Order must have minimum value of $10.",
1236 HyperliquidRejectCode::MinTradeNtl,
1237 ),
1238 (
1239 "Insufficient margin to place order.",
1240 HyperliquidRejectCode::PerpMargin,
1241 ),
1242 (
1243 "Post only order would have immediately matched, bbo was 1.23",
1244 HyperliquidRejectCode::BadAloPx,
1245 ),
1246 (
1247 "Some unknown error",
1248 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1249 ),
1250 ];
1251
1252 for (error_str, expected_code) in test_cases {
1253 assert_eq!(
1254 HyperliquidRejectCode::from_api_error(error_str),
1255 expected_code
1256 );
1257 }
1258 }
1259
1260 #[rstest]
1261 fn test_reduce_only() {
1262 let reduce_only = HyperliquidReduceOnly::new(true);
1263
1264 assert!(reduce_only.is_reduce_only());
1265
1266 let json = serde_json::to_string(&reduce_only).unwrap();
1267 assert_eq!(json, "true");
1268
1269 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1270 assert_eq!(parsed, reduce_only);
1271 }
1272
1273 #[rstest]
1274 fn test_order_status_conversion() {
1275 assert_eq!(
1277 OrderStatus::from(HyperliquidOrderStatus::Open),
1278 OrderStatus::Accepted
1279 );
1280 assert_eq!(
1281 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1282 OrderStatus::Accepted
1283 );
1284 assert_eq!(
1285 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1286 OrderStatus::Triggered
1287 );
1288 assert_eq!(
1289 OrderStatus::from(HyperliquidOrderStatus::Filled),
1290 OrderStatus::Filled
1291 );
1292 assert_eq!(
1293 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1294 OrderStatus::Canceled
1295 );
1296 assert_eq!(
1297 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1298 OrderStatus::Rejected
1299 );
1300
1301 assert_eq!(
1303 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1304 OrderStatus::Canceled
1305 );
1306 assert_eq!(
1307 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1308 OrderStatus::Canceled
1309 );
1310 assert_eq!(
1311 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1312 OrderStatus::Canceled
1313 );
1314
1315 assert_eq!(
1317 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1318 OrderStatus::Rejected
1319 );
1320 assert_eq!(
1321 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1322 OrderStatus::Rejected
1323 );
1324 }
1325
1326 #[rstest]
1327 fn test_order_status_serde_deserialization() {
1328 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1330 assert_eq!(open, HyperliquidOrderStatus::Open);
1331
1332 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1333 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1334
1335 let margin_canceled: HyperliquidOrderStatus =
1336 serde_json::from_str(r#""marginCanceled""#).unwrap();
1337 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1338
1339 let self_trade_canceled: HyperliquidOrderStatus =
1340 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1341 assert_eq!(
1342 self_trade_canceled,
1343 HyperliquidOrderStatus::SelfTradeCanceled
1344 );
1345
1346 let reduce_only_canceled: HyperliquidOrderStatus =
1347 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1348 assert_eq!(
1349 reduce_only_canceled,
1350 HyperliquidOrderStatus::ReduceOnlyCanceled
1351 );
1352
1353 let tick_rejected: HyperliquidOrderStatus =
1354 serde_json::from_str(r#""tickRejected""#).unwrap();
1355 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1356 }
1357
1358 #[rstest]
1359 fn test_hyperliquid_tpsl_serialization() {
1360 let tp = HyperliquidTpSl::Tp;
1361 let sl = HyperliquidTpSl::Sl;
1362
1363 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1364 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1365 }
1366
1367 #[rstest]
1368 fn test_hyperliquid_tpsl_deserialization() {
1369 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1370 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1371
1372 assert_eq!(tp, HyperliquidTpSl::Tp);
1373 assert_eq!(sl, HyperliquidTpSl::Sl);
1374 }
1375
1376 #[rstest]
1377 fn test_conditional_order_type_conversions() {
1378 assert_eq!(
1380 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1381 OrderType::StopMarket
1382 );
1383 assert_eq!(
1384 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1385 OrderType::StopLimit
1386 );
1387 assert_eq!(
1388 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1389 OrderType::MarketIfTouched
1390 );
1391 assert_eq!(
1392 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1393 OrderType::LimitIfTouched
1394 );
1395 assert_eq!(
1396 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1397 OrderType::TrailingStopMarket
1398 );
1399 }
1400
1401 mod error_parsing_tests {
1403 use super::*;
1404
1405 #[rstest]
1406 fn test_parse_tick_size_error() {
1407 let error = "Price must be divisible by tick size 0.01";
1408 let code = HyperliquidRejectCode::from_api_error(error);
1409 assert_eq!(code, HyperliquidRejectCode::Tick);
1410 }
1411
1412 #[rstest]
1413 fn test_parse_tick_size_error_case_insensitive() {
1414 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1415 let code = HyperliquidRejectCode::from_api_error(error);
1416 assert_eq!(code, HyperliquidRejectCode::Tick);
1417 }
1418
1419 #[rstest]
1420 fn test_parse_min_notional_perp() {
1421 let error = "Order must have minimum value of $10";
1422 let code = HyperliquidRejectCode::from_api_error(error);
1423 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1424 }
1425
1426 #[rstest]
1427 fn test_parse_min_notional_spot() {
1428 let error = "Order must have minimum value of 10 USDC";
1429 let code = HyperliquidRejectCode::from_api_error(error);
1430 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1431 }
1432
1433 #[rstest]
1434 fn test_parse_insufficient_margin() {
1435 let error = "Insufficient margin to place order";
1436 let code = HyperliquidRejectCode::from_api_error(error);
1437 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1438 }
1439
1440 #[rstest]
1441 fn test_parse_insufficient_margin_case_variations() {
1442 let variations = vec![
1443 "insufficient margin to place order",
1444 "INSUFFICIENT MARGIN TO PLACE ORDER",
1445 " Insufficient margin to place order ", ];
1447
1448 for error in variations {
1449 let code = HyperliquidRejectCode::from_api_error(error);
1450 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1451 }
1452 }
1453
1454 #[rstest]
1455 fn test_parse_reduce_only_violation() {
1456 let error = "Reduce only order would increase position";
1457 let code = HyperliquidRejectCode::from_api_error(error);
1458 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1459 }
1460
1461 #[rstest]
1462 fn test_parse_reduce_only_with_hyphen() {
1463 let error = "Reduce-only order would increase position";
1464 let code = HyperliquidRejectCode::from_api_error(error);
1465 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1466 }
1467
1468 #[rstest]
1469 fn test_parse_post_only_match() {
1470 let error = "Post only order would have immediately matched";
1471 let code = HyperliquidRejectCode::from_api_error(error);
1472 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1473 }
1474
1475 #[rstest]
1476 fn test_parse_post_only_with_hyphen() {
1477 let error = "Post-only order would have immediately matched";
1478 let code = HyperliquidRejectCode::from_api_error(error);
1479 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1480 }
1481
1482 #[rstest]
1483 fn test_parse_ioc_no_match() {
1484 let error = "Order could not immediately match";
1485 let code = HyperliquidRejectCode::from_api_error(error);
1486 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1487 }
1488
1489 #[rstest]
1490 fn test_parse_invalid_trigger_price() {
1491 let error = "Invalid TP/SL price";
1492 let code = HyperliquidRejectCode::from_api_error(error);
1493 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1494 }
1495
1496 #[rstest]
1497 fn test_parse_no_liquidity() {
1498 let error = "No liquidity available for market order";
1499 let code = HyperliquidRejectCode::from_api_error(error);
1500 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1501 }
1502
1503 #[rstest]
1504 fn test_parse_position_increase_at_oi_cap() {
1505 let error = "PositionIncreaseAtOpenInterestCap";
1506 let code = HyperliquidRejectCode::from_api_error(error);
1507 assert_eq!(
1508 code,
1509 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1510 );
1511 }
1512
1513 #[rstest]
1514 fn test_parse_position_flip_at_oi_cap() {
1515 let error = "PositionFlipAtOpenInterestCap";
1516 let code = HyperliquidRejectCode::from_api_error(error);
1517 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1518 }
1519
1520 #[rstest]
1521 fn test_parse_too_aggressive_at_oi_cap() {
1522 let error = "TooAggressiveAtOpenInterestCap";
1523 let code = HyperliquidRejectCode::from_api_error(error);
1524 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1525 }
1526
1527 #[rstest]
1528 fn test_parse_open_interest_increase() {
1529 let error = "OpenInterestIncrease";
1530 let code = HyperliquidRejectCode::from_api_error(error);
1531 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1532 }
1533
1534 #[rstest]
1535 fn test_parse_insufficient_spot_balance() {
1536 let error = "Insufficient spot balance";
1537 let code = HyperliquidRejectCode::from_api_error(error);
1538 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1539 }
1540
1541 #[rstest]
1542 fn test_parse_oracle_error() {
1543 let error = "Oracle price unavailable";
1544 let code = HyperliquidRejectCode::from_api_error(error);
1545 assert_eq!(code, HyperliquidRejectCode::Oracle);
1546 }
1547
1548 #[rstest]
1549 fn test_parse_max_position() {
1550 let error = "Exceeds max position size";
1551 let code = HyperliquidRejectCode::from_api_error(error);
1552 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1553 }
1554
1555 #[rstest]
1556 fn test_parse_missing_order() {
1557 let error = "MissingOrder";
1558 let code = HyperliquidRejectCode::from_api_error(error);
1559 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1560 }
1561
1562 #[rstest]
1563 fn test_parse_unknown_error() {
1564 let error = "This is a completely new error message";
1565 let code = HyperliquidRejectCode::from_api_error(error);
1566 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1567
1568 if let HyperliquidRejectCode::Unknown(msg) = code {
1570 assert_eq!(msg, error);
1571 }
1572 }
1573
1574 #[rstest]
1575 fn test_parse_empty_error() {
1576 let error = "";
1577 let code = HyperliquidRejectCode::from_api_error(error);
1578 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1579 }
1580
1581 #[rstest]
1582 fn test_parse_whitespace_only() {
1583 let error = " ";
1584 let code = HyperliquidRejectCode::from_api_error(error);
1585 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1586 }
1587
1588 #[rstest]
1589 fn test_normalization_preserves_original_in_unknown() {
1590 let error = " UNKNOWN ERROR MESSAGE ";
1591 let code = HyperliquidRejectCode::from_api_error(error);
1592
1593 if let HyperliquidRejectCode::Unknown(msg) = code {
1595 assert_eq!(msg, error);
1596 } else {
1597 panic!("Expected Unknown variant");
1598 }
1599 }
1600 }
1601
1602 #[rstest]
1603 fn test_conditional_order_type_round_trip() {
1604 assert_eq!(
1605 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1606 OrderType::TrailingStopLimit
1607 );
1608
1609 assert_eq!(
1611 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1612 HyperliquidConditionalOrderType::StopMarket
1613 );
1614 assert_eq!(
1615 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1616 HyperliquidConditionalOrderType::StopLimit
1617 );
1618 }
1619
1620 #[rstest]
1621 fn test_trailing_offset_type_serialization() {
1622 let price = HyperliquidTrailingOffsetType::Price;
1623 let percentage = HyperliquidTrailingOffsetType::Percentage;
1624 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1625
1626 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1627 assert_eq!(
1628 serde_json::to_string(&percentage).unwrap(),
1629 r#""percentage""#
1630 );
1631 assert_eq!(
1632 serde_json::to_string(&basis_points).unwrap(),
1633 r#""basispoints""#
1634 );
1635 }
1636
1637 #[rstest]
1638 fn test_conditional_order_type_serialization() {
1639 assert_eq!(
1640 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1641 r#""STOP_MARKET""#
1642 );
1643 assert_eq!(
1644 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1645 r#""STOP_LIMIT""#
1646 );
1647 assert_eq!(
1648 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1649 r#""TAKE_PROFIT_MARKET""#
1650 );
1651 assert_eq!(
1652 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1653 r#""TAKE_PROFIT_LIMIT""#
1654 );
1655 assert_eq!(
1656 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1657 r#""TRAILING_STOP_MARKET""#
1658 );
1659 assert_eq!(
1660 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1661 r#""TRAILING_STOP_LIMIT""#
1662 );
1663 }
1664
1665 #[rstest]
1666 fn test_order_type_enum_coverage() {
1667 let conditional_types = vec![
1669 HyperliquidConditionalOrderType::StopMarket,
1670 HyperliquidConditionalOrderType::StopLimit,
1671 HyperliquidConditionalOrderType::TakeProfitMarket,
1672 HyperliquidConditionalOrderType::TakeProfitLimit,
1673 HyperliquidConditionalOrderType::TrailingStopMarket,
1674 HyperliquidConditionalOrderType::TrailingStopLimit,
1675 ];
1676
1677 for cond_type in conditional_types {
1678 let order_type = OrderType::from(cond_type);
1679 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1680 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1681 }
1682 }
1683
1684 #[rstest]
1685 #[case("BTC-USD-PERP", HyperliquidProductType::Perp)]
1686 #[case("HYPE-USDC-SPOT", HyperliquidProductType::Spot)]
1687 #[case("25-YES-OUTCOME", HyperliquidProductType::Outcome)]
1688 #[case("25-NO-OUTCOME", HyperliquidProductType::Outcome)]
1689 #[case("0-YES-OUTCOME", HyperliquidProductType::Outcome)]
1690 #[case("#10", HyperliquidProductType::Outcome)]
1691 #[case("+31", HyperliquidProductType::Outcome)]
1692 #[case("#0", HyperliquidProductType::Outcome)]
1693 fn test_product_type_from_symbol(
1694 #[case] symbol: &str,
1695 #[case] expected: HyperliquidProductType,
1696 ) {
1697 assert_eq!(
1698 HyperliquidProductType::from_symbol(symbol).unwrap(),
1699 expected
1700 );
1701 }
1702
1703 #[rstest]
1704 #[case("")]
1705 #[case("BTC")]
1706 #[case("#")]
1707 #[case("+")]
1708 #[case("#abc")]
1709 #[case("+12.5")]
1710 #[case("@1")]
1711 #[case("#-1")]
1712 #[case("+-1")]
1713 #[case("25-YES")]
1714 #[case("OUTCOME")]
1715 #[case("25-YES-outcome")]
1716 fn test_product_type_from_symbol_rejects_invalid(#[case] symbol: &str) {
1717 assert!(HyperliquidProductType::from_symbol(symbol).is_err());
1718 }
1719}