mirror of
https://github.com/himself65/finance-skills.git
synced 2026-09-14 14:40:40 +08:00
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.
This commit is contained in:
@@ -91,6 +91,7 @@ Read-only social media and research feeds — Twitter/X, Discord, LinkedIn, Tele
|
||||
| [finance-sentiment](plugins/data-providers/skills/finance-sentiment/) | Stock sentiment research via Adanos Finance API — Reddit, X.com, news, Polymarket |
|
||||
| [hormuz-strait](plugins/data-providers/skills/hormuz-strait/) | Strait of Hormuz monitoring — shipping, oil impact, insurance risk, crisis timeline |
|
||||
| [tradingview-reader](plugins/data-providers/skills/tradingview-reader/) | Read-only TradingView desktop reader — quotes, full options chains with greeks/IV, expiries, chart state, screenshots — via [opencli](https://github.com/jackwener/opencli) + CDP |
|
||||
| [hyperliquid-reader](plugins/data-providers/skills/hyperliquid-reader/) | Read-only [Hyperliquid](https://app.hyperliquid.xyz) market-data reader — perp/spot markets, mids, funding (incl. cross-venue arb screen), order book, and candles — via [opencli](https://github.com/jackwener/opencli) + public info API |
|
||||
|
||||
### Startup Tools (`finance-startup-tools`)
|
||||
|
||||
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"name": "finance-skills-opencli-plugins",
|
||||
"description": "opencli plugins shipped alongside the finance-skills repo. Currently: tradingview (read-only TradingView desktop adapter).",
|
||||
"description": "opencli plugins shipped alongside the finance-skills repo: tradingview (read-only TradingView desktop adapter) and hyperliquid (read-only Hyperliquid info-API adapter).",
|
||||
"version": "0.1.0",
|
||||
"plugins": {
|
||||
"tradingview": {
|
||||
"path": "opencli-plugins/tradingview"
|
||||
},
|
||||
"hyperliquid": {
|
||||
"path": "opencli-plugins/hyperliquid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
@@ -0,0 +1,104 @@
|
||||
# opencli-plugin-hyperliquid
|
||||
|
||||
Read-only [opencli](https://github.com/jackwener/opencli) adapter for **[Hyperliquid](https://app.hyperliquid.xyz)**, the on-chain perps/spot DEX. Exposes perp + spot market data — markets, mid prices, L2 order book, OHLCV candles, funding history, and a cross-venue funding-arb screen — all from Hyperliquid's **public info API**. No API key, no wallet, no login.
|
||||
|
||||
This plugin lives inside the [`himself65/finance-skills`](https://github.com/himself65/finance-skills) monorepo. Install it via opencli's monorepo subpath syntax:
|
||||
|
||||
```bash
|
||||
opencli plugin install github:himself65/finance-skills/hyperliquid
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Prereqs: Node ≥ 22 (built-in fetch)
|
||||
npm install -g @jackwener/opencli
|
||||
opencli plugin install github:himself65/finance-skills/hyperliquid
|
||||
```
|
||||
|
||||
**Zero setup.** Every command hits `https://api.hyperliquid.xyz/info` directly — no API key, no auth, no cookies, no running app.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | Output columns |
|
||||
|---|---|---|
|
||||
| `hyperliquid markets` | Perp markets table | `coin`, `markPx`, `midPx`, `oraclePx`, `change24hPct`, `fundingHrPct`, `fundingAprPct`, `openInterest`, `oiNotional`, `dayNtlVlm`, `premiumPct`, `maxLeverage` |
|
||||
| `hyperliquid spot-markets` | Spot pairs table | `pair`, `base`, `markPx`, `midPx`, `change24hPct`, `dayNtlVlm`, `circulatingSupply`, `marketCap`, `canonical` |
|
||||
| `hyperliquid mids` | Mid price for every market | `coin`, `mid` |
|
||||
| `hyperliquid book --coin BTC` | L2 order book snapshot | `side`, `level`, `px`, `sz`, `orders` |
|
||||
| `hyperliquid candles --coin BTC` | OHLCV candles | `time`, `open`, `high`, `low`, `close`, `volume`, `trades` |
|
||||
| `hyperliquid funding-history --coin BTC` | Historical hourly funding | `coin`, `fundingRatePct`, `fundingAprPct`, `premiumPct`, `time` |
|
||||
| `hyperliquid funding-compare` | Cross-venue predicted funding (arb) | `coin`, `hlAprPct`, `binanceAprPct`, `bybitAprPct`, `hlVsBinancePct`, `hlVsBybitPct`, `nextHlFunding` |
|
||||
|
||||
`markets` flags: `--coin`, `--sort {dayNtlVlm|change24hPct|fundingAprPct|fundingHrPct|openInterest|oiNotional|markPx|coin}` (default `dayNtlVlm`), `--limit`, `--include-delisted`.
|
||||
|
||||
`spot-markets` flags: `--pair` (pair or base token), `--sort {dayNtlVlm|change24hPct|marketCap|markPx|pair}`, `--limit`, `--canonical-only`.
|
||||
|
||||
`book` flags: `--coin` (required), `--depth` (1-20, default 10), `--n-sig-figs` (2-5 price aggregation).
|
||||
|
||||
`candles` flags: `--coin` (required), `--interval {1m|3m|5m|15m|30m|1h|2h|4h|8h|12h|1d|3d|1w|1M}` (default `1h`), `--limit` (default 100, max 5000).
|
||||
|
||||
`funding-history` flags: `--coin` (required), `--hours` (default 24), `--limit`.
|
||||
|
||||
`funding-compare` flags: `--coin`, `--sort {hlVsBinancePct|hlVsBybitPct|hlAprPct|binanceAprPct|bybitAprPct|coin}` (default `hlVsBinancePct`, ranked by absolute spread), `--limit`.
|
||||
|
||||
All commands accept `-f json|yaml|md|csv|table`.
|
||||
|
||||
## Data path
|
||||
|
||||
Every command issues a single `POST https://api.hyperliquid.xyz/info` with a `{ "type": "..." }` body and normalizes the response:
|
||||
|
||||
| Command | info `type` |
|
||||
|---|---|
|
||||
| `markets` | `metaAndAssetCtxs` |
|
||||
| `spot-markets` | `spotMetaAndAssetCtxs` |
|
||||
| `mids` | `allMids` (+ `spotMeta` to resolve `@index` → pair name) |
|
||||
| `book` | `l2Book` |
|
||||
| `candles` | `candleSnapshot` |
|
||||
| `funding-history` | `fundingHistory` |
|
||||
| `funding-compare` | `predictedFundings` |
|
||||
|
||||
**Numbers** arrive as strings and are coerced to finite numbers (or `null`). **Funding** is reported per interval — Hyperliquid perps fund hourly, so APR = `rate × 24 × 365`; `funding-compare` annualizes each venue with its own interval (Binance/Bybit commonly 4h).
|
||||
|
||||
## Auth model
|
||||
|
||||
None. The info endpoint is fully public and read-only. There is **no** trading path in this plugin — placing/cancelling orders on Hyperliquid requires wallet-signed actions on the separate `/exchange` endpoint, which this adapter never calls.
|
||||
|
||||
## Status
|
||||
|
||||
**v0.1 — wire shapes verified live against `api.hyperliquid.xyz` (June 2026).** Pure-helper normalizers are unit-tested (`npm test`); the HTTP path is a single documented `POST /info` per command.
|
||||
|
||||
Known notes:
|
||||
- `mids` resolves non-canonical spot keys (`@<index>`) to `BASE/QUOTE` via the token table; perp coin names and canonical pair names pass through unchanged, and builder-deployed perp-dex keys (`#<n>`) are surfaced as-is.
|
||||
- `markets`/`spot-markets`/`funding-*` numeric sorts place `null` last.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
opencli-plugins/hyperliquid/
|
||||
├── opencli-plugin.json # plugin manifest
|
||||
├── package.json # Node package (type: module)
|
||||
├── lib/
|
||||
│ ├── api.js # infoFetch POST helper, num/pctChange/funding/isoTime helpers
|
||||
│ ├── markets.js # perp + spot market normalizers, allMids resolver
|
||||
│ ├── funding.js # funding history + cross-venue predicted-funding pivot
|
||||
│ ├── book.js # l2 book flattener + spread summary
|
||||
│ └── candles.js # interval table + OHLCV normalizer
|
||||
├── markets.js # metaAndAssetCtxs → perp markets
|
||||
├── spot-markets.js # spotMetaAndAssetCtxs → spot pairs
|
||||
├── mids.js # allMids → mid prices
|
||||
├── book.js # l2Book → order book
|
||||
├── candles.js # candleSnapshot → OHLCV
|
||||
├── funding-history.js # fundingHistory → historical funding
|
||||
├── funding-compare.js # predictedFundings → cross-venue arb screen
|
||||
└── tests/
|
||||
├── api.test.js # num, pctChange, fundingToApr, isoTime
|
||||
├── markets.test.js # perp/spot normalizers, allMids resolver
|
||||
├── funding.test.js # funding history + cross-venue pivot/APR
|
||||
├── book.test.js # l2 flatten + spread
|
||||
└── candles.test.js # OHLCV normalizer
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* hyperliquid book — L2 order book snapshot via `l2Book`.
|
||||
* Up to `--depth` levels per side; bids first, then asks.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { normalizeBook } from './lib/book.js';
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'book',
|
||||
description: 'L2 order book snapshot (top N levels per side) for a coin',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', required: true, help: 'Coin or spot pair (e.g. BTC, PURR/USDC)' },
|
||||
{ name: 'depth', type: 'int', default: 10, help: 'Levels per side (1-20, default 10)' },
|
||||
{ name: 'n-sig-figs', type: 'int', help: 'Price aggregation significant figures (2-5). Omit for full precision.' },
|
||||
],
|
||||
columns: ['side', 'level', 'px', 'sz', 'orders'],
|
||||
func: async (args) => {
|
||||
const coin = String(args.coin).trim();
|
||||
const body = { type: 'l2Book', coin };
|
||||
const nSigFigs = Number(args['n-sig-figs']);
|
||||
if (Number.isFinite(nSigFigs)) body.nSigFigs = nSigFigs;
|
||||
|
||||
const payload = await infoFetch(body);
|
||||
const rows = normalizeBook(payload, args.depth);
|
||||
if (rows.length === 0) {
|
||||
throw new Error(`No order book returned for "${coin}" — check the coin/pair symbol.`);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* hyperliquid candles — OHLCV history via `candleSnapshot`.
|
||||
* Pulls the most recent `--limit` candles of `--interval` for a coin.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { INTERVAL_MS, normalizeCandles } from './lib/candles.js';
|
||||
|
||||
const INTERVALS = Object.keys(INTERVAL_MS);
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'candles',
|
||||
description: 'OHLCV candles for a coin (most recent N of a given interval)',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', required: true, help: 'Coin or spot pair (e.g. BTC, PURR/USDC)' },
|
||||
{ name: 'interval', default: '1h', choices: INTERVALS, help: 'Candle interval (default 1h)' },
|
||||
{ name: 'limit', type: 'int', default: 100, help: 'Number of most-recent candles (default 100, max 5000)' },
|
||||
],
|
||||
columns: ['time', 'open', 'high', 'low', 'close', 'volume', 'trades'],
|
||||
func: async (args) => {
|
||||
const coin = String(args.coin).trim();
|
||||
const interval = String(args.interval);
|
||||
const stepMs = INTERVAL_MS[interval];
|
||||
if (!stepMs) throw new Error(`Unsupported interval "${interval}". One of: ${INTERVALS.join(', ')}`);
|
||||
|
||||
const limit = Math.min(Math.max(1, Number(args.limit) || 100), 5000);
|
||||
const endTime = Date.now();
|
||||
const startTime = endTime - stepMs * limit;
|
||||
|
||||
const rows = await infoFetch({
|
||||
type: 'candleSnapshot',
|
||||
req: { coin, interval, startTime, endTime },
|
||||
});
|
||||
const out = normalizeCandles(rows);
|
||||
if (out.length === 0) {
|
||||
throw new Error(`No candles returned for "${coin}" @ ${interval} — check the coin/pair symbol.`);
|
||||
}
|
||||
return out.slice(-limit);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* hyperliquid funding-compare — cross-venue predicted funding via
|
||||
* `predictedFundings`, pivoted to one row per coin with each venue's APR and
|
||||
* the HL-vs-venue spread. A funding-arb screen.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { normalizePredictedFundings } from './lib/funding.js';
|
||||
|
||||
const SORT_FIELDS = ['hlVsBinancePct', 'hlVsBybitPct', 'hlAprPct', 'binanceAprPct', 'bybitAprPct', 'coin'];
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'funding-compare',
|
||||
description: 'Cross-venue predicted funding (HL vs Binance vs Bybit), annualized, with spreads — funding-arb screen',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', help: 'Filter to one coin (e.g. BTC). Omit for all.' },
|
||||
{ name: 'sort', default: 'hlVsBinancePct', choices: SORT_FIELDS, help: 'Sort field (by absolute value desc, except coin = asc)' },
|
||||
{ name: 'limit', type: 'int', help: 'Max rows after sort (omit for all)' },
|
||||
],
|
||||
columns: ['coin', 'hlAprPct', 'binanceAprPct', 'bybitAprPct', 'hlVsBinancePct', 'hlVsBybitPct', 'nextHlFunding'],
|
||||
func: async (args) => {
|
||||
const data = await infoFetch({ type: 'predictedFundings' });
|
||||
let rows = normalizePredictedFundings(data, args.coin);
|
||||
if (args.coin && rows.length === 0) {
|
||||
throw new Error(`No predicted funding for coin "${args.coin}".`);
|
||||
}
|
||||
|
||||
const sortKey = String(args.sort || 'hlVsBinancePct');
|
||||
if (sortKey === 'coin') {
|
||||
rows.sort((a, b) => String(a.coin).localeCompare(String(b.coin)));
|
||||
} else {
|
||||
// Spread fields rank by magnitude; raw APR fields rank signed-desc.
|
||||
const byMagnitude = sortKey.startsWith('hlVs');
|
||||
rows.sort((a, b) => {
|
||||
const av = a[sortKey];
|
||||
const bv = b[sortKey];
|
||||
const ak = av == null ? -Infinity : byMagnitude ? Math.abs(av) : av;
|
||||
const bk = bv == null ? -Infinity : byMagnitude ? Math.abs(bv) : bv;
|
||||
return bk - ak;
|
||||
});
|
||||
}
|
||||
|
||||
const limit = Number(args.limit);
|
||||
if (Number.isFinite(limit) && limit > 0) rows = rows.slice(0, limit);
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* hyperliquid funding-history — historical hourly funding for a coin via
|
||||
* `fundingHistory`. Returns prints from now back `--hours` hours.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { normalizeFundingHistory } from './lib/funding.js';
|
||||
|
||||
const HOUR_MS = 3_600_000;
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'funding-history',
|
||||
description: 'Historical hourly funding rates (+ APR, premium) for a coin',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', required: true, help: 'Coin (e.g. BTC)' },
|
||||
{ name: 'hours', type: 'int', default: 24, help: 'Lookback window in hours (default 24)' },
|
||||
{ name: 'limit', type: 'int', help: 'Cap rows to the most recent N (omit for all in window)' },
|
||||
],
|
||||
columns: ['coin', 'fundingRatePct', 'fundingAprPct', 'premiumPct', 'time'],
|
||||
func: async (args) => {
|
||||
const coin = String(args.coin).toUpperCase().trim();
|
||||
const hours = Math.max(1, Number(args.hours) || 24);
|
||||
const startTime = Date.now() - hours * HOUR_MS;
|
||||
|
||||
const rows = await infoFetch({ type: 'fundingHistory', coin, startTime });
|
||||
let out = normalizeFundingHistory(rows);
|
||||
out.sort((a, b) => String(b.time).localeCompare(String(a.time))); // newest first
|
||||
|
||||
const limit = Number(args.limit);
|
||||
if (Number.isFinite(limit) && limit > 0) out = out.slice(0, limit);
|
||||
return out;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Order-book normalizer for `l2Book`.
|
||||
* Payload: { coin, time, levels: [ bids[], asks[] ] }, each level { px, sz, n }.
|
||||
*/
|
||||
|
||||
import { num } from './api.js';
|
||||
|
||||
/**
|
||||
* Flatten an l2 snapshot into rows capped at `depth` levels per side.
|
||||
* Bids first (best/highest px first as returned), then asks.
|
||||
*/
|
||||
export function normalizeBook(payload, depth) {
|
||||
const levels = payload?.levels ?? [[], []];
|
||||
const d = Number.isFinite(Number(depth)) && Number(depth) > 0 ? Number(depth) : 10;
|
||||
const mk = (side) => (lvl, i) => ({
|
||||
side,
|
||||
level: i + 1,
|
||||
px: num(lvl.px),
|
||||
sz: num(lvl.sz),
|
||||
orders: lvl.n ?? null,
|
||||
});
|
||||
const bids = (levels[0] ?? []).slice(0, d).map(mk('bid'));
|
||||
const asks = (levels[1] ?? []).slice(0, d).map(mk('ask'));
|
||||
return [...bids, ...asks];
|
||||
}
|
||||
|
||||
/** Best-bid/ask spread summary from a raw l2 payload (for skill prose). */
|
||||
export function bookSpread(payload) {
|
||||
const levels = payload?.levels ?? [[], []];
|
||||
const bestBid = num(levels[0]?.[0]?.px);
|
||||
const bestAsk = num(levels[1]?.[0]?.px);
|
||||
if (bestBid == null || bestAsk == null) {
|
||||
return { bestBid, bestAsk, mid: null, spread: null, spreadBps: null };
|
||||
}
|
||||
const mid = (bestBid + bestAsk) / 2;
|
||||
const spread = bestAsk - bestBid;
|
||||
return { bestBid, bestAsk, mid, spread, spreadBps: mid ? (spread / mid) * 10000 : null };
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Candle helpers for `candleSnapshot`.
|
||||
* Wire row: { t (open ms), T (close ms), s (coin), i (interval), o, h, l, c, v, n }.
|
||||
*/
|
||||
|
||||
import { num, isoTime } from './api.js';
|
||||
|
||||
/** Supported interval → milliseconds. Used to derive a startTime from a count. */
|
||||
export const INTERVAL_MS = {
|
||||
'1m': 60_000,
|
||||
'3m': 180_000,
|
||||
'5m': 300_000,
|
||||
'15m': 900_000,
|
||||
'30m': 1_800_000,
|
||||
'1h': 3_600_000,
|
||||
'2h': 7_200_000,
|
||||
'4h': 14_400_000,
|
||||
'8h': 28_800_000,
|
||||
'12h': 43_200_000,
|
||||
'1d': 86_400_000,
|
||||
'3d': 259_200_000,
|
||||
'1w': 604_800_000,
|
||||
'1M': 2_592_000_000,
|
||||
};
|
||||
|
||||
/** Normalize raw candle rows to OHLCV with ISO open time. */
|
||||
export function normalizeCandles(rows) {
|
||||
return (rows ?? []).map((c) => ({
|
||||
time: isoTime(c.t),
|
||||
open: num(c.o),
|
||||
high: num(c.h),
|
||||
low: num(c.l),
|
||||
close: num(c.c),
|
||||
volume: num(c.v),
|
||||
trades: c.n ?? null,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Funding normalizers: historical funding (`fundingHistory`) and the
|
||||
* cross-venue predicted-funding screen (`predictedFundings`).
|
||||
*/
|
||||
|
||||
import { num, isoTime, fundingToApr } from './api.js';
|
||||
|
||||
/** One row per historical hourly funding print for a coin. */
|
||||
export function normalizeFundingHistory(rows) {
|
||||
return (rows ?? []).map((r) => {
|
||||
const rate = num(r.fundingRate);
|
||||
const premium = num(r.premium);
|
||||
return {
|
||||
coin: r.coin,
|
||||
fundingRatePct: rate == null ? null : rate * 100,
|
||||
fundingAprPct: fundingToApr(r.fundingRate, 1),
|
||||
premiumPct: premium == null ? null : premium * 100,
|
||||
time: isoTime(r.time),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const VENUE_KEYS = { HlPerp: 'hl', BinPerp: 'binance', BybitPerp: 'bybit' };
|
||||
|
||||
/**
|
||||
* Pivot `predictedFundings` into one row per coin with each venue's funding
|
||||
* normalized to APR %, plus HL-vs-venue spreads — a funding-arb screen.
|
||||
*
|
||||
* Wire shape:
|
||||
* [ [coin, [ [venueName, {fundingRate, fundingIntervalHours, nextFundingTime}], ... ]], ... ]
|
||||
*
|
||||
* Venue funding intervals differ (HL hourly, Binance/Bybit usually 4h), so
|
||||
* each leg is annualized with its own `fundingIntervalHours`.
|
||||
*
|
||||
* @param {Array} data
|
||||
* @param {string} [coinFilter] exact (case-insensitive) coin match
|
||||
*/
|
||||
export function normalizePredictedFundings(data, coinFilter) {
|
||||
const filter = coinFilter ? String(coinFilter).toUpperCase() : null;
|
||||
const rows = [];
|
||||
for (const entry of data ?? []) {
|
||||
const coin = entry?.[0];
|
||||
if (filter && String(coin).toUpperCase() !== filter) continue;
|
||||
const apr = {};
|
||||
let nextHl = null;
|
||||
for (const [vName, v] of entry?.[1] ?? []) {
|
||||
const key = VENUE_KEYS[vName];
|
||||
if (!key || !v) continue;
|
||||
apr[key] = fundingToApr(v.fundingRate, v.fundingIntervalHours);
|
||||
if (key === 'hl') nextHl = v.nextFundingTime;
|
||||
}
|
||||
const hl = apr.hl ?? null;
|
||||
rows.push({
|
||||
coin,
|
||||
hlAprPct: hl,
|
||||
binanceAprPct: apr.binance ?? null,
|
||||
bybitAprPct: apr.bybit ?? null,
|
||||
hlVsBinancePct: hl != null && apr.binance != null ? hl - apr.binance : null,
|
||||
hlVsBybitPct: hl != null && apr.bybit != null ? hl - apr.bybit : null,
|
||||
nextHlFunding: isoTime(nextHl),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Market normalizers for Hyperliquid perp/spot metadata + asset contexts.
|
||||
*
|
||||
* `metaAndAssetCtxs` and `spotMetaAndAssetCtxs` each return a 2-tuple
|
||||
* [meta, ctxs] where `meta.universe[i]` and `ctxs[i]` are PARALLEL arrays in
|
||||
* the same order — zip by index.
|
||||
*/
|
||||
|
||||
import { num, pctChange, fundingToApr } from './api.js';
|
||||
|
||||
/**
|
||||
* Zip perp `meta.universe[]` with `ctxs[]` into one normalized row per market.
|
||||
* @param {{universe: Array<{name:string,maxLeverage?:number,isDelisted?:boolean}>}} meta
|
||||
* @param {Array<object>} ctxs
|
||||
*/
|
||||
export function normalizePerpMarkets(meta, ctxs) {
|
||||
const universe = meta?.universe ?? [];
|
||||
return universe.map((u, i) => {
|
||||
const c = ctxs?.[i] ?? {};
|
||||
const markPx = num(c.markPx);
|
||||
const funding = num(c.funding);
|
||||
const oi = num(c.openInterest);
|
||||
const premium = num(c.premium);
|
||||
return {
|
||||
coin: u.name,
|
||||
markPx,
|
||||
midPx: num(c.midPx),
|
||||
oraclePx: num(c.oraclePx),
|
||||
prevDayPx: num(c.prevDayPx),
|
||||
change24hPct: pctChange(c.markPx, c.prevDayPx),
|
||||
fundingHrPct: funding == null ? null : funding * 100,
|
||||
fundingAprPct: fundingToApr(c.funding, 1),
|
||||
openInterest: oi,
|
||||
oiNotional: oi != null && markPx != null ? oi * markPx : null,
|
||||
dayNtlVlm: num(c.dayNtlVlm),
|
||||
premiumPct: premium == null ? null : premium * 100,
|
||||
maxLeverage: u.maxLeverage ?? null,
|
||||
delisted: u.isDelisted === true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip spot `meta.universe[]` with `ctxs[]` into one normalized row per pair.
|
||||
* Resolves the base-token name via `meta.tokens[]` (universe.tokens[0] = base).
|
||||
*/
|
||||
export function normalizeSpotMarkets(spotMeta, ctxs) {
|
||||
const universe = spotMeta?.universe ?? [];
|
||||
const tokens = spotMeta?.tokens ?? [];
|
||||
const tokenName = (idx) => tokens.find((t) => t.index === idx)?.name ?? `#${idx}`;
|
||||
return universe.map((u, i) => {
|
||||
const c = ctxs?.[i] ?? {};
|
||||
const markPx = num(c.markPx);
|
||||
const supply = num(c.circulatingSupply);
|
||||
return {
|
||||
pair: c.coin ?? u.name,
|
||||
base: Array.isArray(u.tokens) ? tokenName(u.tokens[0]) : null,
|
||||
markPx,
|
||||
midPx: num(c.midPx),
|
||||
prevDayPx: num(c.prevDayPx),
|
||||
change24hPct: pctChange(c.markPx, c.prevDayPx),
|
||||
dayNtlVlm: num(c.dayNtlVlm),
|
||||
circulatingSupply: supply,
|
||||
marketCap: markPx != null && supply != null ? markPx * supply : null,
|
||||
canonical: u.isCanonical === true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `allMids` keys to display names.
|
||||
*
|
||||
* allMids keys come in three forms:
|
||||
* - perp coin name + canonical spot pair name → already friendly, passed through
|
||||
* - "@<index>" → a non-canonical spot pair; resolved to "BASE/QUOTE" via tokens
|
||||
* - "#<n>" → a builder-deployed perp-dex market; passed through as-is
|
||||
*
|
||||
* @param {Record<string,string>} allMids
|
||||
* @param {object} spotMeta result of `spotMeta` (universe + tokens)
|
||||
* @param {string} [coinFilter] case-insensitive substring filter
|
||||
*/
|
||||
export function resolveMids(allMids, spotMeta, coinFilter) {
|
||||
const tokens = spotMeta?.tokens ?? [];
|
||||
const tokenName = (idx) => tokens.find((t) => t.index === idx)?.name ?? `#${idx}`;
|
||||
const nameByKey = new Map();
|
||||
for (const u of spotMeta?.universe ?? []) {
|
||||
if (Array.isArray(u.tokens) && u.tokens.length === 2) {
|
||||
nameByKey.set(`@${u.index}`, `${tokenName(u.tokens[0])}/${tokenName(u.tokens[1])}`);
|
||||
}
|
||||
}
|
||||
const filter = coinFilter ? String(coinFilter).toUpperCase() : null;
|
||||
const rows = [];
|
||||
for (const [key, px] of Object.entries(allMids ?? {})) {
|
||||
const coin = nameByKey.get(key) ?? key;
|
||||
if (filter && !coin.toUpperCase().includes(filter)) continue;
|
||||
rows.push({ coin, mid: num(px) });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* hyperliquid markets — perpetual markets table via `metaAndAssetCtxs`.
|
||||
* Mark/oracle price, 24h change, funding (hourly + annualized), open interest,
|
||||
* and 24h notional volume for every listed perp.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { normalizePerpMarkets } from './lib/markets.js';
|
||||
|
||||
const SORT_FIELDS = ['dayNtlVlm', 'change24hPct', 'fundingAprPct', 'fundingHrPct', 'openInterest', 'oiNotional', 'markPx', 'coin'];
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'markets',
|
||||
description: 'Perpetual markets — mark/oracle price, 24h change, funding (hourly + APR), open interest, 24h volume',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', help: 'Filter to one coin (e.g. BTC). Omit for all perps.' },
|
||||
{ name: 'sort', default: 'dayNtlVlm', choices: SORT_FIELDS, help: 'Sort field (desc, except coin = asc)' },
|
||||
{ name: 'limit', type: 'int', help: 'Max rows after sort (omit for all)' },
|
||||
{ name: 'include-delisted', type: 'boolean', default: false, help: 'Include delisted markets' },
|
||||
],
|
||||
columns: ['coin', 'markPx', 'midPx', 'oraclePx', 'change24hPct', 'fundingHrPct', 'fundingAprPct', 'openInterest', 'oiNotional', 'dayNtlVlm', 'premiumPct', 'maxLeverage'],
|
||||
func: async (args) => {
|
||||
const [meta, ctxs] = await infoFetch({ type: 'metaAndAssetCtxs' });
|
||||
let rows = normalizePerpMarkets(meta, ctxs);
|
||||
if (!args['include-delisted']) rows = rows.filter((r) => !r.delisted);
|
||||
|
||||
if (args.coin) {
|
||||
const c = String(args.coin).toUpperCase().trim();
|
||||
rows = rows.filter((r) => r.coin.toUpperCase() === c);
|
||||
if (rows.length === 0) throw new Error(`No perp market for coin "${args.coin}" — check the symbol.`);
|
||||
}
|
||||
|
||||
const sortKey = String(args.sort || 'dayNtlVlm');
|
||||
if (sortKey === 'coin') {
|
||||
rows.sort((a, b) => String(a.coin).localeCompare(String(b.coin)));
|
||||
} else {
|
||||
rows.sort((a, b) => (b[sortKey] ?? -Infinity) - (a[sortKey] ?? -Infinity));
|
||||
}
|
||||
|
||||
const limit = Number(args.limit);
|
||||
if (Number.isFinite(limit) && limit > 0) rows = rows.slice(0, limit);
|
||||
|
||||
// Drop internal-only fields from the emitted rows.
|
||||
return rows.map(({ delisted, prevDayPx, ...rest }) => rest);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* hyperliquid mids — current mid price for every market via `allMids`.
|
||||
* Perp mids are keyed by coin name; spot mids by "@<index>", resolved to the
|
||||
* pair name via `spotMeta`.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { resolveMids } from './lib/markets.js';
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'mids',
|
||||
description: 'Current mid price for every perp + spot market (allMids)',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'coin', help: 'Case-insensitive substring filter (e.g. BTC). Omit for all markets.' },
|
||||
],
|
||||
columns: ['coin', 'mid'],
|
||||
func: async (args) => {
|
||||
const [allMids, spotMeta] = await Promise.all([
|
||||
infoFetch({ type: 'allMids' }),
|
||||
infoFetch({ type: 'spotMeta' }),
|
||||
]);
|
||||
const rows = resolveMids(allMids, spotMeta, args.coin);
|
||||
if (args.coin && rows.length === 0) {
|
||||
throw new Error(`No market matching "${args.coin}".`);
|
||||
}
|
||||
rows.sort((a, b) => String(a.coin).localeCompare(String(b.coin)));
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "hyperliquid",
|
||||
"description": "Read-only adapter for Hyperliquid's public info API. Perp/spot market data — markets, mid prices, L2 order book, candles, and funding (incl. a cross-venue funding-arb screen).",
|
||||
"version": "0.1.0",
|
||||
"opencli": ">=1.8.0"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@himself65/opencli-plugin-hyperliquid",
|
||||
"version": "0.1.0",
|
||||
"description": "Read-only opencli adapter for Hyperliquid's public info API — perp/spot markets, mids, L2 order book, candles, funding history, and a cross-venue funding-arb screen. Market data only, no API key.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test tests/*.test.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jackwener/opencli": ">=1.8.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "himself65"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/himself65/finance-skills.git",
|
||||
"directory": "opencli-plugins/hyperliquid"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* hyperliquid spot-markets — spot pairs table via `spotMetaAndAssetCtxs`.
|
||||
* Mark/mid price, 24h change, 24h volume, circulating supply, and market cap.
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { infoFetch } from './lib/api.js';
|
||||
import { normalizeSpotMarkets } from './lib/markets.js';
|
||||
|
||||
const SORT_FIELDS = ['dayNtlVlm', 'change24hPct', 'marketCap', 'markPx', 'pair'];
|
||||
|
||||
cli({
|
||||
site: 'hyperliquid',
|
||||
name: 'spot-markets',
|
||||
description: 'Spot pairs — mark/mid price, 24h change, 24h volume, circulating supply, market cap',
|
||||
access: 'read',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'pair', help: 'Filter by pair or base token (e.g. PURR or PURR/USDC). Omit for all.' },
|
||||
{ name: 'sort', default: 'dayNtlVlm', choices: SORT_FIELDS, help: 'Sort field (desc, except pair = asc)' },
|
||||
{ name: 'limit', type: 'int', help: 'Max rows after sort (omit for all)' },
|
||||
{ name: 'canonical-only', type: 'boolean', default: false, help: 'Only show canonical (named) pairs, hiding @index pairs' },
|
||||
],
|
||||
columns: ['pair', 'base', 'markPx', 'midPx', 'change24hPct', 'dayNtlVlm', 'circulatingSupply', 'marketCap', 'canonical'],
|
||||
func: async (args) => {
|
||||
const [meta, ctxs] = await infoFetch({ type: 'spotMetaAndAssetCtxs' });
|
||||
let rows = normalizeSpotMarkets(meta, ctxs);
|
||||
|
||||
if (args['canonical-only']) rows = rows.filter((r) => r.canonical);
|
||||
|
||||
if (args.pair) {
|
||||
const q = String(args.pair).toUpperCase().trim();
|
||||
rows = rows.filter((r) => String(r.pair).toUpperCase().includes(q) || String(r.base).toUpperCase() === q);
|
||||
if (rows.length === 0) throw new Error(`No spot pair matching "${args.pair}".`);
|
||||
}
|
||||
|
||||
const sortKey = String(args.sort || 'dayNtlVlm');
|
||||
if (sortKey === 'pair') {
|
||||
rows.sort((a, b) => String(a.pair).localeCompare(String(b.pair)));
|
||||
} else {
|
||||
rows.sort((a, b) => (b[sortKey] ?? -Infinity) - (a[sortKey] ?? -Infinity));
|
||||
}
|
||||
|
||||
const limit = Number(args.limit);
|
||||
if (Number.isFinite(limit) && limit > 0) rows = rows.slice(0, limit);
|
||||
|
||||
return rows.map(({ prevDayPx, ...rest }) => rest);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { num, pctChange, fundingToApr, isoTime } from '../lib/api.js';
|
||||
|
||||
test('num — coerces numeric strings, else null', () => {
|
||||
assert.equal(num('61791.0'), 61791);
|
||||
assert.equal(num(42), 42);
|
||||
assert.equal(num(0), 0);
|
||||
assert.equal(num(null), null);
|
||||
assert.equal(num(undefined), null);
|
||||
assert.equal(num('not-a-number'), null);
|
||||
});
|
||||
|
||||
test('pctChange — basic + guards', () => {
|
||||
assert.equal(pctChange(110, 100), 10);
|
||||
assert.equal(pctChange('61791.0', '61001.0').toFixed(4), '1.2951');
|
||||
assert.equal(pctChange(100, 0), null);
|
||||
assert.equal(pctChange(null, 100), null);
|
||||
assert.equal(pctChange(100, null), null);
|
||||
});
|
||||
|
||||
test('fundingToApr — hourly and multi-hour intervals', () => {
|
||||
// 0.0000125 hourly → 0.0000125 * 24 * 365 * 100 = 10.95% APR
|
||||
assert.equal(fundingToApr('0.0000125', 1).toFixed(4), '10.9500');
|
||||
// Same rate over a 4h interval annualizes to a quarter of the hourly figure.
|
||||
assert.equal(fundingToApr('0.0000125', 4).toFixed(5), '2.73750');
|
||||
assert.equal(fundingToApr(null), null);
|
||||
// Missing/zero interval defaults to hourly.
|
||||
assert.equal(fundingToApr('0.0000125', 0).toFixed(4), '10.9500');
|
||||
assert.equal(fundingToApr('0.0000125').toFixed(4), '10.9500');
|
||||
});
|
||||
|
||||
test('isoTime — ms epoch → ISO, null-safe', () => {
|
||||
assert.equal(isoTime(1780813166432), '2026-06-07T06:19:26.432Z');
|
||||
assert.equal(isoTime(null), null);
|
||||
assert.equal(isoTime('not-a-number'), null);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeBook, bookSpread } from '../lib/book.js';
|
||||
|
||||
// Shape captured live from l2Book.
|
||||
const BOOK = {
|
||||
coin: 'BTC', time: 1780813166432,
|
||||
levels: [
|
||||
[ // bids (highest first)
|
||||
{ px: '61794.0', sz: '19.80703', n: 156 },
|
||||
{ px: '61793.0', sz: '2.87733', n: 25 },
|
||||
{ px: '61792.0', sz: '2.44433', n: 10 },
|
||||
],
|
||||
[ // asks (lowest first)
|
||||
{ px: '61795.0', sz: '5.0', n: 12 },
|
||||
{ px: '61796.0', sz: '3.0', n: 8 },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
test('normalizeBook — bids then asks, capped at depth', () => {
|
||||
const rows = normalizeBook(BOOK, 2);
|
||||
assert.equal(rows.length, 4); // 2 bids + 2 asks
|
||||
assert.deepEqual(rows[0], { side: 'bid', level: 1, px: 61794, sz: 19.80703, orders: 156 });
|
||||
assert.equal(rows[1].side, 'bid');
|
||||
assert.equal(rows[2].side, 'ask');
|
||||
assert.equal(rows[2].px, 61795);
|
||||
assert.equal(rows[3].px, 61796);
|
||||
});
|
||||
|
||||
test('normalizeBook — default depth 10, fewer levels ok', () => {
|
||||
const rows = normalizeBook(BOOK);
|
||||
assert.equal(rows.filter((r) => r.side === 'bid').length, 3);
|
||||
assert.equal(rows.filter((r) => r.side === 'ask').length, 2);
|
||||
});
|
||||
|
||||
test('normalizeBook — empty payload', () => {
|
||||
assert.deepEqual(normalizeBook({}, 5), []);
|
||||
assert.deepEqual(normalizeBook({ levels: [[], []] }, 5), []);
|
||||
});
|
||||
|
||||
test('bookSpread — best bid/ask, mid, spread, bps', () => {
|
||||
const s = bookSpread(BOOK);
|
||||
assert.equal(s.bestBid, 61794);
|
||||
assert.equal(s.bestAsk, 61795);
|
||||
assert.equal(s.mid, 61794.5);
|
||||
assert.equal(s.spread, 1);
|
||||
assert.equal(s.spreadBps.toFixed(4), (1 / 61794.5 * 10000).toFixed(4));
|
||||
});
|
||||
|
||||
test('bookSpread — missing side → nulls', () => {
|
||||
const s = bookSpread({ levels: [[], []] });
|
||||
assert.equal(s.mid, null);
|
||||
assert.equal(s.spread, null);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeCandles, INTERVAL_MS } from '../lib/candles.js';
|
||||
|
||||
// Shape captured live from candleSnapshot.
|
||||
const CANDLES = [
|
||||
{ t: 1780804800000, T: 1780808399999, s: 'BTC', i: '1h', o: '61685.0', c: '61871.0', h: '61888.0', l: '61463.0', v: '1376.39573', n: 18040 },
|
||||
{ t: 1780808400000, T: 1780811999999, s: 'BTC', i: '1h', o: '61871.0', c: '61721.0', h: '62130.0', l: '61630.0', v: '1733.82548', n: 24641 },
|
||||
];
|
||||
|
||||
test('normalizeCandles — OHLCV with ISO open time', () => {
|
||||
const rows = normalizeCandles(CANDLES);
|
||||
assert.equal(rows.length, 2);
|
||||
const c = rows[0];
|
||||
assert.equal(c.time, new Date(1780804800000).toISOString());
|
||||
assert.equal(c.open, 61685);
|
||||
assert.equal(c.high, 61888);
|
||||
assert.equal(c.low, 61463);
|
||||
assert.equal(c.close, 61871);
|
||||
assert.equal(c.volume, 1376.39573);
|
||||
assert.equal(c.trades, 18040);
|
||||
});
|
||||
|
||||
test('normalizeCandles — empty input', () => {
|
||||
assert.deepEqual(normalizeCandles([]), []);
|
||||
assert.deepEqual(normalizeCandles(null), []);
|
||||
});
|
||||
|
||||
test('INTERVAL_MS — covers the documented set, consistent math', () => {
|
||||
assert.equal(INTERVAL_MS['1h'], 3_600_000);
|
||||
assert.equal(INTERVAL_MS['1d'], 24 * INTERVAL_MS['1h']);
|
||||
assert.equal(INTERVAL_MS['1w'], 7 * INTERVAL_MS['1d']);
|
||||
assert.equal(INTERVAL_MS['15m'], 15 * INTERVAL_MS['1m']);
|
||||
assert.ok(Object.keys(INTERVAL_MS).includes('1M'));
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeFundingHistory, normalizePredictedFundings } from '../lib/funding.js';
|
||||
|
||||
// Shape captured live from fundingHistory.
|
||||
const HISTORY = [
|
||||
{ coin: 'BTC', fundingRate: '0.0000125', premium: '-0.0003305881', time: 1780808400000 },
|
||||
{ coin: 'BTC', fundingRate: '0.0000125', premium: '-0.0003786101', time: 1780812000055 },
|
||||
];
|
||||
|
||||
test('normalizeFundingHistory — rate %, APR, premium %', () => {
|
||||
const rows = normalizeFundingHistory(HISTORY);
|
||||
assert.equal(rows.length, 2);
|
||||
const r = rows[0];
|
||||
assert.equal(r.coin, 'BTC');
|
||||
assert.equal(r.fundingRatePct.toFixed(5), '0.00125'); // 0.0000125 * 100
|
||||
assert.equal(r.fundingAprPct.toFixed(4), '10.9500'); // 0.0000125 * 24 * 365 * 100
|
||||
assert.equal(r.premiumPct.toFixed(6), '-0.033059');
|
||||
assert.ok(r.time.startsWith('2026-'));
|
||||
});
|
||||
|
||||
// Shape captured live from predictedFundings.
|
||||
const PREDICTED = [
|
||||
['0G', [
|
||||
['BinPerp', { fundingRate: '-0.00004554', nextFundingTime: 1780819200000, fundingIntervalHours: 4 }],
|
||||
['HlPerp', { fundingRate: '-0.0000397323', nextFundingTime: 1780812000000, fundingIntervalHours: 1 }],
|
||||
['BybitPerp', { fundingRate: '0.00005', nextFundingTime: 1780819200000, fundingIntervalHours: 4 }],
|
||||
]],
|
||||
['BTC', [
|
||||
['HlPerp', { fundingRate: '0.0000125', nextFundingTime: 1780812000000, fundingIntervalHours: 1 }],
|
||||
]],
|
||||
];
|
||||
|
||||
test('normalizePredictedFundings — pivots to per-coin APR + spreads', () => {
|
||||
const rows = normalizePredictedFundings(PREDICTED);
|
||||
const og = rows.find((r) => r.coin === '0G');
|
||||
// HL hourly: -0.0000397323 * 24 * 365 * 100
|
||||
assert.equal(og.hlAprPct.toFixed(2), (-0.0000397323 * 24 * 365 * 100).toFixed(2));
|
||||
// Binance 4h: -0.00004554 / 4 * 24 * 365 * 100
|
||||
assert.equal(og.binanceAprPct.toFixed(2), (-0.00004554 / 4 * 24 * 365 * 100).toFixed(2));
|
||||
assert.equal(og.bybitAprPct.toFixed(2), (0.00005 / 4 * 24 * 365 * 100).toFixed(2));
|
||||
// Spread = HL APR − Binance APR
|
||||
assert.equal(og.hlVsBinancePct.toFixed(4), (og.hlAprPct - og.binanceAprPct).toFixed(4));
|
||||
assert.ok(og.nextHlFunding.startsWith('2026-'));
|
||||
});
|
||||
|
||||
test('normalizePredictedFundings — missing venues yield null legs/spreads', () => {
|
||||
const rows = normalizePredictedFundings(PREDICTED);
|
||||
const btc = rows.find((r) => r.coin === 'BTC');
|
||||
assert.ok(btc.hlAprPct != null);
|
||||
assert.equal(btc.binanceAprPct, null);
|
||||
assert.equal(btc.bybitAprPct, null);
|
||||
assert.equal(btc.hlVsBinancePct, null);
|
||||
assert.equal(btc.hlVsBybitPct, null);
|
||||
});
|
||||
|
||||
test('normalizePredictedFundings — coin filter (exact, case-insensitive)', () => {
|
||||
const rows = normalizePredictedFundings(PREDICTED, 'btc');
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].coin, 'BTC');
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizePerpMarkets, normalizeSpotMarkets, resolveMids } from '../lib/markets.js';
|
||||
|
||||
// Shapes captured live from metaAndAssetCtxs.
|
||||
const PERP_META = {
|
||||
universe: [
|
||||
{ name: 'BTC', maxLeverage: 40, szDecimals: 5 },
|
||||
{ name: 'MATIC', maxLeverage: 20, isDelisted: true },
|
||||
],
|
||||
};
|
||||
const PERP_CTXS = [
|
||||
{
|
||||
funding: '0.0000120074', openInterest: '33163.84416', prevDayPx: '61001.0',
|
||||
dayNtlVlm: '2384788662.6', premium: '-0.0004852641', oraclePx: '61822.0',
|
||||
markPx: '61791.0', midPx: '61791.5',
|
||||
},
|
||||
{
|
||||
funding: '0.000001', openInterest: '0.0', prevDayPx: '0.5',
|
||||
dayNtlVlm: '0.0', premium: '0.0', oraclePx: '0.5', markPx: '0.5', midPx: '0.5',
|
||||
},
|
||||
];
|
||||
|
||||
test('normalizePerpMarkets — zips universe with ctxs by index', () => {
|
||||
const rows = normalizePerpMarkets(PERP_META, PERP_CTXS);
|
||||
assert.equal(rows.length, 2);
|
||||
const btc = rows[0];
|
||||
assert.equal(btc.coin, 'BTC');
|
||||
assert.equal(btc.markPx, 61791);
|
||||
assert.equal(btc.midPx, 61791.5);
|
||||
assert.equal(btc.oraclePx, 61822);
|
||||
assert.equal(btc.maxLeverage, 40);
|
||||
assert.equal(btc.delisted, false);
|
||||
// change24h = (61791 - 61001) / 61001 * 100
|
||||
assert.equal(btc.change24hPct.toFixed(4), '1.2951');
|
||||
// funding hourly % and APR
|
||||
assert.equal(btc.fundingHrPct.toFixed(6), '0.001201');
|
||||
assert.equal(btc.fundingAprPct.toFixed(2), '10.52');
|
||||
// oi notional = 33163.84416 * 61791
|
||||
assert.equal(Math.round(btc.oiNotional), Math.round(33163.84416 * 61791));
|
||||
// premium %
|
||||
assert.equal(btc.premiumPct.toFixed(6), '-0.048526');
|
||||
});
|
||||
|
||||
test('normalizePerpMarkets — flags delisted', () => {
|
||||
const rows = normalizePerpMarkets(PERP_META, PERP_CTXS);
|
||||
assert.equal(rows[1].coin, 'MATIC');
|
||||
assert.equal(rows[1].delisted, true);
|
||||
});
|
||||
|
||||
// Shapes captured live from spotMetaAndAssetCtxs.
|
||||
const SPOT_META = {
|
||||
tokens: [
|
||||
{ name: 'USDC', index: 0 },
|
||||
{ name: 'PURR', index: 1 },
|
||||
{ name: 'HFUN', index: 2 },
|
||||
],
|
||||
universe: [
|
||||
{ tokens: [1, 0], name: 'PURR/USDC', index: 0, isCanonical: true },
|
||||
{ tokens: [2, 0], name: '@1', index: 1, isCanonical: false },
|
||||
],
|
||||
};
|
||||
const SPOT_CTXS = [
|
||||
{
|
||||
prevDayPx: '0.089779', dayNtlVlm: '931726.7', markPx: '0.090247',
|
||||
midPx: '0.0901115', circulatingSupply: '595295651.9', coin: 'PURR/USDC',
|
||||
},
|
||||
{
|
||||
prevDayPx: '1.0', dayNtlVlm: '10.0', markPx: '2.0',
|
||||
midPx: '2.0', circulatingSupply: '100.0', coin: '@1',
|
||||
},
|
||||
];
|
||||
|
||||
test('normalizeSpotMarkets — resolves base token + market cap', () => {
|
||||
const rows = normalizeSpotMarkets(SPOT_META, SPOT_CTXS);
|
||||
assert.equal(rows[0].pair, 'PURR/USDC');
|
||||
assert.equal(rows[0].base, 'PURR');
|
||||
assert.equal(rows[0].markPx, 0.090247);
|
||||
assert.equal(rows[0].canonical, true);
|
||||
// market cap = markPx * circulatingSupply
|
||||
assert.equal(Math.round(rows[0].marketCap), Math.round(0.090247 * 595295651.9));
|
||||
// change24h = (0.090247 - 0.089779) / 0.089779 * 100
|
||||
assert.equal(rows[0].change24hPct.toFixed(4), '0.5213');
|
||||
// second pair is non-canonical; base token index 2 resolves to HFUN
|
||||
assert.equal(rows[1].canonical, false);
|
||||
assert.equal(rows[1].base, 'HFUN');
|
||||
});
|
||||
|
||||
test('resolveMids — perp keys pass through, @index resolves to BASE/QUOTE', () => {
|
||||
const allMids = { BTC: '61791.5', '@1': '2.0', '#1000': '0.5' };
|
||||
const rows = resolveMids(allMids, SPOT_META);
|
||||
const byCoin = Object.fromEntries(rows.map((r) => [r.coin, r.mid]));
|
||||
assert.equal(byCoin.BTC, 61791.5);
|
||||
assert.equal(byCoin['HFUN/USDC'], 2.0); // @1 (tokens [2,0]) → HFUN/USDC
|
||||
assert.equal(byCoin['#1000'], 0.5); // builder perp-dex key passes through
|
||||
});
|
||||
|
||||
test('resolveMids — case-insensitive substring filter', () => {
|
||||
const allMids = { BTC: '1', ETH: '2', BTCDOM: '3' };
|
||||
const rows = resolveMids(allMids, SPOT_META, 'btc');
|
||||
const coins = rows.map((r) => r.coin).sort();
|
||||
assert.deepEqual(coins, ['BTC', 'BTCDOM']);
|
||||
});
|
||||
@@ -16,6 +16,11 @@
|
||||
"data-provider",
|
||||
"tradingview",
|
||||
"options",
|
||||
"opencli"
|
||||
"opencli",
|
||||
"hyperliquid",
|
||||
"perps",
|
||||
"funding",
|
||||
"crypto",
|
||||
"dex"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# hyperliquid-reader
|
||||
|
||||
Read-only Hyperliquid market-data reader via [opencli](https://github.com/jackwener/opencli) + the [`hyperliquid`](../../../../opencli-plugins/hyperliquid/) opencli plugin shipped alongside this skill.
|
||||
|
||||
## What it does
|
||||
|
||||
Reads [Hyperliquid](https://app.hyperliquid.xyz)'s **public info API** — no API key, no wallet, no login, no scraping. Capabilities:
|
||||
|
||||
- **Perp markets** — mark/oracle/mid price, 24h change, hourly funding + annualized APR, open interest (coins + notional), and 24h volume for every perpetual
|
||||
- **Spot markets** — pair price, 24h change, volume, circulating supply, market cap
|
||||
- **Mids** — current mid price for every perp + spot market in one call
|
||||
- **Order book** — L2 snapshot, top N levels per side, with spread
|
||||
- **Candles** — OHLCV history for any interval (1m … 1M)
|
||||
- **Funding history** — historical hourly funding (rate, APR, premium) per coin
|
||||
- **Funding compare** — cross-venue predicted funding (Hyperliquid vs Binance vs Bybit), annualized, with spreads — a funding-arbitrage screen
|
||||
|
||||
**This skill is read-only and market-data only.** It does NOT read individual accounts, place/modify/cancel orders, or move funds.
|
||||
|
||||
## Authentication
|
||||
|
||||
None. Hyperliquid's `info` market-data endpoints are fully public — nothing to authenticate.
|
||||
|
||||
## Triggers
|
||||
|
||||
- "Hyperliquid funding for X", "what's the funding on BTC perp", "HL open interest"
|
||||
- "Hyperliquid order book for ETH", "HL perp markets", "Hyperliquid spot markets"
|
||||
- "funding arb Hyperliquid vs Binance", "Hyperliquid candles for SOL"
|
||||
- "PURR price on Hyperliquid", "Hyperliquid mid prices"
|
||||
- Any mention of Hyperliquid / app.hyperliquid.xyz / HL DEX in context of reading market data or funding
|
||||
|
||||
## Platform
|
||||
|
||||
Works on **Claude Code** and other CLI-based agents (any OS with Node ≥ 22). Does **not** work on Claude.ai — the sandbox restricts the network access opencli needs. Unlike the TradingView reader, there is no desktop-app or macOS dependency — it's a plain HTTP API.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# As a plugin (recommended — installs all skills in this group)
|
||||
npx plugins add himself65/finance-skills --plugin finance-data-providers
|
||||
|
||||
# Or install just this skill
|
||||
npx skills add himself65/finance-skills --skill hyperliquid-reader
|
||||
```
|
||||
|
||||
See the [main README](../../../../README.md) for more installation options.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js >= 22 — for `npm install -g @jackwener/opencli` and the plugin's built-in `fetch`
|
||||
- The `hyperliquid` opencli plugin: `opencli plugin install github:himself65/finance-skills/hyperliquid` (installs from this repo's monorepo subpath)
|
||||
|
||||
No API key, no wallet, no launch step.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/commands.md` — Complete market-data command reference with all flags, output schemas, and analyst workflows
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: hyperliquid-reader
|
||||
description: >
|
||||
Read Hyperliquid (app.hyperliquid.xyz) perp + spot market data via
|
||||
opencli (read-only, public info API). Use whenever the user wants
|
||||
Hyperliquid perpetual or spot markets, mark/oracle/mid prices, 24h
|
||||
change, funding rates (hourly or annualized APR), open interest, volume,
|
||||
the L2 order book, OHLCV candles, historical funding, or a cross-venue
|
||||
funding comparison (Hyperliquid vs Binance vs Bybit) for funding
|
||||
arbitrage. Triggers: "Hyperliquid funding for BTC", "HL perp markets",
|
||||
"funding on BTC perp", "Hyperliquid order book", "HL open interest",
|
||||
"funding arb Hyperliquid vs Binance", "Hyperliquid candles for SOL",
|
||||
"Hyperliquid spot markets", "PURR price on Hyperliquid", "hyperliquid",
|
||||
"hyperliquid.xyz", "HL DEX". READ-ONLY market data — no account, order,
|
||||
or trade operations.
|
||||
---
|
||||
|
||||
# Hyperliquid Reader (Read-Only)
|
||||
|
||||
Reads [Hyperliquid](https://app.hyperliquid.xyz) — the on-chain perps/spot DEX — for market data via [opencli](https://github.com/jackwener/opencli) and the `hyperliquid` plugin in this repo's [`opencli-plugins/hyperliquid`](https://github.com/himself65/finance-skills/tree/main/opencli-plugins/hyperliquid) tree (a separate plugin from opencli's built-in adapters, installed via opencli's monorepo subpath syntax).
|
||||
|
||||
**This skill is read-only and market-data only.** It reads Hyperliquid's fully public info API for analysis: market tables, funding, order book, and candles. It does NOT read individual accounts, place/modify/cancel orders, or move funds. There is no trading path in the plugin — order placement requires wallet-signed actions on a separate endpoint this adapter never calls.
|
||||
|
||||
**How it works**: every command issues a single `POST https://api.hyperliquid.xyz/info` with a `{ "type": "..." }` body and normalizes the response. **No API key, no wallet, no login, no running app** — the info API is public.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Ensure opencli + Plugin Are Installed and Ready
|
||||
|
||||
**Current environment status:**
|
||||
|
||||
```
|
||||
!`(command -v opencli && opencli hyperliquid markets --coin BTC -f json 2>&1 | head -3 && echo "READY" || echo "SETUP_NEEDED") 2>/dev/null || echo "NOT_INSTALLED"`
|
||||
```
|
||||
|
||||
If the status above shows `READY`, skip to Step 2. Otherwise:
|
||||
|
||||
### NOT_INSTALLED — Install opencli
|
||||
|
||||
```bash
|
||||
npm install -g @jackwener/opencli
|
||||
```
|
||||
|
||||
Requires Node.js >= 22 (built-in `fetch`).
|
||||
|
||||
### SETUP_NEEDED — Install the Hyperliquid plugin
|
||||
|
||||
The Hyperliquid adapter is **not** built into opencli — it's a separate plugin:
|
||||
|
||||
```bash
|
||||
opencli plugin install github:himself65/finance-skills/hyperliquid
|
||||
```
|
||||
|
||||
That's the entire setup — no auth, no launch step. Verify with `opencli hyperliquid markets --coin BTC`.
|
||||
|
||||
### Common setup issues
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| `opencli: command not found` | `npm install -g @jackwener/opencli` (Node ≥ 22) |
|
||||
| `Unknown command: hyperliquid` | `opencli plugin install github:himself65/finance-skills/hyperliquid` |
|
||||
| `hyperliquid info 429` | Rate limited — wait a few seconds and retry |
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Identify What the User Needs
|
||||
|
||||
| User Request | Command | Key Flags |
|
||||
|---|---|---|
|
||||
| Perp markets overview / top by volume | `opencli hyperliquid markets` | `--sort`, `--limit`, `--coin` |
|
||||
| One perp's price + funding + OI | `opencli hyperliquid markets --coin BTC` | — |
|
||||
| Spot pairs overview | `opencli hyperliquid spot-markets` | `--sort`, `--limit`, `--pair`, `--canonical-only` |
|
||||
| All current mid prices | `opencli hyperliquid mids` | `--coin <substring>` |
|
||||
| Order book for a coin | `opencli hyperliquid book --coin ETH` | `--depth`, `--n-sig-figs` |
|
||||
| OHLCV candles | `opencli hyperliquid candles --coin BTC --interval 1h` | `--limit` |
|
||||
| Historical funding for a coin | `opencli hyperliquid funding-history --coin BTC` | `--hours`, `--limit` |
|
||||
| Funding arb: HL vs Binance vs Bybit | `opencli hyperliquid funding-compare` | `--coin`, `--sort`, `--limit` |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Execute the Command
|
||||
|
||||
### General pattern
|
||||
|
||||
```bash
|
||||
# Use -f json or -f yaml for structured output
|
||||
opencli hyperliquid markets --sort fundingAprPct --limit 15 -f json
|
||||
opencli hyperliquid funding-compare --sort hlVsBinancePct --limit 20 -f md
|
||||
opencli hyperliquid candles --coin BTC --interval 4h --limit 50 -f csv
|
||||
opencli hyperliquid book --coin ETH --depth 5 -f json
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
1. **Coin symbols are bare perp names** — `BTC`, `ETH`, `SOL`, `HYPE` (no exchange prefix). Spot pairs are `BASE/USDC` (e.g. `PURR/USDC`); for `book`/`candles` you can pass either a perp coin or a spot pair.
|
||||
2. **`markets` is the default lens for "how is X / the market doing"** — it carries mark/oracle/mid price, 24h change, hourly funding + APR, open interest (coins and notional), and 24h volume in one row per perp. Filter with `--coin` for a single asset.
|
||||
3. **Funding is reported two ways** — `fundingHrPct` is the raw hourly rate as a percent; `fundingAprPct` annualizes it (`hourly × 24 × 365`). Lead with APR when comparing carry across assets; use the hourly figure for "what will I pay next hour".
|
||||
4. **`funding-compare` is the funding-arb screen** — it annualizes each venue with its own interval (HL hourly, Binance/Bybit usually 4h) and reports `hlVsBinancePct` / `hlVsBybitPct` spreads. Default sort ranks by **absolute** HL-vs-Binance spread (widest dislocations first). A positive `hlVsBinancePct` means HL longs pay more than Binance longs.
|
||||
5. **`book` defaults to 10 levels per side** — raise `--depth` (max 20) for more, or `--n-sig-figs 2..5` to aggregate price levels. Compute the spread/mid from the top bid and ask.
|
||||
6. **`candles` pulls the most recent `--limit` candles** of `--interval` (default `1h`, 100 candles). Valid intervals: `1m 3m 5m 15m 30m 1h 2h 4h 8h 12h 1d 3d 1w 1M`. Max 5000.
|
||||
7. **`-f json`** for programmatic processing / feeding other skills; `-f md` or `-f table` for human-readable output.
|
||||
8. **NEVER call any write operation.** This skill is read-only market data — no account reads, no order placement, modification, or cancellation, and no transfers. The plugin intentionally exposes no write endpoints.
|
||||
|
||||
### Output format flag (`-f`)
|
||||
|
||||
| Format | Flag | Best for |
|
||||
|---|---|---|
|
||||
| Table | `-f table` (default) | Human-readable terminal output |
|
||||
| JSON | `-f json` | Programmatic processing, LLM context |
|
||||
| YAML | `-f yaml` | Structured, readable |
|
||||
| Markdown | `-f md` | Reports |
|
||||
| CSV | `-f csv` | Spreadsheet export |
|
||||
|
||||
### Output columns
|
||||
|
||||
- `markets` — `coin`, `markPx`, `midPx`, `oraclePx`, `change24hPct`, `fundingHrPct`, `fundingAprPct`, `openInterest`, `oiNotional`, `dayNtlVlm`, `premiumPct`, `maxLeverage`
|
||||
- `spot-markets` — `pair`, `base`, `markPx`, `midPx`, `change24hPct`, `dayNtlVlm`, `circulatingSupply`, `marketCap`, `canonical`
|
||||
- `mids` — `coin`, `mid`
|
||||
- `book` — `side`, `level`, `px`, `sz`, `orders`
|
||||
- `candles` — `time`, `open`, `high`, `low`, `close`, `volume`, `trades`
|
||||
- `funding-history` — `coin`, `fundingRatePct`, `fundingAprPct`, `premiumPct`, `time`
|
||||
- `funding-compare` — `coin`, `hlAprPct`, `binanceAprPct`, `bybitAprPct`, `hlVsBinancePct`, `hlVsBybitPct`, `nextHlFunding`
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Present the Results
|
||||
|
||||
1. **Lead with the headline number, then the table.** For `markets --coin BTC`: state mark price, 24h change, funding APR, and open interest in prose first. For a full `markets` dump: lead with the count and the top movers / highest-funding names.
|
||||
2. **Frame funding in carry terms** — e.g. "BTC perp funding is +10.9% APR (longs pay shorts)". Positive funding ⇒ longs pay shorts; negative ⇒ shorts pay longs.
|
||||
3. **For `funding-compare`, surface the widest dislocations first** — name the coin, both venues' APRs, and the spread, and remember the spread is annualized; a real arb also pays exchange/withdrawal frictions, so present it as a screen, not a guaranteed edge.
|
||||
4. **For `book`, report the spread** — best bid, best ask, mid, and spread in bps before (or instead of) dumping every level. Don't paste 20 levels unless asked.
|
||||
5. **For `candles`, describe the move** — first/last close, high/low, and direction; only show the full OHLCV table when the user wants the series.
|
||||
6. **Filter aggressively before showing** — `markets` has ~180 perps and `mids` ~700 markets; cap to top 15-20 by the relevant sort unless the user asks for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Diagnostics
|
||||
|
||||
```bash
|
||||
opencli hyperliquid markets --coin BTC
|
||||
```
|
||||
|
||||
A successful BTC row confirms opencli, the plugin, and the public API are all reachable. If it errors with `Unknown command: hyperliquid`, reinstall the plugin (Step 1). A `hyperliquid info 4xx/5xx` is an upstream API issue — retry after a short wait.
|
||||
|
||||
---
|
||||
|
||||
## Error Reference
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `Unknown command: hyperliquid` | Plugin not installed | `opencli plugin install github:himself65/finance-skills/hyperliquid` |
|
||||
| `hyperliquid info 429` | Rate limited | Wait a few seconds, then retry |
|
||||
| `hyperliquid info 422/500` | Malformed body or upstream issue | Re-check the coin/interval; retry after a wait |
|
||||
| `No perp market for coin "X"` | Wrong/unlisted symbol | Run `opencli hyperliquid markets` (or `mids`) to find the exact symbol |
|
||||
|
||||
---
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `references/commands.md` — Every command with all flags, output schemas, and analyst workflows (funding carry, basis/arb, spot snapshot)
|
||||
@@ -0,0 +1,162 @@
|
||||
# Hyperliquid Reader — Command Reference
|
||||
|
||||
Every command issues a single `POST https://api.hyperliquid.xyz/info` and prints normalized rows. All are **read-only market data**, need no auth, and accept `-f json|yaml|md|csv|table`.
|
||||
|
||||
> Symbols: perps are bare names (`BTC`, `ETH`, `HYPE`); spot pairs are `BASE/USDC` (`PURR/USDC`). `book` and `candles` accept either.
|
||||
|
||||
---
|
||||
|
||||
## Market data
|
||||
|
||||
### `markets` — perpetual markets table
|
||||
|
||||
`metaAndAssetCtxs` → one row per perp.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (all) | Filter to one coin (exact, case-insensitive) |
|
||||
| `--sort` | `dayNtlVlm` | One of `dayNtlVlm`, `change24hPct`, `fundingAprPct`, `fundingHrPct`, `openInterest`, `oiNotional`, `markPx`, `coin` (desc; `coin` asc). `null` sorts last |
|
||||
| `--limit` | (all) | Max rows after sort |
|
||||
| `--include-delisted` | `false` | Include delisted markets |
|
||||
|
||||
Columns: `coin`, `markPx`, `midPx`, `oraclePx`, `change24hPct`, `fundingHrPct`, `fundingAprPct`, `openInterest`, `oiNotional`, `dayNtlVlm`, `premiumPct`, `maxLeverage`.
|
||||
|
||||
- `fundingHrPct` — raw hourly funding rate as a percent.
|
||||
- `fundingAprPct` — `hourly × 24 × 365` (annualized).
|
||||
- `openInterest` — in coins; `oiNotional` — `openInterest × markPx` (USD).
|
||||
- `premiumPct` — perp premium/discount vs oracle (mark-implied).
|
||||
|
||||
```bash
|
||||
opencli hyperliquid markets --sort dayNtlVlm --limit 15
|
||||
opencli hyperliquid markets --coin HYPE -f json
|
||||
opencli hyperliquid markets --sort fundingAprPct --limit 20 # highest carry
|
||||
```
|
||||
|
||||
### `spot-markets` — spot pairs table
|
||||
|
||||
`spotMetaAndAssetCtxs` → one row per spot pair.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--pair` | (all) | Filter by pair or base token (e.g. `PURR` or `PURR/USDC`) |
|
||||
| `--sort` | `dayNtlVlm` | One of `dayNtlVlm`, `change24hPct`, `marketCap`, `markPx`, `pair` |
|
||||
| `--limit` | (all) | Max rows after sort |
|
||||
| `--canonical-only` | `false` | Only named pairs (hide `@index` pairs) |
|
||||
|
||||
Columns: `pair`, `base`, `markPx`, `midPx`, `change24hPct`, `dayNtlVlm`, `circulatingSupply`, `marketCap`, `canonical`.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid spot-markets --canonical-only --sort dayNtlVlm --limit 20
|
||||
opencli hyperliquid spot-markets --pair PURR -f json
|
||||
```
|
||||
|
||||
### `mids` — all mid prices
|
||||
|
||||
`allMids` (+ `spotMeta` to resolve names) → `coin`, `mid` for every market.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (all) | Case-insensitive **substring** filter |
|
||||
|
||||
Non-canonical spot keys (`@<index>`) resolve to `BASE/QUOTE`; perp names and canonical pairs pass through; builder perp-dex keys (`#<n>`) are shown as-is.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid mids --coin BTC # BTC, plus any pair containing "BTC"
|
||||
opencli hyperliquid mids -f json | jq '.[] | select(.coin=="ETH")'
|
||||
```
|
||||
|
||||
### `book` — L2 order book snapshot
|
||||
|
||||
`l2Book` → up to `--depth` levels per side, bids first then asks.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (required) | Coin or spot pair |
|
||||
| `--depth` | `10` | Levels per side (1-20) |
|
||||
| `--n-sig-figs` | (full) | Price aggregation, 2-5 |
|
||||
|
||||
Columns: `side` (`bid`/`ask`), `level`, `px`, `sz`, `orders`.
|
||||
|
||||
Spread = best ask − best bid; mid = their average. Top-of-book is `level: 1` on each side.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid book --coin ETH --depth 5
|
||||
opencli hyperliquid book --coin BTC --n-sig-figs 3 -f json
|
||||
```
|
||||
|
||||
### `candles` — OHLCV history
|
||||
|
||||
`candleSnapshot` → most recent `--limit` candles of `--interval`.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (required) | Coin or spot pair |
|
||||
| `--interval` | `1h` | `1m 3m 5m 15m 30m 1h 2h 4h 8h 12h 1d 3d 1w 1M` |
|
||||
| `--limit` | `100` | Number of candles (max 5000) |
|
||||
|
||||
Columns: `time` (ISO, candle open), `open`, `high`, `low`, `close`, `volume`, `trades`.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid candles --coin BTC --interval 4h --limit 60
|
||||
opencli hyperliquid candles --coin SOL --interval 1d --limit 30 -f csv
|
||||
```
|
||||
|
||||
### `funding-history` — historical funding for a coin
|
||||
|
||||
`fundingHistory` → hourly prints within the lookback window, newest first.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (required) | Coin (e.g. `BTC`) |
|
||||
| `--hours` | `24` | Lookback window in hours |
|
||||
| `--limit` | (all) | Cap to most recent N |
|
||||
|
||||
Columns: `coin`, `fundingRatePct`, `fundingAprPct`, `premiumPct`, `time`.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid funding-history --coin BTC --hours 72
|
||||
opencli hyperliquid funding-history --coin ETH --hours 168 --limit 24 -f json
|
||||
```
|
||||
|
||||
### `funding-compare` — cross-venue funding (arb screen)
|
||||
|
||||
`predictedFundings` → per coin, each venue's predicted funding annualized to APR, plus HL-vs-venue spreads.
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--coin` | (all) | Filter to one coin (exact) |
|
||||
| `--sort` | `hlVsBinancePct` | `hlVsBinancePct`, `hlVsBybitPct` (by absolute spread), or `hlAprPct`, `binanceAprPct`, `bybitAprPct`, `coin` (signed) |
|
||||
| `--limit` | (all) | Max rows after sort |
|
||||
|
||||
Columns: `coin`, `hlAprPct`, `binanceAprPct`, `bybitAprPct`, `hlVsBinancePct`, `hlVsBybitPct`, `nextHlFunding`.
|
||||
|
||||
Each venue is annualized with its own interval (HL hourly, Binance/Bybit usually 4h). `hlVsBinancePct = hlAprPct − binanceAprPct`; positive ⇒ HL longs pay more. Default sort surfaces the widest dislocations first. Treat as a screen — a real arb also pays exchange and transfer frictions.
|
||||
|
||||
```bash
|
||||
opencli hyperliquid funding-compare --limit 20 # widest HL/Binance gaps
|
||||
opencli hyperliquid funding-compare --coin BTC -f json
|
||||
opencli hyperliquid funding-compare --sort hlAprPct --limit 15 # highest HL carry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Analyst workflows
|
||||
|
||||
### Funding carry scan
|
||||
1. `markets --sort fundingAprPct --limit 20` — highest (and, reversed mentally, lowest) annualized funding.
|
||||
2. `funding-history --coin <X> --hours 168` — confirm the rate is persistent, not a one-hour spike.
|
||||
3. `markets --coin <X>` — check open interest and premium to size whether the carry is tradeable.
|
||||
|
||||
### Cross-venue funding arbitrage
|
||||
1. `funding-compare --limit 25` — widest HL-vs-Binance/Bybit dislocations.
|
||||
2. For a candidate `<X>`: `funding-compare --coin <X>` for all three venues' APRs and the next HL funding time.
|
||||
3. `book --coin <X>` and `markets --coin <X>` — verify depth and OI can support the size before treating the spread as real (it's annualized and ignores frictions).
|
||||
|
||||
### Basis / premium check
|
||||
1. `markets --coin <X>` — `premiumPct` shows perp rich/cheap vs oracle; `markPx` vs `oraclePx` is the absolute basis.
|
||||
2. `candles --coin <X> --interval 1h` — recent price action around the basis.
|
||||
|
||||
### Spot token snapshot
|
||||
1. `spot-markets --canonical-only --sort dayNtlVlm` — most active named pairs.
|
||||
2. `spot-markets --pair <BASE>` — price, 24h change, market cap for one token.
|
||||
3. `book --coin <BASE>/USDC` — liquidity at top of book.
|
||||
Reference in New Issue
Block a user