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#[serde(rename_all = "lowercase")]
228#[strum(serialize_all = "lowercase")]
229pub enum HyperliquidTpSl {
230 Tp,
232 Sl,
234}
235
236#[derive(
241 Copy,
242 Clone,
243 Debug,
244 Display,
245 PartialEq,
246 Eq,
247 Hash,
248 AsRefStr,
249 EnumIter,
250 EnumString,
251 Serialize,
252 Deserialize,
253)]
254#[cfg_attr(
255 feature = "python",
256 pyo3::pyclass(
257 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
258 from_py_object,
259 rename_all = "SCREAMING_SNAKE_CASE",
260 )
261)]
262#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
263#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
264pub enum HyperliquidConditionalOrderType {
265 StopMarket,
267 StopLimit,
269 TakeProfitMarket,
271 TakeProfitLimit,
273 TrailingStopMarket,
275 TrailingStopLimit,
277}
278
279impl From<HyperliquidConditionalOrderType> for OrderType {
280 fn from(value: HyperliquidConditionalOrderType) -> Self {
281 match value {
282 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
283 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
284 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
285 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
286 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
287 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
288 }
289 }
290}
291
292impl From<OrderType> for HyperliquidConditionalOrderType {
293 fn from(value: OrderType) -> Self {
294 match value {
295 OrderType::StopMarket => Self::StopMarket,
296 OrderType::StopLimit => Self::StopLimit,
297 OrderType::MarketIfTouched => Self::TakeProfitMarket,
298 OrderType::LimitIfTouched => Self::TakeProfitLimit,
299 OrderType::TrailingStopMarket => Self::TrailingStopMarket,
300 OrderType::TrailingStopLimit => Self::TrailingStopLimit,
301 _ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
302 }
303 }
304}
305
306#[derive(
313 Copy,
314 Clone,
315 Debug,
316 Display,
317 PartialEq,
318 Eq,
319 Hash,
320 AsRefStr,
321 EnumIter,
322 EnumString,
323 Serialize,
324 Deserialize,
325)]
326#[cfg_attr(
327 feature = "python",
328 pyo3::pyclass(
329 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
330 from_py_object,
331 rename_all = "SCREAMING_SNAKE_CASE",
332 )
333)]
334#[serde(rename_all = "lowercase")]
335#[strum(serialize_all = "lowercase")]
336pub enum HyperliquidTrailingOffsetType {
337 Price,
339 Percentage,
341 #[serde(rename = "basispoints")]
343 #[strum(serialize = "basispoints")]
344 BasisPoints,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(transparent)]
350pub struct HyperliquidReduceOnly(pub bool);
351
352impl HyperliquidReduceOnly {
353 pub fn new(reduce_only: bool) -> Self {
355 Self(reduce_only)
356 }
357
358 pub fn is_reduce_only(&self) -> bool {
360 self.0
361 }
362}
363
364#[derive(
366 Copy,
367 Clone,
368 Debug,
369 Display,
370 PartialEq,
371 Eq,
372 Hash,
373 AsRefStr,
374 EnumIter,
375 EnumString,
376 Serialize,
377 Deserialize,
378)]
379#[serde(rename_all = "lowercase")]
380#[strum(serialize_all = "lowercase")]
381pub enum HyperliquidLiquidityFlag {
382 Maker,
383 Taker,
384}
385
386impl From<bool> for HyperliquidLiquidityFlag {
387 fn from(crossed: bool) -> Self {
391 if crossed { Self::Taker } else { Self::Maker }
392 }
393}
394
395#[derive(
397 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
398)]
399#[serde(rename_all = "lowercase")]
400#[strum(serialize_all = "lowercase")]
401pub enum HyperliquidLiquidationMethod {
402 Market,
403 Backstop,
404}
405
406#[derive(
408 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
409)]
410#[serde(rename_all = "camelCase")]
411#[strum(serialize_all = "camelCase")]
412pub enum HyperliquidPositionType {
413 OneWay,
414}
415
416#[derive(
418 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
419)]
420#[serde(rename_all = "lowercase")]
421#[strum(serialize_all = "lowercase")]
422pub enum HyperliquidTwapStatus {
423 Activated,
424 Terminated,
425 Finished,
426 Error,
427}
428
429#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
430#[serde(untagged)]
431pub enum HyperliquidRejectCode {
432 Tick,
434 MinTradeNtl,
436 MinTradeSpotNtl,
438 PerpMargin,
440 ReduceOnly,
442 BadAloPx,
444 IocCancel,
446 BadTriggerPx,
448 MarketOrderNoLiquidity,
450 PositionIncreaseAtOpenInterestCap,
452 PositionFlipAtOpenInterestCap,
454 TooAggressiveAtOpenInterestCap,
456 OpenInterestIncrease,
458 InsufficientSpotBalance,
460 Oracle,
462 PerpMaxPosition,
464 MissingOrder,
466 Unknown(String),
468}
469
470impl HyperliquidRejectCode {
471 pub fn from_api_error(error_message: &str) -> Self {
473 Self::from_error_string_internal(error_message)
474 }
475
476 fn from_error_string_internal(error: &str) -> Self {
477 let normalized = error.trim().to_lowercase();
479
480 match normalized.as_str() {
481 s if s.contains("tick size") => Self::Tick,
483
484 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
486 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
487
488 s if s.contains("insufficient margin") => Self::PerpMargin,
490
491 s if s.contains("reduce only order would increase")
493 || s.contains("reduce-only order would increase") =>
494 {
495 Self::ReduceOnly
496 }
497
498 s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
500 || s.contains("post-only order would have immediately matched") =>
501 {
502 Self::BadAloPx
503 }
504
505 s if s.contains("could not immediately match") => Self::IocCancel,
507
508 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
510
511 s if s.contains("no liquidity available for market order") => {
513 Self::MarketOrderNoLiquidity
514 }
515
516 s if s.contains("positionincreaseatopeninterestcap") => {
519 Self::PositionIncreaseAtOpenInterestCap
520 }
521 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
522 s if s.contains("tooaggressiveatopeninterestcap") => {
523 Self::TooAggressiveAtOpenInterestCap
524 }
525 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
526
527 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
529
530 s if s.contains("oracle") => Self::Oracle,
532
533 s if s.contains("max position") => Self::PerpMaxPosition,
535
536 s if s.contains("missingorder") => Self::MissingOrder,
538
539 _ => {
541 log::warn!(
542 "Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
544 Self::Unknown(error.to_string())
545 }
546 }
547 }
548
549 #[deprecated(
554 since = "0.50.0",
555 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
556 )]
557 pub fn from_error_string(error: &str) -> Self {
558 Self::from_error_string_internal(error)
559 }
560}
561
562#[derive(
566 Copy,
567 Clone,
568 Debug,
569 Display,
570 PartialEq,
571 Eq,
572 Hash,
573 AsRefStr,
574 EnumIter,
575 EnumString,
576 Serialize,
577 Deserialize,
578)]
579pub enum HyperliquidOrderStatus {
580 #[serde(rename = "open")]
582 Open,
583 #[serde(rename = "accepted")]
585 Accepted,
586 #[serde(rename = "triggered")]
588 Triggered,
589 #[serde(rename = "filled")]
591 Filled,
592 #[serde(rename = "canceled")]
594 Canceled,
595 #[serde(rename = "rejected")]
597 Rejected,
598 #[serde(rename = "marginCanceled")]
601 MarginCanceled,
602 #[serde(rename = "vaultWithdrawalCanceled")]
604 VaultWithdrawalCanceled,
605 #[serde(rename = "openInterestCapCanceled")]
607 OpenInterestCapCanceled,
608 #[serde(rename = "selfTradeCanceled")]
610 SelfTradeCanceled,
611 #[serde(rename = "reduceOnlyCanceled")]
613 ReduceOnlyCanceled,
614 #[serde(rename = "siblingFilledCanceled")]
616 SiblingFilledCanceled,
617 #[serde(rename = "delistedCanceled")]
619 DelistedCanceled,
620 #[serde(rename = "liquidatedCanceled")]
622 LiquidatedCanceled,
623 #[serde(rename = "scheduledCancel")]
625 ScheduledCancel,
626 #[serde(rename = "tickRejected")]
629 TickRejected,
630 #[serde(rename = "minTradeNtlRejected")]
632 MinTradeNtlRejected,
633 #[serde(rename = "perpMarginRejected")]
635 PerpMarginRejected,
636 #[serde(rename = "reduceOnlyRejected")]
638 ReduceOnlyRejected,
639 #[serde(rename = "badAloPxRejected")]
641 BadAloPxRejected,
642 #[serde(rename = "iocCancelRejected")]
644 IocCancelRejected,
645 #[serde(rename = "badTriggerPxRejected")]
647 BadTriggerPxRejected,
648 #[serde(rename = "marketOrderNoLiquidityRejected")]
650 MarketOrderNoLiquidityRejected,
651 #[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
653 PositionIncreaseAtOpenInterestCapRejected,
654 #[serde(rename = "positionFlipAtOpenInterestCapRejected")]
656 PositionFlipAtOpenInterestCapRejected,
657 #[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
659 TooAggressiveAtOpenInterestCapRejected,
660 #[serde(rename = "openInterestIncreaseRejected")]
662 OpenInterestIncreaseRejected,
663 #[serde(rename = "insufficientSpotBalanceRejected")]
665 InsufficientSpotBalanceRejected,
666 #[serde(rename = "oracleRejected")]
668 OracleRejected,
669 #[serde(rename = "perpMaxPositionRejected")]
671 PerpMaxPositionRejected,
672}
673
674impl From<HyperliquidOrderStatus> for OrderStatus {
675 fn from(status: HyperliquidOrderStatus) -> Self {
676 match status {
677 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
678 HyperliquidOrderStatus::Triggered => Self::Triggered,
679 HyperliquidOrderStatus::Filled => Self::Filled,
680 HyperliquidOrderStatus::Canceled
682 | HyperliquidOrderStatus::MarginCanceled
683 | HyperliquidOrderStatus::VaultWithdrawalCanceled
684 | HyperliquidOrderStatus::OpenInterestCapCanceled
685 | HyperliquidOrderStatus::SelfTradeCanceled
686 | HyperliquidOrderStatus::ReduceOnlyCanceled
687 | HyperliquidOrderStatus::SiblingFilledCanceled
688 | HyperliquidOrderStatus::DelistedCanceled
689 | HyperliquidOrderStatus::LiquidatedCanceled
690 | HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
691 HyperliquidOrderStatus::Rejected
693 | HyperliquidOrderStatus::TickRejected
694 | HyperliquidOrderStatus::MinTradeNtlRejected
695 | HyperliquidOrderStatus::PerpMarginRejected
696 | HyperliquidOrderStatus::ReduceOnlyRejected
697 | HyperliquidOrderStatus::BadAloPxRejected
698 | HyperliquidOrderStatus::IocCancelRejected
699 | HyperliquidOrderStatus::BadTriggerPxRejected
700 | HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
701 | HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
702 | HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
703 | HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
704 | HyperliquidOrderStatus::OpenInterestIncreaseRejected
705 | HyperliquidOrderStatus::InsufficientSpotBalanceRejected
706 | HyperliquidOrderStatus::OracleRejected
707 | HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
708 }
709 }
710}
711
712pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
713 match status {
714 "open" | "accepted" => OrderStatus::Accepted,
715 "triggered" => OrderStatus::Triggered,
716 "filled" => OrderStatus::Filled,
717 "canceled"
719 | "marginCanceled"
720 | "vaultWithdrawalCanceled"
721 | "openInterestCapCanceled"
722 | "selfTradeCanceled"
723 | "reduceOnlyCanceled"
724 | "siblingFilledCanceled"
725 | "delistedCanceled"
726 | "liquidatedCanceled"
727 | "scheduledCancel" => OrderStatus::Canceled,
728 "rejected"
730 | "tickRejected"
731 | "minTradeNtlRejected"
732 | "perpMarginRejected"
733 | "reduceOnlyRejected"
734 | "badAloPxRejected"
735 | "iocCancelRejected"
736 | "badTriggerPxRejected"
737 | "marketOrderNoLiquidityRejected"
738 | "positionIncreaseAtOpenInterestCapRejected"
739 | "positionFlipAtOpenInterestCapRejected"
740 | "tooAggressiveAtOpenInterestCapRejected"
741 | "openInterestIncreaseRejected"
742 | "insufficientSpotBalanceRejected"
743 | "oracleRejected"
744 | "perpMaxPositionRejected" => OrderStatus::Rejected,
745 _ => OrderStatus::Rejected,
747 }
748}
749
750#[derive(
761 Copy,
762 Clone,
763 Debug,
764 Display,
765 PartialEq,
766 Eq,
767 Hash,
768 AsRefStr,
769 EnumIter,
770 EnumString,
771 Serialize,
772 Deserialize,
773)]
774#[serde(rename_all = "PascalCase")]
775#[strum(serialize_all = "PascalCase")]
776pub enum HyperliquidFillDirection {
777 #[serde(rename = "Open Long")]
779 #[strum(serialize = "Open Long")]
780 OpenLong,
781 #[serde(rename = "Open Short")]
783 #[strum(serialize = "Open Short")]
784 OpenShort,
785 #[serde(rename = "Close Long")]
787 #[strum(serialize = "Close Long")]
788 CloseLong,
789 #[serde(rename = "Close Short")]
791 #[strum(serialize = "Close Short")]
792 CloseShort,
793 #[serde(rename = "Long > Short")]
795 #[strum(serialize = "Long > Short")]
796 LongToShort,
797 #[serde(rename = "Short > Long")]
799 #[strum(serialize = "Short > Long")]
800 ShortToLong,
801 Buy,
803 Sell,
805}
806
807#[derive(
811 Copy,
812 Clone,
813 Debug,
814 Display,
815 PartialEq,
816 Eq,
817 Hash,
818 AsRefStr,
819 EnumIter,
820 EnumString,
821 Serialize,
822 Deserialize,
823)]
824#[serde(rename_all = "camelCase")]
825#[strum(serialize_all = "camelCase")]
826pub enum HyperliquidInfoRequestType {
827 Meta,
829 SpotMeta,
831 MetaAndAssetCtxs,
833 SpotMetaAndAssetCtxs,
835 L2Book,
837 UserFills,
839 OrderStatus,
841 OpenOrders,
843 FrontendOpenOrders,
845 ClearinghouseState,
847 CandleSnapshot,
849}
850
851impl HyperliquidInfoRequestType {
852 pub fn as_str(&self) -> &'static str {
853 match self {
854 Self::Meta => "meta",
855 Self::SpotMeta => "spotMeta",
856 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
857 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
858 Self::L2Book => "l2Book",
859 Self::UserFills => "userFills",
860 Self::OrderStatus => "orderStatus",
861 Self::OpenOrders => "openOrders",
862 Self::FrontendOpenOrders => "frontendOpenOrders",
863 Self::ClearinghouseState => "clearinghouseState",
864 Self::CandleSnapshot => "candleSnapshot",
865 }
866 }
867}
868
869#[derive(
871 Copy,
872 Clone,
873 Debug,
874 Display,
875 PartialEq,
876 Eq,
877 Hash,
878 AsRefStr,
879 EnumIter,
880 EnumString,
881 Serialize,
882 Deserialize,
883)]
884#[cfg_attr(
885 feature = "python",
886 pyo3::pyclass(
887 module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
888 from_py_object,
889 rename_all = "SCREAMING_SNAKE_CASE",
890 )
891)]
892#[serde(rename_all = "UPPERCASE")]
893#[strum(serialize_all = "UPPERCASE")]
894pub enum HyperliquidProductType {
895 Perp,
897 Spot,
899}
900
901impl HyperliquidProductType {
902 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
908 if symbol.ends_with("-PERP") {
909 Ok(Self::Perp)
910 } else if symbol.ends_with("-SPOT") {
911 Ok(Self::Spot)
912 } else {
913 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use nautilus_model::enums::OrderType;
921 use rstest::rstest;
922 use serde_json;
923
924 use super::*;
925
926 #[rstest]
927 fn test_side_serde() {
928 let buy_side = HyperliquidSide::Buy;
929 let sell_side = HyperliquidSide::Sell;
930
931 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
932 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
933
934 assert_eq!(
935 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
936 HyperliquidSide::Buy
937 );
938 assert_eq!(
939 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
940 HyperliquidSide::Sell
941 );
942 }
943
944 #[rstest]
945 fn test_side_from_order_side() {
946 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
948 assert_eq!(
949 HyperliquidSide::from(OrderSide::Sell),
950 HyperliquidSide::Sell
951 );
952 }
953
954 #[rstest]
955 fn test_order_side_from_hyperliquid_side() {
956 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
958 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
959 }
960
961 #[rstest]
962 fn test_aggressor_side_from_hyperliquid_side() {
963 assert_eq!(
965 AggressorSide::from(HyperliquidSide::Buy),
966 AggressorSide::Buyer
967 );
968 assert_eq!(
969 AggressorSide::from(HyperliquidSide::Sell),
970 AggressorSide::Seller
971 );
972 }
973
974 #[rstest]
975 fn test_time_in_force_serde() {
976 let test_cases = [
977 (HyperliquidTimeInForce::Alo, "\"Alo\""),
978 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
979 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
980 ];
981
982 for (tif, expected_json) in test_cases {
983 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
984 assert_eq!(
985 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
986 tif
987 );
988 }
989 }
990
991 #[rstest]
992 fn test_liquidity_flag_from_crossed() {
993 assert_eq!(
994 HyperliquidLiquidityFlag::from(true),
995 HyperliquidLiquidityFlag::Taker
996 );
997 assert_eq!(
998 HyperliquidLiquidityFlag::from(false),
999 HyperliquidLiquidityFlag::Maker
1000 );
1001 }
1002
1003 #[rstest]
1004 #[allow(deprecated)]
1005 fn test_reject_code_from_error_string() {
1006 let test_cases = [
1007 (
1008 "Price must be divisible by tick size.",
1009 HyperliquidRejectCode::Tick,
1010 ),
1011 (
1012 "Order must have minimum value of $10.",
1013 HyperliquidRejectCode::MinTradeNtl,
1014 ),
1015 (
1016 "Insufficient margin to place order.",
1017 HyperliquidRejectCode::PerpMargin,
1018 ),
1019 (
1020 "Post only order would have immediately matched, bbo was 1.23",
1021 HyperliquidRejectCode::BadAloPx,
1022 ),
1023 (
1024 "Some unknown error",
1025 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1026 ),
1027 ];
1028
1029 for (error_str, expected_code) in test_cases {
1030 assert_eq!(
1031 HyperliquidRejectCode::from_error_string(error_str),
1032 expected_code
1033 );
1034 }
1035 }
1036
1037 #[rstest]
1038 fn test_reject_code_from_api_error() {
1039 let test_cases = [
1040 (
1041 "Price must be divisible by tick size.",
1042 HyperliquidRejectCode::Tick,
1043 ),
1044 (
1045 "Order must have minimum value of $10.",
1046 HyperliquidRejectCode::MinTradeNtl,
1047 ),
1048 (
1049 "Insufficient margin to place order.",
1050 HyperliquidRejectCode::PerpMargin,
1051 ),
1052 (
1053 "Post only order would have immediately matched, bbo was 1.23",
1054 HyperliquidRejectCode::BadAloPx,
1055 ),
1056 (
1057 "Some unknown error",
1058 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
1059 ),
1060 ];
1061
1062 for (error_str, expected_code) in test_cases {
1063 assert_eq!(
1064 HyperliquidRejectCode::from_api_error(error_str),
1065 expected_code
1066 );
1067 }
1068 }
1069
1070 #[rstest]
1071 fn test_reduce_only() {
1072 let reduce_only = HyperliquidReduceOnly::new(true);
1073
1074 assert!(reduce_only.is_reduce_only());
1075
1076 let json = serde_json::to_string(&reduce_only).unwrap();
1077 assert_eq!(json, "true");
1078
1079 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
1080 assert_eq!(parsed, reduce_only);
1081 }
1082
1083 #[rstest]
1084 fn test_order_status_conversion() {
1085 assert_eq!(
1087 OrderStatus::from(HyperliquidOrderStatus::Open),
1088 OrderStatus::Accepted
1089 );
1090 assert_eq!(
1091 OrderStatus::from(HyperliquidOrderStatus::Accepted),
1092 OrderStatus::Accepted
1093 );
1094 assert_eq!(
1095 OrderStatus::from(HyperliquidOrderStatus::Triggered),
1096 OrderStatus::Triggered
1097 );
1098 assert_eq!(
1099 OrderStatus::from(HyperliquidOrderStatus::Filled),
1100 OrderStatus::Filled
1101 );
1102 assert_eq!(
1103 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1104 OrderStatus::Canceled
1105 );
1106 assert_eq!(
1107 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1108 OrderStatus::Rejected
1109 );
1110
1111 assert_eq!(
1113 OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
1114 OrderStatus::Canceled
1115 );
1116 assert_eq!(
1117 OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
1118 OrderStatus::Canceled
1119 );
1120 assert_eq!(
1121 OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
1122 OrderStatus::Canceled
1123 );
1124
1125 assert_eq!(
1127 OrderStatus::from(HyperliquidOrderStatus::TickRejected),
1128 OrderStatus::Rejected
1129 );
1130 assert_eq!(
1131 OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
1132 OrderStatus::Rejected
1133 );
1134 }
1135
1136 #[rstest]
1137 fn test_order_status_string_mapping() {
1138 assert_eq!(
1140 hyperliquid_status_to_order_status("open"),
1141 OrderStatus::Accepted
1142 );
1143 assert_eq!(
1144 hyperliquid_status_to_order_status("accepted"),
1145 OrderStatus::Accepted
1146 );
1147 assert_eq!(
1148 hyperliquid_status_to_order_status("triggered"),
1149 OrderStatus::Triggered
1150 );
1151 assert_eq!(
1152 hyperliquid_status_to_order_status("filled"),
1153 OrderStatus::Filled
1154 );
1155 assert_eq!(
1156 hyperliquid_status_to_order_status("canceled"),
1157 OrderStatus::Canceled
1158 );
1159 assert_eq!(
1160 hyperliquid_status_to_order_status("rejected"),
1161 OrderStatus::Rejected
1162 );
1163
1164 assert_eq!(
1166 hyperliquid_status_to_order_status("marginCanceled"),
1167 OrderStatus::Canceled
1168 );
1169 assert_eq!(
1170 hyperliquid_status_to_order_status("selfTradeCanceled"),
1171 OrderStatus::Canceled
1172 );
1173 assert_eq!(
1174 hyperliquid_status_to_order_status("reduceOnlyCanceled"),
1175 OrderStatus::Canceled
1176 );
1177 assert_eq!(
1178 hyperliquid_status_to_order_status("liquidatedCanceled"),
1179 OrderStatus::Canceled
1180 );
1181
1182 assert_eq!(
1184 hyperliquid_status_to_order_status("tickRejected"),
1185 OrderStatus::Rejected
1186 );
1187 assert_eq!(
1188 hyperliquid_status_to_order_status("perpMarginRejected"),
1189 OrderStatus::Rejected
1190 );
1191
1192 assert_eq!(
1194 hyperliquid_status_to_order_status("unknown_status"),
1195 OrderStatus::Rejected
1196 );
1197 }
1198
1199 #[rstest]
1200 fn test_order_status_serde_deserialization() {
1201 let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
1203 assert_eq!(open, HyperliquidOrderStatus::Open);
1204
1205 let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
1206 assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
1207
1208 let margin_canceled: HyperliquidOrderStatus =
1209 serde_json::from_str(r#""marginCanceled""#).unwrap();
1210 assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
1211
1212 let self_trade_canceled: HyperliquidOrderStatus =
1213 serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
1214 assert_eq!(
1215 self_trade_canceled,
1216 HyperliquidOrderStatus::SelfTradeCanceled
1217 );
1218
1219 let reduce_only_canceled: HyperliquidOrderStatus =
1220 serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
1221 assert_eq!(
1222 reduce_only_canceled,
1223 HyperliquidOrderStatus::ReduceOnlyCanceled
1224 );
1225
1226 let tick_rejected: HyperliquidOrderStatus =
1227 serde_json::from_str(r#""tickRejected""#).unwrap();
1228 assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
1229 }
1230
1231 #[rstest]
1232 fn test_hyperliquid_tpsl_serialization() {
1233 let tp = HyperliquidTpSl::Tp;
1234 let sl = HyperliquidTpSl::Sl;
1235
1236 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1237 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1238 }
1239
1240 #[rstest]
1241 fn test_hyperliquid_tpsl_deserialization() {
1242 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1243 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1244
1245 assert_eq!(tp, HyperliquidTpSl::Tp);
1246 assert_eq!(sl, HyperliquidTpSl::Sl);
1247 }
1248
1249 #[rstest]
1250 fn test_conditional_order_type_conversions() {
1251 assert_eq!(
1253 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1254 OrderType::StopMarket
1255 );
1256 assert_eq!(
1257 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1258 OrderType::StopLimit
1259 );
1260 assert_eq!(
1261 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1262 OrderType::MarketIfTouched
1263 );
1264 assert_eq!(
1265 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1266 OrderType::LimitIfTouched
1267 );
1268 assert_eq!(
1269 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1270 OrderType::TrailingStopMarket
1271 );
1272 }
1273
1274 mod error_parsing_tests {
1276 use super::*;
1277
1278 #[rstest]
1279 fn test_parse_tick_size_error() {
1280 let error = "Price must be divisible by tick size 0.01";
1281 let code = HyperliquidRejectCode::from_api_error(error);
1282 assert_eq!(code, HyperliquidRejectCode::Tick);
1283 }
1284
1285 #[rstest]
1286 fn test_parse_tick_size_error_case_insensitive() {
1287 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1288 let code = HyperliquidRejectCode::from_api_error(error);
1289 assert_eq!(code, HyperliquidRejectCode::Tick);
1290 }
1291
1292 #[rstest]
1293 fn test_parse_min_notional_perp() {
1294 let error = "Order must have minimum value of $10";
1295 let code = HyperliquidRejectCode::from_api_error(error);
1296 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1297 }
1298
1299 #[rstest]
1300 fn test_parse_min_notional_spot() {
1301 let error = "Order must have minimum value of 10 USDC";
1302 let code = HyperliquidRejectCode::from_api_error(error);
1303 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1304 }
1305
1306 #[rstest]
1307 fn test_parse_insufficient_margin() {
1308 let error = "Insufficient margin to place order";
1309 let code = HyperliquidRejectCode::from_api_error(error);
1310 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1311 }
1312
1313 #[rstest]
1314 fn test_parse_insufficient_margin_case_variations() {
1315 let variations = vec![
1316 "insufficient margin to place order",
1317 "INSUFFICIENT MARGIN TO PLACE ORDER",
1318 " Insufficient margin to place order ", ];
1320
1321 for error in variations {
1322 let code = HyperliquidRejectCode::from_api_error(error);
1323 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1324 }
1325 }
1326
1327 #[rstest]
1328 fn test_parse_reduce_only_violation() {
1329 let error = "Reduce only order would increase position";
1330 let code = HyperliquidRejectCode::from_api_error(error);
1331 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1332 }
1333
1334 #[rstest]
1335 fn test_parse_reduce_only_with_hyphen() {
1336 let error = "Reduce-only order would increase position";
1337 let code = HyperliquidRejectCode::from_api_error(error);
1338 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1339 }
1340
1341 #[rstest]
1342 fn test_parse_post_only_match() {
1343 let error = "Post only order would have immediately matched";
1344 let code = HyperliquidRejectCode::from_api_error(error);
1345 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1346 }
1347
1348 #[rstest]
1349 fn test_parse_post_only_with_hyphen() {
1350 let error = "Post-only order would have immediately matched";
1351 let code = HyperliquidRejectCode::from_api_error(error);
1352 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1353 }
1354
1355 #[rstest]
1356 fn test_parse_ioc_no_match() {
1357 let error = "Order could not immediately match";
1358 let code = HyperliquidRejectCode::from_api_error(error);
1359 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1360 }
1361
1362 #[rstest]
1363 fn test_parse_invalid_trigger_price() {
1364 let error = "Invalid TP/SL price";
1365 let code = HyperliquidRejectCode::from_api_error(error);
1366 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1367 }
1368
1369 #[rstest]
1370 fn test_parse_no_liquidity() {
1371 let error = "No liquidity available for market order";
1372 let code = HyperliquidRejectCode::from_api_error(error);
1373 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1374 }
1375
1376 #[rstest]
1377 fn test_parse_position_increase_at_oi_cap() {
1378 let error = "PositionIncreaseAtOpenInterestCap";
1379 let code = HyperliquidRejectCode::from_api_error(error);
1380 assert_eq!(
1381 code,
1382 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1383 );
1384 }
1385
1386 #[rstest]
1387 fn test_parse_position_flip_at_oi_cap() {
1388 let error = "PositionFlipAtOpenInterestCap";
1389 let code = HyperliquidRejectCode::from_api_error(error);
1390 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1391 }
1392
1393 #[rstest]
1394 fn test_parse_too_aggressive_at_oi_cap() {
1395 let error = "TooAggressiveAtOpenInterestCap";
1396 let code = HyperliquidRejectCode::from_api_error(error);
1397 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1398 }
1399
1400 #[rstest]
1401 fn test_parse_open_interest_increase() {
1402 let error = "OpenInterestIncrease";
1403 let code = HyperliquidRejectCode::from_api_error(error);
1404 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1405 }
1406
1407 #[rstest]
1408 fn test_parse_insufficient_spot_balance() {
1409 let error = "Insufficient spot balance";
1410 let code = HyperliquidRejectCode::from_api_error(error);
1411 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1412 }
1413
1414 #[rstest]
1415 fn test_parse_oracle_error() {
1416 let error = "Oracle price unavailable";
1417 let code = HyperliquidRejectCode::from_api_error(error);
1418 assert_eq!(code, HyperliquidRejectCode::Oracle);
1419 }
1420
1421 #[rstest]
1422 fn test_parse_max_position() {
1423 let error = "Exceeds max position size";
1424 let code = HyperliquidRejectCode::from_api_error(error);
1425 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1426 }
1427
1428 #[rstest]
1429 fn test_parse_missing_order() {
1430 let error = "MissingOrder";
1431 let code = HyperliquidRejectCode::from_api_error(error);
1432 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1433 }
1434
1435 #[rstest]
1436 fn test_parse_unknown_error() {
1437 let error = "This is a completely new error message";
1438 let code = HyperliquidRejectCode::from_api_error(error);
1439 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1440
1441 if let HyperliquidRejectCode::Unknown(msg) = code {
1443 assert_eq!(msg, error);
1444 }
1445 }
1446
1447 #[rstest]
1448 fn test_parse_empty_error() {
1449 let error = "";
1450 let code = HyperliquidRejectCode::from_api_error(error);
1451 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1452 }
1453
1454 #[rstest]
1455 fn test_parse_whitespace_only() {
1456 let error = " ";
1457 let code = HyperliquidRejectCode::from_api_error(error);
1458 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1459 }
1460
1461 #[rstest]
1462 fn test_normalization_preserves_original_in_unknown() {
1463 let error = " UNKNOWN ERROR MESSAGE ";
1464 let code = HyperliquidRejectCode::from_api_error(error);
1465
1466 if let HyperliquidRejectCode::Unknown(msg) = code {
1468 assert_eq!(msg, error);
1469 } else {
1470 panic!("Expected Unknown variant");
1471 }
1472 }
1473 }
1474
1475 #[rstest]
1476 fn test_conditional_order_type_round_trip() {
1477 assert_eq!(
1478 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1479 OrderType::TrailingStopLimit
1480 );
1481
1482 assert_eq!(
1484 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1485 HyperliquidConditionalOrderType::StopMarket
1486 );
1487 assert_eq!(
1488 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1489 HyperliquidConditionalOrderType::StopLimit
1490 );
1491 }
1492
1493 #[rstest]
1494 fn test_trailing_offset_type_serialization() {
1495 let price = HyperliquidTrailingOffsetType::Price;
1496 let percentage = HyperliquidTrailingOffsetType::Percentage;
1497 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1498
1499 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1500 assert_eq!(
1501 serde_json::to_string(&percentage).unwrap(),
1502 r#""percentage""#
1503 );
1504 assert_eq!(
1505 serde_json::to_string(&basis_points).unwrap(),
1506 r#""basispoints""#
1507 );
1508 }
1509
1510 #[rstest]
1511 fn test_conditional_order_type_serialization() {
1512 assert_eq!(
1513 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1514 r#""STOP_MARKET""#
1515 );
1516 assert_eq!(
1517 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1518 r#""STOP_LIMIT""#
1519 );
1520 assert_eq!(
1521 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1522 r#""TAKE_PROFIT_MARKET""#
1523 );
1524 assert_eq!(
1525 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1526 r#""TAKE_PROFIT_LIMIT""#
1527 );
1528 assert_eq!(
1529 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1530 r#""TRAILING_STOP_MARKET""#
1531 );
1532 assert_eq!(
1533 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1534 r#""TRAILING_STOP_LIMIT""#
1535 );
1536 }
1537
1538 #[rstest]
1539 fn test_order_type_enum_coverage() {
1540 let conditional_types = vec![
1542 HyperliquidConditionalOrderType::StopMarket,
1543 HyperliquidConditionalOrderType::StopLimit,
1544 HyperliquidConditionalOrderType::TakeProfitMarket,
1545 HyperliquidConditionalOrderType::TakeProfitLimit,
1546 HyperliquidConditionalOrderType::TrailingStopMarket,
1547 HyperliquidConditionalOrderType::TrailingStopLimit,
1548 ];
1549
1550 for cond_type in conditional_types {
1551 let order_type = OrderType::from(cond_type);
1552 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1553 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1554 }
1555 }
1556}