use std::{fmt::Display, str::FromStr};
use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus, OrderType};
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString};
use super::consts::HYPERLIQUID_POST_ONLY_WOULD_MATCH;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HyperliquidBarInterval {
#[serde(rename = "1m")]
OneMinute,
#[serde(rename = "3m")]
ThreeMinutes,
#[serde(rename = "5m")]
FiveMinutes,
#[serde(rename = "15m")]
FifteenMinutes,
#[serde(rename = "30m")]
ThirtyMinutes,
#[serde(rename = "1h")]
OneHour,
#[serde(rename = "2h")]
TwoHours,
#[serde(rename = "4h")]
FourHours,
#[serde(rename = "8h")]
EightHours,
#[serde(rename = "12h")]
TwelveHours,
#[serde(rename = "1d")]
OneDay,
#[serde(rename = "3d")]
ThreeDays,
#[serde(rename = "1w")]
OneWeek,
#[serde(rename = "1M")]
OneMonth,
}
impl HyperliquidBarInterval {
pub fn as_str(&self) -> &'static str {
match self {
Self::OneMinute => "1m",
Self::ThreeMinutes => "3m",
Self::FiveMinutes => "5m",
Self::FifteenMinutes => "15m",
Self::ThirtyMinutes => "30m",
Self::OneHour => "1h",
Self::TwoHours => "2h",
Self::FourHours => "4h",
Self::EightHours => "8h",
Self::TwelveHours => "12h",
Self::OneDay => "1d",
Self::ThreeDays => "3d",
Self::OneWeek => "1w",
Self::OneMonth => "1M",
}
}
}
impl FromStr for HyperliquidBarInterval {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1m" => Ok(Self::OneMinute),
"3m" => Ok(Self::ThreeMinutes),
"5m" => Ok(Self::FiveMinutes),
"15m" => Ok(Self::FifteenMinutes),
"30m" => Ok(Self::ThirtyMinutes),
"1h" => Ok(Self::OneHour),
"2h" => Ok(Self::TwoHours),
"4h" => Ok(Self::FourHours),
"8h" => Ok(Self::EightHours),
"12h" => Ok(Self::TwelveHours),
"1d" => Ok(Self::OneDay),
"3d" => Ok(Self::ThreeDays),
"1w" => Ok(Self::OneWeek),
"1M" => Ok(Self::OneMonth),
_ => anyhow::bail!("Invalid Hyperliquid bar interval: {s}"),
}
}
}
impl Display for HyperliquidBarInterval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum HyperliquidSide {
#[serde(rename = "B")]
Buy,
#[serde(rename = "A")]
Sell,
}
impl From<OrderSide> for HyperliquidSide {
fn from(value: OrderSide) -> Self {
match value {
OrderSide::Buy => Self::Buy,
OrderSide::Sell => Self::Sell,
_ => panic!("Invalid `OrderSide`"),
}
}
}
impl From<HyperliquidSide> for OrderSide {
fn from(value: HyperliquidSide) -> Self {
match value {
HyperliquidSide::Buy => Self::Buy,
HyperliquidSide::Sell => Self::Sell,
}
}
}
impl From<HyperliquidSide> for AggressorSide {
fn from(value: HyperliquidSide) -> Self {
match value {
HyperliquidSide::Buy => Self::Buyer,
HyperliquidSide::Sell => Self::Seller,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "PascalCase")]
#[strum(serialize_all = "PascalCase")]
pub enum HyperliquidTimeInForce {
Alo,
Ioc,
Gtc,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum HyperliquidOrderType {
#[serde(rename = "limit")]
Limit { tif: HyperliquidTimeInForce },
#[serde(rename = "trigger")]
Trigger {
#[serde(rename = "isMarket")]
is_market: bool,
#[serde(rename = "triggerPx")]
trigger_px: String,
tpsl: HyperliquidTpSl,
},
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidTpSl {
Tp,
Sl,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum HyperliquidConditionalOrderType {
StopMarket,
StopLimit,
TakeProfitMarket,
TakeProfitLimit,
TrailingStopMarket,
TrailingStopLimit,
}
impl From<HyperliquidConditionalOrderType> for OrderType {
fn from(value: HyperliquidConditionalOrderType) -> Self {
match value {
HyperliquidConditionalOrderType::StopMarket => Self::StopMarket,
HyperliquidConditionalOrderType::StopLimit => Self::StopLimit,
HyperliquidConditionalOrderType::TakeProfitMarket => Self::MarketIfTouched,
HyperliquidConditionalOrderType::TakeProfitLimit => Self::LimitIfTouched,
HyperliquidConditionalOrderType::TrailingStopMarket => Self::TrailingStopMarket,
HyperliquidConditionalOrderType::TrailingStopLimit => Self::TrailingStopLimit,
}
}
}
impl From<OrderType> for HyperliquidConditionalOrderType {
fn from(value: OrderType) -> Self {
match value {
OrderType::StopMarket => Self::StopMarket,
OrderType::StopLimit => Self::StopLimit,
OrderType::MarketIfTouched => Self::TakeProfitMarket,
OrderType::LimitIfTouched => Self::TakeProfitLimit,
OrderType::TrailingStopMarket => Self::TrailingStopMarket,
OrderType::TrailingStopLimit => Self::TrailingStopLimit,
_ => panic!("Unsupported OrderType for conditional orders: {value:?}"),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidTrailingOffsetType {
Price,
Percentage,
#[serde(rename = "basispoints")]
#[strum(serialize = "basispoints")]
BasisPoints,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HyperliquidReduceOnly(pub bool);
impl HyperliquidReduceOnly {
pub fn new(reduce_only: bool) -> Self {
Self(reduce_only)
}
pub fn is_reduce_only(&self) -> bool {
self.0
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidLiquidityFlag {
Maker,
Taker,
}
impl From<bool> for HyperliquidLiquidityFlag {
fn from(crossed: bool) -> Self {
if crossed { Self::Taker } else { Self::Maker }
}
}
#[derive(
Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidLiquidationMethod {
Market,
Backstop,
}
#[derive(
Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum HyperliquidPositionType {
OneWay,
}
#[derive(
Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidTwapStatus {
Activated,
Terminated,
Finished,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidRejectCode {
Tick,
MinTradeNtl,
MinTradeSpotNtl,
PerpMargin,
ReduceOnly,
BadAloPx,
IocCancel,
BadTriggerPx,
MarketOrderNoLiquidity,
PositionIncreaseAtOpenInterestCap,
PositionFlipAtOpenInterestCap,
TooAggressiveAtOpenInterestCap,
OpenInterestIncrease,
InsufficientSpotBalance,
Oracle,
PerpMaxPosition,
MissingOrder,
Unknown(String),
}
impl HyperliquidRejectCode {
pub fn from_api_error(error_message: &str) -> Self {
Self::from_error_string_internal(error_message)
}
fn from_error_string_internal(error: &str) -> Self {
let normalized = error.trim().to_lowercase();
match normalized.as_str() {
s if s.contains("tick size") => Self::Tick,
s if s.contains("minimum value of $10") => Self::MinTradeNtl,
s if s.contains("minimum value of 10") => Self::MinTradeSpotNtl,
s if s.contains("insufficient margin") => Self::PerpMargin,
s if s.contains("reduce only order would increase")
|| s.contains("reduce-only order would increase") =>
{
Self::ReduceOnly
}
s if s.contains(&HYPERLIQUID_POST_ONLY_WOULD_MATCH.to_lowercase())
|| s.contains("post-only order would have immediately matched") =>
{
Self::BadAloPx
}
s if s.contains("could not immediately match") => Self::IocCancel,
s if s.contains("invalid tp/sl price") => Self::BadTriggerPx,
s if s.contains("no liquidity available for market order") => {
Self::MarketOrderNoLiquidity
}
s if s.contains("positionincreaseatopeninterestcap") => {
Self::PositionIncreaseAtOpenInterestCap
}
s if s.contains("positionflipatopeninterestcap") => Self::PositionFlipAtOpenInterestCap,
s if s.contains("tooaggressiveatopeninterestcap") => {
Self::TooAggressiveAtOpenInterestCap
}
s if s.contains("openinterestincrease") => Self::OpenInterestIncrease,
s if s.contains("insufficient spot balance") => Self::InsufficientSpotBalance,
s if s.contains("oracle") => Self::Oracle,
s if s.contains("max position") => Self::PerpMaxPosition,
s if s.contains("missingorder") => Self::MissingOrder,
_ => {
log::warn!(
"Unknown Hyperliquid error pattern (consider updating error parsing): {error}" );
Self::Unknown(error.to_string())
}
}
}
#[deprecated(
since = "0.50.0",
note = "String parsing is fragile; use HyperliquidRejectCode::from_api_error() instead"
)]
pub fn from_error_string(error: &str) -> Self {
Self::from_error_string_internal(error)
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum HyperliquidOrderStatus {
#[serde(rename = "open")]
Open,
#[serde(rename = "accepted")]
Accepted,
#[serde(rename = "triggered")]
Triggered,
#[serde(rename = "filled")]
Filled,
#[serde(rename = "canceled")]
Canceled,
#[serde(rename = "rejected")]
Rejected,
#[serde(rename = "marginCanceled")]
MarginCanceled,
#[serde(rename = "vaultWithdrawalCanceled")]
VaultWithdrawalCanceled,
#[serde(rename = "openInterestCapCanceled")]
OpenInterestCapCanceled,
#[serde(rename = "selfTradeCanceled")]
SelfTradeCanceled,
#[serde(rename = "reduceOnlyCanceled")]
ReduceOnlyCanceled,
#[serde(rename = "siblingFilledCanceled")]
SiblingFilledCanceled,
#[serde(rename = "delistedCanceled")]
DelistedCanceled,
#[serde(rename = "liquidatedCanceled")]
LiquidatedCanceled,
#[serde(rename = "scheduledCancel")]
ScheduledCancel,
#[serde(rename = "tickRejected")]
TickRejected,
#[serde(rename = "minTradeNtlRejected")]
MinTradeNtlRejected,
#[serde(rename = "perpMarginRejected")]
PerpMarginRejected,
#[serde(rename = "reduceOnlyRejected")]
ReduceOnlyRejected,
#[serde(rename = "badAloPxRejected")]
BadAloPxRejected,
#[serde(rename = "iocCancelRejected")]
IocCancelRejected,
#[serde(rename = "badTriggerPxRejected")]
BadTriggerPxRejected,
#[serde(rename = "marketOrderNoLiquidityRejected")]
MarketOrderNoLiquidityRejected,
#[serde(rename = "positionIncreaseAtOpenInterestCapRejected")]
PositionIncreaseAtOpenInterestCapRejected,
#[serde(rename = "positionFlipAtOpenInterestCapRejected")]
PositionFlipAtOpenInterestCapRejected,
#[serde(rename = "tooAggressiveAtOpenInterestCapRejected")]
TooAggressiveAtOpenInterestCapRejected,
#[serde(rename = "openInterestIncreaseRejected")]
OpenInterestIncreaseRejected,
#[serde(rename = "insufficientSpotBalanceRejected")]
InsufficientSpotBalanceRejected,
#[serde(rename = "oracleRejected")]
OracleRejected,
#[serde(rename = "perpMaxPositionRejected")]
PerpMaxPositionRejected,
}
impl From<HyperliquidOrderStatus> for OrderStatus {
fn from(status: HyperliquidOrderStatus) -> Self {
match status {
HyperliquidOrderStatus::Open | HyperliquidOrderStatus::Accepted => Self::Accepted,
HyperliquidOrderStatus::Triggered => Self::Triggered,
HyperliquidOrderStatus::Filled => Self::Filled,
HyperliquidOrderStatus::Canceled
| HyperliquidOrderStatus::MarginCanceled
| HyperliquidOrderStatus::VaultWithdrawalCanceled
| HyperliquidOrderStatus::OpenInterestCapCanceled
| HyperliquidOrderStatus::SelfTradeCanceled
| HyperliquidOrderStatus::ReduceOnlyCanceled
| HyperliquidOrderStatus::SiblingFilledCanceled
| HyperliquidOrderStatus::DelistedCanceled
| HyperliquidOrderStatus::LiquidatedCanceled
| HyperliquidOrderStatus::ScheduledCancel => Self::Canceled,
HyperliquidOrderStatus::Rejected
| HyperliquidOrderStatus::TickRejected
| HyperliquidOrderStatus::MinTradeNtlRejected
| HyperliquidOrderStatus::PerpMarginRejected
| HyperliquidOrderStatus::ReduceOnlyRejected
| HyperliquidOrderStatus::BadAloPxRejected
| HyperliquidOrderStatus::IocCancelRejected
| HyperliquidOrderStatus::BadTriggerPxRejected
| HyperliquidOrderStatus::MarketOrderNoLiquidityRejected
| HyperliquidOrderStatus::PositionIncreaseAtOpenInterestCapRejected
| HyperliquidOrderStatus::PositionFlipAtOpenInterestCapRejected
| HyperliquidOrderStatus::TooAggressiveAtOpenInterestCapRejected
| HyperliquidOrderStatus::OpenInterestIncreaseRejected
| HyperliquidOrderStatus::InsufficientSpotBalanceRejected
| HyperliquidOrderStatus::OracleRejected
| HyperliquidOrderStatus::PerpMaxPositionRejected => Self::Rejected,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "PascalCase")]
#[strum(serialize_all = "PascalCase")]
pub enum HyperliquidFillDirection {
#[serde(rename = "Open Long")]
#[strum(serialize = "Open Long")]
OpenLong,
#[serde(rename = "Open Short")]
#[strum(serialize = "Open Short")]
OpenShort,
#[serde(rename = "Close Long")]
#[strum(serialize = "Close Long")]
CloseLong,
#[serde(rename = "Close Short")]
#[strum(serialize = "Close Short")]
CloseShort,
#[serde(rename = "Long > Short")]
#[strum(serialize = "Long > Short")]
LongToShort,
#[serde(rename = "Short > Long")]
#[strum(serialize = "Short > Long")]
ShortToLong,
Buy,
Sell,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum HyperliquidInfoRequestType {
Meta,
SpotMeta,
MetaAndAssetCtxs,
SpotMetaAndAssetCtxs,
L2Book,
AllMids,
UserFills,
UserFillsByTime,
OrderStatus,
OpenOrders,
FrontendOpenOrders,
ClearinghouseState,
SpotClearinghouseState,
ExchangeStatus,
CandleSnapshot,
Candle,
RecentTrades,
HistoricalOrders,
FundingHistory,
UserFunding,
NonUserFundingUpdates,
TwapHistory,
UserTwapSliceFills,
UserTwapSliceFillsByTime,
UserRateLimit,
UserRole,
DelegatorHistory,
DelegatorRewards,
ValidatorStats,
UserFees,
AllPerpMetas,
}
impl HyperliquidInfoRequestType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Meta => "meta",
Self::SpotMeta => "spotMeta",
Self::MetaAndAssetCtxs => "metaAndAssetCtxs",
Self::SpotMetaAndAssetCtxs => "spotMetaAndAssetCtxs",
Self::L2Book => "l2Book",
Self::AllMids => "allMids",
Self::UserFills => "userFills",
Self::UserFillsByTime => "userFillsByTime",
Self::OrderStatus => "orderStatus",
Self::OpenOrders => "openOrders",
Self::FrontendOpenOrders => "frontendOpenOrders",
Self::ClearinghouseState => "clearinghouseState",
Self::SpotClearinghouseState => "spotClearinghouseState",
Self::ExchangeStatus => "exchangeStatus",
Self::CandleSnapshot => "candleSnapshot",
Self::Candle => "candle",
Self::RecentTrades => "recentTrades",
Self::HistoricalOrders => "historicalOrders",
Self::FundingHistory => "fundingHistory",
Self::UserFunding => "userFunding",
Self::NonUserFundingUpdates => "nonUserFundingUpdates",
Self::TwapHistory => "twapHistory",
Self::UserTwapSliceFills => "userTwapSliceFills",
Self::UserTwapSliceFillsByTime => "userTwapSliceFillsByTime",
Self::UserRateLimit => "userRateLimit",
Self::UserRole => "userRole",
Self::DelegatorHistory => "delegatorHistory",
Self::DelegatorRewards => "delegatorRewards",
Self::ValidatorStats => "validatorStats",
Self::UserFees => "userFees",
Self::AllPerpMetas => "allPerpMetas",
}
}
}
#[derive(
Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize, AsRefStr, EnumString,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum HyperliquidLeverageType {
Cross,
Isolated,
#[serde(other)]
Unknown,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.hyperliquid")
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum HyperliquidProductType {
Perp,
Spot,
}
impl HyperliquidProductType {
pub fn from_symbol(symbol: &str) -> anyhow::Result<Self> {
if symbol.ends_with("-PERP") {
Ok(Self::Perp)
} else if symbol.ends_with("-SPOT") {
Ok(Self::Spot)
} else {
anyhow::bail!("Invalid Hyperliquid symbol format: {symbol}")
}
}
}
#[cfg(test)]
mod tests {
use nautilus_model::enums::OrderType;
use rstest::rstest;
use serde_json;
use super::*;
#[rstest]
fn test_side_serde() {
let buy_side = HyperliquidSide::Buy;
let sell_side = HyperliquidSide::Sell;
assert_eq!(serde_json::to_string(&buy_side).unwrap(), "\"B\"");
assert_eq!(serde_json::to_string(&sell_side).unwrap(), "\"A\"");
assert_eq!(
serde_json::from_str::<HyperliquidSide>("\"B\"").unwrap(),
HyperliquidSide::Buy
);
assert_eq!(
serde_json::from_str::<HyperliquidSide>("\"A\"").unwrap(),
HyperliquidSide::Sell
);
}
#[rstest]
fn test_side_from_order_side() {
assert_eq!(HyperliquidSide::from(OrderSide::Buy), HyperliquidSide::Buy);
assert_eq!(
HyperliquidSide::from(OrderSide::Sell),
HyperliquidSide::Sell
);
}
#[rstest]
fn test_order_side_from_hyperliquid_side() {
assert_eq!(OrderSide::from(HyperliquidSide::Buy), OrderSide::Buy);
assert_eq!(OrderSide::from(HyperliquidSide::Sell), OrderSide::Sell);
}
#[rstest]
fn test_aggressor_side_from_hyperliquid_side() {
assert_eq!(
AggressorSide::from(HyperliquidSide::Buy),
AggressorSide::Buyer
);
assert_eq!(
AggressorSide::from(HyperliquidSide::Sell),
AggressorSide::Seller
);
}
#[rstest]
fn test_time_in_force_serde() {
let test_cases = [
(HyperliquidTimeInForce::Alo, "\"Alo\""),
(HyperliquidTimeInForce::Ioc, "\"Ioc\""),
(HyperliquidTimeInForce::Gtc, "\"Gtc\""),
];
for (tif, expected_json) in test_cases {
assert_eq!(serde_json::to_string(&tif).unwrap(), expected_json);
assert_eq!(
serde_json::from_str::<HyperliquidTimeInForce>(expected_json).unwrap(),
tif
);
}
}
#[rstest]
fn test_liquidity_flag_from_crossed() {
assert_eq!(
HyperliquidLiquidityFlag::from(true),
HyperliquidLiquidityFlag::Taker
);
assert_eq!(
HyperliquidLiquidityFlag::from(false),
HyperliquidLiquidityFlag::Maker
);
}
#[rstest]
#[allow(deprecated)]
fn test_reject_code_from_error_string() {
let test_cases = [
(
"Price must be divisible by tick size.",
HyperliquidRejectCode::Tick,
),
(
"Order must have minimum value of $10.",
HyperliquidRejectCode::MinTradeNtl,
),
(
"Insufficient margin to place order.",
HyperliquidRejectCode::PerpMargin,
),
(
"Post only order would have immediately matched, bbo was 1.23",
HyperliquidRejectCode::BadAloPx,
),
(
"Some unknown error",
HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
),
];
for (error_str, expected_code) in test_cases {
assert_eq!(
HyperliquidRejectCode::from_error_string(error_str),
expected_code
);
}
}
#[rstest]
fn test_reject_code_from_api_error() {
let test_cases = [
(
"Price must be divisible by tick size.",
HyperliquidRejectCode::Tick,
),
(
"Order must have minimum value of $10.",
HyperliquidRejectCode::MinTradeNtl,
),
(
"Insufficient margin to place order.",
HyperliquidRejectCode::PerpMargin,
),
(
"Post only order would have immediately matched, bbo was 1.23",
HyperliquidRejectCode::BadAloPx,
),
(
"Some unknown error",
HyperliquidRejectCode::Unknown("Some unknown error".to_string()),
),
];
for (error_str, expected_code) in test_cases {
assert_eq!(
HyperliquidRejectCode::from_api_error(error_str),
expected_code
);
}
}
#[rstest]
fn test_reduce_only() {
let reduce_only = HyperliquidReduceOnly::new(true);
assert!(reduce_only.is_reduce_only());
let json = serde_json::to_string(&reduce_only).unwrap();
assert_eq!(json, "true");
let parsed: HyperliquidReduceOnly = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, reduce_only);
}
#[rstest]
fn test_order_status_conversion() {
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Open),
OrderStatus::Accepted
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Accepted),
OrderStatus::Accepted
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Triggered),
OrderStatus::Triggered
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Filled),
OrderStatus::Filled
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Canceled),
OrderStatus::Canceled
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::Rejected),
OrderStatus::Rejected
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::MarginCanceled),
OrderStatus::Canceled
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::SelfTradeCanceled),
OrderStatus::Canceled
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::ReduceOnlyCanceled),
OrderStatus::Canceled
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::TickRejected),
OrderStatus::Rejected
);
assert_eq!(
OrderStatus::from(HyperliquidOrderStatus::PerpMarginRejected),
OrderStatus::Rejected
);
}
#[rstest]
fn test_order_status_serde_deserialization() {
let open: HyperliquidOrderStatus = serde_json::from_str(r#""open""#).unwrap();
assert_eq!(open, HyperliquidOrderStatus::Open);
let canceled: HyperliquidOrderStatus = serde_json::from_str(r#""canceled""#).unwrap();
assert_eq!(canceled, HyperliquidOrderStatus::Canceled);
let margin_canceled: HyperliquidOrderStatus =
serde_json::from_str(r#""marginCanceled""#).unwrap();
assert_eq!(margin_canceled, HyperliquidOrderStatus::MarginCanceled);
let self_trade_canceled: HyperliquidOrderStatus =
serde_json::from_str(r#""selfTradeCanceled""#).unwrap();
assert_eq!(
self_trade_canceled,
HyperliquidOrderStatus::SelfTradeCanceled
);
let reduce_only_canceled: HyperliquidOrderStatus =
serde_json::from_str(r#""reduceOnlyCanceled""#).unwrap();
assert_eq!(
reduce_only_canceled,
HyperliquidOrderStatus::ReduceOnlyCanceled
);
let tick_rejected: HyperliquidOrderStatus =
serde_json::from_str(r#""tickRejected""#).unwrap();
assert_eq!(tick_rejected, HyperliquidOrderStatus::TickRejected);
}
#[rstest]
fn test_hyperliquid_tpsl_serialization() {
let tp = HyperliquidTpSl::Tp;
let sl = HyperliquidTpSl::Sl;
assert_eq!(serde_json::to_string(&tp).unwrap(), r#""tp""#);
assert_eq!(serde_json::to_string(&sl).unwrap(), r#""sl""#);
}
#[rstest]
fn test_hyperliquid_tpsl_deserialization() {
let tp: HyperliquidTpSl = serde_json::from_str(r#""tp""#).unwrap();
let sl: HyperliquidTpSl = serde_json::from_str(r#""sl""#).unwrap();
assert_eq!(tp, HyperliquidTpSl::Tp);
assert_eq!(sl, HyperliquidTpSl::Sl);
}
#[rstest]
fn test_conditional_order_type_conversions() {
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::StopMarket),
OrderType::StopMarket
);
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::StopLimit),
OrderType::StopLimit
);
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::TakeProfitMarket),
OrderType::MarketIfTouched
);
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::TakeProfitLimit),
OrderType::LimitIfTouched
);
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::TrailingStopMarket),
OrderType::TrailingStopMarket
);
}
mod error_parsing_tests {
use super::*;
#[rstest]
fn test_parse_tick_size_error() {
let error = "Price must be divisible by tick size 0.01";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::Tick);
}
#[rstest]
fn test_parse_tick_size_error_case_insensitive() {
let error = "PRICE MUST BE DIVISIBLE BY TICK SIZE 0.01";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::Tick);
}
#[rstest]
fn test_parse_min_notional_perp() {
let error = "Order must have minimum value of $10";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::MinTradeNtl);
}
#[rstest]
fn test_parse_min_notional_spot() {
let error = "Order must have minimum value of 10 USDC";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::MinTradeSpotNtl);
}
#[rstest]
fn test_parse_insufficient_margin() {
let error = "Insufficient margin to place order";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::PerpMargin);
}
#[rstest]
fn test_parse_insufficient_margin_case_variations() {
let variations = vec![
"insufficient margin to place order",
"INSUFFICIENT MARGIN TO PLACE ORDER",
" Insufficient margin to place order ", ];
for error in variations {
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::PerpMargin);
}
}
#[rstest]
fn test_parse_reduce_only_violation() {
let error = "Reduce only order would increase position";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
}
#[rstest]
fn test_parse_reduce_only_with_hyphen() {
let error = "Reduce-only order would increase position";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::ReduceOnly);
}
#[rstest]
fn test_parse_post_only_match() {
let error = "Post only order would have immediately matched";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::BadAloPx);
}
#[rstest]
fn test_parse_post_only_with_hyphen() {
let error = "Post-only order would have immediately matched";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::BadAloPx);
}
#[rstest]
fn test_parse_ioc_no_match() {
let error = "Order could not immediately match";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::IocCancel);
}
#[rstest]
fn test_parse_invalid_trigger_price() {
let error = "Invalid TP/SL price";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::BadTriggerPx);
}
#[rstest]
fn test_parse_no_liquidity() {
let error = "No liquidity available for market order";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::MarketOrderNoLiquidity);
}
#[rstest]
fn test_parse_position_increase_at_oi_cap() {
let error = "PositionIncreaseAtOpenInterestCap";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(
code,
HyperliquidRejectCode::PositionIncreaseAtOpenInterestCap
);
}
#[rstest]
fn test_parse_position_flip_at_oi_cap() {
let error = "PositionFlipAtOpenInterestCap";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::PositionFlipAtOpenInterestCap);
}
#[rstest]
fn test_parse_too_aggressive_at_oi_cap() {
let error = "TooAggressiveAtOpenInterestCap";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::TooAggressiveAtOpenInterestCap);
}
#[rstest]
fn test_parse_open_interest_increase() {
let error = "OpenInterestIncrease";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::OpenInterestIncrease);
}
#[rstest]
fn test_parse_insufficient_spot_balance() {
let error = "Insufficient spot balance";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::InsufficientSpotBalance);
}
#[rstest]
fn test_parse_oracle_error() {
let error = "Oracle price unavailable";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::Oracle);
}
#[rstest]
fn test_parse_max_position() {
let error = "Exceeds max position size";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::PerpMaxPosition);
}
#[rstest]
fn test_parse_missing_order() {
let error = "MissingOrder";
let code = HyperliquidRejectCode::from_api_error(error);
assert_eq!(code, HyperliquidRejectCode::MissingOrder);
}
#[rstest]
fn test_parse_unknown_error() {
let error = "This is a completely new error message";
let code = HyperliquidRejectCode::from_api_error(error);
assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
if let HyperliquidRejectCode::Unknown(msg) = code {
assert_eq!(msg, error);
}
}
#[rstest]
fn test_parse_empty_error() {
let error = "";
let code = HyperliquidRejectCode::from_api_error(error);
assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
}
#[rstest]
fn test_parse_whitespace_only() {
let error = " ";
let code = HyperliquidRejectCode::from_api_error(error);
assert!(matches!(code, HyperliquidRejectCode::Unknown(_)));
}
#[rstest]
fn test_normalization_preserves_original_in_unknown() {
let error = " UNKNOWN ERROR MESSAGE ";
let code = HyperliquidRejectCode::from_api_error(error);
if let HyperliquidRejectCode::Unknown(msg) = code {
assert_eq!(msg, error);
} else {
panic!("Expected Unknown variant");
}
}
}
#[rstest]
fn test_conditional_order_type_round_trip() {
assert_eq!(
OrderType::from(HyperliquidConditionalOrderType::TrailingStopLimit),
OrderType::TrailingStopLimit
);
assert_eq!(
HyperliquidConditionalOrderType::from(OrderType::StopMarket),
HyperliquidConditionalOrderType::StopMarket
);
assert_eq!(
HyperliquidConditionalOrderType::from(OrderType::StopLimit),
HyperliquidConditionalOrderType::StopLimit
);
}
#[rstest]
fn test_trailing_offset_type_serialization() {
let price = HyperliquidTrailingOffsetType::Price;
let percentage = HyperliquidTrailingOffsetType::Percentage;
let basis_points = HyperliquidTrailingOffsetType::BasisPoints;
assert_eq!(serde_json::to_string(&price).unwrap(), r#""price""#);
assert_eq!(
serde_json::to_string(&percentage).unwrap(),
r#""percentage""#
);
assert_eq!(
serde_json::to_string(&basis_points).unwrap(),
r#""basispoints""#
);
}
#[rstest]
fn test_conditional_order_type_serialization() {
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::StopMarket).unwrap(),
r#""STOP_MARKET""#
);
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::StopLimit).unwrap(),
r#""STOP_LIMIT""#
);
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitMarket).unwrap(),
r#""TAKE_PROFIT_MARKET""#
);
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::TakeProfitLimit).unwrap(),
r#""TAKE_PROFIT_LIMIT""#
);
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopMarket).unwrap(),
r#""TRAILING_STOP_MARKET""#
);
assert_eq!(
serde_json::to_string(&HyperliquidConditionalOrderType::TrailingStopLimit).unwrap(),
r#""TRAILING_STOP_LIMIT""#
);
}
#[rstest]
fn test_order_type_enum_coverage() {
let conditional_types = vec![
HyperliquidConditionalOrderType::StopMarket,
HyperliquidConditionalOrderType::StopLimit,
HyperliquidConditionalOrderType::TakeProfitMarket,
HyperliquidConditionalOrderType::TakeProfitLimit,
HyperliquidConditionalOrderType::TrailingStopMarket,
HyperliquidConditionalOrderType::TrailingStopLimit,
];
for cond_type in conditional_types {
let order_type = OrderType::from(cond_type);
let back_to_cond = HyperliquidConditionalOrderType::from(order_type);
assert_eq!(cond_type, back_to_cond, "Roundtrip conversion failed");
}
}
}