1use nautilus_model::enums::{AggressorSide, OrderSide};
17use serde::{Deserialize, Serialize};
18use strum::{AsRefStr, Display, EnumIter, EnumString};
19
20#[derive(
24 Copy,
25 Clone,
26 Debug,
27 Display,
28 PartialEq,
29 Eq,
30 Hash,
31 AsRefStr,
32 EnumIter,
33 EnumString,
34 Serialize,
35 Deserialize,
36)]
37#[serde(rename_all = "UPPERCASE")]
38#[strum(serialize_all = "UPPERCASE")]
39pub enum HyperliquidSide {
40 #[serde(rename = "B")]
41 Buy,
42 #[serde(rename = "A")]
43 Sell,
44}
45
46impl From<OrderSide> for HyperliquidSide {
47 fn from(value: OrderSide) -> Self {
48 match value {
49 OrderSide::Buy => Self::Buy,
50 OrderSide::Sell => Self::Sell,
51 _ => panic!("Invalid `OrderSide`: {value:?}"),
52 }
53 }
54}
55
56impl From<HyperliquidSide> for OrderSide {
57 fn from(value: HyperliquidSide) -> Self {
58 match value {
59 HyperliquidSide::Buy => Self::Buy,
60 HyperliquidSide::Sell => Self::Sell,
61 }
62 }
63}
64
65impl From<HyperliquidSide> for AggressorSide {
66 fn from(value: HyperliquidSide) -> Self {
67 match value {
68 HyperliquidSide::Buy => Self::Buyer,
69 HyperliquidSide::Sell => Self::Seller,
70 }
71 }
72}
73
74#[derive(
76 Copy,
77 Clone,
78 Debug,
79 Display,
80 PartialEq,
81 Eq,
82 Hash,
83 AsRefStr,
84 EnumIter,
85 EnumString,
86 Serialize,
87 Deserialize,
88)]
89#[serde(rename_all = "PascalCase")]
90#[strum(serialize_all = "PascalCase")]
91pub enum HyperliquidTimeInForce {
92 Alo,
94 Ioc,
96 Gtc,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
102#[serde(tag = "type", rename_all = "lowercase")]
103pub enum HyperliquidOrderType {
104 #[serde(rename = "limit")]
106 Limit { tif: HyperliquidTimeInForce },
107
108 #[serde(rename = "trigger")]
110 Trigger {
111 #[serde(rename = "isMarket")]
112 is_market: bool,
113 #[serde(rename = "triggerPx")]
114 trigger_px: String,
115 tpsl: HyperliquidTpSl,
116 },
117}
118
119#[derive(
121 Copy,
122 Clone,
123 Debug,
124 Display,
125 PartialEq,
126 Eq,
127 Hash,
128 AsRefStr,
129 EnumIter,
130 EnumString,
131 Serialize,
132 Deserialize,
133)]
134#[serde(rename_all = "lowercase")]
135#[strum(serialize_all = "lowercase")]
136pub enum HyperliquidTpSl {
137 Tp,
139 Sl,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(transparent)]
146pub struct HyperliquidReduceOnly(pub bool);
147
148impl HyperliquidReduceOnly {
149 pub fn new(reduce_only: bool) -> Self {
151 Self(reduce_only)
152 }
153
154 pub fn is_reduce_only(&self) -> bool {
156 self.0
157 }
158}
159
160#[derive(
162 Copy,
163 Clone,
164 Debug,
165 Display,
166 PartialEq,
167 Eq,
168 Hash,
169 AsRefStr,
170 EnumIter,
171 EnumString,
172 Serialize,
173 Deserialize,
174)]
175#[serde(rename_all = "lowercase")]
176#[strum(serialize_all = "lowercase")]
177pub enum HyperliquidLiquidityFlag {
178 Maker,
179 Taker,
180}
181
182impl From<bool> for HyperliquidLiquidityFlag {
183 fn from(crossed: bool) -> Self {
187 if crossed {
188 HyperliquidLiquidityFlag::Taker
189 } else {
190 HyperliquidLiquidityFlag::Maker
191 }
192 }
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum HyperliquidRejectCode {
198 Tick,
200 MinTradeNtl,
202 MinTradeSpotNtl,
204 PerpMargin,
206 ReduceOnly,
208 BadAloPx,
210 IocCancel,
212 BadTriggerPx,
214 MarketOrderNoLiquidity,
216 PositionIncreaseAtOpenInterestCap,
218 PositionFlipAtOpenInterestCap,
220 TooAggressiveAtOpenInterestCap,
222 OpenInterestIncrease,
224 InsufficientSpotBalance,
226 Oracle,
228 PerpMaxPosition,
230 MissingOrder,
232 Unknown(String),
234}
235
236impl HyperliquidRejectCode {
237 pub fn from_api_error(error_message: &str) -> Self {
238 Self::from_error_string_internal(error_message)
248 }
249
250 fn from_error_string_internal(error: &str) -> Self {
255 match error {
256 s if s.contains("tick size") => HyperliquidRejectCode::Tick,
257 s if s.contains("minimum value of $10") => HyperliquidRejectCode::MinTradeNtl,
258 s if s.contains("minimum value of 10") => HyperliquidRejectCode::MinTradeSpotNtl,
259 s if s.contains("Insufficient margin") => HyperliquidRejectCode::PerpMargin,
260 s if s.contains("Reduce only order would increase") => {
261 HyperliquidRejectCode::ReduceOnly
262 }
263 s if s.contains("Post only order would have immediately matched") => {
264 HyperliquidRejectCode::BadAloPx
265 }
266 s if s.contains("could not immediately match") => HyperliquidRejectCode::IocCancel,
267 s if s.contains("Invalid TP/SL price") => HyperliquidRejectCode::BadTriggerPx,
268 s if s.contains("No liquidity available for market order") => {
269 HyperliquidRejectCode::MarketOrderNoLiquidity
270 }
271 s if s.contains("PositionIncreaseAtOpenInterestCap") => {
272 HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
273 }
274 s if s.contains("PositionFlipAtOpenInterestCap") => {
275 HyperliquidRejectCode::PositionFlipAtOpenInterestCap
276 }
277 s if s.contains("TooAggressiveAtOpenInterestCap") => {
278 HyperliquidRejectCode::TooAggressiveAtOpenInterestCap
279 }
280 s if s.contains("OpenInterestIncrease") => HyperliquidRejectCode::OpenInterestIncrease,
281 s if s.contains("Insufficient spot balance") => {
282 HyperliquidRejectCode::InsufficientSpotBalance
283 }
284 s if s.contains("Oracle") => HyperliquidRejectCode::Oracle,
285 s if s.contains("max position") => HyperliquidRejectCode::PerpMaxPosition,
286 s if s.contains("MissingOrder") => HyperliquidRejectCode::MissingOrder,
287 s => HyperliquidRejectCode::Unknown(s.to_string()),
288 }
289 }
290
291 #[deprecated(
296 since = "0.50.0",
297 note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
298 )]
299 pub fn from_error_string(error: &str) -> Self {
300 Self::from_error_string_internal(error)
301 }
302}
303
304#[cfg(test)]
309mod tests {
310 use rstest::rstest;
311 use serde_json;
312
313 use super::*;
314
315 #[rstest]
316 fn test_side_serde() {
317 let buy_side = HyperliquidSide::Buy;
318 let sell_side = HyperliquidSide::Sell;
319
320 assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
321 assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
322
323 assert_eq!(
324 serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
325 HyperliquidSide::Buy
326 );
327 assert_eq!(
328 serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
329 HyperliquidSide::Sell
330 );
331 }
332
333 #[rstest]
334 fn test_side_from_order_side() {
335 assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
337 assert_eq!(
338 HyperliquidSide::from(OrderSide::Sell),
339 HyperliquidSide::Sell
340 );
341 }
342
343 #[rstest]
344 fn test_order_side_from_hyperliquid_side() {
345 assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
347 assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
348 }
349
350 #[rstest]
351 fn test_aggressor_side_from_hyperliquid_side() {
352 assert_eq!(
354 AggressorSide::from(HyperliquidSide::Buy),
355 AggressorSide::Buyer
356 );
357 assert_eq!(
358 AggressorSide::from(HyperliquidSide::Sell),
359 AggressorSide::Seller
360 );
361 }
362
363 #[rstest]
364 fn test_time_in_force_serde() {
365 let test_cases = [
366 (HyperliquidTimeInForce::Alo, "\"Alo\""),
367 (HyperliquidTimeInForce::Ioc, "\"Ioc\""),
368 (HyperliquidTimeInForce::Gtc, "\"Gtc\""),
369 ];
370
371 for (tif, expected_json) in test_cases {
372 assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
373 assert_eq!(
374 serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
375 tif
376 );
377 }
378 }
379
380 #[rstest]
381 fn test_liquidity_flag_from_crossed() {
382 assert_eq!(
383 HyperliquidLiquidityFlag::from(true),
384 HyperliquidLiquidityFlag::Taker
385 );
386 assert_eq!(
387 HyperliquidLiquidityFlag::from(false),
388 HyperliquidLiquidityFlag::Maker
389 );
390 }
391
392 #[rstest]
393 #[allow(deprecated)]
394 fn test_reject_code_from_error_string() {
395 let test_cases = [
396 (
397 "Price must be divisible by tick size.",
398 HyperliquidRejectCode::Tick,
399 ),
400 (
401 "Order must have minimum value of $10.",
402 HyperliquidRejectCode::MinTradeNtl,
403 ),
404 (
405 "Insufficient margin to place order.",
406 HyperliquidRejectCode::PerpMargin,
407 ),
408 (
409 "Post only order would have immediately matched, bbo was 1.23",
410 HyperliquidRejectCode::BadAloPx,
411 ),
412 (
413 "Some unknown error",
414 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
415 ),
416 ];
417
418 for (error_str, expected_code) in test_cases {
419 assert_eq!(
420 HyperliquidRejectCode::from_error_string(error_str),
421 expected_code
422 );
423 }
424 }
425
426 #[rstest]
427 fn test_reject_code_from_api_error() {
428 let test_cases = [
429 (
430 "Price must be divisible by tick size.",
431 HyperliquidRejectCode::Tick,
432 ),
433 (
434 "Order must have minimum value of $10.",
435 HyperliquidRejectCode::MinTradeNtl,
436 ),
437 (
438 "Insufficient margin to place order.",
439 HyperliquidRejectCode::PerpMargin,
440 ),
441 (
442 "Post only order would have immediately matched, bbo was 1.23",
443 HyperliquidRejectCode::BadAloPx,
444 ),
445 (
446 "Some unknown error",
447 HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
448 ),
449 ];
450
451 for (error_str, expected_code) in test_cases {
452 assert_eq!(
453 HyperliquidRejectCode::from_api_error(error_str),
454 expected_code
455 );
456 }
457 }
458
459 #[rstest]
460 fn test_reduce_only() {
461 let reduce_only = HyperliquidReduceOnly::new(true);
462
463 assert!(reduce_only.is_reduce_only());
464
465 let json = serde_json::to_string(&reduce_only).unwrap();
466 assert_eq!(json, "true");
467
468 let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
469 assert_eq!(parsed, reduce_only);
470 }
471}