UNPKG

ccxt

Version:

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

1,114 lines (1,110 loc) 102 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var sha3_js = require('@noble/hashes/sha3.js'); var secp256k1_js = require('@noble/curves/secp256k1.js'); var hyperliquid$1 = require('../abstract/prediction/hyperliquid.js'); var Precise = require('../base/Precise.js'); var crypto = require('../base/functions/crypto.js'); var errors = require('../base/errors.js'); // ---------------------------------------------------------------------------- // --------------------------------------------------------------------------- /** * @class hyperliquid * @augments Exchange */ class hyperliquid extends hyperliquid$1["default"] { describe() { return this.deepExtend(super.describe(), { 'id': 'hyperliquid', 'name': 'Hyperliquid', 'countries': [], 'rateLimit': 50, 'certified': false, 'pro': false, 'dex': true, 'has': { 'CORS': undefined, 'spot': false, 'margin': false, 'swap': false, 'future': false, 'option': false, 'cancelOrder': true, 'cancelOrders': true, 'createOrder': true, 'fetchBalance': true, 'fetchCurrencies': false, 'fetchEvents': true, 'fetchMarkets': true, 'fetchMyTrades': true, 'fetchOHLCV': true, 'fetchOpenOrders': true, 'fetchOrder': true, 'fetchOrderBook': true, 'fetchOrders': true, 'fetchPositions': true, 'fetchTicker': true, 'fetchTickers': true, 'fetchTrades': true, 'prediction': 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', }, 'urls': { 'logo': 'https://github.com/user-attachments/assets/550769b3-d270-461e-9e02-8e8b8c0210b8', 'api': { 'public': 'https://api.hyperliquid.xyz', 'private': 'https://api.hyperliquid.xyz', }, '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, 'spotClearinghouseState': 2, 'candleSnapshot': 4, 'orderStatus': 2, }, }, }, }, 'private': { 'post': { 'exchange': 1, }, }, }, 'requiredCredentials': { 'apiKey': false, 'secret': false, 'walletAddress': true, 'privateKey': true, }, 'fees': { 'trading': { 'tierBased': false, 'percentage': true, 'maker': 0.0002, 'taker': 0.0005, }, }, 'options': { 'defaultType': 'prediction', // the whole outcome universe is one cheap outcomeMeta request, so bulk-warming on // a cache miss stays the right trade-off here (the base default is false) 'loadAllOutcomes': true, 'sandboxMode': false, // outcome markets currently deployed on testnet 'outcomeQuoteCurrency': 'USDH', 'defaultSlippage': 0.05, 'zeroAddress': '0x0000000000000000000000000000000000000000', 'builderFee': true, 'builder': '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6', 'feeRate': '0%', // max builder fee rate to approve 'feeInt': 0, // builder fee attached per order, in tenths of a basis point }, 'exceptions': { 'exact': { 'Order was never placed, already canceled, or filled.': errors.OrderNotFound, 'Insufficient spot balance': errors.InsufficientFunds, 'Too many cumulative requests sent': errors.RateLimitExceeded, 'Order has zero size.': errors.InvalidOrder, 'Order has invalid size': errors.InvalidOrder, 'Order price cannot be more than 80% away from the reference price': errors.InvalidOrder, 'No liquidity available for market order.': errors.InvalidOrder, }, 'broad': { 'Insufficient': errors.InsufficientFunds, }, }, }); } setSandboxMode(enabled) { super.setSandboxMode(enabled); this.options['sandboxMode'] = enabled; } /** * @ignore * @method * @name hyperliquid#outcomeEncoding * @description computes the encoding for an outcome side: encoding = 10 * outcomeId + side (side 0 = YES, side 1 = NO) * @param {int} outcomeId integer outcome id * @param {int} side outcome side, 0 = YES, 1 = NO * @returns {int} the outcome side encoding */ outcomeEncoding(outcomeId, side) { return this.sum(10 * outcomeId, side); } /** * @ignore * @method * @name hyperliquid#outcomeAssetId * @description returns the asset id used for orders: 100_000_000 + encoding, e.g. 100000010 * @param {int} encoding outcome side encoding * @returns {int} the asset id */ outcomeAssetId(encoding) { return this.sum(100000000, encoding); } /** * @ignore * @method * @name hyperliquid#outcomeCoin * @description returns the coin name used in API calls: #<encoding>, e.g. #10 for outcome 1 side 0 * @param {int} encoding outcome side encoding * @returns {string} the coin name */ outcomeCoin(encoding) { return '#' + encoding.toString(); } /** * @ignore * @method * @name hyperliquid#outcomeToken * @description returns the token name: +<encoding>, e.g. +10 * @param {int} encoding outcome side encoding * @returns {string} the token name */ outcomeToken(encoding) { return '+' + encoding.toString(); } /** * @ignore * @method * @name hyperliquid#parseOutcomeDescription * @description parses a description string of the form class:priceBinary|underlying:BTC|expiry:20260503-0600|targetPrice:78213|period:1d into a dict * @param {string} description the raw outcome description string * @returns {object} a dict of the parsed key/value pairs */ parseOutcomeDescription(description) { if (!description) { return {}; } const parts = description.split('|'); const result = {}; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const colonIndex = part.indexOf(':'); if (colonIndex > -1) { const key = part.slice(0, colonIndex); const value = part.slice(colonIndex + 1); result[key] = value; } } return result; } /** * @ignore * @method * @name hyperliquid#buildOutcomeSymbol * @description builds a human-readable outcome from a parsed description and side, e.g. BTC_ABOVE_78213_20260503:YES for side 0 and BTC_ABOVE_78213_20260503:NO for side 1 * @param {object} desc parsed outcome description * @param {int} side outcome side, 0 = YES, 1 = NO * @param {int} outcomeId integer outcome id * @returns {string} the outcome */ buildOutcomeSymbol(desc, side, outcomeId) { const underlying = this.safeString(desc, 'underlying', 'OUTCOME' + outcomeId.toString()); const targetPrice = this.safeString(desc, 'targetPrice'); const expiry = this.safeString(desc, 'expiry', ''); // Parse expiry: "20260503-0600" → "20260503" const expiryDate = expiry ? expiry.split('-')[0] : ''; const label = (side === 0) ? 'YES' : 'NO'; let base = underlying.toUpperCase(); if (targetPrice) { base = base + '_ABOVE_' + targetPrice; } if (expiryDate) { base = base + '_' + expiryDate; } return base + ':' + label; } /** * @ignore * @method * @name hyperliquid#buildOutcomeParentSymbol * @description builds a market id (parent outcome without YES/NO) from a parsed description, e.g. BTC_ABOVE_78213_20260503 for priceBinary outcomes or OUTCOME_9345 for non-priceBinary outcomes using the name field * @param {object} desc parsed outcome description * @param {int} outcomeId integer outcome id * @param {string} [name] outcome name * @param {object} [question] linked question object from outcomeMeta * @returns {string} the parent market outcome */ buildOutcomeParentSymbol(desc, outcomeId, name = '', question = {}) { const underlying = this.safeString(desc, 'underlying'); if (underlying) { const targetPrice = this.safeString(desc, 'targetPrice'); const expiry = this.safeString(desc, 'expiry', ''); const expiryDate = expiry ? expiry.split('-')[0] : ''; let base = underlying.toUpperCase(); if (targetPrice) { base = base + '_ABOVE_' + targetPrice; } if (expiryDate) { base = base + '_' + expiryDate; } return base; } const questionDescription = this.safeString(question, 'description'); if (questionDescription) { const questionDesc = this.parseOutcomeDescription(questionDescription); const questionClass = this.safeStringLower(questionDesc, 'class'); if (questionClass === 'pricebucket') { const questionUnderlying = this.safeString(questionDesc, 'underlying'); const questionExpiry = this.safeString(questionDesc, 'expiry', ''); const expiryDate = questionExpiry ? questionExpiry.split('-')[0] : ''; const thresholdsRaw = this.safeString(questionDesc, 'priceThresholds', ''); const indexStr = this.safeString(desc, 'index'); const rawDescription = this.safeStringLower(desc, 'description', ''); const nameLower = name.toLowerCase(); if (questionUnderlying && thresholdsRaw && indexStr !== undefined) { const thresholdParts = thresholdsRaw.split(','); const thresholds = []; for (let i = 0; i < thresholdParts.length; i++) { const trimmed = thresholdParts[i].trim(); if (trimmed.length > 0) { thresholds.push(trimmed); } } const thresholdsLength = thresholds.length; const index = this.parseToInt(indexStr); if (thresholdsLength > 0 && index !== undefined) { let bucketLabel; if (index <= 0) { bucketLabel = 'BELOW_' + thresholds[0]; } else if (index >= thresholdsLength) { const lastIdx = thresholdsLength - 1; bucketLabel = 'ABOVE_' + thresholds[lastIdx]; } else { bucketLabel = 'BETWEEN_' + thresholds[index - 1] + '_' + thresholds[index]; } let base = questionUnderlying.toUpperCase() + '_' + bucketLabel; if (expiryDate) { base = base + '_' + expiryDate; } return base; } } const isFallbackLike = (rawDescription === 'other') || (nameLower.indexOf('fallback') >= 0) || (nameLower.indexOf('other') >= 0); if (questionUnderlying && isFallbackLike) { let base = questionUnderlying.toUpperCase() + '_OTHER'; if (expiryDate) { base = base + '_' + expiryDate; } return base; } } } const questionName = this.safeString(question, 'name'); if (questionName) { const questionSlug = this.shortenSlug(questionName); if (questionSlug) { let outcomeSlug = this.shortenSlug(name); const genericOutcomeNames = { 'RECURRING': true, 'RECURRING_FALLBACK': true, 'RECURRING_NAMED_OUTCOME': true, }; if (outcomeSlug in genericOutcomeNames) { if (outcomeSlug.indexOf('FALLBACK') >= 0) { outcomeSlug = 'OTHER'; } else { outcomeSlug = ''; } } if (outcomeSlug) { return questionSlug + '_' + outcomeSlug + '_' + outcomeId.toString(); } return questionSlug + '_' + outcomeId.toString(); } } // Fallback: use name slugified, or OUTCOME-<id> if (name) { return this.shortenSlug(name) + '_' + outcomeId.toString(); } return 'OUTCOME_' + outcomeId.toString(); } /** * @method * @name hyperliquid#fetchMarkets * @description Retrieves all Hyperliquid outcome markets from outcomeMeta. * Each binary outcome becomes one CCXT prediction market with two outcomes: YES and NO. * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids#outcomes * @param {object} [params] extra parameters * @returns {Market[]} array of market structures */ async fetchMarkets(params = {}) { // // outcomeMeta response: // // { // "outcomes": [ // { // "outcome": 9345, // "name": "Recurring", // "description": "class:priceBinary|underlying:BTC|expiry:20260513-0300|targetPrice:81023|period:1d", // "sideSpecs": [ // { "name": "Yes" }, // { "name": "No" } // ] // }, // ... // ], // "questions": [ // { // "question": 182, // "name": "What will Hypurr eat the most of in May 2026?", // "description": "...", // "fallbackOutcome": 7002, // "namedOutcomes": [7003, 7004, 7005], // "settledNamedOutcomes": [] // }, // ... // ] // } // const response = await this.publicPostInfo(this.extend({ 'type': 'outcomeMeta' }, params)); const outcomesList = this.safeList(response, 'outcomes', []); const questionsList = this.safeList(response, 'questions', []); const outcomesToQuestions = {}; for (let qi = 0; qi < questionsList.length; qi++) { const question = this.safeDict(questionsList, qi, {}); const fallbackOutcome = this.safeInteger(question, 'fallbackOutcome'); if (fallbackOutcome !== undefined) { const fallbackKey = fallbackOutcome.toString(); outcomesToQuestions[fallbackKey] = question; } const namedOutcomes = this.safeList(question, 'namedOutcomes', []); for (let ni = 0; ni < namedOutcomes.length; ni++) { const namedOutcomeId = this.safeInteger(namedOutcomes, ni); if (namedOutcomeId !== undefined) { const namedKey = namedOutcomeId.toString(); outcomesToQuestions[namedKey] = question; } } } const markets = []; if (this.outcomes === undefined) { this.outcomes = {}; } if (this.outcomes_by_id === undefined) { this.outcomes_by_id = {}; } for (let i = 0; i < outcomesList.length; i++) { const outcomeInfo = this.safeDict(outcomesList, i, {}); const outcomeId = this.safeInteger(outcomeInfo, 'outcome', i); const linkedQuestion = this.safeDict(outcomesToQuestions, outcomeId.toString(), {}); const market = this.parseOutcomeMarket(outcomeInfo, outcomeId, linkedQuestion); markets.push(market); // Build outcomes dictionary from market outcomes const marketOutcomes = this.safeList(market, 'outcomes', []); for (let oi = 0; oi < marketOutcomes.length; oi++) { const outcome = this.safeDict(marketOutcomes, oi, {}); const outcomeSymbol = this.safeString2(outcome, 'outcome', 'symbol'); const outcomeId_ = this.safeString2(outcome, 'outcomeId', 'id'); if (outcomeSymbol !== undefined) { this.outcomes[outcomeSymbol] = outcome; } if (outcomeId_ !== undefined) { this.outcomes_by_id[outcomeId_] = outcome; } } } return markets; } /** * @ignore * @method * @name hyperliquid#parseOutcomeMarket * @description parses a single binary outcome market into a CCXT market structure with outcomes[] * @param {object} outcomeInfo raw entry from outcomeMeta outcomes array * @param {int} outcomeId integer outcome id * @param {object} [question] linked question object from outcomeMeta questions array * @returns {object} a [market structure](https://docs.ccxt.com/#/?id=market-structure) */ parseOutcomeMarket(outcomeInfo, outcomeId, question = {}) { const description = this.safeString(outcomeInfo, 'description', ''); const name = this.safeString(outcomeInfo, 'name', ''); const sideSpecs = this.safeList(outcomeInfo, 'sideSpecs', []); const desc = this.parseOutcomeDescription(description); const parentSymbol = this.buildOutcomeParentSymbol(desc, outcomeId, name, question); const yesEncoding = this.outcomeEncoding(outcomeId, 0); const noEncoding = this.outcomeEncoding(outcomeId, 1); const yesOutcomeSymbol = parentSymbol + ':YES'; const noOutcomeSymbol = parentSymbol + ':NO'; // Parse expiry from description const expiry = this.safeString(desc, 'expiry'); let expiryMs = undefined; let expiryDatetime = undefined; if (expiry) { // e.g. "20260503-0600" → "2026-05-03T06:00:00Z" const expParts = expiry.split('-'); const expPartsLength = expParts.length; if (expPartsLength >= 1 && expParts[0].length === 8) { const ymd = expParts[0]; const hm = (expPartsLength >= 2) ? expParts[1] : '0000'; const isoStr = ymd.slice(0, 4) + '-' + ymd.slice(4, 6) + '-' + ymd.slice(6, 8) + 'T' + hm.slice(0, 2) + ':' + hm.slice(2, 4) + ':00Z'; expiryMs = this.parse8601(isoStr); expiryDatetime = isoStr; } } // Side labels from sideSpecs (e.g. "Yes"/"No", but use YES/NO normalised) const yesLabel = this.safeStringUpper(this.safeDict(sideSpecs, 0, {}), 'name', 'YES'); const noLabel = this.safeStringUpper(this.safeDict(sideSpecs, 1, {}), 'name', 'NO'); const quoteCurrency = this.safeString(this.options, 'outcomeQuoteCurrency', 'USDH'); const szDecimals = 4; // outcomes use 4 decimal places const active = true; const outcomePrecision = { 'amount': this.parseNumber(this.parsePrecision(szDecimals.toString())), 'price': 0.0001, }; const outcomes = [ { 'id': this.outcomeCoin(yesEncoding), 'outcomeId': this.outcomeCoin(yesEncoding), 'outcome': yesOutcomeSymbol, 'market': parentSymbol, 'label': yesLabel, 'active': active, 'precision': outcomePrecision, 'info': { 'encoding': yesEncoding, 'assetId': this.outcomeAssetId(yesEncoding), 'coinName': this.outcomeCoin(yesEncoding), 'tokenName': this.outcomeToken(yesEncoding), 'side': 0, 'outcomeId': outcomeId, 'name': name, 'description': description, 'parsedDescription': desc, }, }, { 'id': this.outcomeCoin(noEncoding), 'outcomeId': this.outcomeCoin(noEncoding), 'outcome': noOutcomeSymbol, 'market': parentSymbol, 'label': noLabel, 'active': active, 'precision': outcomePrecision, 'info': { 'encoding': noEncoding, 'assetId': this.outcomeAssetId(noEncoding), 'coinName': this.outcomeCoin(noEncoding), 'tokenName': this.outcomeToken(noEncoding), 'side': 1, 'outcomeId': outcomeId, 'name': name, 'description': description, 'parsedDescription': desc, }, }, ]; const marketRow = this.safeMarketStructure({ 'id': outcomeId.toString(), 'market': parentSymbol, 'base': parentSymbol.split('/')[0], 'quote': quoteCurrency, 'settle': undefined, 'baseId': outcomeId.toString(), 'quoteId': quoteCurrency, 'settleId': undefined, 'type': 'prediction', 'marketType': 'binary', 'executionModel': 'clob', 'spot': false, 'margin': undefined, 'swap': false, 'future': false, 'option': false, 'prediction': true, 'active': active, 'contract': false, 'linear': undefined, 'inverse': undefined, 'taker': 0.0005, 'maker': 0.0002, 'contractSize': undefined, 'expiry': expiryMs, 'expiryDatetime': expiryDatetime, 'strike': undefined, 'optionType': undefined, 'percentage': true, 'tierBased': false, 'feeSide': 'get', 'precision': outcomePrecision, 'limits': { 'leverage': { 'min': undefined, 'max': undefined }, 'amount': { 'min': undefined, 'max': undefined }, 'price': { 'min': 0.0001, 'max': 0.9999 }, 'cost': { 'min': this.parseNumber('1'), 'max': undefined }, }, 'outcomes': outcomes, 'info': this.extend(outcomeInfo, { 'outcomeId': outcomeId, 'parentSymbol': parentSymbol, 'description': description, 'parsedDescription': desc, }), 'created': undefined, }); // omit the deprecated 'symbol' key the safeMarketStructure template injects — // prediction market rows carry only the unified 'market' handle return this.omit(marketRow, 'symbol'); } /** * @ignore * @method * @name hyperliquid#calculatePricePrecision * @description calculates an appropriate price precision tick size given midPx and szDecimals * @param {float} midPx the mid price * @param {int} szDecimals the number of size decimals * @returns {float} the price tick size */ calculatePricePrecision(midPx, szDecimals) { if (midPx <= 0) { return 0.0001; } const midStr = this.numberToString(midPx); const parts = midStr.split('.'); const intPart = parts[0]; const significantDigits = Math.max(5, intPart.length); const maxDecimals = 8 - szDecimals; const pricePrecisionDecimals = Math.max(1, Math.min(maxDecimals, significantDigits - intPart.length)); let zeros = ''; const zeroCount = pricePrecisionDecimals - 1; for (let zi = 0; zi < zeroCount; zi++) { zeros = zeros + '0'; } return this.parseToNumeric('0.' + zeros + '1'); } /** * @method * @name hyperliquid#fetchTicker * @description fetches a ticker for a single outcome market using the L2 order book snapshot * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot * @param {string} outcome unified outcome (e.g. 'BTC_ABOVE_78213_20260503:YES') * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure) */ async fetchTicker(outcome, params = {}) { await this.loadOutcome(outcome); const outcomeObj = this.outcome(outcome); const info = this.safeDict(outcomeObj, 'info', {}); const coin = this.safeString(info, 'coinName'); const request = { 'type': 'l2Book', 'coin': coin, }; const response = await this.publicPostInfo(this.extend(request, params)); // // { // "coin": "#10", // "levels": [ // [ { "n": "2", "px": "0.44", "sz": "500" } ], // bids [0] // [ { "n": "2", "px": "0.46", "sz": "400" } ] // asks [1] // ], // "time": 1704290104840 // } // // l2Book returns null for coins without an order book; coerce to an empty dict const tickerData = this.safeDict({ 'book': response }, 'book', {}); return this.parsePredictionTicker(tickerData, outcomeObj); } /** * @method * @name hyperliquid#fetchTickers * @description fetches all outcome market tickers using allMids then optionally enriches with l2Book * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-all-mids-for-all-actively-traded-coins * @param {string[]} [outcomes] filter by outcome ids or outcomes * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a dictionary of [prediction ticker structures](https://docs.ccxt.com/#/?id=prediction-ticker-structure) */ async fetchTickers(outcomes = undefined, params = {}) { const requestedOutcomeSymbols = {}; if (outcomes !== undefined) { // one warm-up for the whole list (a cold cache bulk-loads once via loadAllOutcomes), // then identities resolve synchronously await this.loadOutcomes(outcomes); for (let i = 0; i < outcomes.length; i++) { const requested = outcomes[i]; const requestedOutcomeObj = this.safeOutcome(requested); const requestedOutcome = this.safeString(requestedOutcomeObj, 'outcome', requested); requestedOutcomeSymbols[requestedOutcome] = true; } } else { // no filter — warm the whole outcome set so identities resolve from the cache await this.loadOutcomes(); } const response = await this.publicPostInfo(this.extend({ 'type': 'allMids' }, params)); // // { "mids": { "#10": "0.45", "#11": "0.55", ... } } // const mids = this.safeDict(response, 'mids', response); const tickers = {}; const outcomesMap = (this.outcomes !== undefined) ? this.outcomes : {}; const outcomeHandles = Object.keys(outcomesMap); for (let i = 0; i < outcomeHandles.length; i++) { const outcomeHandle = outcomeHandles[i]; if (outcomes !== undefined && !(outcomeHandle in requestedOutcomeSymbols)) { continue; } const outcomeObj = this.safeDict(outcomesMap, outcomeHandle, {}); const info = this.safeDict(outcomeObj, 'info', {}); const coin = this.safeString(info, 'coinName'); const mid = this.safeNumber(mids, coin); if (mid === undefined) { continue; } // Build minimal ticker from mid price const ticker = this.parsePredictionTicker({ 'levels': [[], []], 'mid': mid, 'time': this.milliseconds() }, outcomeObj); tickers[outcomeHandle] = ticker; } return tickers; } /** * @ignore * @method * @name hyperliquid#parsePredictionTicker * @description parses a raw l2Book response (or a synthetic mid dict) into a unified ticker object * @param {object} raw l2Book response or { mid, time } object * @param {object} [market] the market the ticker belongs to * @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure) */ parsePredictionTicker(raw, market = undefined) { // // { // "coin": "#10", // "levels": [ // [ { "n": "2", "px": "0.44", "sz": "500" } ], // bids [0] // [ { "n": "2", "px": "0.46", "sz": "400" } ] // asks [1] // ], // "time": 1704290104840 // } // const now = this.milliseconds(); const timestamp = this.safeInteger(raw, 'time', now); // the 2nd arg carries the outcome object (callers pass the resolved outcome) const mkt = this.safeOutcome(undefined, market); const outcome = this.safeString(mkt, 'outcome'); const levels = this.safeList(raw, 'levels', []); const rawBids = this.safeList(levels, 0, []); const rawAsks = this.safeList(levels, 1, []); const topBid = this.safeDict(rawBids, 0); const topAsk = this.safeDict(rawAsks, 0); const bid = (topBid !== undefined) ? this.safeNumber(topBid, 'px') : undefined; const ask = (topAsk !== undefined) ? this.safeNumber(topAsk, 'px') : undefined; const bidVolume = (topBid !== undefined) ? this.safeNumber(topBid, 'sz') : undefined; const askVolume = (topAsk !== undefined) ? this.safeNumber(topAsk, 'sz') : undefined; // Use synthetic mid if no l2Book let mid = this.safeNumber(raw, 'mid'); if (mid === undefined && bid !== undefined && ask !== undefined) { mid = this.sum(bid, ask) / 2; } // day volume lives on the parent market's ctx; resolve it from the outcome's parent market const parentSymbol = this.safeString(mkt, 'market'); const parentMarket = (parentSymbol !== undefined) ? this.safeMarket(parentSymbol) : undefined; const ctx = (parentMarket !== undefined) ? this.safeDict(this.safeDict(parentMarket, 'info', {}), 'ctx', {}) : {}; const dayVolume = this.safeNumber(ctx, 'dayNtlVlm'); return this.safePredictionTicker({ 'outcome': outcome, 'outcomeId': this.safeString2(mkt, 'outcomeId', 'id'), 'label': this.safeString(mkt, 'label'), 'market': this.safeString(mkt, 'market'), 'timestamp': timestamp, 'datetime': this.iso8601(timestamp), 'high': undefined, 'low': undefined, 'bid': bid, 'bidVolume': bidVolume, 'ask': ask, 'askVolume': askVolume, 'vwap': undefined, 'open': undefined, 'close': mid, 'last': mid, 'previousClose': undefined, 'change': undefined, 'percentage': undefined, 'average': mid, 'baseVolume': undefined, 'quoteVolume': dayVolume, 'info': raw, }, market); } /** * @method * @name hyperliquid#fetchOrderBook * @description fetches the L2 order book for an outcome market * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot * @param {string} outcome unified outcome * @param {int} [limit] max depth levels (not used by hyperliquid but accepted) * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a [prediction order book structure](https://docs.ccxt.com/#/?id=prediction-order-book-structure) */ async fetchOrderBook(outcome, limit = undefined, params = {}) { await this.loadOutcome(outcome); const outcomeObj = this.outcome(outcome); const info = this.safeDict(outcomeObj, 'info', {}); const request = { 'type': 'l2Book', 'coin': this.safeString(info, 'coinName'), }; const response = await this.publicPostInfo(this.extend(request, params)); // // { // "coin": "#10", // "levels": [ // [ { "n": "5", "px": "0.44", "sz": "500" }, ... ], // bids [0] // [ { "n": "5", "px": "0.46", "sz": "400" }, ... ] // asks [1] // ], // "time": 1704290104840 // } // const timestamp = this.safeInteger(response, 'time'); const levels = this.safeList(response, 'levels', []); const rawBids = this.safeList(levels, 0, []); const rawAsks = this.safeList(levels, 1, []); const bids = []; const asks = []; for (let i = 0; i < rawBids.length; i++) { const entry = rawBids[i]; bids.push([this.safeNumber(entry, 'px'), this.safeNumber(entry, 'sz')]); } for (let i = 0; i < rawAsks.length; i++) { const entry = rawAsks[i]; asks.push([this.safeNumber(entry, 'px'), this.safeNumber(entry, 'sz')]); } const orderbook = this.parseOrderBook({ 'bids': bids, 'asks': asks }, this.safeString(outcomeObj, 'outcome', outcome), timestamp); return this.safePredictionOrderBook(orderbook, outcomeObj); } /** * @method * @name hyperliquid#fetchOHLCV * @description fetches candlestick OHLCV data for an outcome market * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot * @param {string} outcome unified outcome * @param {string} timeframe '1m', '5m', '15m', '1h', '4h', '1d', etc. * @param {int} [since] timestamp in ms of earliest candle * @param {int} [limit] max number of candles * @param {object} [params] extra parameters specific to the exchange API endpoint * @param {int} [params.until] end timestamp in ms * @returns {int[][]} a list of candles ordered as timestamp, open, high, low, close, volume */ async fetchOHLCV(outcome, timeframe = '1m', since = undefined, limit = undefined, params = {}) { await this.loadOutcome(outcome); const outcomeObj = this.outcome(outcome); // markets are keyed by the parent market outcome, not the outcome handle ("MARKET:LABEL") const market = this.market(this.safeString(outcomeObj, 'market')); const info = this.safeDict(outcomeObj, 'info', {}); const until = this.safeInteger(params, 'until', this.milliseconds()); let startTime = since; if (since === undefined) { const tf = this.parseTimeframe(timeframe); const candleCount = (limit !== undefined) ? limit : 100; const startOffset = tf * candleCount * -1000; startTime = this.sum(until, startOffset); if (startTime === undefined) { throw new errors.ExchangeError(this.id + ' fetchOHLCV() missing startTime'); } if (startTime < 0) { startTime = 0; } } const request = { 'type': 'candleSnapshot', 'req': { 'coin': this.safeString(info, 'coinName'), 'interval': this.safeString(this.timeframes, timeframe, timeframe), 'startTime': startTime, 'endTime': until, }, }; params = this.omit(params, 'until'); const response = await this.publicPostInfo(this.extend(request, params)); // // [ // { // "T": 1704287699999, // close time // "c": "0.45", // "h": "0.47", // "i": "1m", // "l": "0.43", // "n": 46, // number of trades // "o": "0.44", // "s": "#10", // "t": 1704286800000, // open time // "v": "1234.5" // } // ] // return this.parseOHLCVs(response, market, timeframe, since, limit); } /** * @ignore * @method * @name hyperliquid#parseOHLCV * @description parses a single hyperliquid candle object into a CCXT OHLCV tuple * @param {object} ohlcv the raw candle object * @param {object} [market] the market the candle belongs to * @returns {int[]} a candle ordered as timestamp, open, high, low, close, volume */ parseOHLCV(ohlcv, market = undefined) { // // { // "T": 1704287699999, // close time // "c": "0.45", // "h": "0.47", // "i": "1m", // "l": "0.43", // "n": 46, // number of trades // "o": "0.44", // "s": "#10", // "t": 1704286800000, // open time // "v": "1234.5" // } // return [ this.safeInteger(ohlcv, 't'), this.safeNumber(ohlcv, 'o'), this.safeNumber(ohlcv, 'h'), this.safeNumber(ohlcv, 'l'), this.safeNumber(ohlcv, 'c'), this.safeNumber(ohlcv, 'v'), ]; } /** * @method * @name hyperliquid#fetchBalance * @description Fetches spot balance (outcomes use spot-like balance). * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances * @param {object} [params] extra parameters * @param {string} [params.user] wallet address (defaults to this.walletAddress) * @returns {Balances} balance structure */ async fetchBalance(params = {}) { let userAddress; [userAddress, params] = this.handlePublicAddress('fetchBalance', params); const request = { 'type': 'spotClearinghouseState', 'user': userAddress, }; const response = await this.publicPostInfo(this.extend(request, params)); // // { // "balances": [ // { "coin": "USDC", "hold": "0.0", "total": "100.0" }, // { "coin": "+10", "hold": "0.0", "total": "50.0" }, // outcome token // { "coin": "+11", "hold": "0.0", "total": "25.0" } // ] // } // const result = { 'info': response, }; const balances = this.safeList(response, 'balances', []); for (let i = 0; i < balances.length; i++) { const balance = balances[i]; const coin = this.safeString(balance, 'coin'); const total = this.safeString(balance, 'total'); const used = this.safeString(balance, 'hold'); const account = this.account(); account['total'] = total; account['used'] = used; if (coin !== undefined) { result[coin] = account; } } return this.safeBalance(result); } /** * @method * @name hyperliquid#fetchPositions * @description fetches the user's outcome positions; outcome positions are spot token balances under the "+<encoding>" coin form (size and entry notional), the value/entry/mark price/pnl are computed from the current mid prices * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances * @param {string[]} [outcomes] filter by outcome ids or outcomes * @param {object} [params] extra parameters specific to the exchange API endpoint * @param {string} [params.user] wallet address * @returns {object[]} a list of [prediction position structures](https://docs.ccxt.com/#/?id=prediction-position-structure) */ async fetchPositions(outcomes = undefined, params = {}) { const requestedOutcomeSymbols = {}; if (outcomes !== undefined) { // one warm-up for the whole list (a cold cache bulk-loads once via loadAllOutcomes), // then identities resolve synchronously await this.loadOutcomes(outcomes); for (let i = 0; i < outcomes.length; i++) { const requested = outcomes[i]; const requestedOutcomeObj = this.safeOutcome(requested); const requestedOutcome = this.safeString(requestedOutcomeObj, 'outcome', requested); requestedOutcomeSymbols[requestedOutcome] = true; } } else { // no filter — warm the whole outcome set so identities resolve from the cache await this.loadOutcomes(); } let userAddress; [userAddress, params] = this.handlePublicAddress('fetchPositions', params); const request = { 'type': 'spotClearinghouseState', 'user': userAddress, }; // outcome positions are spot token balances under the "+<encoding>" coin form; they carry // the size (total) and entry notional (entryNtl). hyperliquid does not return the position // value / entry price / pnl, so they are computed from the current mid prices const promises = [ this.publicPostInfo(this.extend(request, params)), this.publicPostInfo({ 'type': 'allMids' }), ]; const results = await Promise.all(promises); const response = results[0]; const midsResponse = results[1]; const balances = this.safeList(response, 'balances', []); const mids = this.safeDict(midsResponse, 'mids', midsResponse); const positions = []; for (let i = 0; i < balances.length; i++) { const balance = this.safeDict(balances, i, {}); const coin = this.safeString(balance, 'coin', ''); // outcome tokens use the "+<encoding>" balance form; skip regular spot tokens (USDC, ...) if (coin.indexOf('+') !== 0) { continue; } const totalStr = this.safeString(balance, 'total'); if ((totalStr === undefined) || Precise["default"].stringEq(totalStr, '0')) { continue; } // the trade/orderbook form ("#<encoding>") resolves the outcome and the mid price const tradeCoin = '#' + coin.slice(1); const outcomeObj = this.safeOutcome(tradeCoin); if (outcomes !== undefined) { const outcomeHandle = this.safeString(outcomeObj, 'outcome'); if (outcomeHandle === undefined || !(outcomeHandle in requestedOutcomeSymbols)) { continue; } } const enriched = this.extend(balance, { 'markPx': this.safeString(mids, tradeCoin) }); positions.push(this.parsePredictionPosition(enriched, outcomeObj)); } return positions; } /** * @ignore * @method * @name hyperliquid#parsePredictionPosition * @description parses a spot balance entry for an outcome token into a unified position object * @param {object} position the raw balance entry * @param {object} [market] the outcome object the position belongs to * @returns {object} a [prediction position structure](https://docs.ccxt.com/#/?id=prediction-position-structure) */ parsePredictionPosition(position, market = undefined) { // `position` is a spotClearinghouseState balance entry ({ coin, total, hold, entryNtl }) // enriched with the current mid price (markPx); hyperliquid does not return the position // value / entry price / pnl for outcome tokens, so they are computed here const outcomeObj = this.safeOutcome(undefined, market); const totalStr = this.safeString(position, 'total'); const total = this.parseNumber(totalStr); const entryNtlStr = this.safeString(position, 'entryNtl'); let entryPrice = undefined; if ((entryNtlStr !== undefined) && (totalStr !== undefined) && !Precise["default"].stringEq(totalStr, '0')) { entryPrice = this.parseNumber(Precise["default"].stringDiv(entryNtlStr, totalStr)); } const markPxStr = this.safeString(position, 'markPx'); let notional = undefined; // current position value = size * mark price let unrealizedPnl = undefined; // value - entry notional if ((markPxStr !== undefined) && (totalStr !== undefined)) { const notionalStr = Precise["default"].stringMul(totalStr, markPxStr); notional = this.parseNumber(notionalStr); if (entryNtlStr !== undefined) { unrealizedPnl = this.parseNumber(Precise["default"].stringSub(notionalStr, entryNtlStr)); } } return this.safePredictionPosition({ 'id': undefined, 'outcome': this.safeString(outcomeObj, 'outcome'), 'outcomeId': this.safeString2(outcomeObj, 'outcomeId', 'id'), 'market': this.safeString(outcomeObj, 'market'), 'timestamp': undefined, 'datetime': undefined, 'isolated': false, 'hedged': undefined, 'side': 'long', 'contracts': total, 'contractSize': 1, 'entryPrice': entryPrice, 'markPrice': this.parseNumber(markPxStr), 'notional': notional, 'leverage': undefined, 'collateral': this.safeNumber(position, 'hold'), 'initialMargin': undefined, 'maintenanceMargin': undefined, 'initialMarginPercentage': undefined, 'maintenanceMarginPercentage': undefined, 'unrealizedPnl': unrealizedPnl, 'realizedPnl': undefined, 'liquidationPrice': undefined, 'marginRatio': undefined, 'marginMode': 'cross', 'percentage': undefined, 'info': position, }); } findOutcomeInMarket(market, sideHint = undefined) { const outcomesList = this.safeList(market, 'outcomes', []); const normalizedHint = sideHint ? sideHint.toUpperCase() : undefined; if (normalizedHint !== undefined) { for (let i = 0; i < outcomesList.length; i++) { const oc = this.safeDict(outcomesList, i, {}); const ocSymbol = this.safeString2(oc, 'outcome', 'symbol', ''); const ocLabel = this.safeStringUpper(oc, 'label'); if (ocLabel === normalizedHint || ocSymbol.endsWith(':' + normalizedHint)) { return oc; } } } for (let i = 0; i < outcomesList.length; i++) { const oc = this.safeDict(outcomesList, i, {}); const info = this.safeDict(oc, 'info', {}); if (this.safeInteger(info, 'side') === 0) { return oc; } } return this.safeDict(outcomesList, 0, {}); } parseOutcomeInputSideHint(outcomeInput) { if (!outcomeInput) { return undefined; } const colonIndex = outcomeInput.indexOf(':'); if (colonIndex > -1 && colonIndex < outcomeInput.length - 1) { const side = outcomeInput.slice(colonIndex + 1).toUpperCase(); if (side === 'YES' || side === 'NO') {