mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-02-14 10:10:59 +00:00
Initial implementation of hyperliquid.
- Spot - Swap (long, short, leverage, stoploss_on_exchange) - dry_run_liquidation_price()
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user