mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 05:50:36 +00:00
Merge pull request #12584 from igi01/feature/hyperliquid-hip3-support
Hyperliquid HIP3 support
This commit is contained in:
@@ -428,6 +428,36 @@ Your balance and trades will now be used from your vault / subaccount - and no l
|
||||
|
||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||
|
||||
### HIP-3 DEXes
|
||||
|
||||
Hyperliquid supports HIP-3 decentralized exchanges (DEXes), which are independent exchanges built on top of the Hyperliquid infrastructure.
|
||||
These DEXes operate similarly to the main Hyperliquid exchange but are community-created and managed.
|
||||
|
||||
To trade on HIP-3 DEXes with Freqtrade, you need to add them to your configuration using the `hip3_dexes` parameter:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_master_wallet_address",
|
||||
"privateKey": "your_api_private_key",
|
||||
"hip3_dexes": ["dex_name_1", "dex_name_2"]
|
||||
}
|
||||
```
|
||||
|
||||
Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEXes you want to trade on (e.g. `vntl` and `xyz`).
|
||||
|
||||
!!! Warning "Performance and Rate Limit Impact"
|
||||
Each HIP-3 DEX you add significantly impacts bot performance and rate limits.
|
||||
|
||||
* **Additional API Calls**: For each HIP-3 DEX configured, Freqtrade needs to make additional API calls.
|
||||
* **Rate Limit Pressure**: Additional API calls contribute to Hyperliquid's strict rate limits. With multiple DEXes, you may hit rate limits faster, or rather, slow down bot operations due to enforced delays.
|
||||
|
||||
Please only add HIP-3 DEXes that you actively trade on. Monitor your logs for rate limit warnings or signs of slowed operations, and adjust your configuration accordingly.
|
||||
Different HIP-3 DEXes may also use different quote currencies - so make sure to only add DEXes that are compatible with your stake currency to avoid unnecessary delays.
|
||||
|
||||
!!! Note
|
||||
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
|
||||
|
||||
## Bitvavo
|
||||
|
||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||
|
||||
@@ -1827,16 +1827,16 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
try:
|
||||
balances = self._api.fetch_balance()
|
||||
balances = self._api.fetch_balance(params or {})
|
||||
# Remove additional info from ccxt results
|
||||
balances.pop("info", None)
|
||||
balances.pop("free", None)
|
||||
balances.pop("total", None)
|
||||
balances.pop("used", None)
|
||||
|
||||
self._log_exchange_response("fetch_balance", balances)
|
||||
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
@@ -1848,7 +1848,9 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""
|
||||
Fetch positions from the exchange.
|
||||
If no pair is given, all positions are returned.
|
||||
@@ -1860,7 +1862,7 @@ class Exchange:
|
||||
symbols = None
|
||||
if pair:
|
||||
symbols = [pair]
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
|
||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols, params=params or {})
|
||||
self._log_exchange_response("fetch_positions", positions)
|
||||
return positions
|
||||
except ccxt.DDoSProtection as e:
|
||||
|
||||
@@ -7,9 +7,10 @@ from typing import Any
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||
|
||||
|
||||
@@ -57,12 +58,108 @@ class Hyperliquid(Exchange):
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||
"""Get list of configured HIP-3 DEXes."""
|
||||
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
|
||||
def validate_config(self, config: dict) -> None:
|
||||
"""Validate HIP-3 configuration at bot startup."""
|
||||
super().validate_config(config)
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured or not self.markets:
|
||||
return
|
||||
if self.trading_mode != TradingMode.FUTURES:
|
||||
if configured:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes are only supported in FUTURES trading mode. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
return
|
||||
if configured and self.margin_mode != MarginMode.ISOLATED:
|
||||
raise ConfigurationError(
|
||||
"HIP-3 DEXes require 'isolated' margin mode. "
|
||||
f"Current margin mode: '{self.margin_mode.value}'. "
|
||||
"Please update your configuration!"
|
||||
)
|
||||
|
||||
available = {
|
||||
m.get("info", {}).get("dex")
|
||||
for m in self.get_markets(
|
||||
quote_currencies=[self._config["stake_currency"]],
|
||||
tradable_only=True,
|
||||
active_only=True,
|
||||
).values()
|
||||
if m.get("info", {}).get("hip3")
|
||||
}
|
||||
available.discard(None)
|
||||
|
||||
invalid = set(configured) - available
|
||||
if invalid:
|
||||
raise ConfigurationError(
|
||||
f"Invalid HIP-3 DEXes configured: {sorted(invalid)}. "
|
||||
f"Available DEXes matching your stake currency ({self._config['stake_currency']}): "
|
||||
f"{sorted(available)}. "
|
||||
f"Check your 'hip3_dexes' configuration!"
|
||||
)
|
||||
|
||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||
"""Check if market is tradable, including HIP-3 markets."""
|
||||
parent_check = super().market_is_tradable(market)
|
||||
|
||||
# Exclude hip3 markets for now - which have the format XYZ:GOOGL/USDT:USDT -
|
||||
# and XYZ:GOOGL as base
|
||||
return parent_check and ":" not in market["base"]
|
||||
market_info = market.get("info", {})
|
||||
if market_info.get("hip3") and self._config["runmode"] in NON_UTIL_MODES:
|
||||
configured = self._get_configured_hip3_dexes()
|
||||
if not configured:
|
||||
return False
|
||||
|
||||
market_dex = market_info.get("dex")
|
||||
return parent_check and market_dex in configured
|
||||
|
||||
return parent_check
|
||||
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
"""Fetch balances from default DEX and HIP-3 DEXes needed by tradable pairs.
|
||||
This override is not absolutely necessary and is only there for correct used / total values
|
||||
which are however not used by Freqtrade in futures mode at the moment.
|
||||
"""
|
||||
balances = super().get_balances()
|
||||
dexes = self._get_configured_hip3_dexes()
|
||||
for dex in dexes:
|
||||
try:
|
||||
dex_balance = super().get_balances(params={"dex": dex})
|
||||
|
||||
for currency, amount_info in dex_balance.items():
|
||||
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||
continue
|
||||
|
||||
if currency not in balances:
|
||||
balances[currency] = amount_info
|
||||
else:
|
||||
balances[currency]["free"] += amount_info["free"]
|
||||
balances[currency]["used"] += amount_info["used"]
|
||||
balances[currency]["total"] += amount_info["total"]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||
return balances
|
||||
|
||||
def fetch_positions(
|
||||
self, pair: str | None = None, params: dict | None = None
|
||||
) -> list[CcxtPosition]:
|
||||
"""Fetch positions from default DEX and HIP-3 DEXes needed by tradable pairs."""
|
||||
positions = super().fetch_positions(pair)
|
||||
dexes = self._get_configured_hip3_dexes()
|
||||
for dex in dexes:
|
||||
try:
|
||||
positions.extend(super().fetch_positions(pair, params={"dex": dex}))
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch positions from HIP-3 DEX '{dex}': {e}")
|
||||
if dexes:
|
||||
self._log_exchange_response("fetch_positions", positions, add_info="combined")
|
||||
return positions
|
||||
|
||||
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
||||
# There are no leverage tiers
|
||||
|
||||
@@ -70,7 +70,7 @@ class Kraken(Exchange):
|
||||
return consolidated
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||
if self._config["dry_run"]:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -1,21 +1,125 @@
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def markets_hip3():
|
||||
markets = {
|
||||
"BTC/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "BTC",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 50}},
|
||||
"info": {},
|
||||
},
|
||||
"ETH/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "ETH",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 50}},
|
||||
"info": {},
|
||||
},
|
||||
"SOL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "SOL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 20}},
|
||||
"info": {},
|
||||
},
|
||||
"DOGE/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "DOGE",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 20}},
|
||||
"info": {},
|
||||
},
|
||||
"XYZ-AAPL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-AAPL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-TSLA/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-TSLA",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-GOOGL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-GOOGL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-NVDA/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-NVDA",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"VNTL-SPACEX/USDH:USDH": {
|
||||
"quote": "USDH",
|
||||
"base": "VNTL-SPACEX",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "vntl"},
|
||||
},
|
||||
"VNTL-ANTHROPIC/USDH:USDH": {
|
||||
"quote": "USDH",
|
||||
"base": "VNTL-ANTHROPIC",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "vntl"},
|
||||
},
|
||||
"FLX-TOKEN/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "FLX-TOKEN",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "flx"},
|
||||
},
|
||||
}
|
||||
|
||||
return markets
|
||||
|
||||
|
||||
@pytest.mark.parametrize("margin_mode", ["isolated", "cross"])
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker, margin_mode):
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, markets_hip3, mocker, margin_mode):
|
||||
# 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",
|
||||
@@ -278,6 +382,33 @@ def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker, margin_mode
|
||||
"leverage": 3.0,
|
||||
"liquidationPrice": 45236.52992613,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-AAPL/USDC:USDC",
|
||||
"entryPrice": 250.0,
|
||||
"side": "long",
|
||||
"contracts": 0.5,
|
||||
"collateral": 25.0,
|
||||
"leverage": 5.0,
|
||||
"liquidationPrice": 210.5263157894737,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-GOOGL/USDC:USDC",
|
||||
"entryPrice": 190.0,
|
||||
"side": "short",
|
||||
"contracts": 0.5,
|
||||
"collateral": 9.5,
|
||||
"leverage": 10.0,
|
||||
"liquidationPrice": 199.04761904761904,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-TSLA/USDC:USDC",
|
||||
"entryPrice": 350.0,
|
||||
"side": "long",
|
||||
"contracts": 1.0,
|
||||
"collateral": 50.0,
|
||||
"leverage": 7.0,
|
||||
"liquidationPrice": 315.7894736842105,
|
||||
},
|
||||
]
|
||||
|
||||
api_mock = MagicMock()
|
||||
@@ -285,7 +416,7 @@ def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker, margin_mode
|
||||
default_conf["margin_mode"] = margin_mode
|
||||
default_conf["stake_currency"] = "USDC"
|
||||
api_mock.load_markets = get_mock_coro()
|
||||
api_mock.markets = markets
|
||||
api_mock.markets = markets_hip3
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
@@ -331,41 +462,62 @@ def test_hyperliquid_get_funding_fees(default_conf, mocker):
|
||||
now = datetime.now(UTC)
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock()
|
||||
|
||||
# Spot mode - no funding fees
|
||||
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"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
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)
|
||||
|
||||
# Normal market
|
||||
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
|
||||
# HIP-3 XYZ market
|
||||
exchange._fetch_and_calculate_funding_fees.reset_mock()
|
||||
exchange.get_funding_fees("XYZ-TSLA/USDC:USDC", 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
|
||||
# HIP-3 VNTL market
|
||||
exchange._fetch_and_calculate_funding_fees.reset_mock()
|
||||
exchange.get_funding_fees("VNTL-SPACEX/USDH:USDH", 1, True, 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}}},
|
||||
}
|
||||
def test_hyperliquid_get_max_leverage(default_conf, mocker, markets_hip3):
|
||||
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),
|
||||
)
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets_hip3))
|
||||
|
||||
# Normal 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
|
||||
|
||||
# HIP-3 markets
|
||||
assert exchange.get_max_leverage("XYZ-TSLA/USDC:USDC", 1) == 10
|
||||
assert exchange.get_max_leverage("XYZ-NVDA/USDC:USDC", 5) == 10
|
||||
assert exchange.get_max_leverage("VNTL-SPACEX/USDH:USDH", 2) == 3
|
||||
assert exchange.get_max_leverage("VNTL-ANTHROPIC/USDH:USDH", 1) == 3
|
||||
|
||||
|
||||
def test_hyperliquid__lev_prep(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
@@ -382,25 +534,59 @@ def test_hyperliquid__lev_prep(default_conf, mocker):
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
|
||||
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
|
||||
|
||||
# Normal market
|
||||
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})
|
||||
|
||||
# HIP-3 XYZ market
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("XYZ-TSLA/USDC:USDC", 5.7, "buy")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 5})
|
||||
|
||||
def test_hyperliquid_fetch_order(default_conf_usdt, mocker):
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("XYZ-TSLA/USDC:USDC", 10.0, "sell")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 10})
|
||||
|
||||
# HIP-3 VNTL market
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("VNTL-SPACEX/USDH:USDH", 2.5, "buy")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with(
|
||||
"isolated", "VNTL-SPACEX/USDH:USDH", {"leverage": 2}
|
||||
)
|
||||
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("VNTL-ANTHROPIC/USDH:USDH", 3.0, "sell")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with(
|
||||
"isolated", "VNTL-ANTHROPIC/USDH:USDH", {"leverage": 3}
|
||||
)
|
||||
|
||||
|
||||
def test_hyperliquid_fetch_order(default_conf_usdt, mocker, markets_hip3):
|
||||
default_conf_usdt["dry_run"] = False
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
api_mock = MagicMock()
|
||||
|
||||
# Test with normal market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "12345",
|
||||
@@ -432,9 +618,293 @@ def test_hyperliquid_fetch_order(default_conf_usdt, mocker):
|
||||
},
|
||||
],
|
||||
)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange="hyperliquid")
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
o = exchange.fetch_order("12345", "ETH/USDC:USDC")
|
||||
# Uses weighted average
|
||||
assert o["average"] == 1500
|
||||
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
# Test with HIP-3 XYZ market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "67890",
|
||||
"symbol": "XYZ-TSLA/USDC:USDC",
|
||||
"status": "closed",
|
||||
"filled": 2.5,
|
||||
"average": None,
|
||||
"timestamp": 1630000100,
|
||||
}
|
||||
)
|
||||
gtfo_mock.reset_mock()
|
||||
gtfo_mock.return_value = [
|
||||
{
|
||||
"order_id": "67890",
|
||||
"price": 250,
|
||||
"amount": 1.5,
|
||||
"filled": 1.5,
|
||||
"remaining": 0,
|
||||
},
|
||||
{
|
||||
"order_id": "67890",
|
||||
"price": 260,
|
||||
"amount": 1.0,
|
||||
"filled": 1.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
]
|
||||
|
||||
o = exchange.fetch_order("67890", "XYZ-TSLA/USDC:USDC")
|
||||
# Weighted average: (250*1.5 + 260*1.0) / 2.5 = 254
|
||||
assert o["average"] == 254
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
# Test with HIP-3 VNTL market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "11111",
|
||||
"symbol": "VNTL-SPACEX/USDH:USDH",
|
||||
"status": "closed",
|
||||
"filled": 5.0,
|
||||
"average": None,
|
||||
"timestamp": 1630000200,
|
||||
}
|
||||
)
|
||||
gtfo_mock.reset_mock()
|
||||
gtfo_mock.return_value = [
|
||||
{
|
||||
"order_id": "11111",
|
||||
"price": 100,
|
||||
"amount": 3.0,
|
||||
"filled": 3.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
{
|
||||
"order_id": "11111",
|
||||
"price": 105,
|
||||
"amount": 2.0,
|
||||
"filled": 2.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
]
|
||||
|
||||
o = exchange.fetch_order("11111", "VNTL-SPACEX/USDH:USDH")
|
||||
assert o["average"] == 102
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
|
||||
def test_hyperliquid_hip3_config_validation(default_conf_usdt, mocker, markets_hip3):
|
||||
"""Test HIP-3 DEX configuration validation."""
|
||||
|
||||
api_mock = MagicMock()
|
||||
default_conf_usdt["stake_currency"] = "USDC"
|
||||
|
||||
# Futures mode, no dex configured
|
||||
default_conf_copy = deepcopy(default_conf_usdt)
|
||||
default_conf_copy["trading_mode"] = "futures"
|
||||
default_conf_copy["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_copy, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_copy)
|
||||
|
||||
# Not in futures mode - no dex configured - no error
|
||||
get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
# Not in futures mode
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
with pytest.raises(
|
||||
ConfigurationError, match=r"HIP-3 DEXes are only supported in FUTURES trading mode\."
|
||||
):
|
||||
get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
# Valid single DEX
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
assert exchange._get_configured_hip3_dexes() == ["xyz"]
|
||||
|
||||
# Invalid DEX
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["invalid_dex"]
|
||||
with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
# Mix of valid and invalid DEX
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "invalid_dex"]
|
||||
with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
default_conf_usdt["margin_mode"] = "cross"
|
||||
with pytest.raises(ConfigurationError, match="HIP-3 DEXes require 'isolated' margin mode"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
|
||||
def test_hyperliquid_get_balances_hip3(default_conf, mocker, caplog, markets_hip3):
|
||||
"""Test balance fetching from HIP-3 DEXes."""
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.load_markets = get_mock_coro()
|
||||
|
||||
# Mock balance responses
|
||||
default_balance = {"USDC": {"free": 1000, "used": 0, "total": 1000}}
|
||||
xyz_balance = {"USDC": {"free": 0, "used": 600, "total": 600}}
|
||||
vntl_balance = {"USDH": {"free": 0, "used": 300, "total": 300}}
|
||||
|
||||
def fetch_balance_side_effect(params=None):
|
||||
if params and params.get("dex") == "xyz":
|
||||
return xyz_balance
|
||||
elif params and params.get("dex") == "vntl":
|
||||
return vntl_balance
|
||||
elif params and params.get("dex") == "flx":
|
||||
raise Exception("FLX DEX error")
|
||||
return default_balance
|
||||
|
||||
api_mock.fetch_balance = MagicMock(side_effect=fetch_balance_side_effect)
|
||||
|
||||
# Test with two HIP-3 DEXes
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
|
||||
balances = exchange.get_balances()
|
||||
|
||||
# Should have combined balances
|
||||
assert balances["USDC"]["free"] == 1000
|
||||
assert balances["USDC"]["used"] == 600
|
||||
assert balances["USDC"]["total"] == 1600
|
||||
assert balances["USDH"]["free"] == 0
|
||||
assert balances["USDH"]["used"] == 300
|
||||
assert balances["USDH"]["total"] == 300
|
||||
|
||||
assert api_mock.fetch_balance.call_count == 4
|
||||
assert log_has_re("Could not fetch balance for HIP-3 DEX.*", caplog)
|
||||
|
||||
|
||||
def test_hyperliquid_fetch_positions_hip3(default_conf, mocker, caplog, markets_hip3):
|
||||
"""Test position fetching from HIP-3 DEXes."""
|
||||
api_mock = MagicMock()
|
||||
|
||||
# Mock position responses
|
||||
default_positions = [{"symbol": "BTC/USDC:USDC", "contracts": 0.5}]
|
||||
xyz_positions = [{"symbol": "XYZ-AAPL/USDC:USDC", "contracts": 10}]
|
||||
vntl_positions = [{"symbol": "VNTL-SPACEX/USDH:USDH", "contracts": 5}]
|
||||
|
||||
def fetch_positions_side_effect(symbols=None, params=None):
|
||||
if params and params.get("dex") == "xyz":
|
||||
return xyz_positions
|
||||
elif params and params.get("dex") == "vntl":
|
||||
return vntl_positions
|
||||
elif params and params.get("dex") == "flx":
|
||||
raise Exception("FLX DEX error")
|
||||
return default_positions
|
||||
|
||||
positions_mock = MagicMock(side_effect=fetch_positions_side_effect)
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]
|
||||
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
|
||||
# Mock super().fetch_positions() to return default positions
|
||||
mocker.patch(f"{EXMS}.fetch_positions", positions_mock)
|
||||
|
||||
positions = exchange.fetch_positions()
|
||||
|
||||
assert log_has_re("Could not fetch positions from HIP-3 .*", caplog)
|
||||
|
||||
# Should have all positions combined (default + HIP-3)
|
||||
assert len(positions) == 3
|
||||
assert any(p["symbol"] == "BTC/USDC:USDC" for p in positions)
|
||||
assert any(p["symbol"] == "XYZ-AAPL/USDC:USDC" for p in positions)
|
||||
assert any(p["symbol"] == "VNTL-SPACEX/USDH:USDH" for p in positions)
|
||||
|
||||
# Verify API calls (xyz + vntl, default is mocked separately)
|
||||
assert positions_mock.call_count == 4
|
||||
|
||||
|
||||
def test_hyperliquid_market_is_tradable(default_conf_usdt, mocker, markets_hip3):
|
||||
"""Test market_is_tradable filters HIP-3 markets correctly."""
|
||||
default_conf_usdt["stake_currency"] = "USDC"
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = get_mock_coro(return_value=markets_hip3)
|
||||
api_mock.markets = markets_hip3
|
||||
# Mock parent call - we only want to test hyperliquid specifics here.
|
||||
mocker.patch(f"{EXMS}.market_is_tradable", return_value=True)
|
||||
|
||||
# Test 1: No HIP-3 DEXes configured - only default markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = []
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
# Test 2: Only 'xyz' configured - default + xyz markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
# Test 3: 'xyz' and 'vntl' configured - default + xyz + vntl markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "flx"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is True
|
||||
|
||||
# Use USDH stake currency to enable VNTL markets
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["vntl"]
|
||||
default_conf_usdt["stake_currency"] = "USDH"
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
@@ -523,6 +523,8 @@ EXCHANGES = {
|
||||
"candle_count": 5000,
|
||||
"orderbook_max_entries": 20,
|
||||
"futures_pair": "BTC/USDC:USDC",
|
||||
# Assert that HIP3 pairs are fetched as part of load_markets
|
||||
"futures_alt_pairs": ["XYZ-NVDA/USDC:USDC", "VNTL-ANTHROPIC/USDH:USDH"],
|
||||
"hasQuoteVolumeFutures": True,
|
||||
"leverage_tiers_public": False,
|
||||
"leverage_in_spot_market": False,
|
||||
|
||||
@@ -67,12 +67,14 @@ class TestCCXTExchange:
|
||||
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
pair = EXCHANGES[exchangename].get("futures_pair", pair)
|
||||
pair1 = EXCHANGES[exchangename].get("futures_pair", pair)
|
||||
alternative_pairs = EXCHANGES[exchangename].get("futures_alt_pairs", [])
|
||||
markets = exchange.markets
|
||||
assert pair in markets
|
||||
assert isinstance(markets[pair], dict)
|
||||
for pair in [pair1] + alternative_pairs:
|
||||
assert pair in markets, f"Futures pair {pair} not found in markets"
|
||||
assert isinstance(markets[pair], dict)
|
||||
|
||||
assert exchange.market_is_future(markets[pair])
|
||||
assert exchange.market_is_future(markets[pair])
|
||||
|
||||
def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchange_name = exchange
|
||||
|
||||
Reference in New Issue
Block a user