perp-cli 0.4.12 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,47 +8,55 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
8
8
  market
9
9
  .command("list")
10
10
  .description("List all available markets")
11
- .action(async () => {
11
+ .option("--hip3", "Include HIP-3 dex markets (Hyperliquid)")
12
+ .action(async (opts) => {
12
13
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
14
+ const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"];
15
+ const marketRow = (m) => [
16
+ chalk.white.bold(m.symbol),
17
+ `$${formatUsd(m.markPrice)}`,
18
+ `$${formatUsd(m.indexPrice)}`,
19
+ formatPercent(m.fundingRate),
20
+ `$${formatUsd(m.volume24h)}`,
21
+ `$${formatUsd(m.openInterest)}`,
22
+ String(m.maxLeverage) + "x",
23
+ ];
13
24
  // Multi-exchange mode
14
25
  if (!explicitExchange && getAdapterForExchange) {
15
- const allRows = [];
26
+ const grouped = {};
16
27
  const errors = {};
17
28
  await Promise.all(EXCHANGES.map(async (ex) => {
18
29
  try {
19
30
  const adapter = await getAdapterForExchange(ex);
20
- const markets = await adapter.getMarkets();
21
- for (const m of markets) {
22
- allRows.push([
23
- chalk.white.bold(m.symbol),
24
- chalk.gray(ex.slice(0, 3).toUpperCase()),
25
- `$${formatUsd(m.markPrice)}`,
26
- `$${formatUsd(m.indexPrice)}`,
27
- formatPercent(m.fundingRate),
28
- `$${formatUsd(m.volume24h)}`,
29
- `$${formatUsd(m.openInterest)}`,
30
- String(m.maxLeverage) + "x",
31
- ]);
31
+ grouped[ex] = await adapter.getMarkets();
32
+ // --hip3: fetch HIP-3 dex markets
33
+ if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
34
+ const dexes = await adapter.listDeployedDexes();
35
+ await Promise.allSettled(dexes.map(async (d) => {
36
+ const dexAdapter = Object.create(adapter);
37
+ dexAdapter.setDex(d.name);
38
+ const markets = await dexAdapter.getMarkets();
39
+ if (markets.length > 0)
40
+ grouped[`hip3:${d.name}`] = markets;
41
+ }));
32
42
  }
33
43
  }
34
44
  catch (err) {
35
45
  errors[ex] = err instanceof Error ? err.message : String(err);
36
46
  }
37
47
  }));
38
- // Sort by symbol name
39
- allRows.sort((a, b) => a[0].localeCompare(b[0]));
40
48
  if (isJson()) {
41
- const grouped = {};
42
- await Promise.all(EXCHANGES.map(async (ex) => {
43
- try {
44
- const adapter = await getAdapterForExchange(ex);
45
- grouped[ex] = await adapter.getMarkets();
46
- }
47
- catch { /* skip */ }
48
- }));
49
49
  return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
50
50
  }
51
- console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], allRows));
51
+ // Per-exchange tables
52
+ for (const [ex, markets] of Object.entries(grouped)) {
53
+ if (markets.length === 0)
54
+ continue;
55
+ const isHip3 = ex.startsWith("hip3:");
56
+ const label = isHip3 ? chalk.magenta.bold(` HIP-3: ${ex.slice(5)}`) : chalk.cyan.bold(` ${ex.toUpperCase()}`);
57
+ console.log(`\n${label} ${chalk.gray(`(${markets.length} markets)`)}`);
58
+ console.log(makeTable(HEADERS, markets.map(marketRow)));
59
+ }
52
60
  for (const [ex, msg] of Object.entries(errors)) {
53
61
  console.log(chalk.gray(` ${ex}: ${msg}`));
54
62
  }
@@ -57,58 +65,63 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
57
65
  // Single exchange mode
58
66
  const adapter = await getAdapter();
59
67
  const markets = await adapter.getMarkets();
60
- if (isJson())
68
+ // --hip3: fetch HIP-3 dex markets
69
+ const hip3 = {};
70
+ if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
71
+ const dexes = await adapter.listDeployedDexes();
72
+ await Promise.allSettled(dexes.map(async (d) => {
73
+ const dexAdapter = Object.create(adapter);
74
+ dexAdapter.setDex(d.name);
75
+ const m = await dexAdapter.getMarkets();
76
+ if (m.length > 0)
77
+ hip3[d.name] = m;
78
+ }));
79
+ }
80
+ if (isJson()) {
81
+ if (Object.keys(hip3).length > 0) {
82
+ return printJson(jsonOk({ main: markets, hip3 }));
83
+ }
61
84
  return printJson(jsonOk(markets));
62
- const rows = markets.map((m) => [
63
- chalk.white.bold(m.symbol),
64
- `$${formatUsd(m.markPrice)}`,
65
- `$${formatUsd(m.indexPrice)}`,
66
- formatPercent(m.fundingRate),
67
- `$${formatUsd(m.volume24h)}`,
68
- `$${formatUsd(m.openInterest)}`,
69
- String(m.maxLeverage) + "x",
70
- ]);
71
- console.log(makeTable(["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], rows));
85
+ }
86
+ console.log(makeTable(HEADERS, markets.map(marketRow)));
87
+ for (const [dex, m] of Object.entries(hip3)) {
88
+ console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`) + chalk.gray(` (${m.length} markets)`));
89
+ console.log(makeTable(HEADERS, m.map(marketRow)));
90
+ }
72
91
  });
73
92
  market
74
93
  .command("prices")
75
94
  .description("Get current prices for all markets")
76
95
  .action(async () => {
77
96
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
97
+ const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
98
+ const priceRow = (m) => [
99
+ chalk.white.bold(m.symbol),
100
+ `$${formatUsd(m.markPrice)}`,
101
+ `$${formatUsd(m.indexPrice)}`,
102
+ formatPercent(m.fundingRate),
103
+ ];
78
104
  if (!explicitExchange && getAdapterForExchange) {
79
- const allRows = [];
105
+ const grouped = {};
80
106
  const errors = {};
81
107
  await Promise.all(EXCHANGES.map(async (ex) => {
82
108
  try {
83
109
  const adapter = await getAdapterForExchange(ex);
84
- const markets = await adapter.getMarkets();
85
- for (const m of markets) {
86
- allRows.push([
87
- chalk.white.bold(m.symbol),
88
- chalk.gray(ex.slice(0, 3).toUpperCase()),
89
- `$${formatUsd(m.markPrice)}`,
90
- `$${formatUsd(m.indexPrice)}`,
91
- formatPercent(m.fundingRate),
92
- ]);
93
- }
110
+ grouped[ex] = await adapter.getMarkets();
94
111
  }
95
112
  catch (err) {
96
113
  errors[ex] = err instanceof Error ? err.message : String(err);
97
114
  }
98
115
  }));
99
- allRows.sort((a, b) => a[0].localeCompare(b[0]));
100
116
  if (isJson()) {
101
- const grouped = {};
102
- await Promise.all(EXCHANGES.map(async (ex) => {
103
- try {
104
- const adapter = await getAdapterForExchange(ex);
105
- grouped[ex] = await adapter.getMarkets();
106
- }
107
- catch { /* skip */ }
108
- }));
109
117
  return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
110
118
  }
111
- console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding"], allRows));
119
+ for (const [ex, markets] of Object.entries(grouped)) {
120
+ if (markets.length === 0)
121
+ continue;
122
+ console.log(chalk.cyan.bold(`\n ${ex.toUpperCase()}`) + chalk.gray(` (${markets.length} markets)`));
123
+ console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
124
+ }
112
125
  for (const [ex, msg] of Object.entries(errors))
113
126
  console.log(chalk.gray(` ${ex}: ${msg}`));
114
127
  return;
@@ -128,17 +141,10 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
128
141
  console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
129
142
  }
130
143
  else {
131
- // Generic: use getMarkets as price source
132
144
  const markets = await adapter.getMarkets();
133
145
  if (isJson())
134
146
  return printJson(jsonOk(markets));
135
- const rows = markets.map((m) => [
136
- chalk.white.bold(m.symbol),
137
- `$${formatUsd(m.markPrice)}`,
138
- `$${formatUsd(m.indexPrice)}`,
139
- formatPercent(m.fundingRate),
140
- ]);
141
- console.log(makeTable(["Symbol", "Mark", "Index", "Funding"], rows));
147
+ console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
142
148
  }
143
149
  });
144
150
  market
@@ -148,7 +154,11 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
148
154
  const adapter = await getAdapter();
149
155
  const markets = await adapter.getMarkets();
150
156
  const sym = symbol.toUpperCase();
151
- const m = markets.find((mk) => mk.symbol === sym || mk.symbol === `${sym}-PERP` || mk.symbol.replace(/-PERP$/, "") === sym);
157
+ const m = markets.find((mk) => {
158
+ const s = mk.symbol.toUpperCase();
159
+ return s === sym || s === `${sym}-PERP` || s.replace(/-PERP$/, "") === sym
160
+ || s.split(":").pop() === sym; // HIP-3: "km:US500" matches "US500"
161
+ });
152
162
  if (!m) {
153
163
  if (isJson())
154
164
  return printJson(jsonError("SYMBOL_NOT_FOUND", `Market ${sym} not found`));
@@ -178,15 +188,40 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
178
188
  const depth = parseInt(opts.depth);
179
189
  const asks = book.asks.slice(0, depth).reverse();
180
190
  const bids = book.bids.slice(0, depth);
191
+ // Compute spread
192
+ const bestBid = bids[0] ? Number(bids[0][0]) : 0;
193
+ const bestAsk = asks[asks.length - 1] ? Number(asks[asks.length - 1][0]) : 0;
194
+ const midPrice = bestBid && bestAsk ? (bestBid + bestAsk) / 2 : bestBid || bestAsk;
195
+ const spreadAbs = bestBid && bestAsk ? bestAsk - bestBid : 0;
196
+ const spreadPct = midPrice > 0 ? (spreadAbs / midPrice) * 100 : 0;
197
+ // Compute cumulative sizes + max for bar scaling
198
+ let askCum = 0;
199
+ const askCums = asks.map(([, a]) => { askCum += Number(a); return askCum; });
200
+ let bidCum = 0;
201
+ const bidCums = bids.map(([, a]) => { bidCum += Number(a); return bidCum; });
202
+ const maxCum = Math.max(askCums[askCums.length - 1] || 0, bidCums[bidCums.length - 1] || 0);
203
+ const BAR_WIDTH = 20;
204
+ const bar = (cum) => {
205
+ const len = maxCum > 0 ? Math.round((cum / maxCum) * BAR_WIDTH) : 0;
206
+ return "█".repeat(len);
207
+ };
181
208
  console.log(chalk.cyan.bold(`\n Orderbook: ${symbol.toUpperCase()} (${adapter.name})\n`));
182
- console.log(chalk.gray(" Price Size"));
183
- console.log(chalk.gray(" ─────────────────────"));
184
- asks.forEach(([p, a]) => {
185
- console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${a}`));
209
+ console.log(chalk.gray(" Price Size Cum$ Depth"));
210
+ console.log(chalk.gray(" " + "─".repeat(60)));
211
+ asks.forEach(([p, a], i) => {
212
+ const cumUsd = askCums[i] * (Number(p));
213
+ console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
214
+ chalk.red.dim(bar(askCums[i])));
186
215
  });
187
- console.log(chalk.gray(" ─── spread ───"));
188
- bids.forEach(([p, a]) => {
189
- console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${a}`));
216
+ // Spread line
217
+ const spreadStr = spreadAbs > 0
218
+ ? `$${formatUsd(spreadAbs)} (${spreadPct.toFixed(3)}%)`
219
+ : "─";
220
+ console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
221
+ bids.forEach(([p, a], i) => {
222
+ const cumUsd = bidCums[i] * (Number(p));
223
+ console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
224
+ chalk.green.dim(bar(bidCums[i])));
190
225
  });
191
226
  console.log();
192
227
  });
@@ -312,19 +312,33 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
312
312
  });
313
313
  trade
314
314
  .command("cancel <symbolOrOrderId> [orderId]")
315
- .description("Cancel an order by orderId (auto-detects symbol), or by symbol + orderId")
315
+ .description("Cancel order(s) — by orderId, by symbol + orderId, or by symbol name (cancels all orders for that symbol)")
316
316
  .action(async (symbolOrOrderId, orderId) => {
317
317
  const adapter = await getAdapter();
318
- let symbol;
319
- let oid;
320
318
  if (orderId) {
321
319
  // cancel <symbol> <orderId>
322
- symbol = symbolOrOrderId.toUpperCase();
323
- oid = orderId;
320
+ const symbol = symbolOrOrderId.toUpperCase();
321
+ const oid = orderId;
322
+ if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
323
+ return;
324
+ try {
325
+ const result = await adapter.cancelOrder(symbol, oid);
326
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
327
+ if (isJson())
328
+ return printJson(jsonOk(result));
329
+ console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
330
+ }
331
+ catch (err) {
332
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
333
+ throw err;
334
+ }
335
+ return;
324
336
  }
325
- else {
337
+ // Single argument — is it an orderId (numeric) or a symbol name?
338
+ const isNumeric = /^\d+$/.test(symbolOrOrderId);
339
+ if (isNumeric) {
326
340
  // cancel <orderId> — look up symbol from open orders
327
- oid = symbolOrOrderId;
341
+ const oid = symbolOrOrderId;
328
342
  const orders = await adapter.getOpenOrders();
329
343
  const match = orders.find((o) => String(o.orderId) === oid);
330
344
  if (!match) {
@@ -334,20 +348,46 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
334
348
  console.log(chalk.yellow(`\n ${err}\n`));
335
349
  return;
336
350
  }
337
- symbol = match.symbol;
338
- }
339
- if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
340
- return;
341
- try {
342
- const result = await adapter.cancelOrder(symbol, oid);
343
- logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
344
- if (isJson())
345
- return printJson(jsonOk(result));
346
- console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
351
+ const symbol = match.symbol;
352
+ if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
353
+ return;
354
+ try {
355
+ const result = await adapter.cancelOrder(symbol, oid);
356
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
357
+ if (isJson())
358
+ return printJson(jsonOk(result));
359
+ console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
360
+ }
361
+ catch (err) {
362
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
363
+ throw err;
364
+ }
347
365
  }
348
- catch (err) {
349
- logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
350
- throw err;
366
+ else {
367
+ // cancel <symbol> — cancel all open orders for this symbol
368
+ const symbol = symbolOrOrderId.toUpperCase();
369
+ const orders = await adapter.getOpenOrders();
370
+ const matching = orders.filter((o) => symbolMatch(o.symbol, symbol));
371
+ if (matching.length === 0) {
372
+ const err = `No open orders found for ${symbol}`;
373
+ if (isJson())
374
+ return printJson(jsonOk({ cancelled: false, symbol, reason: err }));
375
+ console.log(chalk.yellow(`\n ${err}\n`));
376
+ return;
377
+ }
378
+ if (dryRunGuard("cancel_by_symbol", { exchange: adapter.name, symbol, count: matching.length }))
379
+ return;
380
+ try {
381
+ const result = await adapter.cancelAllOrders(symbol);
382
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { bySymbol: true, count: matching.length } });
383
+ if (isJson())
384
+ return printJson(jsonOk({ cancelled: true, symbol, count: matching.length, result }));
385
+ console.log(chalk.green(`\n ${matching.length} order(s) for ${symbol} cancelled on ${adapter.name}.\n`));
386
+ }
387
+ catch (err) {
388
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { bySymbol: true } });
389
+ throw err;
390
+ }
351
391
  }
352
392
  });
353
393
  trade
@@ -59,6 +59,15 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
59
59
  * Bypasses the SDK entirely — works for order, batchModify, twapOrder, etc.
60
60
  */
61
61
  private ensureSigner;
62
+ /**
63
+ * Normalize trailing zeros from price ('p') and size ('s') string fields
64
+ * in an action object, recursively. Matches the SDK's normalizeTrailingZeros.
65
+ *
66
+ * The HL validator normalizes these fields before hashing; if we hash without
67
+ * normalizing, the recovered signer address won't match → "User or API Wallet
68
+ * does not exist" error.
69
+ */
70
+ private static _normalizeAction;
62
71
  private _signAndSendAction;
63
72
  /**
64
73
  * Place an order via raw exchange API with EIP-712 signing.
@@ -69,8 +78,8 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
69
78
  reduceOnly?: boolean;
70
79
  tif?: string;
71
80
  }): Promise<unknown>;
72
- cancelOrder(symbol: string, orderId: string): Promise<import("hyperliquid").CancelOrderResponse>;
73
- cancelAllOrders(symbol?: string): Promise<import("hyperliquid").CancelOrderResponse[]>;
81
+ cancelOrder(symbol: string, orderId: string): Promise<unknown>;
82
+ cancelAllOrders(symbol?: string): Promise<unknown[]>;
74
83
  editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
75
84
  setLeverage(symbol: string, leverage: number, marginMode?: "cross" | "isolated"): Promise<any>;
76
85
  stopOrder(symbol: string, side: "buy" | "sell", size: string, triggerPrice: string, opts?: {