3886 lines
164 KiB
Python
3886 lines
164 KiB
Python
# -*- 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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=funding-rate-history-structure>` 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 <https://docs.ccxt.com/#/?id=funding-rate-history-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=add-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 <https://docs.ccxt.com/#/?id=reduce-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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=ledger>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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]
|