mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
@@ -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/)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](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] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [OKX](https://okx.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/)
|
||||
|
||||
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||
|
||||
|
||||
@@ -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"
|
||||
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||
|
||||
@@ -58,6 +58,7 @@ SUPPORTED_EXCHANGES = [
|
||||
"bybit",
|
||||
"gate",
|
||||
"htx",
|
||||
"hyperliquid",
|
||||
"kraken",
|
||||
"okx",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
374
tests/exchange/test_hyperliquid.py
Normal file
374
tests/exchange/test_hyperliquid.py
Normal file
@@ -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})
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user