perp-cli 0.6.2 → 0.6.4

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.
@@ -687,9 +687,10 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
687
687
  .command("funding-history")
688
688
  .alias("funding")
689
689
  .description("Personal funding payment history")
690
- .action(async () => {
690
+ .option("-n, --limit <n>", "Number of records", "200")
691
+ .action(async (opts) => {
691
692
  const adapter = await getAdapter();
692
- const payments = await adapter.getFundingPayments(30);
693
+ const payments = await adapter.getFundingPayments(parseInt(opts.limit));
693
694
  if (isJson())
694
695
  return printJson(jsonOk(payments));
695
696
  if (payments.length === 0) {
@@ -964,12 +965,28 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
964
965
  "",
965
966
  ]);
966
967
  console.log(makeTable(["Exchange", "Equity", "Available", "Margin Used", "uPnL", "Status"], balRows));
967
- // ── Positions ──
968
+ // ── Positions + Funding ──
969
+ // Fetch funding per exchange in parallel
970
+ const fundingByExSym = new Map();
971
+ await Promise.allSettled(exchanges.map(async (exName) => {
972
+ try {
973
+ const adapter = await getAdapterForExchange(exName);
974
+ const payments = await adapter.getFundingPayments(200);
975
+ for (const p of payments) {
976
+ const sym = p.symbol.replace("-PERP", "").toUpperCase();
977
+ const key = `${exName}:${sym}`;
978
+ fundingByExSym.set(key, (fundingByExSym.get(key) ?? 0) + Number(p.payment));
979
+ }
980
+ }
981
+ catch { /* skip */ }
982
+ }));
968
983
  if (summary.positions.length > 0) {
969
984
  console.log(chalk.white.bold("\n Open Positions"));
970
985
  const posRows = summary.positions.map(p => {
971
986
  const sideColor = p.side === "long" ? chalk.green : chalk.red;
972
987
  const notional = Math.abs(Number(p.size) * Number(p.markPrice));
988
+ const sym = p.symbol.replace("-PERP", "").toUpperCase();
989
+ const funding = fundingByExSym.get(`${p.exchange}:${sym}`) ?? 0;
973
990
  return [
974
991
  chalk.white.bold(p.symbol),
975
992
  chalk.gray(p.exchange),
@@ -978,17 +995,19 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
978
995
  `$${formatUsd(p.entryPrice)}`,
979
996
  `$${formatUsd(p.markPrice)}`,
980
997
  formatPnl(p.unrealizedPnl),
998
+ formatPnl(funding),
981
999
  `$${formatUsd(notional)}`,
982
1000
  `${p.leverage}x`,
983
1001
  ];
984
1002
  });
985
- console.log(makeTable(["Symbol", "Exchange", "Side", "Size", "Entry", "Mark", "uPnL", "Notional", "Lev"], posRows));
1003
+ console.log(makeTable(["Symbol", "Exchange", "Side", "Size", "Entry", "Mark", "uPnL", "Funding", "Notional", "Lev"], posRows));
986
1004
  }
987
1005
  else {
988
1006
  console.log(chalk.gray("\n No open positions.\n"));
989
1007
  }
990
1008
  // ── Spot Holdings ──
991
1009
  const spotRows = [];
1010
+ let totalSpotValue = 0;
992
1011
  for (const exName of exchanges) {
993
1012
  try {
994
1013
  const adapter = await getAdapterForExchange(exName);
@@ -1007,6 +1026,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
1007
1026
  const value = price * Number(b.total);
1008
1027
  if (value < 1)
1009
1028
  continue; // skip dust
1029
+ totalSpotValue += value;
1010
1030
  spotRows.push([
1011
1031
  chalk.white.bold(base),
1012
1032
  chalk.gray("hyperliquid"),
@@ -1031,6 +1051,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
1031
1051
  const value = price * Number(b.total);
1032
1052
  if (value < 1)
1033
1053
  continue;
1054
+ totalSpotValue += value;
1034
1055
  spotRows.push([
1035
1056
  chalk.white.bold(b.token),
1036
1057
  chalk.gray("lighter"),
@@ -1047,6 +1068,14 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
1047
1068
  console.log(chalk.white.bold("\n Spot Holdings"));
1048
1069
  console.log(makeTable(["Token", "Exchange", "Amount", "Price", "Value"], spotRows));
1049
1070
  }
1071
+ // ── Total (perp + spot) ──
1072
+ if (totalSpotValue > 0) {
1073
+ const totalValue = summary.totalEquity + totalSpotValue;
1074
+ console.log(chalk.white.bold(" Total Account Value"));
1075
+ console.log(` Perp Equity: $${formatUsd(summary.totalEquity)}`);
1076
+ console.log(` Spot Value: $${formatUsd(totalSpotValue)}`);
1077
+ console.log(` ${chalk.cyan.bold(`Total: $${formatUsd(totalValue)}`)}`);
1078
+ }
1050
1079
  // ── Risk Metrics ──
1051
1080
  console.log(chalk.white.bold("\n Risk Metrics"));
1052
1081
  const levelColor = {
@@ -294,24 +294,34 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
294
294
  const annualPct = hourlyRate * 8760 * 100;
295
295
  const perpNotional = shortPos.size * shortPos.markPrice;
296
296
  const spotValueUsd = spotAmount * shortPos.markPrice; // approx using perp mark price
297
- // Funding income: spot has 0 funding, perp short receives when rate > 0
298
- let estimatedFundingIncome = 0;
299
- if (holdDurationMs && hourlyRate !== 0) {
300
- const holdHours = holdDurationMs / (1000 * 60 * 60);
301
- // Short position: pays when rate < 0, receives when rate > 0
302
- estimatedFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
303
- if (hourlyRate < 0)
304
- estimatedFundingIncome = -estimatedFundingIncome;
297
+ // Funding income: fetch actual payments from exchange API
298
+ let actualFundingIncome = 0;
299
+ try {
300
+ const perpAdapter = await getAdapterForExchange(shortPos.exchange);
301
+ const payments = await perpAdapter.getFundingPayments(200);
302
+ // Filter from entry time minus 1h buffer (multi-leg entry takes time)
303
+ const entryTime = persisted ? new Date(persisted.entryTime).getTime() - 3600000 : 0;
304
+ for (const p of payments) {
305
+ if (p.symbol.toUpperCase().includes(perpSymbol) && (!entryTime || p.time >= entryTime)) {
306
+ actualFundingIncome += Number(p.payment);
307
+ }
308
+ }
305
309
  }
306
- // Add persisted accumulated funding
307
- if (persisted?.accumulatedFunding) {
308
- estimatedFundingIncome += persisted.accumulatedFunding;
310
+ catch {
311
+ // Fallback to estimate if API fails
312
+ if (holdDurationMs && hourlyRate !== 0) {
313
+ const holdHours = holdDurationMs / (1000 * 60 * 60);
314
+ actualFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
315
+ if (hourlyRate < 0)
316
+ actualFundingIncome = -actualFundingIncome;
317
+ }
309
318
  }
310
- // Fees: spot buy + spot sell + perp short entry + perp buy to close
319
+ const estimatedFundingIncome = actualFundingIncome;
320
+ // Fees: entry only (exit not yet paid)
311
321
  const { getTakerFee } = await import("../../constants.js");
312
322
  const spotFee = getTakerFee(spotEx);
313
323
  const perpFee = getTakerFee(shortPos.exchange);
314
- const totalFees = (spotValueUsd * spotFee * 2) + (perpNotional * perpFee * 2);
324
+ const entryFees = (spotValueUsd * spotFee) + (perpNotional * perpFee);
315
325
  const direction = hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp";
316
326
  const dailyFundingEstimate = Math.abs(hourlyRate) * perpNotional * 24;
317
327
  spotPerpPositions.push({
@@ -334,8 +344,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
334
344
  holdDuration,
335
345
  holdDurationMs,
336
346
  estimatedFundingIncome,
337
- estimatedFees: totalFees,
338
- netPnl: shortPos.unrealizedPnl + estimatedFundingIncome - totalFees,
347
+ estimatedFees: entryFees,
348
+ netPnl: (spotValueUsd - shortPos.entryPrice * spotAmount) + shortPos.unrealizedPnl + estimatedFundingIncome - entryFees,
339
349
  dailyFundingEstimate,
340
350
  });
341
351
  }
@@ -441,29 +451,50 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
441
451
  });
442
452
  console.log(makeTable(["Symbol", "Long", "Short", "Size", "Entry", "Mark", "uPnL", "Funding", "Spread", "Basis", "Hold", "Net PnL"], rows));
443
453
  }
444
- // ── Spot-Perp table ──
454
+ // ── Spot-Perp detailed view ──
445
455
  if (spotPerpPositions.length > 0) {
446
- console.log(chalk.cyan.bold(" Spot-Perp Arb Positions\n"));
447
- const spRows = spotPerpPositions.map(sp => {
448
- const spotLabel = sp.spotToken !== sp.symbol
449
- ? `${sp.spotToken} (${sp.spotExchange})`
450
- : sp.spotExchange;
456
+ for (const sp of spotPerpPositions) {
457
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
451
458
  const spreadStr = sp.currentSpread !== null ? `${sp.currentSpread.toFixed(1)}%` : "-";
452
- return [
453
- chalk.white.bold(sp.symbol),
454
- chalk.green(`${sp.spotAmount.toFixed(4)} ${sp.spotToken}`),
455
- spotLabel,
456
- chalk.red(sp.perpExchange),
457
- `$${formatUsd(sp.perpNotionalUsd)}`,
458
- `$${sp.perpMarkPrice.toFixed(2)}`,
459
- formatPnl(sp.perpUnrealizedPnl),
460
- chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`),
461
- spreadStr,
462
- sp.holdDuration ?? "-",
463
- formatPnl(sp.netPnl),
459
+ const spotPriceEach = sp.spotAmount > 0 ? sp.spotValueUsd / sp.spotAmount : 0;
460
+ const spotPnl = sp.spotValueUsd - (sp.perpEntryPrice * sp.spotAmount); // spot value vs entry
461
+ const roi = sp.perpNotionalUsd > 0 ? (sp.netPnl / sp.perpNotionalUsd) * 100 : 0;
462
+ const dailyRoi = sp.holdDurationMs && sp.holdDurationMs > 0 ? roi / (sp.holdDurationMs / 86400000) : 0;
463
+ const annRoi = dailyRoi * 365;
464
+ console.log(chalk.cyan.bold(`\n ${sp.symbol} spot-perp`) + chalk.gray(` (${exAbbr(sp.spotExchange)} ↔ ${exAbbr(sp.perpExchange)})`) + chalk.gray(` ${sp.holdDuration ?? "-"}`));
465
+ console.log("");
466
+ // Leg details
467
+ const legRows = [
468
+ [
469
+ chalk.green("Spot"),
470
+ `${sp.spotAmount.toFixed(4)} ${sp.spotToken}`,
471
+ `$${sp.perpEntryPrice.toFixed(4)}`,
472
+ `$${spotPriceEach.toFixed(4)}`,
473
+ formatPnl(spotPnl),
474
+ ],
475
+ [
476
+ chalk.red("Perp"),
477
+ `${sp.perpSize.toFixed(4)} ${sp.symbol}`,
478
+ `$${sp.perpEntryPrice.toFixed(4)}`,
479
+ `$${sp.perpMarkPrice.toFixed(4)}`,
480
+ formatPnl(sp.perpUnrealizedPnl),
481
+ ],
464
482
  ];
465
- });
466
- console.log(makeTable(["Symbol", "Spot Held", "Spot Ex", "Perp Ex", "Size", "Mark", "uPnL", "Funding", "Spread", "Hold", "Net PnL"], spRows));
483
+ console.log(makeTable(["Leg", "Qty", "Entry", "Current", "PnL"], legRows));
484
+ // Summary
485
+ console.log(chalk.white.bold(" PnL Breakdown:"));
486
+ console.log(` Spot PnL: ${formatPnl(spotPnl)}`);
487
+ console.log(` Perp PnL: ${formatPnl(sp.perpUnrealizedPnl)}`);
488
+ console.log(` Funding Earned: ${chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`)}`);
489
+ console.log(` Trading Fees: ${chalk.red(`-$${sp.estimatedFees.toFixed(4)}`)}`);
490
+ console.log(` ${chalk.white.bold("Total PnL:")} ${formatPnl(sp.netPnl)}${roi !== 0 ? chalk.gray(` (ROI: ${roi >= 0 ? "+" : ""}${roi.toFixed(2)}%)`) : ""}`);
491
+ console.log("");
492
+ console.log(` Current Spread: ${spreadStr}`);
493
+ console.log(` Daily Income: ${chalk.cyan(`$${sp.dailyFundingEstimate.toFixed(4)}/day`)}`);
494
+ if (annRoi !== 0)
495
+ console.log(` Annualized ROI: ${chalk.cyan(`${annRoi >= 0 ? "+" : ""}${annRoi.toFixed(1)}%`)}`);
496
+ }
497
+ console.log("");
467
498
  }
468
499
  // ── Combined Summary ──
469
500
  const totalUpnl = arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0)
@@ -703,18 +703,21 @@ export class HyperliquidAdapter {
703
703
  fee: String(f.fee ?? "0"),
704
704
  }));
705
705
  }
706
- async getFundingPayments(limit = 30) {
706
+ async getFundingPayments(limit = 200) {
707
707
  this.ensureAddress();
708
- const now = Date.now();
709
- const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
710
- return (history ?? []).slice(0, limit).map((h) => {
711
- const delta = (h.delta ?? {});
712
- return {
713
- time: Number(h.time ?? 0),
714
- symbol: String(delta.coin ?? ""),
715
- payment: String(delta.usdc ?? "0"),
716
- };
708
+ // Direct API call — SDK's getUserFunding silently truncates results
709
+ const apiUrl = this._testnet ? "https://api.hyperliquid-testnet.xyz/info" : "https://api.hyperliquid.xyz/info";
710
+ const res = await fetch(apiUrl, {
711
+ method: "POST",
712
+ headers: { "Content-Type": "application/json" },
713
+ body: JSON.stringify({ type: "userFunding", user: this._address, startTime: Date.now() - 7 * 24 * 60 * 60 * 1000 }),
717
714
  });
715
+ const data = await res.json();
716
+ return (data ?? []).slice(0, limit).map((h) => ({
717
+ time: h.time,
718
+ symbol: h.delta.coin + "-PERP",
719
+ payment: h.delta.usdc,
720
+ }));
718
721
  }
719
722
  async getFundingHistory(symbol, limit = 10) {
720
723
  const now = Date.now();
@@ -669,7 +669,7 @@ export class LighterAdapter {
669
669
  fee: String(t.fee ?? "0"),
670
670
  }));
671
671
  }
672
- async getFundingPayments(limit = 30) {
672
+ async getFundingPayments(limit = 200) {
673
673
  if (!this._address)
674
674
  throw new Error("No private key configured — account data unavailable. Run: perp init");
675
675
  const raw = await this._getPositionFundingRaw();
@@ -677,7 +677,7 @@ export class LighterAdapter {
677
677
  return items.slice(0, limit).map((f) => ({
678
678
  time: Number(f.timestamp ?? 0) * 1000,
679
679
  symbol: String(f.symbol ?? ""),
680
- payment: String(f.amount ?? f.payment ?? "0"),
680
+ payment: String(f.change ?? f.amount ?? f.payment ?? "0"),
681
681
  }));
682
682
  }
683
683
  /**
@@ -799,8 +799,10 @@ export class LighterAdapter {
799
799
  return this.restGetAuth("/positionFunding", {
800
800
  account_index: String(this._accountIndex),
801
801
  market_id: String(marketId),
802
+ limit: "100",
803
+ side: "all",
802
804
  }).then((res) => {
803
- const items = (res.funding ?? res.data ?? []);
805
+ const items = (res.position_fundings ?? res.funding ?? res.data ?? []);
804
806
  return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
805
807
  });
806
808
  }));
@@ -281,7 +281,7 @@ export class PacificaAdapter {
281
281
  fee: String(t.fee ?? "0"),
282
282
  }));
283
283
  }
284
- async getFundingPayments(limit = 30) {
284
+ async getFundingPayments(limit = 200) {
285
285
  if (!this._hasRealKey)
286
286
  throw new Error("No private key configured — account data unavailable. Run: perp init");
287
287
  const raw = await this.client.getFundingAccountHistory(this.account);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",