perp-cli 0.4.9 → 0.4.11

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.
@@ -101,30 +101,21 @@ function pac(adapter) {
101
101
  }
102
102
  export function registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange) {
103
103
  const account = program.command("account").description("Account commands");
104
- account
105
- .command("balance")
106
- .alias("info")
107
- .description("Account overview: perp balance, spot holdings, and 24h funding")
108
- .action(async () => {
109
- const adapter = await getAdapter();
110
- const exName = adapter.name.toLowerCase();
111
- // Fetch perp balance + funding in parallel
104
+ async function fetchExchangeBalance(exName, adapter) {
105
+ const name = exName.toLowerCase();
112
106
  const [bal, fundingPayments] = await Promise.all([
113
107
  adapter.getBalance(),
114
108
  adapter.getFundingPayments(200).catch(() => []),
115
109
  ]);
116
- // Spot balances (HL and LT only)
117
110
  let spotBalances = [];
118
111
  try {
119
- if (exName === "hyperliquid") {
112
+ if (name === "hyperliquid") {
120
113
  const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
121
114
  const hlSpot = new HyperliquidSpotAdapter(adapter);
122
115
  await hlSpot.init();
123
116
  const raw = await hlSpot.getSpotBalances();
124
- // Get spot prices for USD valuation
125
117
  const markets = await hlSpot.getSpotMarkets();
126
118
  const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
127
- // HL spot API returns token names with -SPOT suffix (e.g., "HYPE-SPOT", "USDC-SPOT")
128
119
  const stripSuffix = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
129
120
  spotBalances = raw
130
121
  .filter(b => Number(b.total) > 0)
@@ -136,7 +127,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
136
127
  };
137
128
  });
138
129
  }
139
- else if (exName === "lighter") {
130
+ else if (name === "lighter") {
140
131
  const { LighterAdapter } = await import("../exchanges/lighter.js");
141
132
  const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
142
133
  const ltSpot = new LighterSpotAdapter(adapter);
@@ -155,33 +146,52 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
155
146
  }
156
147
  }
157
148
  catch { /* spot not available */ }
158
- // 24h funding summary
149
+ // HL unified account: perp equity already includes spot USDC balance.
150
+ // Detect by checking if adapter is HL without a dex override.
151
+ const isUnified = name === "hyperliquid" && !adapter.dex;
159
152
  const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
160
153
  const recent = fundingPayments.filter(f => f.time >= cutoff24h);
161
154
  const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
162
- // Total spot value
163
155
  const totalSpotUsd = spotBalances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
164
- if (isJson()) {
165
- return printJson(jsonOk({
166
- perp: bal,
167
- spot: spotBalances,
168
- funding24h,
169
- fundingPayments24h: recent.length,
170
- totalSpotValueUsd: totalSpotUsd,
171
- totalAccountValueUsd: Number(bal.equity) + totalSpotUsd,
172
- }));
173
- }
174
- // ── Perp Balance ──
175
- console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance\n`));
176
- console.log(chalk.white.bold(" Perp Account"));
177
- console.log(` Equity: $${formatUsd(bal.equity)}`);
178
- console.log(` Available: $${formatUsd(bal.available)}`);
179
- console.log(` Margin Used: $${formatUsd(bal.marginUsed)}`);
180
- console.log(` Unreal. PnL: ${formatPnl(bal.unrealizedPnl)}`);
181
- // ── Spot Holdings ──
182
- if (spotBalances.length > 0) {
156
+ // For unified accounts, equity already contains USDC spot value.
157
+ // Only add non-USDC spot token values to avoid double-counting.
158
+ const nonUsdcSpotUsd = isUnified
159
+ ? spotBalances
160
+ .filter(b => {
161
+ const tk = b.token.replace(/-SPOT$/i, "").toUpperCase();
162
+ return tk !== "USDC";
163
+ })
164
+ .reduce((s, b) => s + (b.valueUsd ?? 0), 0)
165
+ : totalSpotUsd;
166
+ return {
167
+ exchange: name,
168
+ perp: bal,
169
+ spot: spotBalances,
170
+ funding24h,
171
+ fundingPayments24h: recent.length,
172
+ totalSpotValueUsd: totalSpotUsd,
173
+ totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotUsd,
174
+ unifiedAccount: isUnified,
175
+ };
176
+ }
177
+ function printSingleBalance(exName, r) {
178
+ console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance`));
179
+ if (r.unifiedAccount)
180
+ console.log(chalk.gray(" (Unified account — perp & spot share single balance)\n"));
181
+ else
182
+ console.log();
183
+ console.log(chalk.white.bold(r.unifiedAccount ? " Account" : " Perp Account"));
184
+ console.log(` Equity: $${formatUsd(r.perp.equity)}`);
185
+ console.log(` Available: $${formatUsd(r.perp.available)}`);
186
+ console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
187
+ console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
188
+ // For unified, show non-USDC spot holdings (USDC is already in equity)
189
+ const displaySpot = r.unifiedAccount
190
+ ? r.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
191
+ : r.spot;
192
+ if (displaySpot.length > 0) {
183
193
  console.log(chalk.white.bold("\n Spot Holdings"));
184
- const rows = spotBalances.map(b => [
194
+ const rows = displaySpot.map(b => [
185
195
  chalk.white.bold(b.token),
186
196
  b.total,
187
197
  b.available,
@@ -190,27 +200,176 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
190
200
  ]);
191
201
  console.log(makeTable(["Token", "Total", "Available", "Held", "Value"], rows));
192
202
  }
193
- else if (exName === "hyperliquid" || exName === "lighter") {
194
- console.log(chalk.gray("\n No spot holdings.\n"));
195
- }
196
- // ── Funding 24h ──
197
203
  console.log(chalk.white.bold(" Funding (24h)"));
198
- console.log(` Payments: ${recent.length}`);
199
- console.log(` Total: ${formatPnl(funding24h)}`);
200
- // ── Total ──
201
- const totalValue = Number(bal.equity) + totalSpotUsd;
202
- if (totalSpotUsd > 0) {
204
+ console.log(` Payments: ${r.fundingPayments24h}`);
205
+ console.log(` Total: ${formatPnl(r.funding24h)}`);
206
+ // Only show Total breakdown when there are non-USDC spot tokens adding value
207
+ const nonUsdcSpotValue = displaySpot.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
208
+ if (nonUsdcSpotValue > 0) {
203
209
  console.log(chalk.white.bold("\n Total"));
204
- console.log(` Perp Equity: $${formatUsd(bal.equity)}`);
205
- console.log(` Spot Value: $${formatUsd(totalSpotUsd)}`);
206
- console.log(` Total Value: $${formatUsd(totalValue)}`);
210
+ console.log(` Equity: $${formatUsd(r.perp.equity)}`);
211
+ console.log(` Spot (other): $${formatUsd(nonUsdcSpotValue)}`);
212
+ console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
213
+ }
214
+ }
215
+ account
216
+ .command("balance")
217
+ .description("Account overview: perp balance, spot holdings, and 24h funding")
218
+ .action(async () => {
219
+ // Detect if user explicitly set -e (single exchange mode)
220
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
221
+ // Multi-exchange mode: show all exchanges
222
+ if (!explicitExchange && getAdapterForExchange) {
223
+ const results = [];
224
+ const errors = {};
225
+ await Promise.all(EXCHANGES.map(async (ex) => {
226
+ try {
227
+ const adapter = await getAdapterForExchange(ex);
228
+ results.push(await fetchExchangeBalance(ex, adapter));
229
+ }
230
+ catch (err) {
231
+ errors[ex] = err instanceof Error ? err.message : String(err);
232
+ }
233
+ }));
234
+ // Sort by equity desc
235
+ results.sort((a, b) => b.totalAccountValueUsd - a.totalAccountValueUsd);
236
+ if (isJson()) {
237
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
238
+ return printJson(jsonOk({
239
+ exchanges: results,
240
+ errors: Object.keys(errors).length > 0 ? errors : undefined,
241
+ totalEquity: results.reduce((s, r) => s + Number(r.perp.equity), 0),
242
+ totalSpotValueUsd: results.reduce((s, r) => s + r.totalSpotValueUsd, 0),
243
+ totalAccountValueUsd: grandTotal,
244
+ totalFunding24h: results.reduce((s, r) => s + r.funding24h, 0),
245
+ }));
246
+ }
247
+ console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
248
+ // Summary table
249
+ // For unified accounts, Spot column shows non-USDC only (USDC already in Equity)
250
+ const balRows = results.map(r => {
251
+ const nonUsdcSpot = r.unifiedAccount
252
+ ? r.spot
253
+ .filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
254
+ .reduce((s, b) => s + (b.valueUsd ?? 0), 0)
255
+ : r.totalSpotValueUsd;
256
+ return [
257
+ chalk.white.bold(r.exchange) + (r.unifiedAccount ? chalk.gray(" ★") : ""),
258
+ `$${formatUsd(r.perp.equity)}`,
259
+ `$${formatUsd(r.perp.available)}`,
260
+ `$${formatUsd(r.perp.marginUsed)}`,
261
+ nonUsdcSpot > 0 ? `$${formatUsd(nonUsdcSpot)}` : chalk.gray("-"),
262
+ formatPnl(r.funding24h),
263
+ `$${formatUsd(r.totalAccountValueUsd)}`,
264
+ ];
265
+ });
266
+ const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
267
+ const grandSpot = results.reduce((s, r) => {
268
+ if (r.unifiedAccount) {
269
+ return s + r.spot
270
+ .filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
271
+ .reduce((ss, b) => ss + (b.valueUsd ?? 0), 0);
272
+ }
273
+ return s + r.totalSpotValueUsd;
274
+ }, 0);
275
+ const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
276
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
277
+ balRows.push([
278
+ chalk.cyan.bold("TOTAL"),
279
+ chalk.cyan.bold(`$${formatUsd(grandEquity)}`),
280
+ "",
281
+ "",
282
+ grandSpot > 0 ? chalk.cyan.bold(`$${formatUsd(grandSpot)}`) : "",
283
+ formatPnl(grandFunding),
284
+ chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
285
+ ]);
286
+ console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
287
+ if (results.some(r => r.unifiedAccount)) {
288
+ console.log(chalk.gray(" ★ Unified account — Equity includes spot USDC, Spot shows non-USDC tokens only\n"));
289
+ }
290
+ // Spot details per exchange (for unified, skip USDC as it's in equity)
291
+ for (const r of results) {
292
+ const displaySpot = r.unifiedAccount
293
+ ? r.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
294
+ : r.spot;
295
+ if (displaySpot.length > 0) {
296
+ console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
297
+ const rows = displaySpot.map(b => [
298
+ chalk.white.bold(b.token),
299
+ b.total,
300
+ b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
301
+ ]);
302
+ console.log(makeTable(["Token", "Total", "Value"], rows));
303
+ }
304
+ }
305
+ // Errors
306
+ for (const [ex, msg] of Object.entries(errors)) {
307
+ console.log(chalk.gray(` ${ex}: ${msg}`));
308
+ }
309
+ console.log();
310
+ return;
207
311
  }
312
+ // Single exchange mode (explicit -e or no getAdapterForExchange)
313
+ const adapter = await getAdapter();
314
+ const exName = adapter.name.toLowerCase();
315
+ const result = await fetchExchangeBalance(exName, adapter);
316
+ if (isJson())
317
+ return printJson(jsonOk(result));
318
+ printSingleBalance(exName, result);
208
319
  console.log();
209
320
  });
210
321
  account
211
322
  .command("positions")
212
323
  .description("Show open positions")
213
324
  .action(async () => {
325
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
326
+ if (!explicitExchange && getAdapterForExchange) {
327
+ const allRows = [];
328
+ const errors = {};
329
+ await Promise.all(EXCHANGES.map(async (ex) => {
330
+ try {
331
+ const adapter = await getAdapterForExchange(ex);
332
+ const positions = await adapter.getPositions();
333
+ for (const p of positions) {
334
+ allRows.push([
335
+ chalk.white.bold(p.symbol),
336
+ chalk.gray(ex.slice(0, 3).toUpperCase()),
337
+ p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
338
+ p.size,
339
+ `$${formatUsd(p.entryPrice)}`,
340
+ `$${formatUsd(p.markPrice)}`,
341
+ p.liquidationPrice === "N/A" ? chalk.gray("N/A") : `$${formatUsd(p.liquidationPrice)}`,
342
+ formatPnl(p.unrealizedPnl),
343
+ `${p.leverage}x`,
344
+ ]);
345
+ }
346
+ }
347
+ catch (err) {
348
+ errors[ex] = err instanceof Error ? err.message : String(err);
349
+ }
350
+ }));
351
+ if (isJson()) {
352
+ const grouped = {};
353
+ await Promise.all(EXCHANGES.map(async (ex) => {
354
+ try {
355
+ const adapter = await getAdapterForExchange(ex);
356
+ grouped[ex] = await adapter.getPositions();
357
+ }
358
+ catch { /* skip */ }
359
+ }));
360
+ return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
361
+ }
362
+ if (allRows.length === 0) {
363
+ console.log(chalk.gray("\n No open positions across all exchanges.\n"));
364
+ for (const [ex, msg] of Object.entries(errors))
365
+ console.log(chalk.gray(` ${ex}: ${msg}`));
366
+ return;
367
+ }
368
+ console.log(makeTable(["Symbol", "Exch", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], allRows));
369
+ for (const [ex, msg] of Object.entries(errors))
370
+ console.log(chalk.gray(` ${ex}: ${msg}`));
371
+ return;
372
+ }
214
373
  const adapter = await getAdapter();
215
374
  const positions = await adapter.getPositions();
216
375
  if (isJson())
@@ -235,6 +394,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
235
394
  .command("orders")
236
395
  .description("Show open orders")
237
396
  .action(async () => {
397
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
398
+ if (!explicitExchange && getAdapterForExchange) {
399
+ const allRows = [];
400
+ const errors = {};
401
+ await Promise.all(EXCHANGES.map(async (ex) => {
402
+ try {
403
+ const adapter = await getAdapterForExchange(ex);
404
+ const orders = await adapter.getOpenOrders();
405
+ for (const o of orders) {
406
+ allRows.push([
407
+ o.orderId,
408
+ chalk.white.bold(o.symbol),
409
+ chalk.gray(ex.slice(0, 3).toUpperCase()),
410
+ o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
411
+ o.type,
412
+ `$${formatUsd(o.price)}`,
413
+ o.size,
414
+ o.filled,
415
+ o.status,
416
+ ]);
417
+ }
418
+ }
419
+ catch (err) {
420
+ errors[ex] = err instanceof Error ? err.message : String(err);
421
+ }
422
+ }));
423
+ if (isJson()) {
424
+ const grouped = {};
425
+ await Promise.all(EXCHANGES.map(async (ex) => {
426
+ try {
427
+ const adapter = await getAdapterForExchange(ex);
428
+ grouped[ex] = await adapter.getOpenOrders();
429
+ }
430
+ catch { /* skip */ }
431
+ }));
432
+ return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
433
+ }
434
+ if (allRows.length === 0) {
435
+ console.log(chalk.gray("\n No open orders across all exchanges.\n"));
436
+ for (const [ex, msg] of Object.entries(errors))
437
+ console.log(chalk.gray(` ${ex}: ${msg}`));
438
+ return;
439
+ }
440
+ console.log(makeTable(["ID", "Symbol", "Exch", "Side", "Type", "Price", "Size", "Filled", "Status"], allRows));
441
+ for (const [ex, msg] of Object.entries(errors))
442
+ console.log(chalk.gray(` ${ex}: ${msg}`));
443
+ return;
444
+ }
238
445
  const adapter = await getAdapter();
239
446
  const orders = await adapter.getOpenOrders();
240
447
  if (isJson())
@@ -136,7 +136,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
136
136
  {
137
137
  category: "account",
138
138
  commands: [
139
- "perp account info --json",
139
+ "perp account balance --json",
140
140
  "perp account positions --json",
141
141
  "perp account orders --json",
142
142
  "perp account history --json",
@@ -338,12 +338,12 @@ export function registerAgentCommands(program, getAdapter, isJson) {
338
338
  if (g.includes("buy") || g.includes("long")) {
339
339
  const symbol = extractSymbol(g) || "BTC";
340
340
  const size = extractNumber(g) || "0.01";
341
- steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account info --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} buy ${size} --json`, description: `Buy ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
341
+ steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account balance --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} buy ${size} --json`, description: `Buy ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
342
342
  }
343
343
  else if (g.includes("sell") || g.includes("short")) {
344
344
  const symbol = extractSymbol(g) || "BTC";
345
345
  const size = extractNumber(g) || "0.01";
346
- steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account info --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} sell ${size} --json`, description: `Sell ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
346
+ steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account balance --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} sell ${size} --json`, description: `Sell ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
347
347
  }
348
348
  else if (g.includes("close") || g.includes("exit")) {
349
349
  const symbol = extractSymbol(g) || "ALL";
@@ -360,7 +360,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
360
360
  }
361
361
  else if (g.includes("deposit")) {
362
362
  const amount = extractNumber(g) || "100";
363
- steps.push({ step: 1, command: "perp wallet balance --json", description: "Check wallet balance" }, { step: 2, command: `perp deposit pacifica ${amount} --json`, description: `Deposit $${amount} to Pacifica` }, { step: 3, command: "perp account info --json", description: "Verify deposit arrived" });
363
+ steps.push({ step: 1, command: "perp wallet balance --json", description: "Check wallet balance" }, { step: 2, command: `perp deposit pacifica ${amount} --json`, description: `Deposit $${amount} to Pacifica` }, { step: 3, command: "perp account balance --json", description: "Verify deposit arrived" });
364
364
  }
365
365
  else if (g.includes("price") || g.includes("market")) {
366
366
  const symbol = extractSymbol(g);
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
2
  import type { ExchangeAdapter } from "../exchanges/interface.js";
3
- export declare function registerMarketCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean): void;
3
+ export declare function registerMarketCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -2,12 +2,59 @@ import { PacificaAdapter } from "../exchanges/pacifica.js";
2
2
  import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
3
  import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
4
4
  import chalk from "chalk";
5
- export function registerMarketCommands(program, getAdapter, isJson) {
5
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
6
+ export function registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange) {
6
7
  const market = program.command("market").description("Market data commands");
7
8
  market
8
9
  .command("list")
9
10
  .description("List all available markets")
10
11
  .action(async () => {
12
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
13
+ // Multi-exchange mode
14
+ if (!explicitExchange && getAdapterForExchange) {
15
+ const allRows = [];
16
+ const errors = {};
17
+ await Promise.all(EXCHANGES.map(async (ex) => {
18
+ try {
19
+ 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
+ ]);
32
+ }
33
+ }
34
+ catch (err) {
35
+ errors[ex] = err instanceof Error ? err.message : String(err);
36
+ }
37
+ }));
38
+ // Sort by symbol name
39
+ allRows.sort((a, b) => a[0].localeCompare(b[0]));
40
+ 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
+ return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
50
+ }
51
+ console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], allRows));
52
+ for (const [ex, msg] of Object.entries(errors)) {
53
+ console.log(chalk.gray(` ${ex}: ${msg}`));
54
+ }
55
+ return;
56
+ }
57
+ // Single exchange mode
11
58
  const adapter = await getAdapter();
12
59
  const markets = await adapter.getMarkets();
13
60
  if (isJson())
@@ -27,6 +74,45 @@ export function registerMarketCommands(program, getAdapter, isJson) {
27
74
  .command("prices")
28
75
  .description("Get current prices for all markets")
29
76
  .action(async () => {
77
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
78
+ if (!explicitExchange && getAdapterForExchange) {
79
+ const allRows = [];
80
+ const errors = {};
81
+ await Promise.all(EXCHANGES.map(async (ex) => {
82
+ try {
83
+ 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
+ }
94
+ }
95
+ catch (err) {
96
+ errors[ex] = err instanceof Error ? err.message : String(err);
97
+ }
98
+ }));
99
+ allRows.sort((a, b) => a[0].localeCompare(b[0]));
100
+ 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
+ return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
110
+ }
111
+ console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding"], allRows));
112
+ for (const [ex, msg] of Object.entries(errors))
113
+ console.log(chalk.gray(` ${ex}: ${msg}`));
114
+ return;
115
+ }
30
116
  const adapter = await getAdapter();
31
117
  if (adapter instanceof PacificaAdapter) {
32
118
  const prices = await adapter.sdk.getPrices();
package/dist/index.js CHANGED
@@ -188,7 +188,7 @@ function getHLAdapter() {
188
188
  return _hlAdapter;
189
189
  }
190
190
  // Register command groups with async adapter getter
191
- registerMarketCommands(program, getAdapter, isJson);
191
+ registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
192
192
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
193
193
  registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
194
194
  registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * MCP (Model Context Protocol) server for perp-cli.
4
4
  *
5
- * Read-only advisor mode: provides market data, account info, and CLI command suggestions.
5
+ * Read-only advisor mode: provides market data, account balance, and CLI command suggestions.
6
6
  * Does NOT execute trades directly — instead suggests CLI commands for the user to run.
7
7
  * Adapters are created lazily from environment variables.
8
8
  */
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * MCP (Model Context Protocol) server for perp-cli.
4
4
  *
5
- * Read-only advisor mode: provides market data, account info, and CLI command suggestions.
5
+ * Read-only advisor mode: provides market data, account balance, and CLI command suggestions.
6
6
  * Does NOT execute trades directly — instead suggests CLI commands for the user to run.
7
7
  * Adapters are created lazily from environment variables.
8
8
  */
@@ -270,18 +270,18 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
270
270
  if (g.includes("buy") || g.includes("long")) {
271
271
  const symbol = extractSymbol(g) || "BTC";
272
272
  const size = extractNumber(g) || "<size>";
273
- steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} buy ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} buy ${size}`, description: `Buy ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
273
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} buy ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} buy ${size}`, description: `Buy ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
274
274
  }
275
275
  else if (g.includes("sell") || g.includes("short")) {
276
276
  const symbol = extractSymbol(g) || "BTC";
277
277
  const size = extractNumber(g) || "<size>";
278
- steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} sell ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} sell ${size}`, description: `Sell ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
278
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} sell ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} sell ${size}`, description: `Sell ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
279
279
  }
280
280
  else if (g.includes("limit")) {
281
281
  const symbol = extractSymbol(g) || "BTC";
282
282
  const size = extractNumber(g) || "<size>";
283
283
  const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
284
- steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade limit ${symbol} ${side} <price> ${size}`, description: `Place limit ${side} order`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account orders`, description: "Verify order placed" });
284
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade limit ${symbol} ${side} <price> ${size}`, description: `Place limit ${side} order`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account orders`, description: "Verify order placed" });
285
285
  }
286
286
  else if (g.includes("close") || g.includes("exit") || g.includes("flatten")) {
287
287
  const symbol = extractSymbol(g);
@@ -303,7 +303,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
303
303
  else if (g.includes("scale-in") || g.includes("scale in") || g.includes("dca in")) {
304
304
  const symbol = extractSymbol(g) || "<symbol>";
305
305
  const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
306
- steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade scale-in ${symbol} ${side} --levels '<price1>:<size>,<price2>:<size>'`, description: "Place scaled entry orders at multiple levels", dangerous: true });
306
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade scale-in ${symbol} ${side} --levels '<price1>:<size>,<price2>:<size>'`, description: "Place scaled entry orders at multiple levels", dangerous: true });
307
307
  }
308
308
  else if (g.includes("tpsl") || g.includes("tp/sl") || g.includes("tp sl") || (g.includes("take profit") && g.includes("stop loss"))) {
309
309
  const symbol = extractSymbol(g) || "<symbol>";
@@ -322,13 +322,13 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
322
322
  const symbol = extractSymbol(g) || "<symbol>";
323
323
  const amount = extractNumber(g) || "<usd>";
324
324
  const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
325
- steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade split ${symbol} ${side} ${amount}`, description: `Split ${side} $${amount} ${symbol} into depth-based slices`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
325
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade split ${symbol} ${side} ${amount}`, description: `Split ${side} $${amount} ${symbol} into depth-based slices`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
326
326
  }
327
327
  else if (g.includes("twap")) {
328
328
  const symbol = extractSymbol(g) || "<symbol>";
329
329
  const size = extractNumber(g) || "<size>";
330
330
  const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
331
- steps.push({ step: 1, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json trade twap ${symbol} ${side} ${size} <duration>`, description: `TWAP ${side} ${size} ${symbol} over duration`, dangerous: true });
331
+ steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json trade twap ${symbol} ${side} ${size} <duration>`, description: `TWAP ${side} ${size} ${symbol} over duration`, dangerous: true });
332
332
  }
333
333
  else if (g.includes("reduce")) {
334
334
  const symbol = extractSymbol(g) || "<symbol>";
@@ -362,7 +362,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
362
362
  else if (g.includes("dca")) {
363
363
  const amount = extractNumber(g) || "<amount>";
364
364
  const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
365
- steps.push({ step: 1, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json bot quick-dca ${symbol} ${side} ${amount} <interval>`, description: "Start DCA bot", dangerous: true }, { step: 3, command: "perp --json jobs list", description: "Verify bot is running" });
365
+ steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json bot quick-dca ${symbol} ${side} ${amount} <interval>`, description: "Start DCA bot", dangerous: true }, { step: 3, command: "perp --json jobs list", description: "Verify bot is running" });
366
366
  }
367
367
  else {
368
368
  steps.push({ step: 1, command: "perp --json bot preset-list", description: "List available bot presets" }, { step: 2, command: `perp -e ${ex} --json bot quick-arb`, description: "Start arb bot", dangerous: true }, { step: 3, command: "perp --json jobs list", description: "Verify bot is running" });
@@ -420,11 +420,11 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
420
420
  }
421
421
  else if (g.includes("deposit")) {
422
422
  const amount = extractNumber(g) || "<amount>";
423
- steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account info`, description: "Verify deposit arrived" });
423
+ steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account balance`, description: "Verify deposit arrived" });
424
424
  }
425
425
  else if (g.includes("withdraw")) {
426
426
  const amount = extractNumber(g) || "<amount>";
427
- steps.push({ step: 1, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 2, command: `perp --json funds withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
427
+ steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp --json funds withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
428
428
  }
429
429
  else if (g.includes("leverage")) {
430
430
  const symbol = extractSymbol(g) || "<symbol>";
@@ -523,7 +523,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
523
523
  relatedCommands: [
524
524
  `perp trade check ${symbol} ${side} ${sizeOrPrice || "<size>"}`,
525
525
  `perp market book ${symbol}`,
526
- `perp account info`,
526
+ `perp account balance`,
527
527
  ],
528
528
  };
529
529
  }
@@ -596,7 +596,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
596
596
  ? ["Places multiple reduce-only limit orders", "Total percentage should not exceed 100%"]
597
597
  : ["Places multiple limit orders — total size uses margin", "Review available balance before placing"],
598
598
  category: "write",
599
- relatedCommands: isTP ? ["perp account positions", "perp trade tpsl"] : ["perp account info", "perp market book"],
599
+ relatedCommands: isTP ? ["perp account positions", "perp trade tpsl"] : ["perp account balance", "perp market book"],
600
600
  };
601
601
  }
602
602
  else if (category === "trade" && sub === "twap") {
@@ -611,7 +611,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
611
611
  ],
612
612
  risks: ["Executes real trades over time — total size will be filled", "Market may move during execution"],
613
613
  category: "write",
614
- relatedCommands: ["perp account info", "perp trade cancel-twap"],
614
+ relatedCommands: ["perp account balance", "perp trade cancel-twap"],
615
615
  };
616
616
  }
617
617
  else if (category === "trade" && (sub === "trailing-stop" || sub === "pnl-track")) {
@@ -761,7 +761,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
761
761
  parameters: [{ name: "amount", value: args[3] || args[2], description: "Amount in USDC" }],
762
762
  risks: ["Moves real funds — verify amount before executing"],
763
763
  category: "write",
764
- relatedCommands: ["perp wallet balance", "perp account info"],
764
+ relatedCommands: ["perp wallet balance", "perp account balance"],
765
765
  };
766
766
  }
767
767
  else if (category === "risk") {
@@ -971,7 +971,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
971
971
  account: {
972
972
  description: "Account data (read-only)",
973
973
  subcommands: {
974
- info: { usage: "perp account info", description: "Balance, equity, margin, PnL" },
974
+ info: { usage: "perp account balance", description: "Balance, equity, margin, PnL" },
975
975
  positions: { usage: "perp account positions", description: "Open positions" },
976
976
  orders: { usage: "perp account orders", description: "Open/pending orders" },
977
977
  history: { usage: "perp account history", description: "Order history" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -17,7 +17,7 @@ perp --json wallet generate evm # generate EVM wallet
17
17
  perp --json wallet generate solana # generate Solana wallet
18
18
  perp --json wallet show # check configured wallets
19
19
  perp --json wallet balance # on-chain USDC balances
20
- perp --json -e <EX> account info # exchange account info
20
+ perp --json -e <EX> account balance # exchange account balance
21
21
  perp --json -e <EX> account positions # open positions
22
22
  perp --json -e <EX> market list # available markets
23
23
  perp --json -e <EX> trade market ... # execute trade
@@ -46,7 +46,7 @@ perp --json wallet show
46
46
  # → check "ok": true and address appears
47
47
 
48
48
  # 4. Check balance
49
- perp --json -e hl account info
49
+ perp --json -e hl account balance
50
50
  # → if balance is 0, tell user to deposit USDC
51
51
 
52
52
  # 5. Ready to trade
@@ -68,8 +68,8 @@ perp --json wallet show
68
68
  # → should show both exchanges with addresses
69
69
 
70
70
  # 3. Check balances on both
71
- perp --json -e hl account info
72
- perp --json -e pac account info
71
+ perp --json -e hl account balance
72
+ perp --json -e pac account balance
73
73
 
74
74
  # 4. If one side needs funding, bridge USDC
75
75
  perp --json bridge quote --from solana --to arbitrum --amount 500
@@ -115,7 +115,7 @@ Aliases for exchange names:
115
115
  ### Check On-Chain vs Exchange Balance
116
116
  ```bash
117
117
  perp --json wallet balance # on-chain USDC in your wallet
118
- perp --json -e hl account info # USDC deposited on exchange
118
+ perp --json -e hl account balance # USDC deposited on exchange
119
119
  ```
120
120
 
121
121
  **On-chain balance ≠ exchange balance.** USDC in your wallet must be deposited to the exchange before trading.
@@ -237,7 +237,7 @@ Error case:
237
237
 
238
238
  ### Safe to retry (idempotent):
239
239
  - `wallet show`, `wallet balance` — read-only
240
- - `account info`, `account positions`, `account orders` — read-only
240
+ - `account balance`, `account positions`, `account orders` — read-only
241
241
  - `market list`, `market mid`, `market book` — read-only
242
242
  - `arb scan` — read-only (`arb rates` is deprecated)
243
243
  - `portfolio`, `risk overview` — read-only
@@ -279,7 +279,7 @@ Symbols are auto-resolved by the CLI. **Always use bare symbols** (e.g., `BTC`,
279
279
 
280
280
  1. **Using `perp init`** — interactive, will hang forever. Use `wallet set` instead.
281
281
  2. **Forgetting `--json`** — output becomes unparseable human text.
282
- 3. **Trading with zero balance** — check `account info` first, tell user to deposit.
282
+ 3. **Trading with zero balance** — check `account balance` first, tell user to deposit.
283
283
  4. **Retrying a trade without checking** — leads to double positions. Always check `account positions` after a trade, even if it seemed to fail.
284
284
  5. **Bridging without quoting** — always run `bridge quote` first to show the user fees and estimated time.
285
285
  6. **Assuming deposit is instant** — after `bridge send`, wait for `bridge status` to confirm completion before depositing to the destination exchange.
@@ -17,7 +17,7 @@ perp --json market hip3 # list HIP-3 deployed perp dexes (Hyp
17
17
 
18
18
  ## Account (read-only, safe)
19
19
  ```bash
20
- perp --json account balance # perp balance + spot holdings + 24h funding (alias: account info)
20
+ perp --json account balance # perp balance + spot holdings + 24h funding (alias: account balance)
21
21
  perp --json account positions # open positions
22
22
  perp --json account orders # open/pending orders
23
23
  perp --json account history # order history
@@ -405,8 +405,8 @@ Rules of thumb:
405
405
 
406
406
  Each exchange holds its own separate balance. Before entering any arb:
407
407
  ```bash
408
- perp --json -e <EX_A> account info # check available balance on exchange A
409
- perp --json -e <EX_B> account info # check available balance on exchange B
408
+ perp --json -e <EX_A> account balance # check available balance on exchange A
409
+ perp --json -e <EX_B> account balance # check available balance on exchange B
410
410
  ```
411
411
 
412
412
  **Size each leg to fit the available balance on that exchange:**
@@ -53,8 +53,8 @@ else
53
53
  fi
54
54
 
55
55
  # 2. Balance check on both exchanges
56
- LONG_BAL=$($PERP --json -e "$LONG_EX" account info 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
57
- SHORT_BAL=$($PERP --json -e "$SHORT_EX" account info 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
56
+ LONG_BAL=$($PERP --json -e "$LONG_EX" account balance 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
57
+ SHORT_BAL=$($PERP --json -e "$SHORT_EX" account balance 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
58
58
 
59
59
  NEEDED=$(echo "$SIZE" | awk '{print $1}')
60
60
  if (( $(echo "$LONG_BAL < $NEEDED" | bc -l 2>/dev/null || echo 1) )); then