Files

90 lines
3.9 KiB
TypeScript

/**
* 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<T>(body: Record<string, unknown>): Promise<T> {
// 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<string>; at: number } };
/** Every coin on the dexes above ("BTC", "xyz:XYZ100", …), cached an hour. */
export async function hyperliquidCoins(): Promise<Set<string>> {
if (g.__hlCoins && Date.now() - g.__hlCoins.at < META_TTL_MS) return g.__hlCoins.coins;
const coins = new Set<string>();
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>): 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<number[]> {
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);
}