1use std::{fmt::Display, str::FromStr};
17
18use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus};
19use serde::{Deserialize, Serialize};
20use strum::{AsRefStr, Display, EnumIter, EnumString};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum HyperliquidBarInterval {
24 #[serde(rename = "1m")]
25 OneMinute,
26 #[serde(rename = "3m")]
27 ThreeMinutes,
28 #[serde(rename = "5m")]
29 FiveMinutes,
30 #[serde(rename = "15m")]
31 FifteenMinutes,
32 #[serde(rename = "30m")]
33 ThirtyMinutes,
34 #[serde(rename = "1h")]
35 OneHour,
36 #[serde(rename = "2h")]
37 TwoHours,
38 #[serde(rename = "4h")]
39 FourHours,
40 #[serde(rename = "8h")]
41 EightHours,
42 #[serde(rename = "12h")]
43 TwelveHours,
44 #[serde(rename = "1d")]
45 OneDay,
46 #[serde(rename = "3d")]
47 ThreeDays,
48 #[serde(rename = "1w")]
49 OneWeek,
50 #[serde(rename = "1M")]
51 OneMonth,
52}
53
54impl HyperliquidBarInterval {
55 pub fn as_str(&self) -> &'static str {
56 match self {
57 Self::OneMinute => "1m",
58 Self::ThreeMinutes => "3m",
59 Self::FiveMinutes => "5m",
60 Self::FifteenMinutes => "15m",
61 Self::ThirtyMinutes => "30m",
62 Self::OneHour => "1h",
63 Self::TwoHours => "2h",
64 Self::FourHours => "4h",
65 Self::EightHours => "8h",
66 Self::TwelveHours => "12h",
67 Self::OneDay => "1d",
68 Self::ThreeDays => "3d",
69 Self::OneWeek => "1w",
70 Self::OneMonth => "1M",
71 }
72 }
73}
74
75impl FromStr for HyperliquidBarInterval {
76 type Err = anyhow::Error;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 match s {
80 "1m" => Ok(Self::OneMinute),
81 "3m" => Ok(Self::ThreeMinutes),
82 "5m" => Ok(Self::FiveMinutes),
83 "15m" => Ok(Self::FifteenMinutes),
84 "30m" => Ok(Self::ThirtyMinutes),
85 "1h" => Ok(Self::OneHour),
86 "2h" => Ok(Self::TwoHours),
87 "4h" => Ok(Self::FourHours),
88 "8h" => Ok(Self::EightHours),
89 "12h" => Ok(Self::TwelveHours),
90 "1d" => Ok(Self::OneDay),
91 "3d" => Ok(Self::ThreeDays),
92 "1w" => Ok(Self::OneWeek),
93 "1M" => Ok(Self::OneMonth),
94 _ => anyhow::bail!("Invalid Hyperliquid bar interval: {s}"),
95 }
96 }
97}
98
99impl Display for HyperliquidBarInterval {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(f, "{}", self.as_str())
102 }
103}
104
105#[derive(
107 Copy,
108 Clone,
109 Debug,
110 Display,
111 PartialEq,
112 Eq,
113 Hash,
114 AsRefStr,
115 EnumIter,
116 EnumString,
117 Serialize,
118 Deserialize,
119)]
120#[serde(rename_all = "UPPERCASE")]
121#[strum(serialize_all = "UPPERCASE")]
122pub enum HyperliquidSide {
123 #[serde(rename = "B")]
124 Buy,
125 #[serde(rename = "A")]
126 Sell,
127}
128
129impl From<OrderSide> for HyperliquidSide {
130 fn from(value: OrderSide) -> Self {
131 match value {
132 OrderSide::Buy => Self::Buy,
133 OrderSide::Sell => Self::Sell,
134 _ => panic!("Invalid `OrderSide`"),
135 }
136 }
137}
138
139impl From<HyperliquidSide> for OrderSide {
140 fn from(value: HyperliquidSide) -> Self {
141 match value {
142 HyperliquidSide::Buy => Self::Buy,
143 HyperliquidSide::Sell => Self::Sell,
144 }
145 }
146}
147
148impl From<HyperliquidSide> for AggressorSide {
149 fn from(value: HyperliquidSide) -> Self {
150 match value {
151 HyperliquidSide::Buy => Self::Buyer,
152 HyperliquidSide::Sell => Self::Seller,
153 }
154 }
155}
156
157#[derive(
159 Copy,
160 Clone,
161 Debug,
162 Display,
163 PartialEq,
164 Eq,
165 Hash,
166 AsRefStr,
167 EnumIter,
168 EnumString,
169 Serialize,
170 Deserialize,
171)]
172#[serde(rename_all = "PascalCase")]
173#[strum(serialize_all = "PascalCase")]
174pub enum HyperliquidTimeInForce {
175 Alo,
177 Ioc,
179 Gtc,
181}
182
183#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185#[serde(tag = "type", rename_all = "lowercase")]
186pub enum HyperliquidOrderType {
187 #[serde(rename = "limit")]
189 Limit { tif: HyperliquidTimeInForce },
190
191 #[serde(rename = "trigger")]
193 Trigger {
194 #[serde(rename = "isMarket")]
195 is_market: bool,
196 #[serde(rename = "triggerPx")]
197 trigger_px: String,
198 tpsl: HyperliquidTpSl,
199 },
200}
201
202#[derive(
204 Copy,
205 Clone,
206 Debug,
207 Display,
208 PartialEq,
209 Eq,
210 Hash,
211 AsRefStr,
212 EnumIter,
213 EnumString,
214 Serialize,
215 Deserialize,
216)]
217#[cfg_attr(
218 feature = "python",
219 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
220)]
221#[serde(rename_all = "lowercase")]
222#[strum(serialize_all = "lowercase")]
223pub enum HyperliquidTpSl {
224 Tp,
226 Sl,
228}
229
230#[derive(
237 Copy,
238 Clone,
239 Debug,
240 Display,
241 PartialEq,
242 Eq,
243 Hash,
244 AsRefStr,
245 EnumIter,
246 EnumString,
247 Serialize,
248 Deserialize,
249)]
250#[cfg_attr(
251 feature = "python",
252 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
253)]
254#[serde(rename_all = "lowercase")]
255#[strum(serialize_all = "lowercase")]
256pub enum HyperliquidTriggerPriceType {
257 Last,
259 Mark,
261 Oracle,
263}
264
265impl From<HyperliquidTriggerPriceType> for nautilus_model::enums::TriggerType {
266 fn from(value: HyperliquidTriggerPriceType) -> Self {
267 match value {
268 HyperliquidTriggerPriceType::Last => Self::LastPrice,
269 HyperliquidTriggerPriceType::Mark => Self::MarkPrice,
270 HyperliquidTriggerPriceType::Oracle => Self::IndexPrice,
271 }
272 }
273}
274
275impl From<nautilus_model::enums::TriggerType> for HyperliquidTriggerPriceType {
276 fn from(value: nautilus_model::enums::TriggerType) -> Self {
277 match value {
278 nautilus_model::enums::TriggerType::LastPrice => Self::Last,
279 nautilus_model::enums::TriggerType::MarkPrice => Self::Mark,
280 nautilus_model::enums::TriggerType::IndexPrice => Self::Oracle,
281 _ => Self::Last, }
283 }
284}
285
286#[derive(
291 Copy,
292 Clone,
293 Debug,
294 Display,
295 PartialEq,
296 Eq,
297 Hash,
298 AsRefStr,
299 EnumIter,
300 EnumString,
301 Serialize,
302 Deserialize,
303)]
304#[cfg_attr(
305 feature = "python",
306 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
307)]
308#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
309#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
310pub enum HyperliquidConditionalOrderType {
311 StopMarket,
313 StopLimit,
315 TakeProfitMarket,
317 TakeProfitLimit,
319 TrailingStopMarket,
321 TrailingStopLimit,
323}
324
325impl From<HyperliquidConditionalOrderType> for nautilus_model::enums::OrderType {
326 fn from(value: HyperliquidConditionalOrderType) -> Self {
327 match value {
328 HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
329 HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
330 HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
331 HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
332 HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
333 HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
334 }
335 }
336}
337
338impl From<nautilus_model::enums::OrderType> for HyperliquidConditionalOrderType {
339 fn from(value: nautilus_model::enums::OrderType) -> Self {
340 match value {
341 nautilus_model::enums::OrderType::StopMarket => Self::StopMarket,
342 nautilus_model::enums::OrderType::StopLimit => Self::StopLimit,
343 nautilus_model::enums::OrderType::MarketIfTouched => Self::TakeProfitMarket,
344 nautilus_model::enums::OrderType::LimitIfTouched => Self::TakeProfitLimit,
345 nautilus_model::enums::OrderType::TrailingStopMarket => Self::TrailingStopMarket,
346 nautilus_model::enums::OrderType::TrailingStopLimit => Self::TrailingStopLimit,
347 _ => panic!("Unsupported OrderType for conditional orders: {:?}", value),
348 }
349 }
350}
351
352#[derive(
359 Copy,
360 Clone,
361 Debug,
362 Display,
363 PartialEq,
364 Eq,
365 Hash,
366 AsRefStr,
367 EnumIter,
368 EnumString,
369 Serialize,
370 Deserialize,
371)]
372#[cfg_attr(
373 feature = "python",
374 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
375)]
376#[serde(rename_all = "lowercase")]
377#[strum(serialize_all = "lowercase")]
378pub enum HyperliquidTrailingOffsetType {
379 Price,
381 Percentage,
383 #[serde(rename = "basispoints")]
385 #[strum(serialize = "basispoints")]
386 BasisPoints,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(transparent)]
392pub struct HyperliquidReduceOnly(pub bool);
393
394impl HyperliquidReduceOnly {
395 pub fn new(reduce_only: bool) -> Self {
397 Self(reduce_only)
398 }
399
400 pub fn is_reduce_only(&self) -> bool {
402 self.0
403 }
404}
405
406#[derive(
408 Copy,
409 Clone,
410 Debug,
411 Display,
412 PartialEq,
413 Eq,
414 Hash,
415 AsRefStr,
416 EnumIter,
417 EnumString,
418 Serialize,
419 Deserialize,
420)]
421#[serde(rename_all = "lowercase")]
422#[strum(serialize_all = "lowercase")]
423pub enum HyperliquidLiquidityFlag {
424 Maker,
425 Taker,
426}
427
428impl From<bool> for HyperliquidLiquidityFlag {
429 fn from(crossed: bool) -> Self {
433 if crossed { Self::Taker } else { Self::Maker }
434 }
435}
436
437#[derive(
439 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
440)]
441#[serde(rename_all = "lowercase")]
442#[strum(serialize_all = "lowercase")]
443pub enum HyperliquidLiquidationMethod {
444 Market,
445 Backstop,
446}
447
448#[derive(
450 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
451)]
452#[serde(rename_all = "camelCase")]
453#[strum(serialize_all = "camelCase")]
454pub enum HyperliquidPositionType {
455 OneWay,
456}
457
458#[derive(
460 Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
461)]
462#[serde(rename_all = "lowercase")]
463#[strum(serialize_all = "lowercase")]
464pub enum HyperliquidTwapStatus {
465 Activated,
466 Terminated,
467 Finished,
468 Error,
469}
470
471#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
472#[serde(untagged)]
473pub enum HyperliquidRejectCode {
474 Tick,
476 MinTradeNtl,
478 MinTradeSpotNtl,
480 PerpMargin,
482 ReduceOnly,
484 BadAloPx,
486 IocCancel,
488 BadTriggerPx,
490 MarketOrderNoLiquidity,
492 PositionIncreaseAtOpenInterestCap,
494 PositionFlipAtOpenInterestCap,
496 TooAggressiveAtOpenInterestCap,
498 OpenInterestIncrease,
500 InsufficientSpotBalance,
502 Oracle,
504 PerpMaxPosition,
506 MissingOrder,
508 Unknown(String),
510}
511
512impl HyperliquidRejectCode {
513 pub fn from_api_error(error_message: &str) -> Self {
515 Self::from_error_string_internal(error_message)
516 }
517
518 fn from_error_string_internal(error: &str) -> Self {
519 let normalized = error.trim().to_lowercase();
521
522 match normalized.as_str() {
523 s if s.contains("tick size") => Self::Tick,
525
526 s if s.contains("minimum value of $10") => Self::MinTradeNtl,
528 s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
529
530 s if s.contains("insufficient margin") => Self::PerpMargin,
532
533 s if s.contains("reduce only order would increase")
535 || s.contains("reduce-only order would increase") =>
536 {
537 Self::ReduceOnly
538 }
539
540 s if s.contains("post only order would have immediately matched")
542 || s.contains("post-only order would have immediately matched") =>
543 {
544 Self::BadAloPx
545 }
546
547 s if s.contains("could not immediately match") => Self::IocCancel,
549
550 s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
552
553 s if s.contains("no liquidity available for market order") => {
555 Self::MarketOrderNoLiquidity
556 }
557
558 s if s.contains("positionincreaseatopeninterestcap") => {
561 Self::PositionIncreaseAtOpenInterestCap
562 }
563 s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
564 s if s.contains("tooaggressiveatopeninterestcap") => {
565 Self::TooAggressiveAtOpenInterestCap
566 }
567 s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
568
569 s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
571
572 s if s.contains("oracle") => Self::Oracle,
574
575 s if s.contains("max position") => Self::PerpMaxPosition,
577
578 s if s.contains("missingorder") => Self::MissingOrder,
580
581 _ => {
583 tracing::warn!(
584 "Unknown Hyperliquid error pattern (consider updating error parsing): {}",
585 error );
587 Self::Unknown(error.to_string())
588 }
589 }
590 }
591
592 #[deprecated(
597 since = "0.50.0",
598 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
599 )]
600 pub fn from_error_string(error: &str) -> Self {
601 Self::from_error_string_internal(error)
602 }
603}
604
605#[derive(
607 Copy,
608 Clone,
609 Debug,
610 Display,
611 PartialEq,
612 Eq,
613 Hash,
614 AsRefStr,
615 EnumIter,
616 EnumString,
617 Serialize,
618 Deserialize,
619)]
620#[serde(rename_all = "snake_case")]
621#[strum(serialize_all = "snake_case")]
622pub enum HyperliquidOrderStatus {
623 Open,
625 Accepted,
627 PartiallyFilled,
629 Filled,
631 Canceled,
633 Cancelled,
635 Rejected,
637 Expired,
639}
640
641impl From<HyperliquidOrderStatus> for OrderStatus {
642 fn from(status: HyperliquidOrderStatus) -> Self {
643 match status {
644 HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
645 HyperliquidOrderStatus::PartiallyFilled => Self::PartiallyFilled,
646 HyperliquidOrderStatus::Filled => Self::Filled,
647 HyperliquidOrderStatus::Canceled | HyperliquidOrderStatus::Cancelled => Self::Canceled,
648 HyperliquidOrderStatus::Rejected => Self::Rejected,
649 HyperliquidOrderStatus::Expired => Self::Expired,
650 }
651 }
652}
653
654pub fn hyperliquid_status_to_order_status(status: &str) -> OrderStatus {
655 match status {
656 "open" | "accepted" => OrderStatus::Accepted,
657 "partially_filled" => OrderStatus::PartiallyFilled,
658 "filled" => OrderStatus::Filled,
659 "canceled" | "cancelled" => OrderStatus::Canceled,
660 "rejected" => OrderStatus::Rejected,
661 "expired" => OrderStatus::Expired,
662 _ => OrderStatus::Rejected,
663 }
664}
665
666#[derive(
677 Copy,
678 Clone,
679 Debug,
680 Display,
681 PartialEq,
682 Eq,
683 Hash,
684 AsRefStr,
685 EnumIter,
686 EnumString,
687 Serialize,
688 Deserialize,
689)]
690#[serde(rename_all = "PascalCase")]
691#[strum(serialize_all = "PascalCase")]
692pub enum HyperliquidFillDirection {
693 #[serde(rename = "Open Long")]
695 #[strum(serialize = "Open Long")]
696 OpenLong,
697 #[serde(rename = "Open Short")]
699 #[strum(serialize = "Open Short")]
700 OpenShort,
701 #[serde(rename = "Close Long")]
703 #[strum(serialize = "Close Long")]
704 CloseLong,
705 #[serde(rename = "Close Short")]
707 #[strum(serialize = "Close Short")]
708 CloseShort,
709 Sell,
711}
712
713#[derive(
717 Copy,
718 Clone,
719 Debug,
720 Display,
721 PartialEq,
722 Eq,
723 Hash,
724 AsRefStr,
725 EnumIter,
726 EnumString,
727 Serialize,
728 Deserialize,
729)]
730#[serde(rename_all = "camelCase")]
731#[strum(serialize_all = "camelCase")]
732pub enum HyperliquidInfoRequestType {
733 Meta,
735 SpotMeta,
737 MetaAndAssetCtxs,
739 SpotMetaAndAssetCtxs,
741 L2Book,
743 UserFills,
745 OrderStatus,
747 OpenOrders,
749 FrontendOpenOrders,
751 ClearinghouseState,
753 CandleSnapshot,
755}
756
757impl HyperliquidInfoRequestType {
758 pub fn as_str(&self) -> &'static str {
759 match self {
760 Self::Meta => "meta",
761 Self::SpotMeta => "spotMeta",
762 Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
763 Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
764 Self::L2Book => "l2Book",
765 Self::UserFills => "userFills",
766 Self::OrderStatus => "orderStatus",
767 Self::OpenOrders => "openOrders",
768 Self::FrontendOpenOrders => "frontendOpenOrders",
769 Self::ClearinghouseState => "clearinghouseState",
770 Self::CandleSnapshot => "candleSnapshot",
771 }
772 }
773}
774
775#[derive(
777 Copy,
778 Clone,
779 Debug,
780 Display,
781 PartialEq,
782 Eq,
783 Hash,
784 AsRefStr,
785 EnumIter,
786 EnumString,
787 Serialize,
788 Deserialize,
789)]
790#[cfg_attr(
791 feature = "python",
792 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.hyperliquid")
793)]
794#[serde(rename_all = "UPPERCASE")]
795#[strum(serialize_all = "UPPERCASE")]
796pub enum HyperliquidProductType {
797 Perp,
799 Spot,
801}
802
803impl HyperliquidProductType {
804 pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
810 if symbol.ends_with("-PERP") {
811 Ok(Self::Perp)
812 } else if symbol.ends_with("-SPOT") {
813 Ok(Self::Spot)
814 } else {
815 anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
816 }
817 }
818}
819
820#[cfg(test)]
825mod tests {
826 use nautilus_model::enums::{OrderType, TriggerType};
827 use rstest::rstest;
828 use serde_json;
829
830 use super::*;
831
832 #[rstest]
833 fn test_side_serde() {
834 let buy_side = HyperliquidSide::Buy;
835 let sell_side = HyperliquidSide::Sell;
836
837 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
838 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
839
840 assert_eq!(
841 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
842 HyperliquidSide::Buy
843 );
844 assert_eq!(
845 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
846 HyperliquidSide::Sell
847 );
848 }
849
850 #[rstest]
851 fn test_side_from_order_side() {
852 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
854 assert_eq!(
855 HyperliquidSide::from(OrderSide::Sell),
856 HyperliquidSide::Sell
857 );
858 }
859
860 #[rstest]
861 fn test_order_side_from_hyperliquid_side() {
862 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
864 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
865 }
866
867 #[rstest]
868 fn test_aggressor_side_from_hyperliquid_side() {
869 assert_eq!(
871 AggressorSide::from(HyperliquidSide::Buy),
872 AggressorSide::Buyer
873 );
874 assert_eq!(
875 AggressorSide::from(HyperliquidSide::Sell),
876 AggressorSide::Seller
877 );
878 }
879
880 #[rstest]
881 fn test_time_in_force_serde() {
882 let test_cases = [
883 (HyperliquidTimeInForce::Alo, "\"Alo\""),
884 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
885 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
886 ];
887
888 for (tif, expected_json) in test_cases {
889 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
890 assert_eq!(
891 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
892 tif
893 );
894 }
895 }
896
897 #[rstest]
898 fn test_liquidity_flag_from_crossed() {
899 assert_eq!(
900 HyperliquidLiquidityFlag::from(true),
901 HyperliquidLiquidityFlag::Taker
902 );
903 assert_eq!(
904 HyperliquidLiquidityFlag::from(false),
905 HyperliquidLiquidityFlag::Maker
906 );
907 }
908
909 #[rstest]
910 #[allow(deprecated)]
911 fn test_reject_code_from_error_string() {
912 let test_cases = [
913 (
914 "Price must be divisible by tick size.",
915 HyperliquidRejectCode::Tick,
916 ),
917 (
918 "Order must have minimum value of $10.",
919 HyperliquidRejectCode::MinTradeNtl,
920 ),
921 (
922 "Insufficient margin to place order.",
923 HyperliquidRejectCode::PerpMargin,
924 ),
925 (
926 "Post only order would have immediately matched, bbo was 1.23",
927 HyperliquidRejectCode::BadAloPx,
928 ),
929 (
930 "Some unknown error",
931 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
932 ),
933 ];
934
935 for (error_str, expected_code) in test_cases {
936 assert_eq!(
937 HyperliquidRejectCode::from_error_string(error_str),
938 expected_code
939 );
940 }
941 }
942
943 #[rstest]
944 fn test_reject_code_from_api_error() {
945 let test_cases = [
946 (
947 "Price must be divisible by tick size.",
948 HyperliquidRejectCode::Tick,
949 ),
950 (
951 "Order must have minimum value of $10.",
952 HyperliquidRejectCode::MinTradeNtl,
953 ),
954 (
955 "Insufficient margin to place order.",
956 HyperliquidRejectCode::PerpMargin,
957 ),
958 (
959 "Post only order would have immediately matched, bbo was 1.23",
960 HyperliquidRejectCode::BadAloPx,
961 ),
962 (
963 "Some unknown error",
964 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
965 ),
966 ];
967
968 for (error_str, expected_code) in test_cases {
969 assert_eq!(
970 HyperliquidRejectCode::from_api_error(error_str),
971 expected_code
972 );
973 }
974 }
975
976 #[rstest]
977 fn test_reduce_only() {
978 let reduce_only = HyperliquidReduceOnly::new(true);
979
980 assert!(reduce_only.is_reduce_only());
981
982 let json = serde_json::to_string(&reduce_only).unwrap();
983 assert_eq!(json, "true");
984
985 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
986 assert_eq!(parsed, reduce_only);
987 }
988
989 #[rstest]
990 fn test_order_status_conversion() {
991 assert_eq!(
993 OrderStatus::from(HyperliquidOrderStatus::Open),
994 OrderStatus::Accepted
995 );
996 assert_eq!(
997 OrderStatus::from(HyperliquidOrderStatus::Accepted),
998 OrderStatus::Accepted
999 );
1000 assert_eq!(
1001 OrderStatus::from(HyperliquidOrderStatus::PartiallyFilled),
1002 OrderStatus::PartiallyFilled
1003 );
1004 assert_eq!(
1005 OrderStatus::from(HyperliquidOrderStatus::Filled),
1006 OrderStatus::Filled
1007 );
1008 assert_eq!(
1009 OrderStatus::from(HyperliquidOrderStatus::Canceled),
1010 OrderStatus::Canceled
1011 );
1012 assert_eq!(
1013 OrderStatus::from(HyperliquidOrderStatus::Cancelled),
1014 OrderStatus::Canceled
1015 );
1016 assert_eq!(
1017 OrderStatus::from(HyperliquidOrderStatus::Rejected),
1018 OrderStatus::Rejected
1019 );
1020 assert_eq!(
1021 OrderStatus::from(HyperliquidOrderStatus::Expired),
1022 OrderStatus::Expired
1023 );
1024 }
1025
1026 #[rstest]
1027 fn test_order_status_string_mapping() {
1028 assert_eq!(
1030 hyperliquid_status_to_order_status("open"),
1031 OrderStatus::Accepted
1032 );
1033 assert_eq!(
1034 hyperliquid_status_to_order_status("accepted"),
1035 OrderStatus::Accepted
1036 );
1037 assert_eq!(
1038 hyperliquid_status_to_order_status("partially_filled"),
1039 OrderStatus::PartiallyFilled
1040 );
1041 assert_eq!(
1042 hyperliquid_status_to_order_status("filled"),
1043 OrderStatus::Filled
1044 );
1045 assert_eq!(
1046 hyperliquid_status_to_order_status("canceled"),
1047 OrderStatus::Canceled
1048 );
1049 assert_eq!(
1050 hyperliquid_status_to_order_status("cancelled"),
1051 OrderStatus::Canceled
1052 );
1053 assert_eq!(
1054 hyperliquid_status_to_order_status("rejected"),
1055 OrderStatus::Rejected
1056 );
1057 assert_eq!(
1058 hyperliquid_status_to_order_status("expired"),
1059 OrderStatus::Expired
1060 );
1061 assert_eq!(
1062 hyperliquid_status_to_order_status("unknown_status"),
1063 OrderStatus::Rejected
1064 );
1065 }
1066
1067 #[rstest]
1072 fn test_hyperliquid_tpsl_serialization() {
1073 let tp = HyperliquidTpSl::Tp;
1074 let sl = HyperliquidTpSl::Sl;
1075
1076 assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
1077 assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
1078 }
1079
1080 #[rstest]
1081 fn test_hyperliquid_tpsl_deserialization() {
1082 let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
1083 let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
1084
1085 assert_eq!(tp, HyperliquidTpSl::Tp);
1086 assert_eq!(sl, HyperliquidTpSl::Sl);
1087 }
1088
1089 #[rstest]
1090 fn test_hyperliquid_trigger_price_type_serialization() {
1091 let last = HyperliquidTriggerPriceType::Last;
1092 let mark = HyperliquidTriggerPriceType::Mark;
1093 let oracle = HyperliquidTriggerPriceType::Oracle;
1094
1095 assert_eq!(serde_json::to_string(&last).unwrap(), r#""last""#);
1096 assert_eq!(serde_json::to_string(&mark).unwrap(), r#""mark""#);
1097 assert_eq!(serde_json::to_string(&oracle).unwrap(), r#""oracle""#);
1098 }
1099
1100 #[rstest]
1101 fn test_hyperliquid_trigger_price_type_to_nautilus() {
1102 assert_eq!(
1103 TriggerType::from(HyperliquidTriggerPriceType::Last),
1104 TriggerType::LastPrice
1105 );
1106 assert_eq!(
1107 TriggerType::from(HyperliquidTriggerPriceType::Mark),
1108 TriggerType::MarkPrice
1109 );
1110 assert_eq!(
1111 TriggerType::from(HyperliquidTriggerPriceType::Oracle),
1112 TriggerType::IndexPrice
1113 );
1114 }
1115
1116 #[rstest]
1117 fn test_nautilus_trigger_type_to_hyperliquid() {
1118 assert_eq!(
1119 HyperliquidTriggerPriceType::from(TriggerType::LastPrice),
1120 HyperliquidTriggerPriceType::Last
1121 );
1122 assert_eq!(
1123 HyperliquidTriggerPriceType::from(TriggerType::MarkPrice),
1124 HyperliquidTriggerPriceType::Mark
1125 );
1126 assert_eq!(
1127 HyperliquidTriggerPriceType::from(TriggerType::IndexPrice),
1128 HyperliquidTriggerPriceType::Oracle
1129 );
1130 }
1131
1132 #[rstest]
1133 fn test_conditional_order_type_conversions() {
1134 assert_eq!(
1136 OrderType::from(HyperliquidConditionalOrderType::StopMarket),
1137 OrderType::StopMarket
1138 );
1139 assert_eq!(
1140 OrderType::from(HyperliquidConditionalOrderType::StopLimit),
1141 OrderType::StopLimit
1142 );
1143 assert_eq!(
1144 OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
1145 OrderType::MarketIfTouched
1146 );
1147 assert_eq!(
1148 OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
1149 OrderType::LimitIfTouched
1150 );
1151 assert_eq!(
1152 OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
1153 OrderType::TrailingStopMarket
1154 );
1155 }
1156
1157 mod error_parsing_tests {
1159 use super::*;
1160
1161 #[rstest]
1162 fn test_parse_tick_size_error() {
1163 let error = "Price must be divisible by tick size 0.01";
1164 let code = HyperliquidRejectCode::from_api_error(error);
1165 assert_eq!(code, HyperliquidRejectCode::Tick);
1166 }
1167
1168 #[rstest]
1169 fn test_parse_tick_size_error_case_insensitive() {
1170 let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
1171 let code = HyperliquidRejectCode::from_api_error(error);
1172 assert_eq!(code, HyperliquidRejectCode::Tick);
1173 }
1174
1175 #[rstest]
1176 fn test_parse_min_notional_perp() {
1177 let error = "Order must have minimum value of $10";
1178 let code = HyperliquidRejectCode::from_api_error(error);
1179 assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
1180 }
1181
1182 #[rstest]
1183 fn test_parse_min_notional_spot() {
1184 let error = "Order must have minimum value of 10 USDC";
1185 let code = HyperliquidRejectCode::from_api_error(error);
1186 assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
1187 }
1188
1189 #[rstest]
1190 fn test_parse_insufficient_margin() {
1191 let error = "Insufficient margin to place order";
1192 let code = HyperliquidRejectCode::from_api_error(error);
1193 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1194 }
1195
1196 #[rstest]
1197 fn test_parse_insufficient_margin_case_variations() {
1198 let variations = vec![
1199 "insufficient margin to place order",
1200 "INSUFFICIENT MARGIN TO PLACE ORDER",
1201 " Insufficient margin to place order ", ];
1203
1204 for error in variations {
1205 let code = HyperliquidRejectCode::from_api_error(error);
1206 assert_eq!(code, HyperliquidRejectCode::PerpMargin);
1207 }
1208 }
1209
1210 #[rstest]
1211 fn test_parse_reduce_only_violation() {
1212 let error = "Reduce only order would increase position";
1213 let code = HyperliquidRejectCode::from_api_error(error);
1214 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1215 }
1216
1217 #[rstest]
1218 fn test_parse_reduce_only_with_hyphen() {
1219 let error = "Reduce-only order would increase position";
1220 let code = HyperliquidRejectCode::from_api_error(error);
1221 assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
1222 }
1223
1224 #[rstest]
1225 fn test_parse_post_only_match() {
1226 let error = "Post only order would have immediately matched";
1227 let code = HyperliquidRejectCode::from_api_error(error);
1228 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1229 }
1230
1231 #[rstest]
1232 fn test_parse_post_only_with_hyphen() {
1233 let error = "Post-only order would have immediately matched";
1234 let code = HyperliquidRejectCode::from_api_error(error);
1235 assert_eq!(code, HyperliquidRejectCode::BadAloPx);
1236 }
1237
1238 #[rstest]
1239 fn test_parse_ioc_no_match() {
1240 let error = "Order could not immediately match";
1241 let code = HyperliquidRejectCode::from_api_error(error);
1242 assert_eq!(code, HyperliquidRejectCode::IocCancel);
1243 }
1244
1245 #[rstest]
1246 fn test_parse_invalid_trigger_price() {
1247 let error = "Invalid TP/SL price";
1248 let code = HyperliquidRejectCode::from_api_error(error);
1249 assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
1250 }
1251
1252 #[rstest]
1253 fn test_parse_no_liquidity() {
1254 let error = "No liquidity available for market order";
1255 let code = HyperliquidRejectCode::from_api_error(error);
1256 assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
1257 }
1258
1259 #[rstest]
1260 fn test_parse_position_increase_at_oi_cap() {
1261 let error = "PositionIncreaseAtOpenInterestCap";
1262 let code = HyperliquidRejectCode::from_api_error(error);
1263 assert_eq!(
1264 code,
1265 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
1266 );
1267 }
1268
1269 #[rstest]
1270 fn test_parse_position_flip_at_oi_cap() {
1271 let error = "PositionFlipAtOpenInterestCap";
1272 let code = HyperliquidRejectCode::from_api_error(error);
1273 assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
1274 }
1275
1276 #[rstest]
1277 fn test_parse_too_aggressive_at_oi_cap() {
1278 let error = "TooAggressiveAtOpenInterestCap";
1279 let code = HyperliquidRejectCode::from_api_error(error);
1280 assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
1281 }
1282
1283 #[rstest]
1284 fn test_parse_open_interest_increase() {
1285 let error = "OpenInterestIncrease";
1286 let code = HyperliquidRejectCode::from_api_error(error);
1287 assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
1288 }
1289
1290 #[rstest]
1291 fn test_parse_insufficient_spot_balance() {
1292 let error = "Insufficient spot balance";
1293 let code = HyperliquidRejectCode::from_api_error(error);
1294 assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
1295 }
1296
1297 #[rstest]
1298 fn test_parse_oracle_error() {
1299 let error = "Oracle price unavailable";
1300 let code = HyperliquidRejectCode::from_api_error(error);
1301 assert_eq!(code, HyperliquidRejectCode::Oracle);
1302 }
1303
1304 #[rstest]
1305 fn test_parse_max_position() {
1306 let error = "Exceeds max position size";
1307 let code = HyperliquidRejectCode::from_api_error(error);
1308 assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
1309 }
1310
1311 #[rstest]
1312 fn test_parse_missing_order() {
1313 let error = "MissingOrder";
1314 let code = HyperliquidRejectCode::from_api_error(error);
1315 assert_eq!(code, HyperliquidRejectCode::MissingOrder);
1316 }
1317
1318 #[rstest]
1319 fn test_parse_unknown_error() {
1320 let error = "This is a completely new error message";
1321 let code = HyperliquidRejectCode::from_api_error(error);
1322 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1323
1324 if let HyperliquidRejectCode::Unknown(msg) = code {
1326 assert_eq!(msg, error);
1327 }
1328 }
1329
1330 #[rstest]
1331 fn test_parse_empty_error() {
1332 let error = "";
1333 let code = HyperliquidRejectCode::from_api_error(error);
1334 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1335 }
1336
1337 #[rstest]
1338 fn test_parse_whitespace_only() {
1339 let error = " ";
1340 let code = HyperliquidRejectCode::from_api_error(error);
1341 assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
1342 }
1343
1344 #[rstest]
1345 fn test_normalization_preserves_original_in_unknown() {
1346 let error = " UNKNOWN ERROR MESSAGE ";
1347 let code = HyperliquidRejectCode::from_api_error(error);
1348
1349 if let HyperliquidRejectCode::Unknown(msg) = code {
1351 assert_eq!(msg, error);
1352 } else {
1353 panic!("Expected Unknown variant");
1354 }
1355 }
1356 }
1357
1358 #[rstest]
1359 fn test_conditional_order_type_round_trip() {
1360 assert_eq!(
1361 OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
1362 OrderType::TrailingStopLimit
1363 );
1364
1365 assert_eq!(
1367 HyperliquidConditionalOrderType::from(OrderType::StopMarket),
1368 HyperliquidConditionalOrderType::StopMarket
1369 );
1370 assert_eq!(
1371 HyperliquidConditionalOrderType::from(OrderType::StopLimit),
1372 HyperliquidConditionalOrderType::StopLimit
1373 );
1374 }
1375
1376 #[rstest]
1377 fn test_trailing_offset_type_serialization() {
1378 let price = HyperliquidTrailingOffsetType::Price;
1379 let percentage = HyperliquidTrailingOffsetType::Percentage;
1380 let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
1381
1382 assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
1383 assert_eq!(
1384 serde_json::to_string(&percentage).unwrap(),
1385 r#""percentage""#
1386 );
1387 assert_eq!(
1388 serde_json::to_string(&basis_points).unwrap(),
1389 r#""basispoints""#
1390 );
1391 }
1392
1393 #[rstest]
1394 fn test_conditional_order_type_serialization() {
1395 assert_eq!(
1396 serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
1397 r#""STOP_MARKET""#
1398 );
1399 assert_eq!(
1400 serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
1401 r#""STOP_LIMIT""#
1402 );
1403 assert_eq!(
1404 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
1405 r#""TAKE_PROFIT_MARKET""#
1406 );
1407 assert_eq!(
1408 serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
1409 r#""TAKE_PROFIT_LIMIT""#
1410 );
1411 assert_eq!(
1412 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
1413 r#""TRAILING_STOP_MARKET""#
1414 );
1415 assert_eq!(
1416 serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
1417 r#""TRAILING_STOP_LIMIT""#
1418 );
1419 }
1420
1421 #[rstest]
1422 fn test_order_type_enum_coverage() {
1423 let conditional_types = vec![
1425 HyperliquidConditionalOrderType::StopMarket,
1426 HyperliquidConditionalOrderType::StopLimit,
1427 HyperliquidConditionalOrderType::TakeProfitMarket,
1428 HyperliquidConditionalOrderType::TakeProfitLimit,
1429 HyperliquidConditionalOrderType::TrailingStopMarket,
1430 HyperliquidConditionalOrderType::TrailingStopLimit,
1431 ];
1432
1433 for cond_type in conditional_types {
1434 let order_type = OrderType::from(cond_type);
1435 let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
1436 assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
1437 }
1438 }
1439
1440 #[rstest]
1441 fn test_all_trigger_price_types() {
1442 let trigger_types = vec![
1443 HyperliquidTriggerPriceType::Last,
1444 HyperliquidTriggerPriceType::Mark,
1445 HyperliquidTriggerPriceType::Oracle,
1446 ];
1447
1448 for trigger_type in trigger_types {
1449 let nautilus_type = TriggerType::from(trigger_type);
1450 let back_to_hl = HyperliquidTriggerPriceType::from(nautilus_type);
1451 assert_eq!(trigger_type, back_to_hl, "Trigger type roundtrip failed");
1452 }
1453 }
1454}