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