perp-cli 0.4.2 → 0.4.9
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 +46 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +300 -63
- package/dist/commands/agent.js +16 -9
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +696 -9
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +6 -4
- package/dist/commands/market.js +35 -2
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +270 -267
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +11 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +21 -1
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
- package/dist/exchanges/hyperliquid-spot.js +309 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +171 -73
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +415 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +210 -56
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +64 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +126 -161
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/mcp-server.js +43 -96
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.js +14 -3
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +80 -4
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +54 -32
- package/skills/perp-cli/references/strategies.md +27 -17
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
|
|
3
3
|
import { readExecutionLog, logExecution } from "../../execution-log.js";
|
|
4
|
-
import { computeAnnualSpread } from "../../funding.js";
|
|
4
|
+
import { toHourlyRate, computeAnnualSpread } from "../../funding.js";
|
|
5
5
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
|
|
6
6
|
import { computeBasisRisk } from "../../arb-utils.js";
|
|
7
7
|
import { smartOrder } from "../../smart-order.js";
|
|
8
|
-
import { removePosition as persistRemovePosition } from "../../arb-state.js";
|
|
8
|
+
import { removePosition as persistRemovePosition, getPositions as getArbStatePositions } from "../../arb-state.js";
|
|
9
|
+
import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/spot-interface.js";
|
|
9
10
|
import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
|
|
10
11
|
import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
|
|
11
12
|
import { computeEnhancedStats } from "../../arb-history-stats.js";
|
|
@@ -89,7 +90,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
89
90
|
// ── arb status ──
|
|
90
91
|
arb
|
|
91
92
|
.command("status")
|
|
92
|
-
.description("Show open arb positions with PnL breakdown")
|
|
93
|
+
.description("Show open arb positions (perp-perp + spot-perp) with PnL breakdown")
|
|
93
94
|
.action(async () => {
|
|
94
95
|
if (!isJson())
|
|
95
96
|
console.log(chalk.cyan("\n Checking arb positions across exchanges...\n"));
|
|
@@ -218,66 +219,259 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
}
|
|
222
|
+
// ── Spot-Perp position detection ──
|
|
223
|
+
// Fetch spot balances from HL and LT, match with perp shorts
|
|
224
|
+
const spotPerpPositions = [];
|
|
225
|
+
// Build reverse map: spot token → perp symbol (UETH→ETH, UBTC→BTC, etc.)
|
|
226
|
+
const spotToPerpSymbol = (token) => {
|
|
227
|
+
const upper = token.toUpperCase();
|
|
228
|
+
return SPOT_PERP_TOKEN_MAP[upper] ?? upper;
|
|
229
|
+
};
|
|
230
|
+
// Collect spot balances per exchange
|
|
231
|
+
const spotBalancesByExchange = new Map();
|
|
232
|
+
const SPOT_EXCHANGES = ["hyperliquid", "lighter"];
|
|
233
|
+
for (const exName of SPOT_EXCHANGES) {
|
|
234
|
+
try {
|
|
235
|
+
const adapter = await getAdapterForExchange(exName);
|
|
236
|
+
if (exName === "hyperliquid") {
|
|
237
|
+
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
238
|
+
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
239
|
+
await hlSpot.init();
|
|
240
|
+
const balances = await hlSpot.getSpotBalances();
|
|
241
|
+
spotBalancesByExchange.set(exName, balances.filter(b => b.token !== "USDC" && Number(b.total) > 0));
|
|
242
|
+
}
|
|
243
|
+
else if (exName === "lighter") {
|
|
244
|
+
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
245
|
+
const ltSpot = new LighterSpotAdapter(adapter);
|
|
246
|
+
await ltSpot.init();
|
|
247
|
+
const balances = await ltSpot.getSpotBalances();
|
|
248
|
+
spotBalancesByExchange.set(exName, balances.filter(b => b.token !== "USDC" && b.token !== "USDC_SPOT" && Number(b.total) > 0));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// exchange not configured or no spot support
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Load persisted arb state for spot-perp entries
|
|
256
|
+
const persistedSpotPerp = getArbStatePositions().filter(p => p.mode === "spot-perp");
|
|
257
|
+
const persistedSymbols = new Set(persistedSpotPerp.map(p => p.symbol.toUpperCase()));
|
|
258
|
+
// Match: spot balance + perp short on same symbol (different or same exchange)
|
|
259
|
+
for (const [spotEx, balances] of spotBalancesByExchange) {
|
|
260
|
+
for (const bal of balances) {
|
|
261
|
+
const perpSymbol = spotToPerpSymbol(bal.token);
|
|
262
|
+
const spotAmount = Number(bal.total);
|
|
263
|
+
if (spotAmount <= 0)
|
|
264
|
+
continue;
|
|
265
|
+
// Find matching perp short position (on any exchange)
|
|
266
|
+
const matchingShorts = allPositions.filter(p => p.symbol === perpSymbol && p.side === "short");
|
|
267
|
+
for (const shortPos of matchingShorts) {
|
|
268
|
+
// Check if this short is already used by a perp-perp pair
|
|
269
|
+
const usedByPerpPerp = arbPairs.some(ap => ap.symbol === perpSymbol && ap.shortExchange === shortPos.exchange);
|
|
270
|
+
if (usedByPerpPerp)
|
|
271
|
+
continue;
|
|
272
|
+
// Look up persisted state for hold time and entry info
|
|
273
|
+
const persisted = persistedSpotPerp.find(p => p.symbol.toUpperCase() === perpSymbol
|
|
274
|
+
&& p.spotExchange === spotEx
|
|
275
|
+
&& p.shortExchange === shortPos.exchange);
|
|
276
|
+
const entryTime = persisted?.entryTime ?? null;
|
|
277
|
+
const holdDurationMs = entryTime ? Date.now() - new Date(entryTime).getTime() : null;
|
|
278
|
+
const holdDuration = holdDurationMs ? formatDuration(holdDurationMs) : null;
|
|
279
|
+
// Get current perp funding rate for this symbol on this exchange
|
|
280
|
+
const rates = rateMap.get(perpSymbol);
|
|
281
|
+
const perpRate = rates?.find(r => r.exchange === shortPos.exchange);
|
|
282
|
+
const fundingRate = perpRate?.rate ?? 0;
|
|
283
|
+
const hourlyRate = fundingRate ? toHourlyRate(fundingRate, shortPos.exchange) : 0;
|
|
284
|
+
const annualPct = hourlyRate * 8760 * 100;
|
|
285
|
+
const perpNotional = shortPos.size * shortPos.markPrice;
|
|
286
|
+
const spotValueUsd = spotAmount * shortPos.markPrice; // approx using perp mark price
|
|
287
|
+
// Funding income: spot has 0 funding, perp short receives when rate > 0
|
|
288
|
+
let estimatedFundingIncome = 0;
|
|
289
|
+
if (holdDurationMs && hourlyRate !== 0) {
|
|
290
|
+
const holdHours = holdDurationMs / (1000 * 60 * 60);
|
|
291
|
+
// Short position: pays when rate < 0, receives when rate > 0
|
|
292
|
+
estimatedFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
|
|
293
|
+
if (hourlyRate < 0)
|
|
294
|
+
estimatedFundingIncome = -estimatedFundingIncome;
|
|
295
|
+
}
|
|
296
|
+
// Add persisted accumulated funding
|
|
297
|
+
if (persisted?.accumulatedFunding) {
|
|
298
|
+
estimatedFundingIncome += persisted.accumulatedFunding;
|
|
299
|
+
}
|
|
300
|
+
// Fees: spot buy + spot sell + perp short entry + perp buy to close
|
|
301
|
+
const { getTakerFee } = await import("../../constants.js");
|
|
302
|
+
const spotFee = getTakerFee(spotEx);
|
|
303
|
+
const perpFee = getTakerFee(shortPos.exchange);
|
|
304
|
+
const totalFees = (spotValueUsd * spotFee * 2) + (perpNotional * perpFee * 2);
|
|
305
|
+
const direction = hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp";
|
|
306
|
+
const dailyFundingEstimate = Math.abs(hourlyRate) * perpNotional * 24;
|
|
307
|
+
spotPerpPositions.push({
|
|
308
|
+
symbol: perpSymbol,
|
|
309
|
+
mode: "spot-perp",
|
|
310
|
+
spotExchange: spotEx,
|
|
311
|
+
perpExchange: shortPos.exchange,
|
|
312
|
+
spotToken: bal.token,
|
|
313
|
+
spotAmount,
|
|
314
|
+
spotValueUsd,
|
|
315
|
+
perpSide: "short",
|
|
316
|
+
perpSize: shortPos.size,
|
|
317
|
+
perpEntryPrice: shortPos.entryPrice,
|
|
318
|
+
perpMarkPrice: shortPos.markPrice,
|
|
319
|
+
perpUnrealizedPnl: shortPos.unrealizedPnl,
|
|
320
|
+
perpLeverage: shortPos.leverage,
|
|
321
|
+
perpNotionalUsd: perpNotional,
|
|
322
|
+
direction,
|
|
323
|
+
currentSpread: annualPct || null,
|
|
324
|
+
holdDuration,
|
|
325
|
+
holdDurationMs,
|
|
326
|
+
estimatedFundingIncome,
|
|
327
|
+
estimatedFees: totalFees,
|
|
328
|
+
netPnl: shortPos.unrealizedPnl + estimatedFundingIncome - totalFees,
|
|
329
|
+
dailyFundingEstimate,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Also detect persisted spot-perp entries without matching spot balance (already exited spot)
|
|
335
|
+
for (const persisted of persistedSpotPerp) {
|
|
336
|
+
const sym = persisted.symbol.toUpperCase();
|
|
337
|
+
const alreadyMatched = spotPerpPositions.some(sp => sp.symbol === sym && sp.spotExchange === persisted.spotExchange);
|
|
338
|
+
if (alreadyMatched)
|
|
339
|
+
continue;
|
|
340
|
+
// Check if there's still a perp short for this
|
|
341
|
+
const shortPos = allPositions.find(p => p.symbol === sym && p.side === "short" && p.exchange === persisted.shortExchange);
|
|
342
|
+
if (!shortPos)
|
|
343
|
+
continue; // both legs closed, skip
|
|
344
|
+
const holdDurationMs = Date.now() - new Date(persisted.entryTime).getTime();
|
|
345
|
+
const rates = rateMap.get(sym);
|
|
346
|
+
const perpRate = rates?.find(r => r.exchange === persisted.shortExchange);
|
|
347
|
+
const hourlyRate = perpRate ? toHourlyRate(perpRate.rate, persisted.shortExchange) : 0;
|
|
348
|
+
const perpNotional = shortPos.size * shortPos.markPrice;
|
|
349
|
+
spotPerpPositions.push({
|
|
350
|
+
symbol: sym,
|
|
351
|
+
mode: "spot-perp",
|
|
352
|
+
spotExchange: persisted.spotExchange ?? "unknown",
|
|
353
|
+
perpExchange: persisted.shortExchange,
|
|
354
|
+
spotToken: "?",
|
|
355
|
+
spotAmount: 0,
|
|
356
|
+
spotValueUsd: 0,
|
|
357
|
+
perpSide: "short",
|
|
358
|
+
perpSize: shortPos.size,
|
|
359
|
+
perpEntryPrice: shortPos.entryPrice,
|
|
360
|
+
perpMarkPrice: shortPos.markPrice,
|
|
361
|
+
perpUnrealizedPnl: shortPos.unrealizedPnl,
|
|
362
|
+
perpLeverage: shortPos.leverage,
|
|
363
|
+
perpNotionalUsd: perpNotional,
|
|
364
|
+
direction: hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp",
|
|
365
|
+
currentSpread: hourlyRate ? Math.abs(hourlyRate) * 8760 * 100 : null,
|
|
366
|
+
holdDuration: formatDuration(holdDurationMs),
|
|
367
|
+
holdDurationMs,
|
|
368
|
+
estimatedFundingIncome: persisted.accumulatedFunding ?? 0,
|
|
369
|
+
estimatedFees: 0,
|
|
370
|
+
netPnl: shortPos.unrealizedPnl + (persisted.accumulatedFunding ?? 0),
|
|
371
|
+
dailyFundingEstimate: Math.abs(hourlyRate) * perpNotional * 24,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
// ── JSON output ──
|
|
221
375
|
if (isJson()) {
|
|
222
|
-
const
|
|
223
|
-
const
|
|
376
|
+
const totalNotionalPP = arbPairs.reduce((s, p) => s + (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2, 0);
|
|
377
|
+
const totalDailyPP = arbPairs.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
378
|
+
const totalNotionalSP = spotPerpPositions.reduce((s, p) => s + p.perpNotionalUsd, 0);
|
|
379
|
+
const totalDailySP = spotPerpPositions.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
380
|
+
const totalNotionalJ = totalNotionalPP + totalNotionalSP;
|
|
381
|
+
const totalDailyJ = totalDailyPP + totalDailySP;
|
|
224
382
|
return printJson(jsonOk({
|
|
225
|
-
|
|
383
|
+
perpPerpPairs: arbPairs,
|
|
384
|
+
spotPerpPairs: spotPerpPositions,
|
|
226
385
|
aggregate: {
|
|
227
|
-
|
|
386
|
+
perpPerpCount: arbPairs.length,
|
|
387
|
+
spotPerpCount: spotPerpPositions.length,
|
|
388
|
+
totalPositions: arbPairs.length + spotPerpPositions.length,
|
|
228
389
|
totalNotionalUsd: totalNotionalJ,
|
|
229
|
-
unrealizedPnl: arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0),
|
|
230
|
-
estimatedFunding: arbPairs.reduce((s, p) => s + p.estimatedFundingIncome, 0),
|
|
231
|
-
estimatedFees: arbPairs.reduce((s, p) => s + p.estimatedFees, 0),
|
|
232
|
-
netPnl: arbPairs.reduce((s, p) => s + p.netPnl, 0),
|
|
390
|
+
unrealizedPnl: arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0) + spotPerpPositions.reduce((s, p) => s + p.perpUnrealizedPnl, 0),
|
|
391
|
+
estimatedFunding: arbPairs.reduce((s, p) => s + p.estimatedFundingIncome, 0) + spotPerpPositions.reduce((s, p) => s + p.estimatedFundingIncome, 0),
|
|
392
|
+
estimatedFees: arbPairs.reduce((s, p) => s + p.estimatedFees, 0) + spotPerpPositions.reduce((s, p) => s + p.estimatedFees, 0),
|
|
393
|
+
netPnl: arbPairs.reduce((s, p) => s + p.netPnl, 0) + spotPerpPositions.reduce((s, p) => s + p.netPnl, 0),
|
|
233
394
|
dailyFundingEstimate: totalDailyJ,
|
|
234
395
|
annualizedApr: totalNotionalJ > 0 ? (totalDailyJ * 365 / totalNotionalJ) * 100 : 0,
|
|
235
396
|
},
|
|
236
397
|
}));
|
|
237
398
|
}
|
|
238
|
-
if (arbPairs.length === 0) {
|
|
399
|
+
if (arbPairs.length === 0 && spotPerpPositions.length === 0) {
|
|
239
400
|
console.log(chalk.gray(" No open arb positions found.\n"));
|
|
240
401
|
return;
|
|
241
402
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
?
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
403
|
+
// ── Perp-Perp table ──
|
|
404
|
+
if (arbPairs.length > 0) {
|
|
405
|
+
console.log(chalk.cyan.bold(" Perp-Perp Arb Positions\n"));
|
|
406
|
+
const rows = arbPairs.map(p => {
|
|
407
|
+
const spreadStr = p.currentSpread !== null ? `${p.currentSpread.toFixed(1)}%` : "-";
|
|
408
|
+
const entrySpreadStr = p.entrySpread !== null ? `${p.entrySpread.toFixed(1)}%` : "-";
|
|
409
|
+
const holdStr = p.holdDuration ?? "-";
|
|
410
|
+
const avgNotional = (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2;
|
|
411
|
+
const basis = computeBasisRisk(p.longPosition.markPrice, p.shortPosition.markPrice);
|
|
412
|
+
const basisStr = basis.divergencePct > 0
|
|
413
|
+
? (basis.warning
|
|
414
|
+
? chalk.red(`${basis.divergencePct.toFixed(1)}%`)
|
|
415
|
+
: chalk.gray(`${basis.divergencePct.toFixed(1)}%`))
|
|
416
|
+
: chalk.gray("-");
|
|
417
|
+
return [
|
|
418
|
+
chalk.white.bold(p.symbol),
|
|
419
|
+
chalk.green(p.longExchange),
|
|
420
|
+
chalk.red(p.shortExchange),
|
|
421
|
+
`$${formatUsd(avgNotional)}`,
|
|
422
|
+
`$${p.longPosition.entryPrice.toFixed(2)} / $${p.shortPosition.entryPrice.toFixed(2)}`,
|
|
423
|
+
`$${p.longPosition.markPrice.toFixed(2)}`,
|
|
424
|
+
formatPnl(p.unrealizedPnl),
|
|
425
|
+
chalk.yellow(`$${p.estimatedFundingIncome.toFixed(4)}`),
|
|
426
|
+
`${entrySpreadStr} -> ${spreadStr}`,
|
|
427
|
+
basisStr,
|
|
428
|
+
holdStr,
|
|
429
|
+
formatPnl(p.netPnl),
|
|
430
|
+
];
|
|
431
|
+
});
|
|
432
|
+
console.log(makeTable(["Symbol", "Long", "Short", "Size", "Entry", "Mark", "uPnL", "Funding", "Spread", "Basis", "Hold", "Net PnL"], rows));
|
|
433
|
+
}
|
|
434
|
+
// ── Spot-Perp table ──
|
|
435
|
+
if (spotPerpPositions.length > 0) {
|
|
436
|
+
console.log(chalk.cyan.bold(" Spot-Perp Arb Positions\n"));
|
|
437
|
+
const spRows = spotPerpPositions.map(sp => {
|
|
438
|
+
const spotLabel = sp.spotToken !== sp.symbol
|
|
439
|
+
? `${sp.spotToken} (${sp.spotExchange})`
|
|
440
|
+
: sp.spotExchange;
|
|
441
|
+
const spreadStr = sp.currentSpread !== null ? `${sp.currentSpread.toFixed(1)}%` : "-";
|
|
442
|
+
return [
|
|
443
|
+
chalk.white.bold(sp.symbol),
|
|
444
|
+
chalk.green(`${sp.spotAmount.toFixed(4)} ${sp.spotToken}`),
|
|
445
|
+
spotLabel,
|
|
446
|
+
chalk.red(sp.perpExchange),
|
|
447
|
+
`$${formatUsd(sp.perpNotionalUsd)}`,
|
|
448
|
+
`$${sp.perpMarkPrice.toFixed(2)}`,
|
|
449
|
+
formatPnl(sp.perpUnrealizedPnl),
|
|
450
|
+
chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`),
|
|
451
|
+
spreadStr,
|
|
452
|
+
sp.holdDuration ?? "-",
|
|
453
|
+
formatPnl(sp.netPnl),
|
|
454
|
+
];
|
|
455
|
+
});
|
|
456
|
+
console.log(makeTable(["Symbol", "Spot Held", "Spot Ex", "Perp Ex", "Size", "Mark", "uPnL", "Funding", "Spread", "Hold", "Net PnL"], spRows));
|
|
457
|
+
}
|
|
458
|
+
// ── Combined Summary ──
|
|
459
|
+
const totalUpnl = arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0)
|
|
460
|
+
+ spotPerpPositions.reduce((s, p) => s + p.perpUnrealizedPnl, 0);
|
|
461
|
+
const totalFunding = arbPairs.reduce((s, p) => s + p.estimatedFundingIncome, 0)
|
|
462
|
+
+ spotPerpPositions.reduce((s, p) => s + p.estimatedFundingIncome, 0);
|
|
463
|
+
const totalFees = arbPairs.reduce((s, p) => s + p.estimatedFees, 0)
|
|
464
|
+
+ spotPerpPositions.reduce((s, p) => s + p.estimatedFees, 0);
|
|
465
|
+
const totalNet = arbPairs.reduce((s, p) => s + p.netPnl, 0)
|
|
466
|
+
+ spotPerpPositions.reduce((s, p) => s + p.netPnl, 0);
|
|
467
|
+
const totalDailyFunding = arbPairs.reduce((s, p) => s + p.dailyFundingEstimate, 0)
|
|
468
|
+
+ spotPerpPositions.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
469
|
+
const totalNotional = arbPairs.reduce((s, p) => s + (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2, 0)
|
|
470
|
+
+ spotPerpPositions.reduce((s, p) => s + p.perpNotionalUsd, 0);
|
|
278
471
|
const portfolioApr = totalNotional > 0 ? (totalDailyFunding * 365 / totalNotional) * 100 : 0;
|
|
279
472
|
console.log(chalk.white.bold("\n Summary"));
|
|
280
|
-
console.log(`
|
|
473
|
+
console.log(` Perp-Perp: ${arbPairs.length} positions`);
|
|
474
|
+
console.log(` Spot-Perp: ${spotPerpPositions.length} positions`);
|
|
281
475
|
console.log(` Total Notional: $${formatUsd(totalNotional)}`);
|
|
282
476
|
console.log(` Unrealized PnL: ${formatPnl(totalUpnl)}`);
|
|
283
477
|
console.log(` Est. Funding: ${chalk.yellow(`$${totalFunding.toFixed(4)}`)}`);
|