1use anyhow::Context;
22use nautilus_model::{
23 enums::{OrderType, TimeInForce},
24 identifiers::{InstrumentId, Symbol},
25};
26use rust_decimal::Decimal;
27
28use super::{
29 consts::HYPERLIQUID_VENUE,
30 enums::{
31 HyperliquidConditionalOrderType, HyperliquidOrderType, HyperliquidTimeInForce,
32 HyperliquidTpSl,
33 },
34 parse::{format_outcome_nautilus_symbol, parse_outcome_nautilus_symbol, parse_outcome_symbol},
35 types::HyperliquidAssetId,
36};
37
38pub fn outcome_asset_id_to_coin(asset_id: HyperliquidAssetId) -> anyhow::Result<String> {
44 let encoding = outcome_encoding(asset_id)?;
45 Ok(format!("#{encoding}"))
46}
47
48pub fn outcome_asset_id_to_token(asset_id: HyperliquidAssetId) -> anyhow::Result<String> {
54 let encoding = outcome_encoding(asset_id)?;
55 Ok(format!("+{encoding}"))
56}
57
58pub fn outcome_asset_id_to_instrument_id(
70 asset_id: HyperliquidAssetId,
71) -> anyhow::Result<InstrumentId> {
72 let encoding = outcome_encoding(asset_id)?;
73 let outcome_index = encoding / 10;
74 let side = u8::try_from(encoding % 10).unwrap_or(0);
75 let symbol = format_outcome_nautilus_symbol(outcome_index, side);
76 Ok(InstrumentId::new(Symbol::new(symbol), *HYPERLIQUID_VENUE))
77}
78
79pub fn outcome_asset_id_from_instrument_id(
89 instrument_id: InstrumentId,
90) -> anyhow::Result<HyperliquidAssetId> {
91 let symbol = instrument_id.symbol.as_str();
92
93 if let Some((outcome_index, side)) = parse_outcome_nautilus_symbol(symbol) {
94 return Ok(HyperliquidAssetId::outcome(outcome_index, side));
95 }
96
97 parse_outcome_symbol(symbol)
98}
99
100fn outcome_encoding(asset_id: HyperliquidAssetId) -> anyhow::Result<u32> {
101 asset_id
102 .outcome_encoding()
103 .with_context(|| format!("Invalid Hyperliquid outcome asset ID: {asset_id}"))
104}
105
106pub fn nautilus_order_type_to_hyperliquid(
113 order_type: OrderType,
114 time_in_force: Option<TimeInForce>,
115 trigger_price: Option<Decimal>,
116) -> anyhow::Result<HyperliquidOrderType> {
117 let result = match order_type {
118 OrderType::Limit => {
120 let tif = match time_in_force {
121 Some(t) => nautilus_time_in_force_to_hyperliquid(t)?,
122 None => HyperliquidTimeInForce::Gtc,
123 };
124 HyperliquidOrderType::Limit { tif }
125 }
126
127 OrderType::StopMarket => {
129 let trigger_px = trigger_price
130 .context("Trigger price required for StopMarket order")?
131 .to_string();
132 HyperliquidOrderType::Trigger {
133 is_market: true,
134 trigger_px,
135 tpsl: HyperliquidTpSl::Sl,
136 }
137 }
138
139 OrderType::StopLimit => {
141 let trigger_px = trigger_price
142 .context("Trigger price required for StopLimit order")?
143 .to_string();
144 HyperliquidOrderType::Trigger {
145 is_market: false,
146 trigger_px,
147 tpsl: HyperliquidTpSl::Sl,
148 }
149 }
150
151 OrderType::MarketIfTouched => {
153 let trigger_px = trigger_price
154 .context("Trigger price required for MarketIfTouched order")?
155 .to_string();
156 HyperliquidOrderType::Trigger {
157 is_market: true,
158 trigger_px,
159 tpsl: HyperliquidTpSl::Tp,
160 }
161 }
162
163 OrderType::LimitIfTouched => {
165 let trigger_px = trigger_price
166 .context("Trigger price required for LimitIfTouched order")?
167 .to_string();
168 HyperliquidOrderType::Trigger {
169 is_market: false,
170 trigger_px,
171 tpsl: HyperliquidTpSl::Tp,
172 }
173 }
174
175 OrderType::TrailingStopMarket => {
177 let trigger_px = trigger_price
178 .context("Trigger price required for TrailingStopMarket order")?
179 .to_string();
180 HyperliquidOrderType::Trigger {
181 is_market: true,
182 trigger_px,
183 tpsl: HyperliquidTpSl::Sl,
184 }
185 }
186
187 OrderType::TrailingStopLimit => {
189 let trigger_px = trigger_price
190 .context("Trigger price required for TrailingStopLimit order")?
191 .to_string();
192 HyperliquidOrderType::Trigger {
193 is_market: false,
194 trigger_px,
195 tpsl: HyperliquidTpSl::Sl,
196 }
197 }
198
199 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
200 };
201
202 Ok(result)
203}
204
205pub fn hyperliquid_order_type_to_nautilus(hl_order_type: &HyperliquidOrderType) -> OrderType {
207 match hl_order_type {
208 HyperliquidOrderType::Limit { .. } => OrderType::Limit,
209 HyperliquidOrderType::Trigger {
210 is_market, tpsl, ..
211 } => match (is_market, tpsl) {
212 (true, HyperliquidTpSl::Sl) => OrderType::StopMarket,
213 (false, HyperliquidTpSl::Sl) => OrderType::StopLimit,
214 (true, HyperliquidTpSl::Tp) => OrderType::MarketIfTouched,
215 (false, HyperliquidTpSl::Tp) => OrderType::LimitIfTouched,
216 },
217 }
218}
219
220pub fn hyperliquid_conditional_to_nautilus(
222 conditional_type: HyperliquidConditionalOrderType,
223) -> OrderType {
224 OrderType::from(conditional_type)
225}
226
227pub fn nautilus_to_hyperliquid_conditional(
233 order_type: OrderType,
234) -> HyperliquidConditionalOrderType {
235 HyperliquidConditionalOrderType::from(order_type)
236}
237
238pub fn nautilus_time_in_force_to_hyperliquid(
244 tif: TimeInForce,
245) -> anyhow::Result<HyperliquidTimeInForce> {
246 match tif {
247 TimeInForce::Gtc => Ok(HyperliquidTimeInForce::Gtc),
248 TimeInForce::Ioc => Ok(HyperliquidTimeInForce::Ioc),
249 TimeInForce::Fok => {
250 anyhow::bail!("FOK time in force is not supported by Hyperliquid")
251 }
252 TimeInForce::Gtd => {
253 anyhow::bail!("GTD time in force is not supported by Hyperliquid")
254 }
255 TimeInForce::Day => {
256 anyhow::bail!("DAY time in force is not supported by Hyperliquid")
257 }
258 TimeInForce::AtTheOpen => {
259 anyhow::bail!("AT_THE_OPEN time in force is not supported by Hyperliquid")
260 }
261 TimeInForce::AtTheClose => {
262 anyhow::bail!("AT_THE_CLOSE time in force is not supported by Hyperliquid")
263 }
264 }
265}
266
267pub fn hyperliquid_time_in_force_to_nautilus(hl_tif: HyperliquidTimeInForce) -> TimeInForce {
269 match hl_tif {
270 HyperliquidTimeInForce::Gtc => TimeInForce::Gtc,
271 HyperliquidTimeInForce::Ioc => TimeInForce::Ioc,
272 HyperliquidTimeInForce::Alo => TimeInForce::Gtc, }
274}
275
276pub fn determine_tpsl_type(order_type: OrderType, is_buy: bool) -> HyperliquidTpSl {
288 match order_type {
289 OrderType::StopMarket
290 | OrderType::StopLimit
291 | OrderType::TrailingStopMarket
292 | OrderType::TrailingStopLimit => HyperliquidTpSl::Sl,
293 OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidTpSl::Tp,
294 _ => {
295 if is_buy {
297 HyperliquidTpSl::Sl
298 } else {
299 HyperliquidTpSl::Tp
300 }
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use rstest::rstest;
308
309 use super::*;
310
311 #[rstest]
312 fn test_outcome_asset_id_to_wire_symbols() {
313 let asset_id = HyperliquidAssetId::outcome(1, 0);
314
315 assert_eq!(outcome_asset_id_to_coin(asset_id).unwrap(), "#10");
316 assert_eq!(outcome_asset_id_to_token(asset_id).unwrap(), "+10");
317 }
318
319 #[rstest]
320 fn test_outcome_asset_id_to_wire_symbols_rejects_non_outcome() {
321 let err = outcome_asset_id_to_coin(HyperliquidAssetId::spot(7)).unwrap_err();
322 assert!(
323 err.to_string()
324 .contains("Invalid Hyperliquid outcome asset ID"),
325 "unexpected error: {err}",
326 );
327 }
328
329 #[rstest]
330 fn test_outcome_asset_id_instrument_id_roundtrip() {
331 let asset_id = HyperliquidAssetId::outcome(3, 1);
332 let instrument_id = outcome_asset_id_to_instrument_id(asset_id).unwrap();
333
334 assert_eq!(
335 instrument_id,
336 InstrumentId::from("3-NO-OUTCOME.HYPERLIQUID")
337 );
338 assert_eq!(
339 outcome_asset_id_from_instrument_id(instrument_id).unwrap(),
340 asset_id,
341 );
342 }
343
344 #[rstest]
345 fn test_outcome_asset_id_to_instrument_id_yes_side() {
346 let asset_id = HyperliquidAssetId::outcome(25, 0);
347 let instrument_id = outcome_asset_id_to_instrument_id(asset_id).unwrap();
348
349 assert_eq!(
350 instrument_id,
351 InstrumentId::from("25-YES-OUTCOME.HYPERLIQUID")
352 );
353 }
354
355 #[rstest]
356 #[case("#10.HYPERLIQUID", 1, 0)]
357 #[case("+10.HYPERLIQUID", 1, 0)]
358 #[case("1-YES-OUTCOME.HYPERLIQUID", 1, 0)]
359 #[case("1-NO-OUTCOME.HYPERLIQUID", 1, 1)]
360 fn test_outcome_asset_id_from_instrument_id_accepts_all_forms(
361 #[case] symbol: &str,
362 #[case] outcome_index: u32,
363 #[case] side: u8,
364 ) {
365 let instrument_id = InstrumentId::from(symbol);
366 let asset_id = outcome_asset_id_from_instrument_id(instrument_id).unwrap();
367
368 assert_eq!(asset_id, HyperliquidAssetId::outcome(outcome_index, side));
369 }
370
371 #[rstest]
372 fn test_nautilus_to_hyperliquid_limit_order() {
373 let result =
374 nautilus_order_type_to_hyperliquid(OrderType::Limit, Some(TimeInForce::Gtc), None)
375 .unwrap();
376
377 match result {
378 HyperliquidOrderType::Limit { tif } => {
379 assert_eq!(tif, HyperliquidTimeInForce::Gtc);
380 }
381 _ => panic!("Expected Limit order type"),
382 }
383 }
384
385 #[rstest]
386 fn test_nautilus_to_hyperliquid_stop_market() {
387 let result = nautilus_order_type_to_hyperliquid(
388 OrderType::StopMarket,
389 None,
390 Some(Decimal::new(49000, 0)),
391 )
392 .unwrap();
393
394 match result {
395 HyperliquidOrderType::Trigger {
396 is_market,
397 trigger_px,
398 tpsl,
399 } => {
400 assert!(is_market);
401 assert_eq!(trigger_px, "49000");
402 assert_eq!(tpsl, HyperliquidTpSl::Sl);
403 }
404 _ => panic!("Expected Trigger order type"),
405 }
406 }
407
408 #[rstest]
409 fn test_nautilus_to_hyperliquid_stop_limit() {
410 let result = nautilus_order_type_to_hyperliquid(
411 OrderType::StopLimit,
412 None,
413 Some(Decimal::new(49000, 0)),
414 )
415 .unwrap();
416
417 match result {
418 HyperliquidOrderType::Trigger {
419 is_market,
420 trigger_px,
421 tpsl,
422 } => {
423 assert!(!is_market);
424 assert_eq!(trigger_px, "49000");
425 assert_eq!(tpsl, HyperliquidTpSl::Sl);
426 }
427 _ => panic!("Expected Trigger order type"),
428 }
429 }
430
431 #[rstest]
432 fn test_nautilus_to_hyperliquid_take_profit_market() {
433 let result = nautilus_order_type_to_hyperliquid(
434 OrderType::MarketIfTouched,
435 None,
436 Some(Decimal::new(51000, 0)),
437 )
438 .unwrap();
439
440 match result {
441 HyperliquidOrderType::Trigger {
442 is_market,
443 trigger_px,
444 tpsl,
445 } => {
446 assert!(is_market);
447 assert_eq!(trigger_px, "51000");
448 assert_eq!(tpsl, HyperliquidTpSl::Tp);
449 }
450 _ => panic!("Expected Trigger order type"),
451 }
452 }
453
454 #[rstest]
455 fn test_nautilus_to_hyperliquid_take_profit_limit() {
456 let result = nautilus_order_type_to_hyperliquid(
457 OrderType::LimitIfTouched,
458 None,
459 Some(Decimal::new(51000, 0)),
460 )
461 .unwrap();
462
463 match result {
464 HyperliquidOrderType::Trigger {
465 is_market,
466 trigger_px,
467 tpsl,
468 } => {
469 assert!(!is_market);
470 assert_eq!(trigger_px, "51000");
471 assert_eq!(tpsl, HyperliquidTpSl::Tp);
472 }
473 _ => panic!("Expected Trigger order type"),
474 }
475 }
476
477 #[rstest]
478 fn test_hyperliquid_to_nautilus_limit() {
479 let hl_order = HyperliquidOrderType::Limit {
480 tif: HyperliquidTimeInForce::Gtc,
481 };
482 assert_eq!(
483 hyperliquid_order_type_to_nautilus(&hl_order),
484 OrderType::Limit
485 );
486 }
487
488 #[rstest]
489 fn test_hyperliquid_to_nautilus_stop_market() {
490 let hl_order = HyperliquidOrderType::Trigger {
491 is_market: true,
492 trigger_px: "49000".to_string(),
493 tpsl: HyperliquidTpSl::Sl,
494 };
495 assert_eq!(
496 hyperliquid_order_type_to_nautilus(&hl_order),
497 OrderType::StopMarket
498 );
499 }
500
501 #[rstest]
502 fn test_hyperliquid_to_nautilus_stop_limit() {
503 let hl_order = HyperliquidOrderType::Trigger {
504 is_market: false,
505 trigger_px: "49000".to_string(),
506 tpsl: HyperliquidTpSl::Sl,
507 };
508 assert_eq!(
509 hyperliquid_order_type_to_nautilus(&hl_order),
510 OrderType::StopLimit
511 );
512 }
513
514 #[rstest]
515 fn test_hyperliquid_to_nautilus_take_profit_market() {
516 let hl_order = HyperliquidOrderType::Trigger {
517 is_market: true,
518 trigger_px: "51000".to_string(),
519 tpsl: HyperliquidTpSl::Tp,
520 };
521 assert_eq!(
522 hyperliquid_order_type_to_nautilus(&hl_order),
523 OrderType::MarketIfTouched
524 );
525 }
526
527 #[rstest]
528 fn test_hyperliquid_to_nautilus_take_profit_limit() {
529 let hl_order = HyperliquidOrderType::Trigger {
530 is_market: false,
531 trigger_px: "51000".to_string(),
532 tpsl: HyperliquidTpSl::Tp,
533 };
534 assert_eq!(
535 hyperliquid_order_type_to_nautilus(&hl_order),
536 OrderType::LimitIfTouched
537 );
538 }
539
540 #[rstest]
541 fn test_time_in_force_conversions() {
542 assert_eq!(
544 nautilus_time_in_force_to_hyperliquid(TimeInForce::Gtc).unwrap(),
545 HyperliquidTimeInForce::Gtc
546 );
547 assert_eq!(
548 nautilus_time_in_force_to_hyperliquid(TimeInForce::Ioc).unwrap(),
549 HyperliquidTimeInForce::Ioc
550 );
551
552 assert_eq!(
554 hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Gtc),
555 TimeInForce::Gtc
556 );
557 assert_eq!(
558 hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Ioc),
559 TimeInForce::Ioc
560 );
561 assert_eq!(
562 hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Alo),
563 TimeInForce::Gtc
564 );
565 }
566
567 #[rstest]
568 #[case(TimeInForce::Fok, "FOK")]
569 #[case(TimeInForce::Gtd, "GTD")]
570 #[case(TimeInForce::Day, "DAY")]
571 #[case(TimeInForce::AtTheOpen, "AT_THE_OPEN")]
572 #[case(TimeInForce::AtTheClose, "AT_THE_CLOSE")]
573 fn test_unsupported_time_in_force_returns_error(#[case] tif: TimeInForce, #[case] name: &str) {
574 let result = nautilus_time_in_force_to_hyperliquid(tif);
575 assert!(result.is_err());
576 assert!(
577 result
578 .unwrap_err()
579 .to_string()
580 .contains(&format!("{name} time in force is not supported"))
581 );
582 }
583
584 #[rstest]
585 fn test_conditional_order_type_conversions() {
586 assert_eq!(
588 hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopMarket),
589 OrderType::StopMarket
590 );
591 assert_eq!(
592 hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopLimit),
593 OrderType::StopLimit
594 );
595 assert_eq!(
596 hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitMarket),
597 OrderType::MarketIfTouched
598 );
599 assert_eq!(
600 hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitLimit),
601 OrderType::LimitIfTouched
602 );
603
604 assert_eq!(
606 nautilus_to_hyperliquid_conditional(OrderType::StopMarket),
607 HyperliquidConditionalOrderType::StopMarket
608 );
609 assert_eq!(
610 nautilus_to_hyperliquid_conditional(OrderType::StopLimit),
611 HyperliquidConditionalOrderType::StopLimit
612 );
613 assert_eq!(
614 nautilus_to_hyperliquid_conditional(OrderType::MarketIfTouched),
615 HyperliquidConditionalOrderType::TakeProfitMarket
616 );
617 assert_eq!(
618 nautilus_to_hyperliquid_conditional(OrderType::LimitIfTouched),
619 HyperliquidConditionalOrderType::TakeProfitLimit
620 );
621 }
622
623 #[rstest]
624 fn test_determine_tpsl_type() {
625 assert_eq!(
627 determine_tpsl_type(OrderType::StopMarket, true),
628 HyperliquidTpSl::Sl
629 );
630 assert_eq!(
631 determine_tpsl_type(OrderType::StopLimit, false),
632 HyperliquidTpSl::Sl
633 );
634
635 assert_eq!(
637 determine_tpsl_type(OrderType::MarketIfTouched, true),
638 HyperliquidTpSl::Tp
639 );
640 assert_eq!(
641 determine_tpsl_type(OrderType::LimitIfTouched, false),
642 HyperliquidTpSl::Tp
643 );
644
645 assert_eq!(
647 determine_tpsl_type(OrderType::TrailingStopMarket, true),
648 HyperliquidTpSl::Sl
649 );
650 assert_eq!(
651 determine_tpsl_type(OrderType::TrailingStopLimit, false),
652 HyperliquidTpSl::Sl
653 );
654 }
655}