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": "time", # Possible are "time" or "id"
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
"trades_has_history": False,
|
"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": None,
|
||||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||||
"mark_ohlcv_price": "mark",
|
"mark_ohlcv_price": "mark",
|
||||||
@@ -1274,6 +1275,8 @@ class Exchange:
|
|||||||
rate_for_order,
|
rate_for_order,
|
||||||
params,
|
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:
|
if order.get("status") is None:
|
||||||
# Map empty status to open.
|
# Map empty status to open.
|
||||||
order["status"] = "open"
|
order["status"] = "open"
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class FtHas(TypedDict, total=False):
|
|||||||
trades_pagination_arg: str
|
trades_pagination_arg: str
|
||||||
trades_has_history: bool
|
trades_has_history: bool
|
||||||
trades_pagination_overlap: bool
|
trades_pagination_overlap: bool
|
||||||
|
# Create order
|
||||||
|
create_order_has_all_data: bool
|
||||||
# Orderbook
|
# Orderbook
|
||||||
l2_limit_range: Optional[list[int]]
|
l2_limit_range: Optional[list[int]]
|
||||||
l2_limit_range_required: bool
|
l2_limit_range_required: bool
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"""Hyperliquid exchange subclass"""
|
"""Hyperliquid exchange subclass"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from freqtrade.enums import TradingMode
|
|
||||||
from freqtrade.exchange import Exchange
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -15,21 +19,146 @@ class Hyperliquid(Exchange):
|
|||||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: FtHas = {
|
_ft_has: Dict = {
|
||||||
# Only the most recent 5000 candles are available according to the
|
|
||||||
# exchange's API documentation.
|
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"ohlcv_candle_limit": 5000,
|
"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},
|
"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
|
@property
|
||||||
def _ccxt_config(self) -> dict:
|
def _ccxt_config(self) -> Dict:
|
||||||
# Parameters to add directly to ccxt sync/async initialization.
|
# ccxt Hyperliquid defaults to swap
|
||||||
# ccxt defaults to swap mode.
|
|
||||||
config = {}
|
config = {}
|
||||||
if self.trading_mode == TradingMode.SPOT:
|
if self.trading_mode == TradingMode.SPOT:
|
||||||
config.update({"options": {"defaultType": "spot"}})
|
config.update({"options": {"defaultType": "spot"}})
|
||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return 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
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ bottleneck==1.4.2
|
|||||||
numexpr==2.10.1
|
numexpr==2.10.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.4.20
|
ccxt==4.4.22
|
||||||
cryptography==42.0.8; platform_machine == 'armv7l'
|
cryptography==42.0.8; platform_machine == 'armv7l'
|
||||||
cryptography==43.0.3; platform_machine != 'armv7l'
|
cryptography==43.0.3; platform_machine != 'armv7l'
|
||||||
aiohttp==3.10.10
|
aiohttp==3.10.10
|
||||||
|
|||||||
Reference in New Issue
Block a user