perp-cli 0.4.9 → 0.4.10

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,30 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
155
146
  }
156
147
  }
157
148
  catch { /* spot not available */ }
158
- // 24h funding summary
159
149
  const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
160
150
  const recent = fundingPayments.filter(f => f.time >= cutoff24h);
161
151
  const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
162
- // Total spot value
163
152
  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 ──
153
+ return {
154
+ exchange: name,
155
+ perp: bal,
156
+ spot: spotBalances,
157
+ funding24h,
158
+ fundingPayments24h: recent.length,
159
+ totalSpotValueUsd: totalSpotUsd,
160
+ totalAccountValueUsd: Number(bal.equity) + totalSpotUsd,
161
+ };
162
+ }
163
+ function printSingleBalance(exName, r) {
175
164
  console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance\n`));
176
165
  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) {
166
+ console.log(` Equity: $${formatUsd(r.perp.equity)}`);
167
+ console.log(` Available: $${formatUsd(r.perp.available)}`);
168
+ console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
169
+ console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
170
+ if (r.spot.length > 0) {
183
171
  console.log(chalk.white.bold("\n Spot Holdings"));
184
- const rows = spotBalances.map(b => [
172
+ const rows = r.spot.map(b => [
185
173
  chalk.white.bold(b.token),
186
174
  b.total,
187
175
  b.available,
@@ -190,21 +178,100 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
190
178
  ]);
191
179
  console.log(makeTable(["Token", "Total", "Available", "Held", "Value"], rows));
192
180
  }
193
- else if (exName === "hyperliquid" || exName === "lighter") {
194
- console.log(chalk.gray("\n No spot holdings.\n"));
195
- }
196
- // ── Funding 24h ──
197
181
  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) {
182
+ console.log(` Payments: ${r.fundingPayments24h}`);
183
+ console.log(` Total: ${formatPnl(r.funding24h)}`);
184
+ if (r.totalSpotValueUsd > 0) {
203
185
  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)}`);
186
+ console.log(` Perp Equity: $${formatUsd(r.perp.equity)}`);
187
+ console.log(` Spot Value: $${formatUsd(r.totalSpotValueUsd)}`);
188
+ console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
207
189
  }
190
+ }
191
+ account
192
+ .command("balance")
193
+ .alias("info")
194
+ .description("Account overview: perp balance, spot holdings, and 24h funding")
195
+ .action(async () => {
196
+ // Detect if user explicitly set -e (single exchange mode)
197
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
198
+ // Multi-exchange mode: show all exchanges
199
+ if (!explicitExchange && getAdapterForExchange) {
200
+ const results = [];
201
+ const errors = {};
202
+ await Promise.all(EXCHANGES.map(async (ex) => {
203
+ try {
204
+ const adapter = await getAdapterForExchange(ex);
205
+ results.push(await fetchExchangeBalance(ex, adapter));
206
+ }
207
+ catch (err) {
208
+ errors[ex] = err instanceof Error ? err.message : String(err);
209
+ }
210
+ }));
211
+ // Sort by equity desc
212
+ results.sort((a, b) => b.totalAccountValueUsd - a.totalAccountValueUsd);
213
+ if (isJson()) {
214
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
215
+ return printJson(jsonOk({
216
+ exchanges: results,
217
+ errors: Object.keys(errors).length > 0 ? errors : undefined,
218
+ totalEquity: results.reduce((s, r) => s + Number(r.perp.equity), 0),
219
+ totalSpotValueUsd: results.reduce((s, r) => s + r.totalSpotValueUsd, 0),
220
+ totalAccountValueUsd: grandTotal,
221
+ totalFunding24h: results.reduce((s, r) => s + r.funding24h, 0),
222
+ }));
223
+ }
224
+ console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
225
+ // 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
+ ]);
235
+ const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
236
+ const grandSpot = results.reduce((s, r) => s + r.totalSpotValueUsd, 0);
237
+ const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
238
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
239
+ balRows.push([
240
+ chalk.cyan.bold("TOTAL"),
241
+ chalk.cyan.bold(`$${formatUsd(grandEquity)}`),
242
+ "",
243
+ "",
244
+ grandSpot > 0 ? chalk.cyan.bold(`$${formatUsd(grandSpot)}`) : "",
245
+ formatPnl(grandFunding),
246
+ chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
247
+ ]);
248
+ console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
249
+ // Spot details per exchange
250
+ for (const r of results) {
251
+ if (r.spot.length > 0) {
252
+ console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
253
+ const rows = r.spot.map(b => [
254
+ chalk.white.bold(b.token),
255
+ b.total,
256
+ b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
257
+ ]);
258
+ console.log(makeTable(["Token", "Total", "Value"], rows));
259
+ }
260
+ }
261
+ // Errors
262
+ for (const [ex, msg] of Object.entries(errors)) {
263
+ console.log(chalk.gray(` ${ex}: ${msg}`));
264
+ }
265
+ console.log();
266
+ return;
267
+ }
268
+ // Single exchange mode (explicit -e or no getAdapterForExchange)
269
+ const adapter = await getAdapter();
270
+ const exName = adapter.name.toLowerCase();
271
+ const result = await fetchExchangeBalance(exName, adapter);
272
+ if (isJson())
273
+ return printJson(jsonOk(result));
274
+ printSingleBalance(exName, result);
208
275
  console.log();
209
276
  });
210
277
  account
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",