Files
himself65__finance-skills/opencli-plugins/hyperliquid/lib/api.js
T
Alex Yang 8072082fda feat: add hyperliquid-reader skill + opencli plugin (#76)
* feat: add hyperliquid-reader skill + opencli plugin

Add a read-only Hyperliquid (app.hyperliquid.xyz) reader, mirroring the
tradingview-reader pattern. Unlike TradingView's CDP attach, Hyperliquid
exposes a fully public info API (POST https://api.hyperliquid.xyz/info),
so this needs no API key, wallet, login, or desktop app.

opencli plugin (opencli-plugins/hyperliquid/) — 12 read-only commands:
- Market data: markets, spot-markets, mids, book, candles,
  funding-history, funding-compare (cross-venue HL/Binance/Bybit
  funding-arb screen).
- Account by 0x address: account, positions, spot-balances,
  open-orders, fills.

Skill (plugins/data-providers/skills/hyperliquid-reader/) — SKILL.md
(5-step + error reference), README.md, references/commands.md.

Registrations: root opencli-plugin.json, data-providers plugin.json
(description + keywords), marketplace.json, root README table.

Verification: 31 unit tests pass against captured live wire shapes;
all 12 command data paths smoke-tested end-to-end against the live API.
SKILL.md description is 1019 chars (< 1024 lint cap), no angle brackets.

No trade execution: the plugin exposes no write/exchange endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* refactor: scope hyperliquid-reader to market data only

Drop the account-by-address commands (account, positions, spot-balances,
open-orders, fills) and their lib/account.js + lib/orders.js helpers and
tests. Remove the now-unused normalizeAddress helper from lib/api.js.

The reader is now market-data only: markets, spot-markets, mids, book,
candles, funding-history, funding-compare (7 commands). Updated SKILL.md,
both READMEs, references/commands.md, the plugin/marketplace manifests,
and the root README table accordingly.

21 unit tests pass; all 7 market-data data paths re-verified live.
2026-06-07 16:21:26 -07:00

70 lines
2.3 KiB
JavaScript

/**
* Hyperliquid info API helpers.
*
* Hyperliquid exposes a fully public, read-only POST endpoint:
* POST https://api.hyperliquid.xyz/info body { "type": "...", ... } → JSON
*
* No API key, no auth, no cookies — every market-data read this plugin makes
* works unauthenticated. Placing trades requires wallet-signed actions on the
* separate /exchange endpoint, which this plugin intentionally NEVER touches.
*
* Funding rates come back per funding interval. Hyperliquid perps fund hourly,
* so the hourly rate annualizes as rate * 24 * 365. Other venues surfaced via
* `predictedFundings` may fund on a different interval (commonly 4h) — always
* normalize with the per-row interval, not a hard-coded hour.
*/
const INFO_URL = 'https://api.hyperliquid.xyz/info';
/**
* POST a request to the Hyperliquid info endpoint and return the parsed body.
* @param {object} body e.g. { type: 'metaAndAssetCtxs' }
*/
export async function infoFetch(body) {
const res = await fetch(INFO_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`hyperliquid info ${res.status}: ${text.slice(0, 200)}`);
}
return res.json();
}
/** Coerce a value (often a numeric string from the API) to a finite Number, else null. */
export function num(v) {
if (v == null) return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
/** Percent change of `now` vs `prev`; null if either is missing or prev is 0. */
export function pctChange(now, prev) {
const a = num(now);
const b = num(prev);
if (a == null || b == null || b === 0) return null;
return ((a - b) / b) * 100;
}
/**
* Annualize a per-interval funding rate to a percentage APR.
* @param {string|number} rate funding rate for one interval
* @param {string|number} [intervalHours=1] length of that interval in hours
*/
export function fundingToApr(rate, intervalHours = 1) {
const r = num(rate);
const h = num(intervalHours) || 1;
return r == null ? null : (r / h) * 24 * 365 * 100;
}
/** Millisecond epoch → ISO string (null-safe). */
export function isoTime(ms) {
const n = num(ms);
if (n == null) return null;
return new Date(n).toISOString();
}
export { INFO_URL };