perp-cli 0.6.2 → 0.6.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.
@@ -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) {
@@ -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.3",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",