Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"KRAKEN:ETH/USD",
"HUOBI:ethusdt",
"HUOBI:ETH-USD",
"BINANCE_FUTURES:ethusd_perp"
"BINANCE_FUTURES:ethusd_perp",
"HYPERLIQUID:BTC",
"HYPERLIQUID:@142"
],
"backupInterval": 5000,
"collect": true,
Expand Down
117 changes: 83 additions & 34 deletions src/exchanges/hyperliquid.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
const Exchange = require('../exchange')
const { readProducts, saveProducts } = require('../services/catalog')
const {
readProducts,
saveProducts,
setHyperliquidSpotPairLabels
} = require('../services/catalog')
const axios = require('axios')

function buildSpotPairLabels(spotMeta) {
const labels = {}

if (!spotMeta?.tokens?.length || !spotMeta?.universe?.length) {
return labels
}

for (const product of spotMeta.universe) {
if (product.isDelisted) {
continue
}

const base = spotMeta.tokens[product.tokens[0]]
const quote = spotMeta.tokens[product.tokens[1]]

if (base && quote) {
labels[product.name] = `${base.name}/${quote.name}`
}
}

return labels
}

class Hyperliquid extends Exchange {
constructor() {
super()
Expand All @@ -16,11 +43,9 @@ class Hyperliquid extends Exchange {
return 'wss://api.hyperliquid.xyz/ws'
}

// Custom getProducts implementation for POST API
async getProducts(forceRefreshProducts = false) {
let formatedProducts

// Load from cache if not forcing refresh
if (!forceRefreshProducts) {
try {
formatedProducts = await readProducts(this.id)
Expand All @@ -29,27 +54,39 @@ class Hyperliquid extends Exchange {
}
}

// Fetch new products if no cache available
if (!formatedProducts) {
if (formatedProducts) {
if (formatedProducts.spotPairLabels) {
setHyperliquidSpotPairLabels(formatedProducts.spotPairLabels)
}
} else {
try {
console.log(`[${this.id}] fetching products via POST API`)

const response = await axios.post(
this.endpoints.PRODUCTS,
{ type: 'meta' },
{
headers: { 'Content-Type': 'application/json' }
}
)

formatedProducts = this.formatProducts(response.data) || []
const [perpResponse, spotResponse] = await Promise.all([
axios.post(
this.endpoints.PRODUCTS,
{ type: 'meta' },
{ headers: { 'Content-Type': 'application/json' } }
),
axios.post(
this.endpoints.PRODUCTS,
{ type: 'spotMeta' },
{ headers: { 'Content-Type': 'application/json' } }
)
])

formatedProducts = this.formatProducts([
perpResponse.data,
spotResponse.data
])

if (formatedProducts?.spotPairLabels) {
setHyperliquidSpotPairLabels(formatedProducts.spotPairLabels)
}

// Save to cache
await saveProducts(this.id, formatedProducts)
console.log(
`[${this.id}] saved ${
formatedProducts.products?.length || 0
} products`
`[${this.id}] saved ${formatedProducts.products?.length || 0} products`
)
} catch (error) {
console.error(
Expand All @@ -60,32 +97,41 @@ class Hyperliquid extends Exchange {
}
}

// Set products to instance
if (formatedProducts && formatedProducts.products) {
this.products = formatedProducts.products
}

return this.products
}

formatProducts(response) {
formatProducts(responses) {
const products = []
const perpResponse = Array.isArray(responses) ? responses[0] : responses
const spotResponse = Array.isArray(responses) ? responses[1] : null
let spotPairLabels = {}

if (perpResponse?.universe?.length) {
for (const product of perpResponse.universe) {
if (!product.isDelisted) {
products.push(product.name)
}
}
}

if (response && response.universe && response.universe.length) {
for (const product of response.universe) {
products.push(product.name)
if (spotResponse?.universe?.length) {
spotPairLabels = buildSpotPairLabels(spotResponse)

for (const product of spotResponse.universe) {
if (!product.isDelisted) {
products.push(product.name)
}
}
}

console.log(`[${this.id}] formatted ${products.length} products`)
return { products }
return { products, spotPairLabels }
}

/**
* Sub
* @param {WebSocket} api
* @param {string} pair
*/
async subscribe(api, pair) {
if (!(await super.subscribe.apply(this, arguments))) {
return
Expand All @@ -104,11 +150,6 @@ class Hyperliquid extends Exchange {
return true
}

/**
* Sub
* @param {WebSocket} api
* @param {string} pair
*/
async unsubscribe(api, pair) {
if (!(await super.unsubscribe.apply(this, arguments))) {
return
Expand Down Expand Up @@ -148,6 +189,14 @@ class Hyperliquid extends Exchange {
side: trade.side === 'B' ? 'buy' : 'sell'
}
}

onApiCreated(api) {
this.startKeepAlive(api, { method: 'ping' }, 20000)
}

onApiRemoved(api) {
this.stopKeepAlive(api)
}
}

module.exports = Hyperliquid
55 changes: 53 additions & 2 deletions src/services/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ const standardCurrencyPairLookup = new RegExp(

require('../typedef')

const HYPERLIQUID_SPOT_REGEX = /^@|\//

let hyperliquidSpotPairLabels = {}

module.exports.setHyperliquidSpotPairLabels = function (labels) {
hyperliquidSpotPairLabels = labels || {}
}

module.exports.getHyperliquidSpotPairLabels = function () {
return hyperliquidSpotPairLabels
}

module.exports.saveProducts = async function (exchangeId, data) {
const path = 'products/' + exchangeId + '.json'
const storage = {
Expand Down Expand Up @@ -99,6 +111,10 @@ module.exports.readProducts = async function (exchangeId) {

console.debug(`[${exchangeId}] using stored products`)

if (data.spotPairLabels) {
module.exports.setHyperliquidSpotPairLabels(data.spotPairLabels)
}

resolve(data)
} catch (error) {
reject(error)
Expand Down Expand Up @@ -182,10 +198,11 @@ module.exports.parseMarket = function (exchangeId, symbol, noStable = true) {
type = 'future'
} else if (
exchangeId === 'BINANCE_FUTURES' ||
exchangeId === 'DYDX' ||
exchangeId === 'HYPERLIQUID'
exchangeId === 'DYDX'
) {
type = 'perp'
} else if (exchangeId === 'HYPERLIQUID') {
type = HYPERLIQUID_SPOT_REGEX.test(symbol) ? 'spot' : 'perp'
} else if (exchangeId === 'BITFINEX' && /F0$/.test(symbol)) {
type = 'perp'
} else if (exchangeId === 'HUOBI' && /_(CW|CQ|NW|NQ)$/.test(symbol)) {
Expand Down Expand Up @@ -238,6 +255,10 @@ module.exports.parseMarket = function (exchangeId, symbol, noStable = true) {
localSymbol = localSymbol.replace(/_(\w+)-PERPETUAL/i, '$1')
} else if (exchangeId === 'KUCOIN') {
localSymbol = localSymbol.replace(/M$/, '')
} else if (exchangeId === 'HYPERLIQUID' && type === 'spot') {
if (hyperliquidSpotPairLabels[symbol]) {
localSymbol = hyperliquidSpotPairLabels[symbol]
}
} else if (exchangeId === 'HYPERLIQUID') {
localSymbol = localSymbol.replace(/^k/, '') + 'USD'
} else if (exchangeId === 'PHEMEX') {
Expand Down Expand Up @@ -273,6 +294,36 @@ module.exports.parseMarket = function (exchangeId, symbol, noStable = true) {
}
}
}
if (!match && exchangeId === 'HYPERLIQUID' && type === 'spot') {
const label = hyperliquidSpotPairLabels[symbol]

if (label && label.includes('/')) {
const [base, quote] = label.split('/')

return {
id,
base,
quote,
pair: symbol,
local: noStable ? stripStablePair(base + quote) : base + quote,
exchange: exchangeId,
type
}
}

if (HYPERLIQUID_SPOT_REGEX.test(symbol)) {
return {
id,
base: symbol,
quote: 'USDC',
pair: symbol,
local: symbol,
exchange: exchangeId,
type
}
}
}

if (!match) {
return null
}
Expand Down