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 ccxt](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