/** * Public Hyperliquid market data — the price source for DXtrade firms whose * API user isn't allowed DXtrade's own /marketdata (Tradeify247 answers * "No proper permission is configured", code 34). Tradeify247's instruments * track Hyperliquid's: crypto on the main dex (BTC, ETH), indices and * tokenized stocks on the trade[XYZ] HIP-3 dex (xyz:XYZ100, xyz:SP500…). * * Only the instrument pick uses it — spread for the cost score, hourly closes * for volatility, the mid for sizing. Orders, fills, balances and every * take-profit/stop price still come from DXtrade and its own fill prices. * * No key, no account: POST https://api.hyperliquid.xyz/info. */ const INFO_URL = 'https://api.hyperliquid.xyz/info'; const TIMEOUT_MS = 10_000; const META_TTL_MS = 60 * 60_000; /** Dexes whose coins a DXtrade symbol may map to, main dex first. */ const DEXES = ['', 'xyz']; async function info(body: Record): Promise { // Keep the pure quote helpers usable without opening the instance database. const { default: axios } = await import('./http-client'); const res = await axios.post(INFO_URL, body, { timeout: TIMEOUT_MS, signal: AbortSignal.timeout(TIMEOUT_MS + 5_000), headers: { 'Content-Type': 'application/json' }, }); return res.data as T; } const g = globalThis as typeof globalThis & { __hlCoins?: { coins: Set; at: number } }; /** Every coin on the dexes above ("BTC", "xyz:XYZ100", …), cached an hour. */ export async function hyperliquidCoins(): Promise> { if (g.__hlCoins && Date.now() - g.__hlCoins.at < META_TTL_MS) return g.__hlCoins.coins; const coins = new Set(); for (const dex of DEXES) { const meta = await info<{ universe?: { name?: unknown; isDelisted?: unknown }[] }>(dex ? { type: 'meta', dex } : { type: 'meta' }); for (const u of meta?.universe ?? []) { if (typeof u.name === 'string' && u.isDelisted !== true) coins.add(u.name); } } g.__hlCoins = { coins, at: Date.now() }; return coins; } /** * The Hyperliquid coin for a DXtrade symbol, or null. "BTC/USD" → "BTC" on * the main dex; "XYZ100" or "AAPL/USD" → "xyz:XYZ100" / "xyz:AAPL" on the * trade[XYZ] dex when the main dex doesn't list it. */ export function hyperliquidCoinFor(symbol: string, coins: Set): string | null { const base = symbol.toUpperCase().replace(/[/_-]?(USDT|USDC|USD)$/, ''); if (!base) return null; for (const candidate of [base, `xyz:${base}`]) { if (coins.has(candidate)) return candidate; } return null; } /** Best bid/ask of an l2Book answer; undefined sides when the book is empty. */ export function parseL2Book(book: unknown): { bid?: number; ask?: number; time: number } { const b = book as { levels?: { px?: unknown }[][]; time?: unknown }; const px = (side: number) => { const v = Number(b?.levels?.[side]?.[0]?.px); return Number.isFinite(v) && v > 0 ? v : undefined; }; const time = Number(b?.time); return { bid: px(0), ask: px(1), time: Number.isFinite(time) ? time : NaN }; } export async function hyperliquidQuote(coin: string): Promise<{ bid?: number; ask?: number; time: number }> { return parseL2Book(await info({ type: 'l2Book', coin })); } /** Hourly closes, oldest first, over the last `hours`. */ export async function hyperliquidHourlyCloses(coin: string, hours: number): Promise { const endTime = Date.now(); const candles = await info<{ t?: unknown; c?: unknown }[]>({ type: 'candleSnapshot', req: { coin, interval: '1h', startTime: endTime - (hours + 1) * 3_600_000, endTime }, }); return (Array.isArray(candles) ? candles : []) .map((c) => ({ t: Number(c.t), c: Number(c.c) })) .filter((c) => Number.isFinite(c.t) && Number.isFinite(c.c) && c.c > 0) .sort((a, b) => a.t - b.t) .map((c) => c.c); }