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.
package/dist/commands/account.js
CHANGED
|
@@ -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
|
-
.
|
|
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(
|
|
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:
|
|
298
|
-
let
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
338
|
-
netPnl: shortPos.unrealizedPnl + estimatedFundingIncome -
|
|
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
|
|
454
|
+
// ── Spot-Perp detailed view ──
|
|
445
455
|
if (spotPerpPositions.length > 0) {
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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 =
|
|
706
|
+
async getFundingPayments(limit = 200) {
|
|
707
707
|
this.ensureAddress();
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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 =
|
|
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 =
|
|
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);
|