perp-cli 0.4.10 → 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.
@@ -146,10 +146,23 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
146
146
  }
147
147
  }
148
148
  catch { /* spot not available */ }
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;
149
152
  const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
150
153
  const recent = fundingPayments.filter(f => f.time >= cutoff24h);
151
154
  const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
152
155
  const totalSpotUsd = spotBalances.reduce((s, b) => s + (b.valueUsd ?? 0), 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;
153
166
  return {
154
167
  exchange: name,
155
168
  perp: bal,
@@ -157,19 +170,28 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
157
170
  funding24h,
158
171
  fundingPayments24h: recent.length,
159
172
  totalSpotValueUsd: totalSpotUsd,
160
- totalAccountValueUsd: Number(bal.equity) + totalSpotUsd,
173
+ totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotUsd,
174
+ unifiedAccount: isUnified,
161
175
  };
162
176
  }
163
177
  function printSingleBalance(exName, r) {
164
- console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance\n`));
165
- console.log(chalk.white.bold(" Perp Account"));
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"));
166
184
  console.log(` Equity: $${formatUsd(r.perp.equity)}`);
167
185
  console.log(` Available: $${formatUsd(r.perp.available)}`);
168
186
  console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
169
187
  console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
170
- if (r.spot.length > 0) {
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) {
171
193
  console.log(chalk.white.bold("\n Spot Holdings"));
172
- const rows = r.spot.map(b => [
194
+ const rows = displaySpot.map(b => [
173
195
  chalk.white.bold(b.token),
174
196
  b.total,
175
197
  b.available,
@@ -181,16 +203,17 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
181
203
  console.log(chalk.white.bold(" Funding (24h)"));
182
204
  console.log(` Payments: ${r.fundingPayments24h}`);
183
205
  console.log(` Total: ${formatPnl(r.funding24h)}`);
184
- if (r.totalSpotValueUsd > 0) {
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) {
185
209
  console.log(chalk.white.bold("\n Total"));
186
- console.log(` Perp Equity: $${formatUsd(r.perp.equity)}`);
187
- console.log(` Spot Value: $${formatUsd(r.totalSpotValueUsd)}`);
210
+ console.log(` Equity: $${formatUsd(r.perp.equity)}`);
211
+ console.log(` Spot (other): $${formatUsd(nonUsdcSpotValue)}`);
188
212
  console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
189
213
  }
190
214
  }
191
215
  account
192
216
  .command("balance")
193
- .alias("info")
194
217
  .description("Account overview: perp balance, spot holdings, and 24h funding")
195
218
  .action(async () => {
196
219
  // Detect if user explicitly set -e (single exchange mode)
@@ -223,17 +246,32 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
223
246
  }
224
247
  console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
225
248
  // Summary table
226
- const balRows = results.map(r => [
227
- chalk.white.bold(r.exchange),
228
- `$${formatUsd(r.perp.equity)}`,
229
- `$${formatUsd(r.perp.available)}`,
230
- `$${formatUsd(r.perp.marginUsed)}`,
231
- r.totalSpotValueUsd > 0 ? `$${formatUsd(r.totalSpotValueUsd)}` : chalk.gray("-"),
232
- formatPnl(r.funding24h),
233
- `$${formatUsd(r.totalAccountValueUsd)}`,
234
- ]);
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
+ });
235
266
  const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
236
- const grandSpot = results.reduce((s, r) => s + r.totalSpotValueUsd, 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);
237
275
  const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
238
276
  const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
239
277
  balRows.push([
@@ -246,11 +284,17 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
246
284
  chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
247
285
  ]);
248
286
  console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
249
- // Spot details per exchange
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)
250
291
  for (const r of results) {
251
- if (r.spot.length > 0) {
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) {
252
296
  console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
253
- const rows = r.spot.map(b => [
297
+ const rows = displaySpot.map(b => [
254
298
  chalk.white.bold(b.token),
255
299
  b.total,
256
300
  b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
@@ -278,6 +322,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
278
322
  .command("positions")
279
323
  .description("Show open positions")
280
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
+ }
281
373
  const adapter = await getAdapter();
282
374
  const positions = await adapter.getPositions();
283
375
  if (isJson())
@@ -302,6 +394,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
302
394
  .command("orders")
303
395
  .description("Show open orders")
304
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
+ }
305
445
  const adapter = await getAdapter();
306
446
  const orders = await adapter.getOpenOrders();
307
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.10",
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