Tradeify247's API user may not read DXtrade market data (code 34), which skipped every batch at the instrument pick. Quotes and hourly candles then come from Hyperliquid's public API (BTC, ETH on the main dex; XYZ100, SP500 and tokenized stocks on trade[XYZ]) — for the pick and sizing only; orders, fills and exit prices stay DXtrade's. Closing STOP/LIMIT orders close the whole position by positionCode and must not carry a quantity (code 33) — the exit bracket was refused, and the daily-loss guard flattened both accounts. A close's fill price also falls back to its executions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.8 KiB
TypeScript
90 lines
3.8 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.
|
|
*/
|
|
|
|
import axios from 'axios';
|
|
|
|
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> {
|
|
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);
|
|
}
|