perp-cli 0.4.11 → 0.5.0

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.
@@ -1,4 +1,4 @@
1
- import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
1
+ import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors } from "../utils.js";
2
2
  import chalk from "chalk";
3
3
  import { annualizeRate, toHourlyRate, estimateHourlyFunding, annualizeHourlyRate } from "../funding.js";
4
4
  import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
@@ -53,183 +53,10 @@ async function fetchLighterRates() {
53
53
  // annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
54
54
  export function registerArbCommands(program, isJson, getAdapterForExchange) {
55
55
  const arb = program.command("arb").description("Funding rate arbitrage & basis trading");
56
- // Deprecated: arb funding → use 'perp arb rates'
57
- const fundingCmd = arb.command("funding").description("Use 'perp arb rates'");
58
- fundingCmd._hidden = true;
59
- fundingCmd.action(() => console.log("Use 'perp arb rates --min-spread 5' instead."));
60
- arb
61
- .command("basis")
62
- .description("Show basis (spot vs perp price) opportunities across exchanges")
63
- .option("--min-basis <pct>", "Minimum annual basis % to show", "3")
64
- .action(async (opts) => {
65
- if (!isJson())
66
- console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
67
- const [pacRates, hlRates, ltRates] = await Promise.all([
68
- fetchPacificaRates(),
69
- fetchHyperliquidRates(),
70
- fetchLighterRates(),
71
- ]);
72
- // Compare mark prices across all exchanges for cross-exchange basis
73
- const exchangePrices = new Map();
74
- for (const r of [...pacRates, ...hlRates, ...ltRates]) {
75
- if (r.markPrice <= 0)
76
- continue;
77
- if (!exchangePrices.has(r.symbol))
78
- exchangePrices.set(r.symbol, new Map());
79
- exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
80
- }
81
- const opps = [];
82
- for (const [symbol, prices] of exchangePrices.entries()) {
83
- if (prices.size < 2)
84
- continue;
85
- // Find cheapest and most expensive exchange
86
- let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
87
- for (const [ex, price] of prices) {
88
- if (price < minPrice) {
89
- minPrice = price;
90
- minEx = ex;
91
- }
92
- if (price > maxPrice) {
93
- maxPrice = price;
94
- maxEx = ex;
95
- }
96
- }
97
- if (minEx === maxEx)
98
- continue;
99
- const diff = maxPrice - minPrice;
100
- const avgPrice = (maxPrice + minPrice) / 2;
101
- const pctDiff = (diff / avgPrice) * 100;
102
- if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
103
- const priceObj = {};
104
- for (const [ex, p] of prices)
105
- priceObj[ex] = p;
106
- opps.push({
107
- symbol,
108
- prices: priceObj,
109
- priceDiff: diff,
110
- pctDiff,
111
- buyOn: minEx,
112
- sellOn: maxEx,
113
- });
114
- }
115
- }
116
- opps.sort((a, b) => b.pctDiff - a.pctDiff);
117
- if (isJson())
118
- return printJson(jsonOk(opps));
119
- if (opps.length === 0) {
120
- console.log(chalk.gray(` No basis opportunities above ${opts.minBasis}% threshold.\n`));
121
- return;
122
- }
123
- console.log(chalk.cyan.bold(" Cross-Exchange Basis Opportunities\n"));
124
- console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
125
- const rows = opps.map((o) => [
126
- chalk.white.bold(o.symbol),
127
- o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
128
- o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
129
- o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
130
- `$${formatUsd(o.priceDiff)}`,
131
- `${o.pctDiff.toFixed(4)}%`,
132
- chalk.green(o.buyOn),
133
- chalk.red(o.sellOn),
134
- ]);
135
- console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
136
- });
137
- // ── arb dex ── (HIP-3 cross-dex funding arb scan)
138
- arb
139
- .command("dex")
140
- .description("Scan HIP-3 cross-dex funding arb opportunities on Hyperliquid")
141
- .option("--min-spread <pct>", "Min annual spread % to show", "5")
142
- .option("--max-gap <pct>", "Max price gap % to treat as same asset", "5")
143
- .option("--include-native", "Include native HL perps in comparison", true)
144
- .option("--no-include-native", "Exclude native HL perps")
145
- .option("--top <n>", "Show top N opportunities", "30")
146
- .action(async (opts) => {
147
- if (!isJson())
148
- console.log(chalk.cyan(" Scanning HIP-3 deployed dex funding spreads...\n"));
149
- const pairs = await scanDexArb({
150
- minAnnualSpread: parseFloat(opts.minSpread),
151
- maxPriceGapPct: parseFloat(opts.maxGap),
152
- includeNative: opts.includeNative,
153
- });
154
- const topN = parseInt(opts.top);
155
- const shown = pairs.slice(0, topN);
156
- if (isJson())
157
- return printJson(jsonOk(shown.map(p => ({
158
- underlying: p.underlying,
159
- longDex: p.long.dex,
160
- longSymbol: p.long.raw,
161
- longFunding: p.long.fundingRate,
162
- longPrice: p.long.markPrice,
163
- longOiUsd: p.long.openInterest * p.long.markPrice,
164
- longVolume24h: p.long.volume24h,
165
- shortDex: p.short.dex,
166
- shortSymbol: p.short.raw,
167
- shortFunding: p.short.fundingRate,
168
- shortPrice: p.short.markPrice,
169
- shortOiUsd: p.short.openInterest * p.short.markPrice,
170
- shortVolume24h: p.short.volume24h,
171
- annualSpread: p.annualSpread,
172
- priceGapPct: p.priceGapPct,
173
- minOiUsd: p.minOiUsd,
174
- minVolume24hUsd: p.minVolume24hUsd,
175
- viability: p.viability,
176
- }))));
177
- if (shown.length === 0) {
178
- console.log(chalk.gray(` No cross-dex opportunities above ${opts.minSpread}% annual spread.\n`));
179
- return;
180
- }
181
- console.log(chalk.cyan.bold(" HIP-3 Cross-Dex Funding Arb Opportunities\n"));
182
- console.log(chalk.gray(" Strategy: Long on low-funding dex, Short on high-funding dex (same underlying)\n"));
183
- const rows = shown.map((p) => {
184
- const spreadColor = p.annualSpread >= 50 ? chalk.green.bold
185
- : p.annualSpread >= 20 ? chalk.green
186
- : chalk.yellow;
187
- const viabilityColor = p.viability === "A" ? chalk.green.bold
188
- : p.viability === "B" ? chalk.green
189
- : p.viability === "C" ? chalk.yellow
190
- : chalk.red;
191
- const fmtOi = p.minOiUsd >= 1_000_000 ? `$${(p.minOiUsd / 1_000_000).toFixed(1)}M`
192
- : p.minOiUsd >= 1_000 ? `$${(p.minOiUsd / 1_000).toFixed(0)}K`
193
- : `$${p.minOiUsd.toFixed(0)}`;
194
- const fmtVol = p.minVolume24hUsd >= 1_000_000 ? `$${(p.minVolume24hUsd / 1_000_000).toFixed(1)}M`
195
- : p.minVolume24hUsd >= 1_000 ? `$${(p.minVolume24hUsd / 1_000).toFixed(0)}K`
196
- : `$${p.minVolume24hUsd.toFixed(0)}`;
197
- return [
198
- chalk.white.bold(p.underlying),
199
- `${p.long.dex}:${p.long.base}`,
200
- formatPercent(p.long.fundingRate),
201
- `${p.short.dex}:${p.short.base}`,
202
- formatPercent(p.short.fundingRate),
203
- `$${formatUsd(p.long.markPrice)}`,
204
- spreadColor(`${p.annualSpread.toFixed(1)}%`),
205
- p.priceGapPct < 0.1 ? chalk.green(`${p.priceGapPct.toFixed(3)}%`) : chalk.yellow(`${p.priceGapPct.toFixed(3)}%`),
206
- viabilityColor(p.viability),
207
- viabilityColor(fmtOi),
208
- chalk.gray(fmtVol),
209
- ];
210
- });
211
- console.log(makeTable(["Asset", "Long On", "L.Fund", "Short On", "S.Fund", "Price", "Ann.Spread", "Gap", "Grade", "MinOI", "MinVol"], rows));
212
- // Summary
213
- const dexes = new Set();
214
- for (const p of shown) {
215
- dexes.add(p.long.dex);
216
- dexes.add(p.short.dex);
217
- }
218
- const gradeCount = { A: 0, B: 0, C: 0, D: 0 };
219
- for (const p of shown)
220
- gradeCount[p.viability]++;
221
- console.log(chalk.gray(`\n ${shown.length}/${pairs.length} opportunities | Dexes: ${[...dexes].sort().join(", ")}`));
222
- console.log(chalk.gray(` Grade: ${chalk.green.bold(`A(${gradeCount.A})`)} ${chalk.green(`B(${gradeCount.B})`)} ${chalk.yellow(`C(${gradeCount.C})`)} ${chalk.red(`D(${gradeCount.D})`)}`));
223
- console.log(chalk.gray(` A: OI>$1M B: OI>$100K C: OI>$10K D: OI<$10K (thin — high risk)`));
224
- console.log(chalk.gray(` Note: HIP-3 dexes charge 2x fees. Factor into net profitability.\n`));
225
- });
226
- // ── arb watch/track/alert ── (promoted from arb gap)
227
- registerGapDirectCommands(arb, isJson);
228
- // ── Funding subcommands (merged from funding.ts) ──
229
- registerFundingSubcommands(arb, isJson, getAdapterForExchange);
230
- // Deprecated aliases removed — use arb prices/watch/track/alert/rates directly
56
+ // basis, dex, rates, compare, funding-history, funding-positions, gaps, live monitor
57
+ // → consolidated into 'arb scan' (in arb-auto.ts) with routing options
231
58
  }
232
- async function fetchAllPrices() {
59
+ export async function fetchAllPrices() {
233
60
  const [pacRes, hlRes, ltRes] = await Promise.all([
234
61
  fetchPacificaPricesRaw(),
235
62
  fetchHyperliquidAllMidsRaw(),
@@ -289,14 +116,14 @@ async function fetchAllPrices() {
289
116
  }
290
117
  return snapshots.sort((a, b) => b.maxGapPct - a.maxGapPct);
291
118
  }
292
- function formatGapPrice(p) {
119
+ export function formatGapPrice(p) {
293
120
  if (p >= 1000)
294
121
  return p.toFixed(2);
295
122
  if (p >= 1)
296
123
  return p.toFixed(4);
297
124
  return p.toFixed(6);
298
125
  }
299
- function printGapTable(snapshots, minGap) {
126
+ export function printGapTable(snapshots, minGap) {
300
127
  const filtered = snapshots.filter((s) => s.maxGapPct >= minGap);
301
128
  if (filtered.length === 0) {
302
129
  console.log(chalk.gray(`\n No gaps above ${minGap}%\n`));
@@ -322,7 +149,7 @@ function printGapTable(snapshots, minGap) {
322
149
  const top = filtered[0];
323
150
  console.log(chalk.gray(`\n ${filtered.length} pairs | Avg gap: ${avgGap.toFixed(4)}% | Max: ${top.symbol} ${top.maxGapPct.toFixed(4)}%\n`));
324
151
  }
325
- function printTrackSummary(symbol, samples) {
152
+ export function printTrackSummary(symbol, samples) {
326
153
  if (samples.length === 0) {
327
154
  console.log(chalk.gray("\n No samples collected.\n"));
328
155
  return;
@@ -346,194 +173,6 @@ function printTrackSummary(symbol, samples) {
346
173
  }
347
174
  console.log();
348
175
  }
349
- /**
350
- * Register gap-related commands directly on the arb parent (promoted from arb gap).
351
- * - arb prices: snapshot of cross-exchange prices (was arb gap show)
352
- * - arb watch: live monitoring (was arb gap watch)
353
- * - arb track: track gap over time (was arb gap track)
354
- * - arb alert: threshold alert (was arb gap alert)
355
- */
356
- function registerGapDirectCommands(arb, isJson) {
357
- // ── arb prices (was arb gap show) ──
358
- arb
359
- .command("prices")
360
- .description("Show current prices across all exchanges with gap analysis")
361
- .option("--min <pct>", "Minimum gap % to display", "0.01")
362
- .option("--symbol <sym>", "Filter by symbol (e.g. BTC, ETH)")
363
- .option("--top <n>", "Show top N gaps only")
364
- .action(async (opts) => {
365
- const minGap = parseFloat(opts.min);
366
- if (!isJson())
367
- console.log(chalk.cyan("\n Fetching prices from Pacifica, Hyperliquid & Lighter...\n"));
368
- let snapshots = await fetchAllPrices();
369
- if (opts.symbol) {
370
- const sym = opts.symbol.toUpperCase();
371
- snapshots = snapshots.filter((s) => s.symbol.includes(sym));
372
- }
373
- if (opts.top) {
374
- snapshots = snapshots.slice(0, parseInt(opts.top));
375
- }
376
- if (isJson())
377
- return printJson(jsonOk(snapshots.filter((s) => s.maxGapPct >= minGap)));
378
- printGapTable(snapshots, minGap);
379
- });
380
- // ── arb watch (was arb gap watch) ──
381
- arb
382
- .command("watch")
383
- .description("Live-monitor price gaps across exchanges (auto-refresh)")
384
- .option("--min <pct>", "Minimum gap % to display", "0.05")
385
- .option("--interval <seconds>", "Refresh interval", "5")
386
- .option("--symbol <sym>", "Filter by symbol")
387
- .option("--beep", "Beep on gaps above 0.5%")
388
- .action(async (opts) => {
389
- const minGap = parseFloat(opts.min);
390
- const intervalMs = parseInt(opts.interval) * 1000;
391
- const beep = !!opts.beep;
392
- if (!isJson()) {
393
- console.log(chalk.cyan.bold("\n Price Gap Monitor"));
394
- console.log(` Min gap: ${minGap}%`);
395
- console.log(` Interval: ${opts.interval}s`);
396
- if (opts.symbol)
397
- console.log(` Symbol: ${opts.symbol.toUpperCase()}`);
398
- console.log(chalk.gray(" Ctrl+C to stop\n"));
399
- }
400
- const cycle = async () => {
401
- try {
402
- let snapshots = await fetchAllPrices();
403
- if (opts.symbol) {
404
- const sym = opts.symbol.toUpperCase();
405
- snapshots = snapshots.filter((s) => s.symbol.includes(sym));
406
- }
407
- process.stdout.write("\x1B[2J\x1B[0f");
408
- const now = new Date().toLocaleTimeString();
409
- console.log(chalk.cyan.bold(` Price Gap Monitor`) +
410
- chalk.gray(` ${now} (refresh: ${opts.interval}s)\n`));
411
- printGapTable(snapshots, minGap);
412
- const bigGaps = snapshots.filter((s) => s.maxGapPct >= 0.5);
413
- if (bigGaps.length > 0) {
414
- console.log(chalk.red.bold(` !! ${bigGaps.length} large gap(s): ` +
415
- bigGaps
416
- .map((s) => `${s.symbol}(${s.maxGapPct.toFixed(3)}%)`)
417
- .join(", ")));
418
- if (beep)
419
- process.stdout.write("\x07");
420
- }
421
- }
422
- catch (err) {
423
- console.error(chalk.gray(` Error: ${err instanceof Error ? err.message : String(err)}`));
424
- }
425
- };
426
- await cycle();
427
- setInterval(cycle, intervalMs);
428
- await new Promise(() => { }); // keep alive
429
- });
430
- // ── arb track (was arb gap track) ──
431
- arb
432
- .command("track")
433
- .description("Track a symbol's price gap over time and print stats")
434
- .requiredOption("-s, --symbol <sym>", "Symbol to track (e.g. BTC)")
435
- .option("--duration <minutes>", "How long to track (minutes)", "10")
436
- .option("--interval <seconds>", "Sample interval", "10")
437
- .action(async (opts) => {
438
- const symbol = opts.symbol.toUpperCase();
439
- const durationMs = parseInt(opts.duration) * 60 * 1000;
440
- const intervalMs = parseInt(opts.interval) * 1000;
441
- const endTime = Date.now() + durationMs;
442
- const samples = [];
443
- if (!isJson()) {
444
- console.log(chalk.cyan.bold(`\n Tracking ${symbol} price gap\n`));
445
- console.log(` Duration: ${opts.duration} min`);
446
- console.log(` Interval: ${opts.interval}s`);
447
- console.log(chalk.gray(" Collecting samples...\n"));
448
- }
449
- const sample = async () => {
450
- const snapshots = await fetchAllPrices();
451
- const s = snapshots.find((x) => x.symbol === symbol);
452
- if (!s) {
453
- console.log(chalk.gray(` ${new Date().toLocaleTimeString()} — ${symbol} not found on both exchanges`));
454
- return;
455
- }
456
- samples.push({
457
- time: new Date().toLocaleTimeString(),
458
- gap: s.maxGap,
459
- gapPct: s.maxGapPct,
460
- direction: `${s.cheapest}→${s.expensive}`,
461
- });
462
- const gapColor = s.maxGapPct >= 0.1 ? chalk.yellow : chalk.gray;
463
- const parts = [`PAC: ${s.pacPrice !== null ? "$" + formatGapPrice(s.pacPrice) : "-"}`];
464
- parts.push(`HL: ${s.hlPrice !== null ? "$" + formatGapPrice(s.hlPrice) : "-"}`);
465
- parts.push(`LT: ${s.ltPrice !== null ? "$" + formatGapPrice(s.ltPrice) : "-"}`);
466
- console.log(` ${chalk.white(s.symbol.padEnd(6))} ` +
467
- `${parts.join(" ")} ` +
468
- `${gapColor(`${s.maxGapPct.toFixed(4)}%`)} ${s.cheapest}→${s.expensive}`);
469
- };
470
- await sample();
471
- const timer = setInterval(async () => {
472
- if (Date.now() >= endTime) {
473
- clearInterval(timer);
474
- printTrackSummary(symbol, samples);
475
- return;
476
- }
477
- await sample();
478
- }, intervalMs);
479
- await new Promise((resolve) => {
480
- setTimeout(resolve, durationMs + 1000);
481
- });
482
- });
483
- // ── arb alert (was arb gap alert) ──
484
- arb
485
- .command("alert")
486
- .description("Wait until a symbol's price gap exceeds a threshold, then exit")
487
- .requiredOption("-s, --symbol <sym>", "Symbol to watch")
488
- .requiredOption("--above <pct>", "Gap % threshold to trigger")
489
- .option("--interval <seconds>", "Check interval", "5")
490
- .action(async (opts) => {
491
- const symbol = opts.symbol.toUpperCase();
492
- const threshold = parseFloat(opts.above);
493
- const intervalMs = parseInt(opts.interval) * 1000;
494
- if (!isJson())
495
- console.log(chalk.cyan(`\n Waiting for ${symbol} gap > ${threshold}%...\n`));
496
- const check = async () => {
497
- const snapshots = await fetchAllPrices();
498
- const s = snapshots.find((x) => x.symbol === symbol);
499
- if (!s)
500
- return false;
501
- const now = new Date().toLocaleTimeString();
502
- if (!isJson())
503
- console.log(chalk.gray(` ${now} ${symbol} gap: ${s.maxGapPct.toFixed(4)}%`));
504
- if (s.maxGapPct >= threshold) {
505
- if (isJson()) {
506
- printJson(jsonOk(s));
507
- }
508
- else {
509
- console.log(chalk.green.bold(`\n TRIGGERED! ${symbol} gap: ${s.maxGapPct.toFixed(4)}% (> ${threshold}%)`));
510
- const lines = [];
511
- if (s.pacPrice !== null)
512
- lines.push(` Pacifica: $${formatGapPrice(s.pacPrice)}`);
513
- if (s.hlPrice !== null)
514
- lines.push(` Hyperliquid: $${formatGapPrice(s.hlPrice)}`);
515
- if (s.ltPrice !== null)
516
- lines.push(` Lighter: $${formatGapPrice(s.ltPrice)}`);
517
- lines.push(` Gap: $${s.maxGap.toFixed(4)} (${s.cheapest}→${s.expensive})`);
518
- console.log(lines.join("\n") + "\n");
519
- }
520
- return true;
521
- }
522
- return false;
523
- };
524
- if (await check())
525
- return;
526
- await new Promise((resolve) => {
527
- const timer = setInterval(async () => {
528
- if (await check()) {
529
- clearInterval(timer);
530
- resolve();
531
- }
532
- }, intervalMs);
533
- });
534
- });
535
- }
536
- // registerGapSubcommands removed — use arb prices/watch/track/alert directly
537
176
  // ────────────────────────────────────────────────────────
538
177
  // Funding subcommands (merged from funding.ts)
539
178
  // ────────────────────────────────────────────────────────
@@ -590,162 +229,245 @@ function printDetailedComparison(comparison) {
590
229
  console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
591
230
  console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
592
231
  }
593
- function registerFundingSubcommands(parent, isJson, getAdapterForExchange) {
594
- parent.command("rates").description("Show funding rates across all 3 DEXs for top symbols")
595
- .option("-s, --symbol <symbol>", "Filter to a specific symbol").option("--symbols <list>", "Comma-separated list of symbols")
596
- .option("--all", "Show all available symbols").option("--min-spread <pct>", "Minimum annual spread % to show", "0")
597
- .action(async (opts) => {
598
- const minSpread = parseFloat(opts.minSpread);
599
- let filterSymbols;
600
- if (opts.symbol)
601
- filterSymbols = [opts.symbol.toUpperCase()];
602
- else if (opts.symbols)
603
- filterSymbols = opts.symbols.split(",").map(s => s.trim().toUpperCase());
604
- else if (!opts.all)
605
- filterSymbols = TOP_SYMBOLS;
606
- if (!isJson())
607
- console.log(chalk.cyan(" Fetching funding rates from all exchanges...\n"));
608
- const snapshot = await fetchAllFundingRates({ symbols: filterSymbols, minSpread });
609
- try {
610
- const allRates = snapshot.symbols.flatMap(s => s.rates);
611
- if (allRates.length > 0)
612
- saveFundingSnapshot(allRates);
232
+ // registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
233
+ // ── Exported handler functions for use by arb-auto.ts scan routing ──
234
+ export async function handleRates(isJson, opts) {
235
+ const minSpread = parseFloat(opts.minSpread);
236
+ let filterSymbols;
237
+ if (opts.symbol)
238
+ filterSymbols = [opts.symbol.toUpperCase()];
239
+ else if (opts.symbols)
240
+ filterSymbols = opts.symbols.split(",").map(s => s.trim().toUpperCase());
241
+ else if (!opts.all)
242
+ filterSymbols = TOP_SYMBOLS;
243
+ if (!isJson())
244
+ console.log(chalk.cyan(" Fetching funding rates from all exchanges...\n"));
245
+ const snapshot = await fetchAllFundingRates({ symbols: filterSymbols, minSpread });
246
+ try {
247
+ const allRates = snapshot.symbols.flatMap(s => s.rates);
248
+ if (allRates.length > 0)
249
+ saveFundingSnapshot(allRates);
250
+ }
251
+ catch { /* non-critical */ }
252
+ if (isJson()) {
253
+ if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
254
+ return printJson(jsonOk(snapshot.symbols));
255
+ return printJson(jsonOk(snapshot));
256
+ }
257
+ printSnapshotTable(snapshot);
258
+ printFundingExchangeStatus(snapshot);
259
+ }
260
+ export async function handleBasisScan(isJson, opts) {
261
+ if (!isJson())
262
+ console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
263
+ const [pacRates, hlRates, ltRates] = await Promise.all([fetchPacificaRates(), fetchHyperliquidRates(), fetchLighterRates()]);
264
+ const exchangePrices = new Map();
265
+ for (const r of [...pacRates, ...hlRates, ...ltRates]) {
266
+ if (r.markPrice <= 0)
267
+ continue;
268
+ if (!exchangePrices.has(r.symbol))
269
+ exchangePrices.set(r.symbol, new Map());
270
+ exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
271
+ }
272
+ const opps = [];
273
+ for (const [symbol, prices] of exchangePrices.entries()) {
274
+ if (prices.size < 2)
275
+ continue;
276
+ let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
277
+ for (const [ex, price] of prices) {
278
+ if (price < minPrice) {
279
+ minPrice = price;
280
+ minEx = ex;
281
+ }
282
+ if (price > maxPrice) {
283
+ maxPrice = price;
284
+ maxEx = ex;
285
+ }
613
286
  }
614
- catch { /* non-critical */ }
615
- if (isJson()) {
616
- if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
617
- return printJson(jsonOk(snapshot.symbols));
618
- return printJson(jsonOk(snapshot));
287
+ if (minEx === maxEx)
288
+ continue;
289
+ const diff = maxPrice - minPrice;
290
+ const avgPrice = (maxPrice + minPrice) / 2;
291
+ const pctDiff = (diff / avgPrice) * 100;
292
+ if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
293
+ const priceObj = {};
294
+ for (const [ex, p] of prices)
295
+ priceObj[ex] = p;
296
+ opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: minEx, sellOn: maxEx });
619
297
  }
620
- printSnapshotTable(snapshot);
621
- printFundingExchangeStatus(snapshot);
298
+ }
299
+ opps.sort((a, b) => b.pctDiff - a.pctDiff);
300
+ if (isJson())
301
+ return printJson(jsonOk(opps));
302
+ if (opps.length === 0) {
303
+ console.log(chalk.gray(` No basis opportunities above ${opts.minBasis}% threshold.\n`));
304
+ return;
305
+ }
306
+ console.log(chalk.cyan.bold(" Cross-Exchange Basis Opportunities\n"));
307
+ console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
308
+ const rows = opps.map((o) => [
309
+ chalk.white.bold(o.symbol),
310
+ o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
311
+ o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
312
+ o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
313
+ `$${formatUsd(o.priceDiff)}`, `${o.pctDiff.toFixed(4)}%`, chalk.green(o.buyOn), chalk.red(o.sellOn),
314
+ ]);
315
+ console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
316
+ }
317
+ export async function handleDexScan(isJson, opts) {
318
+ if (!isJson())
319
+ console.log(chalk.cyan(" Scanning HIP-3 deployed dex funding spreads...\n"));
320
+ const pairs = await scanDexArb({ minAnnualSpread: parseFloat(opts.minSpread), maxPriceGapPct: parseFloat(opts.maxGap), includeNative: opts.includeNative });
321
+ const topN = parseInt(opts.top);
322
+ const shown = pairs.slice(0, topN);
323
+ if (isJson())
324
+ return printJson(jsonOk(shown.map(p => ({
325
+ underlying: p.underlying, longDex: p.long.dex, longSymbol: p.long.raw, longFunding: p.long.fundingRate,
326
+ longPrice: p.long.markPrice, longOiUsd: p.long.openInterest * p.long.markPrice, longVolume24h: p.long.volume24h,
327
+ shortDex: p.short.dex, shortSymbol: p.short.raw, shortFunding: p.short.fundingRate, shortPrice: p.short.markPrice,
328
+ shortOiUsd: p.short.openInterest * p.short.markPrice, shortVolume24h: p.short.volume24h,
329
+ annualSpread: p.annualSpread, priceGapPct: p.priceGapPct, minOiUsd: p.minOiUsd, minVolume24hUsd: p.minVolume24hUsd, viability: p.viability,
330
+ }))));
331
+ if (shown.length === 0) {
332
+ console.log(chalk.gray(` No cross-dex opportunities above ${opts.minSpread}% annual spread.\n`));
333
+ return;
334
+ }
335
+ console.log(chalk.cyan.bold(" HIP-3 Cross-Dex Funding Arb Opportunities\n"));
336
+ console.log(chalk.gray(" Strategy: Long on low-funding dex, Short on high-funding dex (same underlying)\n"));
337
+ const rows = shown.map((p) => {
338
+ const spreadColor = p.annualSpread >= 50 ? chalk.green.bold : p.annualSpread >= 20 ? chalk.green : chalk.yellow;
339
+ const viabilityColor = p.viability === "A" ? chalk.green.bold : p.viability === "B" ? chalk.green : p.viability === "C" ? chalk.yellow : chalk.red;
340
+ const fmtOi = p.minOiUsd >= 1_000_000 ? `$${(p.minOiUsd / 1_000_000).toFixed(1)}M` : p.minOiUsd >= 1_000 ? `$${(p.minOiUsd / 1_000).toFixed(0)}K` : `$${p.minOiUsd.toFixed(0)}`;
341
+ const fmtVol = p.minVolume24hUsd >= 1_000_000 ? `$${(p.minVolume24hUsd / 1_000_000).toFixed(1)}M` : p.minVolume24hUsd >= 1_000 ? `$${(p.minVolume24hUsd / 1_000).toFixed(0)}K` : `$${p.minVolume24hUsd.toFixed(0)}`;
342
+ return [chalk.white.bold(p.underlying), `${p.long.dex}:${p.long.base}`, formatPercent(p.long.fundingRate), `${p.short.dex}:${p.short.base}`, formatPercent(p.short.fundingRate), `$${formatUsd(p.long.markPrice)}`, spreadColor(`${p.annualSpread.toFixed(1)}%`), p.priceGapPct < 0.1 ? chalk.green(`${p.priceGapPct.toFixed(3)}%`) : chalk.yellow(`${p.priceGapPct.toFixed(3)}%`), viabilityColor(p.viability), viabilityColor(fmtOi), chalk.gray(fmtVol)];
622
343
  });
623
- parent.command("compare <symbol>").description("Detailed funding rate comparison for a single symbol")
624
- .action(async (symbol) => {
625
- if (!isJson())
626
- console.log(chalk.cyan(` Fetching funding rates for ${symbol.toUpperCase()}...\n`));
627
- const comparison = await fetchSymbolFundingRates(symbol);
628
- if (!comparison) {
629
- if (isJson())
630
- return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
631
- console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
632
- return;
633
- }
344
+ console.log(makeTable(["Asset", "Long On", "L.Fund", "Short On", "S.Fund", "Price", "Ann.Spread", "Gap", "Grade", "MinOI", "MinVol"], rows));
345
+ const dexes = new Set();
346
+ for (const p of shown) {
347
+ dexes.add(p.long.dex);
348
+ dexes.add(p.short.dex);
349
+ }
350
+ const gradeCount = { A: 0, B: 0, C: 0, D: 0 };
351
+ for (const p of shown)
352
+ gradeCount[p.viability]++;
353
+ console.log(chalk.gray(`\n ${shown.length}/${pairs.length} opportunities | Dexes: ${[...dexes].sort().join(", ")}`));
354
+ console.log(chalk.gray(` Grade: ${chalk.green.bold(`A(${gradeCount.A})`)} ${chalk.green(`B(${gradeCount.B})`)} ${chalk.yellow(`C(${gradeCount.C})`)} ${chalk.red(`D(${gradeCount.D})`)}`));
355
+ console.log(chalk.gray(` A: OI>$1M B: OI>$100K C: OI>$10K D: OI<$10K (thin — high risk)`));
356
+ console.log(chalk.gray(` Note: HIP-3 dexes charge 2x fees. Factor into net profitability.\n`));
357
+ }
358
+ export async function handleCompare(isJson, symbol) {
359
+ if (!isJson())
360
+ console.log(chalk.cyan(` Fetching funding rates for ${symbol.toUpperCase()}...\n`));
361
+ const comparison = await fetchSymbolFundingRates(symbol);
362
+ if (!comparison) {
634
363
  if (isJson())
635
- return printJson(jsonOk(comparison));
636
- printDetailedComparison(comparison);
637
- });
638
- parent.command("funding-history").description("Show funding rate trend over time for a symbol")
639
- .requiredOption("-s, --symbol <symbol>", "Symbol").option("--hours <n>", "Hours to look back", "24").option("--exchange <ex>", "Filter exchange")
640
- .action(async (opts) => {
641
- const symbol = opts.symbol.toUpperCase();
642
- const hours = parseInt(opts.hours);
643
- const exchanges = opts.exchange ? [opts.exchange.toLowerCase()] : ["hyperliquid", "pacifica", "lighter"];
644
- const endTime = new Date();
645
- const startTime = new Date(endTime.getTime() - hours * 3600000);
646
- if (isJson()) {
647
- const data = {};
648
- for (const ex of exchanges) {
649
- const rates = getHistoricalRates(symbol, ex, startTime, endTime);
650
- if (rates.length > 0)
651
- data[ex] = rates;
652
- }
653
- return printJson(jsonOk({ symbol, hours, startTime: startTime.toISOString(), endTime: endTime.toISOString(), rates: data }));
654
- }
655
- console.log(chalk.cyan.bold(` ${symbol} Funding Rate History (last ${hours}h)\n`));
656
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
657
- let hasData = false;
364
+ return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
365
+ console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
366
+ return;
367
+ }
368
+ if (isJson())
369
+ return printJson(jsonOk(comparison));
370
+ printDetailedComparison(comparison);
371
+ }
372
+ export async function handleFundingHistory(isJson, opts) {
373
+ const symbol = opts.symbol.toUpperCase();
374
+ const hours = parseInt(opts.hours);
375
+ const exchanges = opts.exchange ? [opts.exchange.toLowerCase()] : ["hyperliquid", "pacifica", "lighter"];
376
+ const endTime = new Date();
377
+ const startTime = new Date(endTime.getTime() - hours * 3600000);
378
+ if (isJson()) {
379
+ const data = {};
658
380
  for (const ex of exchanges) {
659
381
  const rates = getHistoricalRates(symbol, ex, startTime, endTime);
660
- if (rates.length === 0)
661
- continue;
662
- hasData = true;
663
- console.log(chalk.white.bold(` ${exAbbr(ex)} (${rates.length} snapshots):`));
664
- const rows = rates.map(r => { const color = r.rate > 0 ? chalk.red : r.rate < 0 ? chalk.green : chalk.white; return [chalk.gray(new Date(r.ts).toLocaleString()), color(formatPercent(r.rate)), `${(r.hourlyRate * 100).toFixed(6)}%/h`, `${annualizeHourlyRate(r.hourlyRate).toFixed(2)}%/yr`]; });
665
- console.log(makeTable(["Time", "Raw Rate", "Hourly", "Annualized"], rows));
666
- console.log();
382
+ if (rates.length > 0)
383
+ data[ex] = rates;
667
384
  }
668
- if (!hasData)
669
- console.log(chalk.gray(` No historical data. Run 'perp arb rates' to start collecting.\n`));
670
- });
671
- // funding-monitor removed — use 'perp arb monitor' (more features, includes liquidity data)
672
- parent.command("funding-positions").description("Show funding rate impact on your current positions")
673
- .option("--exchanges <list>", "Comma-separated exchanges")
674
- .action(async (opts) => {
675
- if (!getAdapterForExchange) {
676
- if (isJson())
677
- return printJson(jsonError("INTERNAL", "requires adapter"));
678
- console.log(chalk.red(" requires adapter access\n"));
679
- return;
680
- }
681
- await withJsonErrors(isJson(), async () => {
682
- const targetExchanges = opts.exchanges ? opts.exchanges.split(",").map(s => s.trim().toLowerCase()) : [...ALL_EXCHANGES];
683
- if (!isJson())
684
- console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
685
- const results = [];
686
- const exchangeErrors = {};
687
- await Promise.all(targetExchanges.map(async (exchange) => {
688
- try {
689
- const adapter = await getAdapterForExchange(exchange);
690
- const [positions, markets, fp] = await Promise.all([adapter.getPositions(), adapter.getMarkets(), adapter.getFundingPayments(100).catch(() => [])]);
691
- if (positions.length === 0)
692
- return;
693
- const fm = new Map();
694
- for (const m of markets) {
695
- const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
696
- fm.set(sym, { rate: Number(m.fundingRate) || 0, markPrice: Number(m.markPrice) || 0 });
697
- }
698
- const dayAgo = Date.now() - 86400000;
699
- const af = new Map();
700
- for (const f of fp) {
701
- if (f.time < dayAgo)
702
- continue;
703
- const sym = f.symbol.toUpperCase().replace(/-PERP$/, "");
704
- const amt = Number(f.payment) || 0;
705
- if (!af.has(sym))
706
- af.set(sym, { received: 0, paid: 0 });
707
- const e = af.get(sym);
708
- if (amt > 0)
709
- e.received += amt;
710
- else
711
- e.paid += Math.abs(amt);
712
- }
713
- for (const pos of positions) {
714
- const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
715
- const fd = fm.get(sym);
716
- const fr = fd?.rate ?? 0;
717
- const mk = Number(pos.markPrice) || fd?.markPrice || 0;
718
- const sz = Math.abs(Number(pos.size));
719
- const nu = sz * mk;
720
- const hr = toHourlyRate(fr, exchange);
721
- const ap = annualizeRate(fr, exchange);
722
- const hp = estimateHourlyFunding(fr, exchange, nu, pos.side);
723
- const a = af.get(sym);
724
- results.push({ exchange, symbol: sym, side: pos.side, size: pos.size, entryPrice: pos.entryPrice, markPrice: pos.markPrice, unrealizedPnl: pos.unrealizedPnl, leverage: pos.leverage, notionalUsd: nu, fundingRate: fr, hourlyRate: hr, annualPct: ap, hourlyPayment: hp, dailyPayment: hp * 24, actualReceived24h: a?.received ?? 0, actualPaid24h: a?.paid ?? 0, actualNet24h: (a?.received ?? 0) - (a?.paid ?? 0) });
725
- }
385
+ return printJson(jsonOk({ symbol, hours, startTime: startTime.toISOString(), endTime: endTime.toISOString(), rates: data }));
386
+ }
387
+ console.log(chalk.cyan.bold(` ${symbol} Funding Rate History (last ${hours}h)\n`));
388
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
389
+ let hasData = false;
390
+ for (const ex of exchanges) {
391
+ const rates = getHistoricalRates(symbol, ex, startTime, endTime);
392
+ if (rates.length === 0)
393
+ continue;
394
+ hasData = true;
395
+ console.log(chalk.white.bold(` ${exAbbr(ex)} (${rates.length} snapshots):`));
396
+ const rows = rates.map(r => { const color = r.rate > 0 ? chalk.red : r.rate < 0 ? chalk.green : chalk.white; return [chalk.gray(new Date(r.ts).toLocaleString()), color(formatPercent(r.rate)), `${(r.hourlyRate * 100).toFixed(6)}%/h`, `${annualizeHourlyRate(r.hourlyRate).toFixed(2)}%/yr`]; });
397
+ console.log(makeTable(["Time", "Raw Rate", "Hourly", "Annualized"], rows));
398
+ console.log();
399
+ }
400
+ if (!hasData)
401
+ console.log(chalk.gray(` No historical data. Run 'perp arb rates' to start collecting.\n`));
402
+ }
403
+ export async function handleFundingPositions(isJson, getAdapterForExchange, opts) {
404
+ await withJsonErrors(isJson(), async () => {
405
+ const targetExchanges = opts.exchanges ? opts.exchanges.split(",").map(s => s.trim().toLowerCase()) : [...ALL_EXCHANGES];
406
+ if (!isJson())
407
+ console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
408
+ const results = [];
409
+ const exchangeErrors = {};
410
+ await Promise.all(targetExchanges.map(async (exchange) => {
411
+ try {
412
+ const adapter = await getAdapterForExchange(exchange);
413
+ const [positions, markets, fp] = await Promise.all([adapter.getPositions(), adapter.getMarkets(), adapter.getFundingPayments(100).catch(() => [])]);
414
+ if (positions.length === 0)
415
+ return;
416
+ const fm = new Map();
417
+ for (const m of markets) {
418
+ const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
419
+ fm.set(sym, { rate: Number(m.fundingRate) || 0, markPrice: Number(m.markPrice) || 0 });
420
+ }
421
+ const dayAgo = Date.now() - 86400000;
422
+ const af = new Map();
423
+ for (const f of fp) {
424
+ if (f.time < dayAgo)
425
+ continue;
426
+ const sym = f.symbol.toUpperCase().replace(/-PERP$/, "");
427
+ const amt = Number(f.payment) || 0;
428
+ if (!af.has(sym))
429
+ af.set(sym, { received: 0, paid: 0 });
430
+ const e = af.get(sym);
431
+ if (amt > 0)
432
+ e.received += amt;
433
+ else
434
+ e.paid += Math.abs(amt);
726
435
  }
727
- catch (err) {
728
- exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
436
+ for (const pos of positions) {
437
+ const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
438
+ const fd = fm.get(sym);
439
+ const fr = fd?.rate ?? 0;
440
+ const mk = Number(pos.markPrice) || fd?.markPrice || 0;
441
+ const sz = Math.abs(Number(pos.size));
442
+ const nu = sz * mk;
443
+ const hr = toHourlyRate(fr, exchange);
444
+ const ap = annualizeRate(fr, exchange);
445
+ const hp = estimateHourlyFunding(fr, exchange, nu, pos.side);
446
+ const a = af.get(sym);
447
+ results.push({ exchange, symbol: sym, side: pos.side, size: pos.size, entryPrice: pos.entryPrice, markPrice: pos.markPrice, unrealizedPnl: pos.unrealizedPnl, leverage: pos.leverage, notionalUsd: nu, fundingRate: fr, hourlyRate: hr, annualPct: ap, hourlyPayment: hp, dailyPayment: hp * 24, actualReceived24h: a?.received ?? 0, actualPaid24h: a?.paid ?? 0, actualNet24h: (a?.received ?? 0) - (a?.paid ?? 0) });
729
448
  }
730
- }));
731
- if (results.length === 0) {
732
- if (isJson())
733
- return printJson(jsonOk({ positions: [], totals: { predicted: { hourly: 0, daily: 0 }, actual24h: { net: 0 }, notionalUsd: 0 }, errors: exchangeErrors }));
734
- console.log(chalk.gray(" No open positions.\n"));
735
- return;
736
449
  }
737
- results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
738
- const tH = results.reduce((s, r) => s + r.hourlyPayment, 0);
739
- const tD = results.reduce((s, r) => s + r.dailyPayment, 0);
740
- const tN = results.reduce((s, r) => s + r.notionalUsd, 0);
741
- const tA = results.reduce((s, r) => s + r.actualNet24h, 0);
450
+ catch (err) {
451
+ exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
452
+ }
453
+ }));
454
+ if (results.length === 0) {
742
455
  if (isJson())
743
- return printJson(jsonOk({ positions: results.map(r => ({ exchange: r.exchange, symbol: r.symbol, side: r.side, size: r.size, notionalUsd: +r.notionalUsd.toFixed(2), fundingRate: r.fundingRate, annualPct: +r.annualPct.toFixed(2), predicted: { hourly: +r.hourlyPayment.toFixed(6), daily: +r.dailyPayment.toFixed(4) }, actual24h: { received: +r.actualReceived24h.toFixed(6), paid: +r.actualPaid24h.toFixed(6), net: +r.actualNet24h.toFixed(6) }, unrealizedPnl: r.unrealizedPnl })), totals: { predicted: { hourly: +tH.toFixed(6), daily: +tD.toFixed(4) }, actual24h: { net: +tA.toFixed(6) }, notionalUsd: +tN.toFixed(2) }, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
744
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
745
- console.log(chalk.cyan.bold(" Position Funding Impact\n"));
746
- const rows = results.map(r => { const sc = r.side === "long" ? chalk.green : chalk.red; const rc = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white; const as = r.actualNet24h !== 0 ? (r.actualNet24h > 0 ? chalk.green : chalk.red)(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`) : chalk.gray("-"); return [chalk.white.bold(exAbbr(r.exchange)), chalk.white.bold(r.symbol), sc(r.side.toUpperCase()), `$${formatUsd(r.notionalUsd)}`, rc(formatPercent(r.fundingRate)), rc(`${r.annualPct.toFixed(1)}%`), as, formatPnl(r.unrealizedPnl)]; });
747
- console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
748
- console.log(`\n Total: $${formatUsd(tN)} | Actual 24h: ${tA >= 0 ? "+" : ""}$${tA.toFixed(4)} | Predicted: ${tD >= 0 ? "-" : "+"}$${Math.abs(tD).toFixed(4)}/d\n`);
749
- });
456
+ return printJson(jsonOk({ positions: [], totals: { predicted: { hourly: 0, daily: 0 }, actual24h: { net: 0 }, notionalUsd: 0 }, errors: exchangeErrors }));
457
+ console.log(chalk.gray(" No open positions.\n"));
458
+ return;
459
+ }
460
+ results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
461
+ const tH = results.reduce((s, r) => s + r.hourlyPayment, 0);
462
+ const tD = results.reduce((s, r) => s + r.dailyPayment, 0);
463
+ const tN = results.reduce((s, r) => s + r.notionalUsd, 0);
464
+ const tA = results.reduce((s, r) => s + r.actualNet24h, 0);
465
+ if (isJson())
466
+ return printJson(jsonOk({ positions: results.map(r => ({ exchange: r.exchange, symbol: r.symbol, side: r.side, size: r.size, notionalUsd: +r.notionalUsd.toFixed(2), fundingRate: r.fundingRate, annualPct: +r.annualPct.toFixed(2), predicted: { hourly: +r.hourlyPayment.toFixed(6), daily: +r.dailyPayment.toFixed(4) }, actual24h: { received: +r.actualReceived24h.toFixed(6), paid: +r.actualPaid24h.toFixed(6), net: +r.actualNet24h.toFixed(6) }, unrealizedPnl: r.unrealizedPnl })), totals: { predicted: { hourly: +tH.toFixed(6), daily: +tD.toFixed(4) }, actual24h: { net: +tA.toFixed(6) }, notionalUsd: +tN.toFixed(2) }, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
467
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
468
+ console.log(chalk.cyan.bold(" Position Funding Impact\n"));
469
+ const rows = results.map(r => { const sc = r.side === "long" ? chalk.green : chalk.red; const rc = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white; const as = r.actualNet24h !== 0 ? (r.actualNet24h > 0 ? chalk.green : chalk.red)(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`) : chalk.gray("-"); return [chalk.white.bold(exAbbr(r.exchange)), chalk.white.bold(r.symbol), sc(r.side.toUpperCase()), `$${formatUsd(r.notionalUsd)}`, rc(formatPercent(r.fundingRate)), rc(`${r.annualPct.toFixed(1)}%`), as, formatPnl(r.unrealizedPnl)]; });
470
+ console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
471
+ console.log(`\n Total: $${formatUsd(tN)} | Actual 24h: ${tA >= 0 ? "+" : ""}$${tA.toFixed(4)} | Predicted: ${tD >= 0 ? "-" : "+"}$${Math.abs(tD).toFixed(4)}/d\n`);
750
472
  });
751
473
  }