perp-cli 0.5.2 → 0.6.0
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.
- package/README.md +29 -0
- package/dist/commands/account.js +60 -0
- package/dist/commands/agent.js +4 -2
- package/dist/commands/alerts.d.ts +2 -0
- package/dist/commands/alerts.js +487 -0
- package/dist/commands/arb/index.js +12 -4
- package/dist/commands/arb-auto.js +50 -19
- package/dist/commands/arb.js +43 -4
- package/dist/commands/history.js +329 -70
- package/dist/discord.d.ts +29 -0
- package/dist/discord.js +66 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +25 -10
- package/dist/exchanges/hyperliquid.d.ts +2 -0
- package/dist/exchanges/hyperliquid.js +10 -2
- package/dist/exchanges/lighter-spot.d.ts +6 -0
- package/dist/exchanges/lighter-spot.js +80 -31
- package/dist/exchanges/lighter.d.ts +3 -0
- package/dist/exchanges/lighter.js +46 -25
- package/dist/exchanges/pacifica.js +26 -4
- package/dist/index.js +325 -43
- package/dist/mcp-server.js +444 -1
- package/dist/settings.d.ts +26 -0
- package/dist/settings.js +14 -2
- package/dist/telegram.d.ts +32 -0
- package/dist/telegram.js +74 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +32 -0
- package/package.json +1 -1
- package/skills/perp-cli/package.json +35 -0
package/dist/index.js
CHANGED
|
@@ -35,7 +35,8 @@ import { registerPlanCommands } from "./commands/plan.js";
|
|
|
35
35
|
// funding merged into arb.ts
|
|
36
36
|
import { registerBacktestCommands } from "./commands/backtest.js";
|
|
37
37
|
import { registerDashboardCommands } from "./commands/dashboard.js";
|
|
38
|
-
import { registerInitCommand
|
|
38
|
+
import { registerInitCommand } from "./commands/init.js";
|
|
39
|
+
import { registerAlertCommands } from "./commands/alerts.js";
|
|
39
40
|
import { loadSettings, saveSettings } from "./settings.js";
|
|
40
41
|
import { setSharedApiNetwork } from "./shared-api.js";
|
|
41
42
|
const _require = createRequire(import.meta.url);
|
|
@@ -312,6 +313,7 @@ registerPlanCommands(program, getAdapter, isJson);
|
|
|
312
313
|
registerBacktestCommands(program, isJson);
|
|
313
314
|
registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
|
|
314
315
|
registerInitCommand(program);
|
|
316
|
+
registerAlertCommands(program, isJson);
|
|
315
317
|
// Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
|
|
316
318
|
const apiSpecCmd = program
|
|
317
319
|
.command("api-spec")
|
|
@@ -322,16 +324,284 @@ const apiSpecCmd = program
|
|
|
322
324
|
printJson(jsonOk(getCliSpec(program)));
|
|
323
325
|
});
|
|
324
326
|
apiSpecCmd._hidden = true;
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
.
|
|
327
|
+
// Unified dashboard: balances + positions + top arb opportunities
|
|
328
|
+
program.command("status")
|
|
329
|
+
.description("Unified dashboard: balances, positions, and top arb opportunities")
|
|
330
|
+
.option("--health", "Check connectivity only")
|
|
331
|
+
.action(async (opts) => {
|
|
328
332
|
if (opts.health) {
|
|
329
333
|
const { runHealthCheck } = await import("./commands/risk.js");
|
|
330
334
|
return runHealthCheck(isJson);
|
|
331
335
|
}
|
|
332
|
-
|
|
336
|
+
const json = isJson();
|
|
337
|
+
const { formatUsd, formatPnl, makeTable, printJson, jsonOk, withJsonErrors } = await import("./utils.js");
|
|
338
|
+
const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
|
|
339
|
+
const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
|
|
340
|
+
await withJsonErrors(json, async () => {
|
|
341
|
+
const EX_LIST = ["pacifica", "hyperliquid", "lighter"];
|
|
342
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
343
|
+
const snapshotPromises = EX_LIST.map(async (ex) => {
|
|
344
|
+
try {
|
|
345
|
+
const adapter = await getAdapterForExchange(ex);
|
|
346
|
+
const [balance, positions, orders] = await Promise.all([
|
|
347
|
+
adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(),
|
|
348
|
+
]);
|
|
349
|
+
// Fetch spot balances for HL and LT (Pacifica is perp-only)
|
|
350
|
+
let spotHoldings = [];
|
|
351
|
+
let isUnified = false;
|
|
352
|
+
try {
|
|
353
|
+
if (ex === "hyperliquid") {
|
|
354
|
+
const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
|
|
355
|
+
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
356
|
+
await hlSpot.init();
|
|
357
|
+
const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
|
|
358
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
359
|
+
const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
|
|
360
|
+
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => {
|
|
361
|
+
const base = strip(b.token);
|
|
362
|
+
return { ...b, valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total) };
|
|
363
|
+
});
|
|
364
|
+
isUnified = !adapter.dex;
|
|
365
|
+
}
|
|
366
|
+
else if (ex === "lighter") {
|
|
367
|
+
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
368
|
+
const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
|
|
369
|
+
const ltSpot = new LighterSpotAdapter(adapter);
|
|
370
|
+
await ltSpot.init();
|
|
371
|
+
const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
|
|
372
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
373
|
+
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => ({
|
|
374
|
+
...b,
|
|
375
|
+
valueUsd: b.token === "USDC" || b.token === "USDC_SPOT" ? Number(b.total) : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch { /* spot not available */ }
|
|
380
|
+
return { exchange: ex, connected: true, balance, positions, openOrders: orders.length, spotHoldings, isUnified, error: undefined };
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
return { exchange: ex, connected: false, balance: null, positions: [], openOrders: 0, spotHoldings: [], isUnified: false, error: err instanceof Error ? err.message : String(err) };
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
const arbPromise = fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 }).catch(() => null);
|
|
387
|
+
const [snapshots, arbSnapshot] = await Promise.all([Promise.all(snapshotPromises), arbPromise]);
|
|
388
|
+
// Save funding snapshot for sparkline history
|
|
389
|
+
if (arbSnapshot) {
|
|
390
|
+
try {
|
|
391
|
+
const allRates = arbSnapshot.symbols.flatMap(s => s.rates);
|
|
392
|
+
if (allRates.length > 0)
|
|
393
|
+
saveFundingSnapshot(allRates);
|
|
394
|
+
}
|
|
395
|
+
catch { /* */ }
|
|
396
|
+
}
|
|
397
|
+
// Build totals (include non-USDC spot value for unified accounts)
|
|
398
|
+
let totalEquity = 0;
|
|
399
|
+
let totalSpotValue = 0;
|
|
400
|
+
const allPositions = [];
|
|
401
|
+
const allSpotHoldings = [];
|
|
402
|
+
for (const s of snapshots) {
|
|
403
|
+
if (s.balance)
|
|
404
|
+
totalEquity += Number(s.balance.equity);
|
|
405
|
+
for (const p of s.positions)
|
|
406
|
+
allPositions.push({ ...p, exchange: s.exchange });
|
|
407
|
+
// For unified accounts (HL), USDC is already in perp equity — only add non-USDC spot
|
|
408
|
+
const displaySpot = s.isUnified
|
|
409
|
+
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
410
|
+
: s.spotHoldings;
|
|
411
|
+
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
412
|
+
totalSpotValue += spotVal;
|
|
413
|
+
totalEquity += spotVal;
|
|
414
|
+
for (const b of displaySpot)
|
|
415
|
+
allSpotHoldings.push({ ...b, exchange: s.exchange });
|
|
416
|
+
}
|
|
417
|
+
// Top arb
|
|
418
|
+
const topArb = arbSnapshot
|
|
419
|
+
? arbSnapshot.symbols.filter(s => s.maxSpreadAnnual >= 5).sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual).slice(0, 5)
|
|
420
|
+
: [];
|
|
421
|
+
// Risk
|
|
422
|
+
const marginUsed = snapshots.reduce((s, x) => s + (x.balance ? Number(x.balance.marginUsed) : 0), 0);
|
|
423
|
+
const marginPct = totalEquity > 0 ? (marginUsed / totalEquity) * 100 : 0;
|
|
424
|
+
const riskLevel = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
|
|
425
|
+
if (json) {
|
|
426
|
+
const now = new Date();
|
|
427
|
+
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
428
|
+
return printJson(jsonOk({
|
|
429
|
+
version: _pkg.version,
|
|
430
|
+
totalEquity,
|
|
431
|
+
riskLevel,
|
|
432
|
+
exchanges: snapshots.map(s => ({
|
|
433
|
+
name: s.exchange, connected: s.connected,
|
|
434
|
+
equity: s.balance ? Number(s.balance.equity) : 0,
|
|
435
|
+
available: s.balance ? Number(s.balance.available) : 0,
|
|
436
|
+
spotHoldings: s.spotHoldings.filter(b => Number(b.total) > 0).map(b => ({
|
|
437
|
+
token: b.token, total: b.total, available: b.available, valueUsd: b.valueUsd,
|
|
438
|
+
})),
|
|
439
|
+
})),
|
|
440
|
+
totalSpotValue,
|
|
441
|
+
positions: allPositions,
|
|
442
|
+
topArbOpportunities: await (async () => {
|
|
443
|
+
const { getHistoricalAverages: getAvgs } = await import("./funding-history.js");
|
|
444
|
+
const syms = topArb.map(a => a.symbol);
|
|
445
|
+
const avgs = syms.length > 0 ? getAvgs(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
|
|
446
|
+
return topArb.map(a => {
|
|
447
|
+
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
448
|
+
const avg = avgs.get(`${a.symbol}:${bestEx}`);
|
|
449
|
+
return {
|
|
450
|
+
symbol: a.symbol, spreadAnnual: a.maxSpreadAnnual,
|
|
451
|
+
direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
|
|
452
|
+
avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
|
|
453
|
+
avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
|
|
454
|
+
rateHistory: getHistoricalRates(a.symbol, bestEx, h24, now).map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })),
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
})(),
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
// ── Terminal UI ──
|
|
461
|
+
console.log(chalk.cyan.bold(`\n perp-cli v${_pkg.version}`) + chalk.gray(` — ${snapshots.filter(s => s.connected).length} exchanges connected\n`));
|
|
462
|
+
// Balance bars + Top Arb side by side
|
|
463
|
+
const now = new Date();
|
|
464
|
+
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
465
|
+
// Left column: Balances (perp + spot per exchange)
|
|
466
|
+
// Compute per-exchange totals: perp equity + non-USDC spot (unified) or all spot
|
|
467
|
+
const exTotals = snapshots.map(s => {
|
|
468
|
+
if (!s.connected || !s.balance)
|
|
469
|
+
return { total: 0, margin: 0, equity: 0 };
|
|
470
|
+
const equity = Number(s.balance.equity);
|
|
471
|
+
const margin = Number(s.balance.marginUsed);
|
|
472
|
+
const displaySpot = s.isUnified
|
|
473
|
+
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
474
|
+
: s.spotHoldings;
|
|
475
|
+
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
476
|
+
return { total: equity + spotVal, margin, equity };
|
|
477
|
+
});
|
|
478
|
+
const balLines = [];
|
|
479
|
+
balLines.push(chalk.white.bold(" Balances"));
|
|
480
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
481
|
+
const s = snapshots[i];
|
|
482
|
+
if (!s.connected) {
|
|
483
|
+
balLines.push(` ${chalk.gray(exAbbr(s.exchange).padEnd(4))} ${chalk.red("disconnected")}`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const { total, margin, equity } = exTotals[i];
|
|
487
|
+
const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
|
|
488
|
+
const barFull = Math.min(20, Math.round(usagePct / 5));
|
|
489
|
+
const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
|
|
490
|
+
const bar = usageColor("\u2588".repeat(barFull)) + chalk.gray("\u2591".repeat(20 - barFull));
|
|
491
|
+
balLines.push(` ${chalk.white.bold(exAbbr(s.exchange).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
|
|
492
|
+
}
|
|
493
|
+
// Spot holdings (non-USDC tokens with value)
|
|
494
|
+
const displaySpotHoldings = allSpotHoldings.filter(b => {
|
|
495
|
+
const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
|
|
496
|
+
return tk !== "USDC";
|
|
497
|
+
});
|
|
498
|
+
if (displaySpotHoldings.length > 0) {
|
|
499
|
+
balLines.push("");
|
|
500
|
+
balLines.push(chalk.white.bold(" Spot Holdings"));
|
|
501
|
+
for (const b of displaySpotHoldings) {
|
|
502
|
+
const token = b.token.replace(/-SPOT$/i, "");
|
|
503
|
+
const ex = exAbbr(b.exchange);
|
|
504
|
+
balLines.push(` ${chalk.gray(ex.padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const riskColor = riskLevel === "LOW" ? chalk.green : riskLevel === "MEDIUM" ? chalk.yellow : chalk.red;
|
|
508
|
+
balLines.push(` ${"─".repeat(44)}`);
|
|
509
|
+
balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(totalEquity)}`.padEnd(9))} Risk: ${riskColor(riskLevel)}`);
|
|
510
|
+
// Right column: Top Arb Opportunities (with 24h/7d averages from local history — 0 API calls)
|
|
511
|
+
const { getHistoricalAverages } = await import("./funding-history.js");
|
|
512
|
+
const arbSymbols = topArb.map(a => a.symbol);
|
|
513
|
+
const arbExchanges = ["hyperliquid", "pacifica", "lighter"];
|
|
514
|
+
const histAvgs = arbSymbols.length > 0 ? getHistoricalAverages(arbSymbols, arbExchanges) : new Map();
|
|
515
|
+
const arbLines = [];
|
|
516
|
+
arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
|
|
517
|
+
if (topArb.length > 0) {
|
|
518
|
+
// SYMBOL SPREAD 24h TREND 7d DIR
|
|
519
|
+
arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "".padEnd(9) + "7d".padEnd(7)));
|
|
520
|
+
for (const a of topArb) {
|
|
521
|
+
const spreadColor = a.maxSpreadAnnual >= 100 ? chalk.green.bold : a.maxSpreadAnnual >= 30 ? chalk.green : chalk.yellow;
|
|
522
|
+
const dir = `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`;
|
|
523
|
+
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
524
|
+
// 24h trend arrow
|
|
525
|
+
const history = getHistoricalRates(a.symbol, bestEx, h24, now);
|
|
526
|
+
let trend = "";
|
|
527
|
+
if (history.length >= 2) {
|
|
528
|
+
const oldAnn = Math.abs(history[0].hourlyRate) * 8760 * 100;
|
|
529
|
+
const newAnn = Math.abs(history[history.length - 1].hourlyRate) * 8760 * 100;
|
|
530
|
+
const delta = newAnn - oldAnn;
|
|
531
|
+
const abs = Math.abs(delta);
|
|
532
|
+
trend = abs < 1 ? chalk.gray(`\u2500${abs.toFixed(0)}%`)
|
|
533
|
+
: delta > 0 ? chalk.red(`\u25B2+${abs.toFixed(0)}%`)
|
|
534
|
+
: chalk.green(`\u25BC-${abs.toFixed(0)}%`);
|
|
535
|
+
}
|
|
536
|
+
// 24h and 7d averages (annualized from hourly)
|
|
537
|
+
const avg = histAvgs.get(`${a.symbol}:${bestEx}`);
|
|
538
|
+
const avg24h = avg?.avg24h != null ? `${(Math.abs(avg.avg24h) * 8760 * 100).toFixed(0)}%` : "-";
|
|
539
|
+
const avg7d = avg?.avg7d != null ? `${(Math.abs(avg.avg7d) * 8760 * 100).toFixed(0)}%` : "-";
|
|
540
|
+
arbLines.push(` ${chalk.white.bold(a.symbol.padEnd(8))} ${spreadColor(`${a.maxSpreadAnnual.toFixed(1)}%`.padEnd(9))}${chalk.gray(avg24h.padEnd(7))}${trend.padEnd(9)}${chalk.gray(avg7d.padEnd(7))}${chalk.gray(dir)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
arbLines.push(chalk.gray(" No opportunities above 5%"));
|
|
545
|
+
}
|
|
546
|
+
// Print layout — side-by-side if terminal is wide enough, else stacked
|
|
547
|
+
const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
548
|
+
const termW = process.stdout.columns || 80;
|
|
549
|
+
const SIDE_BY_SIDE_MIN = 100; // 2 indent + 1 + 46 + 1 + 48 + 1
|
|
550
|
+
if (termW >= SIDE_BY_SIDE_MIN) {
|
|
551
|
+
const LEFT_W = 46;
|
|
552
|
+
const RIGHT_W = 48;
|
|
553
|
+
const maxLines = Math.max(balLines.length, arbLines.length);
|
|
554
|
+
console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
|
|
555
|
+
for (let i = 0; i < maxLines; i++) {
|
|
556
|
+
const left = balLines[i] ?? "";
|
|
557
|
+
const right = arbLines[i] ?? "";
|
|
558
|
+
const leftPad = LEFT_W - stripAnsi(left).length;
|
|
559
|
+
const rightPad = RIGHT_W - stripAnsi(right).length;
|
|
560
|
+
console.log(` \u2502${left}${" ".repeat(Math.max(0, leftPad))}\u2502${right}${" ".repeat(Math.max(0, rightPad))}\u2502`);
|
|
561
|
+
}
|
|
562
|
+
console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Stacked layout for narrow terminals
|
|
566
|
+
const boxW = Math.min(termW - 4, 74); // 2 indent + 2 borders
|
|
567
|
+
const printBox = (lines) => {
|
|
568
|
+
console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
|
|
569
|
+
for (const line of lines) {
|
|
570
|
+
const pad = boxW - stripAnsi(line).length;
|
|
571
|
+
console.log(` \u2502${line}${" ".repeat(Math.max(0, pad))}\u2502`);
|
|
572
|
+
}
|
|
573
|
+
console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
|
|
574
|
+
};
|
|
575
|
+
printBox(balLines);
|
|
576
|
+
console.log();
|
|
577
|
+
printBox(arbLines);
|
|
578
|
+
}
|
|
579
|
+
// Positions
|
|
580
|
+
if (allPositions.length > 0) {
|
|
581
|
+
console.log(chalk.white.bold("\n Positions"));
|
|
582
|
+
const posRows = allPositions.map(p => {
|
|
583
|
+
const sideColor = p.side === "long" ? chalk.green : chalk.red;
|
|
584
|
+
const notional = Math.abs(Number(p.size) * Number(p.markPrice));
|
|
585
|
+
const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
|
|
586
|
+
const warn = p.leverage >= 5 ? chalk.red(" \u26A0") : "";
|
|
587
|
+
return [
|
|
588
|
+
chalk.white.bold(p.symbol.replace("-PERP", "")),
|
|
589
|
+
chalk.gray(exAbbr(p.exchange)),
|
|
590
|
+
sideColor(p.side.toUpperCase()),
|
|
591
|
+
p.size,
|
|
592
|
+
`$${formatUsd(p.entryPrice)}\u2192$${formatUsd(p.markPrice)}`,
|
|
593
|
+
formatPnl(p.unrealizedPnl),
|
|
594
|
+
`$${formatUsd(notional)}`,
|
|
595
|
+
levStr + warn,
|
|
596
|
+
];
|
|
597
|
+
});
|
|
598
|
+
console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry\u2192Mark", "uPnL", "Notional", "Lev"], posRows));
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
console.log(chalk.gray("\n No open positions.\n"));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
333
604
|
});
|
|
334
|
-
statusCmd._hidden = true;
|
|
335
605
|
// Switch shared API URLs if --network testnet is used
|
|
336
606
|
program.hook("preAction", () => {
|
|
337
607
|
const network = program.opts().network;
|
|
@@ -351,62 +621,74 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
351
621
|
const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
|
|
352
622
|
process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
|
|
353
623
|
process.env.LIGHTER_PRIVATE_KEY);
|
|
624
|
+
// ── ASCII banner ──
|
|
625
|
+
const banner = [
|
|
626
|
+
chalk.cyan(" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"),
|
|
627
|
+
chalk.cyan(" ┃") + chalk.white.bold(" perp-cli ") + chalk.gray(`v${_pkg.version}`) + " ".repeat(Math.max(0, 32 - _pkg.version.length)) + chalk.cyan("┃"),
|
|
628
|
+
chalk.cyan(" ┃") + chalk.gray(" Multi-DEX Perpetual Futures ") + chalk.cyan("┃"),
|
|
629
|
+
chalk.cyan(" ┃") + chalk.gray(" Pacifica · Hyperliquid · Lighter ") + chalk.cyan("┃"),
|
|
630
|
+
chalk.cyan(" ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"),
|
|
631
|
+
];
|
|
632
|
+
console.log("\n" + banner.join("\n"));
|
|
354
633
|
if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
|
|
355
634
|
// Fresh install — onboarding
|
|
356
|
-
console.log(chalk.
|
|
357
|
-
console.log("
|
|
358
|
-
console.log(` Get started: ${chalk.cyan("perp wallet set <exchange> <key>")}`);
|
|
635
|
+
console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
|
|
636
|
+
console.log(` ${chalk.cyan("perp wallet set <exchange> <key>")}`);
|
|
359
637
|
console.log(chalk.gray(`\n Or explore without a wallet:`));
|
|
360
638
|
console.log(` ${chalk.green("perp market list")} available markets`);
|
|
361
|
-
console.log(` ${chalk.green("perp -e
|
|
639
|
+
console.log(` ${chalk.green("perp -e hl market list")} Hyperliquid markets`);
|
|
362
640
|
console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
|
|
363
641
|
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
364
642
|
}
|
|
365
643
|
else {
|
|
366
|
-
// Configured — show status
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
? w.address.slice(0, 6) + "..." + w.address.slice(-4)
|
|
378
|
-
: w.address;
|
|
379
|
-
console.log(` ${chalk.cyan(exchange.padEnd(14))} ${chalk.white(walletName)} ${chalk.gray(addr)}`);
|
|
380
|
-
}
|
|
644
|
+
// Configured — show exchange status + balance
|
|
645
|
+
const EX_NAMES = ["pacifica", "hyperliquid", "lighter"];
|
|
646
|
+
const exLabel = (e) => e === "pacifica" ? "Pacifica" : e === "hyperliquid" ? "Hyperliquid" : "Lighter";
|
|
647
|
+
// Ping + balance in parallel (with 5s timeout to keep landing fast)
|
|
648
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);
|
|
649
|
+
const statusResults = await Promise.allSettled(EX_NAMES.map(async (ex) => {
|
|
650
|
+
try {
|
|
651
|
+
const adapter = await withTimeout(getAdapterForExchange(ex), 5000);
|
|
652
|
+
const [balance, positions] = await withTimeout(Promise.all([adapter.getBalance(), adapter.getPositions()]), 5000);
|
|
653
|
+
const posCount = positions.filter(p => Number(p.size) > 0).length;
|
|
654
|
+
return { exchange: ex, ok: true, equity: Number(balance.equity), positions: posCount };
|
|
381
655
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
656
|
+
catch {
|
|
657
|
+
return { exchange: ex, ok: false, equity: 0, positions: 0 };
|
|
658
|
+
}
|
|
659
|
+
}));
|
|
660
|
+
console.log(chalk.white.bold("\n Exchanges:"));
|
|
661
|
+
let totalEquity = 0;
|
|
662
|
+
let totalPositions = 0;
|
|
663
|
+
for (const r of statusResults) {
|
|
664
|
+
if (r.status !== "fulfilled")
|
|
665
|
+
continue;
|
|
666
|
+
const s = r.value;
|
|
667
|
+
const icon = s.ok ? chalk.green("●") : chalk.red("○");
|
|
668
|
+
const eq = s.ok ? chalk.white(`$${Number(s.equity).toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`) : chalk.gray("—");
|
|
669
|
+
const pos = s.ok && s.positions > 0 ? chalk.yellow(` ${s.positions} pos`) : "";
|
|
670
|
+
console.log(` ${icon} ${chalk.cyan(exLabel(s.exchange).padEnd(14))} ${eq}${pos}`);
|
|
671
|
+
if (s.ok) {
|
|
672
|
+
totalEquity += s.equity;
|
|
673
|
+
totalPositions += s.positions;
|
|
397
674
|
}
|
|
398
675
|
}
|
|
399
|
-
|
|
676
|
+
if (totalEquity > 0) {
|
|
677
|
+
console.log(chalk.gray(" ─".repeat(20)));
|
|
678
|
+
console.log(` ${chalk.white.bold("Total".padEnd(16))} ${chalk.white.bold(`$${totalEquity.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`)}${totalPositions > 0 ? chalk.yellow(` ${totalPositions} pos`) : ""}`);
|
|
679
|
+
}
|
|
680
|
+
console.log(chalk.white.bold("\n Commands:"));
|
|
400
681
|
console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
|
|
401
|
-
console.log(` ${chalk.green("perp
|
|
682
|
+
console.log(` ${chalk.green("perp status")} full dashboard`);
|
|
402
683
|
console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
|
|
403
|
-
console.log(` ${chalk.green("perp dashboard")} live monitoring`);
|
|
684
|
+
console.log(` ${chalk.green("perp dashboard")} live web monitoring`);
|
|
404
685
|
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
405
686
|
}
|
|
406
687
|
}
|
|
407
688
|
catch {
|
|
408
689
|
program.help();
|
|
409
690
|
}
|
|
691
|
+
setTimeout(() => process.exit(0), 500);
|
|
410
692
|
})();
|
|
411
693
|
}
|
|
412
694
|
else {
|