mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 14:00:38 +00:00
compatible with HIP3
This commit is contained in:
@@ -9,7 +9,7 @@ from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, CcxtPosition, FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||
|
||||
|
||||
@@ -59,10 +59,169 @@ class Hyperliquid(Exchange):
|
||||
|
||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||
parent_check = super().market_is_tradable(market)
|
||||
hip3_dexes = self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
|
||||
# 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"]
|
||||
# Allow HIP-3 markets (with ':' in base) only if hip3_dexes configured
|
||||
if ":" in market["base"]:
|
||||
return parent_check and bool(hip3_dexes)
|
||||
|
||||
return parent_check
|
||||
|
||||
def _fetch_hip3_balances(self, hip3_dexes: list[str], base_balance: dict) -> dict:
|
||||
"""Fetch balances from configured HIP-3 DEXes and merge them."""
|
||||
logger.info(f"Fetching balances from {len(hip3_dexes)} HIP-3 DEX(es): {hip3_dexes}")
|
||||
|
||||
for dex in hip3_dexes:
|
||||
try:
|
||||
logger.debug(f"Fetching balance for HIP-3 DEX: {dex}")
|
||||
dex_balance = self._api.fetch_balance({"dex": dex})
|
||||
|
||||
if not dex_balance or "info" not in dex_balance:
|
||||
logger.error(
|
||||
f"HIP-3 DEX '{dex}' returned invalid response. Check configuration."
|
||||
)
|
||||
continue
|
||||
|
||||
base_balance = self._merge_hip3_balances(base_balance, dex_balance)
|
||||
|
||||
positions_count = len(dex_balance.get("info", {}).get("assetPositions", []))
|
||||
if positions_count > 0:
|
||||
logger.info(f"Merged {positions_count} position(s) from HIP-3 DEX '{dex}'")
|
||||
else:
|
||||
logger.warning(f"HIP-3 DEX '{dex}' returned no positions")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||
|
||||
return base_balance
|
||||
|
||||
def get_balances(self) -> dict:
|
||||
"""Fetch balance including HIP-3 DEX balances if configured."""
|
||||
balances = super().get_balances()
|
||||
|
||||
# Fetch raw balance info (CCXT normalizes and removes 'info')
|
||||
try:
|
||||
raw_balance = self._api.fetch_balance()
|
||||
if "info" in raw_balance:
|
||||
balances["info"] = raw_balance["info"]
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch raw balance info: {e}")
|
||||
|
||||
# Fetch HIP-3 DEX balances if configured
|
||||
hip3_dexes = self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
if hip3_dexes:
|
||||
balances = self._fetch_hip3_balances(hip3_dexes, balances)
|
||||
|
||||
return balances
|
||||
|
||||
def _merge_hip3_balances(self, base_balance: dict, new_balance: dict) -> dict:
|
||||
"""Merge balances from different Hyperliquid DEXes."""
|
||||
# Merge assetPositions (raw API response)
|
||||
if "info" in new_balance and "assetPositions" in new_balance.get("info", {}):
|
||||
if "info" not in base_balance:
|
||||
base_balance["info"] = {}
|
||||
if "assetPositions" not in base_balance["info"]:
|
||||
base_balance["info"]["assetPositions"] = []
|
||||
|
||||
new_positions = new_balance["info"]["assetPositions"]
|
||||
base_balance["info"]["assetPositions"].extend(new_positions)
|
||||
logger.debug(f"Merged {len(new_positions)} asset position(s) from HIP-3 DEX")
|
||||
|
||||
# Merge normalized CCXT balance structure (skip standard fields)
|
||||
for currency, amount_info in new_balance.items():
|
||||
if currency in ["info", "USDC", "free", "used", "total"]:
|
||||
continue
|
||||
|
||||
if isinstance(amount_info, dict):
|
||||
normalized_currency = currency.upper() if ":" in currency else currency
|
||||
base_balance[normalized_currency] = amount_info
|
||||
logger.debug(f"Added balance for currency: {normalized_currency}")
|
||||
|
||||
return base_balance
|
||||
|
||||
def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
|
||||
"""
|
||||
Fetch positions including HIP-3 positions from assetPositions.
|
||||
|
||||
Override standard fetch_positions to add HIP-3 equity positions
|
||||
which are not returned by the standard CCXT fetch_positions call.
|
||||
"""
|
||||
positions = super().fetch_positions(pair)
|
||||
|
||||
hip3_dexes = self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||
if not hip3_dexes:
|
||||
return positions
|
||||
|
||||
try:
|
||||
balances = self.get_balances()
|
||||
hip3_positions = self._parse_hip3_positions(balances)
|
||||
|
||||
if hip3_positions:
|
||||
positions.extend(hip3_positions)
|
||||
logger.debug(f"Added {len(hip3_positions)} HIP-3 position(s)")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch HIP-3 positions: {e}")
|
||||
|
||||
return positions
|
||||
|
||||
def _parse_hip3_positions(self, balances: dict) -> list[CcxtPosition]:
|
||||
"""
|
||||
Parse HIP-3 positions from balance info into CCXT position format.
|
||||
|
||||
HIP-3 positions are stored in assetPositions with a colon in the coin name
|
||||
(e.g., 'xyz:TSLA'). This method converts them to the standard CCXT format
|
||||
that Freqtrade expects.
|
||||
"""
|
||||
hip3_positions: list[CcxtPosition] = []
|
||||
|
||||
asset_positions = balances.get("info", {}).get("assetPositions", [])
|
||||
if not isinstance(asset_positions, list):
|
||||
return hip3_positions
|
||||
|
||||
for asset_pos in asset_positions:
|
||||
position = asset_pos.get("position", {})
|
||||
if not isinstance(position, dict):
|
||||
continue
|
||||
|
||||
coin = position.get("coin", "")
|
||||
if not coin or ":" not in coin:
|
||||
continue # Skip non-HIP-3 positions
|
||||
|
||||
szi = float(position.get("szi", 0))
|
||||
if szi == 0:
|
||||
continue # Skip empty positions
|
||||
|
||||
# Parse leverage
|
||||
leverage_info = position.get("leverage", {})
|
||||
if isinstance(leverage_info, dict):
|
||||
leverage = float(leverage_info.get("value", 1))
|
||||
else:
|
||||
leverage = 1.0
|
||||
|
||||
# Parse collateral
|
||||
collateral = float(position.get("marginUsed", 0))
|
||||
|
||||
# Convert to pair format: xyz:TSLA → XYZ-TSLA/USDC:USDC
|
||||
symbol = f"{coin.upper().replace(':', '-')}/USDC:USDC"
|
||||
side = "short" if szi < 0 else "long"
|
||||
contracts = abs(szi)
|
||||
|
||||
hip3_positions.append(
|
||||
{
|
||||
"symbol": symbol,
|
||||
"contracts": contracts,
|
||||
"side": side,
|
||||
"collateral": collateral,
|
||||
"initialMargin": collateral,
|
||||
"leverage": leverage,
|
||||
"liquidationPrice": None,
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(f"Parsed HIP-3 position: {symbol} = {contracts} contracts {side}")
|
||||
|
||||
return hip3_positions
|
||||
|
||||
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
||||
# There are no leverage tiers
|
||||
|
||||
Reference in New Issue
Block a user