Initial implementation of hyperliquid.

- Spot
- Swap (long, short, leverage, stoploss_on_exchange)
- dry_run_liquidation_price()
This commit is contained in:
gaardiolor
2024-10-25 17:24:59 +02:00
parent b8f8d1d4b1
commit b88db55db3
4 changed files with 144 additions and 10 deletions

View File

@@ -144,6 +144,7 @@ class Exchange:
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
"trades_has_history": False,
"create_order_has_all_data": True, # Set to False if create_order doesn't return all data
"l2_limit_range": None,
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark",
@@ -1274,6 +1275,8 @@ class Exchange:
rate_for_order,
params,
)
if not self._ft_has.get("create_order_has_all_data"):
order = self._api.fetch_order(order['id'], pair)
if order.get("status") is None:
# Map empty status to open.
order["status"] = "open"

View File

@@ -34,6 +34,8 @@ class FtHas(TypedDict, total=False):
trades_pagination_arg: str
trades_has_history: bool
trades_pagination_overlap: bool
# Create order
create_order_has_all_data: bool
# Orderbook
l2_limit_range: Optional[list[int]]
l2_limit_range_required: bool

View File

@@ -1,10 +1,14 @@
"""Hyperliquid exchange subclass"""
import logging
from typing import Optional
from freqtrade.enums import TradingMode
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
from typing import List, Tuple, Dict
from freqtrade.enums import MarginMode, TradingMode, CandleType
from freqtrade.exceptions import OperationalException, ExchangeError
from freqtrade.constants import BuySell
from datetime import datetime
logger = logging.getLogger(__name__)
@@ -15,21 +19,146 @@ class Hyperliquid(Exchange):
Contains adjustments needed for Freqtrade to work with this exchange.
"""
_ft_has: FtHas = {
# Only the most recent 5000 candles are available according to the
# exchange's API documentation.
_ft_has: Dict = {
"ohlcv_has_history": False,
"ohlcv_candle_limit": 5000,
"trades_has_history": False, # Trades endpoint doesn't seem available.
"orderbook_max_entries": 20,
"l2_limit_range": [20],
"trades_has_history": False,
"tickers_have_bid_ask": False,
"stoploss_on_exchange": True,
"exchange_has_overrides": {"fetchTrades": False},
"stoploss_order_types": {"limit": "limit"},
"funding_fee_timeframe": "1h",
"create_order_has_all_data": False
}
_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.
def _ccxt_config(self) -> Dict:
# 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: Optional[float]) -> 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: Optional[int] = None
) -> int:
# Funding rate candles have a different limit
if candle_type in 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
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
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 = stake_amount
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 deviding 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.
"""
# Bybit does not provide "applied" funding fees per position.
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

View File

@@ -4,7 +4,7 @@ bottleneck==1.4.2
numexpr==2.10.1
pandas-ta==0.3.14b
ccxt==4.4.20
ccxt==4.4.22
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==43.0.3; platform_machine != 'armv7l'
aiohttp==3.10.10