perp-cli 0.5.0 → 0.5.3
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 +34 -10
- package/dist/cli-spec.js +2 -2
- package/dist/commands/account.js +93 -8
- package/dist/commands/arb/index.js +38 -4
- package/dist/commands/arb-auto.js +50 -19
- package/dist/commands/arb.js +46 -4
- package/dist/commands/trade.js +60 -20
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +25 -10
- package/dist/exchanges/hyperliquid.d.ts +11 -0
- package/dist/exchanges/hyperliquid.js +50 -4
- 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 +47 -26
- package/dist/exchanges/pacifica.js +26 -4
- package/dist/index.js +273 -5
- package/dist/mcp-server.js +53 -50
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +32 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +90 -189
package/dist/index.js
CHANGED
|
@@ -322,16 +322,284 @@ const apiSpecCmd = program
|
|
|
322
322
|
printJson(jsonOk(getCliSpec(program)));
|
|
323
323
|
});
|
|
324
324
|
apiSpecCmd._hidden = true;
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
.
|
|
325
|
+
// Unified dashboard: balances + positions + top arb opportunities
|
|
326
|
+
program.command("status")
|
|
327
|
+
.description("Unified dashboard: balances, positions, and top arb opportunities")
|
|
328
|
+
.option("--health", "Check connectivity only")
|
|
329
|
+
.action(async (opts) => {
|
|
328
330
|
if (opts.health) {
|
|
329
331
|
const { runHealthCheck } = await import("./commands/risk.js");
|
|
330
332
|
return runHealthCheck(isJson);
|
|
331
333
|
}
|
|
332
|
-
|
|
334
|
+
const json = isJson();
|
|
335
|
+
const { formatUsd, formatPnl, makeTable, printJson, jsonOk, withJsonErrors } = await import("./utils.js");
|
|
336
|
+
const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
|
|
337
|
+
const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
|
|
338
|
+
await withJsonErrors(json, async () => {
|
|
339
|
+
const EX_LIST = ["pacifica", "hyperliquid", "lighter"];
|
|
340
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
341
|
+
const snapshotPromises = EX_LIST.map(async (ex) => {
|
|
342
|
+
try {
|
|
343
|
+
const adapter = await getAdapterForExchange(ex);
|
|
344
|
+
const [balance, positions, orders] = await Promise.all([
|
|
345
|
+
adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(),
|
|
346
|
+
]);
|
|
347
|
+
// Fetch spot balances for HL and LT (Pacifica is perp-only)
|
|
348
|
+
let spotHoldings = [];
|
|
349
|
+
let isUnified = false;
|
|
350
|
+
try {
|
|
351
|
+
if (ex === "hyperliquid") {
|
|
352
|
+
const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
|
|
353
|
+
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
354
|
+
await hlSpot.init();
|
|
355
|
+
const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
|
|
356
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
357
|
+
const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
|
|
358
|
+
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => {
|
|
359
|
+
const base = strip(b.token);
|
|
360
|
+
return { ...b, valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total) };
|
|
361
|
+
});
|
|
362
|
+
isUnified = !adapter.dex;
|
|
363
|
+
}
|
|
364
|
+
else if (ex === "lighter") {
|
|
365
|
+
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
366
|
+
const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
|
|
367
|
+
const ltSpot = new LighterSpotAdapter(adapter);
|
|
368
|
+
await ltSpot.init();
|
|
369
|
+
const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
|
|
370
|
+
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
371
|
+
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => ({
|
|
372
|
+
...b,
|
|
373
|
+
valueUsd: b.token === "USDC" || b.token === "USDC_SPOT" ? Number(b.total) : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch { /* spot not available */ }
|
|
378
|
+
return { exchange: ex, connected: true, balance, positions, openOrders: orders.length, spotHoldings, isUnified, error: undefined };
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return { exchange: ex, connected: false, balance: null, positions: [], openOrders: 0, spotHoldings: [], isUnified: false, error: err instanceof Error ? err.message : String(err) };
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const arbPromise = fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 }).catch(() => null);
|
|
385
|
+
const [snapshots, arbSnapshot] = await Promise.all([Promise.all(snapshotPromises), arbPromise]);
|
|
386
|
+
// Save funding snapshot for sparkline history
|
|
387
|
+
if (arbSnapshot) {
|
|
388
|
+
try {
|
|
389
|
+
const allRates = arbSnapshot.symbols.flatMap(s => s.rates);
|
|
390
|
+
if (allRates.length > 0)
|
|
391
|
+
saveFundingSnapshot(allRates);
|
|
392
|
+
}
|
|
393
|
+
catch { /* */ }
|
|
394
|
+
}
|
|
395
|
+
// Build totals (include non-USDC spot value for unified accounts)
|
|
396
|
+
let totalEquity = 0;
|
|
397
|
+
let totalSpotValue = 0;
|
|
398
|
+
const allPositions = [];
|
|
399
|
+
const allSpotHoldings = [];
|
|
400
|
+
for (const s of snapshots) {
|
|
401
|
+
if (s.balance)
|
|
402
|
+
totalEquity += Number(s.balance.equity);
|
|
403
|
+
for (const p of s.positions)
|
|
404
|
+
allPositions.push({ ...p, exchange: s.exchange });
|
|
405
|
+
// For unified accounts (HL), USDC is already in perp equity — only add non-USDC spot
|
|
406
|
+
const displaySpot = s.isUnified
|
|
407
|
+
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
408
|
+
: s.spotHoldings;
|
|
409
|
+
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
410
|
+
totalSpotValue += spotVal;
|
|
411
|
+
totalEquity += spotVal;
|
|
412
|
+
for (const b of displaySpot)
|
|
413
|
+
allSpotHoldings.push({ ...b, exchange: s.exchange });
|
|
414
|
+
}
|
|
415
|
+
// Top arb
|
|
416
|
+
const topArb = arbSnapshot
|
|
417
|
+
? arbSnapshot.symbols.filter(s => s.maxSpreadAnnual >= 5).sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual).slice(0, 5)
|
|
418
|
+
: [];
|
|
419
|
+
// Risk
|
|
420
|
+
const marginUsed = snapshots.reduce((s, x) => s + (x.balance ? Number(x.balance.marginUsed) : 0), 0);
|
|
421
|
+
const marginPct = totalEquity > 0 ? (marginUsed / totalEquity) * 100 : 0;
|
|
422
|
+
const riskLevel = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
|
|
423
|
+
if (json) {
|
|
424
|
+
const now = new Date();
|
|
425
|
+
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
426
|
+
return printJson(jsonOk({
|
|
427
|
+
version: _pkg.version,
|
|
428
|
+
totalEquity,
|
|
429
|
+
riskLevel,
|
|
430
|
+
exchanges: snapshots.map(s => ({
|
|
431
|
+
name: s.exchange, connected: s.connected,
|
|
432
|
+
equity: s.balance ? Number(s.balance.equity) : 0,
|
|
433
|
+
available: s.balance ? Number(s.balance.available) : 0,
|
|
434
|
+
spotHoldings: s.spotHoldings.filter(b => Number(b.total) > 0).map(b => ({
|
|
435
|
+
token: b.token, total: b.total, available: b.available, valueUsd: b.valueUsd,
|
|
436
|
+
})),
|
|
437
|
+
})),
|
|
438
|
+
totalSpotValue,
|
|
439
|
+
positions: allPositions,
|
|
440
|
+
topArbOpportunities: await (async () => {
|
|
441
|
+
const { getHistoricalAverages: getAvgs } = await import("./funding-history.js");
|
|
442
|
+
const syms = topArb.map(a => a.symbol);
|
|
443
|
+
const avgs = syms.length > 0 ? getAvgs(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
|
|
444
|
+
return topArb.map(a => {
|
|
445
|
+
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
446
|
+
const avg = avgs.get(`${a.symbol}:${bestEx}`);
|
|
447
|
+
return {
|
|
448
|
+
symbol: a.symbol, spreadAnnual: a.maxSpreadAnnual,
|
|
449
|
+
direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
|
|
450
|
+
avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
|
|
451
|
+
avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
|
|
452
|
+
rateHistory: getHistoricalRates(a.symbol, bestEx, h24, now).map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
})(),
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
// ── Terminal UI ──
|
|
459
|
+
console.log(chalk.cyan.bold(`\n perp-cli v${_pkg.version}`) + chalk.gray(` — ${snapshots.filter(s => s.connected).length} exchanges connected\n`));
|
|
460
|
+
// Balance bars + Top Arb side by side
|
|
461
|
+
const now = new Date();
|
|
462
|
+
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
463
|
+
// Left column: Balances (perp + spot per exchange)
|
|
464
|
+
// Compute per-exchange totals: perp equity + non-USDC spot (unified) or all spot
|
|
465
|
+
const exTotals = snapshots.map(s => {
|
|
466
|
+
if (!s.connected || !s.balance)
|
|
467
|
+
return { total: 0, margin: 0, equity: 0 };
|
|
468
|
+
const equity = Number(s.balance.equity);
|
|
469
|
+
const margin = Number(s.balance.marginUsed);
|
|
470
|
+
const displaySpot = s.isUnified
|
|
471
|
+
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
472
|
+
: s.spotHoldings;
|
|
473
|
+
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
474
|
+
return { total: equity + spotVal, margin, equity };
|
|
475
|
+
});
|
|
476
|
+
const balLines = [];
|
|
477
|
+
balLines.push(chalk.white.bold(" Balances"));
|
|
478
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
479
|
+
const s = snapshots[i];
|
|
480
|
+
if (!s.connected) {
|
|
481
|
+
balLines.push(` ${chalk.gray(exAbbr(s.exchange).padEnd(4))} ${chalk.red("disconnected")}`);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const { total, margin, equity } = exTotals[i];
|
|
485
|
+
const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
|
|
486
|
+
const barFull = Math.min(20, Math.round(usagePct / 5));
|
|
487
|
+
const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
|
|
488
|
+
const bar = usageColor("\u2588".repeat(barFull)) + chalk.gray("\u2591".repeat(20 - barFull));
|
|
489
|
+
balLines.push(` ${chalk.white.bold(exAbbr(s.exchange).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
|
|
490
|
+
}
|
|
491
|
+
// Spot holdings (non-USDC tokens with value)
|
|
492
|
+
const displaySpotHoldings = allSpotHoldings.filter(b => {
|
|
493
|
+
const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
|
|
494
|
+
return tk !== "USDC";
|
|
495
|
+
});
|
|
496
|
+
if (displaySpotHoldings.length > 0) {
|
|
497
|
+
balLines.push("");
|
|
498
|
+
balLines.push(chalk.white.bold(" Spot Holdings"));
|
|
499
|
+
for (const b of displaySpotHoldings) {
|
|
500
|
+
const token = b.token.replace(/-SPOT$/i, "");
|
|
501
|
+
const ex = exAbbr(b.exchange);
|
|
502
|
+
balLines.push(` ${chalk.gray(ex.padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const riskColor = riskLevel === "LOW" ? chalk.green : riskLevel === "MEDIUM" ? chalk.yellow : chalk.red;
|
|
506
|
+
balLines.push(` ${"─".repeat(44)}`);
|
|
507
|
+
balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(totalEquity)}`.padEnd(9))} Risk: ${riskColor(riskLevel)}`);
|
|
508
|
+
// Right column: Top Arb Opportunities (with 24h/7d averages from local history — 0 API calls)
|
|
509
|
+
const { getHistoricalAverages } = await import("./funding-history.js");
|
|
510
|
+
const arbSymbols = topArb.map(a => a.symbol);
|
|
511
|
+
const arbExchanges = ["hyperliquid", "pacifica", "lighter"];
|
|
512
|
+
const histAvgs = arbSymbols.length > 0 ? getHistoricalAverages(arbSymbols, arbExchanges) : new Map();
|
|
513
|
+
const arbLines = [];
|
|
514
|
+
arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
|
|
515
|
+
if (topArb.length > 0) {
|
|
516
|
+
// SYMBOL SPREAD 24h TREND 7d DIR
|
|
517
|
+
arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "".padEnd(9) + "7d".padEnd(7)));
|
|
518
|
+
for (const a of topArb) {
|
|
519
|
+
const spreadColor = a.maxSpreadAnnual >= 100 ? chalk.green.bold : a.maxSpreadAnnual >= 30 ? chalk.green : chalk.yellow;
|
|
520
|
+
const dir = `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`;
|
|
521
|
+
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
522
|
+
// 24h trend arrow
|
|
523
|
+
const history = getHistoricalRates(a.symbol, bestEx, h24, now);
|
|
524
|
+
let trend = "";
|
|
525
|
+
if (history.length >= 2) {
|
|
526
|
+
const oldAnn = Math.abs(history[0].hourlyRate) * 8760 * 100;
|
|
527
|
+
const newAnn = Math.abs(history[history.length - 1].hourlyRate) * 8760 * 100;
|
|
528
|
+
const delta = newAnn - oldAnn;
|
|
529
|
+
const abs = Math.abs(delta);
|
|
530
|
+
trend = abs < 1 ? chalk.gray(`\u2500${abs.toFixed(0)}%`)
|
|
531
|
+
: delta > 0 ? chalk.red(`\u25B2+${abs.toFixed(0)}%`)
|
|
532
|
+
: chalk.green(`\u25BC-${abs.toFixed(0)}%`);
|
|
533
|
+
}
|
|
534
|
+
// 24h and 7d averages (annualized from hourly)
|
|
535
|
+
const avg = histAvgs.get(`${a.symbol}:${bestEx}`);
|
|
536
|
+
const avg24h = avg?.avg24h != null ? `${(Math.abs(avg.avg24h) * 8760 * 100).toFixed(0)}%` : "-";
|
|
537
|
+
const avg7d = avg?.avg7d != null ? `${(Math.abs(avg.avg7d) * 8760 * 100).toFixed(0)}%` : "-";
|
|
538
|
+
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)}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
arbLines.push(chalk.gray(" No opportunities above 5%"));
|
|
543
|
+
}
|
|
544
|
+
// Print layout — side-by-side if terminal is wide enough, else stacked
|
|
545
|
+
const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
546
|
+
const termW = process.stdout.columns || 80;
|
|
547
|
+
const SIDE_BY_SIDE_MIN = 100; // 2 indent + 1 + 46 + 1 + 48 + 1
|
|
548
|
+
if (termW >= SIDE_BY_SIDE_MIN) {
|
|
549
|
+
const LEFT_W = 46;
|
|
550
|
+
const RIGHT_W = 48;
|
|
551
|
+
const maxLines = Math.max(balLines.length, arbLines.length);
|
|
552
|
+
console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
|
|
553
|
+
for (let i = 0; i < maxLines; i++) {
|
|
554
|
+
const left = balLines[i] ?? "";
|
|
555
|
+
const right = arbLines[i] ?? "";
|
|
556
|
+
const leftPad = LEFT_W - stripAnsi(left).length;
|
|
557
|
+
const rightPad = RIGHT_W - stripAnsi(right).length;
|
|
558
|
+
console.log(` \u2502${left}${" ".repeat(Math.max(0, leftPad))}\u2502${right}${" ".repeat(Math.max(0, rightPad))}\u2502`);
|
|
559
|
+
}
|
|
560
|
+
console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
// Stacked layout for narrow terminals
|
|
564
|
+
const boxW = Math.min(termW - 4, 74); // 2 indent + 2 borders
|
|
565
|
+
const printBox = (lines) => {
|
|
566
|
+
console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
|
|
567
|
+
for (const line of lines) {
|
|
568
|
+
const pad = boxW - stripAnsi(line).length;
|
|
569
|
+
console.log(` \u2502${line}${" ".repeat(Math.max(0, pad))}\u2502`);
|
|
570
|
+
}
|
|
571
|
+
console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
|
|
572
|
+
};
|
|
573
|
+
printBox(balLines);
|
|
574
|
+
console.log();
|
|
575
|
+
printBox(arbLines);
|
|
576
|
+
}
|
|
577
|
+
// Positions
|
|
578
|
+
if (allPositions.length > 0) {
|
|
579
|
+
console.log(chalk.white.bold("\n Positions"));
|
|
580
|
+
const posRows = allPositions.map(p => {
|
|
581
|
+
const sideColor = p.side === "long" ? chalk.green : chalk.red;
|
|
582
|
+
const notional = Math.abs(Number(p.size) * Number(p.markPrice));
|
|
583
|
+
const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
|
|
584
|
+
const warn = p.leverage >= 5 ? chalk.red(" \u26A0") : "";
|
|
585
|
+
return [
|
|
586
|
+
chalk.white.bold(p.symbol.replace("-PERP", "")),
|
|
587
|
+
chalk.gray(exAbbr(p.exchange)),
|
|
588
|
+
sideColor(p.side.toUpperCase()),
|
|
589
|
+
p.size,
|
|
590
|
+
`$${formatUsd(p.entryPrice)}\u2192$${formatUsd(p.markPrice)}`,
|
|
591
|
+
formatPnl(p.unrealizedPnl),
|
|
592
|
+
`$${formatUsd(notional)}`,
|
|
593
|
+
levStr + warn,
|
|
594
|
+
];
|
|
595
|
+
});
|
|
596
|
+
console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry\u2192Mark", "uPnL", "Notional", "Lev"], posRows));
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
console.log(chalk.gray("\n No open positions.\n"));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
333
602
|
});
|
|
334
|
-
statusCmd._hidden = true;
|
|
335
603
|
// Switch shared API URLs if --network testnet is used
|
|
336
604
|
program.hook("preAction", () => {
|
|
337
605
|
const network = program.opts().network;
|
package/dist/mcp-server.js
CHANGED
|
@@ -339,15 +339,15 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
339
339
|
steps.push({ step: 1, command: `perp -e ${ex} --json account orders`, description: "List open orders to find order ID" }, { step: 2, command: `perp -e ${ex} --json trade edit <symbol> <orderId> <newPrice> <newSize>`, description: "Modify the order", dangerous: true });
|
|
340
340
|
}
|
|
341
341
|
else if (g.includes("arb") || g.includes("arbitrage")) {
|
|
342
|
-
steps.push({ step: 1, command: "perp --json arb scan --min 5", description: "Scan funding rate arbitrage opportunities" }, { step: 2, command: "perp --json arb scan
|
|
342
|
+
steps.push({ step: 1, command: "perp --json arb scan --min 5", description: "Scan funding rate arbitrage opportunities" }, { step: 2, command: "perp --json arb scan --gaps", description: "Check cross-exchange price gaps" }, { step: 3, command: "perp --json arb scan --hip3", description: "HIP-3 cross-dex arb opportunities (Hyperliquid)" });
|
|
343
343
|
}
|
|
344
344
|
else if (g.includes("funding")) {
|
|
345
345
|
const symbol = extractSymbol(g);
|
|
346
346
|
if (symbol) {
|
|
347
|
-
steps.push({ step: 1, command: `perp --json arb rates`, description: `Funding rates across all exchanges` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
|
|
347
|
+
steps.push({ step: 1, command: `perp --json arb scan --rates`, description: `Funding rates across all exchanges` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
|
|
348
348
|
}
|
|
349
349
|
else {
|
|
350
|
-
steps.push({ step: 1, command: "perp --json arb rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json arb scan --min 5", description: "Funding rate arb opportunities" });
|
|
350
|
+
steps.push({ step: 1, command: "perp --json arb scan --rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json arb scan --min 5", description: "Funding rate arb opportunities" });
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
else if (g.includes("bridge") || g.includes("transfer") || g.includes("cross-chain")) {
|
|
@@ -386,22 +386,22 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
386
386
|
steps.push({ step: 1, command: `perp -e ${ex} --json risk status`, description: "Portfolio risk overview" }, { step: 2, command: `perp -e ${ex} --json risk limits`, description: "Current position limits" });
|
|
387
387
|
}
|
|
388
388
|
else if (g.includes("analytic") || g.includes("performance") || g.includes("pnl") || g.includes("p&l")) {
|
|
389
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
389
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json history summary`, description: "Trading performance summary" }, { step: 2, command: `perp -e ${ex} --json history pnl`, description: "P&L breakdown" }, { step: 3, command: `perp -e ${ex} --json history funding`, description: "Funding payment history" });
|
|
390
390
|
}
|
|
391
391
|
else if (g.includes("history") || g.includes("log") || g.includes("audit")) {
|
|
392
392
|
steps.push({ step: 1, command: `perp -e ${ex} --json history list`, description: "Execution audit trail" }, { step: 2, command: `perp -e ${ex} --json history stats`, description: "Execution statistics" });
|
|
393
393
|
}
|
|
394
|
-
else if (g.includes("
|
|
394
|
+
else if (g.includes("live") || g.includes("watch") || g.includes("realtime")) {
|
|
395
395
|
const symbol = extractSymbol(g);
|
|
396
396
|
if (symbol) {
|
|
397
|
-
steps.push({ step: 1, command: `perp -e ${ex}
|
|
397
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `${symbol} orderbook` }, { step: 2, command: `perp -e ${ex} --json market trades ${symbol}`, description: `${symbol} recent trades` }, { step: 3, command: `perp --json arb scan --gaps --live`, description: "Live cross-exchange price gap monitor" });
|
|
398
398
|
}
|
|
399
399
|
else {
|
|
400
|
-
steps.push({ step: 1, command:
|
|
400
|
+
steps.push({ step: 1, command: "perp --json arb scan --gaps --live", description: "Live cross-exchange price gap monitor" }, { step: 2, command: `perp -e ${ex} --json market prices`, description: "Current prices across exchanges" });
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
else if (g.includes("gap")) {
|
|
404
|
-
steps.push({ step: 1, command: "perp --json
|
|
404
|
+
steps.push({ step: 1, command: "perp --json arb scan --gaps", description: "Cross-exchange price gaps" }, { step: 2, command: "perp --json arb scan --gaps --live", description: "Live gap monitor" });
|
|
405
405
|
}
|
|
406
406
|
else if (g.includes("rebalance")) {
|
|
407
407
|
steps.push({ step: 1, command: "perp --json rebalance check", description: "Check balance distribution" }, { step: 2, command: "perp --json rebalance plan", description: "Generate rebalance plan" }, { step: 3, command: "perp --json rebalance execute", description: "Execute rebalance", dangerous: true });
|
|
@@ -446,7 +446,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
446
446
|
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `${symbol} orderbook` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` }, { step: 3, command: `perp -e ${ex} --json market kline ${symbol} 1h`, description: `${symbol} hourly candles` });
|
|
447
447
|
}
|
|
448
448
|
else {
|
|
449
|
-
steps.push({ step: 1, command: "perp --json market prices", description: "All market prices" }, { step: 2, command: "perp --json
|
|
449
|
+
steps.push({ step: 1, command: "perp --json market prices", description: "All market prices" }, { step: 2, command: "perp --json arb scan --gaps", description: "Cross-exchange price gaps" });
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
452
|
else if (g.includes("setting") || g.includes("config") || g.includes("referral")) {
|
|
@@ -708,6 +708,8 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
708
708
|
history: "Shows order history (filled, cancelled, etc.)",
|
|
709
709
|
trades: "Shows trade execution history with prices and fees",
|
|
710
710
|
"funding-history": "Shows funding payments received/paid",
|
|
711
|
+
funding: "Shows funding payments received/paid (alias for funding-history)",
|
|
712
|
+
"twap-orders": "Shows active TWAP orders",
|
|
711
713
|
pnl: "Shows profit & loss summary",
|
|
712
714
|
margin: `Shows margin info for ${args[2] || "a symbol"}`,
|
|
713
715
|
settings: "Shows account settings (leverage, margin mode per symbol)",
|
|
@@ -715,26 +717,26 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
715
717
|
parameters: [],
|
|
716
718
|
risks: [],
|
|
717
719
|
category: "read",
|
|
718
|
-
relatedCommands: ["perp portfolio", "perp
|
|
720
|
+
relatedCommands: ["perp portfolio", "perp account balance"],
|
|
719
721
|
};
|
|
720
722
|
}
|
|
721
723
|
else if (category === "arb") {
|
|
722
724
|
explanation = {
|
|
723
725
|
command,
|
|
724
726
|
description: {
|
|
725
|
-
|
|
726
|
-
scan: "Scans for funding rate arbitrage opportunities with the largest spreads",
|
|
727
|
-
funding: "Detailed funding rate analysis across exchanges",
|
|
728
|
-
dex: "HIP-3 cross-dex arbitrage opportunities on Hyperliquid",
|
|
727
|
+
scan: "Scans for funding rate arbitrage opportunities. Use --rates for funding rates, --gaps for price gaps, --hip3 for cross-dex arb, --positions for funding impact, --basis for basis trading",
|
|
729
728
|
auto: "Auto-execute funding rate arbitrage (runs as background job)",
|
|
729
|
+
exec: "Execute an arb trade (enter paired positions on two exchanges)",
|
|
730
730
|
status: "Shows current arb positions and P&L",
|
|
731
731
|
close: "Closes an arb position on both exchanges",
|
|
732
732
|
history: "Shows arb execution history",
|
|
733
|
+
config: "View/edit arb configuration",
|
|
734
|
+
rebalance: "Rebalance funds across exchanges for arb positions",
|
|
733
735
|
}[sub] || `Arbitrage command: ${sub}`,
|
|
734
736
|
parameters: [],
|
|
735
|
-
risks: ["auto"
|
|
736
|
-
category: sub === "auto" || sub === "close" ? "write" : "analysis",
|
|
737
|
-
relatedCommands: ["perp arb scan", "perp arb
|
|
737
|
+
risks: [sub === "auto" || sub === "exec" ? "Executes trades on exchanges" : ""].filter(Boolean),
|
|
738
|
+
category: sub === "auto" || sub === "close" || sub === "exec" || sub === "rebalance" ? "write" : "analysis",
|
|
739
|
+
relatedCommands: ["perp arb scan", "perp arb status", "perp arb scan --gaps"],
|
|
738
740
|
};
|
|
739
741
|
}
|
|
740
742
|
else if (category === "bridge") {
|
|
@@ -774,16 +776,6 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
774
776
|
relatedCommands: ["perp account positions", "perp portfolio"],
|
|
775
777
|
};
|
|
776
778
|
}
|
|
777
|
-
else if (category === "alert") {
|
|
778
|
-
explanation = {
|
|
779
|
-
command,
|
|
780
|
-
description: { add: "Add a price, funding rate, PnL, or liquidation alert", list: "List all active alerts", remove: "Remove an alert by ID", daemon: "Start the alert monitoring daemon", test: "Send a test alert notification", config: "Configure alert notification channels (Telegram, Discord)" }[sub] || `Alert command: ${sub}`,
|
|
781
|
-
parameters: [],
|
|
782
|
-
risks: [],
|
|
783
|
-
category: "read",
|
|
784
|
-
relatedCommands: ["perp risk status", "perp risk limits"],
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
779
|
else if (category === "bot" || category === "run") {
|
|
788
780
|
explanation = {
|
|
789
781
|
command,
|
|
@@ -797,9 +789,9 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
797
789
|
else if (category === "history") {
|
|
798
790
|
explanation = {
|
|
799
791
|
command,
|
|
800
|
-
description: { summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report", perf: "Performance breakdown (daily/weekly/summary)" }[sub] || `History: ${sub}`,
|
|
792
|
+
description: { list: "Execution audit trail", stats: "Execution statistics", positions: "Position history", summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report", snapshot: "Portfolio snapshot", track: "Track position over time", perf: "Performance breakdown (daily/weekly/summary)", prune: "Prune old execution records" }[sub] || `History: ${sub}`,
|
|
801
793
|
parameters: [],
|
|
802
|
-
risks: [],
|
|
794
|
+
risks: sub === "prune" ? ["Permanently removes old execution records"] : [],
|
|
803
795
|
category: "read",
|
|
804
796
|
relatedCommands: ["perp history summary", "perp history list"],
|
|
805
797
|
};
|
|
@@ -807,21 +799,11 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
807
799
|
else if (category === "wallet") {
|
|
808
800
|
explanation = {
|
|
809
801
|
command,
|
|
810
|
-
description: { list: "List configured wallets", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new wallet keypair", use: "Set active wallet" }[sub] || `Wallet: ${sub}`,
|
|
802
|
+
description: { show: "Show configured wallets with public addresses", list: "List configured wallets", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new wallet keypair", import: "Import an existing private key", set: "Set private key for an exchange", use: "Set active wallet", remove: "Remove a wallet", rename: "Rename a wallet" }[sub] || `Wallet: ${sub}`,
|
|
811
803
|
parameters: [],
|
|
812
|
-
risks: sub === "generate" ? ["Store the private key securely — it cannot be recovered"] : [],
|
|
804
|
+
risks: sub === "generate" ? ["Store the private key securely — it cannot be recovered"] : sub === "remove" ? ["Wallet will be removed — ensure you have the private key backed up"] : [],
|
|
813
805
|
category: "read",
|
|
814
|
-
relatedCommands: ["perp wallet
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
else if (category === "gap") {
|
|
818
|
-
explanation = {
|
|
819
|
-
command,
|
|
820
|
-
description: { show: "Shows cross-exchange price gaps for all symbols", watch: "Live monitor for price gaps above threshold", track: "Track gap history", alert: "Alert when gaps exceed threshold" }[sub] || `Gap analysis: ${sub}`,
|
|
821
|
-
parameters: [],
|
|
822
|
-
risks: [],
|
|
823
|
-
category: "analysis",
|
|
824
|
-
relatedCommands: ["perp gap show", "perp arb scan"],
|
|
806
|
+
relatedCommands: ["perp wallet show", "perp wallet balance"],
|
|
825
807
|
};
|
|
826
808
|
}
|
|
827
809
|
else if (category === "backtest") {
|
|
@@ -976,7 +958,8 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
976
958
|
orders: { usage: "perp account orders", description: "Open/pending orders" },
|
|
977
959
|
history: { usage: "perp account history", description: "Order history" },
|
|
978
960
|
trades: { usage: "perp account trades", description: "Trade fill history" },
|
|
979
|
-
"funding-history": { usage: "perp account funding-history", description: "Funding payments" },
|
|
961
|
+
"funding-history|funding": { usage: "perp account funding-history", description: "Funding payments" },
|
|
962
|
+
"twap-orders": { usage: "perp account twap-orders", description: "Active TWAP orders" },
|
|
980
963
|
pnl: { usage: "perp account pnl", description: "Profit & loss" },
|
|
981
964
|
margin: { usage: "perp account margin <symbol>", description: "Position margin info" },
|
|
982
965
|
settings: { usage: "perp account settings", description: "Account settings (leverage per symbol)" },
|
|
@@ -1007,19 +990,23 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1007
990
|
fills: { usage: "perp trade fills [symbol]", description: "Recent fills" },
|
|
1008
991
|
status: { usage: "perp trade status <orderId>", description: "Check order status" },
|
|
1009
992
|
"pnl-track": { usage: "perp trade pnl-track", description: "Real-time PnL tracker" },
|
|
993
|
+
split: { usage: "perp trade split <symbol> <side> <usd>", description: "Split large order into depth-based slices" },
|
|
994
|
+
multi: { usage: "perp trade multi <legs...>", description: "Execute multi-leg orders (exchange:symbol:side:size)" },
|
|
995
|
+
"cancel-stop": { usage: "perp trade cancel-stop <symbol> <stopOrderId>", description: "Cancel a stop order" },
|
|
996
|
+
"cancel-twap": { usage: "perp trade cancel-twap <symbol> <twapOrderId>", description: "Cancel a TWAP order" },
|
|
1010
997
|
},
|
|
1011
998
|
},
|
|
1012
999
|
arb: {
|
|
1013
1000
|
description: "Funding rate arbitrage & basis trading",
|
|
1014
1001
|
subcommands: {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
dex: { usage: "perp arb dex", description: "HIP-3 cross-dex arb" },
|
|
1019
|
-
auto: { usage: "perp arb auto --min-spread <bps>", description: "Auto-execute funding arb" },
|
|
1002
|
+
scan: { usage: "perp arb scan --min <pct>", description: "Find arb opportunities (use --rates, --gaps, --hip3, --positions, --basis, --compare, --history, --live)" },
|
|
1003
|
+
auto: { usage: "perp arb auto --min-spread <pct>", description: "Auto-execute funding arb" },
|
|
1004
|
+
exec: { usage: "perp arb exec <symbol> <longEx> <shortEx> <size>", description: "Execute arb trade on two exchanges" },
|
|
1020
1005
|
status: { usage: "perp arb status", description: "Current arb positions" },
|
|
1021
1006
|
close: { usage: "perp arb close <symbol>", description: "Close arb position" },
|
|
1022
|
-
history: { usage: "perp arb history", description: "Arb execution history" },
|
|
1007
|
+
"history|log": { usage: "perp arb history", description: "Arb execution history" },
|
|
1008
|
+
config: { usage: "perp arb config", description: "View/edit arb configuration" },
|
|
1009
|
+
rebalance: { usage: "perp arb rebalance", description: "Rebalance funds across exchanges for arb" },
|
|
1023
1010
|
},
|
|
1024
1011
|
},
|
|
1025
1012
|
risk: {
|
|
@@ -1043,32 +1030,48 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1043
1030
|
bot: {
|
|
1044
1031
|
description: "Automated trading bots",
|
|
1045
1032
|
subcommands: {
|
|
1033
|
+
start: { usage: "perp bot start <config>", description: "Start bot from config file" },
|
|
1046
1034
|
twap: { usage: "perp bot twap <symbol> <side> <size> <duration>", description: "TWAP execution" },
|
|
1047
1035
|
grid: { usage: "perp bot grid <symbol> --range <pct> --grids <n> --size <usd>", description: "Grid trading bot" },
|
|
1048
1036
|
dca: { usage: "perp bot dca <symbol> <side> <amount> <interval>", description: "DCA bot" },
|
|
1037
|
+
"funding-arb": { usage: "perp bot funding-arb", description: "Funding arb bot" },
|
|
1049
1038
|
"trailing-stop": { usage: "perp bot trailing-stop <symbol>", description: "Trailing stop bot" },
|
|
1050
1039
|
"quick-grid": { usage: "perp bot quick-grid <symbol>", description: "Quick grid bot" },
|
|
1051
1040
|
"quick-dca": { usage: "perp bot quick-dca <symbol> <side> <amount> <interval>", description: "Quick DCA bot" },
|
|
1052
1041
|
"quick-arb": { usage: "perp bot quick-arb", description: "Quick arb bot" },
|
|
1042
|
+
"preset-list": { usage: "perp bot preset-list", description: "List available bot presets" },
|
|
1043
|
+
preset: { usage: "perp bot preset <name> [symbol]", description: "Run a bot preset" },
|
|
1044
|
+
example: { usage: "perp bot example", description: "Show example bot config" },
|
|
1053
1045
|
},
|
|
1054
1046
|
},
|
|
1055
1047
|
wallet: {
|
|
1056
1048
|
description: "Wallet management",
|
|
1057
1049
|
subcommands: {
|
|
1050
|
+
show: { usage: "perp wallet show", description: "Show configured wallets with public addresses" },
|
|
1058
1051
|
list: { usage: "perp wallet list", description: "List wallets" },
|
|
1059
|
-
balance: { usage: "perp wallet balance", description: "On-chain balances" },
|
|
1052
|
+
balance: { usage: "perp wallet balance [name]", description: "On-chain balances" },
|
|
1060
1053
|
generate: { usage: "perp wallet generate solana|evm", description: "Generate new wallet" },
|
|
1054
|
+
import: { usage: "perp wallet import solana|evm <privateKey>", description: "Import existing private key" },
|
|
1055
|
+
set: { usage: "perp wallet set <exchange> <key>", description: "Set private key for exchange" },
|
|
1056
|
+
use: { usage: "perp wallet use <name> [exchange]", description: "Set active wallet" },
|
|
1057
|
+
remove: { usage: "perp wallet remove <name>", description: "Remove a wallet" },
|
|
1058
|
+
rename: { usage: "perp wallet rename <oldName> <newName>", description: "Rename a wallet" },
|
|
1061
1059
|
},
|
|
1062
1060
|
},
|
|
1063
1061
|
history: {
|
|
1064
1062
|
description: "Execution log, performance & audit trail",
|
|
1065
1063
|
subcommands: {
|
|
1066
1064
|
list: { usage: "perp history list", description: "Execution log" },
|
|
1065
|
+
stats: { usage: "perp history stats", description: "Execution statistics" },
|
|
1066
|
+
positions: { usage: "perp history positions", description: "Position history" },
|
|
1067
1067
|
summary: { usage: "perp history summary", description: "Performance summary" },
|
|
1068
1068
|
pnl: { usage: "perp history pnl", description: "P&L breakdown" },
|
|
1069
1069
|
funding: { usage: "perp history funding", description: "Funding payment history" },
|
|
1070
1070
|
report: { usage: "perp history report", description: "Full performance report" },
|
|
1071
|
+
snapshot: { usage: "perp history snapshot", description: "Portfolio snapshot" },
|
|
1072
|
+
track: { usage: "perp history track", description: "Track position over time" },
|
|
1071
1073
|
perf: { usage: "perp history perf --period daily|weekly|summary", description: "Performance breakdown" },
|
|
1074
|
+
prune: { usage: "perp history prune", description: "Prune old execution records" },
|
|
1072
1075
|
},
|
|
1073
1076
|
},
|
|
1074
1077
|
backtest: {
|
package/dist/utils.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function formatUsd(value: string | number): string;
|
|
|
3
3
|
export declare function formatPnl(value: string | number): string;
|
|
4
4
|
export declare function formatPercent(value: string | number): string;
|
|
5
5
|
export declare function makeTable(head: string[], rows: string[][]): string;
|
|
6
|
+
/** Render a sparkline from numeric values. Returns unicode bar chart string. */
|
|
7
|
+
export declare function sparkline(values: number[], width?: number): string;
|
|
6
8
|
export declare function printJson(data: unknown): void;
|
|
7
9
|
export declare function errorAndExit(msg: string): never;
|
|
8
10
|
/** Standard JSON response envelope */
|
package/dist/utils.js
CHANGED
|
@@ -96,6 +96,38 @@ function sanitizeValue(val) {
|
|
|
96
96
|
}
|
|
97
97
|
return val;
|
|
98
98
|
}
|
|
99
|
+
// ── Sparkline ──
|
|
100
|
+
// Density-based shading — renders consistently across all terminal fonts
|
|
101
|
+
// (height-based ▁▂▃▄▅▆▇█ looks identical in many monospace fonts)
|
|
102
|
+
const SPARK_CHARS = [" ", "░", "▒", "▓", "█"];
|
|
103
|
+
/** Render a sparkline from numeric values. Returns unicode bar chart string. */
|
|
104
|
+
export function sparkline(values, width) {
|
|
105
|
+
if (values.length === 0)
|
|
106
|
+
return "";
|
|
107
|
+
const data = width && values.length > width ? downsample(values, width) : values;
|
|
108
|
+
const min = Math.min(...data);
|
|
109
|
+
const max = Math.max(...data);
|
|
110
|
+
const range = max - min;
|
|
111
|
+
if (range === 0)
|
|
112
|
+
return SPARK_CHARS[2].repeat(data.length);
|
|
113
|
+
return data.map(v => {
|
|
114
|
+
const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
|
|
115
|
+
return SPARK_CHARS[idx];
|
|
116
|
+
}).join("");
|
|
117
|
+
}
|
|
118
|
+
function downsample(values, targetLen) {
|
|
119
|
+
const bucketSize = values.length / targetLen;
|
|
120
|
+
const result = [];
|
|
121
|
+
for (let i = 0; i < targetLen; i++) {
|
|
122
|
+
const start = Math.floor(i * bucketSize);
|
|
123
|
+
const end = Math.floor((i + 1) * bucketSize);
|
|
124
|
+
let sum = 0;
|
|
125
|
+
for (let j = start; j < end; j++)
|
|
126
|
+
sum += values[j];
|
|
127
|
+
result.push(sum / (end - start));
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
99
131
|
// ── JSON Output ──
|
|
100
132
|
const _isNdjson = () => process.argv.includes("--ndjson");
|
|
101
133
|
export function printJson(data) {
|