Files
himself65__finance-skills/opencli-plugins/hyperliquid/tests/book.test.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

56 lines
1.8 KiB
JavaScript

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);
});