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.
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) {
|
|
@@ -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:
|
|
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);
|