1use anyhow::Context;
62use nautilus_core::UnixNanos;
63pub use nautilus_core::serialization::{
64 deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
65 deserialize_vec_decimal_from_str, serialize_decimal_as_str, serialize_optional_decimal_as_str,
66 serialize_vec_decimal_as_str,
67};
68use nautilus_model::{
69 data::bar::BarType,
70 enums::{AggregationSource, BarAggregation, OrderSide, OrderStatus, OrderType, TimeInForce},
71 identifiers::{ClientOrderId, InstrumentId, Symbol, TradeId, Venue},
72 orders::{Order, any::OrderAny},
73 types::{AccountBalance, Currency, MarginBalance, Money},
74};
75use rust_decimal::Decimal;
76
77use crate::{
78 common::enums::{
79 HyperliquidBarInterval::{self, *},
80 HyperliquidOrderStatus, HyperliquidTpSl,
81 },
82 http::models::{
83 Cloid, CrossMarginSummary, HyperliquidExchangeResponse,
84 HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams, HyperliquidExecOrderKind,
85 HyperliquidExecPlaceOrderRequest, HyperliquidExecTif, HyperliquidExecTpSl,
86 HyperliquidExecTriggerParams, RESPONSE_STATUS_OK,
87 },
88 websocket::messages::TrailingOffsetType,
89};
90
91pub fn make_fill_trade_id(
99 hash: &str,
100 oid: u64,
101 px: &str,
102 sz: &str,
103 time: u64,
104 start_position: &str,
105) -> TradeId {
106 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
108 for &b in hash.as_bytes() {
109 h ^= b as u64;
110 h = h.wrapping_mul(0x0100_0000_01b3);
111 }
112 for b in oid.to_le_bytes() {
113 h ^= b as u64;
114 h = h.wrapping_mul(0x0100_0000_01b3);
115 }
116 for &b in px.as_bytes() {
117 h ^= b as u64;
118 h = h.wrapping_mul(0x0100_0000_01b3);
119 }
120 for &b in sz.as_bytes() {
121 h ^= b as u64;
122 h = h.wrapping_mul(0x0100_0000_01b3);
123 }
124 for b in time.to_le_bytes() {
125 h ^= b as u64;
126 h = h.wrapping_mul(0x0100_0000_01b3);
127 }
128 for &b in start_position.as_bytes() {
129 h ^= b as u64;
130 h = h.wrapping_mul(0x0100_0000_01b3);
131 }
132 TradeId::new(format!("{h:016x}-{oid:016x}"))
133}
134
135#[inline]
137pub fn round_down_to_tick(price: Decimal, tick_size: Decimal) -> Decimal {
138 if tick_size.is_zero() {
139 return price;
140 }
141 (price / tick_size).floor() * tick_size
142}
143
144#[inline]
146pub fn round_down_to_step(qty: Decimal, step_size: Decimal) -> Decimal {
147 if step_size.is_zero() {
148 return qty;
149 }
150 (qty / step_size).floor() * step_size
151}
152
153#[inline]
155pub fn ensure_min_notional(
156 price: Decimal,
157 qty: Decimal,
158 min_notional: Decimal,
159) -> Result<(), String> {
160 let notional = price * qty;
161 if notional < min_notional {
162 Err(format!(
163 "Notional value {notional} is less than minimum required {min_notional}"
164 ))
165 } else {
166 Ok(())
167 }
168}
169
170pub fn round_to_sig_figs(value: Decimal, sig_figs: u32) -> Decimal {
173 if value.is_zero() {
174 return Decimal::ZERO;
175 }
176
177 let abs_val = value.abs();
179 let float_val: f64 = abs_val.to_string().parse().unwrap_or(0.0);
180 let magnitude = float_val.log10().floor() as i32;
181
182 let shift = sig_figs as i32 - 1 - magnitude;
184 let factor = Decimal::from(10_i64.pow(shift.unsigned_abs()));
185
186 if shift >= 0 {
187 (value * factor).round() / factor
188 } else {
189 (value / factor).round() * factor
190 }
191}
192
193pub fn normalize_price(price: Decimal, decimals: u8) -> Decimal {
195 let sig_fig_price = round_to_sig_figs(price, 5);
197 let scale = Decimal::from(10_u64.pow(decimals as u32));
199 (sig_fig_price * scale).floor() / scale
200}
201
202pub fn normalize_quantity(qty: Decimal, decimals: u8) -> Decimal {
204 let scale = Decimal::from(10_u64.pow(decimals as u32));
205 (qty * scale).floor() / scale
206}
207
208pub fn normalize_order(
210 price: Decimal,
211 qty: Decimal,
212 tick_size: Decimal,
213 step_size: Decimal,
214 min_notional: Decimal,
215 price_decimals: u8,
216 size_decimals: u8,
217) -> Result<(Decimal, Decimal), String> {
218 let normalized_price = normalize_price(price, price_decimals);
220 let normalized_qty = normalize_quantity(qty, size_decimals);
221
222 let final_price = round_down_to_tick(normalized_price, tick_size);
224 let final_qty = round_down_to_step(normalized_qty, step_size);
225
226 ensure_min_notional(final_price, final_qty, min_notional)?;
228
229 Ok((final_price, final_qty))
230}
231
232#[inline]
234pub fn millis_to_nanos(millis: u64) -> anyhow::Result<UnixNanos> {
235 let value = nautilus_core::datetime::millis_to_nanos(millis as f64)?;
236 Ok(UnixNanos::from(value))
237}
238
239pub fn time_in_force_to_hyperliquid_tif(
245 tif: TimeInForce,
246 is_post_only: bool,
247) -> anyhow::Result<HyperliquidExecTif> {
248 match (tif, is_post_only) {
249 (_, true) => Ok(HyperliquidExecTif::Alo), (TimeInForce::Gtc, false) => Ok(HyperliquidExecTif::Gtc),
251 (TimeInForce::Ioc, false) => Ok(HyperliquidExecTif::Ioc),
252 (TimeInForce::Fok, false) => {
253 anyhow::bail!("FOK time in force is not supported by Hyperliquid")
254 }
255 _ => anyhow::bail!("Unsupported time in force for Hyperliquid: {tif:?}"),
256 }
257}
258
259fn determine_tpsl_type(
260 order_type: OrderType,
261 order_side: OrderSide,
262 trigger_price: Decimal,
263 current_price: Option<Decimal>,
264) -> HyperliquidExecTpSl {
265 match order_type {
266 OrderType::StopMarket | OrderType::StopLimit => HyperliquidExecTpSl::Sl,
268
269 OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidExecTpSl::Tp,
271
272 _ => {
274 if let Some(current) = current_price {
275 match order_side {
276 OrderSide::Buy => {
277 if trigger_price > current {
279 HyperliquidExecTpSl::Sl
280 } else {
281 HyperliquidExecTpSl::Tp
282 }
283 }
284 OrderSide::Sell => {
285 if trigger_price < current {
287 HyperliquidExecTpSl::Sl
288 } else {
289 HyperliquidExecTpSl::Tp
290 }
291 }
292 _ => HyperliquidExecTpSl::Sl, }
294 } else {
295 HyperliquidExecTpSl::Sl
297 }
298 }
299 }
300}
301
302pub fn bar_type_to_interval(bar_type: &BarType) -> anyhow::Result<HyperliquidBarInterval> {
308 let spec = bar_type.spec();
309 let step = spec.step.get();
310
311 anyhow::ensure!(
312 bar_type.aggregation_source() == AggregationSource::External,
313 "Only EXTERNAL aggregation is supported"
314 );
315
316 let interval = match spec.aggregation {
317 BarAggregation::Minute => match step {
318 1 => OneMinute,
319 3 => ThreeMinutes,
320 5 => FiveMinutes,
321 15 => FifteenMinutes,
322 30 => ThirtyMinutes,
323 _ => anyhow::bail!("Unsupported minute step: {step}"),
324 },
325 BarAggregation::Hour => match step {
326 1 => OneHour,
327 2 => TwoHours,
328 4 => FourHours,
329 8 => EightHours,
330 12 => TwelveHours,
331 _ => anyhow::bail!("Unsupported hour step: {step}"),
332 },
333 BarAggregation::Day => match step {
334 1 => OneDay,
335 3 => ThreeDays,
336 _ => anyhow::bail!("Unsupported day step: {step}"),
337 },
338 BarAggregation::Week if step == 1 => OneWeek,
339 BarAggregation::Month if step == 1 => OneMonth,
340 a => anyhow::bail!("Hyperliquid does not support {a:?} aggregation"),
341 };
342
343 Ok(interval)
344}
345
346pub fn order_to_hyperliquid_request_with_asset(
352 order: &OrderAny,
353 asset: u32,
354) -> anyhow::Result<HyperliquidExecPlaceOrderRequest> {
355 let is_buy = matches!(order.order_side(), OrderSide::Buy);
356 let reduce_only = order.is_reduce_only();
357 let order_side = order.order_side();
358 let order_type = order.order_type();
359
360 let price_decimal = match order.price() {
363 Some(price) => Decimal::from_str_exact(&price.to_string())
364 .with_context(|| format!("Failed to convert price to decimal: {price}"))?
365 .normalize(),
366 None => {
367 if matches!(
368 order_type,
369 OrderType::Market | OrderType::StopMarket | OrderType::MarketIfTouched
370 ) {
371 Decimal::ZERO
372 } else {
373 anyhow::bail!("Limit orders require a price")
374 }
375 }
376 };
377
378 let size_decimal = Decimal::from_str_exact(&order.quantity().to_string())
379 .with_context(|| {
380 format!(
381 "Failed to convert quantity to decimal: {}",
382 order.quantity()
383 )
384 })?
385 .normalize();
386
387 let kind = match order_type {
389 OrderType::Market => HyperliquidExecOrderKind::Limit {
390 limit: HyperliquidExecLimitParams {
391 tif: HyperliquidExecTif::Ioc,
392 },
393 },
394 OrderType::Limit => {
395 let tif =
396 time_in_force_to_hyperliquid_tif(order.time_in_force(), order.is_post_only())?;
397 HyperliquidExecOrderKind::Limit {
398 limit: HyperliquidExecLimitParams { tif },
399 }
400 }
401 OrderType::StopMarket => {
402 if let Some(trigger_price) = order.trigger_price() {
403 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
404 .with_context(|| {
405 format!("Failed to convert trigger price to decimal: {trigger_price}")
406 })?
407 .normalize();
408 let tpsl = determine_tpsl_type(order_type, order_side, trigger_price_decimal, None);
409 HyperliquidExecOrderKind::Trigger {
410 trigger: HyperliquidExecTriggerParams {
411 is_market: true,
412 trigger_px: trigger_price_decimal,
413 tpsl,
414 },
415 }
416 } else {
417 anyhow::bail!("Stop market orders require a trigger price")
418 }
419 }
420 OrderType::StopLimit => {
421 if let Some(trigger_price) = order.trigger_price() {
422 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
423 .with_context(|| {
424 format!("Failed to convert trigger price to decimal: {trigger_price}")
425 })?
426 .normalize();
427 let tpsl = determine_tpsl_type(order_type, order_side, trigger_price_decimal, None);
428 HyperliquidExecOrderKind::Trigger {
429 trigger: HyperliquidExecTriggerParams {
430 is_market: false,
431 trigger_px: trigger_price_decimal,
432 tpsl,
433 },
434 }
435 } else {
436 anyhow::bail!("Stop limit orders require a trigger price")
437 }
438 }
439 OrderType::MarketIfTouched => {
440 if let Some(trigger_price) = order.trigger_price() {
441 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
442 .with_context(|| {
443 format!("Failed to convert trigger price to decimal: {trigger_price}")
444 })?
445 .normalize();
446 HyperliquidExecOrderKind::Trigger {
447 trigger: HyperliquidExecTriggerParams {
448 is_market: true,
449 trigger_px: trigger_price_decimal,
450 tpsl: HyperliquidExecTpSl::Tp,
451 },
452 }
453 } else {
454 anyhow::bail!("Market-if-touched orders require a trigger price")
455 }
456 }
457 OrderType::LimitIfTouched => {
458 if let Some(trigger_price) = order.trigger_price() {
459 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
460 .with_context(|| {
461 format!("Failed to convert trigger price to decimal: {trigger_price}")
462 })?
463 .normalize();
464 HyperliquidExecOrderKind::Trigger {
465 trigger: HyperliquidExecTriggerParams {
466 is_market: false,
467 trigger_px: trigger_price_decimal,
468 tpsl: HyperliquidExecTpSl::Tp,
469 },
470 }
471 } else {
472 anyhow::bail!("Limit-if-touched orders require a trigger price")
473 }
474 }
475 _ => anyhow::bail!("Unsupported order type for Hyperliquid: {order_type:?}"),
476 };
477
478 let cloid = Some(Cloid::from_client_order_id(order.client_order_id()));
480
481 Ok(HyperliquidExecPlaceOrderRequest {
482 asset,
483 is_buy,
484 price: price_decimal,
485 size: size_decimal,
486 reduce_only,
487 kind,
488 cloid,
489 })
490}
491
492pub fn client_order_id_to_cancel_request_with_asset(
494 client_order_id: &str,
495 asset: u32,
496) -> HyperliquidExecCancelByCloidRequest {
497 let cloid = Cloid::from_client_order_id(ClientOrderId::from(client_order_id));
498 HyperliquidExecCancelByCloidRequest { asset, cloid }
499}
500
501pub fn extract_error_message(response: &HyperliquidExchangeResponse) -> String {
503 match response {
504 HyperliquidExchangeResponse::Status { status, response } => {
505 if status == RESPONSE_STATUS_OK {
506 "Operation successful".to_string()
507 } else {
508 if let Some(error_msg) = response.get("error").and_then(|v| v.as_str()) {
510 error_msg.to_string()
511 } else {
512 format!("Request failed with status: {status}")
513 }
514 }
515 }
516 HyperliquidExchangeResponse::Error { error } => error.clone(),
517 }
518}
519
520pub fn is_conditional_order_data(trigger_px: Option<&str>, tpsl: Option<&HyperliquidTpSl>) -> bool {
526 trigger_px.is_some() && tpsl.is_some()
527}
528
529pub fn parse_trigger_order_type(is_market: bool, tpsl: &HyperliquidTpSl) -> OrderType {
535 match (is_market, tpsl) {
536 (true, HyperliquidTpSl::Sl) => OrderType::StopMarket,
537 (false, HyperliquidTpSl::Sl) => OrderType::StopLimit,
538 (true, HyperliquidTpSl::Tp) => OrderType::MarketIfTouched,
539 (false, HyperliquidTpSl::Tp) => OrderType::LimitIfTouched,
540 }
541}
542
543pub fn parse_order_status_with_trigger(
549 status: HyperliquidOrderStatus,
550 trigger_activated: Option<bool>,
551) -> (OrderStatus, Option<String>) {
552 let base_status = OrderStatus::from(status);
553
554 if let Some(activated) = trigger_activated {
556 let trigger_status = if activated {
557 Some("activated".to_string())
558 } else {
559 Some("pending".to_string())
560 };
561 (base_status, trigger_status)
562 } else {
563 (base_status, None)
564 }
565}
566
567pub fn format_trailing_stop_info(
569 offset: &str,
570 offset_type: TrailingOffsetType,
571 callback_price: Option<&str>,
572) -> String {
573 let offset_desc = offset_type.format_offset(offset);
574
575 if let Some(callback) = callback_price {
576 format!("Trailing stop: {offset_desc} offset, callback at {callback}")
577 } else {
578 format!("Trailing stop: {offset_desc} offset")
579 }
580}
581
582pub fn validate_conditional_order_params(
588 trigger_px: Option<&str>,
589 tpsl: Option<&HyperliquidTpSl>,
590 is_market: Option<bool>,
591) -> anyhow::Result<()> {
592 if trigger_px.is_none() {
593 anyhow::bail!("Conditional order missing trigger price");
594 }
595
596 if tpsl.is_none() {
597 anyhow::bail!("Conditional order missing tpsl indicator");
598 }
599
600 if is_market.is_none() {
603 anyhow::bail!("Conditional order missing is_market flag");
604 }
605
606 Ok(())
607}
608
609pub fn parse_trigger_price(trigger_px: &str) -> anyhow::Result<Decimal> {
615 Decimal::from_str_exact(trigger_px)
616 .with_context(|| format!("Failed to parse trigger price: {trigger_px}"))
617}
618
619pub fn parse_account_balances_and_margins(
625 cross_margin_summary: &CrossMarginSummary,
626) -> anyhow::Result<(Vec<AccountBalance>, Vec<MarginBalance>)> {
627 let mut balances = Vec::new();
628 let mut margins = Vec::new();
629
630 let currency = Currency::USDC();
631
632 let mut total_value = cross_margin_summary
633 .account_value
634 .to_string()
635 .parse::<f64>()?
636 .max(0.0);
637
638 let free_value = cross_margin_summary
639 .withdrawable
640 .map(|w| w.to_string().parse::<f64>())
641 .transpose()?
642 .unwrap_or(total_value)
643 .max(0.0);
644
645 if free_value > total_value {
647 total_value = free_value;
648 }
649
650 let locked_value = total_value - free_value;
651
652 let total = Money::new(total_value, currency);
653 let locked = Money::new(locked_value, currency);
654 let free = Money::new(free_value, currency);
655
656 let balance = AccountBalance::new(total, locked, free);
657 balances.push(balance);
658
659 let margin_used = cross_margin_summary
660 .total_margin_used
661 .to_string()
662 .parse::<f64>()?;
663
664 if margin_used > 0.0 {
665 let margin_instrument_id =
666 InstrumentId::new(Symbol::new("ACCOUNT"), Venue::new("HYPERLIQUID"));
667
668 let initial_margin = Money::new(margin_used, currency);
669 let maintenance_margin = Money::new(margin_used, currency);
670
671 let margin_balance =
672 MarginBalance::new(initial_margin, maintenance_margin, margin_instrument_id);
673
674 margins.push(margin_balance);
675 }
676
677 Ok((balances, margins))
678}
679
680#[cfg(test)]
681mod tests {
682 use std::str::FromStr;
683
684 use rstest::rstest;
685 use rust_decimal::Decimal;
686 use rust_decimal_macros::dec;
687 use serde::{Deserialize, Serialize};
688
689 use super::*;
690
691 #[derive(Serialize, Deserialize)]
692 struct TestStruct {
693 #[serde(
694 serialize_with = "serialize_decimal_as_str",
695 deserialize_with = "deserialize_decimal_from_str"
696 )]
697 value: Decimal,
698 #[serde(
699 serialize_with = "serialize_optional_decimal_as_str",
700 deserialize_with = "deserialize_optional_decimal_from_str"
701 )]
702 optional_value: Option<Decimal>,
703 }
704
705 #[rstest]
706 fn test_decimal_serialization_roundtrip() {
707 let original = TestStruct {
708 value: Decimal::from_str("123.456789012345678901234567890").unwrap(),
709 optional_value: Some(Decimal::from_str("0.000000001").unwrap()),
710 };
711
712 let json = serde_json::to_string(&original).unwrap();
713 println!("Serialized: {json}");
714
715 assert!(json.contains("\"123.45678901234567890123456789\""));
717 assert!(json.contains("\"0.000000001\""));
718
719 let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
720 assert_eq!(original.value, deserialized.value);
721 assert_eq!(original.optional_value, deserialized.optional_value);
722 }
723
724 #[rstest]
725 fn test_decimal_precision_preservation() {
726 let test_cases = [
727 "0",
728 "1",
729 "0.1",
730 "0.01",
731 "0.001",
732 "123.456789012345678901234567890",
733 "999999999999999999.999999999999999999",
734 ];
735
736 for case in test_cases {
737 let decimal = Decimal::from_str(case).unwrap();
738 let test_struct = TestStruct {
739 value: decimal,
740 optional_value: Some(decimal),
741 };
742
743 let json = serde_json::to_string(&test_struct).unwrap();
744 let parsed: TestStruct = serde_json::from_str(&json).unwrap();
745
746 assert_eq!(decimal, parsed.value, "Failed for case: {case}");
747 assert_eq!(
748 Some(decimal),
749 parsed.optional_value,
750 "Failed for case: {case}"
751 );
752 }
753 }
754
755 #[rstest]
756 fn test_optional_none_handling() {
757 let test_struct = TestStruct {
758 value: Decimal::from_str("42.0").unwrap(),
759 optional_value: None,
760 };
761
762 let json = serde_json::to_string(&test_struct).unwrap();
763 assert!(json.contains("null"));
764
765 let parsed: TestStruct = serde_json::from_str(&json).unwrap();
766 assert_eq!(test_struct.value, parsed.value);
767 assert_eq!(None, parsed.optional_value);
768 }
769
770 #[rstest]
771 fn test_round_down_to_tick() {
772 assert_eq!(round_down_to_tick(dec!(100.07), dec!(0.05)), dec!(100.05));
773 assert_eq!(round_down_to_tick(dec!(100.03), dec!(0.05)), dec!(100.00));
774 assert_eq!(round_down_to_tick(dec!(100.05), dec!(0.05)), dec!(100.05));
775
776 assert_eq!(round_down_to_tick(dec!(100.07), dec!(0)), dec!(100.07));
778 }
779
780 #[rstest]
781 fn test_round_down_to_step() {
782 assert_eq!(
783 round_down_to_step(dec!(0.12349), dec!(0.0001)),
784 dec!(0.1234)
785 );
786 assert_eq!(round_down_to_step(dec!(1.5555), dec!(0.1)), dec!(1.5));
787 assert_eq!(round_down_to_step(dec!(1.0001), dec!(0.0001)), dec!(1.0001));
788
789 assert_eq!(round_down_to_step(dec!(0.12349), dec!(0)), dec!(0.12349));
791 }
792
793 #[rstest]
794 fn test_min_notional_validation() {
795 assert!(ensure_min_notional(dec!(100), dec!(0.1), dec!(10)).is_ok());
797 assert!(ensure_min_notional(dec!(100), dec!(0.11), dec!(10)).is_ok());
798
799 assert!(ensure_min_notional(dec!(100), dec!(0.05), dec!(10)).is_err());
801 assert!(ensure_min_notional(dec!(1), dec!(5), dec!(10)).is_err());
802
803 assert!(ensure_min_notional(dec!(100), dec!(0.1), dec!(10)).is_ok());
805 }
806
807 #[rstest]
808 fn test_round_to_sig_figs() {
809 assert_eq!(round_to_sig_figs(dec!(104567.3), 5), dec!(104570));
811 assert_eq!(round_to_sig_figs(dec!(104522.5), 5), dec!(104520));
812 assert_eq!(round_to_sig_figs(dec!(99999.9), 5), dec!(100000));
813
814 assert_eq!(round_to_sig_figs(dec!(1234.5), 5), dec!(1234.5));
816 assert_eq!(round_to_sig_figs(dec!(0.12345), 5), dec!(0.12345));
817 assert_eq!(round_to_sig_figs(dec!(0.123456), 5), dec!(0.12346));
818
819 assert_eq!(round_to_sig_figs(dec!(0.000123456), 5), dec!(0.00012346));
821 assert_eq!(round_to_sig_figs(dec!(0.000999999), 5), dec!(0.0010000)); assert_eq!(round_to_sig_figs(dec!(0), 5), dec!(0));
825 }
826
827 #[rstest]
828 fn test_normalize_price() {
829 assert_eq!(normalize_price(dec!(100.12345), 2), dec!(100.12));
831 assert_eq!(normalize_price(dec!(100.19999), 2), dec!(100.2)); assert_eq!(normalize_price(dec!(100.999), 0), dec!(101)); assert_eq!(normalize_price(dec!(100.12345), 4), dec!(100.12)); assert_eq!(normalize_price(dec!(104567.3), 1), dec!(104570));
837 }
838
839 #[rstest]
840 fn test_normalize_quantity() {
841 assert_eq!(normalize_quantity(dec!(1.12345), 3), dec!(1.123));
842 assert_eq!(normalize_quantity(dec!(1.99999), 3), dec!(1.999));
843 assert_eq!(normalize_quantity(dec!(1.999), 0), dec!(1));
844 assert_eq!(normalize_quantity(dec!(1.12345), 5), dec!(1.12345));
845 }
846
847 #[rstest]
848 fn test_normalize_order_complete() {
849 let result = normalize_order(
850 dec!(100.12345), dec!(0.123456), dec!(0.01), dec!(0.0001), dec!(10), 2, 4, );
858
859 assert!(result.is_ok());
860 let (price, qty) = result.unwrap();
861 assert_eq!(price, dec!(100.12)); assert_eq!(qty, dec!(0.1234)); }
864
865 #[rstest]
866 fn test_normalize_order_min_notional_fail() {
867 let result = normalize_order(
868 dec!(100.12345), dec!(0.05), dec!(0.01), dec!(0.0001), dec!(10), 2, 4, );
876
877 assert!(result.is_err());
878 assert!(result.unwrap_err().contains("Notional value"));
879 }
880
881 #[rstest]
882 fn test_edge_cases() {
883 assert_eq!(
885 round_down_to_tick(dec!(0.000001), dec!(0.000001)),
886 dec!(0.000001)
887 );
888
889 assert_eq!(round_down_to_tick(dec!(999999.99), dec!(1.0)), dec!(999999));
891
892 assert_eq!(
894 round_down_to_tick(dec!(100.009999), dec!(0.01)),
895 dec!(100.00)
896 );
897 }
898
899 #[rstest]
900 fn test_is_conditional_order_data() {
901 assert!(is_conditional_order_data(
903 Some("50000.0"),
904 Some(&HyperliquidTpSl::Sl)
905 ));
906
907 assert!(!is_conditional_order_data(Some("50000.0"), None));
909
910 assert!(!is_conditional_order_data(None, Some(&HyperliquidTpSl::Tp)));
912
913 assert!(!is_conditional_order_data(None, None));
915 }
916
917 #[rstest]
918 fn test_parse_trigger_order_type() {
919 assert_eq!(
921 parse_trigger_order_type(true, &HyperliquidTpSl::Sl),
922 OrderType::StopMarket
923 );
924
925 assert_eq!(
927 parse_trigger_order_type(false, &HyperliquidTpSl::Sl),
928 OrderType::StopLimit
929 );
930
931 assert_eq!(
933 parse_trigger_order_type(true, &HyperliquidTpSl::Tp),
934 OrderType::MarketIfTouched
935 );
936
937 assert_eq!(
939 parse_trigger_order_type(false, &HyperliquidTpSl::Tp),
940 OrderType::LimitIfTouched
941 );
942 }
943
944 #[rstest]
945 fn test_parse_order_status_with_trigger() {
946 let (status, trigger_status) =
948 parse_order_status_with_trigger(HyperliquidOrderStatus::Open, Some(true));
949 assert_eq!(status, OrderStatus::Accepted);
950 assert_eq!(trigger_status, Some("activated".to_string()));
951
952 let (status, trigger_status) =
954 parse_order_status_with_trigger(HyperliquidOrderStatus::Open, Some(false));
955 assert_eq!(status, OrderStatus::Accepted);
956 assert_eq!(trigger_status, Some("pending".to_string()));
957
958 let (status, trigger_status) =
960 parse_order_status_with_trigger(HyperliquidOrderStatus::Open, None);
961 assert_eq!(status, OrderStatus::Accepted);
962 assert_eq!(trigger_status, None);
963 }
964
965 #[rstest]
966 fn test_format_trailing_stop_info() {
967 let info = format_trailing_stop_info("100.0", TrailingOffsetType::Price, Some("50000.0"));
969 assert!(info.contains("100.0"));
970 assert!(info.contains("callback at 50000.0"));
971
972 let info = format_trailing_stop_info("5.0", TrailingOffsetType::Percentage, None);
974 assert!(info.contains("5.0%"));
975 assert!(info.contains("Trailing stop"));
976
977 let info =
979 format_trailing_stop_info("250", TrailingOffsetType::BasisPoints, Some("49000.0"));
980 assert!(info.contains("250 bps"));
981 assert!(info.contains("49000.0"));
982 }
983
984 #[rstest]
985 fn test_parse_trigger_price() {
986 let result = parse_trigger_price("50000.0");
988 assert!(result.is_ok());
989 assert_eq!(result.unwrap(), dec!(50000.0));
990
991 let result = parse_trigger_price("49000");
993 assert!(result.is_ok());
994 assert_eq!(result.unwrap(), dec!(49000));
995
996 let result = parse_trigger_price("invalid");
998 assert!(result.is_err());
999
1000 let result = parse_trigger_price("");
1002 assert!(result.is_err());
1003 }
1004}