Skip to content

Commit

Permalink
feat: add hyperliquid exchange support
Browse files Browse the repository at this point in the history
  • Loading branch information
thaaddeus committed Nov 22, 2024
1 parent b78c5de commit 3caae2e
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 7 deletions.
9 changes: 7 additions & 2 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export const EXCHANGES = [
'woo-x',
'blockchain-com',
'bitget',
'bitget-futures'
'bitget-futures',
'hyperliquid'
] as const

const BINANCE_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker', 'recentTrades', 'borrowInterest'] as const
Expand Down Expand Up @@ -478,6 +479,9 @@ const KUCOIN_FUTURES_CHANNELS = [
const BITGET_CHANNELS = ['trade', 'books1', 'books15']
const BITGET_FUTURES_CHANNELS = ['trade', 'books1', 'books15', 'ticker']
const COINBASE_INTERNATIONAL_CHANNELS = ['INSTRUMENTS', 'MATCH', 'FUNDING', 'RISK', 'LEVEL1', 'LEVEL2', 'CANDLES_ONE_MINUTE']

const HYPERLIQUID_CHANNELS = ['l2Book', 'trades', 'activeAssetCtx', 'activeSpotAssetCtx']

export const EXCHANGE_CHANNELS_INFO = {
bitmex: BITMEX_CHANNELS,
coinbase: COINBASE_CHANNELS,
Expand Down Expand Up @@ -536,5 +540,6 @@ export const EXCHANGE_CHANNELS_INFO = {
'okex-spreads': OKEX_SPREADS_CHANNELS,
'kucoin-futures': KUCOIN_FUTURES_CHANNELS,
bitget: BITGET_CHANNELS,
'bitget-futures': BITGET_FUTURES_CHANNELS
'bitget-futures': BITGET_FUTURES_CHANNELS,
hyperliquid: HYPERLIQUID_CHANNELS
}
171 changes: 171 additions & 0 deletions src/mappers/hyperliquid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { upperCaseSymbols } from '../handy'
import { BookChange, DerivativeTicker, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'

export class HyperliquidTradesMapper implements Mapper<'hyperliquid', Trade> {
private readonly _seenSymbols = new Set<string>()

canHandle(message: HyperliquidTradeMessage) {
return message.channel === 'trades'
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: 'trades',
symbols
}
]
}

*map(message: HyperliquidTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
for (const hyperliquidTrade of message.data) {
if (this._seenSymbols.has(hyperliquidTrade.coin) === false) {
this._seenSymbols.add(hyperliquidTrade.coin)
break
}
yield {
type: 'trade',
symbol: hyperliquidTrade.coin,
exchange: 'hyperliquid',
id: hyperliquidTrade.tid.toString(),
price: Number(hyperliquidTrade.px),
amount: Number(hyperliquidTrade.sz),
side: hyperliquidTrade.side === 'B' ? 'buy' : 'sell',
timestamp: new Date(hyperliquidTrade.time),
localTimestamp: localTimestamp
}
}
}
}

function mapHyperliquidLevel(level: HyperliquidWsLevel) {
return {
price: Number(level.px),
amount: Number(level.sz)
}
}
export class HyperliquidBookChangeMapper implements Mapper<'hyperliquid', BookChange> {
canHandle(message: HyperliquidWsBookMessage) {
return message.channel === 'l2Book'
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: 'l2Book',
symbols
}
]
}

*map(message: HyperliquidWsBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
yield {
type: 'book_change',
symbol: message.data.coin,
exchange: 'hyperliquid',
isSnapshot: true,
bids: (message.data.levels[0] ? message.data.levels[0] : []).map(mapHyperliquidLevel),
asks: (message.data.levels[1] ? message.data.levels[1] : []).map(mapHyperliquidLevel),
timestamp: new Date(message.data.time),
localTimestamp
}
}
}

export class HyperliquidDerivativeTickerMapper implements Mapper<'hyperliquid', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()

canHandle(message: HyperliquidContextMessage) {
return message.channel === 'activeAssetCtx'
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: 'activeAssetCtx',
symbols
}
]
}

*map(message: HyperliquidContextMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
const symbol = message.data.coin

const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'hyperliquid')

if (message.data.ctx.funding !== undefined) {
pendingTickerInfo.updateFundingRate(Number(message.data.ctx.funding))
}

if (message.data.ctx.markPx !== undefined) {
pendingTickerInfo.updateMarkPrice(Number(message.data.ctx.markPx))
}

if (message.data.ctx.openInterest !== undefined) {
pendingTickerInfo.updateOpenInterest(Number(message.data.ctx.openInterest))
}

if (message.data.ctx.oraclePx !== undefined) {
pendingTickerInfo.updateIndexPrice(Number(message.data.ctx.oraclePx))
}

if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}

type HyperliquidTradeMessage = {
channel: 'trades'
data: [
{
coin: string
side: string
px: string
sz: string
hash: string
time: number
tid: number // ID unique across all assets
}
]
}

type HyperliquidWsBookMessage = {
channel: 'l2Book'
data: {
coin: 'ATOM'
time: 1730160007687
levels: [HyperliquidWsLevel[], HyperliquidWsLevel[]]
}
}

type HyperliquidWsLevel = {
px: string // price
sz: string // size
n: number // number of orders
}

type HyperliquidContextMessage = {
channel: 'activeAssetCtx'
data: {
coin: 'RENDER'
ctx: {
funding: '0.0000125'
openInterest: '231067.2'
prevDayPx: '4.8744'
dayNtlVlm: '387891.57092'
premium: '0.0'
oraclePx: '4.9185'
markPx: '4.919'
midPx: '4.9183'
impactPxs: ['4.9176', '4.9191']
}
}
}
10 changes: 7 additions & 3 deletions src/mappers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
HuobiBookTickerMapper,
HuobiTradesMapper
} from './huobi'
import { HyperliquidBookChangeMapper, HyperliquidDerivativeTickerMapper, HyperliquidTradesMapper } from './hyperliquid'
import { krakenBookChangeMapper, krakenBookTickerMapper, krakenTradesMapper } from './kraken'
import { KucoinBookChangeMapper, KucoinBookTickerMapper, KucoinTradesMapper } from './kucoin'
import {
Expand Down Expand Up @@ -281,7 +282,8 @@ const tradesMappers = {
'okex-spreads': () => new OkexSpreadsTradesMapper(),
bitget: () => new BitgetTradesMapper('bitget'),
'bitget-futures': () => new BitgetTradesMapper('bitget-futures'),
'coinbase-international': () => coinbaseInternationalTradesMapper
'coinbase-international': () => coinbaseInternationalTradesMapper,
hyperliquid: () => new HyperliquidTradesMapper()
}

const bookChangeMappers = {
Expand Down Expand Up @@ -372,7 +374,8 @@ const bookChangeMappers = {
'okex-spreads': () => new OkexSpreadsBookChangeMapper(),
bitget: () => new BitgetBookChangeMapper('bitget'),
'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures'),
'coinbase-international': () => new CoinbaseInternationalBookChangMapper()
'coinbase-international': () => new CoinbaseInternationalBookChangMapper(),
hyperliquid: () => new HyperliquidBookChangeMapper()
}

const derivativeTickersMappers = {
Expand Down Expand Up @@ -408,7 +411,8 @@ const derivativeTickersMappers = {
'woo-x': () => new WooxDerivativeTickerMapper(),
'kucoin-futures': () => new KucoinFuturesDerivativeTickerMapper(),
'bitget-futures': () => new BitgetDerivativeTickerMapper(),
'coinbase-international': () => new CoinbaseInternationalDerivativeTickerMapper()
'coinbase-international': () => new CoinbaseInternationalDerivativeTickerMapper(),
hyperliquid: () => new HyperliquidDerivativeTickerMapper()
}

const optionsSummaryMappers = {
Expand Down
38 changes: 38 additions & 0 deletions src/realtimefeeds/hyperliquid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Filter } from '../types'
import { RealTimeFeedBase } from './realtimefeed'

export class HyperliquidRealTimeFeed extends RealTimeFeedBase {
protected wssURL = 'wss://api.hyperliquid.xyz/ws'

protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
return filters
.map((filter) => {
if (!filter.symbols || filter.symbols.length === 0) {
throw new Error('HyperliquidRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
}

return filter.symbols.map((symbol) => {
return {
method: 'subscribe',
subscription: {
coin: symbol,
type: filter.channel
}
}
})
})
.flatMap((f) => f)
}

protected messageIsError(message: any): boolean {
return message.channel === 'error'
}

protected messageIsHeartbeat(message: any): boolean {
return message.channel === 'pong'
}

protected sendCustomPing = () => {
this.send({ method: 'ping' })
}
}
4 changes: 3 additions & 1 deletion src/realtimefeeds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { KucoinFuturesRealTimeFeed } from './kucoinfutures'
import { DydxV4RealTimeFeed } from './dydx_v4'
import { BitgetFuturesRealTimeFeed, BitgetRealTimeFeed } from './bitget'
import { CoinbaseInternationalRealTimeFeed } from './coinbaseinternational'
import { HyperliquidRealTimeFeed } from './hyperliquid'

export * from './realtimefeed'

Expand Down Expand Up @@ -115,7 +116,8 @@ const realTimeFeedsMap: {
'dydx-v4': DydxV4RealTimeFeed,
bitget: BitgetRealTimeFeed,
'bitget-futures': BitgetFuturesRealTimeFeed,
'coinbase-international': CoinbaseInternationalRealTimeFeed
'coinbase-international': CoinbaseInternationalRealTimeFeed,
hyperliquid: HyperliquidRealTimeFeed
}

export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
Expand Down
Loading

0 comments on commit 3caae2e

Please sign in to comment.