diff --git a/README.md b/README.md
index 9f4fc51fb..5a4330bb9 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
+- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
@@ -41,6 +42,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
+- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
diff --git a/docs/exchanges.md b/docs/exchanges.md
index 468c7bc8e..4b324346f 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -303,6 +303,42 @@ It's therefore required to pass the UID as well.
!!! Warning "Necessary Verification"
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
+## Hyperliquid
+
+!!! Tip "Stoploss on Exchange"
+ Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
+
+Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet.
+This needs to be configured like this:
+
+```json
+"exchange": {
+ "name": "hyperliquid",
+ "walletAddress": "your_eth_wallet_address",
+ "privateKey": "your_private_key",
+ // ...
+}
+```
+
+* walletAddress must be in hex format: `0x<40 hex characters>`, and can be easily copied from your wallet.
+* privateKey also must be in hex format: `0x<64 hex characters>`, and can either be exported from your wallet or regenerated using your mnemonic phrase.
+
+Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
+
+!!! Note "Hyperliquid general usage Notes"
+ Hyperliquid does not support market orders, however ccxt will simulate market orders by placing limit orders with a maximum slippage of 5%.
+ Unfortunately, hyperliquid only offers 5000 historic candles, so backtesting will either need to build candles historically (by waiting and downloading the data incrementally over time) - or will be limited to the last 5000 candles.
+
+!!! Info "Some general best practices (non exhaustive)"
+ * Beware of supply chain attacks, like pip package poisoning etcetera. However you export or (re-)generate your private key, make sure your environment is safe.
+ * Interact as little with the private key as possible. Store it in a separate file from the config.json (secrets.json for example) that you never have to touch, and secure it.
+ * Always keep your mnemonic phrase and private key private.
+ * Don't use the same mnemonic as the one you had to backup when initializing a hardware wallet, using the same mnemonic basically deletes the security of your hardware wallet.
+ * Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet / private key to trade on Hyperliquid.
+ * Remember that if someone hacks the host you use for trading, or any other host you stored your private key / mnemonic on, you will lose the funds protected by that private key. That means the funds on that wallet and the funds deposited on Hyperliquid.
+ * If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
+
+
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
diff --git a/docs/index.md b/docs/index.md
index d6dca4880..87b76b6ad 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -40,11 +40,12 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/)
-- [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
+- [X] [Bitmart](https://bitmart.com/)
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
+- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others through
](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
@@ -52,9 +53,10 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
### Supported Futures Exchanges (experimental)
- [X] [Binance](https://www.binance.com/)
+- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
+- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
-- [X] [Bybit](https://bybit.com/)
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
diff --git a/docs/stoploss.md b/docs/stoploss.md
index e0353d4da..7442484de 100644
--- a/docs/stoploss.md
+++ b/docs/stoploss.md
@@ -36,6 +36,7 @@ The Order-type will be ignored if only one mode is available.
| Gate | limit |
| Okx | limit |
| Kucoin | stop-limit, stop-market|
+| Hyperliquid (futures only) | limit |
!!! Note "Tight stoploss"
Do not set too low/tight stoploss value when using stop loss on exchange!
diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py
index cd451062f..4fd9cea63 100644
--- a/freqtrade/exchange/common.py
+++ b/freqtrade/exchange/common.py
@@ -58,6 +58,7 @@ SUPPORTED_EXCHANGES = [
"bybit",
"gate",
"htx",
+ "hyperliquid",
"kraken",
"okx",
]
diff --git a/freqtrade/exchange/hyperliquid.py b/freqtrade/exchange/hyperliquid.py
index 144edbf3a..2807f1be6 100644
--- a/freqtrade/exchange/hyperliquid.py
+++ b/freqtrade/exchange/hyperliquid.py
@@ -1,8 +1,11 @@
"""Hyperliquid exchange subclass"""
import logging
+from datetime import datetime
-from freqtrade.enums import TradingMode
+from freqtrade.constants import BuySell
+from freqtrade.enums import CandleType, MarginMode, TradingMode
+from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
@@ -16,20 +19,146 @@ class Hyperliquid(Exchange):
"""
_ft_has: FtHas = {
- # Only the most recent 5000 candles are available according to the
- # exchange's API documentation.
"ohlcv_has_history": False,
"ohlcv_candle_limit": 5000,
- "trades_has_history": False, # Trades endpoint doesn't seem available.
+ "l2_limit_range": [20],
+ "trades_has_history": False,
+ "tickers_have_bid_ask": False,
+ "stoploss_on_exchange": False,
"exchange_has_overrides": {"fetchTrades": False},
+ "funding_fee_timeframe": "1h",
+ "marketOrderRequiresPrice": True,
}
+ _ft_has_futures: FtHas = {
+ "stoploss_on_exchange": True,
+ "stoploss_order_types": {"limit": "limit"},
+ }
+
+ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
+ (TradingMode.FUTURES, MarginMode.ISOLATED)
+ ]
@property
def _ccxt_config(self) -> dict:
- # Parameters to add directly to ccxt sync/async initialization.
- # ccxt defaults to swap mode.
+ # ccxt Hyperliquid defaults to swap
config = {}
if self.trading_mode == TradingMode.SPOT:
config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config)
return config
+
+ def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
+ # There are no leverage tiers
+ if self.trading_mode == TradingMode.FUTURES:
+ return self.markets[pair]["limits"]["leverage"]["max"]
+ else:
+ return 1.0
+
+ def ohlcv_candle_limit(
+ self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
+ ) -> int:
+ # Funding rate candles have a different limit
+ if candle_type == CandleType.FUNDING_RATE:
+ return 500
+
+ return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
+
+ def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
+ if self.trading_mode != TradingMode.SPOT:
+ # Hyperliquid expects leverage to be an int
+ leverage = int(leverage)
+ # Hyperliquid needs the parameter leverage.
+ # Don't use _set_leverage(), as this sets margin back to cross
+ self.set_margin_mode(pair, self.margin_mode, params={"leverage": leverage})
+
+ def dry_run_liquidation_price(
+ self,
+ pair: str,
+ open_rate: float, # Entry price of position
+ is_short: bool,
+ amount: float,
+ stake_amount: float,
+ leverage: float,
+ wallet_balance: float, # Or margin balance
+ open_trades: list,
+ ) -> float | None:
+ """
+ Optimized
+ Docs: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/liquidations
+ Below can be done in fewer lines of code, but like this it matches the documentation.
+
+ Tested with 196 unique ccxt fetch_positions() position outputs
+ - Only first output per position where pnl=0.0
+ - Compare against returned liquidation price
+ Positions: 197 Average deviation: 0.00028980% Max deviation: 0.01309453%
+ Positions info:
+ {'leverage': {1.0: 23, 2.0: 155, 3.0: 8, 4.0: 7, 5.0: 4},
+ 'side': {'long': 133, 'short': 64},
+ 'symbol': {'BTC/USDC:USDC': 81,
+ 'DOGE/USDC:USDC': 20,
+ 'ETH/USDC:USDC': 53,
+ 'SOL/USDC:USDC': 43}}
+ """
+ # Defining/renaming variables to match the documentation
+ isolated_margin = wallet_balance
+ position_size = amount
+ price = open_rate
+ position_value = price * position_size
+ max_leverage = self.markets[pair]["limits"]["leverage"]["max"]
+
+ # Docs: The maintenance margin is half of the initial margin at max leverage,
+ # which varies from 3-50x. In other words, the maintenance margin is between 1%
+ # (for 50x max leverage assets) and 16.7% (for 3x max leverage assets)
+ # depending on the asset
+ # The key thing here is 'Half of the initial margin at max leverage'.
+ # A bit ambiguous, but this interpretation leads to accurate results:
+ # 1. Start from the position value
+ # 2. Assume max leverage, calculate the initial margin by dividing the position value
+ # by the max leverage
+ # 3. Divide this by 2
+ maintenance_margin_required = position_value / max_leverage / 2
+
+ # Docs: margin_available (isolated) = isolated_margin - maintenance_margin_required
+ margin_available = isolated_margin - maintenance_margin_required
+
+ # Docs: The maintenance margin is half of the initial margin at max leverage
+ # The docs don't explicitly specify maintenance leverage, but this works.
+ # Double because of the statement 'half of the initial margin at max leverage'
+ maintenance_leverage = max_leverage * 2
+
+ # Docs: l = 1 / MAINTENANCE_LEVERAGE (Using 'll' to comply with PEP8: E741)
+ ll = 1 / maintenance_leverage
+
+ # Docs: side = 1 for long and -1 for short
+ side = -1 if is_short else 1
+
+ # Docs: liq_price = price - side * margin_available / position_size / (1 - l * side)
+ liq_price = price - side * margin_available / position_size / (1 - ll * side)
+
+ if self.trading_mode == TradingMode.FUTURES:
+ return liq_price
+ else:
+ raise OperationalException(
+ "Freqtrade only supports isolated futures for leverage trading"
+ )
+
+ def get_funding_fees(
+ self, pair: str, amount: float, is_short: bool, open_date: datetime
+ ) -> float:
+ """
+ Fetch funding fees, either from the exchange (live) or calculates them
+ based on funding rate/mark price history
+ :param pair: The quote/base pair of the trade
+ :param is_short: trade direction
+ :param amount: Trade amount
+ :param open_date: Open date of the trade
+ :return: funding fee since open_date
+ :raises: ExchangeError if something goes wrong.
+ """
+ # Hyperliquid does not have fetchFundingHistory
+ if self.trading_mode == TradingMode.FUTURES:
+ try:
+ return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
+ except ExchangeError:
+ logger.warning(f"Could not update funding fees for {pair}.")
+ return 0.0
diff --git a/requirements.txt b/requirements.txt
index c27ab0201..b80d8f7e4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ bottleneck==1.4.2
numexpr==2.10.1
pandas-ta==0.3.14b
-ccxt==4.4.29
+ccxt==4.4.31
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==43.0.3; platform_machine != 'armv7l'
aiohttp==3.10.10
diff --git a/tests/exchange/test_hyperliquid.py b/tests/exchange/test_hyperliquid.py
new file mode 100644
index 000000000..b754685f8
--- /dev/null
+++ b/tests/exchange/test_hyperliquid.py
@@ -0,0 +1,374 @@
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, PropertyMock
+
+import pytest
+
+from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
+
+
+def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
+ # test if liq price calculated by dry_run_liquidation_price() is close to ccxt liq price
+ # testing different pairs with large/small prices, different leverages, long, short
+ markets = {
+ "BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
+ "ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
+ "SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
+ "DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
+ }
+ positions = [
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2458.5,
+ "side": "long",
+ "contracts": 0.015,
+ "collateral": 36.864593,
+ "leverage": 1.0,
+ "liquidationPrice": 0.86915825,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 63287.0,
+ "side": "long",
+ "contracts": 0.00039,
+ "collateral": 24.673292,
+ "leverage": 1.0,
+ "liquidationPrice": 22.37166537,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 146.82,
+ "side": "long",
+ "contracts": 0.16,
+ "collateral": 23.482979,
+ "leverage": 1.0,
+ "liquidationPrice": 0.05269872,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 145.83,
+ "side": "long",
+ "contracts": 0.33,
+ "collateral": 24.045107,
+ "leverage": 2.0,
+ "liquidationPrice": 74.83696193,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2459.5,
+ "side": "long",
+ "contracts": 0.0199,
+ "collateral": 24.454895,
+ "leverage": 2.0,
+ "liquidationPrice": 1243.0411908,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 62739.0,
+ "side": "long",
+ "contracts": 0.00077,
+ "collateral": 24.137992,
+ "leverage": 2.0,
+ "liquidationPrice": 31708.03843631,
+ },
+ {
+ "symbol": "DOGE/USDC:USDC",
+ "entryPrice": 0.11586,
+ "side": "long",
+ "contracts": 437.0,
+ "collateral": 25.29769,
+ "leverage": 2.0,
+ "liquidationPrice": 0.05945697,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2642.8,
+ "side": "short",
+ "contracts": 0.019,
+ "collateral": 25.091876,
+ "leverage": 2.0,
+ "liquidationPrice": 3924.18322043,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 155.89,
+ "side": "short",
+ "contracts": 0.32,
+ "collateral": 24.924941,
+ "leverage": 2.0,
+ "liquidationPrice": 228.07847866,
+ },
+ {
+ "symbol": "DOGE/USDC:USDC",
+ "entryPrice": 0.14333,
+ "side": "short",
+ "contracts": 351.0,
+ "collateral": 25.136807,
+ "leverage": 2.0,
+ "liquidationPrice": 0.20970228,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 68595.0,
+ "side": "short",
+ "contracts": 0.00069,
+ "collateral": 23.64871,
+ "leverage": 2.0,
+ "liquidationPrice": 101849.99354283,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 65536.0,
+ "side": "short",
+ "contracts": 0.00099,
+ "collateral": 21.604172,
+ "leverage": 3.0,
+ "liquidationPrice": 86493.46174617,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 173.06,
+ "side": "long",
+ "contracts": 0.6,
+ "collateral": 20.735658,
+ "leverage": 5.0,
+ "liquidationPrice": 142.05186667,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2545.5,
+ "side": "long",
+ "contracts": 0.0329,
+ "collateral": 20.909894,
+ "leverage": 4.0,
+ "liquidationPrice": 1929.23322895,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 67400.0,
+ "side": "short",
+ "contracts": 0.00031,
+ "collateral": 20.887308,
+ "leverage": 1.0,
+ "liquidationPrice": 133443.97317151,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2552.0,
+ "side": "short",
+ "contracts": 0.0327,
+ "collateral": 20.833393,
+ "leverage": 4.0,
+ "liquidationPrice": 3157.53150453,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 66930.0,
+ "side": "long",
+ "contracts": 0.0015,
+ "collateral": 20.043862,
+ "leverage": 5.0,
+ "liquidationPrice": 54108.51043771,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 67033.0,
+ "side": "long",
+ "contracts": 0.00121,
+ "collateral": 20.251817,
+ "leverage": 4.0,
+ "liquidationPrice": 50804.00091827,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2521.9,
+ "side": "long",
+ "contracts": 0.0237,
+ "collateral": 19.902091,
+ "leverage": 3.0,
+ "liquidationPrice": 1699.14071943,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 68139.0,
+ "side": "short",
+ "contracts": 0.00145,
+ "collateral": 19.72573,
+ "leverage": 5.0,
+ "liquidationPrice": 80933.61590987,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 178.29,
+ "side": "short",
+ "contracts": 0.11,
+ "collateral": 19.605036,
+ "leverage": 1.0,
+ "liquidationPrice": 347.82205322,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 176.23,
+ "side": "long",
+ "contracts": 0.33,
+ "collateral": 19.364946,
+ "leverage": 3.0,
+ "liquidationPrice": 120.56240404,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 173.08,
+ "side": "short",
+ "contracts": 0.33,
+ "collateral": 19.01881,
+ "leverage": 3.0,
+ "liquidationPrice": 225.08561715,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 68240.0,
+ "side": "short",
+ "contracts": 0.00105,
+ "collateral": 17.887922,
+ "leverage": 4.0,
+ "liquidationPrice": 84431.79820839,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2518.4,
+ "side": "short",
+ "contracts": 0.007,
+ "collateral": 17.62263,
+ "leverage": 1.0,
+ "liquidationPrice": 4986.05799151,
+ },
+ {
+ "symbol": "ETH/USDC:USDC",
+ "entryPrice": 2533.2,
+ "side": "long",
+ "contracts": 0.0347,
+ "collateral": 17.555195,
+ "leverage": 5.0,
+ "liquidationPrice": 2047.7642302,
+ },
+ {
+ "symbol": "DOGE/USDC:USDC",
+ "entryPrice": 0.13284,
+ "side": "long",
+ "contracts": 360.0,
+ "collateral": 15.943218,
+ "leverage": 3.0,
+ "liquidationPrice": 0.09082388,
+ },
+ {
+ "symbol": "SOL/USDC:USDC",
+ "entryPrice": 163.11,
+ "side": "short",
+ "contracts": 0.48,
+ "collateral": 15.650731,
+ "leverage": 5.0,
+ "liquidationPrice": 190.94213618,
+ },
+ {
+ "symbol": "BTC/USDC:USDC",
+ "entryPrice": 67141.0,
+ "side": "long",
+ "contracts": 0.00067,
+ "collateral": 14.979079,
+ "leverage": 3.0,
+ "liquidationPrice": 45236.52992613,
+ },
+ ]
+
+ api_mock = MagicMock()
+ default_conf["trading_mode"] = "futures"
+ default_conf["margin_mode"] = "isolated"
+ default_conf["stake_currency"] = "USDC"
+ api_mock.load_markets = get_mock_coro(return_value=markets)
+ exchange = get_patched_exchange(
+ mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=False
+ )
+
+ for position in positions:
+ is_short = True if position["side"] == "short" else False
+ liq_price_returned = position["liquidationPrice"]
+ liq_price_calculated = exchange.dry_run_liquidation_price(
+ position["symbol"],
+ position["entryPrice"],
+ is_short,
+ position["contracts"],
+ position["collateral"],
+ position["leverage"],
+ position["collateral"],
+ [],
+ )
+ assert pytest.approx(liq_price_returned, rel=0.0001) == liq_price_calculated
+
+
+def test_hyperliquid_get_funding_fees(default_conf, mocker):
+ now = datetime.now(timezone.utc)
+ exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
+ exchange._fetch_and_calculate_funding_fees = MagicMock()
+ exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
+ assert exchange._fetch_and_calculate_funding_fees.call_count == 0
+
+ default_conf["trading_mode"] = "futures"
+ default_conf["margin_mode"] = "isolated"
+ exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
+ exchange._fetch_and_calculate_funding_fees = MagicMock()
+ exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
+
+ assert exchange._fetch_and_calculate_funding_fees.call_count == 1
+
+
+def test_hyperliquid_get_max_leverage(default_conf, mocker):
+ markets = {
+ "BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
+ "ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
+ "SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
+ "DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
+ }
+ exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
+ assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 1.0
+
+ default_conf["trading_mode"] = "futures"
+ default_conf["margin_mode"] = "isolated"
+ exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
+ mocker.patch.multiple(
+ EXMS,
+ markets=PropertyMock(return_value=markets),
+ )
+
+ assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 50
+ assert exchange.get_max_leverage("ETH/USDC:USDC", 20) == 50
+ assert exchange.get_max_leverage("SOL/USDC:USDC", 50) == 20
+ assert exchange.get_max_leverage("DOGE/USDC:USDC", 3) == 20
+
+
+def test_hyperliquid__lev_prep(default_conf, mocker):
+ api_mock = MagicMock()
+ api_mock.set_margin_mode = MagicMock()
+ type(api_mock).has = PropertyMock(return_value={"setMarginMode": True})
+ exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
+ exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
+
+ assert api_mock.set_margin_mode.call_count == 0
+
+ # test in futures mode
+ api_mock.set_margin_mode.reset_mock()
+ default_conf["dry_run"] = False
+
+ default_conf["trading_mode"] = "futures"
+ default_conf["margin_mode"] = "isolated"
+
+ exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
+ exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
+
+ assert api_mock.set_margin_mode.call_count == 1
+ api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 3})
+
+ api_mock.reset_mock()
+
+ exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell")
+
+ assert api_mock.set_margin_mode.call_count == 1
+ api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 19})
diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py
index e9c594299..b815babec 100644
--- a/tests/exchange_online/conftest.py
+++ b/tests/exchange_online/conftest.py
@@ -339,6 +339,18 @@ EXCHANGES = {
},
],
},
+ "hyperliquid": {
+ "pair": "PURR/USDC",
+ "stake_currency": "USDC",
+ "hasQuoteVolume": False,
+ "timeframe": "1h",
+ "futures": True,
+ "orderbook_max_entries": 20,
+ "futures_pair": "BTC/USDC:USDC",
+ "hasQuoteVolumeFutures": True,
+ "leverage_tiers_public": False,
+ "leverage_in_spot_market": False,
+ },
}
diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py
index 32133d8e5..a64b8d310 100644
--- a/tests/exchange_online/test_ccxt_compat.py
+++ b/tests/exchange_online/test_ccxt_compat.py
@@ -118,9 +118,10 @@ class TestCCXTExchange:
tickers = exch.get_tickers()
assert pair in tickers
assert "ask" in tickers[pair]
- assert tickers[pair]["ask"] is not None
assert "bid" in tickers[pair]
- assert tickers[pair]["bid"] is not None
+ if EXCHANGES[exchangename].get("tickers_have_bid_ask"):
+ assert tickers[pair]["bid"] is not None
+ assert tickers[pair]["ask"] is not None
assert "quoteVolume" in tickers[pair]
if EXCHANGES[exchangename].get("hasQuoteVolume"):
assert tickers[pair]["quoteVolume"] is not None
@@ -150,9 +151,10 @@ class TestCCXTExchange:
ticker = exch.fetch_ticker(pair)
assert "ask" in ticker
- assert ticker["ask"] is not None
assert "bid" in ticker
- assert ticker["bid"] is not None
+ if EXCHANGES[exchangename].get("tickers_have_bid_ask"):
+ assert ticker["ask"] is not None
+ assert ticker["bid"] is not None
assert "quoteVolume" in ticker
if EXCHANGES[exchangename].get("hasQuoteVolume"):
assert ticker["quoteVolume"] is not None