ccxt
Version:
1,114 lines (1,110 loc) • 102 kB
JavaScript
'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') {