# -*- coding: utf-8 -*- # PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code from ccxt.async_support.base.exchange import Exchange from ccxt.abstract.hyperliquid import ImplicitAPI import asyncio import math from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, MarginModification, Market, Num, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry from typing import List from ccxt.base.errors import ExchangeError from ccxt.base.errors import ArgumentsRequired from ccxt.base.errors import BadRequest from ccxt.base.errors import InsufficientFunds from ccxt.base.errors import InvalidOrder from ccxt.base.errors import OrderNotFound from ccxt.base.errors import NotSupported from ccxt.base.decimal_to_precision import ROUND from ccxt.base.decimal_to_precision import DECIMAL_PLACES from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.precise import Precise class hyperliquid(Exchange, ImplicitAPI): def describe(self) -> Any: return self.deep_extend(super(hyperliquid, self).describe(), { 'id': 'hyperliquid', 'name': 'Hyperliquid', 'countries': [], 'version': 'v1', 'rateLimit': 50, # 1200 requests per minute, 20 request per second 'certified': True, 'pro': True, 'dex': True, 'has': { 'CORS': None, 'spot': True, 'margin': False, 'swap': True, 'future': True, 'option': False, 'addMargin': True, 'borrowCrossMargin': False, 'borrowIsolatedMargin': False, 'cancelAllOrders': False, 'cancelAllOrdersAfter': True, 'cancelOrder': True, 'cancelOrders': True, 'cancelOrdersForSymbols': True, 'closeAllPositions': False, 'closePosition': False, 'createMarketBuyOrderWithCost': False, 'createMarketOrderWithCost': False, 'createMarketSellOrderWithCost': False, 'createOrder': True, 'createOrders': True, 'createOrderWithTakeProfitAndStopLoss': True, 'createReduceOnlyOrder': True, 'createStopOrder': True, 'createTriggerOrder': True, 'editOrder': True, 'editOrders': True, 'fetchAccounts': False, 'fetchBalance': True, 'fetchBorrowInterest': False, 'fetchBorrowRateHistories': False, 'fetchBorrowRateHistory': False, 'fetchCanceledAndClosedOrders': True, 'fetchCanceledOrders': True, 'fetchClosedOrders': True, 'fetchCrossBorrowRate': False, 'fetchCrossBorrowRates': False, 'fetchCurrencies': True, 'fetchDepositAddress': False, 'fetchDepositAddresses': False, 'fetchDeposits': True, 'fetchDepositWithdrawFee': 'emulated', 'fetchDepositWithdrawFees': False, 'fetchFundingHistory': True, 'fetchFundingRate': False, 'fetchFundingRateHistory': True, 'fetchFundingRates': True, 'fetchIndexOHLCV': False, 'fetchIsolatedBorrowRate': False, 'fetchIsolatedBorrowRates': False, 'fetchLedger': True, 'fetchLeverage': False, 'fetchLeverageTiers': False, 'fetchLiquidations': False, 'fetchMarginMode': None, 'fetchMarketLeverageTiers': False, 'fetchMarkets': True, 'fetchMarkOHLCV': False, 'fetchMyLiquidations': False, 'fetchMyTrades': True, 'fetchOHLCV': True, 'fetchOpenInterest': True, 'fetchOpenInterestHistory': False, 'fetchOpenInterests': True, 'fetchOpenOrders': True, 'fetchOrder': True, 'fetchOrderBook': True, 'fetchOrders': True, 'fetchOrderTrades': False, 'fetchPosition': True, 'fetchPositionMode': False, 'fetchPositions': True, 'fetchPositionsRisk': False, 'fetchPremiumIndexOHLCV': False, 'fetchTicker': 'emulated', 'fetchTickers': True, 'fetchTime': False, 'fetchTrades': True, 'fetchTradingFee': True, 'fetchTradingFees': False, 'fetchTransfer': False, 'fetchTransfers': False, 'fetchWithdrawal': False, 'fetchWithdrawals': True, 'reduceMargin': True, 'repayCrossMargin': False, 'repayIsolatedMargin': False, 'sandbox': True, 'setLeverage': True, 'setMarginMode': True, 'setPositionMode': False, 'transfer': True, 'withdraw': True, }, 'timeframes': { '1m': '1m', '3m': '3m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1h', '2h': '2h', '4h': '4h', '8h': '8h', '12h': '12h', '1d': '1d', '3d': '3d', '1w': '1w', '1M': '1M', }, 'hostname': 'hyperliquid.xyz', 'urls': { 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b', 'api': { 'public': 'https://api.{hostname}', 'private': 'https://api.{hostname}', }, 'test': { 'public': 'https://api.hyperliquid-testnet.xyz', 'private': 'https://api.hyperliquid-testnet.xyz', }, 'www': 'https://hyperliquid.xyz', 'doc': 'https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api', 'fees': 'https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees', 'referral': 'https://app.hyperliquid.xyz/', }, 'api': { 'public': { 'post': { 'info': { 'cost': 20, 'byType': { 'l2Book': 2, 'allMids': 2, 'clearinghouseState': 2, 'orderStatus': 2, 'spotClearinghouseState': 2, 'exchangeStatus': 2, 'candleSnapshot': 4, }, }, }, }, 'private': { 'post': { 'exchange': 1, }, }, }, 'fees': { 'swap': { 'taker': self.parse_number('0.00045'), 'maker': self.parse_number('0.00015'), }, 'spot': { 'taker': self.parse_number('0.0007'), 'maker': self.parse_number('0.0004'), }, }, 'requiredCredentials': { 'apiKey': False, 'secret': False, 'walletAddress': True, 'privateKey': True, }, 'exceptions': { 'exact': { }, 'broad': { 'Price must be divisible by tick size.': InvalidOrder, 'Order must have minimum value of $10': InvalidOrder, 'Insufficient margin to place order.': InsufficientFunds, 'Reduce only order would increase position.': InvalidOrder, 'Post only order would have immediately matched,': InvalidOrder, 'Order could not immediately match against any resting orders.': InvalidOrder, 'Invalid TP/SL price.': InvalidOrder, 'No liquidity available for market order.': InvalidOrder, 'Order was never placed, already canceled, or filled.': OrderNotFound, 'User or API Wallet ': InvalidOrder, 'Order has invalid size': InvalidOrder, 'Order price cannot be more than 80% away from the reference price': InvalidOrder, 'Order has zero size.': InvalidOrder, 'Insufficient spot balance asset': InsufficientFunds, 'Insufficient balance for withdrawal': InsufficientFunds, 'Insufficient balance for token transfer': InsufficientFunds, }, }, 'precisionMode': TICK_SIZE, 'commonCurrencies': { }, 'options': { 'defaultType': 'swap', 'sandboxMode': False, 'defaultSlippage': 0.05, 'zeroAddress': '0x0000000000000000000000000000000000000000', 'spotCurrencyMapping': { 'UDZ': '2Z', 'UBONK': 'BONK', 'UBTC': 'BTC', 'UETH': 'ETH', 'UFART': 'FARTCOIN', 'HPENGU': 'PENGU', 'UPUMP': 'PUMP', 'USOL': 'SOL', 'UUUSPX': 'SPX', 'USDT0': 'USDT', 'XAUT0': 'XAUT', 'UXPL': 'XPL', }, }, 'features': { 'default': { 'sandbox': True, 'createOrder': { 'marginMode': False, 'triggerPrice': False, 'triggerPriceType': None, 'triggerDirection': False, 'stopLossPrice': False, 'takeProfitPrice': False, 'attachedStopLossTakeProfit': { 'triggerPriceType': { 'last': False, 'mark': False, 'index': False, }, 'triggerPrice': True, 'type': True, 'price': True, }, 'timeInForce': { 'IOC': True, 'FOK': False, 'PO': True, 'GTD': False, }, 'hedged': False, 'trailing': False, 'leverage': False, 'marketBuyByCost': False, 'marketBuyRequiresPrice': False, 'selfTradePrevention': False, 'iceberg': False, }, 'createOrders': { 'max': 1000, }, 'fetchMyTrades': { 'marginMode': False, 'limit': 2000, 'daysBack': None, 'untilDays': None, 'symbolRequired': True, }, 'fetchOrder': { 'marginMode': False, 'trigger': False, 'trailing': False, 'symbolRequired': True, }, 'fetchOpenOrders': { 'marginMode': False, 'limit': 2000, 'trigger': False, 'trailing': False, 'symbolRequired': True, }, 'fetchOrders': { 'marginMode': False, 'limit': 2000, 'daysBack': None, 'untilDays': None, 'trigger': False, 'trailing': False, 'symbolRequired': True, }, 'fetchClosedOrders': { 'marginMode': False, 'limit': 2000, 'daysBack': None, 'daysBackCanceled': None, 'untilDays': None, 'trigger': False, 'trailing': False, 'symbolRequired': True, }, 'fetchOHLCV': { 'limit': 5000, }, }, 'spot': { 'extends': 'default', }, 'forPerps': { 'extends': 'default', 'createOrder': { 'stopLossPrice': True, 'takeProfitPrice': True, 'attachedStopLossTakeProfit': None, # todo, in two orders }, }, 'swap': { 'linear': { 'extends': 'forPerps', }, 'inverse': { 'extends': 'forPerps', }, }, 'future': { 'linear': { 'extends': 'forPerps', }, 'inverse': { 'extends': 'forPerps', }, }, }, }) def set_sandbox_mode(self, enabled): super(hyperliquid, self).set_sandbox_mode(enabled) self.options['sandboxMode'] = enabled def market(self, symbol: str) -> MarketInterface: if self.markets is None: raise ExchangeError(self.id + ' markets not loaded') if symbol in self.markets: market = self.markets[symbol] if market['spot']: baseName = self.safe_string(market, 'baseName') spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) if baseName in spotCurrencyMapping: unifiedBaseName = self.safe_string(spotCurrencyMapping, baseName) quote = self.safe_string(market, 'quote') newSymbol = self.safe_currency_code(unifiedBaseName) + '/' + quote if newSymbol in self.markets: return self.markets[newSymbol] res = super(hyperliquid, self).market(symbol) return res def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: if marketId is not None: if (self.markets_by_id is not None) and (marketId in self.markets_by_id): markets = self.markets_by_id[marketId] numMarkets = len(markets) if numMarkets == 1: return markets[0] else: if numMarkets > 2: raise ExchangeError(self.id + ' safeMarket() found more than two markets with the same market id ' + marketId) firstMarket = markets[0] secondMarket = markets[1] if self.safe_string(firstMarket, 'type') != self.safe_string(secondMarket, 'type'): raise ExchangeError(self.id + ' safeMarket() found two different market types with the same market id ' + marketId) baseCurrency = self.safe_string(firstMarket, 'base') spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) if baseCurrency in spotCurrencyMapping: return secondMarket return firstMarket return super(hyperliquid, self).safe_market(marketId, market, delimiter, marketType) async def fetch_currencies(self, params={}) -> Currencies: """ fetches all available currencies on an exchange https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-metadata :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an associative dictionary of currencies """ if self.check_required_credentials(False): await self.initialize_client() request: dict = { 'type': 'meta', } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "universe": [ # { # "maxLeverage": 50, # "name": "SOL", # "onlyIsolated": False, # "szDecimals": 2 # } # ] # } # ] # meta = self.safe_list(response, 'universe', []) result: dict = {} for i in range(0, len(meta)): data = self.safe_dict(meta, i, {}) id = i name = self.safe_string(data, 'name') code = self.safe_currency_code(name) result[code] = self.safe_currency_structure({ 'id': id, 'name': name, 'code': code, 'precision': None, 'info': data, 'active': None, 'deposit': None, 'withdraw': None, 'networks': None, 'fee': None, 'type': 'crypto', 'limits': { 'amount': { 'min': None, 'max': None, }, 'withdraw': { 'min': None, 'max': None, }, }, }) return result async def fetch_markets(self, params={}) -> List[Market]: """ retrieves data on all markets for hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ rawPromises = [ self.fetch_swap_markets(params), self.fetch_spot_markets(params), ] promises = await asyncio.gather(*rawPromises) swapMarkets = promises[0] spotMarkets = promises[1] return self.array_concat(swapMarkets, spotMarkets) async def fetch_swap_markets(self, params={}) -> List[Market]: """ retrieves data on all swap markets for hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ request: dict = { 'type': 'metaAndAssetCtxs', } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "universe": [ # { # "maxLeverage": 50, # "name": "SOL", # "onlyIsolated": False, # "szDecimals": 2 # } # ] # }, # [ # { # "dayNtlVlm": "9450588.2273", # "funding": "0.0000198", # "impactPxs": [ # "108.04", # "108.06" # ], # "markPx": "108.04", # "midPx": "108.05", # "openInterest": "10764.48", # "oraclePx": "107.99", # "premium": "0.00055561", # "prevDayPx": "111.81" # } # ] # ] # # meta = self.safe_dict(response, 0, {}) universe = self.safe_list(meta, 'universe', []) assetCtxs = self.safe_list(response, 1, []) result = [] for i in range(0, len(universe)): data = self.extend( self.safe_dict(universe, i, {}), self.safe_dict(assetCtxs, i, {}) ) data['baseId'] = i result.append(data) return self.parse_markets(result) def calculate_price_precision(self, price: float, amountPrecision: float, maxDecimals: float): """ Helper function to calculate the Hyperliquid DECIMAL_PLACES price precision :param float price: the price to use in the calculation :param int amountPrecision: the amountPrecision to use in the calculation :param int maxDecimals: the maxDecimals to use in the calculation :returns int: The calculated price precision """ pricePrecision = 0 priceStr = self.number_to_string(price) if priceStr is None: return 0 priceSplitted = priceStr.split('.') if Precise.string_eq(priceStr, '0'): # Significant digits is always hasattr(self, 5) case significantDigits = 5 # Integer digits is always hasattr(self, 0) case(0 doesn't count) integerDigits = 0 # Calculate the price precision pricePrecision = min(maxDecimals - amountPrecision, significantDigits - integerDigits) elif Precise.string_gt(priceStr, '0') and Precise.string_lt(priceStr, '1'): # Significant digits, always hasattr(self, 5) case significantDigits = 5 # Get the part after the decimal separator decimalPart = self.safe_string(priceSplitted, 1, '') # Count the number of leading zeros in the decimal part leadingZeros = 0 while((leadingZeros <= len(decimalPart)) and (decimalPart[leadingZeros] == '0')): leadingZeros = leadingZeros + 1 # Calculate price precision based on leading zeros and significant digits pricePrecision = leadingZeros + significantDigits # Calculate the price precision based on maxDecimals - szDecimals and the calculated price precision from the previous step pricePrecision = min(maxDecimals - amountPrecision, pricePrecision) else: # Count the numbers before the decimal separator integerPart = self.safe_string(priceSplitted, 0, '') # Get significant digits, take the max() of 5 and the integer digits count significantDigits = max(5, len(integerPart)) # Calculate price precision based on maxDecimals - szDecimals and significantDigits - len(integerPart) pricePrecision = min(maxDecimals - amountPrecision, significantDigits - len(integerPart)) return self.parse_to_int(pricePrecision) async def fetch_spot_markets(self, params={}) -> List[Market]: """ retrieves data on all spot markets for hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ request: dict = { 'type': 'spotMetaAndAssetCtxs', } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "tokens": [ # { # "name": "USDC", # "szDecimals": 8, # "weiDecimals" 8, # "index": 0, # "tokenId": "0x6d1e7cde53ba9467b783cb7c530ce054", # "isCanonical": True, # "evmContract":null, # "fullName":null # }, # { # "name": "PURR", # "szDecimals": 0, # "weiDecimals": 5, # "index": 1, # "tokenId": "0xc1fb593aeffbeb02f85e0308e9956a90", # "isCanonical": True, # "evmContract":null, # "fullName":null # } # ], # "universe": [ # { # "name": "PURR/USDC", # "tokens": [1, 0], # "index": 0, # "isCanonical": True # } # ] # }, # [ # { # "dayNtlVlm":"8906.0", # "markPx":"0.14", # "midPx":"0.209265", # "prevDayPx":"0.20432" # } # ] # ] # first = self.safe_dict(response, 0, {}) second = self.safe_list(response, 1, []) meta = self.safe_list(first, 'universe', []) tokens = self.safe_list(first, 'tokens', []) markets = [] for i in range(0, len(meta)): market = self.safe_dict(meta, i, {}) index = self.safe_integer(market, 'index') extraData = self.safe_dict(second, index, {}) marketName = self.safe_string(market, 'name') # if marketName.find('/') < 0: # # there are some weird spot markets in testnet, eg @2 # continue # } # marketParts = marketName.split('/') # baseName = self.safe_string(marketParts, 0) # quoteId = self.safe_string(marketParts, 1) fees = self.safe_dict(self.fees, 'spot', {}) taker = self.safe_number(fees, 'taker') maker = self.safe_number(fees, 'maker') tokensPos = self.safe_list(market, 'tokens', []) baseTokenPos = self.safe_integer(tokensPos, 0) quoteTokenPos = self.safe_integer(tokensPos, 1) baseTokenInfo = self.safe_dict(tokens, baseTokenPos, {}) quoteTokenInfo = self.safe_dict(tokens, quoteTokenPos, {}) baseName = self.safe_string(baseTokenInfo, 'name') quoteId = self.safe_string(quoteTokenInfo, 'name') # do spot currency mapping spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) mappedBaseName = self.safe_string(spotCurrencyMapping, baseName, baseName) mappedQuoteId = self.safe_string(spotCurrencyMapping, quoteId, quoteId) mappedBase = self.safe_currency_code(mappedBaseName) mappedQuote = self.safe_currency_code(mappedQuoteId) mappedSymbol = mappedBase + '/' + mappedQuote innerBaseTokenInfo = self.safe_dict(baseTokenInfo, 'spec', baseTokenInfo) # innerQuoteTokenInfo = self.safe_dict(quoteTokenInfo, 'spec', quoteTokenInfo) amountPrecisionStr = self.safe_string(innerBaseTokenInfo, 'szDecimals') amountPrecision = int(amountPrecisionStr) price = self.safe_number(extraData, 'midPx') pricePrecision = 0 if price is not None: pricePrecision = self.calculate_price_precision(price, amountPrecision, 8) pricePrecisionStr = self.number_to_string(pricePrecision) # quotePrecision = self.parse_number(self.parse_precision(self.safe_string(innerQuoteTokenInfo, 'szDecimals'))) baseId = self.number_to_string(index + 10000) entry = { 'id': marketName, 'symbol': mappedSymbol, 'base': mappedBase, 'quote': mappedQuote, 'settle': None, 'baseId': baseId, 'baseName': baseName, 'quoteId': quoteId, 'settleId': None, 'type': 'spot', 'spot': True, 'subType': None, 'margin': None, 'swap': False, 'future': False, 'option': False, 'active': True, 'contract': False, 'linear': None, 'inverse': None, 'taker': taker, 'maker': maker, 'contractSize': None, 'expiry': None, 'expiryDatetime': None, 'strike': None, 'optionType': None, 'precision': { 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), }, 'limits': { 'leverage': { 'min': None, 'max': None, }, 'amount': { 'min': None, 'max': None, }, 'price': { 'min': None, 'max': None, }, 'cost': { 'min': self.parse_number('10'), 'max': None, }, }, 'created': None, 'info': self.extend(extraData, market), } markets.append(self.safe_market_structure(entry)) # backward support base = self.safe_currency_code(baseName) quote = self.safe_currency_code(quoteId) newEntry = self.extend({}, entry) symbol = base + '/' + quote if symbol != mappedSymbol: newEntry['symbol'] = symbol newEntry['base'] = base newEntry['quote'] = quote newEntry['baseName'] = baseName markets.append(self.safe_market_structure(newEntry)) return markets def parse_market(self, market: dict) -> Market: # # { # "maxLeverage": "50", # "name": "ETH", # "onlyIsolated": False, # "szDecimals": "4", # "dayNtlVlm": "1709813.11535", # "funding": "0.00004807", # "impactPxs": [ # "2369.3", # "2369.6" # ], # "markPx": "2369.6", # "midPx": "2369.45", # "openInterest": "1815.4712", # "oraclePx": "2367.3", # "premium": "0.00090821", # "prevDayPx": "2381.5" # } # quoteId = 'USDC' baseName = self.safe_string(market, 'name') base = self.safe_currency_code(baseName) quote = self.safe_currency_code(quoteId) baseId = self.safe_string(market, 'baseId') settleId = 'USDC' settle = self.safe_currency_code(settleId) symbol = base + '/' + quote contract = True swap = True if contract: if swap: symbol = symbol + ':' + settle fees = self.safe_dict(self.fees, 'swap', {}) taker = self.safe_number(fees, 'taker') maker = self.safe_number(fees, 'maker') amountPrecisionStr = self.safe_string(market, 'szDecimals') amountPrecision = int(amountPrecisionStr) price = self.safe_number(market, 'markPx', 0) pricePrecision = 0 if price is not None: pricePrecision = self.calculate_price_precision(price, amountPrecision, 6) pricePrecisionStr = self.number_to_string(pricePrecision) isDelisted = self.safe_bool(market, 'isDelisted') active = True if isDelisted is not None: active = not isDelisted return self.safe_market_structure({ 'id': baseId, 'symbol': symbol, 'base': base, 'quote': quote, 'settle': settle, 'baseId': baseId, 'baseName': baseName, 'quoteId': quoteId, 'settleId': settleId, 'type': 'swap', 'spot': False, 'margin': None, 'swap': swap, 'future': False, 'option': False, 'active': active, 'contract': contract, 'linear': True, 'inverse': False, 'taker': taker, 'maker': maker, 'contractSize': self.parse_number('1'), 'expiry': None, 'expiryDatetime': None, 'strike': None, 'optionType': None, 'precision': { 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), }, 'limits': { 'leverage': { 'min': None, 'max': self.safe_integer(market, 'maxLeverage'), }, 'amount': { 'min': None, 'max': None, }, 'price': { 'min': None, 'max': None, }, 'cost': { 'min': self.parse_number('10'), 'max': None, }, }, 'created': None, 'info': market, }) async def fetch_balance(self, params={}) -> Balances: """ query for balance and get the amount of funds available for trading or funds locked in orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.type]: wallet type, ['spot', 'swap'], defaults to swap :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None :param str [params.subAccountAddress]: sub account user address :returns dict: a `balance structure ` """ userAddress = None userAddress, params = self.handle_public_address('fetchBalance', params) type = None type, params = self.handle_market_type_and_params('fetchBalance', None, params) marginMode = None marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) isSpot = (type == 'spot') request: dict = { 'type': 'spotClearinghouseState' if (isSpot) else 'clearinghouseState', 'user': userAddress, } response = await self.publicPostInfo(self.extend(request, params)) # # { # "assetPositions": [], # "crossMaintenanceMarginUsed": "0.0", # "crossMarginSummary": { # "accountValue": "100.0", # "totalMarginUsed": "0.0", # "totalNtlPos": "0.0", # "totalRawUsd": "100.0" # }, # "marginSummary": { # "accountValue": "100.0", # "totalMarginUsed": "0.0", # "totalNtlPos": "0.0", # "totalRawUsd": "100.0" # }, # "time": "1704261007014", # "withdrawable": "100.0" # } # spot # # { # "balances":[ # { # "coin":"USDC", # "hold":"0.0", # "total":"1481.844" # }, # { # "coin":"PURR", # "hold":"0.0", # "total":"999.65004" # } # } # balances = self.safe_list(response, 'balances') if balances is not None: spotBalances: dict = {'info': response} for i in range(0, len(balances)): balance = balances[i] code = self.safe_currency_code(self.safe_string(balance, 'coin')) account = self.account() total = self.safe_string(balance, 'total') used = self.safe_string(balance, 'hold') account['total'] = total account['used'] = used spotBalances[code] = account return self.safe_balance(spotBalances) data = self.safe_dict(response, 'marginSummary', {}) usdcBalance = { 'total': self.safe_number(data, 'accountValue'), } if (marginMode is not None) and (marginMode == 'isolated'): usdcBalance['free'] = self.safe_number(response, 'withdrawable') else: usdcBalance['used'] = self.safe_number(data, 'totalMarginUsed') result: dict = { 'info': response, 'USDC': usdcBalance, } timestamp = self.safe_integer(response, 'time') result['timestamp'] = timestamp result['datetime'] = self.iso8601(timestamp) return self.safe_balance(result) async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: """ fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot :param str symbol: unified symbol of the market to fetch the order book for :param int [limit]: the maximum amount of order book entries to return :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order book structures ` indexed by market symbols """ await self.load_markets() market = self.market(symbol) request: dict = { 'type': 'l2Book', 'coin': market['baseName'] if market['swap'] else market['id'], } response = await self.publicPostInfo(self.extend(request, params)) # # { # "coin": "ETH", # "levels": [ # [ # { # "n": "2", # "px": "2216.2", # "sz": "74.0637" # } # ], # [ # { # "n": "2", # "px": "2216.5", # "sz": "70.5893" # } # ] # ], # "time": "1704290104840" # } # data = self.safe_list(response, 'levels', []) result: dict = { 'bids': self.safe_list(data, 0, []), 'asks': self.safe_list(data, 1, []), } timestamp = self.safe_integer(response, 'time') return self.parse_order_book(result, market['symbol'], timestamp, 'bids', 'asks', 'px', 'sz') async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.type]: 'spot' or 'swap', by default fetches both :returns dict: a dictionary of `ticker structures ` """ await self.load_markets() symbols = self.market_symbols(symbols) # at self stage, to get tickers data, we use fetchMarkets endpoints response = [] type = self.safe_string(params, 'type') params = self.omit(params, 'type') if type == 'spot': response = await self.fetch_spot_markets(params) elif type == 'swap': response = await self.fetch_swap_markets(params) else: response = await self.fetch_markets(params) # same response "fetchMarkets" result: dict = {} for i in range(0, len(response)): market = response[i] info = market['info'] ticker = self.parse_ticker(info, market) symbol = self.safe_string(ticker, 'symbol') result[symbol] = ticker return self.filter_by_array_tickers(result, 'symbol', symbols) async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: """ retrieves data on all swap markets for hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc :param str[] [symbols]: list of unified market symbols :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ request: dict = { 'type': 'metaAndAssetCtxs', } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "universe": [ # { # "maxLeverage": 50, # "name": "SOL", # "onlyIsolated": False, # "szDecimals": 2 # } # ] # }, # [ # { # "dayNtlVlm": "9450588.2273", # "funding": "0.0000198", # "impactPxs": [ # "108.04", # "108.06" # ], # "markPx": "108.04", # "midPx": "108.05", # "openInterest": "10764.48", # "oraclePx": "107.99", # "premium": "0.00055561", # "prevDayPx": "111.81" # } # ] # ] # # meta = self.safe_dict(response, 0, {}) universe = self.safe_list(meta, 'universe', []) assetCtxs = self.safe_list(response, 1, []) result = [] for i in range(0, len(universe)): data = self.extend( self.safe_dict(universe, i, {}), self.safe_dict(assetCtxs, i, {}) ) result.append(data) return self.parse_funding_rates(result, symbols) def parse_funding_rate(self, info, market: Market = None) -> FundingRate: # # { # "maxLeverage": "50", # "name": "ETH", # "onlyIsolated": False, # "szDecimals": "4", # "dayNtlVlm": "1709813.11535", # "funding": "0.00004807", # "impactPxs": [ # "2369.3", # "2369.6" # ], # "markPx": "2369.6", # "midPx": "2369.45", # "openInterest": "1815.4712", # "oraclePx": "2367.3", # "premium": "0.00090821", # "prevDayPx": "2381.5" # } # base = self.safe_string(info, 'name') marketId = self.coin_to_market_id(base) symbol = self.safe_symbol(marketId, market) funding = self.safe_number(info, 'funding') markPx = self.safe_number(info, 'markPx') oraclePx = self.safe_number(info, 'oraclePx') fundingTimestamp = (int(math.floor(self.milliseconds()) / 60 / 60 / 1000) + 1) * 60 * 60 * 1000 return { 'info': info, 'symbol': symbol, 'markPrice': markPx, 'indexPrice': oraclePx, 'interestRate': None, 'estimatedSettlePrice': None, 'timestamp': None, 'datetime': None, 'fundingRate': funding, 'fundingTimestamp': fundingTimestamp, 'fundingDatetime': self.iso8601(fundingTimestamp), 'nextFundingRate': None, 'nextFundingTimestamp': None, 'nextFundingDatetime': None, 'previousFundingRate': None, 'previousFundingTimestamp': None, 'previousFundingDatetime': None, 'interval': '1h', } def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: # # { # "prevDayPx": "3400.5", # "dayNtlVlm": "511297257.47936022", # "markPx": "3464.7", # "midPx": "3465.05", # "oraclePx": "3460.1", # only in swap # "openInterest": "64638.1108", # only in swap # "premium": "0.00141614", # only in swap # "funding": "0.00008727", # only in swap # "impactPxs": ["3465.0", "3465.1"], # only in swap # "coin": "PURR", # only in spot # "circulatingSupply": "998949190.03400207", # only in spot # }, # bidAsk = self.safe_list(ticker, 'impactPxs') return self.safe_ticker({ 'symbol': market['symbol'], 'timestamp': None, 'datetime': None, 'previousClose': self.safe_number(ticker, 'prevDayPx'), 'close': self.safe_number(ticker, 'midPx'), 'bid': self.safe_number(bidAsk, 0), 'ask': self.safe_number(bidAsk, 1), 'quoteVolume': self.safe_number(ticker, 'dayNtlVlm'), 'info': ticker, }, market) async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: """ fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot :param str symbol: unified symbol of the market to fetch OHLCV data for :param str timeframe: the length of time each candle represents, support '1m', '15m', '1h', '1d' :param int [since]: timestamp in ms of the earliest candle to fetch :param int [limit]: the maximum amount of candles to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest candle to fetch :returns int[][]: A list of candles ordered, open, high, low, close, volume """ await self.load_markets() market = self.market(symbol) until = self.safe_integer(params, 'until', self.milliseconds()) useTail = since is None originalSince = since if since is None: if limit is not None: # optimization if limit is provided timeframeInMilliseconds = self.parse_timeframe(timeframe) * 1000 since = self.sum(until, timeframeInMilliseconds * limit * -1) if since < 0: since = 0 useTail = False else: since = 0 params = self.omit(params, ['until']) request: dict = { 'type': 'candleSnapshot', 'req': { 'coin': market['baseName'] if market['swap'] else market['id'], 'interval': self.safe_string(self.timeframes, timeframe, timeframe), 'startTime': since, 'endTime': until, }, } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "T": 1704287699999, # "c": "2226.4", # "h": "2247.9", # "i": "15m", # "l": "2224.6", # "n": 46, # "o": "2247.9", # "s": "ETH", # "t": 1704286800000, # "v": "591.6427" # } # ] # return self.parse_ohlcvs(response, market, timeframe, originalSince, limit, useTail) def parse_ohlcv(self, ohlcv, market: Market = None) -> list: # # { # "T": 1704287699999, # "c": "2226.4", # "h": "2247.9", # "i": "15m", # "l": "2224.6", # "n": 46, # "o": "2247.9", # "s": "ETH", # "t": 1704286800000, # "v": "591.6427" # } # return [ self.safe_integer(ohlcv, 't'), self.safe_number(ohlcv, 'o'), self.safe_number(ohlcv, 'h'), self.safe_number(ohlcv, 'l'), self.safe_number(ohlcv, 'c'), self.safe_number(ohlcv, 'v'), ] async def fetch_trades(self, symbol: Str, since: Int = None, limit: Int = None, params={}): """ get the list of most recent trades for a particular symbol https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch trades for :param int [limit]: the maximum number of trades structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest trade :param str [params.address]: wallet address that made trades :param str [params.user]: wallet address that made trades :param str [params.subAccountAddress]: sub account user address :returns Trade[]: a list of `trade structures ` """ userAddress = None userAddress, params = self.handle_public_address('fetchTrades', params) await self.load_markets() market = self.safe_market(symbol) request: dict = { 'user': userAddress, } if since is not None: request['type'] = 'userFillsByTime' request['startTime'] = since else: request['type'] = 'userFills' until = self.safe_integer(params, 'until') params = self.omit(params, 'until') if until is not None: request['endTime'] = until response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "closedPnl": "0.19343", # "coin": "ETH", # "crossed": True, # "dir": "Close Long", # "fee": "0.050062", # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", # "liquidationMarkPx": null, # "oid": 3929354691, # "px": "2381.1", # "side": "A", # "startPosition": "0.0841", # "sz": "0.0841", # "tid": 128423918764978, # "time": 1704262888911 # } # ] # return self.parse_trades(response, market, since, limit) def amount_to_precision(self, symbol, amount): market = self.market(symbol) return self.decimal_to_precision(amount, ROUND, market['precision']['amount'], self.precisionMode, self.paddingMode) def price_to_precision(self, symbol: str, price) -> str: market = self.market(symbol) priceStr = self.number_to_string(price) integerPart = priceStr.split('.')[0] significantDigits = max(5, len(integerPart)) result = self.decimal_to_precision(price, ROUND, significantDigits, SIGNIFICANT_DIGITS, self.paddingMode) maxDecimals = 8 if market['spot'] else 6 subtractedValue = maxDecimals - self.precision_from_string(self.safe_string(market['precision'], 'amount')) return self.decimal_to_precision(result, ROUND, subtractedValue, DECIMAL_PLACES, self.paddingMode) def hash_message(self, message): return '0x' + self.hash(message, 'keccak', 'hex') def sign_hash(self, hash, privateKey): signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) return { 'r': '0x' + signature['r'], 's': '0x' + signature['s'], 'v': self.sum(27, signature['v']), } def sign_message(self, message, privateKey): return self.sign_hash(self.hash_message(message), privateKey[-64:]) def construct_phantom_agent(self, hash, isTestnet=True): source = 'b' if (isTestnet) else 'a' return { 'source': source, 'connectionId': hash, } def action_hash(self, action, vaultAddress, nonce): dataBinary = self.packb(action) dataHex = self.binary_to_base16(dataBinary) data = dataHex data += '00000' + self.int_to_base16(nonce) if vaultAddress is None: data += '00' else: data += '01' data += vaultAddress return self.hash(self.base16_to_binary(data), 'keccak', 'binary') def sign_l1_action(self, action, nonce, vaultAdress=None) -> object: hash = self.action_hash(action, vaultAdress, nonce) isTestnet = self.safe_bool(self.options, 'sandboxMode', False) phantomAgent = self.construct_phantom_agent(hash, isTestnet) # data: Dict = { # 'domain': { # 'chainId': 1337, # 'name': 'Exchange', # 'verifyingContract': '0x0000000000000000000000000000000000000000', # 'version': '1', # }, # 'types': { # 'Agent': [ # {'name': 'source', 'type': 'string'}, # {'name': 'connectionId', 'type': 'bytes32'}, # ], # 'EIP712Domain': [ # {'name': 'name', 'type': 'string'}, # {'name': 'version', 'type': 'string'}, # {'name': 'chainId', 'type': 'uint256'}, # {'name': 'verifyingContract', 'type': 'address'}, # ], # }, # 'primaryType': 'Agent', # 'message': phantomAgent, # } zeroAddress = self.safe_string(self.options, 'zeroAddress') chainId = 1337 # check self out domain: dict = { 'chainId': chainId, 'name': 'Exchange', 'verifyingContract': zeroAddress, 'version': '1', } messageTypes: dict = { 'Agent': [ {'name': 'source', 'type': 'string'}, {'name': 'connectionId', 'type': 'bytes32'}, ], } msg = self.eth_encode_structured_data(domain, messageTypes, phantomAgent) signature = self.sign_message(msg, self.privateKey) return signature def sign_user_signed_action(self, messageTypes, message): zeroAddress = self.safe_string(self.options, 'zeroAddress') chainId = 421614 # check self out domain: dict = { 'chainId': chainId, 'name': 'HyperliquidSignTransaction', 'verifyingContract': zeroAddress, 'version': '1', } msg = self.eth_encode_structured_data(domain, messageTypes, message) signature = self.sign_message(msg, self.privateKey) return signature def build_usd_send_sig(self, message): messageTypes: dict = { 'HyperliquidTransaction:UsdSend': [ {'name': 'hyperliquidChain', 'type': 'string'}, {'name': 'destination', 'type': 'string'}, {'name': 'amount', 'type': 'string'}, {'name': 'time', 'type': 'uint64'}, ], } return self.sign_user_signed_action(messageTypes, message) def build_usd_class_send_sig(self, message): messageTypes: dict = { 'HyperliquidTransaction:UsdClassTransfer': [ {'name': 'hyperliquidChain', 'type': 'string'}, {'name': 'amount', 'type': 'string'}, {'name': 'toPerp', 'type': 'bool'}, {'name': 'nonce', 'type': 'uint64'}, ], } return self.sign_user_signed_action(messageTypes, message) def build_withdraw_sig(self, message): messageTypes: dict = { 'HyperliquidTransaction:Withdraw': [ {'name': 'hyperliquidChain', 'type': 'string'}, {'name': 'destination', 'type': 'string'}, {'name': 'amount', 'type': 'string'}, {'name': 'time', 'type': 'uint64'}, ], } return self.sign_user_signed_action(messageTypes, message) def build_approve_builder_fee_sig(self, message): messageTypes: dict = { 'HyperliquidTransaction:ApproveBuilderFee': [ {'name': 'hyperliquidChain', 'type': 'string'}, {'name': 'maxFeeRate', 'type': 'string'}, {'name': 'builder', 'type': 'address'}, {'name': 'nonce', 'type': 'uint64'}, ], } return self.sign_user_signed_action(messageTypes, message) async def set_ref(self): if self.safe_bool(self.options, 'refSet', False): return True self.options['refSet'] = True action = { 'type': 'setReferrer', 'code': self.safe_string(self.options, 'ref', 'CCXT1'), } nonce = self.milliseconds() signature = self.sign_l1_action(action, nonce) request: dict = { 'action': action, 'nonce': nonce, 'signature': signature, } response = None try: response = await self.privatePostExchange(request) return response except Exception as e: response = None # ignore self return response async def approve_builder_fee(self, builder: str, maxFeeRate: str): nonce = self.milliseconds() isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) payload: dict = { 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', 'maxFeeRate': maxFeeRate, 'builder': builder, 'nonce': nonce, } sig = self.build_approve_builder_fee_sig(payload) action = { 'hyperliquidChain': payload['hyperliquidChain'], 'signatureChainId': '0x66eee', 'maxFeeRate': payload['maxFeeRate'], 'builder': payload['builder'], 'nonce': nonce, 'type': 'approveBuilderFee', } request: dict = { 'action': action, 'nonce': nonce, 'signature': sig, 'vaultAddress': None, } # # { # "status": "ok", # "response": { # "type": "default" # } # } # return await self.privatePostExchange(request) async def initialize_client(self): try: await asyncio.gather(*[self.handle_builder_fee_approval(), self.set_ref()]) except Exception as e: return False return True async def handle_builder_fee_approval(self): buildFee = self.safe_bool(self.options, 'builderFee', True) if not buildFee: return False # skip if builder fee is not enabled approvedBuilderFee = self.safe_bool(self.options, 'approvedBuilderFee', False) if approvedBuilderFee: return True # skip if builder fee is already approved try: builder = self.safe_string(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') maxFeeRate = self.safe_string(self.options, 'feeRate', '0.01%') await self.approve_builder_fee(builder, maxFeeRate) self.options['approvedBuilderFee'] = True except Exception as e: self.options['builderFee'] = False # disable builder fee if an error occurs return True async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): """ create a trade order https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order :param str symbol: unified symbol of the market to create an order in :param str type: 'market' or 'limit' :param str side: 'buy' or 'sell' :param float amount: how much of currency you want to trade in units of base currency :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' :param bool [params.postOnly]: True or False whether the order is post-only :param bool [params.reduceOnly]: True or False whether the order is reduce-only :param float [params.triggerPrice]: The price at which a trigger order is triggered at :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) :param str [params.slippage]: the slippage for market order :param str [params.vaultAddress]: the vault address for order :param str [params.subAccountAddress]: sub account user address :returns dict: an `order structure ` """ await self.load_markets() order, globalParams = self.parse_create_edit_order_args(None, symbol, type, side, amount, price, params) orders = await self.create_orders([order], globalParams) return orders[0] async def create_orders(self, orders: List[OrderRequest], params={}): """ create a list of trade orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an `order structure ` """ await self.load_markets() await self.initialize_client() request = self.create_orders_request(orders, params) response = await self.privatePostExchange(request) # # { # "status": "ok", # "response": { # "type": "order", # "data": { # "statuses": [ # { # "resting": { # "oid": 5063830287 # } # } # ] # } # } # } # responseObj = self.safe_dict(response, 'response', {}) data = self.safe_dict(responseObj, 'data', {}) statuses = self.safe_list(data, 'statuses', []) ordersToBeParsed = [] for i in range(0, len(statuses)): order = statuses[i] if order == 'waitingForTrigger': ordersToBeParsed.append({'status': order}) # tp/sl orders can return a string like "waitingForTrigger", else: ordersToBeParsed.append(order) return self.parse_orders(ordersToBeParsed, None) def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: str, price: Str = None, params={}): market = self.market(symbol) type = type.upper() side = side.upper() isMarket = (type == 'MARKET') isBuy = (side == 'BUY') clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') slippage = self.safe_string(params, 'slippage') defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' postOnly = self.safe_bool(params, 'postOnly', False) if postOnly: defaultTimeInForce = 'alo' timeInForce = self.safe_string_lower(params, 'timeInForce', defaultTimeInForce) timeInForce = self.capitalize(timeInForce) triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') stopLossPrice = self.safe_string(params, 'stopLossPrice', triggerPrice) takeProfitPrice = self.safe_string(params, 'takeProfitPrice') isTrigger = (stopLossPrice or takeProfitPrice) px = None if isMarket: if price is None: raise ArgumentsRequired(self.id + ' market orders require price to calculate the max slippage price. Default slippage can be set in options(default is 5%).') px = Precise.string_mul(price, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(price, Precise.string_sub('1', slippage)) px = self.price_to_precision(symbol, px) # round after adding slippage else: px = self.price_to_precision(symbol, price) sz = self.amount_to_precision(symbol, amount) reduceOnly = self.safe_bool(params, 'reduceOnly', False) orderType: dict = {} if isTrigger: isTp = False if takeProfitPrice is not None: triggerPrice = self.price_to_precision(symbol, takeProfitPrice) isTp = True else: triggerPrice = self.price_to_precision(symbol, stopLossPrice) orderType['trigger'] = { 'isMarket': isMarket, 'triggerPx': triggerPrice, 'tpsl': 'tp' if (isTp) else 'sl', } else: orderType['limit'] = { 'tif': timeInForce, } params = self.omit(params, ['clientOrderId', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce', 'client_id', 'reduceOnly', 'postOnly']) orderObj: dict = { 'a': self.parse_to_int(market['baseId']), 'b': isBuy, 'p': px, 's': sz, 'r': reduceOnly, 't': orderType, # 'c': clientOrderId, } if clientOrderId is not None: orderObj['c'] = clientOrderId return orderObj def create_orders_request(self, orders, params={}) -> dict: """ create a list of trade orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params :returns dict: an `order structure ` """ self.check_required_credentials() defaultSlippage = self.safe_string(self.options, 'defaultSlippage') defaultSlippage = self.safe_string(params, 'slippage', defaultSlippage) hasClientOrderId = False for i in range(0, len(orders)): rawOrder = orders[i] orderParams = self.safe_dict(rawOrder, 'params', {}) clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') if clientOrderId is not None: hasClientOrderId = True if hasClientOrderId: for i in range(0, len(orders)): rawOrder = orders[i] orderParams = self.safe_dict(rawOrder, 'params', {}) clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') if clientOrderId is None: raise ArgumentsRequired(self.id + ' createOrders() all orders must have clientOrderId if at least one has a clientOrderId') params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) nonce = self.milliseconds() orderReq = [] grouping = 'na' for i in range(0, len(orders)): rawOrder = orders[i] marketId = self.safe_string(rawOrder, 'symbol') market = self.market(marketId) symbol = market['symbol'] type = self.safe_string_upper(rawOrder, 'type') side = self.safe_string_upper(rawOrder, 'side') amount = self.safe_string(rawOrder, 'amount') price = self.safe_string(rawOrder, 'price') orderParams = self.safe_dict(rawOrder, 'params', {}) slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) orderParams['slippage'] = slippage stopLoss = self.safe_value(orderParams, 'stopLoss') takeProfit = self.safe_value(orderParams, 'takeProfit') isTrigger = (stopLoss or takeProfit) orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) mainOrderObj: dict = self.create_order_request(symbol, type, side, amount, price, orderParams) orderReq.append(mainOrderObj) if isTrigger: # grouping opposed orders for sl/tp stopLossOrderTriggerPrice = self.safe_string_n(stopLoss, ['triggerPrice', 'stopPrice']) stopLossOrderType = self.safe_string(stopLoss, 'type', 'limit') stopLossOrderLimitPrice = self.safe_string_n(stopLoss, ['price', 'stopLossPrice'], stopLossOrderTriggerPrice) takeProfitOrderTriggerPrice = self.safe_string_n(takeProfit, ['triggerPrice', 'stopPrice']) takeProfitOrderType = self.safe_string(takeProfit, 'type', 'limit') takeProfitOrderLimitPrice = self.safe_string_n(takeProfit, ['price', 'takeProfitPrice'], takeProfitOrderTriggerPrice) grouping = 'normalTpsl' orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) triggerOrderSide = '' if side == 'BUY': triggerOrderSide = 'sell' else: triggerOrderSide = 'buy' if takeProfit is not None: orderObj: dict = self.create_order_request(symbol, takeProfitOrderType, triggerOrderSide, amount, takeProfitOrderLimitPrice, self.extend(orderParams, { 'takeProfitPrice': takeProfitOrderTriggerPrice, 'reduceOnly': True, })) orderReq.append(orderObj) if stopLoss is not None: orderObj: dict = self.create_order_request(symbol, stopLossOrderType, triggerOrderSide, amount, stopLossOrderLimitPrice, self.extend(orderParams, { 'stopLossPrice': stopLossOrderTriggerPrice, 'reduceOnly': True, })) orderReq.append(orderObj) vaultAddress = None vaultAddress, params = self.handle_option_and_params(params, 'createOrder', 'vaultAddress') vaultAddress = self.format_vault_address(vaultAddress) orderAction: dict = { 'type': 'order', 'orders': orderReq, 'grouping': grouping, } if self.safe_bool(self.options, 'approvedBuilderFee', False): wallet = self.safe_string_lower(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') orderAction['builder'] = {'b': wallet, 'f': self.safe_integer(self.options, 'feeInt', 10)} signature = self.sign_l1_action(orderAction, nonce, vaultAddress) request: dict = { 'action': orderAction, 'nonce': nonce, 'signature': signature, # 'vaultAddress': vaultAddress, } if vaultAddress is not None: params = self.omit(params, 'vaultAddress') request['vaultAddress'] = vaultAddress return request async def cancel_order(self, id: str, symbol: Str = None, params={}): """ cancels an open order https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid :param str id: order id :param str symbol: unified symbol of the market the order was made in :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) :param str [params.vaultAddress]: the vault address for order :param str [params.subAccountAddress]: sub account user address :returns dict: An `order structure ` """ orders = await self.cancel_orders([id], symbol, params) return self.safe_dict(orders, 0) async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): """ cancel multiple orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid :param str[] ids: order ids :param str [symbol]: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param string|str[] [params.clientOrderId]: client order ids,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: an list of `order structures ` """ self.check_required_credentials() if symbol is None: raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') await self.load_markets() await self.initialize_client() request = self.cancel_orders_request(ids, symbol, params) response = await self.privatePostExchange(request) # # { # "status":"ok", # "response":{ # "type":"cancel", # "data":{ # "statuses":[ # "success" # ] # } # } # } # innerResponse = self.safe_dict(response, 'response') data = self.safe_dict(innerResponse, 'data') statuses = self.safe_list(data, 'statuses') orders = [] for i in range(0, len(statuses)): status = statuses[i] orders.append(self.safe_order({ 'info': status, 'status': status, })) return orders def cancel_orders_request(self, ids: List[str], symbol: Str = None, params={}) -> dict: """ build the request payload for cancelling multiple orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid :param str[] ids: order ids :param str symbol: unified market symbol :param dict [params]: :returns dict: the raw request object to be sent to the exchange """ market = self.market(symbol) clientOrderId = self.safe_value_2(params, 'clientOrderId', 'client_id') params = self.omit(params, ['clientOrderId', 'client_id']) nonce = self.milliseconds() request: dict = { 'nonce': nonce, # 'vaultAddress': vaultAddress, } cancelReq = [] cancelAction: dict = { 'type': '', 'cancels': [], } baseId = self.parse_to_numeric(market['baseId']) if clientOrderId is not None: if not isinstance(clientOrderId, list): clientOrderId = [clientOrderId] cancelAction['type'] = 'cancelByCloid' for i in range(0, len(clientOrderId)): cancelReq.append({ 'asset': baseId, 'cloid': clientOrderId[i], }) else: cancelAction['type'] = 'cancel' for i in range(0, len(ids)): cancelReq.append({ 'a': baseId, 'o': self.parse_to_numeric(ids[i]), }) cancelAction['cancels'] = cancelReq vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrders', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) request['action'] = cancelAction request['signature'] = signature if vaultAddress is not None: params = self.omit(params, 'vaultAddress') request['vaultAddress'] = vaultAddress return request async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): """ cancel multiple orders for multiple symbols https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: an list of `order structures ` """ self.check_required_credentials() await self.load_markets() await self.initialize_client() nonce = self.milliseconds() request: dict = { 'nonce': nonce, # 'vaultAddress': vaultAddress, } cancelReq = [] cancelAction: dict = { 'type': '', 'cancels': [], } cancelByCloid = False for i in range(0, len(orders)): order = orders[i] clientOrderId = self.safe_string(order, 'clientOrderId') if clientOrderId is not None: cancelByCloid = True id = self.safe_string(order, 'id') symbol = self.safe_string(order, 'symbol') if symbol is None: raise ArgumentsRequired(self.id + ' cancelOrdersForSymbols() requires a symbol argument in each order') if id is not None and cancelByCloid: raise BadRequest(self.id + ' cancelOrdersForSymbols() all orders must have either id or clientOrderId') assetKey = 'asset' if cancelByCloid else 'a' idKey = 'cloid' if cancelByCloid else 'o' market = self.market(symbol) cancelObj: dict = {} cancelObj[assetKey] = self.parse_to_numeric(market['baseId']) cancelObj[idKey] = clientOrderId if cancelByCloid else self.parse_to_numeric(id) cancelReq.append(cancelObj) cancelAction['type'] = 'cancelByCloid' if cancelByCloid else 'cancel' cancelAction['cancels'] = cancelReq vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrdersForSymbols', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) request['action'] = cancelAction request['signature'] = signature if vaultAddress is not None: params = self.omit(params, 'vaultAddress') request['vaultAddress'] = vaultAddress response = await self.privatePostExchange(request) # # { # "status":"ok", # "response":{ # "type":"cancel", # "data":{ # "statuses":[ # "success" # ] # } # } # } # return [self.safe_order({'info': response})] async def cancel_all_orders_after(self, timeout: Int, params={}): """ dead man's switch, cancel all orders after the given timeout :param number timeout: time in milliseconds, 0 represents cancel the timer :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: the api result """ self.check_required_credentials() await self.load_markets() await self.initialize_client() params = self.omit(params, ['clientOrderId', 'client_id']) nonce = self.milliseconds() request: dict = { 'nonce': nonce, # 'vaultAddress': vaultAddress, } cancelAction: dict = { 'type': 'scheduleCancel', 'time': nonce + timeout, } vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'cancelAllOrdersAfter', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) request['action'] = cancelAction request['signature'] = signature if vaultAddress is not None: params = self.omit(params, 'vaultAddress') request['vaultAddress'] = vaultAddress response = await self.privatePostExchange(request) # # { # "status":"err", # "response":"Cannot set scheduled cancel time until enough volume traded. Required: $1000000. Traded: $373.47205." # } # return response def edit_orders_request(self, orders, params={}): self.check_required_credentials() hasClientOrderId = False for i in range(0, len(orders)): rawOrder = orders[i] orderParams = self.safe_dict(rawOrder, 'params', {}) clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') if clientOrderId is not None: hasClientOrderId = True if hasClientOrderId: for i in range(0, len(orders)): rawOrder = orders[i] orderParams = self.safe_dict(rawOrder, 'params', {}) clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') if clientOrderId is None: raise ArgumentsRequired(self.id + ' editOrders() all orders must have clientOrderId if at least one has a clientOrderId') params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) modifies = [] for i in range(0, len(orders)): rawOrder = orders[i] id = self.safe_string(rawOrder, 'id') marketId = self.safe_string(rawOrder, 'symbol') market = self.market(marketId) symbol = market['symbol'] type = self.safe_string_upper(rawOrder, 'type') isMarket = (type == 'MARKET') side = self.safe_string_upper(rawOrder, 'side') isBuy = (side == 'BUY') amount = self.safe_string(rawOrder, 'amount') price = self.safe_string(rawOrder, 'price') orderParams = self.safe_dict(rawOrder, 'params', {}) defaultSlippage = self.safe_string(self.options, 'defaultSlippage') slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' postOnly = self.safe_bool(orderParams, 'postOnly', False) if postOnly: defaultTimeInForce = 'alo' timeInForce = self.safe_string_lower(orderParams, 'timeInForce', defaultTimeInForce) timeInForce = self.capitalize(timeInForce) clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') stopLossPrice = self.safe_string(orderParams, 'stopLossPrice', triggerPrice) takeProfitPrice = self.safe_string(orderParams, 'takeProfitPrice') isTrigger = (stopLossPrice or takeProfitPrice) reduceOnly = self.safe_bool(orderParams, 'reduceOnly', False) orderParams = self.omit(orderParams, ['slippage', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'clientOrderId', 'client_id', 'postOnly', 'reduceOnly']) px = self.number_to_string(price) if isMarket: px = Precise.string_mul(px, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(px, Precise.string_sub('1', slippage)) px = self.price_to_precision(symbol, px) else: px = self.price_to_precision(symbol, px) sz = self.amount_to_precision(symbol, amount) orderType: dict = {} if isTrigger: isTp = False if takeProfitPrice is not None: triggerPrice = self.price_to_precision(symbol, takeProfitPrice) isTp = True else: triggerPrice = self.price_to_precision(symbol, stopLossPrice) orderType['trigger'] = { 'isMarket': isMarket, 'triggerPx': triggerPrice, 'tpsl': 'tp' if (isTp) else 'sl', } else: orderType['limit'] = { 'tif': timeInForce, } if triggerPrice is None: triggerPrice = '0' orderReq: dict = { 'a': self.parse_to_int(market['baseId']), 'b': isBuy, 'p': px, 's': sz, 'r': reduceOnly, 't': orderType, # 'c': clientOrderId, } if clientOrderId is not None: orderReq['c'] = clientOrderId modifyReq: dict = { 'oid': self.parse_to_int(id), 'order': orderReq, } modifies.append(modifyReq) nonce = self.milliseconds() modifyAction: dict = { 'type': 'batchModify', 'modifies': modifies, } vaultAddress = None vaultAddress, params = self.handle_option_and_params(params, 'editOrder', 'vaultAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(modifyAction, nonce, vaultAddress) request: dict = { 'action': modifyAction, 'nonce': nonce, 'signature': signature, # 'vaultAddress': vaultAddress, } if vaultAddress is not None: request['vaultAddress'] = vaultAddress return request async def edit_order(self, id: str, symbol: str, type: str, side: str, amount: Num = None, price: Num = None, params={}): """ edit a trade order https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders :param str id: cancel order id :param str symbol: unified symbol of the market to create an order in :param str type: 'market' or 'limit' :param str side: 'buy' or 'sell' :param float amount: how much of currency you want to trade in units of base currency :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' :param bool [params.postOnly]: True or False whether the order is post-only :param bool [params.reduceOnly]: True or False whether the order is reduce-only :param float [params.triggerPrice]: The price at which a trigger order is triggered at :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) :param str [params.vaultAddress]: the vault address for order :param str [params.subAccountAddress]: sub account user address :returns dict: an `order structure ` """ await self.load_markets() if id is None: raise ArgumentsRequired(self.id + ' editOrder() requires an id argument') order, globalParams = self.parse_create_edit_order_args(id, symbol, type, side, amount, price, params) orders = await self.edit_orders([order], globalParams) return orders[0] async def edit_orders(self, orders: List[OrderRequest], params={}): """ edit a list of trade orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an `order structure ` """ await self.load_markets() await self.initialize_client() request = self.edit_orders_request(orders, params) response = await self.privatePostExchange(request) # # { # "status": "ok", # "response": { # "type": "order", # "data": { # "statuses": [ # { # "resting": { # "oid": 5063830287 # } # } # ] # } # } # } # when the order is filled immediately # { # "status":"ok", # "response":{ # "type":"order", # "data":{ # "statuses":[ # { # "filled":{ # "totalSz":"0.1", # "avgPx":"100.84", # "oid":6195281425 # } # } # ] # } # } # } # responseObject = self.safe_dict(response, 'response', {}) dataObject = self.safe_dict(responseObject, 'data', {}) statuses = self.safe_list(dataObject, 'statuses', []) return self.parse_orders(statuses) async def create_vault(self, name: str, description: str, initialUsd: int, params={}): """ creates a value :param str name: The name of the vault :param str description: The description of the vault :param number initialUsd: The initialUsd of the vault :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: the api result """ self.check_required_credentials() await self.load_markets() nonce = self.milliseconds() request: dict = { 'nonce': nonce, } usd = self.parse_to_int(Precise.string_mul(self.number_to_string(initialUsd), '1000000')) action: dict = { 'type': 'createVault', 'name': name, 'description': description, 'initialUsd': usd, 'nonce': nonce, } signature = self.sign_l1_action(action, nonce) request['action'] = action request['signature'] = signature response = await self.privatePostExchange(self.extend(request, params)) # # { # "status": "ok", # "response": { # "type": "createVault", # "data": "0x04fddcbc9ce80219301bd16f18491bedf2a8c2b8" # } # } # return response async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): """ fetches historical funding rate prices https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-historical-funding-rates :param str symbol: unified symbol of the market to fetch the funding rate history for :param int [since]: timestamp in ms of the earliest funding rate to fetch :param int [limit]: the maximum amount of `funding rate structures ` to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest funding rate :returns dict[]: a list of `funding rate structures ` """ await self.load_markets() if symbol is None: raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') market = self.market(symbol) request: dict = { 'type': 'fundingHistory', 'coin': market['baseName'], } if since is not None: request['startTime'] = since else: maxLimit = 500 if (limit is None) else limit request['startTime'] = self.milliseconds() - maxLimit * 60 * 60 * 1000 until = self.safe_integer(params, 'until') params = self.omit(params, 'until') if until is not None: request['endTime'] = until response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "coin": "ETH", # "fundingRate": "0.0000125", # "premium": "0.00057962", # "time": 1704290400031 # } # ] # result = [] for i in range(0, len(response)): entry = response[i] timestamp = self.safe_integer(entry, 'time') result.append({ 'info': entry, 'symbol': self.safe_symbol(None, market), 'fundingRate': self.safe_number(entry, 'fundingRate'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), }) sorted = self.sort_by(result, 'timestamp') return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all unfilled currently open orders https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-open-orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.method]: 'openOrders' or 'frontendOpenOrders' default is 'frontendOpenOrders' :param str [params.subAccountAddress]: sub account user address :returns Order[]: a list of `order structures ` """ userAddress = None userAddress, params = self.handle_public_address('fetchOpenOrders', params) method = None method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'frontendOpenOrders') await self.load_markets() market = self.safe_market(symbol) request: dict = { 'type': method, 'user': userAddress, } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "coin": "ETH", # "limitPx": "2000.0", # "oid": 3991946565, # "origSz": "0.1", # "side": "B", # "sz": "0.1", # "timestamp": 1704346468838 # } # ] # orderWithStatus = [] for i in range(0, len(response)): order = response[i] extendOrder = {} if self.safe_string(order, 'status') is None: extendOrder['ccxtStatus'] = 'open' orderWithStatus.append(self.extend(order, extendOrder)) return self.parse_orders(orderWithStatus, market, since, limit) async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all unfilled currently closed orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :returns Order[]: a list of `order structures ` """ await self.load_markets() orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders closedOrders = self.filter_by_array(orders, 'status', ['closed'], False) return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all canceled orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :returns Order[]: a list of `order structures ` """ await self.load_markets() orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders closedOrders = self.filter_by_array(orders, 'status', ['canceled'], False) return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all closed and canceled orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :returns Order[]: a list of `order structures ` """ await self.load_markets() orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders closedOrders = self.filter_by_array(orders, 'status', ['canceled', 'closed', 'rejected'], False) return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.subAccountAddress]: sub account user address :returns Order[]: a list of `order structures ` """ userAddress = None userAddress, params = self.handle_public_address('fetchOrders', params) await self.load_markets() market = self.safe_market(symbol) request: dict = { 'type': 'historicalOrders', 'user': userAddress, } response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "coin": "ETH", # "limitPx": "2000.0", # "oid": 3991946565, # "origSz": "0.1", # "side": "B", # "sz": "0.1", # "timestamp": 1704346468838 # } # ] # return self.parse_orders(response, market, since, limit) async def fetch_order(self, id: str, symbol: Str = None, params={}): """ fetches information on an order made by the user https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-order-status-by-oid-or-cloid :param str id: order id :param str symbol: unified symbol of the market the order was made in :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.subAccountAddress]: sub account user address :returns dict: An `order structure ` """ userAddress = None userAddress, params = self.handle_public_address('fetchOrder', params) await self.load_markets() market = self.safe_market(symbol) clientOrderId = self.safe_string(params, 'clientOrderId') request: dict = { 'type': 'orderStatus', # 'oid': id if isClientOrderId else self.parse_to_numeric(id), 'user': userAddress, } if clientOrderId is not None: params = self.omit(params, 'clientOrderId') request['oid'] = clientOrderId else: isClientOrderId = len(id) >= 34 request['oid'] = id if isClientOrderId else self.parse_to_numeric(id) response = await self.publicPostInfo(self.extend(request, params)) # # { # "order": { # "order": { # "children": [], # "cloid": null, # "coin": "ETH", # "isPositionTpsl": False, # "isTrigger": False, # "limitPx": "2000.0", # "oid": "3991946565", # "orderType": "Limit", # "origSz": "0.1", # "reduceOnly": False, # "side": "B", # "sz": "0.1", # "tif": "Gtc", # "timestamp": "1704346468838", # "triggerCondition": "N/A", # "triggerPx": "0.0" # }, # "status": "open", # "statusTimestamp": "1704346468838" # }, # "status": "order" # } # data = self.safe_dict(response, 'order') return self.parse_order(data, market) def parse_order(self, order: dict, market: Market = None) -> Order: # # createOrdersWs error # # {error: 'Insufficient margin to place order. asset=159'} # # fetchOpenOrders # # { # "coin": "ETH", # "limitPx": "2000.0", # "oid": 3991946565, # "origSz": "0.1", # "side": "B", # "sz": "0.1", # "timestamp": 1704346468838 # } # fetchClosedorders # { # "cloid": null, # "closedPnl": "0.0", # "coin": "SOL", # "crossed": True, # "dir": "Open Long", # "fee": "0.003879", # "hash": "0x4a2647998682b7f07bc5040ab531e1011400f9a51bfa0346a0b41ebe510e8875", # "liquidationMarkPx": null, # "oid": "6463280784", # "px": "110.83", # "side": "B", # "startPosition": "1.64", # "sz": "0.1", # "tid": "232174667018988", # "time": "1709142268394" # } # # fetchOrder # # { # "order": { # "children": [], # "cloid": null, # "coin": "ETH", # "isPositionTpsl": False, # "isTrigger": False, # "limitPx": "2000.0", # "oid": "3991946565", # "orderType": "Limit", # "origSz": "0.1", # "reduceOnly": False, # "side": "B", # "sz": "0.1", # "tif": "Gtc", # "timestamp": "1704346468838", # "triggerCondition": "N/A", # "triggerPx": "0.0" # }, # "status": "open", # "statusTimestamp": "1704346468838" # } # # createOrder # # { # "resting": { # "oid": 5063830287 # } # } # # { # "filled":{ # "totalSz":"0.1", # "avgPx":"100.84", # "oid":6195281425 # } # } # frontendOrder # { # "children": [], # "cloid": null, # "coin": "BLUR", # "isPositionTpsl": False, # "isTrigger": True, # "limitPx": "0.5", # "oid": 8670487141, # "orderType": "Stop Limit", # "origSz": "20.0", # "reduceOnly": False, # "side": "B", # "sz": "20.0", # "tif": null, # "timestamp": 1715523663687, # "triggerCondition": "Price above 0.6", # "triggerPx": "0.6" # } # error = self.safe_string(order, 'error') if error is not None: return self.safe_order({ 'info': order, 'status': 'rejected', }) entry = self.safe_dict_n(order, ['order', 'resting', 'filled']) if entry is None: entry = order coin = self.safe_string(entry, 'coin') marketId = None if coin is not None: marketId = self.coin_to_market_id(coin) if self.safe_string(entry, 'id') is None: market = self.safe_market(marketId, None) else: market = self.safe_market(marketId, market) symbol = market['symbol'] timestamp = self.safe_integer(entry, 'timestamp') status = self.safe_string_2(order, 'status', 'ccxtStatus') order = self.omit(order, ['ccxtStatus']) side = self.safe_string(entry, 'side') if side is not None: side = 'sell' if (side == 'A') else 'buy' totalAmount = self.safe_string_2(entry, 'origSz', 'totalSz') remaining = self.safe_string(entry, 'sz') tif = self.safe_string_upper(entry, 'tif') postOnly = None if tif is not None: postOnly = (tif == 'ALO') return self.safe_order({ 'info': order, 'id': self.safe_string(entry, 'oid'), 'clientOrderId': self.safe_string(entry, 'cloid'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastTradeTimestamp': None, 'lastUpdateTimestamp': self.safe_integer(order, 'statusTimestamp'), 'symbol': symbol, 'type': self.parse_order_type(self.safe_string_lower(entry, 'orderType')), 'timeInForce': tif, 'postOnly': postOnly, 'reduceOnly': self.safe_bool(entry, 'reduceOnly'), 'side': side, 'price': self.safe_string(entry, 'limitPx'), 'triggerPrice': self.safe_number(entry, 'triggerPx') if self.safe_bool(entry, 'isTrigger') else None, 'amount': totalAmount, 'cost': None, 'average': self.safe_string(entry, 'avgPx'), 'filled': Precise.string_sub(totalAmount, remaining), 'remaining': remaining, 'status': self.parse_order_status(status), 'fee': None, 'trades': None, }, market) def parse_order_status(self, status: Str): if status is None: return None statuses: dict = { 'triggered': 'open', 'filled': 'closed', 'open': 'open', 'canceled': 'canceled', 'rejected': 'rejected', 'marginCanceled': 'canceled', } if status.endswith('Rejected'): return 'rejected' if status.endswith('Canceled'): return 'canceled' return self.safe_string(statuses, status, status) def parse_order_type(self, status): statuses: dict = { 'stop limit': 'limit', 'stop market': 'market', } return self.safe_string(statuses, status, status) async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): """ fetch all trades made by the user https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch trades for :param int [limit]: the maximum number of trades structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest trade :param str [params.subAccountAddress]: sub account user address :returns Trade[]: a list of `trade structures ` """ userAddress = None userAddress, params = self.handle_public_address('fetchMyTrades', params) await self.load_markets() market = self.safe_market(symbol) request: dict = { 'user': userAddress, } if since is not None: request['type'] = 'userFillsByTime' request['startTime'] = since else: request['type'] = 'userFills' until = self.safe_integer(params, 'until') params = self.omit(params, 'until') if until is not None: request['endTime'] = until response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "closedPnl": "0.19343", # "coin": "ETH", # "crossed": True, # "dir": "Close Long", # "fee": "0.050062", # "feeToken": "USDC", # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", # "liquidationMarkPx": null, # "oid": 3929354691, # "px": "2381.1", # "side": "A", # "startPosition": "0.0841", # "sz": "0.0841", # "tid": 128423918764978, # "time": 1704262888911 # } # ] # return self.parse_trades(response, market, since, limit) def parse_trade(self, trade: dict, market: Market = None) -> Trade: # # { # "closedPnl": "0.19343", # "coin": "ETH", # "crossed": True, # "dir": "Close Long", # "fee": "0.050062", # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", # "liquidationMarkPx": null, # "oid": 3929354691, # "px": "2381.1", # "side": "A", # "startPosition": "0.0841", # "sz": "0.0841", # "tid": 128423918764978, # "time": 1704262888911 # } # timestamp = self.safe_integer(trade, 'time') price = self.safe_string(trade, 'px') amount = self.safe_string(trade, 'sz') coin = self.safe_string(trade, 'coin') marketId = self.coin_to_market_id(coin) market = self.safe_market(marketId, None) symbol = market['symbol'] id = self.safe_string(trade, 'tid') side = self.safe_string(trade, 'side') if side is not None: side = 'sell' if (side == 'A') else 'buy' fee = self.safe_string(trade, 'fee') takerOrMaker = None crossed = self.safe_bool(trade, 'crossed') if crossed is not None: takerOrMaker = 'taker' if crossed else 'maker' return self.safe_trade({ 'info': trade, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'id': id, 'order': self.safe_string(trade, 'oid'), 'type': None, 'side': side, 'takerOrMaker': takerOrMaker, 'price': price, 'amount': amount, 'cost': None, 'fee': { 'cost': fee, 'currency': self.safe_string(trade, 'feeToken'), 'rate': None, }, }, market) async def fetch_position(self, symbol: str, params={}): """ fetch data on an open position https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary :param str symbol: unified market symbol of the market the position is held in :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :returns dict: a `position structure ` """ positions = await self.fetch_positions([symbol], params) return self.safe_dict(positions, 0, {}) async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: """ fetch all open positions https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary :param str[] [symbols]: list of unified market symbols :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.subAccountAddress]: sub account user address :returns dict[]: a list of `position structure ` """ await self.load_markets() userAddress = None userAddress, params = self.handle_public_address('fetchPositions', params) symbols = self.market_symbols(symbols) request: dict = { 'type': 'clearinghouseState', 'user': userAddress, } response = await self.publicPostInfo(self.extend(request, params)) # # { # "assetPositions": [ # { # "position": { # "coin": "ETH", # "cumFunding": { # "allTime": "0.0", # "sinceChange": "0.0", # "sinceOpen": "0.0" # }, # "entryPx": "2213.9", # "leverage": { # "rawUsd": "-475.23904", # "type": "isolated", # "value": "20" # }, # "liquidationPx": "2125.00856238", # "marginUsed": "24.88097", # "maxLeverage": "50", # "positionValue": "500.12001", # "returnOnEquity": "0.0", # "szi": "0.2259", # "unrealizedPnl": "0.0" # }, # "type": "oneWay" # } # ], # "crossMaintenanceMarginUsed": "0.0", # "crossMarginSummary": { # "accountValue": "100.0", # "totalMarginUsed": "0.0", # "totalNtlPos": "0.0", # "totalRawUsd": "100.0" # }, # "marginSummary": { # "accountValue": "100.0", # "totalMarginUsed": "0.0", # "totalNtlPos": "0.0", # "totalRawUsd": "100.0" # }, # "time": "1704261007014", # "withdrawable": "100.0" # } # data = self.safe_list(response, 'assetPositions', []) result = [] for i in range(0, len(data)): result.append(self.parse_position(data[i], None)) return self.filter_by_array_positions(result, 'symbol', symbols, False) def parse_position(self, position: dict, market: Market = None): # # { # "position": { # "coin": "ETH", # "cumFunding": { # "allTime": "0.0", # "sinceChange": "0.0", # "sinceOpen": "0.0" # }, # "entryPx": "2213.9", # "leverage": { # "rawUsd": "-475.23904", # "type": "isolated", # "value": "20" # }, # "liquidationPx": "2125.00856238", # "marginUsed": "24.88097", # "maxLeverage": "50", # "positionValue": "500.12001", # "returnOnEquity": "0.0", # "szi": "0.2259", # "unrealizedPnl": "0.0" # }, # "type": "oneWay" # } # entry = self.safe_dict(position, 'position', {}) coin = self.safe_string(entry, 'coin') marketId = self.coin_to_market_id(coin) market = self.safe_market(marketId, None) symbol = market['symbol'] leverage = self.safe_dict(entry, 'leverage', {}) marginMode = self.safe_string(leverage, 'type') isIsolated = (marginMode == 'isolated') rawSize = self.safe_string(entry, 'szi') size = rawSize side = None if size is not None: side = 'long' if Precise.string_gt(rawSize, '0') else 'short' size = Precise.string_abs(size) rawUnrealizedPnl = self.safe_string(entry, 'unrealizedPnl') absRawUnrealizedPnl = Precise.string_abs(rawUnrealizedPnl) marginUsed = self.safe_string(entry, 'marginUsed') initialMargin = None if isIsolated: initialMargin = Precise.string_sub(marginUsed, rawUnrealizedPnl) else: initialMargin = marginUsed percentage = Precise.string_mul(Precise.string_div(absRawUnrealizedPnl, marginUsed), '100') return self.safe_position({ 'info': position, 'id': None, 'symbol': symbol, 'timestamp': None, 'datetime': None, 'isolated': isIsolated, 'hedged': None, 'side': side, 'contracts': self.parse_number(size), 'contractSize': None, 'entryPrice': self.safe_number(entry, 'entryPx'), 'markPrice': None, 'notional': self.safe_number(entry, 'positionValue'), 'leverage': self.safe_number(leverage, 'value'), 'collateral': self.parse_number(marginUsed), 'initialMargin': self.parse_number(initialMargin), 'maintenanceMargin': None, 'initialMarginPercentage': None, 'maintenanceMarginPercentage': None, 'unrealizedPnl': self.parse_number(rawUnrealizedPnl), 'liquidationPrice': self.safe_number(entry, 'liquidationPx'), 'marginMode': marginMode, 'percentage': self.parse_number(percentage), }) async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): """ set margin mode(symbol) :param str marginMode: margin mode must be either [isolated, cross] :param str symbol: unified market symbol of the market the position is held in, default is None :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.leverage]: the rate of leverage, is required if setting trade mode(symbol) :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: response from the exchange """ if symbol is None: raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') await self.load_markets() market = self.market(symbol) leverage = self.safe_integer(params, 'leverage') if leverage is None: raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') asset = self.parse_to_int(market['baseId']) isCross = (marginMode == 'cross') nonce = self.milliseconds() params = self.omit(params, ['leverage']) updateAction: dict = { 'type': 'updateLeverage', 'asset': asset, 'isCross': isCross, 'leverage': leverage, } vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'setMarginMode', 'vaultAddress', 'subAccountAddress') if vaultAddress is not None: if vaultAddress.startswith('0x'): vaultAddress = vaultAddress.replace('0x', '') signature = self.sign_l1_action(updateAction, nonce, vaultAddress) request: dict = { 'action': updateAction, 'nonce': nonce, 'signature': signature, # 'vaultAddress': vaultAddress, } if vaultAddress is not None: request['vaultAddress'] = vaultAddress response = await self.privatePostExchange(request) # # { # 'response': { # 'type': 'default' # }, # 'status': 'ok' # } # return response async def set_leverage(self, leverage: int, symbol: Str = None, params={}): """ set the level of leverage for a market :param float leverage: the rate of leverage :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.marginMode]: margin mode must be either [isolated, cross], default is cross :returns dict: response from the exchange """ if symbol is None: raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') await self.load_markets() market = self.market(symbol) marginMode = self.safe_string(params, 'marginMode', 'cross') isCross = (marginMode == 'cross') asset = self.parse_to_int(market['baseId']) nonce = self.milliseconds() params = self.omit(params, 'marginMode') updateAction: dict = { 'type': 'updateLeverage', 'asset': asset, 'isCross': isCross, 'leverage': leverage, } vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'setLeverage', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(updateAction, nonce, vaultAddress) request: dict = { 'action': updateAction, 'nonce': nonce, 'signature': signature, # 'vaultAddress': vaultAddress, } if vaultAddress is not None: params = self.omit(params, 'vaultAddress') request['vaultAddress'] = vaultAddress response = await self.privatePostExchange(request) # # { # 'response': { # 'type': 'default' # }, # 'status': 'ok' # } # return response async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: """ add margin https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin :param str symbol: unified market symbol :param float amount: amount of margin to add :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: a `margin structure ` """ return await self.modify_margin_helper(symbol, amount, 'add', params) async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: """ https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin remove margin from a position :param str symbol: unified market symbol :param float amount: the amount of margin to remove :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: the vault address :param str [params.subAccountAddress]: sub account user address :returns dict: a `margin structure ` """ return await self.modify_margin_helper(symbol, amount, 'reduce', params) async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: await self.load_markets() market = self.market(symbol) asset = self.parse_to_int(market['baseId']) sz = self.parse_to_int(Precise.string_mul(self.amount_to_precision(symbol, amount), '1000000')) if type == 'reduce': sz = -sz nonce = self.milliseconds() updateAction: dict = { 'type': 'updateIsolatedMargin', 'asset': asset, 'isBuy': True, 'ntli': sz, } vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'modifyMargin', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) signature = self.sign_l1_action(updateAction, nonce, vaultAddress) request: dict = { 'action': updateAction, 'nonce': nonce, 'signature': signature, # 'vaultAddress': vaultAddress, } if vaultAddress is not None: request['vaultAddress'] = vaultAddress response = await self.privatePostExchange(request) # # { # 'response': { # 'type': 'default' # }, # 'status': 'ok' # } # return self.extend(self.parse_margin_modification(response, market), { 'code': self.safe_string(response, 'status'), }) def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: # # { # 'type': 'default' # } # return { 'info': data, 'symbol': self.safe_symbol(None, market), 'type': None, 'marginMode': 'isolated', 'amount': None, 'total': None, 'code': self.safe_string(market, 'settle'), 'status': None, 'timestamp': None, 'datetime': None, } async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: """ transfer currency internally between wallets on the same account https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#l1-usdc-transfer :param str code: unified currency code :param float amount: amount to transfer :param str fromAccount: account to transfer from *spot, swap* :param str toAccount: account to transfer to *swap, spot or address* :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: the vault address for order :returns dict: a `transfer structure ` """ self.check_required_credentials() await self.load_markets() isSandboxMode = self.safe_bool(self.options, 'sandboxMode') nonce = self.milliseconds() if self.in_array(fromAccount, ['spot', 'swap', 'perp']): # handle swap <> spot account transfer if not self.in_array(toAccount, ['spot', 'swap', 'perp']): raise NotSupported(self.id + ' transfer() only support spot <> swap transfer') strAmount = self.number_to_string(amount) vaultAddress = self.safe_string_2(params, 'vaultAddress', 'subAccountAddress') if vaultAddress is not None: vaultAddress = self.format_vault_address(vaultAddress) strAmount = strAmount + ' subaccount:' + vaultAddress toPerp = (toAccount == 'perp') or (toAccount == 'swap') transferPayload: dict = { 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', 'amount': strAmount, 'toPerp': toPerp, 'nonce': nonce, } transferSig = self.build_usd_class_send_sig(transferPayload) transferRequest: dict = { 'action': { 'hyperliquidChain': transferPayload['hyperliquidChain'], 'signatureChainId': '0x66eee', 'type': 'usdClassTransfer', 'amount': strAmount, 'toPerp': toPerp, 'nonce': nonce, }, 'nonce': nonce, 'signature': transferSig, } transferResponse = await self.privatePostExchange(transferRequest) return transferResponse # transfer between main account and subaccount isDeposit = False subAccountAddress = None if fromAccount == 'main': subAccountAddress = toAccount isDeposit = True elif toAccount == 'main': subAccountAddress = fromAccount else: raise NotSupported(self.id + ' transfer() only support main <> subaccount transfer') self.check_address(subAccountAddress) if code is None or code.upper() == 'USDC': # Transfer USDC with subAccountTransfer usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000')) action = { 'type': 'subAccountTransfer', 'subAccountUser': subAccountAddress, 'isDeposit': isDeposit, 'usd': usd, } sig = self.sign_l1_action(action, nonce) request: dict = { 'action': action, 'nonce': nonce, 'signature': sig, } response = await self.privatePostExchange(request) # # {'response': {'type': 'default'}, 'status': 'ok'} # return self.parse_transfer(response) else: # Transfer non-USDC with subAccountSpotTransfer symbol = self.symbol(code) action = { 'type': 'subAccountSpotTransfer', 'subAccountUser': subAccountAddress, 'isDeposit': isDeposit, 'token': symbol, 'amount': self.number_to_string(amount), } sig = self.sign_l1_action(action, nonce) request: dict = { 'action': action, 'nonce': nonce, 'signature': sig, } response = await self.privatePostExchange(request) return self.parse_transfer(response) def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: # # {'response': {'type': 'default'}, 'status': 'ok'} # return { 'info': transfer, 'id': None, 'timestamp': None, 'datetime': None, 'currency': None, 'amount': None, 'fromAccount': None, 'toAccount': None, 'status': 'ok', } async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: """ make a withdrawal(only support USDC) https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#initiate-a-withdrawal-request https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#deposit-or-withdraw-from-a-vault :param str code: unified currency code :param float amount: the amount to withdraw :param str address: the address to withdraw to :param str tag: :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.vaultAddress]: vault address withdraw from :returns dict: a `transaction structure ` """ self.check_required_credentials() await self.load_markets() self.check_address(address) if code is not None: code = code.upper() if code != 'USDC': raise NotSupported(self.id + ' withdraw() only support USDC') vaultAddress = None vaultAddress, params = self.handle_option_and_params(params, 'withdraw', 'vaultAddress') vaultAddress = self.format_vault_address(vaultAddress) params = self.omit(params, 'vaultAddress') nonce = self.milliseconds() action: dict = {} sig = None if vaultAddress is not None: action = { 'type': 'vaultTransfer', 'vaultAddress': '0x' + vaultAddress, 'isDeposit': False, 'usd': amount, } sig = self.sign_l1_action(action, nonce) else: isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) payload: dict = { 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', 'destination': address, 'amount': str(amount), 'time': nonce, } sig = self.build_withdraw_sig(payload) action = { 'hyperliquidChain': payload['hyperliquidChain'], 'signatureChainId': '0x66eee', # check self out 'destination': address, 'amount': str(amount), 'time': nonce, 'type': 'withdraw3', } request: dict = { 'action': action, 'nonce': nonce, 'signature': sig, } response = await self.privatePostExchange(request) return self.parse_transaction(response) def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: # # {status: 'ok', response: {type: 'default'}} # # fetchDeposits / fetchWithdrawals # { # "time":1724762307531, # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", # "delta":{ # "type":"accountClassTransfer", # "usdc":"50.0", # "toPerp":false # } # } # timestamp = self.safe_integer(transaction, 'time') delta = self.safe_dict(transaction, 'delta', {}) fee = None feeCost = self.safe_integer(delta, 'fee') if feeCost is not None: fee = { 'currency': 'USDC', 'cost': feeCost, } internal = None type = self.safe_string(delta, 'type') if type is not None: internal = (type == 'internalTransfer') return { 'info': transaction, 'id': None, 'txid': self.safe_string(transaction, 'hash'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'network': None, 'address': None, 'addressTo': self.safe_string(delta, 'destination'), 'addressFrom': self.safe_string(delta, 'user'), 'tag': None, 'tagTo': None, 'tagFrom': None, 'type': None, 'amount': self.safe_number(delta, 'usdc'), 'currency': None, 'status': self.safe_string(transaction, 'status'), 'updated': None, 'comment': None, 'internal': internal, 'fee': fee, } async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: """ fetch the trading fees for a market :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.user]: user address, will default to self.walletAddress if not provided :param str [params.subAccountAddress]: sub account user address :returns dict: a `fee structure ` """ await self.load_markets() userAddress = None userAddress, params = self.handle_public_address('fetchTradingFee', params) market = self.market(symbol) request: dict = { 'type': 'userFees', 'user': userAddress, } response = await self.publicPostInfo(self.extend(request, params)) # # { # "dailyUserVlm": [ # { # "date": "2024-07-08", # "userCross": "0.0", # "userAdd": "0.0", # "exchange": "90597185.23639999" # } # ], # "feeSchedule": { # "cross": "0.00035", # "add": "0.0001", # "tiers": { # "vip": [ # { # "ntlCutoff": "5000000.0", # "cross": "0.0003", # "add": "0.00005" # } # ], # "mm": [ # { # "makerFractionCutoff": "0.005", # "add": "-0.00001" # } # ] # }, # "referralDiscount": "0.04" # }, # "userCrossRate": "0.00035", # "userAddRate": "0.0001", # "activeReferralDiscount": "0.0" # } # data: dict = { 'userCrossRate': self.safe_string(response, 'userCrossRate'), 'userAddRate': self.safe_string(response, 'userAddRate'), } return self.parse_trading_fee(data, market) def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: # # { # "dailyUserVlm": [ # { # "date": "2024-07-08", # "userCross": "0.0", # "userAdd": "0.0", # "exchange": "90597185.23639999" # } # ], # "feeSchedule": { # "cross": "0.00035", # "add": "0.0001", # "tiers": { # "vip": [ # { # "ntlCutoff": "5000000.0", # "cross": "0.0003", # "add": "0.00005" # } # ], # "mm": [ # { # "makerFractionCutoff": "0.005", # "add": "-0.00001" # } # ] # }, # "referralDiscount": "0.04" # }, # "userCrossRate": "0.00035", # "userAddRate": "0.0001", # "activeReferralDiscount": "0.0" # } # symbol = self.safe_symbol(None, market) return { 'info': fee, 'symbol': symbol, 'maker': self.safe_number(fee, 'userAddRate'), 'taker': self.safe_number(fee, 'userCrossRate'), 'percentage': None, 'tierBased': None, } async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: """ fetch the history of changes, actions done by the user or operations that altered the balance of the user :param str [code]: unified currency code :param int [since]: timestamp in ms of the earliest ledger entry :param int [limit]: max number of ledger entries to return :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest ledger entry :param str [params.subAccountAddress]: sub account user address :returns dict: a `ledger structure ` """ await self.load_markets() userAddress = None userAddress, params = self.handle_public_address('fetchLedger', params) request: dict = { 'type': 'userNonFundingLedgerUpdates', 'user': userAddress, } if since is not None: request['startTime'] = since until = self.safe_integer(params, 'until') if until is not None: request['endTime'] = until params = self.omit(params, ['until']) response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "time":1724762307531, # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", # "delta":{ # "type":"accountClassTransfer", # "usdc":"50.0", # "toPerp":false # } # } # ] # return self.parse_ledger(response, None, since, limit) def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: # # { # "time":1724762307531, # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", # "delta":{ # "type":"accountClassTransfer", # "usdc":"50.0", # "toPerp":false # } # } # timestamp = self.safe_integer(item, 'time') delta = self.safe_dict(item, 'delta', {}) fee = None feeCost = self.safe_integer(delta, 'fee') if feeCost is not None: fee = { 'currency': 'USDC', 'cost': feeCost, } type = self.safe_string(delta, 'type') amount = self.safe_string(delta, 'usdc') return self.safe_ledger_entry({ 'info': item, 'id': self.safe_string(item, 'hash'), 'direction': None, 'account': None, 'referenceAccount': self.safe_string(delta, 'user'), 'referenceId': self.safe_string(item, 'hash'), 'type': self.parse_ledger_entry_type(type), 'currency': None, 'amount': self.parse_number(amount), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'before': None, 'after': None, 'status': None, 'fee': fee, }, currency) def parse_ledger_entry_type(self, type): ledgerType: dict = { 'internalTransfer': 'transfer', 'accountClassTransfer': 'transfer', } return self.safe_string(ledgerType, type, type) async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): """ fetch all deposits made to an account :param str code: unified currency code :param int [since]: the earliest time in ms to fetch deposits for :param int [limit]: the maximum number of deposits structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: the latest time in ms to fetch withdrawals for :param str [params.subAccountAddress]: sub account user address :param str [params.vaultAddress]: vault address :returns dict[]: a list of `transaction structures ` """ await self.load_markets() userAddress = None userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) request: dict = { 'type': 'userNonFundingLedgerUpdates', 'user': userAddress, } if since is not None: request['startTime'] = since until = self.safe_integer(params, 'until') if until is not None: if since is None: raise ArgumentsRequired(self.id + ' fetchDeposits requires since while until is set') request['endTime'] = until params = self.omit(params, ['until']) response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "time":1724762307531, # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", # "delta":{ # "type":"accountClassTransfer", # "usdc":"50.0", # "toPerp":false # } # } # ] # records = self.extract_type_from_delta(response) vaultAddress = None vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') vaultAddress = self.format_vault_address(vaultAddress) deposits = [] if vaultAddress is not None: for i in range(0, len(records)): record = records[i] if record['type'] == 'vaultDeposit': delta = self.safe_dict(record, 'delta') if delta['vault'] == '0x' + vaultAddress: deposits.append(record) else: deposits = self.filter_by_array(records, 'type', ['deposit'], False) return self.parse_transactions(deposits, None, since, limit) async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: """ fetch all withdrawals made from an account :param str code: unified currency code :param int [since]: the earliest time in ms to fetch withdrawals for :param int [limit]: the maximum number of withdrawals structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: the latest time in ms to fetch withdrawals for :param str [params.subAccountAddress]: sub account user address :param str [params.vaultAddress]: vault address :returns dict[]: a list of `transaction structures ` """ await self.load_markets() userAddress = None userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) request: dict = { 'type': 'userNonFundingLedgerUpdates', 'user': userAddress, } if since is not None: request['startTime'] = since until = self.safe_integer(params, 'until') if until is not None: request['endTime'] = until params = self.omit(params, ['until']) response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "time":1724762307531, # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", # "delta":{ # "type":"accountClassTransfer", # "usdc":"50.0", # "toPerp":false # } # } # ] # records = self.extract_type_from_delta(response) vaultAddress = None vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') vaultAddress = self.format_vault_address(vaultAddress) withdrawals = [] if vaultAddress is not None: for i in range(0, len(records)): record = records[i] if record['type'] == 'vaultWithdraw': delta = self.safe_dict(record, 'delta') if delta['vault'] == '0x' + vaultAddress: withdrawals.append(record) else: withdrawals = self.filter_by_array(records, 'type', ['withdraw'], False) return self.parse_transactions(withdrawals, None, since, limit) async def fetch_open_interests(self, symbols: Strings = None, params={}): """ Retrieves the open interest for a list of symbols :param str[] [symbols]: Unified CCXT market symbol :param dict [params]: exchange specific parameters :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: """ await self.load_markets() symbols = self.market_symbols(symbols) swapMarkets = await self.fetch_swap_markets() return self.parse_open_interests(swapMarkets, symbols) async def fetch_open_interest(self, symbol: str, params={}): """ retrieves the open interest of a contract trading pair :param str symbol: unified CCXT market symbol :param dict [params]: exchange specific parameters :returns dict: an `open interest structure ` """ symbol = self.symbol(symbol) await self.load_markets() ois = await self.fetch_open_interests([symbol], params) return ois[symbol] def parse_open_interest(self, interest, market: Market = None): # # { # szDecimals: '2', # name: 'HYPE', # maxLeverage: '3', # funding: '0.00014735', # openInterest: '14677900.74', # prevDayPx: '26.145', # dayNtlVlm: '299643445.12560016', # premium: '0.00081613', # oraclePx: '27.569', # markPx: '27.63', # midPx: '27.599', # impactPxs: ['27.5915', '27.6319'], # dayBaseVlm: '10790652.83', # baseId: 159 # } # interest = self.safe_dict(interest, 'info', {}) coin = self.safe_string(interest, 'name') marketId = None if coin is not None: marketId = self.coin_to_market_id(coin) return self.safe_open_interest({ 'symbol': self.safe_symbol(marketId), 'openInterestAmount': self.safe_number(interest, 'openInterest'), 'openInterestValue': None, 'timestamp': None, 'datetime': None, 'info': interest, }, market) async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): """ fetch the history of funding payments paid and received on self account :param str [symbol]: unified market symbol :param int [since]: the earliest time in ms to fetch funding history for :param int [limit]: the maximum number of funding history structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.subAccountAddress]: sub account user address :returns dict: a `funding history structure ` """ await self.load_markets() market = None if symbol is not None: market = self.market(symbol) userAddress = None userAddress, params = self.handle_public_address('fetchFundingHistory', params) request: dict = { 'user': userAddress, 'type': 'userFunding', } if since is not None: request['startTime'] = since until = self.safe_integer(params, 'until') params = self.omit(params, 'until') if until is not None: request['endTime'] = until response = await self.publicPostInfo(self.extend(request, params)) # # [ # { # "time": 1734026400057, # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", # "delta": { # "type": "funding", # "coin": "SOL", # "usdc": "75.635093", # "szi": "-7375.9", # "fundingRate": "0.00004381", # "nSamples": null # } # } # ] # return self.parse_incomes(response, market, since, limit) def parse_income(self, income, market: Market = None): # # { # "time": 1734026400057, # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", # "delta": { # "type": "funding", # "coin": "SOL", # "usdc": "75.635093", # "szi": "-7375.9", # "fundingRate": "0.00004381", # "nSamples": null # } # } # id = self.safe_string(income, 'hash') timestamp = self.safe_integer(income, 'time') delta = self.safe_dict(income, 'delta') baseId = self.safe_string(delta, 'coin') marketSymbol = baseId + '/USDC:USDC' market = self.safe_market(marketSymbol) symbol = market['symbol'] amount = self.safe_string(delta, 'usdc') code = self.safe_currency_code('USDC') rate = self.safe_number(delta, 'fundingRate') return { 'info': income, 'symbol': symbol, 'code': code, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'id': id, 'amount': self.parse_number(amount), 'rate': rate, } async def reserve_request_weight(self, weight: Num, params={}) -> dict: """ Instead of trading to increase the address based rate limits, self action allows reserving additional actions for 0.0005 USDC per request. The cost is paid from the Perps balance. :param number weight: the weight to reserve, 1 weight = 1 action, 0.0005 USDC per action :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a response object """ nonce = self.milliseconds() request: dict = { 'nonce': nonce, } action: dict = { 'type': 'reserveRequestWeight', 'weight': weight, } signature = self.sign_l1_action(action, nonce) request['action'] = action request['signature'] = signature response = await self.privatePostExchange(self.extend(request, params)) return response def extract_type_from_delta(self, data=[]): records = [] for i in range(0, len(data)): record = data[i] record['type'] = record['delta']['type'] records.append(record) return records def format_vault_address(self, address: Str = None): if address is None: return None if address.startswith('0x'): return address.replace('0x', '') return address def handle_public_address(self, methodName: str, params: dict): userAux = None userAux, params = self.handle_option_and_params_2(params, methodName, 'user', 'subAccountAddress') user = userAux user, params = self.handle_option_and_params(params, methodName, 'address', userAux) if (user is not None) and (user != ''): return [user, params] if (self.walletAddress is not None) and (self.walletAddress != ''): return [self.walletAddress, params] raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a user parameter inside \'params\' or the wallet address set') def coin_to_market_id(self, coin: Str): if coin.find('/') > -1 or coin.find('@') > -1: return coin # spot return self.safe_currency_code(coin) + '/USDC:USDC' def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): if not response: return None # fallback to default error handler # {"status":"err","response":"User or API Wallet 0xb8a6f8b26223de27c31938d56e470a5b832703a5 does not exist."} # # { # status: 'ok', # response: {type: 'order', data: {statuses: [{error: 'Insufficient margin to place order. asset=4'}]}} # } # {"status":"ok","response":{"type":"order","data":{"statuses":[{"error":"Insufficient margin to place order. asset=84"}]}}} # # {"status":"unknownOid"} # status = self.safe_string(response, 'status', '') error = self.safe_string(response, 'error') message = None if status == 'err': message = self.safe_string(response, 'response') elif status == 'unknownOid': raise OrderNotFound(self.id + ' ' + body) # {"status":"unknownOid"} elif error is not None: message = error else: responsePayload = self.safe_dict(response, 'response', {}) data = self.safe_dict(responsePayload, 'data', {}) statuses = self.safe_list(data, 'statuses', []) for i in range(0, len(statuses)): message = self.safe_string(statuses[i], 'error') if message is not None: break feedback = self.id + ' ' + body nonEmptyMessage = ((message is not None) and (message != '')) if nonEmptyMessage: self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) if nonEmptyMessage: raise ExchangeError(feedback) # unknown message return None def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): url = self.implode_hostname(self.urls['api'][api]) + '/' + path if method == 'POST': headers = { 'Content-Type': 'application/json', } body = self.json(params) return {'url': url, 'method': method, 'body': body, 'headers': headers} def calculate_rate_limiter_cost(self, api, method, path, params, config={}): if ('byType' in config) and ('type' in params): type = params['type'] byType = config['byType'] if type in byType: return byType[type] return self.safe_value(config, 'cost', 1) def parse_create_edit_order_args(self, id: Str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): market = self.market(symbol) vaultAddress = None vaultAddress, params = self.handle_option_and_params_2(params, 'createOrder', 'vaultAddress', 'subAccountAddress') vaultAddress = self.format_vault_address(vaultAddress) symbol = market['symbol'] order = { 'symbol': symbol, 'type': type, 'side': side, 'amount': amount, 'price': price, 'params': params, } globalParams = {} if vaultAddress is not None: globalParams['vaultAddress'] = vaultAddress if id is not None: order['id'] = id return [order, globalParams]